diff --git a/package.json b/package.json index 107e0641..ba783cb5 100644 --- a/package.json +++ b/package.json @@ -366,6 +366,17 @@ "xdebugCloudToken": { "type": "string", "description": "Xdebug Could token" + }, + "stream": { + "type": "object", + "description": "Xdebug stream settings", + "properties": { + "stdout": { + "type": "number", + "description": "Redirect stdout stream: 0 (disable), 1 (copy), 2 (redirect)", + "default": 0 + } + } } } } diff --git a/src/phpDebug.ts b/src/phpDebug.ts index a04eed72..5fc152f2 100644 --- a/src/phpDebug.ts +++ b/src/phpDebug.ts @@ -98,6 +98,10 @@ export interface LaunchRequestArguments extends VSCodeDebugProtocol.LaunchReques maxConnections?: number /** Xdebug cloud token */ xdebugCloudToken?: string + /** Xdebug stream settings */ + stream?: { + stdout?: 0 | 1 | 2 + } // CLI options @@ -561,6 +565,15 @@ class PhpDebugSession extends vscode.DebugSession { 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 diff --git a/src/test/adapter.ts b/src/test/adapter.ts index c4c8ad9f..f343b03a 100644 --- a/src/test/adapter.ts +++ b/src/test/adapter.ts @@ -5,7 +5,8 @@ import { DebugClient } from '@vscode/debugadapter-testsupport' import { DebugProtocol } from '@vscode/debugprotocol' import * as semver from 'semver' import * as net from 'net' -import { describe, it, beforeEach, afterEach } from 'mocha' +import * as childProcess from 'child_process' +import { describe, it, beforeEach, afterEach, after } from 'mocha' chai.use(chaiAsPromised) const assert = chai.assert @@ -822,6 +823,20 @@ describe('PHP Debug Adapter', () => { }) }) + describe('stream tests', () => { + const program = path.join(TEST_PROJECT, 'output.php') + + it('listen with externalConsole', async () => { + // this is how we can currently turn on stdout redirect + await Promise.all([client.launch({ stream: { stdout: '1' } }), client.configurationSequence()]) + + const script = childProcess.spawn('php', [program]) + after(() => script.kill()) + await client.assertOutput('stdout', 'stdout output 1') + await client.assertOutput('stdout', 'stdout output 2') + }) + }) + describe('special adapter tests', () => { it('max connections', async () => { await Promise.all([client.launch({ maxConnections: 1, log: true }), client.configurationSequence()]) diff --git a/src/xdebugConnection.ts b/src/xdebugConnection.ts index f1908a4b..1df7e760 100644 --- a/src/xdebugConnection.ts +++ b/src/xdebugConnection.ts @@ -179,6 +179,24 @@ export class UserNotify extends Notify { } } +export class Stream { + /** Type of stream */ + type: string + /** Data of the stream */ + value: string + + /** Constructs a stream object from an XML node from a Xdebug response */ + constructor(document: XMLDocument) { + this.type = document.documentElement.getAttribute('type')! + const encoding = document.documentElement.getAttribute('encoding') + if (encoding) { + this.value = iconv.encode(document.documentElement.textContent!, encoding).toString() + } else { + this.value = document.documentElement.textContent! + } + } +} + export type BreakpointType = 'line' | 'call' | 'return' | 'exception' | 'conditional' | 'watch' export type BreakpointState = 'enabled' | 'disabled' export type BreakpointResolved = 'resolved' | 'unresolved' @@ -745,6 +763,7 @@ export declare interface Connection extends DbgpConnection { on(event: 'log', listener: (text: string) => void): this on(event: 'notify_user', listener: (notify: UserNotify) => void): this on(event: 'notify_breakpoint_resolved', listener: (notify: BreakpointResolvedNotify) => void): this + on(event: 'stream', listener: (stream: Stream) => void): this } /** @@ -809,6 +828,9 @@ export class Connection extends DbgpConnection { } else if (response.documentElement.nodeName === 'notify') { const n = Notify.fromXml(response, this) this.emit('notify_' + n.name, n) + } else if (response.documentElement.nodeName === 'stream') { + const s = new Stream(response) + this.emit('stream', s) } else { const transactionId = parseInt(response.documentElement.getAttribute('transaction_id')!) if (this._pendingCommands.has(transactionId)) { @@ -1109,4 +1131,14 @@ export class Connection extends DbgpConnection { public async sendEvalCommand(expression: string): Promise { return new EvalResponse(await this._enqueueCommand('eval', undefined, expression), this) } + + // ------------------------------ stream ---------------------------------------- + + public async sendStdout(mode: 0 | 1 | 2): Promise { + return new Response(await this._enqueueCommand('stdout', `-c ${mode}`), this) + } + + public async sendStderr(mode: 0 | 1 | 2): Promise { + return new Response(await this._enqueueCommand('stderr', `-c ${mode}`), this) + } }