Skip to content

Commit 5cde51d

Browse files
committed
Add support for runInTerminal request
1 parent 5c37f12 commit 5cde51d

File tree

3 files changed

+106
-14
lines changed

3 files changed

+106
-14
lines changed

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,9 +201,14 @@
201201
},
202202
"externalConsole": {
203203
"type": "boolean",
204-
"description": "Launch debug target in external console.",
204+
"description": "DEPRECATED: Launch debug target in external console.",
205205
"default": false
206206
},
207+
"console": {
208+
"enum": ["internalConsole", "integratedTerminal", "externalTerminal"],
209+
"description": "Where to launch the debug target: internal console, integrated terminal, or external terminal",
210+
"default": "internalConsole"
211+
},
207212
"args": {
208213
"type": "array",
209214
"description": "Command line arguments passed to the program.",

src/phpDebug.ts

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import * as childProcess from 'child_process'
88
import * as path from 'path'
99
import * as util from 'util'
1010
import * as fs from 'fs'
11-
import { Terminal } from './terminal'
11+
import { Terminal, IProgram, ProgramPidWrapper } from './terminal'
1212
import { convertClientPathToDebugger, convertDebuggerPathToClient, isPositiveMatchInGlobs } from './paths'
1313
import minimatch from 'minimatch'
1414
import { BreakpointManager, BreakpointAdapter } from './breakpoints'
@@ -115,19 +115,24 @@ export interface LaunchRequestArguments extends VSCodeDebugProtocol.LaunchReques
115115
env?: { [key: string]: string }
116116
/** Absolute path to a file containing environment variable definitions. */
117117
envFile?: string
118-
/** If true launch the target in an external console. */
118+
/** DEPRECATED: If true launch the target in an external console. */
119119
externalConsole?: boolean
120+
/** Where to launch the debug target: internal console, integrated terminal, or external terminal. */
121+
console?: 'internalConsole' | 'integratedTerminal' | 'externalTerminal'
120122
}
121123

