| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // The module 'vscode' contains the VS Code extensibility API |
| // Import the module and reference it with the alias vscode in your code below |
| |
| import * as Net from 'net'; |
| import * as vscode from 'vscode'; |
| import { CONFIG_CONNECT_TIMEOUT } from '../common-config'; |
| import { Ffx } from '../ffx'; |
| import * as logger from '../logger'; |
| import { ChildProcessWithoutNullStreams } from 'child_process'; |
| |
| |
| const ZXDB_COMMAND_OPT = ['debug', 'connect', '--', '--enable-debug-adapter']; |
| // 5 seconds. Usually "ffx debug connect" takes less than 1 second. |
| const DEFAULT_ZXDB_TIMEOUT = 5000; |
| const INVALID_SERVER_PORT = -1; |
| |
| /** |
| * Manage the initial connection and the lifetime of the zxdb process (`ffx debug`). |
| * The zxdb process communicates with the IDE debug adapter through a socket. |
| */ |
| export class ZxdbConsole { |
| ffx: Ffx; |
| zxdbProcess?: ZxdbProcess; |
| // Overwrite the default waittime (for debugging). |
| public retryWaitTimeMs?: number; |
| |
| public constructor(ffx: Ffx) { |
| this.ffx = ffx; |
| } |
| |
| // Send port 0 to ask the OS to allocate a port. |
| private findFreePort() { |
| return new Promise<number>((resolve, reject) => { |
| const s = Net.createServer(); |
| s.listen(() => { |
| const addr = s.address() as Net.AddressInfo; |
| // wait till after we close this to return the address, |
| // so that something else can bind to it. |
| s.close(() => resolve(addr.port)); |
| }); |
| }); |
| } |
| |
| /** |
| * Launch the zxdb debugger in an available port and verify that it's reachable using a socket. |
| * If the zxdb process crashes, it will be invoke one more time (possibly on a different port). |
| * @param session vscode debug session. |
| * @param timeoutOverride this overrides the default timeout. |
| * @returns a promise to the port number. |
| */ |
| public async lauchZxdb(session: vscode.DebugSession, timeoutOverride?: number) { |
| let inputPort = await this.findFreePort(); |
| // TODO(fxbug.dev/112695): Capture analytics for how frequently we relaunch. |
| return this.connectToZxdb(session, inputPort, /*errorOnBindFailure*/false, timeoutOverride) |
| .then(async (portNumber) => { |
| if (portNumber !== INVALID_SERVER_PORT) { return portNumber; } |
| |
| // Find a free port, and try connecting again. |
| inputPort = await this.findFreePort(); |
| return await this.connectToZxdb(session, inputPort, /*errorOnBindFailure*/true, |
| timeoutOverride); |
| }); |
| } |
| |
| private sleep(ms: number) { |
| return new Promise((resolve) => { |
| setTimeout(resolve, ms); |
| }); |
| } |
| |
| /** |
| * Await for the spawned zxdb console to be ready. If we cannot connect to zxdb, the user will |
| * receive an error message and the zxdb process and window will be closed. |
| * @param timeoutOverride this overrides the default timeout. |
| * @returns void promise. |
| */ |
| connectToZxdb(session: vscode.DebugSession, portNumber: number, exceptionOnFailure: boolean, |
| timeoutOverride?: number) { |
| return new Promise<number>((resolve, reject) => { |
| // We check when the zxdb process is ready by connecting to the debug adapter server port. |
| // Once the connection succeeds, it means that the server is ready. We then disconnect and |
| // continue debugging. |
| let timeout: number = timeoutOverride ?? |
| vscode.workspace.getConfiguration().get(CONFIG_CONNECT_TIMEOUT, DEFAULT_ZXDB_TIMEOUT); |
| |
| let socket: Net.Socket; |
| let done = false; |
| |
| let exitAndCleanUp = () => { |
| done = true; |
| if (socket) { socket.destroy(); } |
| }; |
| |
| let onCloseCallback = (errorMsg: string) => { |
| if (done) { return; } |
| exitAndCleanUp(); |
| |
| if (!exceptionOnFailure) { |
| resolve(INVALID_SERVER_PORT); |
| } |
| |
| if (errorMsg.toLowerCase().includes('could not bind socket to port')) { |
| reject(new Error(`Zxdb could not connect to port ${portNumber}`)); |
| } else { |
| reject(new Error('Zxdb process terminated')); |
| } |
| }; |
| |
| let connect = () => { |
| socket = new Net.Socket(); |
| socket.connect(portNumber, 'localhost'); |
| logger.debug(`attempting to connect to localhost:${portNumber}`); |
| |
| socket.on('connect', () => { |
| if (done) { return; } |
| exitAndCleanUp(); |
| logger.info('zxdb console has started.'); |
| resolve(portNumber); |
| }); |
| |
| let retryConnect = async () => { |
| if (done) { return; } |
| socket.destroy(); |
| await this.sleep(this.retryWaitTimeMs ?? 1000); |
| connect(); |
| }; |
| |
| socket.on('error', (err) => { |
| logger.debug(`socket error - ${err}`); |
| void /* in bg */ retryConnect(); |
| }); |
| }; |
| |
| let zxdbProcess = new ZxdbProcess(this.ffx, session, portNumber, onCloseCallback); |
| this.zxdbProcess = zxdbProcess; |
| connect(); |
| |
| let timeoutTimer = setTimeout(() => { |
| if (done) { return; } |
| exitAndCleanUp(); |
| // End the process that was started from this request |
| zxdbProcess?.kill(); |
| clearTimeout(timeoutTimer); |
| |
| logger.debug('timeout expired', 'zxdb'); |
| logger.info('Timeout starting zxdb console.'); |
| reject(new Error('Timeout starting zxdb console')); |
| }, timeout); |
| }); |
| } |
| |
| /** |
| * Kill the zxdb process. |
| */ |
| public destroyZxdbProcess() { |
| logger.info('zxdb console is destroyed.'); |
| this.zxdbProcess?.kill(); |
| } |
| } |
| |
| export class ZxdbProcess { |
| // Keep track of the zxdb process object. |
| zxdbProcess?: ChildProcessWithoutNullStreams; |
| |
| /** |
| * Invoke `ffx debug connect` process and monitor its state and data. |
| * @param session vscode debug session. |
| * @param portNumber that the zxdb debugger should utilize. |
| * @returns |
| */ |
| constructor(ffx: Ffx, session: vscode.DebugSession, portNumber: number, |
| onCloseCallback: (errorMsg: string) => void) { |
| try { |
| logger.debug(`Starting new zxdb process connected to port ${portNumber}`, 'zxdb-console'); |
| this.zxdbProcess = ffx.runFfxStreaming(ZXDB_COMMAND_OPT.concat(['--debug-adapter-port', `${portNumber}`])); |
| if (!this.zxdbProcess) { |
| logger.warn('Cannot start zxdb, no path to ffx'); |
| return; |
| } |
| |
| let errorMsg = ''; |
| this.zxdbProcess.stdout.on('data', (data) => { this.processOutput(session, 'stdout', data); }); |
| this.zxdbProcess.stderr.on('data', (data) => { |
| this.processOutput(session, 'stderr', data); |
| errorMsg = `${errorMsg}${data}\n`; |
| }); |
| this.zxdbProcess.on('close', (code) => { |
| onCloseCallback(errorMsg); |
| logger.warn(`debugger connection closed: ${code}`, 'zxdb'); |
| }); |
| } catch (e) { |
| logger.error('Cannot start zxdb console: ', 'zxdb-console', e); |
| void vscode.window.showErrorMessage( |
| `Cannot start console: ${(e instanceof Error) ? e.message : e}`); |
| } |
| } |
| |
| public kill() { |
| this.zxdbProcess?.kill(); |
| } |
| |
| private processOutput(session: vscode.DebugSession, source: string, data: string) { |
| if (session?.id === vscode.debug.activeDebugSession?.id) { |
| vscode.debug.activeDebugConsole.appendLine(`${source}: ${data}`); |
| } else { |
| logger.warn(`${source}: ${data}`, 'zxdb'); |
| } |
| } |
| } |