From ec385ca258161e9048dced6ec2f93696f9c6ec2b Mon Sep 17 00:00:00 2001 From: Damjan Cvetko Date: Tue, 23 Feb 2021 17:39:46 +0100 Subject: [PATCH 1/8] Setting port=0 will use a random port and pass that to either env or runtimeArgs. --- package.json | 61 +++++++++++++++++++++++++++++++-- src/phpDebug.ts | 24 ++++++++----- testproject/.vscode/launch.json | 18 +++++++--- tsconfig.json | 2 +- 4 files changed, 88 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 8c153f16..bbe3ddb8 100644 --- a/package.json +++ b/package.json @@ -202,7 +202,7 @@ "port": { "type": "number", "description": "Port on which to listen for Xdebug", - "default": 9000 + "default": 9003 }, "serverSourceRoot": { "type": "string", @@ -261,7 +261,7 @@ "name": "Listen for Xdebug", "type": "php", "request": "launch", - "port": 9000 + "port": 9003 }, { "name": "Launch currently open script", @@ -269,7 +269,62 @@ "request": "launch", "program": "${file}", "cwd": "${fileDirname}", - "port": 9000 + "port": 0, + "runtimeArgs": [ + "-dxdebug.start_with_request=yes" + ], + "env": { + "XDEBUG_MODE": "debug,develop", + "XDEBUG_CONFIG": "client_port=${port}" + } + } + ], + "configurationSnippets": [ + { + "label": "PHP: Listen for Xdebug", + "description": "Listening for Xdebug", + "body": { + "name": "Listen for Xdebug", + "type": "php", + "request": "launch", + "port": 9003 + } + }, + { + "label": "PHP: Launch currently open script", + "description": "Launch currently open script", + "body": { + "name": "Launch currently open script", + "type": "php", + "request": "launch", + "program": "^\"${1:\\${file\\}}\"", + "cwd": "^\"${2:\\${fileDirname\\}}\"", + "port": 0, + "runtimeArgs": [ + "-dxdebug.start_with_request=yes" + ], + "env": { + "XDEBUG_MODE": "debug,develop", + "XDEBUG_CONFIG": "^\"client_port=\\${port\\}\"" + } + } + }, + { + "label": "PHP: Launch currently open script with Xdebug 2", + "description": "Launch currently open script", + "body": { + "name": "Launch currently open script", + "type": "php", + "request": "launch", + "program": "^\"${1:\\${file\\}}\"", + "cwd": "^\"${2:\\${fileDirname\\}}\"", + "port": 0, + "runtimeArgs": [ + "-dxdebug.remote_enable=1", + "-dxdebug.remote_autostart=1", + "^\"-dxdebug.remote_port=\\${port\\}\"" + ] + } } ] } diff --git a/src/phpDebug.ts b/src/phpDebug.ts index 082ca6aa..84dae8bd 100644 --- a/src/phpDebug.ts +++ b/src/phpDebug.ts @@ -205,16 +205,18 @@ class PhpDebugSession extends vscode.DebugSession { } this._args = args /** launches the script as CLI */ - const launchScript = async () => { + const launchScript = async (port: number) => { // check if program exists await new Promise((resolve, reject) => fs.access(args.program!, fs.constants.F_OK, err => (err ? reject(err) : resolve())) ) - const runtimeArgs = args.runtimeArgs || [] + const runtimeArgs = (args.runtimeArgs || []).map(v => v.replace('${port}', port.toString())) const runtimeExecutable = args.runtimeExecutable || 'php' const programArgs = args.args || [] const cwd = args.cwd || process.cwd() - const env = args.env || process.env + const env = Object.fromEntries( + Object.entries(args.env || process.env).map(v => [v[0], v[1]?.replace('${port}', port.toString())]) + ) // launch in CLI mode if (args.externalConsole) { const script = await Terminal.launchInTerminal( @@ -252,7 +254,7 @@ class PhpDebugSession extends vscode.DebugSession { } /** sets up a TCP server to listen for Xdebug connections */ const createServer = () => - new Promise((resolve, reject) => { + new Promise((resolve, reject) => { const server = (this._server = net.createServer()) server.on('connection', async (socket: net.Socket) => { try { @@ -316,16 +318,22 @@ class PhpDebugSession extends vscode.DebugSession { }) server.on('error', (error: Error) => { this.sendEvent(new vscode.OutputEvent(util.inspect(error) + '\n')) - this.sendErrorResponse(response, error) + reject(error) + }) + server.on('listening', () => { + const port = (server.address() as net.AddressInfo).port + resolve(port) }) - server.listen(args.port || 9000, args.hostname, () => resolve()) + const listenPort = args.port === undefined ? 9000 : args.port + server.listen(listenPort, args.hostname) }) try { + let port = 0 if (!args.noDebug) { - await createServer() + port = await createServer() } if (args.program) { - await launchScript() + await launchScript(port) } } catch (error) { this.sendErrorResponse(response, error) diff --git a/testproject/.vscode/launch.json b/testproject/.vscode/launch.json index 993d88da..6b31e12a 100644 --- a/testproject/.vscode/launch.json +++ b/testproject/.vscode/launch.json @@ -1,4 +1,7 @@ { + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { @@ -6,17 +9,22 @@ "name": "Listen for Xdebug", "type": "php", "request": "launch", - "port": 9000, + "port": 9003, "log": true }, { //"debugServer": 4711, // Uncomment for debugging the adapter - "name": "Launch", - "request": "launch", + "name": "Launch currently open script", "type": "php", + "request": "launch", "program": "${file}", - "cwd": "${workspaceRoot}", - "externalConsole": false + "cwd": "${fileDirname}", + "port": 0, + "runtimeArgs": ["-dxdebug.start_with_request=yes"], + "env": { + "XDEBUG_MODE": "debug,develop", + "XDEBUG_CONFIG": "client_port=${port}" + } } ] } diff --git a/tsconfig.json b/tsconfig.json index 54b90696..c65d129e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "include": ["src/**/*"], "compilerOptions": { - "target": "es6", + "target": "es2019", "module": "commonjs", "rootDir": "src", "outDir": "out", From 37bc38fc98cde3d3793a4bd9294d43e21b87fe41 Mon Sep 17 00:00:00 2001 From: Damjan Cvetko Date: Wed, 24 Feb 2021 18:47:27 +0100 Subject: [PATCH 2/8] POC implementation of debug session separation. One DAP session listens for connection and initiates a new DAP session via custom NewDbgpConnectionEvent. The extension activate/deactivate is created to short-circuit starting on new DAP sessions and forcing them to all be in-process. --- package-lock.json | 16 +++++--- package.json | 15 +++++--- src/extension.ts | 45 +++++++++++++++++++++++ src/phpDebug.ts | 93 ++++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 154 insertions(+), 15 deletions(-) create mode 100644 src/extension.ts diff --git a/package-lock.json b/package-lock.json index 2f59a6ee..75fcc015 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1195,6 +1195,12 @@ "integrity": "sha512-4Gqya3/CmZ280CA6DvBnHmnq/vfgIqXJvzUC2S7a8kxmv/gbmt0uhJbx6m7ypWJvNn9n8DKWj5LD8wFpSxeQuA==", "dev": true }, + "@types/vscode": { + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.53.0.tgz", + "integrity": "sha512-XjFWbSPOM0EKIT2XhhYm3D3cx3nn3lshMUcWNy1eqefk+oqRuBq8unVb6BYIZqXy9lQZyeUl7eaBCOZWv+LcXQ==", + "dev": true + }, "@types/xmldom": { "version": "0.1.30", "resolved": "https://registry.npmjs.org/@types/xmldom/-/xmldom-0.1.30.tgz", @@ -2438,7 +2444,7 @@ "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "integrity": "sha1-Hsoc9xGu+BTAT2IlKjamL2yyO1c=", "dev": true, "requires": { "core-util-is": "~1.0.0", @@ -2462,7 +2468,7 @@ "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", "dev": true, "requires": { "safe-buffer": "~5.1.0" @@ -2679,7 +2685,7 @@ "human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha1-3JH8ukLk0G5Kuu0zs+ejwC9RTqA=", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true }, "husky": { @@ -3757,7 +3763,7 @@ "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha1-UoI2KaFN0AyXcPtq1H3GMQ8sH2A=", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, "merge2": { @@ -8943,7 +8949,7 @@ "strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha1-ibhS+y/L6Tb29LMYevsKEsGrWK0=", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true }, "strip-json-comments": { diff --git a/package.json b/package.json index bbe3ddb8..dbc52ef3 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@types/semver": "^7.3.4", "@types/urlencode": "^1.1.2", "@types/xmldom": "^0.1.30", + "@types/vscode": "^1.53.0", "chai": "^4.3.0", "chai-as-promised": "^7.1.1", "husky": "^4.3.8", @@ -130,6 +131,10 @@ "out/test/**/*.*" ] }, + "main": "./out/extension.js", + "activationEvents": [ + "onDebug" + ], "contributes": { "breakpoints": [ { @@ -302,12 +307,12 @@ "port": 0, "runtimeArgs": [ "-dxdebug.start_with_request=yes" - ], - "env": { - "XDEBUG_MODE": "debug,develop", - "XDEBUG_CONFIG": "^\"client_port=\\${port\\}\"" - } + ], + "env": { + "XDEBUG_MODE": "debug,develop", + "XDEBUG_CONFIG": "^\"client_port=\\${port\\}\"" } + } }, { "label": "PHP: Launch currently open script with Xdebug 2", diff --git a/src/extension.ts b/src/extension.ts new file mode 100644 index 00000000..51871982 --- /dev/null +++ b/src/extension.ts @@ -0,0 +1,45 @@ +import * as vscode from 'vscode' +import { PhpDebugSession} from './phpDebug' + +export function activate(context: vscode.ExtensionContext) { + console.log('activate') + + context.subscriptions.push(vscode.debug.onDidStartDebugSession(session => { + console.log('onDidStartDebugSession', session) + //session.customRequest('test1', { test2: "test3" }) + })) + context.subscriptions.push(vscode.debug.onDidTerminateDebugSession(session => { + console.log('onDidTerminateDebugSession', session) + })) + + context.subscriptions.push(vscode.debug.onDidReceiveDebugSessionCustomEvent(event => { + console.log('onDidReceiveDebugSessionCustomEvent', event) + if (event.event === 'newDbgpConnection') { + const config: vscode.DebugConfiguration = { + ...event.session.configuration + } + config.request = 'attach' + config.name = 'DBGp connection ' + event.body.connId + config.connId = event.body.connId + vscode.debug.startDebugging(undefined, config, event.session) + } + })) + + const factory = new InlineDebugAdapterFactory() + context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory('php', factory)); + if ('dispose' in factory) { + context.subscriptions.push(factory); + } +} + +export function deactivate() { + console.log('deactivate') +} + +class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory { + + createDebugAdapterDescriptor(_session: vscode.DebugSession): vscode.ProviderResult { + // since DebugAdapterInlineImplementation is proposed API, a cast to is required for now + return new vscode.DebugAdapterInlineImplementation(new PhpDebugSession()); + } +} \ No newline at end of file diff --git a/src/phpDebug.ts b/src/phpDebug.ts index 84dae8bd..94b5b448 100644 --- a/src/phpDebug.ts +++ b/src/phpDebug.ts @@ -46,6 +46,18 @@ function formatPropertyValue(property: xdebug.BaseProperty): string { return displayValue } +class NewDbgpConnectionEvent extends vscode.Event { + body: { + connId: number + } + constructor(connId: number) { + super('newDbgpConnection'); + this.body = { + connId: connId + } + } +} + /** * This interface should always match the schema found in the mock-debug extension manifest. */ @@ -85,9 +97,11 @@ interface LaunchRequestArguments extends VSCodeDebugProtocol.LaunchRequestArgume env?: { [key: string]: string } /** If true launch the target in an external console. */ externalConsole?: boolean + /** Internal variable for passing connections between adapter instances */ + connId?: number } -class PhpDebugSession extends vscode.DebugSession { +export class PhpDebugSession extends vscode.DebugSession { /** The arguments that were given to launchRequest */ private _args: LaunchRequestArguments @@ -143,6 +157,9 @@ class PhpDebugSession extends vscode.DebugSession { /** A flag to indicate that the adapter has already processed the stopOnEntry step request */ private _hasStoppedOnEntry = false + /** Static store of all active connections used to pass them betweeen adapter instances */ + private static _allConnections = new Map() + public constructor() { super() this.setDebuggerColumnsStartAt1(true) @@ -186,12 +203,69 @@ class PhpDebugSession extends vscode.DebugSession { this.sendResponse(response) } - protected attachRequest( + protected async attachRequest( response: VSCodeDebugProtocol.AttachResponse, - args: VSCodeDebugProtocol.AttachRequestArguments + args2: VSCodeDebugProtocol.AttachRequestArguments ) { - this.sendErrorResponse(response, new Error('Attach requests are not supported')) - this.shutdown() + const args = args2 as LaunchRequestArguments + if (!args.connId || !PhpDebugSession._allConnections.has(args.connId)) { + this.sendErrorResponse(response, new Error('Cant find connection')) + this.shutdown() + return + } + this.sendResponse(response) + + this._args = args + const connection = PhpDebugSession._allConnections.get(args.connId!)! + + 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( + 'connection ' + connection.id + ': ' + error.message + '\n' + ) + ) + } + this.sendEvent(new vscode.ContinuedEvent(connection.id, false)) + this.sendEvent(new vscode.ThreadEvent('exited', connection.id)) + connection.close() + this._connections.delete(connection.id) + this._waitingConnections.delete(connection) + } + PhpDebugSession._allConnections.delete(connection.id) + this.sendEvent(new vscode.TerminatedEvent()) + } + connection.on('warning', (warning: string) => { + this.sendEvent(new vscode.OutputEvent(warning + '\n')) + }) + connection.on('error', disposeConnection) + connection.on('close', disposeConnection) + await connection.waitForInitPacket() + + // override features from launch.json + try { + const xdebugSettings = args.xdebugSettings || {} + await Promise.all( + Object.keys(xdebugSettings).map(setting => + connection.sendFeatureSetCommand(setting, xdebugSettings[setting]) + ) + ) + } catch (error) { + throw new Error( + 'Error applying xdebugSettings: ' + (error instanceof Error ? error.message : error) + ) + } + + this.sendEvent(new vscode.ThreadEvent('started', connection.id)) + + // request breakpoints from VS Code + this.sendEvent(new vscode.InitializedEvent()) } protected async launchRequest(response: VSCodeDebugProtocol.LaunchResponse, args: LaunchRequestArguments) { @@ -263,6 +337,9 @@ class PhpDebugSession extends vscode.DebugSession { if (args.log) { this.sendEvent(new vscode.OutputEvent('new connection ' + connection.id + '\n'), true) } + PhpDebugSession._allConnections.set(connection.id, connection) + this.sendEvent(new NewDbgpConnectionEvent(connection.id)) + /* this._connections.set(connection.id, connection) this._waitingConnections.add(connection) const disposeConnection = (error?: Error) => { @@ -282,6 +359,7 @@ class PhpDebugSession extends vscode.DebugSession { connection.close() this._connections.delete(connection.id) this._waitingConnections.delete(connection) + PhpDebugSession._allConnections.delete(connection.id) } } connection.on('warning', (warning: string) => { @@ -309,6 +387,7 @@ class PhpDebugSession extends vscode.DebugSession { // request breakpoints from VS Code await this.sendEvent(new vscode.InitializedEvent()) + */ } catch (error) { this.sendEvent( new vscode.OutputEvent((error instanceof Error ? error.message : error) + '\n', 'stderr') @@ -1076,6 +1155,10 @@ class PhpDebugSession extends vscode.DebugSession { this.sendErrorResponse(response, error) } } + + protected customRequest(command: string, response: VSCodeDebugProtocol.Response, args: any): void { + console.log('test', args) + } } vscode.DebugSession.run(PhpDebugSession) From 4c93d166486a84db40f1a81d6ec61abe632dca70 Mon Sep 17 00:00:00 2001 From: Damjan Cvetko Date: Tue, 2 Mar 2021 09:21:54 +0100 Subject: [PATCH 3/8] Prettier. --- src/extension.ts | 63 ++++++++++++++++++++++++++---------------------- src/phpDebug.ts | 20 ++++++--------- 2 files changed, 41 insertions(+), 42 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 51871982..f0531c52 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,35 +1,41 @@ import * as vscode from 'vscode' -import { PhpDebugSession} from './phpDebug' +import { PhpDebugSession } from './phpDebug' export function activate(context: vscode.ExtensionContext) { console.log('activate') - context.subscriptions.push(vscode.debug.onDidStartDebugSession(session => { - console.log('onDidStartDebugSession', session) - //session.customRequest('test1', { test2: "test3" }) - })) - context.subscriptions.push(vscode.debug.onDidTerminateDebugSession(session => { - console.log('onDidTerminateDebugSession', session) - })) + context.subscriptions.push( + vscode.debug.onDidStartDebugSession(session => { + console.log('onDidStartDebugSession', session) + // session.customRequest('test1', { test2: "test3" }) + }) + ) + context.subscriptions.push( + vscode.debug.onDidTerminateDebugSession(session => { + console.log('onDidTerminateDebugSession', session) + }) + ) - context.subscriptions.push(vscode.debug.onDidReceiveDebugSessionCustomEvent(event => { - console.log('onDidReceiveDebugSessionCustomEvent', event) - if (event.event === 'newDbgpConnection') { - const config: vscode.DebugConfiguration = { - ...event.session.configuration + context.subscriptions.push( + vscode.debug.onDidReceiveDebugSessionCustomEvent(event => { + console.log('onDidReceiveDebugSessionCustomEvent', event) + if (event.event === 'newDbgpConnection') { + const config: vscode.DebugConfiguration = { + ...event.session.configuration, + } + config.request = 'attach' + config.name = 'DBGp connection ' + event.body.connId + config.connId = event.body.connId + vscode.debug.startDebugging(undefined, config, event.session) } - config.request = 'attach' - config.name = 'DBGp connection ' + event.body.connId - config.connId = event.body.connId - vscode.debug.startDebugging(undefined, config, event.session) - } - })) + }) + ) const factory = new InlineDebugAdapterFactory() - context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory('php', factory)); - if ('dispose' in factory) { - context.subscriptions.push(factory); - } + context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory('php', factory)) + if ('dispose' in factory) { + context.subscriptions.push(factory) + } } export function deactivate() { @@ -37,9 +43,8 @@ export function deactivate() { } class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory { - - createDebugAdapterDescriptor(_session: vscode.DebugSession): vscode.ProviderResult { - // since DebugAdapterInlineImplementation is proposed API, a cast to is required for now - return new vscode.DebugAdapterInlineImplementation(new PhpDebugSession()); - } -} \ No newline at end of file + createDebugAdapterDescriptor(_session: vscode.DebugSession): vscode.ProviderResult { + // since DebugAdapterInlineImplementation is proposed API, a cast to is required for now + return new vscode.DebugAdapterInlineImplementation(new PhpDebugSession()) + } +} diff --git a/src/phpDebug.ts b/src/phpDebug.ts index 94b5b448..a71c767e 100644 --- a/src/phpDebug.ts +++ b/src/phpDebug.ts @@ -51,9 +51,9 @@ class NewDbgpConnectionEvent extends vscode.Event { connId: number } constructor(connId: number) { - super('newDbgpConnection'); + super('newDbgpConnection') this.body = { - connId: connId + connId: connId, } } } @@ -207,8 +207,8 @@ export class PhpDebugSession extends vscode.DebugSession { response: VSCodeDebugProtocol.AttachResponse, args2: VSCodeDebugProtocol.AttachRequestArguments ) { - const args = args2 as LaunchRequestArguments - if (!args.connId || !PhpDebugSession._allConnections.has(args.connId)) { + const args = args2 as LaunchRequestArguments + if (!args.connId || !PhpDebugSession._allConnections.has(args.connId)) { this.sendErrorResponse(response, new Error('Cant find connection')) this.shutdown() return @@ -217,7 +217,7 @@ export class PhpDebugSession extends vscode.DebugSession { this._args = args const connection = PhpDebugSession._allConnections.get(args.connId!)! - + this._connections.set(connection.id, connection) this._waitingConnections.add(connection) const disposeConnection = (error?: Error) => { @@ -226,11 +226,7 @@ export class PhpDebugSession extends vscode.DebugSession { this.sendEvent(new vscode.OutputEvent('connection ' + connection.id + ' closed\n')) } if (error) { - this.sendEvent( - new vscode.OutputEvent( - 'connection ' + connection.id + ': ' + error.message + '\n' - ) - ) + this.sendEvent(new vscode.OutputEvent('connection ' + connection.id + ': ' + error.message + '\n')) } this.sendEvent(new vscode.ContinuedEvent(connection.id, false)) this.sendEvent(new vscode.ThreadEvent('exited', connection.id)) @@ -257,9 +253,7 @@ export class PhpDebugSession extends vscode.DebugSession { ) ) } catch (error) { - throw new Error( - 'Error applying xdebugSettings: ' + (error instanceof Error ? error.message : error) - ) + throw new Error('Error applying xdebugSettings: ' + (error instanceof Error ? error.message : error)) } this.sendEvent(new vscode.ThreadEvent('started', connection.id)) From 03faef8827f1093fc3d98a186911f5c76c122b8e Mon Sep 17 00:00:00 2001 From: Damjan Cvetko Date: Sun, 7 Mar 2021 21:10:01 +0100 Subject: [PATCH 4/8] Refactor PhpDebugSession object to switch between single instance and multi instance, depending on how it is started. When closing listen socket, do not wait for all clients to close. Some can continue to be connected and live on in different instances of DAP sessions. Refactor launch closure function into private methods for better reuse. Handle both launchRequest and attachRequest - but only as connection handover. --- src/extension.ts | 27 +---- src/phpDebug.ts | 301 ++++++++++++++++++++++------------------------- 2 files changed, 145 insertions(+), 183 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index f0531c52..2a6792d4 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,29 +1,14 @@ import * as vscode from 'vscode' -import { PhpDebugSession } from './phpDebug' +import { PhpDebugSession, StartRequestArguments } from './phpDebug' export function activate(context: vscode.ExtensionContext) { - console.log('activate') - - context.subscriptions.push( - vscode.debug.onDidStartDebugSession(session => { - console.log('onDidStartDebugSession', session) - // session.customRequest('test1', { test2: "test3" }) - }) - ) - context.subscriptions.push( - vscode.debug.onDidTerminateDebugSession(session => { - console.log('onDidTerminateDebugSession', session) - }) - ) - context.subscriptions.push( vscode.debug.onDidReceiveDebugSessionCustomEvent(event => { - console.log('onDidReceiveDebugSessionCustomEvent', event) if (event.event === 'newDbgpConnection') { - const config: vscode.DebugConfiguration = { + const config: vscode.DebugConfiguration & StartRequestArguments = { ...event.session.configuration, } - config.request = 'attach' + config.request = 'launch' config.name = 'DBGp connection ' + event.body.connId config.connId = event.body.connId vscode.debug.startDebugging(undefined, config, event.session) @@ -38,13 +23,9 @@ export function activate(context: vscode.ExtensionContext) { } } -export function deactivate() { - console.log('deactivate') -} - class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory { createDebugAdapterDescriptor(_session: vscode.DebugSession): vscode.ProviderResult { // since DebugAdapterInlineImplementation is proposed API, a cast to is required for now - return new vscode.DebugAdapterInlineImplementation(new PhpDebugSession()) + return new vscode.DebugAdapterInlineImplementation(new PhpDebugSession(true)) } } diff --git a/src/phpDebug.ts b/src/phpDebug.ts index a71c767e..98445726 100644 --- a/src/phpDebug.ts +++ b/src/phpDebug.ts @@ -61,7 +61,7 @@ class NewDbgpConnectionEvent extends vscode.Event { /** * This interface should always match the schema found in the mock-debug extension manifest. */ -interface LaunchRequestArguments extends VSCodeDebugProtocol.LaunchRequestArguments { +export interface StartRequestArguments { /** The address to bind to for listening for Xdebug connections (default: all IPv6 connections if available, else all IPv4 connections) */ hostname?: string /** The port where the adapter should listen for Xdebug connections (default: 9000) */ @@ -103,7 +103,7 @@ interface LaunchRequestArguments extends VSCodeDebugProtocol.LaunchRequestArgume export class PhpDebugSession extends vscode.DebugSession { /** The arguments that were given to launchRequest */ - private _args: LaunchRequestArguments + private _args: StartRequestArguments /** The TCP server that listens for Xdebug connections */ private _server: net.Server @@ -160,8 +160,12 @@ export class PhpDebugSession extends vscode.DebugSession { /** Static store of all active connections used to pass them betweeen adapter instances */ private static _allConnections = new Map() - public constructor() { + /** A flag indicating the adapter was initiatefd from vscode extension - controls how sessions are handeled */ + private _fromExtension = false + + public constructor(fromExtension = false) { super() + this._fromExtension = fromExtension this.setDebuggerColumnsStartAt1(true) this.setDebuggerLinesStartAt1(true) this.setDebuggerPathFormat('uri') @@ -205,11 +209,10 @@ export class PhpDebugSession extends vscode.DebugSession { protected async attachRequest( response: VSCodeDebugProtocol.AttachResponse, - args2: VSCodeDebugProtocol.AttachRequestArguments + args: StartRequestArguments & VSCodeDebugProtocol.AttachRequestArguments ) { - const args = args2 as LaunchRequestArguments if (!args.connId || !PhpDebugSession._allConnections.has(args.connId)) { - this.sendErrorResponse(response, new Error('Cant find connection')) + this.sendErrorResponse(response, new Error("Can't find connection")) this.shutdown() return } @@ -218,6 +221,137 @@ export class PhpDebugSession extends vscode.DebugSession { this._args = args const connection = PhpDebugSession._allConnections.get(args.connId!)! + await this.processConnection(connection, args) + } + + protected async launchRequest( + response: VSCodeDebugProtocol.LaunchResponse, + args: StartRequestArguments & VSCodeDebugProtocol.LaunchRequestArguments + ) { + if (args.localSourceRoot && args.serverSourceRoot) { + let pathMappings: { [index: string]: string } = {} + if (args.pathMappings) { + pathMappings = args.pathMappings + } + pathMappings[args.serverSourceRoot] = args.localSourceRoot + args.pathMappings = pathMappings + } + this._args = args + try { + if (args.connId) { + if (!PhpDebugSession._allConnections.has(args.connId)) { + this.sendErrorResponse(response, new Error("Can't find connection")) + this.shutdown() + return + } + const connection = PhpDebugSession._allConnections.get(args.connId!)! + await this.processConnection(connection, args) + } else { + let port = 0 + if (!args.noDebug) { + port = await this.createServer(args) + } + if (args.program) { + await this.launchScript(port, args) + } + } + } catch (error) { + this.sendErrorResponse(response, error) + return + } + this.sendResponse(response) + } + + /** launches the script as CLI */ + private async launchScript(port: number, args: StartRequestArguments): Promise { + // check if program exists + await new Promise((resolve, reject) => + fs.access(args.program!, fs.constants.F_OK, err => (err ? reject(err) : resolve())) + ) + const runtimeArgs = (args.runtimeArgs || []).map(v => v.replace('${port}', port.toString())) + const runtimeExecutable = args.runtimeExecutable || 'php' + const programArgs = args.args || [] + const cwd = args.cwd || process.cwd() + const env = Object.fromEntries( + Object.entries(args.env || process.env).map(v => [v[0], v[1]?.replace('${port}', port.toString())]) + ) + // launch in CLI mode + if (args.externalConsole) { + const script = await Terminal.launchInTerminal( + cwd, + [runtimeExecutable, ...runtimeArgs, args.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', () => { + 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(util.inspect(error) + '\n')) + }) + this._phpProcess = script + } + } + + /** sets up a TCP server to listen for Xdebug connections */ + private createServer(args: StartRequestArguments): Promise { + return 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) + } + + // HANDLE NEW SESSION + if (this._fromExtension) { + PhpDebugSession._allConnections.set(connection.id, connection) + this.sendEvent(new NewDbgpConnectionEvent(connection.id)) + } else { + // handle as thread + this.processConnection(connection, args) + } + } catch (error) { + this.sendEvent( + new vscode.OutputEvent((error instanceof Error ? error.message : error) + '\n', 'stderr') + ) + this.shutdown() + } + }) + server.on('error', (error: Error) => { + this.sendEvent(new vscode.OutputEvent(util.inspect(error) + '\n')) + reject(error) + }) + server.on('listening', () => { + const port = (server.address() as net.AddressInfo).port + resolve(port) + }) + const listenPort = args.port === undefined ? 9003 : args.port + server.listen(listenPort, args.hostname) + }) + } + + private async processConnection(connection: xdebug.Connection, args: StartRequestArguments): Promise { this._connections.set(connection.id, connection) this._waitingConnections.add(connection) const disposeConnection = (error?: Error) => { @@ -262,159 +396,6 @@ export class PhpDebugSession extends vscode.DebugSession { this.sendEvent(new vscode.InitializedEvent()) } - protected async launchRequest(response: VSCodeDebugProtocol.LaunchResponse, args: LaunchRequestArguments) { - if (args.localSourceRoot && args.serverSourceRoot) { - let pathMappings: { [index: string]: string } = {} - if (args.pathMappings) { - pathMappings = args.pathMappings - } - pathMappings[args.serverSourceRoot] = args.localSourceRoot - args.pathMappings = pathMappings - } - this._args = args - /** launches the script as CLI */ - const launchScript = async (port: number) => { - // check if program exists - await new Promise((resolve, reject) => - fs.access(args.program!, fs.constants.F_OK, err => (err ? reject(err) : resolve())) - ) - const runtimeArgs = (args.runtimeArgs || []).map(v => v.replace('${port}', port.toString())) - const runtimeExecutable = args.runtimeExecutable || 'php' - const programArgs = args.args || [] - const cwd = args.cwd || process.cwd() - const env = Object.fromEntries( - Object.entries(args.env || process.env).map(v => [v[0], v[1]?.replace('${port}', port.toString())]) - ) - // launch in CLI mode - if (args.externalConsole) { - const script = await Terminal.launchInTerminal( - cwd, - [runtimeExecutable, ...runtimeArgs, args.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', () => { - 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(util.inspect(error) + '\n')) - }) - this._phpProcess = script - } - } - /** 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) - } - PhpDebugSession._allConnections.set(connection.id, connection) - this.sendEvent(new NewDbgpConnectionEvent(connection.id)) - /* - 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( - 'connection ' + connection.id + ': ' + error.message + '\n' - ) - ) - } - this.sendEvent(new vscode.ContinuedEvent(connection.id, false)) - this.sendEvent(new vscode.ThreadEvent('exited', connection.id)) - connection.close() - this._connections.delete(connection.id) - this._waitingConnections.delete(connection) - PhpDebugSession._allConnections.delete(connection.id) - } - } - connection.on('warning', (warning: string) => { - this.sendEvent(new vscode.OutputEvent(warning + '\n')) - }) - connection.on('error', disposeConnection) - connection.on('close', disposeConnection) - await connection.waitForInitPacket() - - // override features from launch.json - try { - const xdebugSettings = args.xdebugSettings || {} - await Promise.all( - Object.keys(xdebugSettings).map(setting => - connection.sendFeatureSetCommand(setting, xdebugSettings[setting]) - ) - ) - } catch (error) { - throw new Error( - 'Error applying xdebugSettings: ' + (error instanceof Error ? error.message : error) - ) - } - - this.sendEvent(new vscode.ThreadEvent('started', connection.id)) - - // request breakpoints from VS Code - await this.sendEvent(new vscode.InitializedEvent()) - */ - } catch (error) { - this.sendEvent( - new vscode.OutputEvent((error instanceof Error ? error.message : error) + '\n', 'stderr') - ) - this.shutdown() - } - }) - server.on('error', (error: Error) => { - this.sendEvent(new vscode.OutputEvent(util.inspect(error) + '\n')) - reject(error) - }) - server.on('listening', () => { - const port = (server.address() as net.AddressInfo).port - resolve(port) - }) - const listenPort = args.port === undefined ? 9000 : args.port - server.listen(listenPort, args.hostname) - }) - try { - let port = 0 - if (!args.noDebug) { - port = await createServer() - } - if (args.program) { - await launchScript(port) - } - } catch (error) { - this.sendErrorResponse(response, error) - return - } - this.sendResponse(response) - } - /** * Checks the status of a StatusResponse and notifies VS Code accordingly * @param {xdebug.StatusResponse} response @@ -1103,7 +1084,7 @@ export class PhpDebugSession extends vscode.DebugSession { ) // If listening for connections, close server if (this._server) { - await new Promise(resolve => this._server.close(resolve)) + this._server.close() } // If launched as CLI, kill process if (this._phpProcess) { From a6a6b9feb920edf9914e9e12f6c1aa861e32b920 Mon Sep 17 00:00:00 2001 From: Damjan Cvetko Date: Sun, 7 Mar 2021 21:10:38 +0100 Subject: [PATCH 5/8] Upgrade main launch.json. --- .vscode/launch.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index dca4a80b..d9356757 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,7 +12,7 @@ "NODE_ENV": "development" }, "sourceMaps": true, - "outDir": "${workspaceRoot}/out" + "outFiles": ["${workspaceRoot}/out/**/*.js"] }, { "name": "Launch Extension", @@ -21,7 +21,7 @@ "runtimeExecutable": "${execPath}", "args": ["--extensionDevelopmentPath=${workspaceRoot}"], "sourceMaps": true, - "outDir": "${workspaceRoot}/out" + "outFiles": ["${workspaceRoot}/out/**/*.js"] }, { "name": "Mocha", @@ -31,7 +31,7 @@ "args": ["out/test", "--no-timeouts", "--colors"], "cwd": "${workspaceRoot}", "sourceMaps": true, - "outDir": "${workspaceRoot}/out" + "outFiles": ["${workspaceRoot}/out/**/*.js"] } ] } From 6da5d06a3c02e276fac8a6090502cfd9ec4319ef Mon Sep 17 00:00:00 2001 From: Damjan Cvetko Date: Sun, 7 Mar 2021 21:12:19 +0100 Subject: [PATCH 6/8] Revert default port to 9000. --- src/phpDebug.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/phpDebug.ts b/src/phpDebug.ts index 98445726..692511d8 100644 --- a/src/phpDebug.ts +++ b/src/phpDebug.ts @@ -346,7 +346,7 @@ export class PhpDebugSession extends vscode.DebugSession { const port = (server.address() as net.AddressInfo).port resolve(port) }) - const listenPort = args.port === undefined ? 9003 : args.port + const listenPort = args.port === undefined ? 9000 : args.port server.listen(listenPort, args.hostname) }) } From 2b3ad92ef8c895e5762687616bc8c6aac8a96fc8 Mon Sep 17 00:00:00 2001 From: Damjan Cvetko Date: Wed, 10 Mar 2021 23:55:07 +0100 Subject: [PATCH 7/8] Do not add parent session to new Xdebug connections. If we stop parent one, all others loose their UI. --- src/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index 2a6792d4..2ca283aa 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -11,7 +11,7 @@ export function activate(context: vscode.ExtensionContext) { config.request = 'launch' config.name = 'DBGp connection ' + event.body.connId config.connId = event.body.connId - vscode.debug.startDebugging(undefined, config, event.session) + vscode.debug.startDebugging(undefined, config) } }) ) From a1c679fd8a99570922124ae554ee27740ac408ec Mon Sep 17 00:00:00 2001 From: Damjan Cvetko Date: Mon, 3 May 2021 23:35:44 +0200 Subject: [PATCH 8/8] Do not terminate if we are in thread mode. TerminateEvent causes VSC to send a disconnectRequest this this session and that causes all threads to disconnect. --- src/extension.ts | 4 +++- src/phpDebug.ts | 11 ++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 2ca283aa..579a7e99 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -26,6 +26,8 @@ export function activate(context: vscode.ExtensionContext) { class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory { createDebugAdapterDescriptor(_session: vscode.DebugSession): vscode.ProviderResult { // since DebugAdapterInlineImplementation is proposed API, a cast to is required for now - return new vscode.DebugAdapterInlineImplementation(new PhpDebugSession(true)) + const dap = new PhpDebugSession() + dap.setFromExtension(true) + return new vscode.DebugAdapterInlineImplementation(dap) } } diff --git a/src/phpDebug.ts b/src/phpDebug.ts index 692511d8..362e1c3f 100644 --- a/src/phpDebug.ts +++ b/src/phpDebug.ts @@ -163,14 +163,17 @@ export class PhpDebugSession extends vscode.DebugSession { /** A flag indicating the adapter was initiatefd from vscode extension - controls how sessions are handeled */ private _fromExtension = false - public constructor(fromExtension = false) { + public constructor() { super() - this._fromExtension = fromExtension this.setDebuggerColumnsStartAt1(true) this.setDebuggerLinesStartAt1(true) this.setDebuggerPathFormat('uri') } + public setFromExtension(fromExtension = false): void { + this._fromExtension = fromExtension + } + protected initializeRequest( response: VSCodeDebugProtocol.InitializeResponse, args: VSCodeDebugProtocol.InitializeRequestArguments @@ -369,7 +372,9 @@ export class PhpDebugSession extends vscode.DebugSession { this._waitingConnections.delete(connection) } PhpDebugSession._allConnections.delete(connection.id) - this.sendEvent(new vscode.TerminatedEvent()) + if (this._fromExtension) { + this.sendEvent(new vscode.TerminatedEvent()) + } } connection.on('warning', (warning: string) => { this.sendEvent(new vscode.OutputEvent(warning + '\n'))