122124
class PhpDebugSession extends vscode.DebugSession {
125+
/** The arguments that were given to initializeRequest */
126+
private _initializeArgs: VSCodeDebugProtocol.InitializeRequestArguments
127+
123128
/** The arguments that were given to launchRequest */
124129
private _args: LaunchRequestArguments
125130

126131
/** The TCP server that listens for Xdebug connections */
127132
private _server: net.Server
128133

129134
/** The child process of the launched PHP script, if launched by the debug adapter */
130-
private _phpProcess?: childProcess.ChildProcess
135+
private _phpProcess?: IProgram
131136

132137
/**
133138
* A map from VS Code thread IDs to Xdebug Connections.
@@ -211,6 +216,7 @@ class PhpDebugSession extends vscode.DebugSession {
211216
response: VSCodeDebugProtocol.InitializeResponse,
212217
args: VSCodeDebugProtocol.InitializeRequestArguments
213218
): void {
219+
this._initializeArgs = args
214220
response.body = {
215221
supportsConfigurationDoneRequest: true,
216222
supportsEvaluateForHovers: true,
@@ -287,29 +293,48 @@ class PhpDebugSession extends vscode.DebugSession {
287293
const program = args.program ? [args.program] : []
288294
const cwd = args.cwd || process.cwd()
289295
const env = Object.fromEntries(
290-
Object.entries({ ...process.env, ...getConfiguredEnvironment(args) }).map(v => [
296+
Object.entries(getConfiguredEnvironment(args)).map(v => [
291297
v[0],
292298
v[1]?.replace('${port}', port.toString()),
293299
])
294300
)
295301
// launch in CLI mode
296-
if (args.externalConsole) {
297-
const script = await Terminal.launchInTerminal(
298-
cwd,
299-
[runtimeExecutable, ...runtimeArgs, ...program, ...programArgs],
300-
env
301-
)
302+
if (args.externalConsole || args.console == 'integratedTerminal' || args.console == 'externalTerminal') {
303+
let script: IProgram | undefined
304+
if (this._initializeArgs.supportsRunInTerminalRequest) {
305+
const kind: 'integrated' | 'external' =
306+
args.externalConsole || args.console === 'externalTerminal' ? 'external' : 'integrated'
307+
const ritr = await new Promise<VSCodeDebugProtocol.RunInTerminalResponse>((resolve, reject) => {
308+
this.runInTerminalRequest(
309+
{ args: [runtimeExecutable, ...runtimeArgs, ...program, ...programArgs], env, cwd, kind },
310+
5000,
311+
resolve
312+
)
313+
})
314+
script =
315+
ritr.success && ritr.body.shellProcessId
316+
? new ProgramPidWrapper(ritr.body.shellProcessId)
317+
: undefined
318+
} else {
319+
script = await Terminal.launchInTerminal(
320+
cwd,
321+
[runtimeExecutable, ...runtimeArgs, ...program, ...programArgs],
322+
env
323+
)
324+
}
325+
302326
if (script) {
303327
// we only do this for CLI mode. In normal listen mode, only a thread exited event is send.
304328
script.on('exit', (code: number | null) => {
305329
this.sendEvent(new vscode.ExitedEvent(code ?? 0))
306330
this.sendEvent(new vscode.TerminatedEvent())
307331
})
308332
}
333+
// this._phpProcess = script
309334
} else {
310335
const script = childProcess.spawn(runtimeExecutable, [...runtimeArgs, ...program, ...programArgs], {
311336
cwd,
312-
env,
337+
env: { ...process.env, ...env },
313338
})
314339
// redirect output to debug console
315340
script.stdout.on('data', (data: Buffer) => {
@@ -1423,8 +1448,10 @@ class PhpDebugSession extends vscode.DebugSession {
14231448
}
14241449
}
14251450
// If launched as CLI, kill process
1426-
if (this._phpProcess) {
1427-
this._phpProcess.kill()
1451+
if (this._phpProcess?.pid) {
1452+
Terminal.killTree(this._phpProcess.pid).catch(err =>
1453+
this.sendEvent(new vscode.OutputEvent(`killTree: ${err as string}\n`))
1454+
)
14281455
}
14291456
} catch (error) {
14301457
this.sendErrorResponse(response, error as Error)

src/terminal.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import * as Path from 'path'
77
import * as FS from 'fs'
88
import * as CP from 'child_process'
9+
import { EventEmitter } from 'stream'
910

1011
export class Terminal {
1112
private static _terminalService: ITerminalService
@@ -45,6 +46,65 @@ export class Terminal {
4546
}
4647
}
4748

49+
export interface IProgram {
50+
readonly pid?: number | undefined
51+
kill(signal?: NodeJS.Signals | number): boolean
52+
on(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this
53+
}
54+
55+
export class ProgramPidWrapper extends EventEmitter implements IProgram {
56+
/**
57+
* How often to check and see if the process exited.
58+
*/
59+
private static readonly terminationPollInterval = 1000
60+
61+
/**
62+
* How often to check and see if the process exited after we send a close signal.
63+
*/
64+
//private static readonly killConfirmInterval = 200;
65+
66+
private loop?: { timer: NodeJS.Timeout; processId: number }
67+
68+
constructor(readonly pid?: number) {
69+
super()
70+
71+
if (pid) {
72+
this.startPollLoop(pid)
73+
}
74+
}
75+
76+
kill(signal?: number | NodeJS.Signals | undefined): boolean {
77+
return false
78+
}
79+
80+
private startPollLoop(processId: number, interval = ProgramPidWrapper.terminationPollInterval) {
81+
if (this.loop) {
82+
clearInterval(this.loop.timer)
83+
}
84+
85+
const loop = {
86+
processId,
87+
timer: setInterval(() => {
88+
if (!isProcessAlive(processId)) {
89+
clearInterval(loop.timer)
90+
this.emit('exit')
91+
}
92+
}, interval),
93+
}
94+
95+
this.loop = loop
96+
}
97+
}
98+
function isProcessAlive(processId: number) {
99+
try {
100+
// kill with signal=0 just test for whether the proc is alive. It throws if not.
101+
process.kill(processId, 0)
102+
return true
103+
} catch {
104+
return false
105+
}
106+
}
107+
48108
interface ITerminalService {
49109
launchInTerminal(
50110
dir: string,

0 commit comments

Comments
 (0)