Skip to content

Commit 2c617d6

Browse files
authored
feat: Add support for runInTerminal request (#966)
* Add support for runInTerminal request * Changelog and Readme instructions. * In case of cli, observe the process with the PID from the Init packet
1 parent e2231bc commit 2c617d6

File tree

7 files changed

+128
-14
lines changed

7 files changed

+128
-14
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
77
## [1.37.0]
88

99
- Expection info and support for virtual exception property (Xdebug 3.5.0)
10+
- Support for console option with internalConsole, integratedTerminal and externalTerminal options.
1011

1112
## [1.36.2]
1213

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ Options specific to CLI debugging:
100100
- `cwd`: The current working directory to use when launching the script
101101
- `runtimeExecutable`: Path to the PHP binary used for launching the script. By default the one on the PATH.
102102
- `runtimeArgs`: Additional arguments to pass to the PHP binary
103-
- `externalConsole`: Launches the script in an external console window instead of the debug console (default: `false`)
103+
- `externalConsole`: _DEPRECATED_ Launches the script in an external console window instead of the debug console (default: `false`)
104+
- `console`: What kind of console to use for running the script. Possible values are: `internalConsole` (default), `integratedTerminal` or `externalTerminal`.
104105
- `env`: Environment variables to pass to the script
105106
- `envFile`: Optional path to a file containing environment variable definitions
106107

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,14 @@
197197
},
198198
"externalConsole": {
199199
"type": "boolean",
200-
"description": "Launch debug target in external console.",
200+
"description": "DEPRECATED: Launch debug target in external console.",
201201
"default": false
202202
},
203+
"console": {
204+
"enum": ["internalConsole", "integratedTerminal", "externalTerminal"],
205+
"description": "Where to launch the debug target: internal console, integrated terminal, or external terminal",
206+
"default": "internalConsole"
207+
},
203208
"args": {
204209
"type": "array",
205210
"description": "Command line arguments passed to the program.",

src/cloud.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Transport, DbgpConnection, ENCODING } from './dbgp'
44
import * as tls from 'tls'
55
import * as iconv from 'iconv-lite'
66
import * as xdebug from './xdebugConnection'
7-
import { EventEmitter } from 'stream'
7+
import { EventEmitter } from 'events'
88

99
export declare interface XdebugCloudConnection {
1010
on(event: 'error', listener: (error: Error) => void): this

src/phpDebug.ts

Lines changed: 51 additions & 11 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, isProcessAlive } from './terminal'
1212
import { convertClientPathToDebugger, convertDebuggerPathToClient, isPositiveMatchInGlobs } from './paths'
1313
import { minimatch } from 'minimatch'
1414
import { BreakpointManager, BreakpointAdapter } from './breakpoints'
@@ -120,19 +120,24 @@ export interface LaunchRequestArguments extends VSCodeDebugProtocol.LaunchReques
120120
env?: { [key: string]: string }
121121
/** Absolute path to a file containing environment variable definitions. */
122122
envFile?: string
123-
/** If true launch the target in an external console. */
123+
/** DEPRECATED: If true launch the target in an external console. */
124124
externalConsole?: boolean
125+
/** Where to launch the debug target: internal console, integrated terminal, or external terminal. */
126+
console?: 'internalConsole' | 'integratedTerminal' | 'externalTerminal'
125127
}
126128

