Skip to content

Commit

Permalink
feat: Xdebug Cloud support (#842)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
zobo authored Oct 9, 2022
1 parent 922add3 commit cc18cd4
Show file tree
Hide file tree
Showing 9 changed files with 658 additions and 155 deletions.
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.29.0]

- Xdebug Cloud support.

## [1.28.0]

- Support for envFile.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ More general information on debugging with VS Code can be found on https://code.
- `max_data`: max amount of variable data to initially retrieve.
- `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).
- `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.
- `xdebugCloudToken`: Instead of listening locally, open a connection and register with Xdebug Cloud and accept debugging sessions on that connection.

Options specific to CLI debugging:

Expand Down Expand Up @@ -121,6 +122,7 @@ Options specific to CLI debugging:
- Run as CLI
- Run without debugging
- DBGp Proxy registration and unregistration support
- Xdebug Cloud support

## Remote Host Debugging

Expand Down
28 changes: 21 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@vscode/debugadapter": "^1.57.0",
"@vscode/debugprotocol": "^1.55.1",
"@xmldom/xmldom": "^0.8.2",
"buffer-crc32": "^0.2.13",
"dotenv": "^16.0.1",
"file-url": "^3.0.0",
"iconv-lite": "^0.6.3",
Expand All @@ -61,6 +62,7 @@
"devDependencies": {
"@commitlint/cli": "^17.1.1",
"@commitlint/config-conventional": "^17.1.0",
"@types/buffer-crc32": "^0.2.0",
"@types/chai": "4.3.3",
"@types/chai-as-promised": "^7.1.5",
"@types/minimatch": "^5.1.0",
Expand Down Expand Up @@ -337,6 +339,10 @@
"type": "number",
"description": "The maximum allowed parallel debugging sessions",
"default": 0
},
"xdebugCloudToken": {
"type": "string",
"description": "Xdebug Could token"
}
}
}
Expand Down Expand Up @@ -464,6 +470,16 @@
"action": "openExternally"
}
}
},
{
"label": "PHP: Xdebug Cloud",
"description": "Register with Xdebug Cloud and wait for debug sessions",
"body": {
"name": "Xdebug Cloud",
"type": "php",
"request": "launch",
"xdebugCloudToken": "${1}"
}
}
]
}
Expand Down
219 changes: 219 additions & 0 deletions src/cloud.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import * as crc32 from 'buffer-crc32'
import * as net from 'net'
import { Transport, DbgpConnection, ENCODING } from './dbgp'
import * as tls from 'tls'
import * as iconv from 'iconv-lite'
import * as xdebug from './xdebugConnection'
import { EventEmitter } from 'stream'

export declare interface XdebugCloudConnection {
on(event: 'error', listener: (error: Error) => void): this
on(event: 'close', listener: () => void): this
on(event: 'log', listener: (text: string) => void): this
on(event: 'connection', listener: (conn: xdebug.Connection) => void): this
}

