11import * as vscode from 'vscode' ;
22import { extensionLogOutputChannel } from '../../utils/logging' ;
33import { noCsharpBuildTask , buildFailedWithExitCode , noOutputFromMsbuild , failedToGetTargetPath , invalidLaunchConfiguration } from '../../loc/strings' ;
4- import { execFile } from 'child_process' ;
4+ import { ChildProcessWithoutNullStreams , execFile , spawn } from 'child_process' ;
55import * as util from 'util' ;
66import * as path from 'path' ;
7+ import * as readline from 'readline' ;
8+ import * as os from 'os' ;
79import { doesFileExist } from '../../utils/io' ;
810import { AspireResourceExtendedDebugConfiguration , isProjectLaunchConfiguration } from '../../dcp/types' ;
911import { 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
2528class 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
117182export 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}
0 commit comments