-
-
Notifications
You must be signed in to change notification settings - Fork 177
/
phpDebug.ts
1518 lines (1440 loc) Β· 66.8 KB
/
phpDebug.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import * as vscode from '@vscode/debugadapter'
import { DebugProtocol as VSCodeDebugProtocol } from '@vscode/debugprotocol'
import * as net from 'net'
import * as xdebug from './xdebugConnection'
import moment from 'moment'
import * as url from 'url'
import * as childProcess from 'child_process'
import * as path from 'path'
import * as util from 'util'
import * as fs from 'fs'
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'
if (process.env['VSCODE_NLS_CONFIG']) {
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
moment.locale(JSON.parse(process.env.VSCODE_NLS_CONFIG).locale)
} catch {
// ignore
}
}
/** formats a xdebug property value for VS Code */
function formatPropertyValue(property: xdebug.BaseProperty): string {
let displayValue: string
if (property.hasChildren || property.type === 'array' || property.type === 'object') {
if (property.type === 'array') {
// for arrays, show the length, like a var_dump would do
displayValue = `array(${property.hasChildren ? property.numberOfChildren : 0})`
} else if (property.type === 'object' && property.class) {
// for objects, show the class name as type (if specified)
displayValue = property.class
} else {
// edge case: show the type of the property as the value
displayValue = property.type
}
} else {
// for null, uninitialized, resource, etc. show the type
displayValue = property.value || property.type === 'string' ? property.value : property.type
if (property.type === 'string') {
displayValue = `"${displayValue}"`
} else if (property.type === 'bool') {
displayValue = Boolean(parseInt(displayValue, 10)).toString()
}
}
return displayValue
}
/**
* This interface should always match the schema found in the mock-debug extension manifest.
*/
export interface LaunchRequestArguments extends VSCodeDebugProtocol.LaunchRequestArguments {
/** Name of the configuration */
name?: string
/** The address to bind to for listening for Xdebug connections (default: all IPv6 connections if available, else all IPv4 connections) or unix socket */
hostname?: string
/** The port where the adapter should listen for Xdebug connections (default: 9003) */
port?: number
/** Automatically stop target after launch. If not specified, target does not stop. */
stopOnEntry?: boolean
/** The source root on the server when doing remote debugging on a different host */
serverSourceRoot?: string
/** The path to the source root on this machine that is the equivalent to the serverSourceRoot on the server. */
localSourceRoot?: string
/** The path to the source root on this machine that is the equivalent to the serverSourceRoot on the server. */
pathMappings?: { [index: string]: string }
/** If true, will log all communication between VS Code and the adapter to the console */
log?: boolean
/** Array of glob patterns that errors should be ignored from */
ignore?: string[]
/** Array of glob patterns that exceptions should be ignored from */
ignoreExceptions?: string[]
/** An array of glob pattern to skip if the initial entry file is matched. */
skipEntryPaths?: string[]
/** Array of glob patterns that debugger should not step in */
skipFiles?: string[]
/** Xdebug configuration */
xdebugSettings?: { [featureName: string]: string | number }
/** proxy connection configuration */
proxy?: {
allowMultipleSessions: boolean
enable: boolean
host: string
key: string
port: number
timeout: number
}
/** Maximum allowed parallel debugging sessions */
maxConnections?: number
/** Xdebug cloud token */
xdebugCloudToken?: string
/** Xdebug stream settings */
stream?: {
stdout?: 0 | 1 | 2
}
// CLI options
/** If set, launches the specified PHP script in CLI mode */
program?: string
/** Optional arguments passed to the debuggee. */
args?: string[]
/** Launch the debuggee in this working directory (specified as an absolute path). If omitted the debuggee is launched in its own directory. */
cwd?: string
/** Absolute path to the runtime executable to be used. Default is the runtime executable on the PATH. */
runtimeExecutable?: string
/** Optional arguments passed to the runtime executable. */
runtimeArgs?: string[]
/** Optional environment variables to pass to the debuggee. The string valued properties of the 'environmentVariables' are used as key/value pairs. */
env?: { [key: string]: string }
/** Absolute path to a file containing environment variable definitions. */
envFile?: string
/** If true launch the target in an external console. */
externalConsole?: boolean
}
class PhpDebugSession extends vscode.DebugSession {
/** The arguments that were given to launchRequest */
private _args: LaunchRequestArguments
/** The TCP server that listens for Xdebug connections */
private _server: net.Server
/** The child process of the launched PHP script, if launched by the debug adapter */
private _phpProcess?: childProcess.ChildProcess
/**
* A map from VS Code thread IDs to Xdebug Connections.
* Xdebug makes a new connection for each request to the webserver, we present these as threads to VS Code.
* The threadId key is equal to the id attribute of the connection.
*/
private _connections = new Map<number, xdebug.Connection>()
/** A counter for unique source IDs */
private _sourceIdCounter = 1
/** A map of VS Code source IDs to Xdebug file URLs for virtual files (dpgp://whatever) and the corresponding connection */
private _sources = new Map<number, { connection: xdebug.Connection; url: string }>()
/** A counter for unique stackframe IDs */
private _stackFrameIdCounter = 1
/** A map from unique stackframe IDs (even across connections) to Xdebug stackframes */
private _stackFrames = new Map<number, xdebug.StackFrame>()
/** A map from Xdebug connections to their current status */
private _statuses = new Map<xdebug.Connection, xdebug.StatusResponse>()
/** A counter for unique context, property and eval result properties (as these are all requested by a VariableRequest from VS Code) */
private _variableIdCounter = 1
/** A map from unique VS Code variable IDs to Xdebug statuses for virtual error stack frames */
private _errorStackFrames = new Map<number, xdebug.StatusResponse>()
/** A map from unique VS Code variable IDs to Xdebug statuses for virtual error scopes */
private _errorScopes = new Map<number, xdebug.StatusResponse>()
/** A map from unique VS Code variable IDs to an Xdebug contexts */
private _contexts = new Map<number, xdebug.Context>()
/** A map from unique VS Code variable IDs to a Xdebug properties */
private _properties = new Map<number, xdebug.Property>()
/** A map from unique VS Code variable IDs to Xdebug eval result properties, because property children returned from eval commands are always inlined */
private _evalResultProperties = new Map<number, xdebug.EvalResultProperty>()
/** A flag to indicate that the adapter has already processed the stopOnEntry step request */
private _hasStoppedOnEntry = false
/** A map from Xdebug connection id to state of skipping files */
private _skippingFiles = new Map<number, boolean>()
/** Breakpoint Manager to map VS Code to Xdebug breakpoints */
private _breakpointManager = new BreakpointManager()
/** Breakpoint Adapters */
private _breakpointAdapters = new Map<xdebug.Connection, BreakpointAdapter>()
/**
* The manager for logpoints. Since xdebug does not support anything like logpoints,
* it has to be managed by the extension/debug server. It does that by a Map referencing
* the log messages per file. Xdebug sees it as a regular breakpoint.
*/
private _logPointManager = new LogPointManager()
/** The proxy initialization and termination connection. */
private _proxyConnect: ProxyConnect
/** Optional cloud connection */
private _xdebugCloudConnection: XdebugCloudConnection
/** the promise that gets resolved once we receive the done request */
private _donePromise: Promise<void>
/** resolves the done promise */
private _donePromiseResolveFn: () => void
constructor() {
super()
this.setDebuggerColumnsStartAt1(true)
this.setDebuggerLinesStartAt1(true)
this.setDebuggerPathFormat('uri')
}
protected initializeRequest(
response: VSCodeDebugProtocol.InitializeResponse,
args: VSCodeDebugProtocol.InitializeRequestArguments
): void {
response.body = {
supportsConfigurationDoneRequest: true,
supportsEvaluateForHovers: true,
supportsConditionalBreakpoints: true,
supportsFunctionBreakpoints: true,
supportsLogPoints: true,
supportsHitConditionalBreakpoints: true,
supportsSetVariable: true,
exceptionBreakpointFilters: [
{
filter: 'Notice',
label: 'Notices',
},
{
filter: 'Warning',
label: 'Warnings',
},
{
filter: 'Error',
label: 'Errors',
},
{
filter: 'Exception',
label: 'Exceptions',
},
{
filter: '*',
label: 'Everything',
},
],
supportTerminateDebuggee: true,
supportsDelayedStackTraceLoading: false,
}
this.sendResponse(response)
}
protected attachRequest(
response: VSCodeDebugProtocol.AttachResponse,
args: VSCodeDebugProtocol.AttachRequestArguments
): void {
this.sendErrorResponse(response, new Error('Attach requests are not supported'))
this.shutdown()
}
protected async launchRequest(
response: VSCodeDebugProtocol.LaunchResponse,
args: LaunchRequestArguments
): Promise<void> {
if (args.localSourceRoot && args.serverSourceRoot) {
let pathMappings: { [index: string]: string } = {}
if (args.pathMappings) {
pathMappings = args.pathMappings
}
pathMappings[args.serverSourceRoot] = args.localSourceRoot
args.pathMappings = pathMappings
}
this._args = args
this._donePromise = new Promise<void>((resolve, reject) => {
this._donePromiseResolveFn = resolve
})
/** launches the script as CLI */
const launchScript = async (port: number | string): Promise<void> => {
// check if program exists
if (args.program) {
await new Promise<void>((resolve, reject) =>
fs.access(args.program!, fs.constants.F_OK, err => (err ? reject(err) : resolve()))
)
}
const runtimeArgs = (args.runtimeArgs || []).map(v => v.replace('${port}', port.toString()))
const runtimeExecutable = args.runtimeExecutable || 'php'
const programArgs = args.args || []
const program = args.program ? [args.program] : []
const cwd = args.cwd || process.cwd()
const env = Object.fromEntries(
Object.entries({ ...process.env, ...getConfiguredEnvironment(args) }).map(v => [
v[0],
v[1]?.replace('${port}', port.toString()),
])
)
// launch in CLI mode
if (args.externalConsole) {
const script = await Terminal.launchInTerminal(
cwd,
[runtimeExecutable, ...runtimeArgs, ...program, ...programArgs],
env
)
if (script) {
// we only do this for CLI mode. In normal listen mode, only a thread exited event is send.
script.on('exit', (code: number | null) => {
this.sendEvent(new vscode.ExitedEvent(code ?? 0))
this.sendEvent(new vscode.TerminatedEvent())
})
}
} else {
const script = childProcess.spawn(runtimeExecutable, [...runtimeArgs, ...program, ...programArgs], {
cwd,
env,
})
// redirect output to debug console
script.stdout.on('data', (data: Buffer) => {
this.sendEvent(new vscode.OutputEvent(data.toString(), 'stdout'))
})
script.stderr.on('data', (data: Buffer) => {
this.sendEvent(new vscode.OutputEvent(data.toString(), 'stderr'))
})
// we only do this for CLI mode. In normal listen mode, only a thread exited event is send.
script.on('exit', (code: number | null) => {
this.sendEvent(new vscode.ExitedEvent(code ?? 0))
this.sendEvent(new vscode.TerminatedEvent())
})
script.on('error', (error: Error) => {
this.sendEvent(new vscode.OutputEvent(util.inspect(error) + '\n'))
})
this._phpProcess = script
}
}
/** sets up a TCP server to listen for Xdebug connections */
const createServer = (): Promise<number | string> =>
new Promise<number | string>((resolve, reject) => {
const server = (this._server = net.createServer())
// eslint-disable-next-line @typescript-eslint/no-misused-promises
server.on('connection', async (socket: net.Socket) => {
try {
// new Xdebug connection
// first check if we have a limit on connections
if (args.maxConnections ?? 0 > 0) {
if (this._connections.size >= args.maxConnections!) {
if (args.log) {
this.sendEvent(
new vscode.OutputEvent(
`new connection from ${
socket.remoteAddress || 'unknown'
} - dropping due to max connection limit\n`
),
true
)
}
socket.end()
return
}
}
const connection = new xdebug.Connection(socket)
if (this._args.log) {
this.sendEvent(
new vscode.OutputEvent(
`new connection ${connection.id} from ${socket.remoteAddress || 'unknown'}\n`
),
true
)
}
this.setupConnection(connection)
try {
await this.initializeConnection(connection)
} catch (error) {
this.sendEvent(
new vscode.OutputEvent(
`Failed initializing connection ${connection.id}: ${
error instanceof Error ? error.message : (error as string)
}\n`,
'stderr'
)
)
this.disposeConnection(connection)
socket.destroy()
}
} catch (error) {
this.sendEvent(
new vscode.OutputEvent(
`Error in socket server: ${
error instanceof Error ? error.message : (error as string)
}\n`,
'stderr'
)
)
this.shutdown()
}
})
server.on('error', (error: Error) => {
this.sendEvent(new vscode.OutputEvent(util.inspect(error) + '\n'))
reject(error)
})
server.on('listening', () => {
if (args.log) {
this.sendEvent(new vscode.OutputEvent(`Listening on ${util.inspect(server.address())}\n`), true)
}
if (typeof server.address() === 'string') {
resolve(<string>server.address())
} else {
const port = (server.address() as net.AddressInfo).port
resolve(port)
}
})
if (
args.port !== undefined &&
(args.hostname?.toLowerCase()?.startsWith('unix://') === true ||
args.hostname?.startsWith('\\\\') === true)
) {
throw new Error('Cannot have port and socketPath set at the same time')
}
if (args.hostname?.toLowerCase()?.startsWith('unix://') === true) {
if (fs.existsSync(args.hostname.substring(7))) {
throw new Error(
`File ${args.hostname.substring(7)} exists and cannot be used for Unix Domain socket`
)
}
server.listen(args.hostname.substring(7))
} else if (args.hostname?.startsWith('\\\\') === true) {
server.listen(args.hostname)
} else {
const listenPort = args.port === undefined ? 9003 : args.port
server.listen(listenPort, args.hostname)
}
})
try {
// Some checks
if (
args.env &&
Object.keys(args.env).length !== 0 &&
args.program === undefined &&
args.runtimeArgs === undefined
) {
throw new Error(
`Cannot set env without running a program.\nPlease remove env from [${
args.name || 'unknown'
}] configuration.`
)
}
if (
(args.hostname?.toLowerCase()?.startsWith('unix://') === true ||
args.hostname?.startsWith('\\\\') === true) &&
args.proxy?.enable === true
) {
throw new Error('Proxy does not support socket path listen, only port.')
}
let port = <number | string>0
if (!args.noDebug) {
if (args.xdebugCloudToken) {
port = 9021
await this.setupXdebugCloud(args.xdebugCloudToken)
} else {
port = await createServer()
if (typeof port === 'number' && args.proxy?.enable === true) {
await this.setupProxy(port)
}
}
}
if (args.program || args.runtimeArgs) {
await launchScript(port)
}
} catch (error) {
this.sendErrorResponse(response, error as Error)
return
}
this.sendResponse(response)
// request breakpoints
this.sendEvent(new vscode.InitializedEvent())
}
private setupConnection(connection: xdebug.Connection): void {
this._connections.set(connection.id, connection)
connection.on('warning', (warning: string) => {
this.sendEvent(new vscode.OutputEvent(warning + '\n'))
})
connection.on('error', (error: Error) => {
if (error && this._args?.log) {
this.sendEvent(new vscode.OutputEvent(`connection ${connection.id}: ${error.message}\n`))
}
})
connection.on('close', () => this.disposeConnection(connection))
connection.on('log', (text: string) => {
if (this._args && this._args.log) {
const log = `xd(${connection.id}) ${text}\n`
this.sendEvent(new vscode.OutputEvent(log), true)
}
})
}
private async initializeConnection(connection: xdebug.Connection): Promise<void> {
const initPacket = await connection.waitForInitPacket()
// check if this connection should be skipped
if (
this._args.skipEntryPaths &&
isPositiveMatchInGlobs(
convertDebuggerPathToClient(initPacket.fileUri).replace(/\\/g, '/'),
this._args.skipEntryPaths
)
) {
this.sendEvent(
new vscode.OutputEvent(
`skipping entry point ${convertDebuggerPathToClient(initPacket.fileUri).replace(
/\\/g,
'/'
)} on connection ${connection.id}\n`
)
)
this.disposeConnection(connection)
return
}
// 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 })
if (
supportedEngine ||
((feat = await connection.sendFeatureGetCommand('resolved_breakpoints')) && feat.supported === '1')
) {
await connection.sendFeatureSetCommand('resolved_breakpoints', '1')
}
if (
supportedEngine ||
((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 ||
((feat = await connection.sendFeatureGetCommand('extended_properties')) && feat.supported === '1')
) {
await connection.sendFeatureSetCommand('extended_properties', '1')
}
if (
supportedEngine32 ||
((feat = await connection.sendFeatureGetCommand('breakpoint_include_return_value')) &&
feat.supported === '1')
) {
await connection.sendFeatureSetCommand('breakpoint_include_return_value', '1')
}
// override features from launch.json
try {
const xdebugSettings = this._args.xdebugSettings || {}
// Required defaults for indexedVariables
xdebugSettings.max_children = xdebugSettings.max_children || 100
await Promise.all(
Object.keys(xdebugSettings).map(setting =>
connection.sendFeatureSetCommand(setting, xdebugSettings[setting])
)
)
this._args.xdebugSettings = xdebugSettings
} catch (error) {
throw new Error(`Error applying xdebugSettings: ${String(error instanceof Error ? error.message : error)}`)
}
const stdout =
this._args.stream?.stdout === undefined ? (this._args.externalConsole ? 1 : 0) : this._args.stream.stdout
if (stdout) {
await connection.sendStdout(stdout)
connection.on('stream', (stream: xdebug.Stream) =>
this.sendEvent(new vscode.OutputEvent(stream.value, 'stdout'))
)
}
this.sendEvent(new vscode.ThreadEvent('started', connection.id))
// wait for all breakpoints
await this._donePromise
const bpa = new BreakpointAdapter(connection, this._breakpointManager)
bpa.on('dapEvent', event => this.sendEvent(event))
this._breakpointAdapters.set(connection, bpa)
// sync breakpoints to connection
await bpa.process()
let xdebugResponse: xdebug.StatusResponse
// either tell VS Code we stopped on entry or run the script
if (this._args.stopOnEntry) {
// do one step to the first statement
this._hasStoppedOnEntry = false
xdebugResponse = await connection.sendStepIntoCommand()
} else {
xdebugResponse = await connection.sendRunCommand()
}
await this._checkStatus(xdebugResponse)
}
private disposeConnection(connection: xdebug.Connection): void {
if (this._connections.has(connection.id)) {
if (this._args.log) {
this.sendEvent(new vscode.OutputEvent(`connection ${connection.id} closed\n`))
}
this.sendEvent(new vscode.ContinuedEvent(connection.id, false))
this.sendEvent(new vscode.ThreadEvent('exited', connection.id))
connection
.close()
.catch(err => this.sendEvent(new vscode.OutputEvent(`connection ${connection.id}: ${err as string}\n`)))
this._connections.delete(connection.id)
this._statuses.delete(connection)
this._breakpointAdapters.delete(connection)
this._skippingFiles.delete(connection.id)
}
}
private async setupXdebugCloud(token: string): Promise<void> {
this._xdebugCloudConnection = new XdebugCloudConnection(token)
this._xdebugCloudConnection.on('log', (text: string) => {
if (this._args && this._args.log) {
const log = `xdc ${text}\n`
this.sendEvent(new vscode.OutputEvent(log), true)
}
})
this._xdebugCloudConnection.on('connection', (connection: xdebug.Connection) => {
this.setupConnection(connection)
if (this._args.log) {
this.sendEvent(new vscode.OutputEvent(`new connection ${connection.id} from cloud\n`), true)
}
this.initializeConnection(connection).catch(error => {
this.sendEvent(
new vscode.OutputEvent(
`Failed initializing connection ${connection.id}: ${
error instanceof Error ? error.message : (error as string)
}\n`,
'stderr'
)
)
this.disposeConnection(connection)
})
})
try {
const xdc = new XdebugCloudConnection(token)
xdc.on('log', (text: string) => {
if (this._args && this._args.log) {
const log = `xdc2 ${text}\n`
this.sendEvent(new vscode.OutputEvent(log), true)
}
})
await xdc.connectAndStop()
} catch (error) {
// just ignore
}
await this._xdebugCloudConnection.connect()
this._xdebugCloudConnection.once('close', () => {
this.sendEvent(new vscode.TerminatedEvent())
})
}
private async setupProxy(idePort: number): Promise<void> {
this._proxyConnect = new ProxyConnect(
this._args.proxy!.host,
this._args.proxy!.port,
idePort,
this._args.proxy!.allowMultipleSessions,
this._args.proxy!.key,
this._args.proxy!.timeout
)
const proxyConsole = (str: string) => this.sendEvent(new vscode.OutputEvent(str + '\n'), true)
this._proxyConnect.on('log_request', proxyConsole)
this._proxyConnect.on('log_response', proxyConsole)
this._proxyConnect.on('log_error', (error: Error) => {
this.sendEvent(new vscode.OutputEvent('PROXY ERROR: ' + error.message + '\n', 'stderr'))
})
return this._proxyConnect.sendProxyInitCommand()
}
/**
* Checks the status of a StatusResponse and notifies VS Code accordingly
*
* @param {xdebug.StatusResponse} response
*/
private async _checkStatus(response: xdebug.StatusResponse): Promise<void> {
const connection = response.connection
this._statuses.set(connection, response)
if (response.status === 'stopping') {
const response = await connection.sendStopCommand()
await this._checkStatus(response)
} else if (response.status === 'stopped') {
this._connections.delete(connection.id)
this._statuses.delete(connection)
this._breakpointAdapters.delete(connection)
this.sendEvent(new vscode.ThreadEvent('exited', connection.id))
await connection.close()
} else if (response.status === 'break') {
// First sync breakpoints
const bpa = this._breakpointAdapters.get(connection)
if (bpa) {
await bpa.process()
}
// StoppedEvent reason can be 'step', 'breakpoint', 'exception' or 'pause'
let stoppedEventReason: 'step' | 'breakpoint' | 'exception' | 'pause' | 'entry'
let exceptionText: string | undefined
if (response.exception) {
// If one of the ignore patterns matches, ignore this exception
if (
// ignore files
(this._args.ignore &&
this._args.ignore.some(glob =>
minimatch(convertDebuggerPathToClient(response.fileUri).replace(/\\/g, '/'), glob)
)) ||
// ignore exception class name
(this._args.ignoreExceptions &&
shouldIgnoreException(response.exception.name, this._args.ignoreExceptions))
) {
const response = await connection.sendRunCommand()
await this._checkStatus(response)
return
}
stoppedEventReason = 'exception'
exceptionText = response.exception.name + ': ' + response.exception.message // this seems to be ignored currently by VS Code
} else if (this._args.stopOnEntry && !this._hasStoppedOnEntry) {
stoppedEventReason = 'entry'
this._hasStoppedOnEntry = true
} else if (response.command.startsWith('step')) {
await this._processLogPoints(response)
// check just my code
if (
this._args.skipFiles &&
isPositiveMatchInGlobs(
convertDebuggerPathToClient(response.fileUri).replace(/\\/g, '/'),
this._args.skipFiles
)
) {
if (!this._skippingFiles.has(connection.id)) {
this._skippingFiles.set(connection.id, true)
}
if (this._skippingFiles.get(connection.id)) {
let stepResponse
switch (response.command) {
case 'step_out':
stepResponse = await connection.sendStepOutCommand()
break
case 'step_over':
stepResponse = await connection.sendStepOverCommand()
break
default:
stepResponse = await connection.sendStepIntoCommand()
}
await this._checkStatus(stepResponse)
return
}
this._skippingFiles.delete(connection.id)
}
stoppedEventReason = 'step'
} else {
if (await this._processLogPoints(response)) {
const responseCommand = await connection.sendRunCommand()
await this._checkStatus(responseCommand)
return
}
stoppedEventReason = 'breakpoint'
}
const event: VSCodeDebugProtocol.StoppedEvent = new vscode.StoppedEvent(
stoppedEventReason,
connection.id,
exceptionText
)
event.body.allThreadsStopped = false
this.sendEvent(event)
}
}
private async _processLogPoints(response: xdebug.StatusResponse): Promise<boolean> {
const connection = response.connection
if (this._logPointManager.hasLogPoint(response.fileUri, response.line)) {
const logMessage = await this._logPointManager.resolveExpressions(
response.fileUri,
response.line,
async (expr: string): Promise<string> => {
const evaluated = await connection.sendEvalCommand(expr)
return formatPropertyValue(evaluated.result)
}
)
this.sendEvent(new vscode.OutputEvent(logMessage + '\n', 'console'))
return true
}
return false
}
/** Logs all requests before dispatching */
protected dispatchRequest(request: VSCodeDebugProtocol.Request): void {
if (this._args?.log) {
const log = `-> ${request.command}Request\n${util.inspect(request, { depth: Infinity, compact: true })}\n\n`
super.sendEvent(new vscode.OutputEvent(log))
}
super.dispatchRequest(request)
}
public sendEvent(event: VSCodeDebugProtocol.Event, bypassLog: boolean = false): void {
if (this._args?.log && !bypassLog) {
const log = `<- ${event.event}Event\n${util.inspect(event, { depth: Infinity, compact: true })}\n\n`
super.sendEvent(new vscode.OutputEvent(log))
}
super.sendEvent(event)
}
public sendResponse(response: VSCodeDebugProtocol.Response): void {
if (this._args?.log) {
const log = `<- ${response.command}Response\n${util.inspect(response, {
depth: Infinity,
compact: true,
})}\n\n`
super.sendEvent(new vscode.OutputEvent(log))
}
super.sendResponse(response)
}
protected sendErrorResponse(
response: VSCodeDebugProtocol.Response,
error: Error,
dest?: vscode.ErrorDestination
): void
protected sendErrorResponse(
response: VSCodeDebugProtocol.Response,
codeOrMessage: number | VSCodeDebugProtocol.Message,
format?: string,
variables?: any,
dest?: vscode.ErrorDestination
): void
protected sendErrorResponse(response: VSCodeDebugProtocol.Response) {
if (arguments[1] instanceof Error) {
const error = arguments[1] as Error & { code?: number | string; errno?: number }
const dest = arguments[2] as vscode.ErrorDestination
let code: number
if (typeof error.code === 'number') {
code = error.code
} else if (typeof error.errno === 'number') {
code = error.errno
} else {
code = 0
}
super.sendErrorResponse(response, code, error.message, dest)
} else {
super.sendErrorResponse(
response,
arguments[1] as number,
arguments[2] as string,
arguments[3],
arguments[4] as vscode.ErrorDestination
)
}
}
protected handleUserNotify(notify: xdebug.UserNotify, connection: xdebug.Connection) {
if (notify.property !== undefined) {
const event: VSCodeDebugProtocol.OutputEvent = new vscode.OutputEvent('', 'stdout')
const property = new xdebug.SyntheticProperty('', 'object', formatPropertyValue(notify.property), [
notify.property,
])
const variablesReference = this._variableIdCounter++
this._evalResultProperties.set(variablesReference, property)
event.body.variablesReference = variablesReference
if (notify.fileUri.startsWith('file://')) {
const filePath = convertDebuggerPathToClient(notify.fileUri, this._args.pathMappings)
event.body.source = { name: path.basename(filePath), path: filePath }
event.body.line = notify.line
}
this.sendEvent(event)
}
}
/** This is called for each source file that has breakpoints with all the breakpoints in that file and whenever these change. */
protected setBreakPointsRequest(
response: VSCodeDebugProtocol.SetBreakpointsResponse,
args: VSCodeDebugProtocol.SetBreakpointsArguments
): void {
try {
const fileUri = convertClientPathToDebugger(args.source.path!, this._args.pathMappings)
const vscodeBreakpoints = this._breakpointManager.setBreakPoints(args.source, fileUri, args.breakpoints!)
response.body = { breakpoints: vscodeBreakpoints }
// Process logpoints
this._logPointManager.clearFromFile(fileUri)
args.breakpoints!.filter(breakpoint => breakpoint.logMessage).forEach(breakpoint => {
this._logPointManager.addLogPoint(fileUri, breakpoint.line, breakpoint.logMessage!)
})
} catch (error) {
this.sendErrorResponse(response, error as Error)
return
}
this.sendResponse(response)
this._breakpointManager.process()
}
/** This is called once after all line breakpoints have been set and whenever the breakpoints settings change */
protected setExceptionBreakPointsRequest(
response: VSCodeDebugProtocol.SetExceptionBreakpointsResponse,
args: VSCodeDebugProtocol.SetExceptionBreakpointsArguments
): void {
try {
const vscodeBreakpoints = this._breakpointManager.setExceptionBreakPoints(args.filters)
response.body = { breakpoints: vscodeBreakpoints }
} catch (error) {
this.sendErrorResponse(response, error as Error)
return
}
this.sendResponse(response)
this._breakpointManager.process()
}
protected setFunctionBreakPointsRequest(
response: VSCodeDebugProtocol.SetFunctionBreakpointsResponse,
args: VSCodeDebugProtocol.SetFunctionBreakpointsArguments
): void {
try {
const vscodeBreakpoints = this._breakpointManager.setFunctionBreakPointsRequest(args.breakpoints)
response.body = { breakpoints: vscodeBreakpoints }
} catch (error) {
this.sendErrorResponse(response, error as Error)
return
}
this.sendResponse(response)
this._breakpointManager.process()
}
/** Executed after all breakpoints have been set by VS Code */
protected configurationDoneRequest(
response: VSCodeDebugProtocol.ConfigurationDoneResponse,
args: VSCodeDebugProtocol.ConfigurationDoneArguments
): void {
this.sendResponse(response)
this._donePromiseResolveFn()
}
/** Executed after a successful launch or attach request and after a ThreadEvent */
protected threadsRequest(response: VSCodeDebugProtocol.ThreadsResponse): void {
// PHP doesn't have threads, but it may have multiple requests in parallel.
// Think about a website that makes multiple, parallel AJAX requests to your PHP backend.
// Xdebug opens a new socket connection for each of them, we tell VS Code that these are our threads.
const connections = Array.from(this._connections.values())
response.body = {
threads: connections.map(
connection =>
new vscode.Thread(
connection.id,
`Request ${connection.id} (${moment(connection.timeEstablished).format('LTS')})`
)
),
}
this.sendResponse(response)
}
/** Called by VS Code after a StoppedEvent */
protected async stackTraceRequest(
response: VSCodeDebugProtocol.StackTraceResponse,
args: VSCodeDebugProtocol.StackTraceArguments
): Promise<void> {
try {
const connection = this._connections.get(args.threadId)
if (!connection) {
throw new Error('Unknown thread ID')
}
let { stack } = await connection.sendStackGetCommand()
// First delete the old stack trace info ???
// this._stackFrames.clear();
// this._properties.clear();
// this._contexts.clear();
const status = this._statuses.get(connection)
if (stack.length === 0 && status && status.exception) {
// special case: if a fatal error occurs (for example after an uncaught exception), the stack trace is EMPTY.
// in that case, VS Code would normally not show any information to the user at all
// to avoid this, we create a virtual stack frame with the info from the last status response we got
const status = this._statuses.get(connection)!
const id = this._stackFrameIdCounter++
const name = status.exception.name
let line = status.line
let source: VSCodeDebugProtocol.Source
const urlObject = url.parse(status.fileUri)
if (urlObject.protocol === 'dbgp:') {
let sourceReference
const src = Array.from(this._sources).find(
([, v]) => v.url === status.fileUri && v.connection === connection
)
if (src) {
sourceReference = src[0]
} else {
sourceReference = this._sourceIdCounter++
this._sources.set(sourceReference, { connection, url: status.fileUri })
}
// for eval code, we need to include .php extension to get syntax highlighting
source = { name: status.exception.name + '.php', sourceReference, origin: status.exception.name }
// for eval code, we add a "<?php" line at the beginning to get syntax highlighting (see sourceRequest)