Skip to content

Commit cc18cd4

Browse files
authored
feat: Xdebug Cloud support (#842)
* Xdebug Cloud support * Small refarcor of cloud.ts. Fixing lint errors. * Refactor and tests * Added tests for connection. * Extended xdc tests, need to make some calls async with setTimeout so that internal dbgp logic can clean up. Removed extra logging. * Try to unregister with stop, same as on startup. * Fix lint * Docs. * Configuration snippet. * Add logging of unregister xdc connection. Remove comments.
1 parent 922add3 commit cc18cd4

File tree

9 files changed

+658
-155
lines changed

9 files changed

+658
-155
lines changed

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.29.0]
8+
9+
- Xdebug Cloud support.
10+
711
## [1.28.0]
812

913
- Support for envFile.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ More general information on debugging with VS Code can be found on https://code.
9090
- `max_data`: max amount of variable data to initially retrieve.
9191
- `max_depth`: maximum depth that the debugger engine may return when sending arrays, hashes or object structures to the IDE (there should be no need to change this as depth is retrieved incrementally, large value can cause IDE to hang).
9292
- `show_hidden`: This feature can get set by the IDE if it wants to have more detailed internal information on properties (eg. private members of classes, etc.) Zero means that hidden members are not shown to the IDE.
93+
- `xdebugCloudToken`: Instead of listening locally, open a connection and register with Xdebug Cloud and accept debugging sessions on that connection.
9394

9495
Options specific to CLI debugging:
9596

@@ -121,6 +122,7 @@ Options specific to CLI debugging:
121122
- Run as CLI
122123
- Run without debugging
123124
- DBGp Proxy registration and unregistration support
125+
- Xdebug Cloud support
124126

125127
## Remote Host Debugging
126128

package-lock.json

Lines changed: 21 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"@vscode/debugadapter": "^1.57.0",
4747
"@vscode/debugprotocol": "^1.55.1",
4848
"@xmldom/xmldom": "^0.8.2",
49+
"buffer-crc32": "^0.2.13",
4950
"dotenv": "^16.0.1",
5051
"file-url": "^3.0.0",
5152
"iconv-lite": "^0.6.3",
@@ -61,6 +62,7 @@
6162
"devDependencies": {
6263
"@commitlint/cli": "^17.1.1",
6364
"@commitlint/config-conventional": "^17.1.0",
65+
"@types/buffer-crc32": "^0.2.0",
6466
"@types/chai": "4.3.3",
6567
"@types/chai-as-promised": "^7.1.5",
6668
"@types/minimatch": "^5.1.0",
@@ -337,6 +339,10 @@
337339
"type": "number",
338340
"description": "The maximum allowed parallel debugging sessions",
339341
"default": 0
342+
},
343+
"xdebugCloudToken": {
344+
"type": "string",
345+
"description": "Xdebug Could token"
340346
}
341347
}
342348
}
@@ -464,6 +470,16 @@
464470
"action": "openExternally"
465471
}
466472
}
473+
},
474+
{
475+
"label": "PHP: Xdebug Cloud",
476+
"description": "Register with Xdebug Cloud and wait for debug sessions",
477+
"body": {
478+
"name": "Xdebug Cloud",
479+
"type": "php",
480+
"request": "launch",
481+
"xdebugCloudToken": "${1}"
482+
}
467483
}
468484
]
469485
}

