1
1
import * as vscode from 'vscode' ;
2
2
import { extensionLogOutputChannel } from '../../utils/logging' ;
3
3
import { noCsharpBuildTask , buildFailedWithExitCode , noOutputFromMsbuild , failedToGetTargetPath , invalidLaunchConfiguration } from '../../loc/strings' ;
4
- import { execFile } from 'child_process' ;
4
+ import { ChildProcessWithoutNullStreams , execFile , spawn } from 'child_process' ;
5
5
import * as util from 'util' ;
6
6
import * as path from 'path' ;
7
+ import * as readline from 'readline' ;
8
+ import * as os from 'os' ;
7
9
import { doesFileExist } from '../../utils/io' ;
8
10
import { AspireResourceExtendedDebugConfiguration , isProjectLaunchConfiguration } from '../../dcp/types' ;
9
11
import { ResourceDebuggerExtension } from '../debuggerExtensions' ;
@@ -20,6 +22,7 @@ interface IDotNetService {
20
22
getAndActivateDevKit ( ) : Promise < boolean >
21
23
buildDotNetProject ( projectFile : string ) : Promise < void > ;
22
24
getDotNetTargetPath ( projectFile : string ) : Promise < string > ;
25
+ getDotNetRunApiOutput ( projectFile : string ) : Promise < string > ;
23
26
}
24
27
25
28
class DotNetService implements IDotNetService {
@@ -112,6 +115,68 @@ class DotNetService implements IDotNetService {
112
115
throw new Error ( failedToGetTargetPath ( String ( err ) ) ) ;
113
116
}
114
117
}
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
+ }
115
180
}
116
181
117
182
export function createProjectDebuggerExtension ( dotNetService : IDotNetService ) : ResourceDebuggerExtension {
@@ -148,20 +213,27 @@ export function createProjectDebuggerExtension(dotNetService: IDotNetService): R
148
213
? `Using launch profile '${ profileName } ' for project: ${ projectPath } `
149
214
: `No launch profile selected for project: ${ projectPath } ` ) ;
150
215
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
-
157
216
// Configure debug session with launch profile settings
158
- debugConfiguration . program = outputPath ;
159
217
debugConfiguration . cwd = determineWorkingDirectory ( projectPath , baseProfile ) ;
160
218
debugConfiguration . args = determineArguments ( baseProfile ?. commandLineArgs , args ) ;
161
219
debugConfiguration . env = Object . fromEntries ( mergeEnvironmentVariables ( baseProfile ?. environmentVariables , env ) ) ;
162
220
debugConfiguration . executablePath = baseProfile ?. executablePath ;
163
221
debugConfiguration . checkForDevCert = baseProfile ?. useSSL ;
164
222
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
+ }
165
237
}
166
238
} ;
167
239
}
0 commit comments