Skip to content

Commit 3fffcd4

Browse files
authored
Merge branch 'main' into copilot/fix-addpythonapp-virtual-env-error
2 parents 4302976 + 64c1bd7 commit 3fffcd4

File tree

70 files changed

+1282
-380
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+1282
-380
lines changed

extension/src/dcp/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ export function isProjectLaunchConfiguration(obj: any): obj is ProjectLaunchConf
3030

3131
export interface PythonLaunchConfiguration extends ExecutableLaunchConfiguration {
3232
type: "python";
33-
program_path: string;
33+
program_path?: string;
34+
project_path?: string; // leftover from 9.5 usage of project path
3435
}
3536

3637
export function isPythonLaunchConfiguration(obj: any): obj is PythonLaunchConfiguration {

extension/src/debugger/languages/dotnet.ts

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import * as vscode from 'vscode';
22
import { extensionLogOutputChannel } from '../../utils/logging';
33
import { noCsharpBuildTask, buildFailedWithExitCode, noOutputFromMsbuild, failedToGetTargetPath, invalidLaunchConfiguration } from '../../loc/strings';
4-
import { execFile } from 'child_process';
4+
import { ChildProcessWithoutNullStreams, execFile, spawn } from 'child_process';
55
import * as util from 'util';
66
import * as path from 'path';
7+
import * as readline from 'readline';
8+
import * as os from 'os';
79
import { doesFileExist } from '../../utils/io';
810
import { AspireResourceExtendedDebugConfiguration, isProjectLaunchConfiguration } from '../../dcp/types';
911
import { ResourceDebuggerExtension } from '../debuggerExtensions';
@@ -20,6 +22,7 @@ interface IDotNetService {
2022
getAndActivateDevKit(): Promise<boolean>
2123
buildDotNetProject(projectFile: string): Promise<void>;
2224
getDotNetTargetPath(projectFile: string): Promise<string>;
25+
getDotNetRunApiOutput(projectFile: string): Promise<string>;
2326
}
2427

2528
class DotNetService implements IDotNetService {
@@ -112,6 +115,68 @@ class DotNetService implements IDotNetService {
112115
throw new Error(failedToGetTargetPath(String(err)));
113116
}
114117
}
118+
119+
async getDotNetRunApiOutput(projectPath: string): Promise<string> {
120+
return new Promise<string>(async (resolve, reject) => {
121+
try {
122+
let childProcess: ChildProcessWithoutNullStreams;
123+
const timeout = setTimeout(() => {
124+
childProcess?.kill();
125+
reject(new Error('Timeout while waiting for dotnet run-api response'));
126+
}, 10_000);
127+
128+
extensionLogOutputChannel.info('dotnet run-api - starting process');
129+
130+
childProcess = spawn('dotnet', ['run-api'], {
131+
cwd: path.dirname(projectPath),
132+
env: process.env,
133+
stdio: ['pipe', 'pipe', 'pipe']
134+
});
135+
136+
childProcess.on('error', reject);
137+
childProcess.on('exit', (code, signal) => {
138+
clearTimeout(timeout);
139+
reject(new Error(`dotnet run-api exited with ${code ?? signal}`));
140+
});
141+
142+
const rl = readline.createInterface(childProcess.stdout);
143+
rl.on('line', line => {
144+
clearTimeout(timeout);
145+
extensionLogOutputChannel.info(`dotnet run-api - received: ${line}`);
146+
resolve(line);
147+
});
148+
149+
const message = JSON.stringify({ ['$type']: 'GetRunCommand', ['EntryPointFileFullPath']: projectPath });
150+
extensionLogOutputChannel.info(`dotnet run-api - sending: ${message}`);
151+
childProcess.stdin.write(message + os.EOL);
152+
childProcess.stdin.end();
153+
} catch (e) {
154+
reject(e);
155+
}
156+
});
157+
}
158+
}
159+
160+
function isSingleFileAppHost(projectPath: string): boolean {
161+
return path.basename(projectPath).toLowerCase() === 'apphost.cs';
162+
}
163+
164+
function applyRunApiOutputToDebugConfiguration(runApiOutput: string, debugConfiguration: AspireResourceExtendedDebugConfiguration) {
165+
const parsed = JSON.parse(runApiOutput);
166+
if (parsed.$type === 'Error') {
167+
throw new Error(`dotnet run-api failed: ${parsed.Message}`);
168+
}
169+
else if (parsed.$type !== 'RunCommand') {
170+
throw new Error(`dotnet run-api failed: Unexpected response type '${parsed.$type}'`);
171+
}
172+
173+
debugConfiguration.program = parsed.ExecutablePath;
174+
if (parsed.EnvironmentVariables) {
175+
debugConfiguration.env = {
176+
...debugConfiguration.env,
177+
...parsed.EnvironmentVariables
178+
};
179+
}
115180
}
116181

