diff --git a/package.json b/package.json index e16fed0805..2380a48a38 100644 --- a/package.json +++ b/package.json @@ -636,6 +636,11 @@ { "type": "docker", "label": "Docker: Debug in Container", + "languages": [ + "csharp", + "razor", + "aspnetcorerazor" + ], "configurationAttributes": { "launch": { "properties": { @@ -1500,6 +1505,9 @@ "required": [ "dockerCompose" ] + }, + { + "type": "dotnet-container-sdk" } ], "languages": [ diff --git a/resources/netCore/GetProjectProperties.targets b/resources/netCore/GetProjectProperties.targets index b2506173b4..929dd3ae4e 100644 --- a/resources/netCore/GetProjectProperties.targets +++ b/resources/netCore/GetProjectProperties.targets @@ -1,11 +1,16 @@ - - + + $(GetProjectPropertiesDependsOn);ComputeContainerConfig; + + + diff --git a/src/debugging/DebugHelper.ts b/src/debugging/DebugHelper.ts index 024cf5cde6..df544874a4 100644 --- a/src/debugging/DebugHelper.ts +++ b/src/debugging/DebugHelper.ts @@ -12,6 +12,7 @@ import { DockerDebugConfiguration, DockerDebugConfigurationProvider } from './Do import { DockerPlatform } from './DockerPlatformHelper'; import { registerServerReadyAction } from './DockerServerReadyAction'; import { netCoreDebugHelper } from './netcore/NetCoreDebugHelper'; +import { netSdkDebugHelper } from './netSdk/NetSdkDebugHelper'; import { nodeDebugHelper } from './node/NodeDebugHelper'; import { pythonDebugHelper } from './python/PythonDebugHelper'; @@ -51,6 +52,7 @@ export function registerDebugProvider(ctx: ExtensionContext): void { netCore: netCoreDebugHelper, node: nodeDebugHelper, python: pythonDebugHelper, + netSdk: netSdkDebugHelper } ) ) diff --git a/src/debugging/DockerDebugConfigurationProvider.ts b/src/debugging/DockerDebugConfigurationProvider.ts index 83e8a7f531..0e4a56e276 100644 --- a/src/debugging/DockerDebugConfigurationProvider.ts +++ b/src/debugging/DockerDebugConfigurationProvider.ts @@ -3,14 +3,16 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { callWithTelemetryAndErrorHandling, IActionContext, registerEvent } from '@microsoft/vscode-azext-utils'; -import { CancellationToken, commands, debug, DebugConfiguration, DebugConfigurationProvider, DebugSession, l10n, MessageItem, ProviderResult, window, workspace, WorkspaceFolder } from 'vscode'; -import { DockerOrchestration } from '../constants'; +import { callWithTelemetryAndErrorHandling, IActionContext, registerEvent, UserCancelledError } from '@microsoft/vscode-azext-utils'; +import { CancellationToken, commands, debug, DebugConfiguration, DebugConfigurationProvider, DebugSession, l10n, ProviderResult, workspace, WorkspaceFolder } from 'vscode'; +import { CSPROJ_GLOB_PATTERN, DockerOrchestration } from '../constants'; import { ext } from '../extensionVariables'; import { getAssociatedDockerRunTask } from '../tasks/TaskHelper'; +import { resolveFilesOfPattern } from '../utils/quickPickFile'; import { DebugHelper, DockerDebugContext, ResolvedDebugConfiguration } from './DebugHelper'; import { DockerPlatform, getPlatform } from './DockerPlatformHelper'; import { NetCoreDockerDebugConfiguration } from './netcore/NetCoreDebugHelper'; +import { netSdkDebugHelper } from './netSdk/NetSdkDebugHelper'; import { NodeDockerDebugConfiguration } from './node/NodeDebugHelper'; export interface DockerDebugConfiguration extends NetCoreDockerDebugConfiguration, NodeDockerDebugConfiguration { @@ -42,21 +44,14 @@ export class DockerDebugConfigurationProvider implements DebugConfigurationProvi } public provideDebugConfigurations(folder: WorkspaceFolder | undefined, token?: CancellationToken): ProviderResult { - const add: MessageItem = { title: l10n.t('Add Docker Files') }; - - // Prompt them to add Docker files since they probably haven't - /* eslint-disable-next-line @typescript-eslint/no-floating-promises */ - window.showErrorMessage( - l10n.t('To debug in a Docker container on supported platforms, use the command "Docker: Add Docker Files to Workspace", or click "Add Docker Files".'), - ...[add]) - .then((result) => { - if (result === add) { - /* eslint-disable-next-line @typescript-eslint/no-floating-promises */ - commands.executeCommand('vscode-docker.configure'); - } - }); - return []; + return callWithTelemetryAndErrorHandling( + 'provideDebugConfigurations', + async (actionContext: IActionContext) => { + return this.handleEmptyDebugConfig(folder, actionContext); + } + ); + } public resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: DockerDebugConfiguration, token?: CancellationToken): ProviderResult { @@ -75,10 +70,15 @@ export class DockerDebugConfigurationProvider implements DebugConfigurationProvi } } - if (debugConfiguration.type === undefined) { - // If type is undefined, they may be doing F5 without creating any real launch.json, which won't work - // VSCode subsequently will call provideDebugConfigurations which will show an error message - return null; + if (Object.keys(debugConfiguration).length === 0) { + + const newlyCreatedDebugConfig = await this.handleEmptyDebugConfig(folder, actionContext); + // if there is no debugConfiguration, we should return undefined to exit the debug session + if (newlyCreatedDebugConfig.length === 0 || !newlyCreatedDebugConfig[0]) { + return undefined; + } + + debugConfiguration = newlyCreatedDebugConfig[0]; } if (!debugConfiguration.request) { @@ -170,4 +170,35 @@ export class DockerDebugConfigurationProvider implements DebugConfigurationProvi } } } + + /** + * If the user has an empty debug launch.json, then we will: + * 1. check if it's a .NET Core project, if so, we will provide .NET Core debug configurations + * 2. otherwise, we will scaffold docker files + */ + private async handleEmptyDebugConfig(folder: WorkspaceFolder, actionContext: IActionContext): Promise { + + // NOTE: We can not determine the language from `DockerDebugContext`, so we need to check the + // type of files inside the folder here to determine the language. + + // check if it's a .NET Core project + const csProjUris = await resolveFilesOfPattern(folder, [CSPROJ_GLOB_PATTERN]); + if (csProjUris) { + return await netSdkDebugHelper.provideDebugConfigurations( + { + actionContext, + dockerfile: undefined, + folder: folder + }, + { + appProject: csProjUris[0]?.absoluteFilePath || '', + } + ); + } else { + // for now, we scaffold docker files + await commands.executeCommand('vscode-docker.configure'); + throw new UserCancelledError(); + } + // TODO: (potentially) in the future, we can add more support for ambient tasks for other types of projects + } } diff --git a/src/debugging/DockerPlatformHelper.ts b/src/debugging/DockerPlatformHelper.ts index b204d2914b..17c8db27c0 100644 --- a/src/debugging/DockerPlatformHelper.ts +++ b/src/debugging/DockerPlatformHelper.ts @@ -3,17 +3,22 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -export type DockerPlatform = 'netCore' | 'node' | 'python'; +import { netSdkDebugHelper } from "./netSdk/NetSdkDebugHelper"; + +export type DockerPlatform = 'netCore' | 'node' | 'python' | 'netSdk'; interface DockerPlatformConfiguration { platform?: DockerPlatform; netCore?: unknown; node?: unknown; python?: unknown; + preLaunchTask?: string; } export function getPlatform(configuration: T): DockerPlatform | undefined { - if (configuration.platform === 'netCore' || configuration.netCore !== undefined) { + if (netSdkDebugHelper.isDotNetSdkBuild(configuration?.preLaunchTask) && configuration.netCore !== undefined) { + return 'netSdk'; + } else if (configuration.platform === 'netCore' || configuration.netCore !== undefined) { return 'netCore'; } else if (configuration.platform === 'node' || configuration.node !== undefined) { return 'node'; diff --git a/src/debugging/netSdk/NetSdkDebugHelper.ts b/src/debugging/netSdk/NetSdkDebugHelper.ts new file mode 100644 index 0000000000..3aae79ed9b --- /dev/null +++ b/src/debugging/netSdk/NetSdkDebugHelper.ts @@ -0,0 +1,52 @@ +import { commands } from "vscode"; +import { NetChooseBuildTypeContext, netContainerBuild } from "../../scaffolding/wizard/net/NetContainerBuild"; +import { AllNetContainerBuildOptions } from "../../scaffolding/wizard/net/NetSdkChooseBuildStep"; +import { unresolveWorkspaceFolder } from "../../utils/resolveVariables"; +import { DockerDebugScaffoldContext } from "../DebugHelper"; +import { DockerDebugConfiguration } from "../DockerDebugConfigurationProvider"; +import { NetCoreDebugHelper, NetCoreDebugScaffoldingOptions } from "../netcore/NetCoreDebugHelper"; + +const NetSdkTaskFullSymbol = 'dotnet-container-sdk: debug'; +export class NetSdkDebugHelper extends NetCoreDebugHelper { + + public async provideDebugConfigurations(context: DockerDebugScaffoldContext, options?: NetCoreDebugScaffoldingOptions): Promise { + + const configurations: DockerDebugConfiguration[] = []; + + const netCoreBuildContext: NetChooseBuildTypeContext = { + ...context.actionContext, + scaffoldType: 'debugging', + workspaceFolder: context.folder, + }; + + await netContainerBuild(netCoreBuildContext); + + if (netCoreBuildContext?.containerBuildOptions === AllNetContainerBuildOptions[1]) { + configurations.push({ + name: 'Docker .NET Container SDK Launch', + type: 'docker', + request: 'launch', + preLaunchTask: NetSdkTaskFullSymbol, + netCore: { + appProject: unresolveWorkspaceFolder(options.appProject, context.folder), + }, + }); + } else { + await commands.executeCommand('vscode-docker.configure'); + } + + return configurations; + } + + /** + * Checks if the launch task is using the .NET SDK Container build + * @param preLaunchTask + * @returns true if the launch task is using the .NET SDK Container build + * false otherwise + */ + public isDotNetSdkBuild(preLaunchTask: string): boolean { + return preLaunchTask === NetSdkTaskFullSymbol; + } +} + +export const netSdkDebugHelper = new NetSdkDebugHelper(); diff --git a/src/debugging/netcore/NetCoreDebugHelper.ts b/src/debugging/netcore/NetCoreDebugHelper.ts index 439980d601..f2828c3256 100644 --- a/src/debugging/netcore/NetCoreDebugHelper.ts +++ b/src/debugging/netcore/NetCoreDebugHelper.ts @@ -9,6 +9,7 @@ import * as path from 'path'; import { DebugConfiguration, MessageItem, ProgressLocation, l10n, window } from 'vscode'; import { ext } from '../../extensionVariables'; import { CommandLineArgs, ContainerOS, VoidCommandResponse, composeArgs, withArg, withQuotedArg } from '../../runtimes/docker'; +import { NetContainerBuildOptionsKey } from '../../scaffolding/wizard/net/NetSdkChooseBuildStep'; import { NetCoreTaskHelper, NetCoreTaskOptions } from '../../tasks/netcore/NetCoreTaskHelper'; import { ContainerTreeItem } from '../../tree/containers/ContainerTreeItem'; import { getNetCoreProjectInfo } from '../../utils/netCoreUtils'; @@ -18,6 +19,7 @@ import { PlatformOS } from '../../utils/platform'; import { unresolveWorkspaceFolder } from '../../utils/resolveVariables'; import { DebugHelper, DockerDebugContext, DockerDebugScaffoldContext, ResolvedDebugConfiguration, inferContainerName, resolveDockerServerReadyAction } from '../DebugHelper'; import { DockerAttachConfiguration, DockerDebugConfiguration } from '../DockerDebugConfigurationProvider'; +import { netSdkDebugHelper } from '../netSdk/NetSdkDebugHelper'; import { exportCertificateIfNecessary, getHostSecretsFolders, trustCertificateIfNecessary } from './AspNetSslHelper'; import { VsDbgType, installDebuggersIfNecessary, vsDbgInstallBasePath } from './VsDbgHelper'; @@ -68,7 +70,7 @@ export class NetCoreDebugHelper implements DebugHelper { debugConfiguration.netCore.appProject = await NetCoreTaskHelper.inferAppProject(context, debugConfiguration.netCore); // This method internally checks the user-defined input first const { configureSsl, containerName, platformOS } = await this.loadExternalInfo(context, debugConfiguration); - const appOutput = await this.inferAppOutput(debugConfiguration.netCore); + const appOutput = debugConfiguration.netCore?.appOutput || await this.inferAppOutput(debugConfiguration); if (context.cancellationToken && context.cancellationToken.isCancellationRequested) { // inferAppOutput is slow, give a chance to cancel return undefined; @@ -90,7 +92,9 @@ export class NetCoreDebugHelper implements DebugHelper { const additionalProbingPathsArgs = NetCoreDebugHelper.getAdditionalProbingPathsArgs(platformOS); - const containerAppOutput = NetCoreDebugHelper.getContainerAppOutput(debugConfiguration, appOutput, platformOS); + const containerAppOutput = netSdkDebugHelper.isDotNetSdkBuild(debugConfiguration?.preLaunchTask) + ? appOutput + : NetCoreDebugHelper.getContainerAppOutput(debugConfiguration, appOutput, platformOS); const dockerServerReadyAction = resolveDockerServerReadyAction( debugConfiguration, @@ -176,12 +180,22 @@ export class NetCoreDebugHelper implements DebugHelper { }; } - private async inferAppOutput(helperOptions: NetCoreDebugOptions): Promise { - const projectInfo = await getNetCoreProjectInfo('GetProjectProperties', helperOptions.appProject); + private async inferAppOutput(debugConfiguration: DockerDebugConfiguration): Promise { + const projectInfo = await getNetCoreProjectInfo('GetProjectProperties', debugConfiguration.netCore?.appProject); + if (projectInfo.length < 3) { throw new Error(l10n.t('Unable to determine assembly output path.')); } + if (netSdkDebugHelper.isDotNetSdkBuild(debugConfiguration.preLaunchTask) && projectInfo.length >= 5) { // if .NET has support for SDK Build + if (projectInfo[4] === 'true') { // fifth is whether .NET supports SDK Containers + return projectInfo[3]; // fourth is output path + } else { + await ext.context.workspaceState.update(NetContainerBuildOptionsKey, ''); // clear the workspace state + throw new Error(l10n.t('Your current version of .NET SDK does not support SDK Container build. Please update to a later version of .NET SDK to use this feature.')); + } + } + return projectInfo[2]; // First line is assembly name, second is target framework, third+ are output path(s) } diff --git a/src/runtimes/docker/clients/DockerClientBase/DockerClientBase.ts b/src/runtimes/docker/clients/DockerClientBase/DockerClientBase.ts index 1c3cfd7c7f..65379de30e 100644 --- a/src/runtimes/docker/clients/DockerClientBase/DockerClientBase.ts +++ b/src/runtimes/docker/clients/DockerClientBase/DockerClientBase.ts @@ -106,6 +106,7 @@ import { withContainerPathArg } from './withContainerPathArg'; import { withDockerAddHostArg } from './withDockerAddHostArg'; import { withDockerBuildArg } from './withDockerBuildArg'; import { withDockerEnvArg } from './withDockerEnvArg'; +import { withDockerExposePortsArg } from './withDockerExposePortsArg'; import { withDockerBooleanFilterArg, withDockerFilterArg } from './withDockerFilterArg'; import { withDockerIgnoreSizeArg } from './withDockerIgnoreSizeArg'; import { withDockerJsonFormatArg } from "./withDockerJsonFormatArg"; @@ -683,6 +684,7 @@ export abstract class DockerClientBase extends ConfigurableClient implements ICo withDockerEnvArg(options.environmentVariables), withNamedArg('--env-file', options.environmentFiles), withNamedArg('--entrypoint', options.entrypoint), + withDockerExposePortsArg(options.exposePorts), withVerbatimArg(options.customOptions), withArg(options.imageRef), typeof options.command === 'string' ? withVerbatimArg(options.command) : withArg(...(toArray(options.command || []))), diff --git a/src/runtimes/docker/clients/DockerClientBase/withDockerExposePortsArg.ts b/src/runtimes/docker/clients/DockerClientBase/withDockerExposePortsArg.ts new file mode 100644 index 0000000000..31d0e43f73 --- /dev/null +++ b/src/runtimes/docker/clients/DockerClientBase/withDockerExposePortsArg.ts @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { withNamedArg } from "../../utils/commandLineBuilder"; + +export function withDockerExposePortsArg(ports?: Array) { + return withNamedArg('--expose', (ports || []).map(port => port.toString()), { shouldQuote: false }); +} diff --git a/src/runtimes/docker/contracts/ContainerClient.ts b/src/runtimes/docker/contracts/ContainerClient.ts index 6ebc507c15..417dbd4443 100644 --- a/src/runtimes/docker/contracts/ContainerClient.ts +++ b/src/runtimes/docker/contracts/ContainerClient.ts @@ -744,6 +744,10 @@ export type RunContainerCommandOptions = CommonCommandOptions & { * Optional command to use in starting the container */ command?: Array | string; + /** + * Optional expose ports for the container + */ + exposePorts?: Array; /** * Additional custom options to pass */ diff --git a/src/scaffolding/wizard/net/NetContainerBuild.ts b/src/scaffolding/wizard/net/NetContainerBuild.ts new file mode 100644 index 0000000000..3187d98e9e --- /dev/null +++ b/src/scaffolding/wizard/net/NetContainerBuild.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizard, AzureWizardPromptStep, UserCancelledError } from '@microsoft/vscode-azext-utils'; +import * as vscode from 'vscode'; +import { ScaffoldingWizardContext } from '../ScaffoldingWizardContext'; +import { NetContainerBuildOptions, NetSdkChooseBuildStep } from './NetSdkChooseBuildStep'; + +export interface NetChooseBuildTypeContext extends ScaffoldingWizardContext { + containerBuildOptions?: NetContainerBuildOptions; +} + +export async function netContainerBuild(wizardContext: Partial, apiInput?: NetChooseBuildTypeContext): Promise { + if (!vscode.workspace.isTrusted) { + throw new UserCancelledError('enforceTrust'); + } + + const promptSteps: AzureWizardPromptStep[] = [ + new NetSdkChooseBuildStep() + ]; + + const wizard = new AzureWizard(wizardContext as NetChooseBuildTypeContext, { + promptSteps: promptSteps, + title: vscode.l10n.t('Initialize for Debugging'), + }); + + await wizard.prompt(); + await wizard.execute(); +} diff --git a/src/scaffolding/wizard/net/NetSdkChooseBuildStep.ts b/src/scaffolding/wizard/net/NetSdkChooseBuildStep.ts new file mode 100644 index 0000000000..9d86ff743d --- /dev/null +++ b/src/scaffolding/wizard/net/NetSdkChooseBuildStep.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IAzureQuickPickItem } from '@microsoft/vscode-azext-utils'; +import * as vscode from 'vscode'; +import { ext } from '../../../extensionVariables'; +import { TelemetryPromptStep } from '../TelemetryPromptStep'; +import { NetChooseBuildTypeContext } from './NetContainerBuild'; + +/** Key to .NET Container Build Options workplace momento storage */ +export const NetContainerBuildOptionsKey = 'netContainerBuildOptions'; + +export const AllNetContainerBuildOptions = [ + vscode.l10n.t('Use a Dockerfile'), + vscode.l10n.t('Use .NET SDK') +] as const; + +type NetContainerBuildOptionsTuple = typeof AllNetContainerBuildOptions; +export type NetContainerBuildOptions = NetContainerBuildOptionsTuple[number]; + +export class NetSdkChooseBuildStep extends TelemetryPromptStep { + public async prompt(wizardContext: NetChooseBuildTypeContext): Promise { + + // get workspace momento storage + const containerBuildOptions = await ext.context.workspaceState.get(NetContainerBuildOptionsKey); + + // only remember if it was 'Use .NET SDK', otherwise prompt again + if (containerBuildOptions === AllNetContainerBuildOptions[1]) { + wizardContext.containerBuildOptions = containerBuildOptions; + return; + } + + const opt: vscode.QuickPickOptions = { + matchOnDescription: true, + matchOnDetail: true, + placeHolder: vscode.l10n.t('How would you like to build your container image?'), + }; + + const buildOptions = AllNetContainerBuildOptions as readonly NetContainerBuildOptions[]; + const items = buildOptions.map(p => >{ label: p, data: p }); + + const response = await wizardContext.ui.showQuickPick(items, opt); + wizardContext.containerBuildOptions = response.data; + + // update workspace momento storage + await ext.context.workspaceState.update(NetContainerBuildOptionsKey, wizardContext.containerBuildOptions); + } + + public shouldPrompt(wizardContext: NetChooseBuildTypeContext): boolean { + return !wizardContext.containerBuildOptions; + } + + protected setTelemetry(wizardContext: NetChooseBuildTypeContext): void { + wizardContext.telemetry.properties.netSdkBuildStep = wizardContext.containerBuildOptions; + } +} diff --git a/src/tasks/DockerPseudoterminal.ts b/src/tasks/DockerPseudoterminal.ts index 1b4ea47096..31fc59658c 100644 --- a/src/tasks/DockerPseudoterminal.ts +++ b/src/tasks/DockerPseudoterminal.ts @@ -52,9 +52,9 @@ export class DockerPseudoterminal implements Pseudoterminal { this.closeEmitter.fire(code || 0); } - public getCommandRunner(options: Omit): (commandResponse: VoidCommandResponse | PromiseCommandResponse) => Promise { + public getCommandRunner(options: Omit): (commandResponse: VoidCommandResponse | PromiseCommandResponse) => Promise { return async (commandResponse: VoidCommandResponse | PromiseCommandResponse) => { - const output = await this.executeCommandInTerminal({ + const output = await this.executeCommandResponseInTerminal({ ...options, commandResponse: commandResponse, }); @@ -96,16 +96,21 @@ export class DockerPseudoterminal implements Pseudoterminal { this.writeEmitter.fire(`\x1b[${color}${message}\x1b[0m`); } - private async executeCommandInTerminal(options: ExecuteCommandInTerminalOptions): Promise { + private async executeCommandResponseInTerminal(options: ExecuteCommandResponseInTerminalOptions): Promise { const quotedArgs = Shell.getShellOrDefault().quote(options.commandResponse.args); const resolvedQuotedArgs = resolveVariables(quotedArgs, options.folder); const commandLine = [options.commandResponse.command, ...resolvedQuotedArgs].join(' '); + return await this.execAsyncInTerminal(commandLine, options); + } + + public async execAsyncInTerminal(command: string, options?: ExecAsyncInTerminalOptions): Promise { + // Output what we're doing, same style as VSCode does for ShellExecution/ProcessExecution - this.write(`> ${commandLine} <\r\n\r\n`, DEFAULTBOLD); + this.write(`> ${command} <\r\n\r\n`, DEFAULTBOLD); return await execAsync( - commandLine, + command, { cwd: this.resolvedDefinition.options?.cwd || options.folder.uri.fsPath, env: withDockerEnvSettings({ ...process.env, ...this.resolvedDefinition.options?.env }), @@ -120,10 +125,14 @@ export class DockerPseudoterminal implements Pseudoterminal { } ); } + } -type ExecuteCommandInTerminalOptions = { +type ExecuteCommandResponseInTerminalOptions = ExecAsyncInTerminalOptions & { commandResponse: VoidCommandResponse | PromiseCommandResponse; +}; + +type ExecAsyncInTerminalOptions = { folder: WorkspaceFolder; token?: CancellationToken; }; diff --git a/src/tasks/TaskHelper.ts b/src/tasks/TaskHelper.ts index 571ccdaf27..3d0c93dc8e 100644 --- a/src/tasks/TaskHelper.ts +++ b/src/tasks/TaskHelper.ts @@ -20,11 +20,12 @@ import { DockerPseudoterminal } from './DockerPseudoterminal'; import { DockerContainerVolume, DockerRunOptions, DockerRunTaskDefinitionBase } from './DockerRunTaskDefinitionBase'; import { DockerRunTask, DockerRunTaskDefinition, DockerRunTaskProvider } from './DockerRunTaskProvider'; import { TaskDefinitionBase } from './TaskDefinitionBase'; +import { NetSdkRunTaskProvider } from './netSdk/NetSdkRunTaskProvider'; import { netCoreTaskHelper } from './netcore/NetCoreTaskHelper'; import { nodeTaskHelper } from './node/NodeTaskHelper'; import { pythonTaskHelper } from './python/PythonTaskHelper'; -export type DockerTaskProviderName = 'docker-build' | 'docker-run' | 'docker-compose'; +export type DockerTaskProviderName = 'docker-build' | 'docker-run' | 'docker-compose' | 'dotnet-container-sdk'; export interface DockerTaskContext { folder: WorkspaceFolder; @@ -77,7 +78,8 @@ export function registerTaskProviders(ctx: ExtensionContext): void { const helpers = { netCore: netCoreTaskHelper, node: nodeTaskHelper, - python: pythonTaskHelper + python: pythonTaskHelper, + netSdk: undefined }; ctx.subscriptions.push( @@ -100,6 +102,13 @@ export function registerTaskProviders(ctx: ExtensionContext): void { new DockerComposeTaskProvider() ) ); + + ctx.subscriptions.push( + tasks.registerTaskProvider( + 'dotnet-container-sdk', + new NetSdkRunTaskProvider() + ) + ); } export function hasTask(taskLabel: string, folder: WorkspaceFolder): boolean { diff --git a/src/tasks/netSdk/NetSdkRunTaskProvider.ts b/src/tasks/netSdk/NetSdkRunTaskProvider.ts new file mode 100644 index 0000000000..c17ade759c --- /dev/null +++ b/src/tasks/netSdk/NetSdkRunTaskProvider.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken, CustomExecution, Task, TaskDefinition, TaskScope } from "vscode"; +import { DockerPseudoterminal } from "../DockerPseudoterminal"; +import { DockerTaskProvider } from '../DockerTaskProvider'; +import { DockerTaskExecutionContext } from '../TaskHelper'; +import { NetSdkRunTaskType, getNetSdkBuildCommand, getNetSdkRunCommand } from './netSdkTaskUtils'; + +const NetSdkDebugTaskName = 'debug'; +export class NetSdkRunTaskProvider extends DockerTaskProvider { + + public constructor() { super(NetSdkRunTaskType, undefined); } + + public provideTasks(token: CancellationToken): Task[] { + + // we need to initialize a task first so we can pass it into `DockerPseudoterminal` + const task = new Task( + { type: NetSdkRunTaskType }, + TaskScope.Workspace, + NetSdkDebugTaskName, + NetSdkRunTaskType + ); + + task.execution = new CustomExecution(async (resolvedDefinition: TaskDefinition) => + Promise.resolve(new DockerPseudoterminal(this, task, resolvedDefinition)) + ); + + return [task]; + } + + protected async executeTaskInternal(context: DockerTaskExecutionContext, task: Task): Promise { + + // use dotnet to build the image + const buildCommand = await getNetSdkBuildCommand(context); + await context.terminal.execAsyncInTerminal( + buildCommand, + { + folder: context.folder, + token: context.cancellationToken, + } + ); + + // use docker run to run the image + const runCommand = await getNetSdkRunCommand(context); + await context.terminal.execAsyncInTerminal( + runCommand, + { + folder: context.folder, + token: context.cancellationToken, + } + ); + + return Promise.resolve(); + } +} diff --git a/src/tasks/netSdk/netSdkTaskUtils.ts b/src/tasks/netSdk/netSdkTaskUtils.ts new file mode 100644 index 0000000000..d4e062cef2 --- /dev/null +++ b/src/tasks/netSdk/netSdkTaskUtils.ts @@ -0,0 +1,128 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IActionContext } from '@microsoft/vscode-azext-utils'; +import * as os from 'os'; +import { WorkspaceFolder, l10n } from "vscode"; +import { vsDbgInstallBasePath } from "../../debugging/netcore/VsDbgHelper"; +import { ext } from "../../extensionVariables"; +import { RunContainerBindMount, Shell, composeArgs, withArg, withNamedArg } from "../../runtimes/docker"; +import { getValidImageName } from "../../utils/getValidImageName"; +import { getDockerOSType } from "../../utils/osUtils"; +import { quickPickProjectFileItem } from "../../utils/quickPickFile"; +import { quickPickWorkspaceFolder } from "../../utils/quickPickWorkspaceFolder"; +import { defaultVsCodeLabels } from "../TaskDefinitionBase"; +import { DockerTaskExecutionContext, getDefaultContainerName, getDefaultImageName } from "../TaskHelper"; +import { NetCoreTaskHelper } from "../netcore/NetCoreTaskHelper"; + +/** + * Native architecture of the current machine in the RID format + * {@link https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.NETCore.Platforms/src/runtime.json} + */ +export type RidCpuArchitecture = + | 'x64' + | 'x86' + | 'arm64' + | 'arm' + | 'ppc64le' + | 'mips64' + | 's390x' + | string; + +export const NetSdkRunTaskType = 'dotnet-container-sdk'; +const NetSdkDefaultImageTag = 'dev'; // intentionally default to dev tag for phase 1 of this feature +const ErrMsgNoWorkplaceFolder = l10n.t(`Unable to determine task scope to execute task ${NetSdkRunTaskType}. Please open a workspace folder.`); + +export async function getNetSdkBuildCommand(context: DockerTaskExecutionContext): Promise { + + const configuration = 'Debug'; // intentionally default to Debug configuration for phase 1 of this feature + const projPath = await inferProjPath(context.actionContext, context.folder); + + // {@link https://github.com/dotnet/sdk-container-builds/issues/141} this could change in the future + const publishFlag = NetCoreTaskHelper.isWebApp(projPath) ? '-p:PublishProfile=DefaultContainer' : '/t:PublishContainer'; + + const folderName = await quickPickWorkspaceFolder(context.actionContext, ErrMsgNoWorkplaceFolder); + + const args = composeArgs( + withArg('dotnet', 'publish'), + withNamedArg('--os', await normalizeOsToRidOs()), + withNamedArg('--arch', await normalizeArchitectureToRidArchitecture()), + withArg(publishFlag), + withNamedArg('--configuration', configuration), + withNamedArg('-p:ContainerImageName', getValidImageName(folderName.name), { assignValue: true }), + withNamedArg('-p:ContainerImageTag', NetSdkDefaultImageTag, { assignValue: true }) + )(); + + const quotedArgs = Shell.getShellOrDefault().quote(args); + return quotedArgs.join(' '); +} + +export async function getNetSdkRunCommand(context: DockerTaskExecutionContext): Promise { + const client = await ext.runtimeManager.getClient(); + const folderName = await quickPickWorkspaceFolder(context.actionContext, ErrMsgNoWorkplaceFolder); + + const command = await client.runContainer({ + detached: true, + publishAllPorts: true, + name: getDefaultContainerName(folderName.name, NetSdkDefaultImageTag), + environmentVariables: {}, + removeOnExit: true, + imageRef: getDefaultImageName(folderName.name, NetSdkDefaultImageTag), + labels: defaultVsCodeLabels, + mounts: await getRemoteDebuggerMount(), + exposePorts: [8080], // hard coded for now since the default port is 8080 + entrypoint: '/bin/sh' + }); + + const quotedArgs = Shell.getShellOrDefault().quote(command.args); + const commandLine = [client.commandName, ...quotedArgs].join(' '); + return commandLine; +} + +async function inferProjPath(context: IActionContext, folder: WorkspaceFolder): Promise { + const noProjectFileErrMessage = l10n.t('No .csproj file could be found.'); + const item = await quickPickProjectFileItem(context, undefined, folder, noProjectFileErrMessage); + return item.absoluteFilePath; +} + +/** + * This method normalizes the Docker OS type to match the .NET Core SDK conventions. + * {@link https://learn.microsoft.com/en-us/dotnet/core/rid-catalog} + */ +async function normalizeOsToRidOs(): Promise<'linux' | 'win'> { + const dockerOsType = await getDockerOSType(); + return dockerOsType === 'windows' ? 'win' : 'linux'; +} + +/** + * This method normalizes the native architecture to match the .NET Core SDK conventions. + * {@link https://learn.microsoft.com/en-us/dotnet/core/rid-catalog} + */ +async function normalizeArchitectureToRidArchitecture(): Promise { + const architecture = os.arch(); + switch (architecture) { + case 'x32': + case 'ia32': + return 'x86'; + default: + return architecture; + } +} + +/** + * This methods returns the mount for the remote debugger ONLY as the SDK built container will have + * everything it needs to run the app already inside. + */ +async function getRemoteDebuggerMount(): Promise { + const debuggerVolume: RunContainerBindMount = { + type: 'bind', + source: vsDbgInstallBasePath, + destination: await getDockerOSType() === 'windows' ? 'C:\\remote_debugger' : '/remote_debugger', + readOnly: true + }; + return [debuggerVolume]; +} + +