127129
class PhpDebugSession extends vscode.DebugSession {
130+
/** The arguments that were given to initializeRequest */
131+
private _initializeArgs: VSCodeDebugProtocol.InitializeRequestArguments
132+
128133
/** The arguments that were given to launchRequest */
129134
private _args: LaunchRequestArguments
130135

131136
/** The TCP server that listens for Xdebug connections */
132137
private _server: net.Server
133138

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

137142
/**
138143
* A map from VS Code thread IDs to Xdebug Connections.
@@ -216,6 +221,7 @@ class PhpDebugSession extends vscode.DebugSession {
216221
response: VSCodeDebugProtocol.InitializeResponse,
217222
args: VSCodeDebugProtocol.InitializeRequestArguments
218223
): void {
224+
this._initializeArgs = args
219225
response.body = {
220226
supportsConfigurationDoneRequest: true,
221227
supportsEvaluateForHovers: true,
@@ -294,29 +300,48 @@ class PhpDebugSession extends vscode.DebugSession {
294300
const program = args.program ? [args.program] : []
295301
const cwd = args.cwd || process.cwd()
296302
const env = Object.fromEntries(
297-
Object.entries({ ...process.env, ...getConfiguredEnvironment(args) }).map(v => [
303+
Object.entries(getConfiguredEnvironment(args)).map(v => [
298304
v[0],
299305
v[1]?.replace('${port}', port.toString()),
300306
])
301307
)
302308
// launch in CLI mode
303-
if (args.externalConsole) {
304-
const script = await Terminal.launchInTerminal(
305-
cwd,
306-
[runtimeExecutable, ...runtimeArgs, ...program, ...programArgs],
307-
env
308-
)
309+
if (args.externalConsole || args.console == 'integratedTerminal' || args.console == 'externalTerminal') {
310+
let script: IProgram | undefined
311+
if (this._initializeArgs.supportsRunInTerminalRequest) {
312+
const kind: 'integrated' | 'external' =
313+
args.externalConsole || args.console === 'externalTerminal' ? 'external' : 'integrated'
314+
const ritr = await new Promise<VSCodeDebugProtocol.RunInTerminalResponse>((resolve, reject) => {
315+
this.runInTerminalRequest(
316+
{ args: [runtimeExecutable, ...runtimeArgs, ...program, ...programArgs], env, cwd, kind },
317+
5000,
318+
resolve
319+
)
320+
})
321+
script =
322+
ritr.success && ritr.body.shellProcessId
323+
? new ProgramPidWrapper(ritr.body.shellProcessId)
324+
: undefined
325+
} else {
326+
script = await Terminal.launchInTerminal(
327+
cwd,
328+
[runtimeExecutable, ...runtimeArgs, ...program, ...programArgs],
329+
env
330+
)
331+
}
332+
309333
if (script) {
310334
// we only do this for CLI mode. In normal listen mode, only a thread exited event is send.
311335
script.on('exit', (code: number | null) => {
312336
this.sendEvent(new vscode.ExitedEvent(code ?? 0))
313337
this.sendEvent(new vscode.TerminatedEvent())
314338
})
315339
}
340+
// this._phpProcess = script
316341
} else {
317342
const script = childProcess.spawn(runtimeExecutable, [...runtimeArgs, ...program, ...programArgs], {
318343
cwd,
319-
env,
344+
env: { ...process.env, ...env },
320345
})
321346
// redirect output to debug console
322347
script.stdout.on('data', (data: Buffer) => {
@@ -501,6 +526,21 @@ class PhpDebugSession extends vscode.DebugSession {
501526
private async initializeConnection(connection: xdebug.Connection): Promise<void> {
502527
const initPacket = await connection.waitForInitPacket()
503528

529+
// track the process, if we asked the IDE to spawn it
530+
if (
531+
!this._phpProcess &&
532+
(this._args.program || this._args.runtimeArgs) &&
533+
initPacket.appid &&
534+
isProcessAlive(parseInt(initPacket.appid))
535+
) {
536+
this._phpProcess = new ProgramPidWrapper(parseInt(initPacket.appid))
537+
// we only do this for CLI mode. In normal listen mode, only a thread exited event is send.
538+
this._phpProcess.on('exit', (code: number | null) => {
539+
this.sendEvent(new vscode.ExitedEvent(code ?? 0))
540+
this.sendEvent(new vscode.TerminatedEvent())
541+
})
542+
}
543+
504544
// check if this connection should be skipped
505545
if (
506546
this._args.skipEntryPaths &&

src/terminal.ts

Lines changed: 64 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 'events'
910

1011
export class Terminal {
1112
private static _terminalService: ITerminalService
@@ -45,6 +46,69 @@ 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+
this.startPollLoop(this.pid, ProgramPidWrapper.killConfirmInterval)
78+
Terminal.killTree(this.pid).catch(err => {
79+
// ignore
80+
})
81+
return true
82+
}
83+
84+
private startPollLoop(processId: number, interval = ProgramPidWrapper.terminationPollInterval) {
85+
if (this.loop) {
86+
clearInterval(this.loop.timer)
87+
}
88+
89+
const loop = {
90+
processId,
91+
timer: setInterval(() => {
92+
if (!isProcessAlive(processId)) {
93+
clearInterval(loop.timer)
94+
this.emit('exit')
95+
}
96+
}, interval),
97+
}
98+
99+
this.loop = loop
100+
}
101+
}
102+
export function isProcessAlive(processId: number) {
103+
try {
104+
// kill with signal=0 just test for whether the proc is alive. It throws if not.
105+
process.kill(processId, 0)
106+
return true
107+
} catch {
108+
return false
109+
}
110+
}
111+
48112
interface ITerminalService {
49113
launchInTerminal(
50114
dir: string,

src/xdebugConnection.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export class InitPacket {
1818
engineVersion: string
1919
/** the name of the engine */
2020
engineName: string
21+
/** the internal PID */
22+
appid: string
2123
/**
2224
* @param {XMLDocument} document - An XML document to read from
2325
* @param {Connection} connection
@@ -30,6 +32,7 @@ export class InitPacket {
3032
this.ideKey = documentElement.getAttribute('idekey')!
3133
this.engineVersion = documentElement.getElementsByTagName('engine').item(0)?.getAttribute('version') ?? ''
3234
this.engineName = documentElement.getElementsByTagName('engine').item(0)?.textContent ?? ''
35+
this.appid = documentElement.getAttribute('appid') ?? ''
3336
this.connection = connection
3437
}
3538
}

0 commit comments

Comments
 (0)