117182
export function createProjectDebuggerExtension(dotNetService: IDotNetService): ResourceDebuggerExtension {
@@ -148,20 +213,27 @@ export function createProjectDebuggerExtension(dotNetService: IDotNetService): R
148213
? `Using launch profile '${profileName}' for project: ${projectPath}`
149214
: `No launch profile selected for project: ${projectPath}`);
150215

151-
// Build project if needed
152-
const outputPath = await dotNetService.getDotNetTargetPath(projectPath);
153-
if ((!(await doesFileExist(outputPath)) || launchOptions.forceBuild) && await dotNetService.getAndActivateDevKit()) {
154-
await dotNetService.buildDotNetProject(projectPath);
155-
}
156-
157216
// Configure debug session with launch profile settings
158-
debugConfiguration.program = outputPath;
159217
debugConfiguration.cwd = determineWorkingDirectory(projectPath, baseProfile);
160218
debugConfiguration.args = determineArguments(baseProfile?.commandLineArgs, args);
161219
debugConfiguration.env = Object.fromEntries(mergeEnvironmentVariables(baseProfile?.environmentVariables, env));
162220
debugConfiguration.executablePath = baseProfile?.executablePath;
163221
debugConfiguration.checkForDevCert = baseProfile?.useSSL;
164222
debugConfiguration.serverReadyAction = determineServerReadyAction(baseProfile?.launchBrowser, baseProfile?.applicationUrl);
223+
224+
// Build project if needed
225+
if (!isSingleFileAppHost(projectPath)) {
226+
const outputPath = await dotNetService.getDotNetTargetPath(projectPath);
227+
if ((!(await doesFileExist(outputPath)) || launchOptions.forceBuild) && await dotNetService.getAndActivateDevKit()) {
228+
await dotNetService.buildDotNetProject(projectPath);
229+
}
230+
231+
debugConfiguration.program = outputPath;
232+
}
233+
else {
234+
const runApiOutput = await dotNetService.getDotNetRunApiOutput(projectPath);
235+
applyRunApiOutputToDebugConfiguration(runApiOutput, debugConfiguration);
236+
}
165237
}
166238
};
167239
}

extension/src/debugger/languages/python.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ export const pythonDebuggerExtension: ResourceDebuggerExtension = {
99
displayName: 'Python',
1010
getProjectFile: (launchConfig) => {
1111
if (isPythonLaunchConfiguration(launchConfig)) {
12-
return launchConfig.program_path;
12+
const programPath = launchConfig.program_path || launchConfig.project_path;
13+
if (programPath) {
14+
return programPath;
15+
}
1316
}
1417

1518
throw new Error(invalidLaunchConfiguration(JSON.stringify(launchConfig)));

extension/src/server/interactionService.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ export class InteractionService implements IInteractionService {
8787
}
8888

8989
return null;
90-
}
90+
},
91+
ignoreFocusOut: true
9192
});
9293

9394
return input || null;
@@ -117,7 +118,8 @@ export class InteractionService implements IInteractionService {
117118
}
118119

119120
return null;
120-
}
121+
},
122+
ignoreFocusOut: true
121123
});
122124

123125
return input || null;
@@ -317,11 +319,14 @@ export class InteractionService implements IInteractionService {
317319
}
318320

