Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for runInTerminal request #966

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).

## [1.35.0]

- Support for console option with internalConsole, integratedTerminal and externalTerminal options.

## [1.34.0]

- Partial support for virtual workspaces
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ Options specific to CLI debugging:
- `cwd`: The current working directory to use when launching the script
- `runtimeExecutable`: Path to the PHP binary used for launching the script. By default the one on the PATH.
- `runtimeArgs`: Additional arguments to pass to the PHP binary
- `externalConsole`: Launches the script in an external console window instead of the debug console (default: `false`)
- `externalConsole`: _DEPRECATED_ Launches the script in an external console window instead of the debug console (default: `false`)
- `console`: What kind of console to use for running the script. Possible values are: `internalConsole` (default), `integratedTerminal` or `externalTerminal`.
- `env`: Environment variables to pass to the script
- `envFile`: Optional path to a file containing environment variable definitions

Expand Down
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,14 @@
},
"externalConsole": {
"type": "boolean",
"description": "Launch debug target in external console.",
"description": "DEPRECATED: Launch debug target in external console.",
"default": false
},
"console": {
"enum": ["internalConsole", "integratedTerminal", "externalTerminal"],
"description": "Where to launch the debug target: internal console, integrated terminal, or external terminal",
"default": "internalConsole"
},
"args": {
"type": "array",
"description": "Command line arguments passed to the program.",
Expand Down
2 changes: 1 addition & 1 deletion src/cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Transport, DbgpConnection, ENCODING } from './dbgp'
import * as tls from 'tls'
import * as iconv from 'iconv-lite'
import * as xdebug from './xdebugConnection'
import { EventEmitter } from 'stream'
import { EventEmitter } from 'events'

export declare interface XdebugCloudConnection {
on(event: 'error', listener: (error: Error) => void): this
Expand Down
62 changes: 51 additions & 11 deletions src/phpDebug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import * as path from 'path'
import * as util from 'util'
import * as fs from 'fs'
import { Terminal } from './terminal'
import { Terminal, IProgram, ProgramPidWrapper, isProcessAlive } from './terminal'
import { convertClientPathToDebugger, convertDebuggerPathToClient, isPositiveMatchInGlobs } from './paths'
import minimatch from 'minimatch'
import { BreakpointManager, BreakpointAdapter } from './breakpoints'
Expand Down Expand Up @@ -115,19 +115,24 @@
env?: { [key: string]: string }
/** Absolute path to a file containing environment variable definitions. */
envFile?: string
/** If true launch the target in an external console. */
/** DEPRECATED: If true launch the target in an external console. */
externalConsole?: boolean
/** Where to launch the debug target: internal console, integrated terminal, or external terminal. */
console?: 'internalConsole' | 'integratedTerminal' | 'externalTerminal'
}