export class XdebugCloudConnection extends EventEmitter {
private _token: string

private _netSocket: net.Socket
private _tlsSocket: net.Socket

private _resolveFn: (() => void) | null
private _rejectFn: ((error?: Error) => void) | null

private _dbgpConnection: DbgpConnection

constructor(token: string, testSocket?: net.Socket) {
super()
if (testSocket != null) {
this._netSocket = testSocket
this._tlsSocket = testSocket
} else {
this._netSocket = new net.Socket()
this._tlsSocket = new tls.TLSSocket(this._netSocket)
}
this._token = token
this._resolveFn = null
this._rejectFn = null
this._dbgpConnection = new DbgpConnection(this._tlsSocket)

this._dbgpConnection.on('message', (response: XMLDocument) => {
this.emit('log', response)
if (response.documentElement.nodeName === 'cloudinit') {
if (response.documentElement.firstChild && response.documentElement.firstChild.nodeName === 'error') {
this._rejectFn?.(
new Error(`Error in CloudInit ${response.documentElement.firstChild.textContent ?? ''}`)
)
} else {
this._resolveFn?.()
}
} else if (response.documentElement.nodeName === 'cloudstop') {
if (response.documentElement.firstChild && response.documentElement.firstChild.nodeName === 'error') {
this._rejectFn?.(
new Error(`Error in CloudStop ${response.documentElement.firstChild.textContent ?? ''}`)
)
} else {
this._resolveFn?.()
}
} else if (response.documentElement.nodeName === 'init') {
// spawn a new xdebug.Connection
const cx = new xdebug.Connection(new InnerCloudTransport(this._tlsSocket))
cx.emit('message', response)
this.emit('connection', cx)
}
})

this._dbgpConnection.on('error', (err: Error) => {
this.emit('log', `dbgp error: ${err.toString()}`)
this._rejectFn?.(err instanceof Error ? err : new Error(err))
})
/*
this._netSocket.on('error', (err: Error) => {
this.emit('log', `netSocket error ${err.toString()}`)
this._rejectFn?.(err instanceof Error ? err : new Error(err))
})
*/

/*
this._netSocket.on('connect', () => {
this.emit('log', `netSocket connected`)
// this._resolveFn?.()
})
this._tlsSocket.on('secureConnect', () => {
this.emit('log', `tlsSocket secureConnect`)
//this._resolveFn?.()
})
*/

/*
this._netSocket.on('close', had_error => {
this.emit('log', 'netSocket close')
this._rejectFn?.() // err instanceof Error ? err : new Error(err))
})
this._tlsSocket.on('close', had_error => {
this.emit('log', 'tlsSocket close')
this._rejectFn?.()
})
*/
this._dbgpConnection.on('close', () => {
this.emit('log', `dbgp close`)
this._rejectFn?.() // err instanceof Error ? err : new Error(err))
this.emit('close')
})
}

private computeCloudHost(token: string): string {
const c = crc32.default(token)
const last = c[3] & 0x0f
const url = `${String.fromCharCode(97 + last)}.cloud.xdebug.com`

return url
}

public async connect(): Promise<void> {
await new Promise<void>((resolveFn, rejectFn) => {
this._resolveFn = resolveFn
this._rejectFn = rejectFn

this._netSocket
.connect(
{
host: this.computeCloudHost(this._token),
servername: this.computeCloudHost(this._token),
port: 9021,
} as net.SocketConnectOpts,
resolveFn
)
.on('error', rejectFn)
})

const commandString = `cloudinit -i 1 -u ${this._token}\0`
const data = iconv.encode(commandString, ENCODING)

const p2 = new Promise<void>((resolveFn, rejectFn) => {
this._resolveFn = resolveFn
this._rejectFn = rejectFn
})

await this._dbgpConnection.write(data)

await p2
}

public async stop(): Promise<void> {
if (!this._tlsSocket.writable) {
return Promise.resolve()
}

const commandString = `cloudstop -i 2 -u ${this._token}\0`
const data = iconv.encode(commandString, ENCODING)

const p2 = new Promise<void>((resolveFn, rejectFn) => {
this._resolveFn = resolveFn
this._rejectFn = rejectFn
})

await this._dbgpConnection.write(data)
return p2
}

public async close(): Promise<void> {
return new Promise<void>(resolve => {
this._tlsSocket.end(resolve)
})
}

public async connectAndStop(): Promise<void> {
await new Promise<void>((resolveFn, rejectFn) => {
// this._resolveFn = resolveFn
this._rejectFn = rejectFn
this._netSocket
.connect(
{
host: this.computeCloudHost(this._token),
servername: this.computeCloudHost(this._token),
port: 9021,
} as net.SocketConnectOpts,
resolveFn
)
.on('error', rejectFn)
})
await this.stop()
await this.close()
}
}

class InnerCloudTransport extends EventEmitter implements Transport {
private _open = true

constructor(private _socket: net.Socket) {
super()

this._socket.on('data', (data: Buffer) => {
if (this._open) this.emit('data', data)
})
this._socket.on('error', (error: Error) => {
if (this._open) this.emit('error', error)
})
this._socket.on('close', () => {
if (this._open) this.emit('close')
})
}

public get writable(): boolean {
return this._open && this._socket.writable
}

write(buffer: string | Uint8Array, cb?: ((err?: Error | undefined) => void) | undefined): boolean {
return this._socket.write(buffer, cb)
}

end(callback?: (() => void) | undefined): this {
if (this._open) {
this._open = false
this.emit('close')
}
return this
}
}
Loading

0 comments on commit cc18cd4

Please sign in to comment.