319321
logMessage(logLevel: CSLogLevel, message: string) {
322+
// Unable to log trace or debug messages, these levels are ignored by default
323+
// and we cannot set the log level programmatically. So for now, log as info
324+
// https://github.com/microsoft/vscode/issues/223536
320325
if (logLevel === 'Trace') {
321-
extensionLogOutputChannel.trace(formatText(message));
326+
extensionLogOutputChannel.info(`[trace] ${formatText(message)}`);
322327
}
323328
else if (logLevel === 'Debug') {
324-
extensionLogOutputChannel.debug(formatText(message));
329+
extensionLogOutputChannel.info(`[debug] ${formatText(message)}`);
325330
}
326331
else if (logLevel === 'Information') {
327332
extensionLogOutputChannel.info(formatText(message));
@@ -368,7 +373,8 @@ export class InteractionService implements IInteractionService {
368373
noDebug: !debug,
369374
};
370375

371-
const didDebugStart = await vscode.debug.startDebugging(vscode.workspace.workspaceFolders?.[0], debugConfiguration);
376+
const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(workingDirectory));
377+
const didDebugStart = await vscode.debug.startDebugging(workspaceFolder, debugConfiguration);
372378
if (!didDebugStart) {
373379
throw new Error(failedToStartDebugSession);
374380
}

extension/src/test/dotnetDebugger.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ class TestDotNetService {
3737
getAndActivateDevKit(): Promise<boolean> {
3838
return Promise.resolve(this._hasDevKit);
3939
}
40+
41+
getDotNetRunApiOutput(projectPath: string): Promise<string> {
42+
return Promise.resolve('');
43+
}
4044
}
4145

4246
suite('Dotnet Debugger Extension Tests', () => {

extension/src/utils/AspireTerminalProvider.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { aspireTerminalName, dcpServerNotInitialized, rpcServerNotInitialized }
33
import { extensionLogOutputChannel } from './logging';
44
import { RpcServerConnectionInfo } from '../server/AspireRpcServer';
55
import { DcpServerConnectionInfo } from '../dcp/types';
6-
import { getRunSessionInfo } from '../capabilities';
6+
import { getRunSessionInfo, getSupportedCapabilities } from '../capabilities';
77

88
export interface AspireTerminal {
99
terminal: vscode.Terminal;
@@ -114,7 +114,9 @@ export class AspireTerminalProvider implements vscode.Disposable {
114114
env.ASPIRE_EXTENSION_DEBUG_SESSION_ID = debugSessionId;
115115
env.DCP_INSTANCE_ID_PREFIX = debugSessionId + '-';
116116
env.DEBUG_SESSION_RUN_MODE = noDebug === false ? "Debug" : "NoDebug";
117+
env.ASPIRE_EXTENSION_DEBUG_RUN_MODE = noDebug === false ? "Debug" : "NoDebug";
117118
env.DEBUG_SESSION_INFO = JSON.stringify(getRunSessionInfo());
119+
env.ASPIRE_EXTENSION_CAPABILITIES = getSupportedCapabilities().join(',');
118120
}
119121

120122
return env;

src/Aspire.Cli/Commands/DeployCommand.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.CommandLine;
45
using Aspire.Cli.Configuration;
56
using Aspire.Cli.DotNet;
67
using Aspire.Cli.Interaction;
@@ -13,16 +14,23 @@ namespace Aspire.Cli.Commands;
1314

1415
internal sealed class DeployCommand : PublishCommandBase
1516
{
17+
private readonly Option<bool> _clearCacheOption;
18+
1619
public DeployCommand(IDotNetCliRunner runner, IInteractionService interactionService, IProjectLocator projectLocator, AspireCliTelemetry telemetry, IDotNetSdkInstaller sdkInstaller, IFeatures features, ICliUpdateNotifier updateNotifier, CliExecutionContext executionContext)
1720
: base("deploy", DeployCommandStrings.Description, runner, interactionService, projectLocator, telemetry, sdkInstaller, features, updateNotifier, executionContext)
1821
{
22+
_clearCacheOption = new Option<bool>("--clear-cache")
23+
{
24+
Description = "Clear the deployment cache associated with the current environment and do not save deployment state"
25+
};
26+
Options.Add(_clearCacheOption);
1927
}
2028

2129
protected override string OperationCompletedPrefix => DeployCommandStrings.OperationCompletedPrefix;
2230
protected override string OperationFailedPrefix => DeployCommandStrings.OperationFailedPrefix;
2331
protected override string GetOutputPathDescription() => DeployCommandStrings.OutputPathArgumentDescription;
2432

25-
protected override string[] GetRunArguments(string? fullyQualifiedOutputPath, string[] unmatchedTokens)
33+
protected override string[] GetRunArguments(string? fullyQualifiedOutputPath, string[] unmatchedTokens, ParseResult parseResult)
2634
{
2735
var baseArgs = new List<string> { "--operation", "publish", "--publisher", "default" };
2836

@@ -32,6 +40,13 @@ protected override string[] GetRunArguments(string? fullyQualifiedOutputPath, st
3240
}
3341

3442
baseArgs.AddRange(["--deploy", "true"]);
43+
44+
var clearCache = parseResult.GetValue(_clearCacheOption);
45+
if (clearCache)
46+
{
47+
baseArgs.AddRange(["--clear-cache", "true"]);
48+
}
49+
3550
baseArgs.AddRange(unmatchedTokens);
3651

3752
return [.. baseArgs];

src/Aspire.Cli/Commands/PublishCommand.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.CommandLine;
45
using Aspire.Cli.Configuration;
56
using Aspire.Cli.DotNet;
67
using Aspire.Cli.Interaction;
@@ -44,7 +45,7 @@ public PublishCommand(IDotNetCliRunner runner, IInteractionService interactionSe
4445
protected override string OperationFailedPrefix => PublishCommandStrings.OperationFailedPrefix;
4546
protected override string GetOutputPathDescription() => PublishCommandStrings.OutputPathArgumentDescription;
4647

47-
protected override string[] GetRunArguments(string? fullyQualifiedOutputPath, string[] unmatchedTokens)
48+
protected override string[] GetRunArguments(string? fullyQualifiedOutputPath, string[] unmatchedTokens, ParseResult parseResult)
4849
{
4950
var baseArgs = new List<string> { "--operation", "publish", "--publisher", "default" };
5051

src/Aspire.Cli/Commands/PublishCommandBase.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ protected PublishCommandBase(string name, string description, IDotNetCliRunner r
7575
}
7676

7777
protected abstract string GetOutputPathDescription();
78-
protected abstract string[] GetRunArguments(string? fullyQualifiedOutputPath, string[] unmatchedTokens);
78+
protected abstract string[] GetRunArguments(string? fullyQualifiedOutputPath, string[] unmatchedTokens, ParseResult parseResult);
7979
protected abstract string GetCanceledMessage();
8080
protected abstract string GetProgressMessage();
8181

@@ -173,7 +173,7 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
173173
effectiveAppHostFile,
174174
false,
175175
true,
176-
GetRunArguments(fullyQualifiedOutputPath, unmatchedTokens),
176+
GetRunArguments(fullyQualifiedOutputPath, unmatchedTokens, parseResult),
177177
env,
178178
backchannelCompletionSource,
179179
operationRunOptions,

src/Aspire.Cli/Commands/RunCommand.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
144144

145145
if (!watch)
146146
{
147-
if (!isSingleFileAppHost)
147+
if (!isSingleFileAppHost || isExtensionHost)
148148
{
149149
var buildOptions = new DotNetCliRunnerInvocationOptions
150150
{
@@ -154,7 +154,8 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
154154

155155
// The extension host will build the app host project itself, so we don't need to do it here if host exists.
156156
if (!ExtensionHelper.IsExtensionHost(InteractionService, out _, out var extensionBackchannel)
157-
|| !await extensionBackchannel.HasCapabilityAsync(KnownCapabilities.DevKit, cancellationToken))
157+
|| !await extensionBackchannel.HasCapabilityAsync(KnownCapabilities.DevKit, cancellationToken)
158+
|| isSingleFileAppHost)
158159
{
159160
var buildExitCode = await AppHostHelper.BuildAppHostAsync(_runner, InteractionService, effectiveAppHostFile, buildOptions, ExecutionContext.WorkingDirectory, cancellationToken);
160161

0 commit comments

Comments
 (0)