class PhpDebugSession extends vscode.DebugSession {
/** The arguments that were given to initializeRequest */
private _initializeArgs: VSCodeDebugProtocol.InitializeRequestArguments

/** The arguments that were given to launchRequest */
private _args: LaunchRequestArguments

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

/** The child process of the launched PHP script, if launched by the debug adapter */
private _phpProcess?: childProcess.ChildProcess
private _phpProcess?: IProgram

/**
* A map from VS Code thread IDs to Xdebug Connections.
Expand Down Expand Up @@ -211,6 +216,7 @@
response: VSCodeDebugProtocol.InitializeResponse,
args: VSCodeDebugProtocol.InitializeRequestArguments
): void {
this._initializeArgs = args
response.body = {
supportsConfigurationDoneRequest: true,
supportsEvaluateForHovers: true,
Expand Down Expand Up @@ -287,29 +293,48 @@
const program = args.program ? [args.program] : []
const cwd = args.cwd || process.cwd()
const env = Object.fromEntries(
Object.entries({ ...process.env, ...getConfiguredEnvironment(args) }).map(v => [
Object.entries(getConfiguredEnvironment(args)).map(v => [

Check warning on line 296 in src/phpDebug.ts

View check run for this annotation

Codecov / codecov/patch

src/phpDebug.ts#L296

Added line #L296 was not covered by tests
v[0],
v[1]?.replace('${port}', port.toString()),
])
)
// launch in CLI mode
if (args.externalConsole) {
const script = await Terminal.launchInTerminal(
cwd,
[runtimeExecutable, ...runtimeArgs, ...program, ...programArgs],
env
)
if (args.externalConsole || args.console == 'integratedTerminal' || args.console == 'externalTerminal') {
let script: IProgram | undefined
if (this._initializeArgs.supportsRunInTerminalRequest) {

Check warning on line 304 in src/phpDebug.ts

View check run for this annotation

Codecov / codecov/patch

src/phpDebug.ts#L304

Added line #L304 was not covered by tests
const kind: 'integrated' | 'external' =
args.externalConsole || args.console === 'externalTerminal' ? 'external' : 'integrated'
const ritr = await new Promise<VSCodeDebugProtocol.RunInTerminalResponse>((resolve, reject) => {
this.runInTerminalRequest(

Check warning on line 308 in src/phpDebug.ts

View check run for this annotation

Codecov / codecov/patch

src/phpDebug.ts#L306-L308

Added lines #L306 - L308 were not covered by tests
{ args: [runtimeExecutable, ...runtimeArgs, ...program, ...programArgs], env, cwd, kind },
5000,
resolve
)
})
script =
ritr.success && ritr.body.shellProcessId
? new ProgramPidWrapper(ritr.body.shellProcessId)
: undefined

Check warning on line 317 in src/phpDebug.ts

View check run for this annotation

Codecov / codecov/patch

src/phpDebug.ts#L314-L317

Added lines #L314 - L317 were not covered by tests
} else {
script = await Terminal.launchInTerminal(

Check warning on line 319 in src/phpDebug.ts

View check run for this annotation

Codecov / codecov/patch

src/phpDebug.ts#L319

Added line #L319 was not covered by tests
cwd,
[runtimeExecutable, ...runtimeArgs, ...program, ...programArgs],
env
)
}

if (script) {
// we only do this for CLI mode. In normal listen mode, only a thread exited event is send.
script.on('exit', (code: number | null) => {
this.sendEvent(new vscode.ExitedEvent(code ?? 0))
this.sendEvent(new vscode.TerminatedEvent())
})
}
// this._phpProcess = script
} else {
const script = childProcess.spawn(runtimeExecutable, [...runtimeArgs, ...program, ...programArgs], {
cwd,
env,
env: { ...process.env, ...env },
})
// redirect output to debug console
script.stdout.on('data', (data: Buffer) => {
Expand Down Expand Up @@ -489,6 +514,21 @@
private async initializeConnection(connection: xdebug.Connection): Promise<void> {
const initPacket = await connection.waitForInitPacket()

// track the process, if we asked the IDE to spawn it
if (
!this._phpProcess &&
(this._args.program || this._args.runtimeArgs) &&
initPacket.appid &&
isProcessAlive(parseInt(initPacket.appid))

Check warning on line 522 in src/phpDebug.ts

View check run for this annotation

Codecov / codecov/patch

src/phpDebug.ts#L520-L522

Added lines #L520 - L522 were not covered by tests
) {
this._phpProcess = new ProgramPidWrapper(parseInt(initPacket.appid))

Check warning on line 524 in src/phpDebug.ts

View check run for this annotation

Codecov / codecov/patch

src/phpDebug.ts#L524

Added line #L524 was not covered by tests
// we only do this for CLI mode. In normal listen mode, only a thread exited event is send.
this._phpProcess.on('exit', (code: number | null) => {
this.sendEvent(new vscode.ExitedEvent(code ?? 0))
this.sendEvent(new vscode.TerminatedEvent())

Check warning on line 528 in src/phpDebug.ts

View check run for this annotation

Codecov / codecov/patch

src/phpDebug.ts#L526-L528

Added lines #L526 - L528 were not covered by tests
})
}

// check if this connection should be skipped
if (
this._args.skipEntryPaths &&
Expand Down
64 changes: 64 additions & 0 deletions src/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import * as Path from 'path'
import * as FS from 'fs'
import * as CP from 'child_process'
import { EventEmitter } from 'events'

export class Terminal {
private static _terminalService: ITerminalService
Expand Down Expand Up @@ -45,6 +46,69 @@
}
}

export interface IProgram {
readonly pid?: number | undefined
kill(signal?: NodeJS.Signals | number): boolean
on(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this
}

export class ProgramPidWrapper extends EventEmitter implements IProgram {
/**
* How often to check and see if the process exited.
*/
private static readonly terminationPollInterval = 1000

/**
* How often to check and see if the process exited after we send a close signal.
*/
private static readonly killConfirmInterval = 200

private loop?: { timer: NodeJS.Timeout; processId: number }

constructor(readonly pid: number) {
super()

Check warning on line 69 in src/terminal.ts

View check run for this annotation

Codecov / codecov/patch

src/terminal.ts#L68-L69

Added lines #L68 - L69 were not covered by tests

if (pid) {
this.startPollLoop(pid)

Check warning on line 72 in src/terminal.ts

View check run for this annotation

Codecov / codecov/patch

src/terminal.ts#L71-L72

Added lines #L71 - L72 were not covered by tests
}
}

kill(signal?: number | NodeJS.Signals | undefined): boolean {
this.startPollLoop(this.pid, ProgramPidWrapper.killConfirmInterval)
Terminal.killTree(this.pid).catch(err => {

Check warning on line 78 in src/terminal.ts

View check run for this annotation

Codecov / codecov/patch

src/terminal.ts#L76-L78

Added lines #L76 - L78 were not covered by tests
// ignore
})
return true

Check warning on line 81 in src/terminal.ts

View check run for this annotation

Codecov / codecov/patch

src/terminal.ts#L81

Added line #L81 was not covered by tests
}

private startPollLoop(processId: number, interval = ProgramPidWrapper.terminationPollInterval) {
if (this.loop) {
clearInterval(this.loop.timer)

Check warning on line 86 in src/terminal.ts

View check run for this annotation

Codecov / codecov/patch

src/terminal.ts#L84-L86

Added lines #L84 - L86 were not covered by tests
}

const loop = {

Check warning on line 89 in src/terminal.ts

View check run for this annotation

Codecov / codecov/patch

src/terminal.ts#L89

Added line #L89 was not covered by tests
processId,
timer: setInterval(() => {
if (!isProcessAlive(processId)) {
clearInterval(loop.timer)
this.emit('exit')

Check warning on line 94 in src/terminal.ts

View check run for this annotation

Codecov / codecov/patch

src/terminal.ts#L91-L94

Added lines #L91 - L94 were not covered by tests
}
}, interval),
}

this.loop = loop

Check warning on line 99 in src/terminal.ts

View check run for this annotation

Codecov / codecov/patch

src/terminal.ts#L99

Added line #L99 was not covered by tests
}
}
export function isProcessAlive(processId: number) {
try {

Check warning on line 103 in src/terminal.ts

View check run for this annotation

Codecov / codecov/patch

src/terminal.ts#L103

Added line #L103 was not covered by tests
// kill with signal=0 just test for whether the proc is alive. It throws if not.
process.kill(processId, 0)
return true

Check warning on line 106 in src/terminal.ts

View check run for this annotation

Codecov / codecov/patch

src/terminal.ts#L105-L106

Added lines #L105 - L106 were not covered by tests
} catch {
return false

Check warning on line 108 in src/terminal.ts

View check run for this annotation

Codecov / codecov/patch

src/terminal.ts#L108

Added line #L108 was not covered by tests
}
}

interface ITerminalService {
launchInTerminal(
dir: string,
Expand Down
3 changes: 3 additions & 0 deletions src/xdebugConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export class InitPacket {
engineVersion: string
/** the name of the engine */
engineName: string
/** the internal PID */
appid: string
/**
* @param {XMLDocument} document - An XML document to read from
* @param {Connection} connection
Expand All @@ -30,6 +32,7 @@ export class InitPacket {
this.ideKey = documentElement.getAttribute('idekey')!
this.engineVersion = documentElement.getElementsByTagName('engine').item(0)?.getAttribute('version') ?? ''
this.engineName = documentElement.getElementsByTagName('engine').item(0)?.textContent ?? ''
this.appid = documentElement.getAttribute('appid') ?? ''
this.connection = connection
}
}
Expand Down
Loading