Skip to content

Commit 49ad6f4

Browse files
committed
Fix a lot of bugs, implement exception breakpoints and eval
1 parent 5ab8139 commit 49ad6f4

File tree

3 files changed

+144
-79
lines changed

3 files changed

+144
-79
lines changed

src/phpDebug.ts

Lines changed: 101 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,6 @@ import * as url from 'url';
77
import * as path from 'path';
88
import * as util from 'util';
99

10-
/** PHP expression that is executed with an eval command if breaking on exceptions is enabled */
11-
const SET_EXCEPTION_HANDLER_PHP = `
12-
set_exception_handler("xdebug_break");
13-
set_error_handler("xdebug_break");
14-
`;
15-
1610
/** converts a file path to file:// URI */
1711
function path2uri(str: string): string {
1812
var pathName = str.replace(/\\/g, '/');
@@ -27,6 +21,32 @@ function uri2path(uri: string): string {
2721
return url.parse(uri).pathname.substr(1);
2822
}
2923

24+
/** formats a xdebug property value for VS Code */
25+
function formatPropertyValue(property: xdebug.BaseProperty): string {
26+
let displayValue: string;
27+
if (property.hasChildren || property.type === 'array' || property.type === 'object') {
28+
if (property.type === 'array') {
29+
// for arrays, show the length, like a var_dump would do
30+
displayValue = 'array(' + (property.hasChildren ? property.numberOfChildren : 0) + ')';
31+
} else if (property.type === 'object' && property.class) {
32+
// for objects, show the class name as type (if specified)
33+
displayValue = property.class;
34+
} else {
35+
// edge case: show the type of the property as the value
36+
displayValue = property.type;
37+
}
38+
} else {
39+
// for null, uninitialized, resource, etc. show the type
40+
displayValue = property.value || property.type === 'string' ? property.value : property.type;
41+
if (property.type === 'string') {
42+
displayValue = '"' + displayValue + '"';
43+
} else if (property.type === 'bool') {
44+
displayValue = !!parseInt(displayValue) + '';
45+
}
46+
}
47+
return displayValue;
48+
}
49+
3050
/**
3151
* This interface should always match the schema found in the mock-debug extension manifest.
3252
*/
@@ -47,8 +67,8 @@ class PhpDebugSession extends vscode.DebugSession {
4767
private _connections = new Map<number, xdebug.Connection>();
4868
/** The first connection we receive */
4969
private _mainConnection: xdebug.Connection = null;
50-
/** Gets set to true after _runOrStopOnEntry is called the first time */
51-
private _running = false;
70+
/** Gets set to true after _runOrStopOnEntry is called the first time, which means all exceptions etc. are set */
71+
private _initialized = false;
5272
/** A map of file URIs to lines: breakpoints received from VS Code */
5373
private _breakpoints = new Map<string, number[]>();
5474
/** Gets set after a setExceptionBreakpointsRequest */
@@ -82,7 +102,7 @@ class PhpDebugSession extends vscode.DebugSession {
82102
// new XDebug connection
83103
const connection = new xdebug.Connection(socket);
84104
this._connections.set(connection.id, connection);
85-
if (this._running) {
105+
if (this._initialized) {
86106
// this is a new connection, for example triggered by a seperate, parallel request to the webserver.
87107
connection.waitForInitPacket()
88108
// tell VS Code that this is a new thread
@@ -102,7 +122,7 @@ class PhpDebugSession extends vscode.DebugSession {
102122
// restore exception breakpoint settings for the new connection
103123
.then(() => {
104124
if (this._breakOnExceptions) {
105-
return connection.sendEvalCommand(SET_EXCEPTION_HANDLER_PHP);
125+
return connection.sendBreakpointSetCommand({type: 'exception', exception: '*'});
106126
}
107127
})
108128
// run the script or stop on entry
@@ -139,6 +159,7 @@ class PhpDebugSession extends vscode.DebugSession {
139159
/** is called after all breakpoints etc. are initialized and either runs the script or notifies VS Code that we stopped on entry, depending on launch settings */
140160
private _runOrStopOnEntry(connection: xdebug.Connection): void {
141161
// either tell VS Code we stopped on entry or run the script
162+
this._initialized = true;
142163
if (this._args.stopOnEntry) {
143164
this.sendEvent(new vscode.StoppedEvent('entry', connection.id));
144165
} else {
@@ -197,28 +218,36 @@ class PhpDebugSession extends vscode.DebugSession {
197218
/** This is called for each source file that has breakpoints with all the breakpoints in that file and whenever these change. */
198219
protected setBreakPointsRequest(response: VSCodeDebugProtocol.SetBreakpointsResponse, args: VSCodeDebugProtocol.SetBreakpointsArguments) {
199220
const file = path2uri(args.source.path);
200-
this._breakpoints.set(file, args.lines);
201221
const breakpoints: vscode.Breakpoint[] = [];
202222
const connections = Array.from(this._connections.values());
203223
return Promise.all(connections.map(connection =>
204-
Promise.all(args.lines.map(line =>
205-
connection.sendBreakpointSetCommand({type: 'line', file, line})
206-
.then(xdebugResponse => {
207-
// only capture each breakpoint once (for the main connection)
208-
if (connection === this._mainConnection) {
209-
breakpoints.push(new vscode.Breakpoint(true, line));
210-
}
211-
})
212-
.catch(error => {
213-
// only capture each breakpoint once (for the main connection)
214-
if (connection === this._mainConnection) {
215-
console.error('breakpoint could not be set: ', error);
216-
breakpoints.push(new vscode.Breakpoint(false, line));
217-
}
218-
})
219-
))
224+
// clear breakpoints for this file
225+
connection.sendBreakpointListCommand()
226+
.then(response => Promise.all(
227+
response.breakpoints
228+
.filter(breakpoint => breakpoint.type === 'line' && breakpoint.fileUri === file)
229+
.map(breakpoint => breakpoint.remove())
230+
))
231+
// set them
232+
.then(() => Promise.all(args.lines.map(line =>
233+
connection.sendBreakpointSetCommand({type: 'line', file, line})
234+
.then(xdebugResponse => {
235+
// only capture each breakpoint once (for the main connection)
236+
if (connection === this._mainConnection) {
237+
breakpoints.push(new vscode.Breakpoint(true, line));
238+
}
239+
})
240+
.catch(error => {
241+
// only capture each breakpoint once (for the main connection)
242+
if (connection === this._mainConnection) {
243+
console.error('breakpoint could not be set: ', error);
244+
breakpoints.push(new vscode.Breakpoint(false, line));
245+
}
246+
})
247+
)))
220248
)).then(() => {
221249
response.body = {breakpoints};
250+
this._breakpoints.set(file, args.lines);
222251
this.sendResponse(response);
223252
}).catch(error => {
224253
this.sendErrorResponse(response, error.code, error.message);
@@ -228,21 +257,37 @@ class PhpDebugSession extends vscode.DebugSession {
228257
/** This is called once after all line breakpoints have been set and whenever the breakpoints settings change */
229258
protected setExceptionBreakPointsRequest(response: VSCodeDebugProtocol.SetExceptionBreakpointsResponse, args: VSCodeDebugProtocol.SetExceptionBreakpointsArguments): void {
230259
// args.filters can contain 'all' and 'uncaught', but 'uncaught' is the only setting XDebug supports
231-
this._breakOnExceptions = args.filters.indexOf('uncaught') !== -1;
260+
const breakOnExceptions = args.filters.indexOf('uncaught') !== -1;
261+
if (args.filters.indexOf('all') !== -1) {
262+
this.sendEvent(new vscode.OutputEvent('breaking on caught exceptions is not supported by XDebug', 'stderr'));
263+
}
232264
Promise.resolve()
233-
.then(() => {
234-
if (this._breakOnExceptions) {
235-
// tell PHP to break on uncaught exceptions and errors
265+
.then<any>(() => {
266+
// does the new setting differ from the current setting?
267+
if (breakOnExceptions !== !!this._breakOnExceptions) {
236268
const connections = Array.from(this._connections.values());
237-
return Promise.all(connections.map(connection => connection.sendEvalCommand(SET_EXCEPTION_HANDLER_PHP)));
269+
if (breakOnExceptions) {
270+
// set an exception breakpoint for all exceptions
271+
return Promise.all(connections.map(connection => connection.sendBreakpointSetCommand({type: 'exception', exception: '*'})));
272+
} else {
273+
// remove all exception breakpoints
274+
return Promise.all(connections.map(connection =>
275+
connection.sendBreakpointListCommand()
276+
.then(response => Promise.all(
277+
response.breakpoints
278+
.filter(breakpoint => breakpoint.type === 'exception')
279+
.map(breakpoint => breakpoint.remove())
280+
))
281+
));
282+
}
238283
}
239284
})
240285
.then(() => {
286+
this._breakOnExceptions = breakOnExceptions;
241287
this.sendResponse(response);
242288
// if this is the first time this is called and the main connection is not yet running, trigger a run because now everything is set up
243-
if (!this._running) {
289+
if (!this._initialized) {
244290
this._runOrStopOnEntry(this._mainConnection);
245-
this._running = true;
246291
}
247292
})
248293
.catch(error => {
@@ -314,7 +359,7 @@ class PhpDebugSession extends vscode.DebugSession {
314359
}
315360

316361
protected variablesRequest(response: VSCodeDebugProtocol.VariablesResponse, args: VSCodeDebugProtocol.VariablesArguments): void {
317-
const variablesReference = args.variablesReference
362+
const variablesReference = args.variablesReference;
318363
let propertiesPromise: Promise<xdebug.BaseProperty[]>;
319364
if (this._contexts.has(variablesReference)) {
320365
// VS Code is requesting the variables for a SCOPE, so we have to do a context_get
@@ -323,7 +368,7 @@ class PhpDebugSession extends vscode.DebugSession {
323368
} else if (this._properties.has(variablesReference)) {
324369
// VS Code is requesting the subelements for a variable, so we have to do a property_get
325370
const property = this._properties.get(variablesReference);
326-
propertiesPromise = property.getChildren();
371+
propertiesPromise = property.hasChildren ? property.getChildren() : Promise.resolve([]);
327372
} else if (this._evalResultProperties.has(variablesReference)) {
328373
// the children of properties returned from an eval command are always inlined, so we simply resolve them
329374
const property = this._evalResultProperties.get(variablesReference);
@@ -338,8 +383,8 @@ class PhpDebugSession extends vscode.DebugSession {
338383
.then(properties => {
339384
response.body = {
340385
variables: properties.map(property => {
386+
const displayValue = formatPropertyValue(property);
341387
let variablesReference: number;
342-
let displayValue: string;
343388
if (property.hasChildren || property.type === 'array' || property.type === 'object') {
344389
// if the property has children, we have to send a variableReference back to VS Code
345390
// so it can receive the child elements in another request.
@@ -350,26 +395,8 @@ class PhpDebugSession extends vscode.DebugSession {
350395
} else if (property instanceof xdebug.EvalResultProperty) {
351396
this._evalResultProperties.set(variablesReference, property);
352397
}
353-
// we show the type of the property ("array", "object") as the value
354-
displayValue = property.type;
355-
if (property.type === 'array') {
356-
// show the length, like a var_dump would do
357-
displayValue += '(' + property.numberOfChildren + ')';
358-
}
359398
} else {
360399
variablesReference = 0;
361-
if (property.value) {
362-
displayValue = property.value;
363-
} else if (property.type === 'uninitialized' || property.type === 'null') {
364-
displayValue = property.type;
365-
} else {
366-
displayValue = '';
367-
}
368-
if (property.type === 'string') {
369-
displayValue = '"' + displayValue + '"';
370-
} else if (property.type === 'bool') {
371-
displayValue = !!parseInt(displayValue) + '';
372-
}
373400
}
374401
return new vscode.Variable(property.name, displayValue, variablesReference);
375402
})
@@ -383,31 +410,31 @@ class PhpDebugSession extends vscode.DebugSession {
383410
}
384411

385412
protected continueRequest(response: VSCodeDebugProtocol.ContinueResponse, args: VSCodeDebugProtocol.ContinueArguments): void {
386-
const connection = this._connections.get(args.threadId);
413+
const connection = this._connections.get(args.threadId) || this._mainConnection;
387414
connection.sendRunCommand()
388415
.then(response => this._checkStatus(response))
389416
.catch(error => this.sendErrorResponse(response, error.code, error.message));
390417
this.sendResponse(response);
391418
}
392419

393420
protected nextRequest(response: VSCodeDebugProtocol.NextResponse, args: VSCodeDebugProtocol.NextArguments): void {
394-
const connection = this._connections.get(args.threadId);
421+
const connection = this._connections.get(args.threadId) || this._mainConnection;
395422
connection.sendStepOverCommand()
396423
.then(response => this._checkStatus(response))
397424
.catch(error => this.sendErrorResponse(response, error.code, error.message));
398425
this.sendResponse(response);
399426
}
400427

401428
protected stepInRequest(response: VSCodeDebugProtocol.StepInResponse, args: VSCodeDebugProtocol.StepInArguments) : void {
402-
const connection = this._connections.get(args.threadId);
429+
const connection = this._connections.get(args.threadId) || this._mainConnection;
403430
connection.sendStepIntoCommand()
404431
.then(response => this._checkStatus(response))
405432
.catch(error => this.sendErrorResponse(response, error.code, error.message));
406433
this.sendResponse(response);
407434
}
408435

409436
protected stepOutRequest(response: VSCodeDebugProtocol.StepOutResponse, args: VSCodeDebugProtocol.StepOutArguments) : void {
410-
const connection = this._connections.get(args.threadId);
437+
const connection = this._connections.get(args.threadId) || this._mainConnection;
411438
connection.sendStepOutCommand()
412439
.then(response => this._checkStatus(response))
413440
.catch(error => this.sendErrorResponse(response, error.code, error.message));
@@ -428,6 +455,7 @@ class PhpDebugSession extends vscode.DebugSession {
428455
this._mainConnection = null;
429456
}
430457
})
458+
.catch(() => {})
431459
)).then(() => {
432460
this._server.close(() => {
433461
this.shutdown();
@@ -439,18 +467,23 @@ class PhpDebugSession extends vscode.DebugSession {
439467
}
440468

441469
protected evaluateRequest(response: VSCodeDebugProtocol.EvaluateResponse, args: VSCodeDebugProtocol.EvaluateArguments): void {
442-
this._stackFrames.get(args.frameId).connection.sendEvalCommand(args.expression)
470+
const connection = this._stackFrames.has(args.frameId) ? this._stackFrames.get(args.frameId).connection : this._mainConnection;
471+
connection.sendEvalCommand(args.expression)
443472
.then(xdebugResponse => {
444-
const value = xdebugResponse.result.value;
445-
let variablesReference: number;
446-
// if the property has children, generate a variable ID and save the property (including children) so VS Code can request them
447-
if (xdebugResponse.result.hasChildren) {
448-
variablesReference = this._variableIdCounter++;
449-
this._evalResultProperties.set(variablesReference, xdebugResponse.result);
473+
if (xdebugResponse.result) {
474+
const displayValue = formatPropertyValue(xdebugResponse.result);
475+
let variablesReference: number;
476+
// if the property has children, generate a variable ID and save the property (including children) so VS Code can request them
477+
if (xdebugResponse.result.hasChildren || xdebugResponse.result.type === 'array' || xdebugResponse.result.type === 'object') {
478+
variablesReference = this._variableIdCounter++;
479+
this._evalResultProperties.set(variablesReference, xdebugResponse.result);
480+
} else {
481+
variablesReference = 0;
482+
}
483+
response.body = {result: displayValue, variablesReference};
450484
} else {
451-
variablesReference = 0;
485+
response.body = {result: 'no result', variablesReference: 0};
452486
}
453-
response.body = {result: value, variablesReference};
454487
this.sendResponse(response);
455488
})
456489
.catch(error => {

0 commit comments

Comments
 (0)