src/cloud.ts

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import * as crc32 from 'buffer-crc32'
2+
import * as net from 'net'
3+
import { Transport, DbgpConnection, ENCODING } from './dbgp'
4+
import * as tls from 'tls'
5+
import * as iconv from 'iconv-lite'
6+
import * as xdebug from './xdebugConnection'
7+
import { EventEmitter } from 'stream'
8+
9+
export declare interface XdebugCloudConnection {
10+
on(event: 'error', listener: (error: Error) => void): this
11+
on(event: 'close', listener: () => void): this
12+
on(event: 'log', listener: (text: string) => void): this
13+
on(event: 'connection', listener: (conn: xdebug.Connection) => void): this
14+
}
15+
16+
export class XdebugCloudConnection extends EventEmitter {
17+
private _token: string
18+
19+
private _netSocket: net.Socket
20+
private _tlsSocket: net.Socket
21+
22+
private _resolveFn: (() => void) | null
23+
private _rejectFn: ((error?: Error) => void) | null
24+
25+
private _dbgpConnection: DbgpConnection
26+
27+
constructor(token: string, testSocket?: net.Socket) {
28+
super()
29+
if (testSocket != null) {
30+
this._netSocket = testSocket
31+
this._tlsSocket = testSocket
32+
} else {
33+
this._netSocket = new net.Socket()
34+
this._tlsSocket = new tls.TLSSocket(this._netSocket)
35+
}
36+
this._token = token
37+
this._resolveFn = null
38+
this._rejectFn = null
39+
this._dbgpConnection = new DbgpConnection(this._tlsSocket)
40+
41+
this._dbgpConnection.on('message', (response: XMLDocument) => {
42+
this.emit('log', response)
43+
if (response.documentElement.nodeName === 'cloudinit') {
44+
if (response.documentElement.firstChild && response.documentElement.firstChild.nodeName === 'error') {
45+
this._rejectFn?.(
46+
new Error(`Error in CloudInit ${response.documentElement.firstChild.textContent ?? ''}`)
47+
)
48+
} else {
49+
this._resolveFn?.()
50+
}
51+
} else if (response.documentElement.nodeName === 'cloudstop') {
52+
if (response.documentElement.firstChild && response.documentElement.firstChild.nodeName === 'error') {
53+
this._rejectFn?.(
54+
new Error(`Error in CloudStop ${response.documentElement.firstChild.textContent ?? ''}`)
55+
)
56+
} else {
57+
this._resolveFn?.()
58+
}
59+
} else if (response.documentElement.nodeName === 'init') {
60+
// spawn a new xdebug.Connection
61+
const cx = new xdebug.Connection(new InnerCloudTransport(this._tlsSocket))
62+
cx.emit('message', response)
63+
this.emit('connection', cx)
64+
}
65+
})
66+
67+
this._dbgpConnection.on('error', (err: Error) => {
68+
this.emit('log', `dbgp error: ${err.toString()}`)
69+
this._rejectFn?.(err instanceof Error ? err : new Error(err))
70+
})
71+
/*
72+
this._netSocket.on('error', (err: Error) => {
73+
this.emit('log', `netSocket error ${err.toString()}`)
74+
this._rejectFn?.(err instanceof Error ? err : new Error(err))
75+
})
76+
*/
77+
78+
/*
79+
this._netSocket.on('connect', () => {
80+
this.emit('log', `netSocket connected`)
81+
// this._resolveFn?.()
82+
})
83+
this._tlsSocket.on('secureConnect', () => {
84+
this.emit('log', `tlsSocket secureConnect`)
85+
//this._resolveFn?.()
86+
})
87+
*/
88+
89+
/*
90+
this._netSocket.on('close', had_error => {
91+
this.emit('log', 'netSocket close')
92+
this._rejectFn?.() // err instanceof Error ? err : new Error(err))
93+
})
94+
this._tlsSocket.on('close', had_error => {
95+
this.emit('log', 'tlsSocket close')
96+
this._rejectFn?.()
97+
})
98+
*/
99+
this._dbgpConnection.on('close', () => {
100+
this.emit('log', `dbgp close`)
101+
this._rejectFn?.() // err instanceof Error ? err : new Error(err))
102+
this.emit('close')
103+
})
104+
}
105+
106+
private computeCloudHost(token: string): string {
107+
const c = crc32.default(token)
108+
const last = c[3] & 0x0f
109+
const url = `${String.fromCharCode(97 + last)}.cloud.xdebug.com`
110+
111+
return url
112+
}
113+
114+
public async connect(): Promise<void> {
115+
await new Promise<void>((resolveFn, rejectFn) => {
116+
this._resolveFn = resolveFn
117+
this._rejectFn = rejectFn
118+
119+
this._netSocket
120+
.connect(
121+
{
122+
host: this.computeCloudHost(this._token),
123+
servername: this.computeCloudHost(this._token),
124+
port: 9021,
125+
} as net.SocketConnectOpts,
126+
resolveFn
127+
)
128+
.on('error', rejectFn)
129+
})
130+
131+
const commandString = `cloudinit -i 1 -u ${this._token}\0`
132+
const data = iconv.encode(commandString, ENCODING)
133+
134+
const p2 = new Promise<void>((resolveFn, rejectFn) => {
135+
this._resolveFn = resolveFn
136+
this._rejectFn = rejectFn
137+
})
138+
139+
await this._dbgpConnection.write(data)
140+
141+
await p2
142+
}
143+
144+
public async stop(): Promise<void> {
145+
if (!this._tlsSocket.writable) {
146+
return Promise.resolve()
147+
}
148+
149+
const commandString = `cloudstop -i 2 -u ${this._token}\0`
150+
const data = iconv.encode(commandString, ENCODING)
151+
152+
const p2 = new Promise<void>((resolveFn, rejectFn) => {
153+
this._resolveFn = resolveFn
154+
this._rejectFn = rejectFn
155+
})
156+
157+
await this._dbgpConnection.write(data)
158+
return p2
159+
}
160+
161+
public async close(): Promise<void> {
162+
return new Promise<void>(resolve => {
163+
this._tlsSocket.end(resolve)
164+
})
165+
}
166+
167+
public async connectAndStop(): Promise<void> {
168+
await new Promise<void>((resolveFn, rejectFn) => {
169+
// this._resolveFn = resolveFn
170+
this._rejectFn = rejectFn
171+
this._netSocket
172+
.connect(
173+
{
174+
host: this.computeCloudHost(this._token),
175+
servername: this.computeCloudHost(this._token),
176+
port: 9021,
177+
} as net.SocketConnectOpts,
178+
resolveFn
179+
)
180+
.on('error', rejectFn)
181+
})
182+
await this.stop()
183+
await this.close()
184+
}
185+
}
186+
187+
class InnerCloudTransport extends EventEmitter implements Transport {
188+
private _open = true
189+
190+
constructor(private _socket: net.Socket) {
191+
super()
192+
193+
this._socket.on('data', (data: Buffer) => {
194+
if (this._open) this.emit('data', data)
195+
})
196+
this._socket.on('error', (error: Error) => {
197+
if (this._open) this.emit('error', error)
198+
})
199+
this._socket.on('close', () => {
200+
if (this._open) this.emit('close')
201+
})
202+
}
203+
204+
public get writable(): boolean {
205+
return this._open && this._socket.writable
206+
}
207+
208+
write(buffer: string | Uint8Array, cb?: ((err?: Error | undefined) => void) | undefined): boolean {
209+
return this._socket.write(buffer, cb)
210+
}
211+
212+
end(callback?: (() => void) | undefined): this {
213+
if (this._open) {
214+
this._open = false
215+
this.emit('close')
216+
}
217+
return this
218+
}
219+
}

0 commit comments

Comments
 (0)