diff --git a/package.json b/package.json index 693ee931..4d8bc91f 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "file-url": "^1.1.0", "iconv-lite": "^0.4.13", "moment": "^2.14.1", + "mz": "^2.4.0", "semver": "^5.3.0", "url-relative": "^1.0.0", "vscode-debugadapter": "^1.11.0", @@ -98,9 +99,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.", diff --git a/src/TerminalHelper.scpt b/src/TerminalHelper.scpt deleted file mode 100644 index 9c04e241..00000000 Binary files a/src/TerminalHelper.scpt and /dev/null differ diff --git a/src/phpDebug.ts b/src/phpDebug.ts index 0543db32..1f5f1330 100644 --- a/src/phpDebug.ts +++ b/src/phpDebug.ts @@ -7,8 +7,7 @@ import * as url from 'url'; import * as childProcess from 'child_process'; import * as path from 'path'; import * as util from 'util'; -import * as fs from 'fs'; -import {Terminal} from './terminal'; +import * as fs from 'mz/fs'; import {isSameUri, convertClientPathToDebugger, convertDebuggerPathToClient} from './paths'; import * as semver from 'semver'; @@ -75,8 +74,10 @@ interface LaunchRequestArguments extends VSCodeDebugProtocol.LaunchRequestArgume runtimeArgs?: string[]; /** Optional environment variables to pass to the debuggee. The string valued properties of the 'environmentVariables' are used as key/value pairs. */ env?: { [key: string]: 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 { @@ -171,98 +172,93 @@ class PhpDebugSession extends vscode.DebugSession { this.shutdown(); } - protected async launchRequest(response: VSCodeDebugProtocol.LaunchResponse, args: LaunchRequestArguments) { - this._args = args; - /** launches the script as CLI */ - const launchScript = async () => { - // check if program exists - await new Promise((resolve, reject) => fs.access(args.program!, fs.F_OK, err => err ? reject(err) : resolve())); - const runtimeArgs = args.runtimeArgs || []; - const runtimeExecutable = args.runtimeExecutable || 'php'; - const programArgs = args.args || []; - const cwd = args.cwd || process.cwd(); - const env = args.env || process.env; - // launch in CLI mode - if (args.externalConsole) { - const script = await Terminal.launchInTerminal(cwd, [runtimeExecutable, ...runtimeArgs, args.program!, ...programArgs], env); - // we only do this for CLI mode. In normal listen mode, only a thread exited event is send. - script.on('exit', () => { - this.sendEvent(new vscode.TerminatedEvent()); - }); - } else { - const script = childProcess.spawn(runtimeExecutable, [...runtimeArgs, args.program!, ...programArgs], {cwd, env}); - // redirect output to debug console - script.stdout.on('data', (data: Buffer) => { - this.sendEvent(new vscode.OutputEvent(data + '', 'stdout')); - }); - script.stderr.on('data', (data: Buffer) => { - this.sendEvent(new vscode.OutputEvent(data + '', 'stderr')); - }); - // we only do this for CLI mode. In normal listen mode, only a thread exited event is send. - script.on('exit', () => { - this.sendEvent(new vscode.TerminatedEvent()); - }); - script.on('error', (error: Error) => { - this.sendEvent(new vscode.OutputEvent(error.message)); - }); - } - }; - /** sets up a TCP server to listen for XDebug connections */ - const createServer = () => new Promise((resolve, reject) => { - const server = this._server = net.createServer(); - server.on('connection', async (socket: net.Socket) => { - try { - // new XDebug connection - const connection = new xdebug.Connection(socket); - if (args.log) { - this.sendEvent(new vscode.OutputEvent('new connection ' + connection.id + '\n'), true); - } - this._connections.set(connection.id, connection); - this._waitingConnections.add(connection); - const disposeConnection = (error?: Error) => { - if (this._connections.has(connection.id)) { - if (args.log) { - this.sendEvent(new vscode.OutputEvent('connection ' + connection.id + ' closed\n')); - } - if (error) { - this.sendEvent(new vscode.OutputEvent(error.message)); + protected async launchRequest(response: VSCodeDebugProtocol.LaunchResponse, launchArgs: LaunchRequestArguments) { + this._args = launchArgs; + try { + if (!launchArgs.noDebug) { + await new Promise((resolve, reject) => { + const server = this._server = net.createServer(); + server.on('connection', async (socket: net.Socket) => { + try { + // new XDebug connection + const connection = new xdebug.Connection(socket); + if (launchArgs.log) { + this.sendEvent(new vscode.OutputEvent('new connection ' + connection.id + '\n'), true); } - this.sendEvent(new vscode.ThreadEvent('exited', connection.id)); - connection.close(); - this._connections.delete(connection.id); - this._waitingConnections.delete(connection); + this._connections.set(connection.id, connection); + this._waitingConnections.add(connection); + const disposeConnection = (error?: Error) => { + if (this._connections.has(connection.id)) { + if (launchArgs.log) { + this.sendEvent(new vscode.OutputEvent('connection ' + connection.id + ' closed\n')); + } + if (error) { + this.sendEvent(new vscode.OutputEvent(error.message)); + } + this.sendEvent(new vscode.ThreadEvent('exited', connection.id)); + connection.close(); + this._connections.delete(connection.id); + this._waitingConnections.delete(connection); + } + }; + connection.on('warning', (warning: string) => { + this.sendEvent(new vscode.OutputEvent(warning)); + }); + connection.on('error', disposeConnection); + connection.on('close', disposeConnection); + const initPacket = await connection.waitForInitPacket(); + this.sendEvent(new vscode.ThreadEvent('started', connection.id)); + await connection.sendFeatureSetCommand('max_depth', '5'); + // raise default of 32 + await connection.sendFeatureSetCommand('max_children', '100'); + // don't truncate long variable values + await connection.sendFeatureSetCommand('max_data', semver.lt(initPacket.engineVersion.replace(/((?:dev|alpha|beta|RC|stable)\d*)$/, '-$1'), '2.2.4') ? '10000' : '0'); + // request breakpoints from VS Code + await this.sendEvent(new vscode.InitializedEvent()); + } catch (error) { + this.sendEvent(new vscode.OutputEvent(error instanceof Error ? error.message : error)); } - }; - connection.on('warning', (warning: string) => { - this.sendEvent(new vscode.OutputEvent(warning)); }); - connection.on('error', disposeConnection); - connection.on('close', disposeConnection); - const initPacket = await connection.waitForInitPacket(); - this.sendEvent(new vscode.ThreadEvent('started', connection.id)); - await connection.sendFeatureSetCommand('max_depth', '5'); - // raise default of 32 - await connection.sendFeatureSetCommand('max_children', '100'); - // don't truncate long variable values - await connection.sendFeatureSetCommand('max_data', semver.lt(initPacket.engineVersion.replace(/((?:dev|alpha|beta|RC|stable)\d*)$/, '-$1'), '2.2.4') ? '10000' : '0'); - // request breakpoints from VS Code - await this.sendEvent(new vscode.InitializedEvent()); - } catch (error) { - this.sendEvent(new vscode.OutputEvent(error instanceof Error ? error.message : error)); - } - }); - server.on('error', (error: Error) => { - this.sendEvent(new vscode.OutputEvent(error.message)); - this.shutdown(); - }); - server.listen(args.port || 9000, (error: NodeJS.ErrnoException) => error ? reject(error) : resolve()); - }); - try { - if (!args.noDebug) { - await createServer(); + server.on('error', (error: Error) => { + this.sendEvent(new vscode.OutputEvent(error.message)); + this.shutdown(); + }); + server.listen(launchArgs.port || 9000, (error: NodeJS.ErrnoException) => error ? reject(error) : resolve()); + }); } - if (args.program) { - await launchScript(); + // When program is specified, launch it + if (launchArgs.program) { + // check if program exists + await fs.access(launchArgs.program); + const runtimeArgs = launchArgs.runtimeArgs || []; + const runtimeExecutable = launchArgs.runtimeExecutable || 'php'; + const programArgs = launchArgs.args || []; + const args = [...runtimeArgs, launchArgs.program, ...programArgs]; + const cwd = launchArgs.cwd || process.cwd(); + const env = launchArgs.env; + if (launchArgs.externalConsole || launchArgs.console === 'externalTerminal' || launchArgs.console === 'integratedTerminal') { + // If external console or integrated terminal, send a runInTerminal request + const kind: 'integrated' | 'external' = launchArgs.externalConsole || launchArgs.console === 'externalTerminal' ? 'external' : 'integrated'; + await new Promise((resolve, reject) => { + this.runInTerminalRequest({args: [runtimeExecutable, ...args], env, cwd, kind}, 5000, resolve); + }); + } else { + // Else spawn in an "internal" console + const script = childProcess.spawn(runtimeExecutable, args, {cwd, env}); + // redirect output to debug console + script.stdout.on('data', (data: Buffer) => { + this.sendEvent(new vscode.OutputEvent(data + '', 'stdout')); + }); + script.stderr.on('data', (data: Buffer) => { + this.sendEvent(new vscode.OutputEvent(data + '', 'stderr')); + }); + script.on('error', (error: Error) => { + this.sendEvent(new vscode.OutputEvent(error.message)); + }); + script.on('exit', (code: number) => { + this.sendEvent(new vscode.TerminatedEvent()); + }); + } } } catch (error) { this.sendErrorResponse(response, error); @@ -283,7 +279,13 @@ class PhpDebugSession extends vscode.DebugSession { this._checkStatus(response); } else if (response.status === 'stopped') { this._connections.delete(connection.id); - this.sendEvent(new vscode.ThreadEvent('exited', connection.id)); + if (this._args.program) { + // In CLI mode, send a TerminatedEvent + this.sendEvent(new vscode.TerminatedEvent()); + } else { + // For normal listen mode, notify that a thread exited + this.sendEvent(new vscode.ThreadEvent('exited', connection.id)); + } connection.close(); } else if (response.status === 'break') { // StoppedEvent reason can be 'step', 'breakpoint', 'exception' or 'pause' @@ -695,6 +697,9 @@ class PhpDebugSession extends vscode.DebugSession { type: property.type, variablesReference }; + if (property.hasChildren) { + variable.namedVariables = property.numberOfChildren; + } return variable; }); } diff --git a/src/terminal.ts b/src/terminal.ts deleted file mode 100644 index ac3314e3..00000000 --- a/src/terminal.ts +++ /dev/null @@ -1,254 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as Path from 'path'; -import * as FS from 'fs'; -import * as CP from 'child_process'; - -export class Terminal { - - private static _terminalService: ITerminalService; - - public static launchInTerminal(dir: string, args: string[], envVars: { [key: string]: string; }): Promise { - return this.terminalService().launchInTerminal(dir, args, envVars); - } - - public static killTree(processId: number): Promise { - return this.terminalService().killTree(processId); - } - - /* - * Is the given runtime executable on the PATH. - */ - public static isOnPath(program: string): boolean { - return this.terminalService().isOnPath(program); - } - - private static terminalService(): ITerminalService { - if (!this._terminalService) { - if (process.platform === 'win32') { - this._terminalService = new WindowsTerminalService(); - } else if (process.platform === 'darwin') { - this._terminalService = new MacTerminalService(); - } else if (process.platform === 'linux') { - this._terminalService = new LinuxTerminalService(); - } else { - this._terminalService = new DefaultTerminalService(); - } - } - return this._terminalService; - } -} - -interface ITerminalService { - launchInTerminal(dir: string, args: string[], envVars: { [key: string]: string; }): Promise; - killTree(pid: number): Promise; - isOnPath(program: string): boolean; -} - -class DefaultTerminalService implements ITerminalService { - - protected static TERMINAL_TITLE = 'VS Code Console'; - - public launchInTerminal(dir: string, args: string[], envVars: { [key: string]: string; }): Promise { - throw new Error('launchInTerminal not implemented'); - } - - public killTree(pid: number): Promise { - - // on linux and OS X we kill all direct and indirect child processes as well - - return new Promise((resolve, reject) => { - try { - const cmd = Path.join(__dirname, './terminateProcess.sh'); - const result = (CP).spawnSync(cmd, [pid.toString()]); - if (result.error) { - reject(result.error); - } else { - resolve(); - } - } catch (err) { - reject(err); - } - }); - } - - public isOnPath(program: string): boolean { - /* - var which = FS.existsSync(DefaultTerminalService.WHICH) ? DefaultTerminalService.WHICH : DefaultTerminalService.WHERE; - var cmd = Utils.format('{0} \'{1}\'', which, program); - - try { - CP.execSync(cmd); - - return process.ExitCode == 0; - } - catch (Exception) { - // ignore - } - - return false; - */ - - return true; - } -} - -class WindowsTerminalService extends DefaultTerminalService { - - private static CMD = 'cmd.exe'; - - public launchInTerminal(dir: string, args: string[], envVars: { [key: string]: string; }): Promise { - - return new Promise((resolve, reject) => { - - const title = `"${dir} - ${WindowsTerminalService.TERMINAL_TITLE}"`; - const command = `""${args.join('" "')}" & pause"`; // use '|' to only pause on non-zero exit code - - const cmdArgs = [ - '/c', 'start', title, '/wait', - 'cmd.exe', '/c', command - ]; - - // merge environment variables into a copy of the process.env - const env = extendObject(extendObject({}, process.env), envVars); - - const options: any = { - cwd: dir, - env: env, - windowsVerbatimArguments: true - }; - - const cmd = CP.spawn(WindowsTerminalService.CMD, cmdArgs, options); - cmd.on('error', reject); - - resolve(cmd); - }); - } - - public killTree(pid: number): Promise { - - // when killing a process in Windows its child processes are *not* killed but become root processes. - // Therefore we use TASKKILL.EXE - - return new Promise((resolve, reject) => { - const cmd = `taskkill /F /T /PID ${pid}`; - try { - CP.execSync(cmd); - resolve(); - } catch (err) { - reject(err); - } - }); - } -} - -class LinuxTerminalService extends DefaultTerminalService { - - private static LINUX_TERM = '/usr/bin/gnome-terminal'; // private const string LINUX_TERM = "/usr/bin/x-terminal-emulator"; - private static WAIT_MESSAGE = 'Press any key to continue...'; - - public launchInTerminal(dir: string, args: string[], envVars: { [key: string]: string; }): Promise { - - return new Promise((resolve, reject) => { - - if (!FS.existsSync(LinuxTerminalService.LINUX_TERM)) { - reject(new Error(`Cannot find '${LinuxTerminalService.LINUX_TERM}' for launching the node program. See http://go.microsoft.com/fwlink/?linkID=534832#_20002`)); - return; - } - - const bashCommand = `cd "${dir}"; "${args.join('" "')}"; echo; read -p "${LinuxTerminalService.WAIT_MESSAGE}" -n1;`; - - const termArgs = [ - '--title', `"${LinuxTerminalService.TERMINAL_TITLE}"`, - '-x', 'bash', '-c', - `\'\'${bashCommand}\'\'` // wrapping argument in two sets of ' because node is so "friendly" that it removes one set... - ]; - - // merge environment variables into a copy of the process.env - const env = extendObject(extendObject({}, process.env), envVars); - - const options: any = { - env: env - }; - - const cmd = CP.spawn(LinuxTerminalService.LINUX_TERM, termArgs, options); - cmd.on('error', reject); - cmd.on('exit', (code: number) => { - if (code === 0) { // OK - resolve(); // since cmd is not the terminal process but just a launcher, we do not pass it in the resolve to the caller - } else { - reject(new Error('exit code: ' + code)); - } - }); - }); - } -} - -class MacTerminalService extends DefaultTerminalService { - - private static OSASCRIPT = '/usr/bin/osascript'; // osascript is the AppleScript interpreter on OS X - - public launchInTerminal(dir: string, args: string[], envVars: { [key: string]: string; }): Promise { - - return new Promise((resolve, reject) => { - - // first fix the PATH so that 'runtimePath' can be found if installed with 'brew' - // Utilities.FixPathOnOSX(); - - // On OS X we do not launch the program directly but we launch an AppleScript that creates (or reuses) a Terminal window - // and then launches the program inside that window. - - const osaArgs = [ - Path.join(__dirname, './terminalHelper.scpt'), - '-t', MacTerminalService.TERMINAL_TITLE, - '-w', dir - ]; - - for (const a of args) { - osaArgs.push('-pa'); - osaArgs.push(a); - } - - if (envVars) { - for (const key in envVars) { - osaArgs.push('-e'); - osaArgs.push(key + '=' + envVars[key]); - } - } - - let stderr = ''; - const osa = CP.spawn(MacTerminalService.OSASCRIPT, osaArgs); - osa.on('error', reject); - osa.stderr.on('data', (data: Buffer) => { - stderr += data.toString(); - }); - osa.on('exit', (code: number) => { - if (code === 0) { // OK - resolve(); // since cmd is not the terminal process but just the osa tool, we do not pass it in the resolve to the caller - } else { - if (stderr) { - reject(new Error(stderr)); - } else { - reject(new Error('exit code: ' + code)); - } - } - }); - }); - } -} - -// ---- private utilities ---- - -function extendObject(objectCopy: T, object: T): T { - - for (let key in object) { - if (object.hasOwnProperty(key)) { - (objectCopy)[key] = (object)[key]; - } - } - - return objectCopy; -} diff --git a/src/terminateProcess.sh b/src/terminateProcess.sh deleted file mode 100644 index fd9e0348..00000000 --- a/src/terminateProcess.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -terminateTree() { - for cpid in $(pgrep -P $1); do - terminateTree $cpid - done - kill -9 $1 > /dev/null 2>&1 -} - -for pid in $*; do - terminateTree $pid -done diff --git a/typings.json b/typings.json index a856ffe7..45543e64 100644 --- a/typings.json +++ b/typings.json @@ -2,6 +2,7 @@ "name": "php-debug", "version": false, "dependencies": { + "mz": "registry:npm/mz#2.4.0+20160911015431", "semver": "registry:npm/semver#5.0.0+20160723033700" }, "globalDependencies": {