Skip to content

Commit e2231bc

Browse files
authored
feat: exception info and virtual exception property (#1038)
* New implementation of Exception Info with support for virtual $__EXCEPTION property * eslint and prettier * Remove semver * Update build to include CODECOV_TOKEN * Exception info basic test * Improve launch.json * Changelog
1 parent 6ce118f commit e2231bc

File tree

7 files changed

+132
-16
lines changed

7 files changed

+132
-16
lines changed

.github/workflows/build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ jobs:
6161
run: ./node_modules/.bin/nyc report --reporter=json
6262
- name: Upload coverage to Codecov
6363
uses: codecov/codecov-action@v5
64+
with:
65+
token: ${{ secrets.CODECOV_TOKEN }}
6466
release:
6567
runs-on: ubuntu-22.04
6668
needs: test

.vscode/launch.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,18 @@
1212
"NODE_ENV": "development"
1313
},
1414
"sourceMaps": true,
15-
"outFiles": ["${workspaceFolder}/out/**/*.js"]
15+
"outFiles": ["${workspaceFolder}/out/**/*.js"],
16+
"preLaunchTask": "npm: watch"
1617
},
1718
{
1819
"name": "Launch Extension",
1920
"type": "extensionHost",
2021
"request": "launch",
2122
"runtimeExecutable": "${execPath}",
22-
"args": ["--extensionDevelopmentPath=${workspaceRoot}"],
23+
"args": ["--extensionDevelopmentPath=${workspaceRoot}", "${workspaceFolder}/testproject"],
2324
"sourceMaps": true,
24-
"outFiles": ["${workspaceFolder}/out/**/*.js"]
25+
"outFiles": ["${workspaceFolder}/out/**/*.js"],
26+
"preLaunchTask": "npm: watch"
2527
},
2628
{
2729
"name": "Mocha",

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
44

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

7+
## [1.37.0]
8+
9+
- Expection info and support for virtual exception property (Xdebug 3.5.0)
10+
711
## [1.36.2]
812

913
- Revert watch handling to eval

src/phpDebug.ts

Lines changed: 85 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ import { Terminal } from './terminal'
1212
import { convertClientPathToDebugger, convertDebuggerPathToClient, isPositiveMatchInGlobs } from './paths'
1313
import { minimatch } from 'minimatch'
1414
import { BreakpointManager, BreakpointAdapter } from './breakpoints'
15-
import * as semver from 'semver'
1615
import { LogPointManager } from './logpoint'
1716
import { ProxyConnect } from './proxyConnect'
1817
import { randomUUID } from 'crypto'
1918
import { getConfiguredEnvironment } from './envfile'
2019
import { XdebugCloudConnection } from './cloud'
2120
import { shouldIgnoreException } from './ignore'
2221
import { varExportProperty } from './varExport'
22+
import { supportedEngine } from './xdebugUtils'
2323

2424
if (process.env['VSCODE_NLS_CONFIG']) {
2525
try {
@@ -249,6 +249,7 @@ class PhpDebugSession extends vscode.DebugSession {
249249
supportTerminateDebuggee: true,
250250
supportsDelayedStackTraceLoading: false,
251251
supportsClipboardContext: true,
252+
supportsExceptionInfoRequest: true,
252253
}
253254
this.sendResponse(response)
254255
}
@@ -522,29 +523,24 @@ class PhpDebugSession extends vscode.DebugSession {
522523

523524
// support for breakpoints
524525
let feat: xdebug.FeatureGetResponse
525-
const supportedEngine =
526-
initPacket.engineName === 'Xdebug' &&
527-
semver.valid(initPacket.engineVersion, { loose: true }) &&
528-
semver.gte(initPacket.engineVersion, '3.0.0', { loose: true })
529-
const supportedEngine32 =
530-
initPacket.engineName === 'Xdebug' &&
531-
semver.valid(initPacket.engineVersion, { loose: true }) &&
532-
semver.gte(initPacket.engineVersion, '3.2.0', { loose: true })
526+
const supportedEngine30 = supportedEngine(initPacket, '3.0.0')
527+
const supportedEngine32 = supportedEngine(initPacket, '3.2.0')
528+
const supportedEngine35 = supportedEngine(initPacket, '3.5.0')
533529
if (
534-
supportedEngine ||
530+
supportedEngine30 ||
535531
((feat = await connection.sendFeatureGetCommand('resolved_breakpoints')) && feat.supported === '1')
536532
) {
537533
await connection.sendFeatureSetCommand('resolved_breakpoints', '1')
538534
}
539535
if (
540-
supportedEngine ||
536+
supportedEngine30 ||
541537
((feat = await connection.sendFeatureGetCommand('notify_ok')) && feat.supported === '1')
542538
) {
543539
await connection.sendFeatureSetCommand('notify_ok', '1')
544540
connection.on('notify_user', (notify: xdebug.UserNotify) => this.handleUserNotify(notify, connection))
545541
}
546542
if (
547-
supportedEngine ||
543+
supportedEngine30 ||
548544
((feat = await connection.sendFeatureGetCommand('extended_properties')) && feat.supported === '1')
549545
) {
550546
await connection.sendFeatureSetCommand('extended_properties', '1')
@@ -556,6 +552,12 @@ class PhpDebugSession extends vscode.DebugSession {
556552
) {
557553
await connection.sendFeatureSetCommand('breakpoint_include_return_value', '1')
558554
}
555+
if (
556+
supportedEngine35 ||
557+
((feat = await connection.sendFeatureGetCommand('virtual_exception_value')) && feat.supported === '1')
558+
) {
559+
await connection.sendFeatureSetCommand('virtual_exception_value', '1')
560+
}
559561

560562
// override features from launch.json
561563
try {
@@ -1529,6 +1531,77 @@ class PhpDebugSession extends vscode.DebugSession {
15291531
this.sendResponse(response)
15301532
}
15311533
}
1534+
1535+
protected async exceptionInfoRequest(
1536+
response: VSCodeDebugProtocol.ExceptionInfoResponse,
1537+
args: VSCodeDebugProtocol.ExceptionInfoArguments
1538+
): Promise<void> {
1539+
const connection = this._connections.get(args.threadId)
1540+
if (!connection) {
1541+
throw new Error('Unknown thread ID')
1542+
}
1543+
1544+
const status = this._statuses.get(connection)!
1545+
1546+
let ex: VSCodeDebugProtocol.ExceptionDetails | undefined
1547+
1548+
if (connection.featureSet('virtual_exception_value')) {
1549+
let old_max_depth = 3
1550+
try {
1551+
const { stack } = await connection.sendStackGetCommand() // CACHE?
1552+
1553+
if (stack.length > 0) {
1554+
const ctx = await stack[0].getContexts() // CACHE
1555+
old_max_depth = <number>connection.featureSet('max_depth') ?? 1
1556+
if (old_max_depth < 3) {
1557+
await connection.sendFeatureSetCommand('max_depth', 3)
1558+
}
1559+
1560+
const res = await connection.sendPropertyGetNameCommand('$__EXCEPTION', ctx[0])
1561+
1562+
const s = res.property.children.find(
1563+
p => p.name === 'trace' || p.name === '*Exception*trace' || p.name === '*Error*trace'
1564+
)
1565+
1566+
const at = `Created at ${
1567+
res.property.children.find(p => p.name == 'file')?.value ?? '**UNKNOWN**'
1568+
}:${res.property.children.find(p => p.name == 'line')?.value ?? '?'}\n`
1569+
const st = s?.children
1570+
.map(
1571+
(t, i) =>
1572+
`at ${t.children.find(p => p.name == 'class')?.value ?? ''}${
1573+
t.children.find(p => p.name == 'type')?.value ?? ''
1574+
}${t.children.find(p => p.name == 'function')?.value ?? '?'}() (${
1575+
t.children.find(p => p.name == 'file')?.value ?? '**UNKNOWN**'
1576+
}:${t.children.find(p => p.name == 'line')?.value ?? '?'})`
1577+
)
1578+
.join('\n')
1579+
ex = {
1580+
message: res.property.children.find(p => p.name === 'message')?.value ?? undefined,
1581+
typeName: res.property.class,
1582+
fullTypeName: res.property.class,
1583+
evaluateName: '$__EXCEPTION',
1584+
stackTrace: `${at}${st ?? ''}`,
1585+
// TODO process inner/previous exception
1586+
}
1587+
}
1588+
} catch {
1589+
// Probably $__EXCEPTION not present
1590+
} finally {
1591+
if (old_max_depth < 3) {
1592+
await connection.sendFeatureSetCommand('max_depth', old_max_depth)
1593+
}
1594+
}
1595+
}
1596+
1597+
response.body = {
1598+
exceptionId: status.exception.name,
1599+
breakMode: 'always',
1600+
description: status.exception.message,
1601+
details: ex,
1602+
}
1603+
this.sendResponse(response)
1604+
}
15321605
}
15331606

15341607
vscode.DebugSession.run(PhpDebugSession)

src/test/adapter.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -946,4 +946,21 @@ describe('PHP Debug Adapter', () => {
946946
await client.assertOutput('console', 'skipping entry point')
947947
})
948948
})
949+
950+
describe('exception info', () => {
951+
it('should show exception info', async () => {
952+
const program = path.join(TEST_PROJECT, 'error.php')
953+
954+
await client.launch({ program })
955+
await client.setExceptionBreakpointsRequest({ filters: ['Exception'] })
956+
const [, { threadId }] = await Promise.all([
957+
client.configurationDoneRequest(),
958+
assertStoppedLocation('exception', program, 12),
959+
])
960+
const response = await client.exceptionInfoRequest({ threadId })
961+
assert.equal(response.body.exceptionId, 'Exception')
962+
assert.equal(response.body.description, 'this is an exception')
963+
await Promise.all([client.continueRequest({ threadId }), client.waitForEvent('terminated')])
964+
})
965+
})
949966
})

