Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
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.37.0]

- Expection info and support for virtual exception property (Xdebug 3.5.0)

## [1.36.2]

- Revert watch handling to eval
Expand Down
97 changes: 85 additions & 12 deletions src/phpDebug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ 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'
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 {
Expand Down Expand Up @@ -249,6 +249,7 @@ class PhpDebugSession extends vscode.DebugSession {
supportTerminateDebuggee: true,
supportsDelayedStackTraceLoading: false,
supportsClipboardContext: true,
supportsExceptionInfoRequest: true,
}
this.sendResponse(response)
}
Expand Down Expand Up @@ -522,29 +523,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')
Expand All @@ -556,6 +552,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 {
Expand Down Expand Up @@ -1529,6 +1531,77 @@ class PhpDebugSession extends vscode.DebugSession {
this.sendResponse(response)
}
}

protected async exceptionInfoRequest(
response: VSCodeDebugProtocol.ExceptionInfoResponse,
args: VSCodeDebugProtocol.ExceptionInfoArguments
): Promise<void> {
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 = 3
try {
const { stack } = await connection.sendStackGetCommand() // CACHE?

if (stack.length > 0) {
const ctx = await stack[0].getContexts() // CACHE
old_max_depth = <number>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)
17 changes: 17 additions & 0 deletions src/test/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')])
})
})
})
10 changes: 9 additions & 1 deletion src/xdebugConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,12 @@ export class Connection extends DbgpConnection {
return this._pendingExecuteCommand
}

private _featureSet = new Map<string, number | string>()

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)
Expand Down Expand Up @@ -999,7 +1005,9 @@ export class Connection extends DbgpConnection {
* - notify_ok
*/
public async sendFeatureSetCommand(feature: string, value: string | number): Promise<FeatureSetResponse> {
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 ------------------------------------
Expand Down
10 changes: 10 additions & 0 deletions src/xdebugUtils.ts
Original file line number Diff line number Diff line change
@@ -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 })
)
}