Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion extension/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
dist/
.localization/
out/
node_modules/
node_modules/
extension/.vscode-test/
16 changes: 14 additions & 2 deletions extension/src/debugger/AspireDebugConfigurationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,20 @@ export class AspireDebugConfigurationProvider implements vscode.DebugConfigurati
}

async resolveDebugConfiguration(folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration, token?: vscode.CancellationToken): Promise<vscode.DebugConfiguration> {
if (config.program === '') {
config.program = folder?.uri.fsPath || '';
if (!config.type) {
config.type = 'aspire';
}

if (!config.request) {
config.request = 'launch';
}

if (!config.name) {
config.name = defaultConfigurationName;
}

if (!config.program) {
config.program = folder?.uri.fsPath || '${workspaceFolder}';
}

return config;
Expand Down
5 changes: 2 additions & 3 deletions extension/src/debugger/debuggerExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@ export interface ResourceDebuggerExtension {
debugAdapter: string;
extensionId: string | null;
displayName: string;

createDebugSessionConfigurationCallback?: (launchConfig: LaunchConfiguration, args: string[], env: EnvVar[], launchOptions: LaunchOptions, debugConfiguration: AspireResourceExtendedDebugConfiguration) => Promise<void>;
createDebugSessionConfigurationCallback?: (launchConfig: LaunchConfiguration, args: string[] | undefined, env: EnvVar[], launchOptions: LaunchOptions, debugConfiguration: AspireResourceExtendedDebugConfiguration) => Promise<void>;
}

export async function createDebugSessionConfiguration(launchConfig: LaunchConfiguration, args: string[], env: EnvVar[], launchOptions: LaunchOptions, debuggerExtension: ResourceDebuggerExtension | null): Promise<AspireResourceExtendedDebugConfiguration> {
export async function createDebugSessionConfiguration(launchConfig: LaunchConfiguration, args: string[] | undefined, env: EnvVar[], launchOptions: LaunchOptions, debuggerExtension: ResourceDebuggerExtension | null): Promise<AspireResourceExtendedDebugConfiguration> {
if (debuggerExtension === null) {
extensionLogOutputChannel.warn(`Unknown type: ${launchConfig.type}.`);
}
Expand Down
219 changes: 128 additions & 91 deletions extension/src/debugger/languages/dotnet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,109 +7,146 @@ import * as path from 'path';
import { doesFileExist } from '../../utils/io';
import { AspireResourceExtendedDebugConfiguration } from '../../dcp/types';
import { ResourceDebuggerExtension } from '../debuggerExtensions';
import {
readLaunchSettings,
determineBaseLaunchProfile,
mergeEnvironmentVariables,
determineArguments,
determineWorkingDirectory,
determineServerReadyAction
} from '../launchProfiles';

interface IDotNetService {
getAndActivateDevKit(): Promise<boolean>
buildDotNetProject(projectFile: string): Promise<void>;
getDotNetTargetPath(projectFile: string): Promise<string>;
}

const execFileAsync = util.promisify(execFile);

export const projectDebuggerExtension: ResourceDebuggerExtension = {
resourceType: 'project',
debugAdapter: 'coreclr',
extensionId: 'ms-dotnettools.csharp',
displayName: 'C#',
createDebugSessionConfigurationCallback: async (launchConfig, args, env, launchOptions, debugConfiguration: AspireResourceExtendedDebugConfiguration): Promise<void> => {
const projectPath = launchConfig.project_path;
const workingDirectory = path.dirname(launchConfig.project_path);

const outputPath = await getDotNetTargetPath(projectPath);
class DotNetService implements IDotNetService {
execFileAsync = util.promisify(execFile);

if (!(await doesFileExist(outputPath)) || launchOptions.forceBuild) {
await buildDotNetProject(projectPath);
async getAndActivateDevKit(): Promise<boolean> {
const csharpDevKit = vscode.extensions.getExtension('ms-dotnettools.csdevkit');
if (!csharpDevKit) {
// If c# dev kit is not installed, we will have already built this project on the command line using the Aspire CLI
// thus we should just immediately return
return Promise.resolve(false);
}

debugConfiguration.program = outputPath;
debugConfiguration.cwd = workingDirectory;
}
};

async function buildDotNetProject(projectFile: string): Promise<void> {
const csharpDevKit = vscode.extensions.getExtension('ms-dotnettools.csdevkit');
if (!csharpDevKit) {
// If c# dev kit is not installed, we will have already built this project on the command line using the Aspire CLI
// thus we should just immediately return
return Promise.resolve();
}
if (!csharpDevKit.isActive) {
extensionLogOutputChannel.info('Activating C# Dev Kit extension...');
await csharpDevKit.activate();
}

if (!csharpDevKit.isActive) {
extensionLogOutputChannel.info('Activating C# Dev Kit extension...');
await csharpDevKit.activate();
return Promise.resolve(true);
}

// C# Dev Kit may not register the build task immediately, so we need to retry until it is available
const pRetry = (await import('p-retry')).default;
const buildTask = await pRetry(async () => {
const tasks = await vscode.tasks.fetchTasks();
const buildTask = tasks.find(t => t.source === "dotnet" && t.name?.includes('build'));
if (!buildTask) {
throw new Error(noCsharpBuildTask);
}
async buildDotNetProject(projectFile: string): Promise<void> {
// C# Dev Kit may not register the build task immediately, so we need to retry until it is available
const pRetry = (await import('p-retry')).default;
const buildTask = await pRetry(async () => {
const tasks = await vscode.tasks.fetchTasks();
const buildTask = tasks.find(t => t.source === "dotnet" && t.name?.includes('build'));
if (!buildTask) {
throw new Error(noCsharpBuildTask);
}

return buildTask;
});
return buildTask;
}, { retries: 10 });

// Modify the task to target the specific project
const projectName = path.basename(projectFile, '.csproj');

// Create a modified task definition with just the project file
const modifiedDefinition = {
...buildTask.definition,
file: projectFile // This will make it build the specific project directly
};

// Create a new task with the modified definition
const modifiedTask = new vscode.Task(
modifiedDefinition,
buildTask.scope || vscode.TaskScope.Workspace,
`build ${projectName}`,
buildTask.source,
buildTask.execution,
buildTask.problemMatchers
);

extensionLogOutputChannel.info(`Executing build task: ${modifiedTask.name} for project: ${projectFile}`);
await vscode.tasks.executeTask(modifiedTask);

let disposable: vscode.Disposable;
return new Promise<void>((resolve, reject) => {
disposable = vscode.tasks.onDidEndTaskProcess(async e => {
if (e.execution.task === modifiedTask) {
if (e.exitCode !== 0) {
reject(new Error(buildFailedWithExitCode(e.exitCode ?? 'unknown')));
}
else {
return resolve();
}
}
});
}).finally(() => disposable.dispose());
}

// Modify the task to target the specific project
const projectName = path.basename(projectFile, '.csproj');
async getDotNetTargetPath(projectFile: string): Promise<string> {
const args = [
'msbuild',
projectFile,
'-nologo',
'-getProperty:TargetPath',
'-v:q',
'-property:GenerateFullPaths=true'
];
try {
const { stdout } = await this.execFileAsync('dotnet', args, { encoding: 'utf8' });
const output = stdout.trim();
if (!output) {
throw new Error(noOutputFromMsbuild);
}

// Create a modified task definition with just the project file
const modifiedDefinition = {
...buildTask.definition,
file: projectFile // This will make it build the specific project directly
};
return output;
} catch (err) {
throw new Error(failedToGetTargetPath(String(err)));
}
}
}

// Create a new task with the modified definition
const modifiedTask = new vscode.Task(
modifiedDefinition,
buildTask.scope || vscode.TaskScope.Workspace,
`build ${projectName}`,
buildTask.source,
buildTask.execution,
buildTask.problemMatchers
);

extensionLogOutputChannel.info(`Executing build task: ${modifiedTask.name} for project: ${projectFile}`);
await vscode.tasks.executeTask(modifiedTask);

let disposable: vscode.Disposable;
return new Promise<void>((resolve, reject) => {
disposable = vscode.tasks.onDidEndTaskProcess(async e => {
if (e.execution.task === modifiedTask) {
if (e.exitCode !== 0) {
reject(new Error(buildFailedWithExitCode(e.exitCode ?? 0)));
}
else {
return resolve();
}
export function createProjectDebuggerExtension(dotNetService: IDotNetService): ResourceDebuggerExtension {
return {
resourceType: 'project',
debugAdapter: 'coreclr',
extensionId: 'ms-dotnettools.csharp',
displayName: 'C#',
createDebugSessionConfigurationCallback: async (launchConfig, args, env, launchOptions, debugConfiguration: AspireResourceExtendedDebugConfiguration): Promise<void> => {
const projectPath = launchConfig.project_path;

// Apply launch profile settings if available
const launchSettings = await readLaunchSettings(projectPath);
const { profile: baseProfile, profileName } = determineBaseLaunchProfile(launchConfig, launchSettings);

extensionLogOutputChannel.info(profileName
? `Using launch profile '${profileName}' for project: ${projectPath}`
: `No launch profile selected for project: ${projectPath}`);

// Build project if needed
const outputPath = await dotNetService.getDotNetTargetPath(projectPath);
if ((!(await doesFileExist(outputPath)) || launchOptions.forceBuild) && await dotNetService.getAndActivateDevKit()) {
await dotNetService.buildDotNetProject(projectPath);
}
});
}).finally(() => disposable.dispose());
}

async function getDotNetTargetPath(projectFile: string): Promise<string> {
const args = [
'msbuild',
projectFile,
'-nologo',
'-getProperty:TargetPath',
'-v:q',
'-property:GenerateFullPaths=true'
];
try {
const { stdout } = await execFileAsync('dotnet', args, { encoding: 'utf8' });
const output = stdout.trim();
if (!output) {
throw new Error(noOutputFromMsbuild);
// Configure debug session with launch profile settings
debugConfiguration.program = outputPath;
debugConfiguration.cwd = determineWorkingDirectory(projectPath, baseProfile);
debugConfiguration.args = determineArguments(baseProfile?.commandLineArgs, args);
debugConfiguration.env = Object.fromEntries(mergeEnvironmentVariables(baseProfile?.environmentVariables, env));
debugConfiguration.executablePath = baseProfile?.executablePath;
debugConfiguration.checkForDevCert = baseProfile?.useSSL;
debugConfiguration.serverReadyAction = determineServerReadyAction(baseProfile?.launchBrowser, baseProfile?.applicationUrl);
}

return output;
} catch (err) {
throw new Error(failedToGetTargetPath(String(err)));
}
};
}

export const projectDebuggerExtension: ResourceDebuggerExtension = createProjectDebuggerExtension(new DotNetService());
Loading
Loading