Skip to content

Commit 88998ea

Browse files
authored
fix: Improve breakpoint handling with multiple connections #606
Request breakpoint info after initialization. Sync breakpoints to Xdebug connection from internal BreakpointManager state. Show breakpoints as verified if there are no connections. Remove _waitingConnections map because the ConfigurationDone handler is not in use anymore. Cleanup _statuses and _breakpointAdapters to prevent leaks. Fix typos. Fix test because the order of startup events changed. Co-authored-by: Damjan Cvetko <[email protected]>
1 parent 4b7ced2 commit 88998ea

File tree

4 files changed

+78
-50
lines changed

4 files changed

+78
-50
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ 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.16.1]
8+
9+
### Fixed
10+
11+
- Do not request all breakpoints on every new Xdebug connection. Use internal BreakpointManager state.
12+
- Show breakpoints as verified when there are no connections.
13+
714
## [1.16.0]
815

916
### Added

src/breakpoints.ts

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export class BreakpointManager extends EventEmitter {
5858
}
5959

6060
let vscodeBreakpoint: VSCodeDebugProtocol.Breakpoint = {
61-
verified: false,
61+
verified: this.listeners('add').length === 0,
6262
line: sourceBreakpoint.line,
6363
source: source,
6464
id: this._nextId++,
@@ -93,7 +93,7 @@ export class BreakpointManager extends EventEmitter {
9393
filters.forEach(filter => {
9494
let xdebugBreakpoint: xdebug.Breakpoint = new xdebug.ExceptionBreakpoint(filter)
9595
let vscodeBreakpoint: VSCodeDebugProtocol.Breakpoint = {
96-
verified: false,
96+
verified: this.listeners('add').length === 0,
9797
id: this._nextId++,
9898
}
9999
this._exceptionBreakpoints.set(vscodeBreakpoint.id!, xdebugBreakpoint)
@@ -130,7 +130,7 @@ export class BreakpointManager extends EventEmitter {
130130
)
131131

132132
let vscodeBreakpoint: VSCodeDebugProtocol.Breakpoint = {
133-
verified: false,
133+
verified: this.listeners('add').length === 0,
134134
id: this._nextId++,
135135
}
136136
this._callBreakpoints.set(vscodeBreakpoint.id!, xdebugBreakpoint)
@@ -153,6 +153,22 @@ export class BreakpointManager extends EventEmitter {
153153
// this will trigger a process on all adapters
154154
this.emit('process')
155155
}
156+
157+
public getAll(): Map<number, xdebug.Breakpoint> {
158+
let toAdd = new Map<number, xdebug.Breakpoint>()
159+
for (const [_, lbp] of this._lineBreakpoints) {
160+
for (const [id, bp] of lbp) {
161+
toAdd.set(id, bp)
162+
}
163+
}
164+
for (const [id, bp] of this._exceptionBreakpoints) {
165+
toAdd.set(id, bp)
166+
}
167+
for (const [id, bp] of this._callBreakpoints) {
168+
toAdd.set(id, bp)
169+
}
170+
return toAdd
171+
}
156172
}
157173

158174
interface AdapterBreakpoint {
@@ -170,7 +186,7 @@ export declare interface BreakpointAdapter {
170186

171187
/**
172188
* Listens to changes from BreakpointManager and delivers them their own Xdebug Connection.
173-
* If DBGp connection is busy, trach changes locally.
189+
* If DBGp connection is busy, track changes locally.
174190
*/
175191
export class BreakpointAdapter extends EventEmitter {
176192
private _connection: xdebug.Connection
@@ -179,18 +195,19 @@ export class BreakpointAdapter extends EventEmitter {
179195
private _queue: (() => void)[] = []
180196
private _executing = false
181197

182-
constructor(connecton: xdebug.Connection, breakpointManager: BreakpointManager) {
198+
constructor(connection: xdebug.Connection, breakpointManager: BreakpointManager) {
183199
super()
184-
this._connection = connecton
200+
this._connection = connection
185201
this._breakpointManager = breakpointManager
202+
this._add(breakpointManager.getAll())
186203
// listeners
187204
this._breakpointManager.on('add', this._add)
188205
this._breakpointManager.on('remove', this._remove)
189-
this._breakpointManager.on('process', this._process)
206+
this._breakpointManager.on('process', this.process)
190207
this._connection.on('close', (error?: Error) => {
191208
this._breakpointManager.off('add', this._add)
192209
this._breakpointManager.off('remove', this._remove)
193-
this._breakpointManager.off('process', this._process)
210+
this._breakpointManager.off('process', this.process)
194211
})
195212
this._connection.on('notify_breakpoint_resolved', this._notify)
196213
}
@@ -238,7 +255,17 @@ export class BreakpointAdapter extends EventEmitter {
238255
}
239256
}
240257

241-
protected _process = async (): Promise<void> => {
258+
private _processPromise: Promise<void>
259+
260+
public process = (): Promise<void> => {
261+
if (this._executing) {
262+
return this._processPromise
263+
}
264+
this._processPromise = this.__process()
265+
return this._processPromise
266+
}
267+
268+
protected __process = async (): Promise<void> => {
242269
if (this._executing) {
243270
// Protect from re-entry
244271
return
@@ -254,7 +281,7 @@ export class BreakpointAdapter extends EventEmitter {
254281
f()
255282
}
256283

257-
// do not execute netowrk operations until network channel available
284+
// do not execute network operations until network channel available
258285
if (this._connection.isPendingExecuteCommand) {
259286
return
260287
}
@@ -311,9 +338,9 @@ export class BreakpointAdapter extends EventEmitter {
311338
this._executing = false
312339
}
313340

314-
// If there were any concurent chanegs to the op-queue, rerun processing right away
341+
// If there were any concurrent changes to the op-queue, rerun processing right away
315342
if (this._queue.length > 0) {
316-
this._process()
343+
return await this.__process()
317344
}
318345
}
319346
}

src/phpDebug.ts

Lines changed: 31 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ interface LaunchRequestArguments extends VSCodeDebugProtocol.LaunchRequestArgume
7676
program?: string
7777
/** Optional arguments passed to the debuggee. */
7878
args?: string[]
79-
/** Launch the debuggee in this working directory (specified as an absolute path). If omitted the debuggee is lauched in its own directory. */
79+
/** Launch the debuggee in this working directory (specified as an absolute path). If omitted the debuggee is launched in its own directory. */
8080
cwd?: string
8181
/** Absolute path to the runtime executable to be used. Default is the runtime executable on the PATH. */
8282
runtimeExecutable?: string
@@ -105,9 +105,6 @@ class PhpDebugSession extends vscode.DebugSession {
105105
*/
106106
private _connections = new Map<number, xdebug.Connection>()
107107

108-
/** A set of connections which are not yet running and are waiting for configurationDoneRequest */
109-
private _waitingConnections = new Set<xdebug.Connection>()
110-
111108
/** A counter for unique source IDs */
112109
private _sourceIdCounter = 1
113110

@@ -147,6 +144,9 @@ class PhpDebugSession extends vscode.DebugSession {
147144
/** Breakpoint Manager to map VS Code to Xdebug breakpoints */
148145
private _breakpointManager = new BreakpointManager()
149146

147+
/** Breakpoint Adapters */
148+
private _breakpointAdapters = new Map<xdebug.Connection, BreakpointAdapter>()
149+
150150
public constructor() {
151151
super()
152152
this.setDebuggerColumnsStartAt1(true)
@@ -188,6 +188,8 @@ class PhpDebugSession extends vscode.DebugSession {
188188
supportTerminateDebuggee: true,
189189
}
190190
this.sendResponse(response)
191+
// request breakpoints right away
192+
this.sendEvent(new vscode.InitializedEvent())
191193
}
192194

193195
protected attachRequest(
@@ -271,7 +273,6 @@ class PhpDebugSession extends vscode.DebugSession {
271273
this.sendEvent(new vscode.OutputEvent('new connection ' + connection.id + '\n'), true)
272274
}
273275
this._connections.set(connection.id, connection)
274-
this._waitingConnections.add(connection)
275276
const disposeConnection = (error?: Error) => {
276277
if (this._connections.has(connection.id)) {
277278
if (args.log) {
@@ -288,7 +289,8 @@ class PhpDebugSession extends vscode.DebugSession {
288289
this.sendEvent(new vscode.ThreadEvent('exited', connection.id))
289290
connection.close()
290291
this._connections.delete(connection.id)
291-
this._waitingConnections.delete(connection)
292+
this._statuses.delete(connection)
293+
this._breakpointAdapters.delete(connection)
292294
}
293295
}
294296
connection.on('warning', (warning: string) => {
@@ -336,8 +338,19 @@ class PhpDebugSession extends vscode.DebugSession {
336338

337339
let bpa = new BreakpointAdapter(connection, this._breakpointManager)
338340
bpa.on('dapEvent', event => this.sendEvent(event))
339-
// request breakpoints from VS Code
340-
await this.sendEvent(new vscode.InitializedEvent())
341+
this._breakpointAdapters.set(connection, bpa)
342+
// sync breakpoints to connection
343+
await bpa.process()
344+
let xdebugResponse: xdebug.StatusResponse
345+
// either tell VS Code we stopped on entry or run the script
346+
if (this._args.stopOnEntry) {
347+
// do one step to the first statement
348+
this._hasStoppedOnEntry = false
349+
xdebugResponse = await connection.sendStepIntoCommand()
350+
} else {
351+
xdebugResponse = await connection.sendRunCommand()
352+
}
353+
this._checkStatus(xdebugResponse)
341354
} catch (error) {
342355
this.sendEvent(
343356
new vscode.OutputEvent((error instanceof Error ? error.message : error) + '\n', 'stderr')
@@ -383,9 +396,16 @@ class PhpDebugSession extends vscode.DebugSession {
383396
this._checkStatus(response)
384397
} else if (response.status === 'stopped') {
385398
this._connections.delete(connection.id)
399+
this._statuses.delete(connection)
400+
this._breakpointAdapters.delete(connection)
386401
this.sendEvent(new vscode.ThreadEvent('exited', connection.id))
387402
connection.close()
388403
} else if (response.status === 'break') {
404+
// First sync breakpoints
405+
let bpa = this._breakpointAdapters.get(connection)
406+
if (bpa) {
407+
await bpa.process()
408+
}
389409
// StoppedEvent reason can be 'step', 'breakpoint', 'exception' or 'pause'
390410
let stoppedEventReason: 'step' | 'breakpoint' | 'exception' | 'pause' | 'entry'
391411
let exceptionText: string | undefined
@@ -418,7 +438,6 @@ class PhpDebugSession extends vscode.DebugSession {
418438
)
419439
event.body.allThreadsStopped = false
420440
this.sendEvent(event)
421-
this._breakpointManager.process()
422441
}
423442
}
424443

@@ -530,35 +549,10 @@ class PhpDebugSession extends vscode.DebugSession {
530549
response: VSCodeDebugProtocol.ConfigurationDoneResponse,
531550
args: VSCodeDebugProtocol.ConfigurationDoneArguments
532551
) {
533-
let xdebugResponses: xdebug.StatusResponse[] = []
534-
try {
535-
xdebugResponses = await Promise.all<xdebug.StatusResponse>(
536-
Array.from(this._waitingConnections).map(connection => {
537-
this._waitingConnections.delete(connection)
538-
// either tell VS Code we stopped on entry or run the script
539-
if (this._args.stopOnEntry) {
540-
// do one step to the first statement
541-
this._hasStoppedOnEntry = false
542-
return connection.sendStepIntoCommand()
543-
} else {
544-
return connection.sendRunCommand()
545-
}
546-
})
547-
)
548-
} catch (error) {
549-
this.sendErrorResponse(response, <Error>error)
550-
for (const response of xdebugResponses) {
551-
this._checkStatus(response)
552-
}
553-
return
554-
}
555552
this.sendResponse(response)
556-
for (const response of xdebugResponses) {
557-
this._checkStatus(response)
558-
}
559553
}
560554

561-
/** Executed after a successfull launch or attach request and after a ThreadEvent */
555+
/** Executed after a successful launch or attach request and after a ThreadEvent */
562556
protected threadsRequest(response: VSCodeDebugProtocol.ThreadsResponse): void {
563557
// PHP doesn't have threads, but it may have multiple requests in parallel.
564558
// Think about a website that makes multiple, parallel AJAX requests to your PHP backend.
@@ -952,7 +946,8 @@ class PhpDebugSession extends vscode.DebugSession {
952946
}
953947
await connection.close()
954948
this._connections.delete(id)
955-
this._waitingConnections.delete(connection)
949+
this._statuses.delete(connection)
950+
this._breakpointAdapters.delete(connection)
956951
})
957952
)
958953
// If listening for connections, close server

src/test/adapter.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,8 +367,7 @@ describe('PHP Debug Adapter', () => {
367367
const program = path.join(TEST_PROJECT, 'function.php')
368368

369369
it('should stop on a function breakpoint', async () => {
370-
await client.launch({ program })
371-
await client.waitForEvent('initialized')
370+
await Promise.all([client.launch({ program }), client.waitForEvent('initialized')])
372371
const breakpoint = (
373372
await client.setFunctionBreakpointsRequest({
374373
breakpoints: [{ name: 'a_function' }],

0 commit comments

Comments
 (0)