From 16804b578082e5715feadc04e0845c73e58ee268 Mon Sep 17 00:00:00 2001 From: Damjan Cvetko Date: Wed, 6 Aug 2025 11:09:02 +0200 Subject: [PATCH 1/7] New implementation of Exception Info with support for virtual $__EXCEPTION property --- src/phpDebug.ts | 98 ++++++++++++++++++++++++++++++++++++----- src/xdebugConnection.ts | 10 ++++- src/xdebugUtils.ts | 10 +++++ 3 files changed, 106 insertions(+), 12 deletions(-) create mode 100644 src/xdebugUtils.ts diff --git a/src/phpDebug.ts b/src/phpDebug.ts index b28669a6..69548781 100644 --- a/src/phpDebug.ts +++ b/src/phpDebug.ts @@ -20,6 +20,7 @@ import { getConfiguredEnvironment } from './envfile' import { XdebugCloudConnection } from './cloud' import { shouldIgnoreException } from './ignore' import { varExportProperty } from './varExport' +import { supportedEngine } from './xdebugUtils' if (process.env['VSCODE_NLS_CONFIG']) { try { @@ -249,6 +250,7 @@ class PhpDebugSession extends vscode.DebugSession { supportTerminateDebuggee: true, supportsDelayedStackTraceLoading: false, supportsClipboardContext: true, + supportsExceptionInfoRequest: true, } this.sendResponse(response) } @@ -522,29 +524,24 @@ class PhpDebugSession extends vscode.DebugSession { // support for breakpoints let feat: xdebug.FeatureGetResponse - const supportedEngine = - initPacket.engineName === 'Xdebug' && - semver.valid(initPacket.engineVersion, { loose: true }) && - semver.gte(initPacket.engineVersion, '3.0.0', { loose: true }) - const supportedEngine32 = - initPacket.engineName === 'Xdebug' && - semver.valid(initPacket.engineVersion, { loose: true }) && - semver.gte(initPacket.engineVersion, '3.2.0', { loose: true }) + const supportedEngine30 = supportedEngine(initPacket, '3.0.0') + const supportedEngine32 = supportedEngine(initPacket, '3.2.0') + const supportedEngine35 = supportedEngine(initPacket, '3.5.0') if ( - supportedEngine || + supportedEngine30 || ((feat = await connection.sendFeatureGetCommand('resolved_breakpoints')) && feat.supported === '1') ) { await connection.sendFeatureSetCommand('resolved_breakpoints', '1') } if ( - supportedEngine || + supportedEngine30 || ((feat = await connection.sendFeatureGetCommand('notify_ok')) && feat.supported === '1') ) { await connection.sendFeatureSetCommand('notify_ok', '1') connection.on('notify_user', (notify: xdebug.UserNotify) => this.handleUserNotify(notify, connection)) } if ( - supportedEngine || + supportedEngine30 || ((feat = await connection.sendFeatureGetCommand('extended_properties')) && feat.supported === '1') ) { await connection.sendFeatureSetCommand('extended_properties', '1') @@ -556,6 +553,12 @@ class PhpDebugSession extends vscode.DebugSession { ) { await connection.sendFeatureSetCommand('breakpoint_include_return_value', '1') } + if ( + supportedEngine35 || + ((feat = await connection.sendFeatureGetCommand('virtual_exception_value')) && feat.supported === '1') + ) { + await connection.sendFeatureSetCommand('virtual_exception_value', '1') + } // override features from launch.json try { @@ -1529,6 +1532,79 @@ class PhpDebugSession extends vscode.DebugSession { this.sendResponse(response) } } + + protected async exceptionInfoRequest( + response: VSCodeDebugProtocol.ExceptionInfoResponse, + args: VSCodeDebugProtocol.ExceptionInfoArguments + ): Promise { + const connection = this._connections.get(args.threadId) + if (!connection) { + throw new Error('Unknown thread ID') + } + + const status = this._statuses.get(connection)! + + let ex: VSCodeDebugProtocol.ExceptionDetails | undefined + + if (connection.featureSet('virtual_exception_value')) { + let old_max_depth: number = 3 + try { + const { stack } = await connection.sendStackGetCommand() // CACHE? + + if (stack.length > 0) { + const ctx = await stack[0].getContexts() // CACHE + old_max_depth = connection.featureSet('max_depth') ?? 1 + if (old_max_depth < 3) { + await connection.sendFeatureSetCommand('max_depth', 3) + } + + const res = await connection.sendPropertyGetNameCommand('$__EXCEPTION', ctx[0]) + + const s = res.property.children.find( + p => p.name === 'trace' || p.name === '*Exception*trace' || p.name === '*Error*trace' + ) + + const at = `Created at ${res.property.children.find(p => p.name == 'file')?.value ?? '**UNKNOWN**'}:${ + res.property.children.find(p => p.name == 'line')?.value ?? '?' + }\n` + const st = s?.children + .map( + (t, i) => + `at ${ + t.children.find(p => p.name == 'class')?.value ?? ''}${ + t.children.find(p => p.name == 'type')?.value ?? '' + }${t.children.find(p => p.name == 'function')?.value ?? '?'}() (${ + t.children.find(p => p.name == 'file')?.value ?? '**UNKNOWN**'}:${ + t.children.find(p => p.name == 'line')?.value ?? '?' + })` + ) + .join('\n') + ex = { + message: res.property.children.find(p => p.name === 'message')?.value ?? undefined, + typeName: res.property.class, + fullTypeName: res.property.class, + evaluateName: '$__EXCEPTION', + stackTrace: `${at}${st}`, + // TODO process inner/previous exception + } + } + } catch { + // Probably $__EXCEPTION not present + } finally { + if (old_max_depth < 3) { + await connection.sendFeatureSetCommand('max_depth', old_max_depth) + } + } + } + + response.body = { + exceptionId: status.exception.name, + breakMode: 'always', + description: status.exception.message, + details: ex, + } + this.sendResponse(response) + } } vscode.DebugSession.run(PhpDebugSession) diff --git a/src/xdebugConnection.ts b/src/xdebugConnection.ts index e8253e80..30069111 100644 --- a/src/xdebugConnection.ts +++ b/src/xdebugConnection.ts @@ -845,6 +845,12 @@ export class Connection extends DbgpConnection { return this._pendingExecuteCommand } + private _featureSet = new Map() + + public featureSet(feature:string): number|string|undefined { + return this._featureSet.get(feature) + } + /** Constructs a new connection that uses the given socket to communicate with Xdebug. */ constructor(socket: Transport) { super(socket) @@ -999,7 +1005,9 @@ export class Connection extends DbgpConnection { * - notify_ok */ public async sendFeatureSetCommand(feature: string, value: string | number): Promise { - return new FeatureSetResponse(await this._enqueueCommand('feature_set', `-n ${feature} -v ${value}`), this) + const res = new FeatureSetResponse(await this._enqueueCommand('feature_set', `-n ${feature} -v ${value}`), this) + this._featureSet.set(feature, value) + return res } // ---------------------------- breakpoints ------------------------------------ diff --git a/src/xdebugUtils.ts b/src/xdebugUtils.ts new file mode 100644 index 00000000..c46e37c7 --- /dev/null +++ b/src/xdebugUtils.ts @@ -0,0 +1,10 @@ +import * as semver from 'semver' +import * as xdebug from './xdebugConnection' + +export function supportedEngine(initPacket: xdebug.InitPacket, version: string): boolean { + return ( + initPacket.engineName === 'Xdebug' && + semver.valid(initPacket.engineVersion.replace('-dev', ''), { loose: true }) !== null && + semver.gte(initPacket.engineVersion.replace('-dev', ''), version, { loose: true }) + ) +} From 2ed20bf6ed03dc6f233e41f33b65067403aaf814 Mon Sep 17 00:00:00 2001 From: Damjan Cvetko Date: Wed, 6 Aug 2025 11:12:30 +0200 Subject: [PATCH 2/7] eslint and prettier --- src/phpDebug.ts | 32 +++++++++++++++----------------- src/xdebugConnection.ts | 4 ++-- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src/phpDebug.ts b/src/phpDebug.ts index 69548781..617680be 100644 --- a/src/phpDebug.ts +++ b/src/phpDebug.ts @@ -1547,7 +1547,7 @@ class PhpDebugSession extends vscode.DebugSession { let ex: VSCodeDebugProtocol.ExceptionDetails | undefined if (connection.featureSet('virtual_exception_value')) { - let old_max_depth: number = 3 + let old_max_depth = 3 try { const { stack } = await connection.sendStackGetCommand() // CACHE? @@ -1556,7 +1556,7 @@ class PhpDebugSession extends vscode.DebugSession { old_max_depth = connection.featureSet('max_depth') ?? 1 if (old_max_depth < 3) { await connection.sendFeatureSetCommand('max_depth', 3) - } + } const res = await connection.sendPropertyGetNameCommand('$__EXCEPTION', ctx[0]) @@ -1564,27 +1564,25 @@ class PhpDebugSession extends vscode.DebugSession { p => p.name === 'trace' || p.name === '*Exception*trace' || p.name === '*Error*trace' ) - const at = `Created at ${res.property.children.find(p => p.name == 'file')?.value ?? '**UNKNOWN**'}:${ - res.property.children.find(p => p.name == 'line')?.value ?? '?' - }\n` + const at = `Created at ${ + res.property.children.find(p => p.name == 'file')?.value ?? '**UNKNOWN**' + }:${res.property.children.find(p => p.name == 'line')?.value ?? '?'}\n` const st = s?.children - .map( - (t, i) => - `at ${ - t.children.find(p => p.name == 'class')?.value ?? ''}${ - t.children.find(p => p.name == 'type')?.value ?? '' - }${t.children.find(p => p.name == 'function')?.value ?? '?'}() (${ - t.children.find(p => p.name == 'file')?.value ?? '**UNKNOWN**'}:${ - t.children.find(p => p.name == 'line')?.value ?? '?' - })` - ) - .join('\n') + .map( + (t, i) => + `at ${t.children.find(p => p.name == 'class')?.value ?? ''}${ + t.children.find(p => p.name == 'type')?.value ?? '' + }${t.children.find(p => p.name == 'function')?.value ?? '?'}() (${ + t.children.find(p => p.name == 'file')?.value ?? '**UNKNOWN**' + }:${t.children.find(p => p.name == 'line')?.value ?? '?'})` + ) + .join('\n') ex = { message: res.property.children.find(p => p.name === 'message')?.value ?? undefined, typeName: res.property.class, fullTypeName: res.property.class, evaluateName: '$__EXCEPTION', - stackTrace: `${at}${st}`, + stackTrace: `${at}${st ?? ''}`, // TODO process inner/previous exception } } diff --git a/src/xdebugConnection.ts b/src/xdebugConnection.ts index 30069111..9bb04864 100644 --- a/src/xdebugConnection.ts +++ b/src/xdebugConnection.ts @@ -845,9 +845,9 @@ export class Connection extends DbgpConnection { return this._pendingExecuteCommand } - private _featureSet = new Map() + private _featureSet = new Map() - public featureSet(feature:string): number|string|undefined { + public featureSet(feature: string): number | string | undefined { return this._featureSet.get(feature) } From 34e616ace9995884708717d6a2c73d1541c92593 Mon Sep 17 00:00:00 2001 From: Damjan Cvetko Date: Wed, 6 Aug 2025 11:17:06 +0200 Subject: [PATCH 3/7] Remove semver --- src/phpDebug.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/phpDebug.ts b/src/phpDebug.ts index 617680be..c5dd9efb 100644 --- a/src/phpDebug.ts +++ b/src/phpDebug.ts @@ -12,7 +12,6 @@ import { Terminal } from './terminal' import { convertClientPathToDebugger, convertDebuggerPathToClient, isPositiveMatchInGlobs } from './paths' import { minimatch } from 'minimatch' import { BreakpointManager, BreakpointAdapter } from './breakpoints' -import * as semver from 'semver' import { LogPointManager } from './logpoint' import { ProxyConnect } from './proxyConnect' import { randomUUID } from 'crypto' From 3fc2a69d6fd47e5fb08983bf02c9de46e631f697 Mon Sep 17 00:00:00 2001 From: Damjan Cvetko Date: Wed, 6 Aug 2025 19:29:54 +0200 Subject: [PATCH 4/7] Update build to include CODECOV_TOKEN --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 220ba636..41977c83 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -61,6 +61,8 @@ jobs: run: ./node_modules/.bin/nyc report --reporter=json - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} release: runs-on: ubuntu-22.04 needs: test From 35740f78627e75092b5208e20ec68afdce165f17 Mon Sep 17 00:00:00 2001 From: Damjan Cvetko Date: Wed, 6 Aug 2025 20:38:16 +0200 Subject: [PATCH 5/7] Exception info basic test --- src/test/adapter.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/test/adapter.ts b/src/test/adapter.ts index 250db813..d09de4f5 100644 --- a/src/test/adapter.ts +++ b/src/test/adapter.ts @@ -946,4 +946,21 @@ describe('PHP Debug Adapter', () => { await client.assertOutput('console', 'skipping entry point') }) }) + + describe('exception info', () => { + it('should show exception info', async () => { + const program = path.join(TEST_PROJECT, 'error.php') + + await client.launch({ program }) + await client.setExceptionBreakpointsRequest({ filters: ['Exception'] }) + const [, { threadId }] = await Promise.all([ + client.configurationDoneRequest(), + assertStoppedLocation('exception', program, 12), + ]) + const response = await client.exceptionInfoRequest({ threadId }) + assert.equal(response.body.exceptionId, 'Exception') + assert.equal(response.body.description, 'this is an exception') + await Promise.all([client.continueRequest({ threadId }), client.waitForEvent('terminated')]) + }) + }) }) From f7f395b5e1d9ba18f8a12bf3b798fe3e484bb0f8 Mon Sep 17 00:00:00 2001 From: Damjan Cvetko Date: Wed, 6 Aug 2025 20:38:55 +0200 Subject: [PATCH 6/7] Improve launch.json --- .vscode/launch.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 8b63fec0..ac1bdce9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,16 +12,18 @@ "NODE_ENV": "development" }, "sourceMaps": true, - "outFiles": ["${workspaceFolder}/out/**/*.js"] + "outFiles": ["${workspaceFolder}/out/**/*.js"], + "preLaunchTask": "npm: watch" }, { "name": "Launch Extension", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", - "args": ["--extensionDevelopmentPath=${workspaceRoot}"], + "args": ["--extensionDevelopmentPath=${workspaceRoot}", "${workspaceFolder}/testproject"], "sourceMaps": true, - "outFiles": ["${workspaceFolder}/out/**/*.js"] + "outFiles": ["${workspaceFolder}/out/**/*.js"], + "preLaunchTask": "npm: watch" }, { "name": "Mocha", From 2389de68d3fd5a4f539d9132dde09c0c6e117de1 Mon Sep 17 00:00:00 2001 From: Damjan Cvetko Date: Wed, 6 Aug 2025 23:21:01 +0200 Subject: [PATCH 7/7] Changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ac173b3..9b7cadde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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.37.0] + +- Expection info and support for virtual exception property (Xdebug 3.5.0) + ## [1.36.2] - Revert watch handling to eval