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];
+}
+
+