src/xdebugConnection.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,12 @@ export class Connection extends DbgpConnection {
845845
return this._pendingExecuteCommand
846846
}
847847

848+
private _featureSet = new Map<string, number | string>()
849+
850+
public featureSet(feature: string): number | string | undefined {
851+
return this._featureSet.get(feature)
852+
}
853+
848854
/** Constructs a new connection that uses the given socket to communicate with Xdebug. */
849855
constructor(socket: Transport) {
850856
super(socket)
@@ -999,7 +1005,9 @@ export class Connection extends DbgpConnection {
9991005
* - notify_ok
10001006
*/
10011007
public async sendFeatureSetCommand(feature: string, value: string | number): Promise<FeatureSetResponse> {
1002-
return new FeatureSetResponse(await this._enqueueCommand('feature_set', `-n ${feature} -v ${value}`), this)
1008+
const res = new FeatureSetResponse(await this._enqueueCommand('feature_set', `-n ${feature} -v ${value}`), this)
1009+
this._featureSet.set(feature, value)
1010+
return res
10031011
}
10041012

10051013
// ---------------------------- breakpoints ------------------------------------

src/xdebugUtils.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as semver from 'semver'
2+
import * as xdebug from './xdebugConnection'
3+
4+
export function supportedEngine(initPacket: xdebug.InitPacket, version: string): boolean {
5+
return (
6+
initPacket.engineName === 'Xdebug' &&
7+
semver.valid(initPacket.engineVersion.replace('-dev', ''), { loose: true }) !== null &&
8+
semver.gte(initPacket.engineVersion.replace('-dev', ''), version, { loose: true })
9+
)
10+
}

0 commit comments

Comments
 (0)