| // 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 * as logger from '../logger'; |
| import { type ChildProcessWithoutNullStreams } from 'child_process'; |
| import { Fx } from '../fx'; |
| |
| const ZXDB_COMMAND_OPT = ['debug', '--new-agent', '--', '--enable-debug-adapter']; |
| // Although `ffx debug connect` typically services requests within a second of launch, |
| // `fx debug` might start a temporary package server, so use a 30 second timeout. |
| const DEFAULT_ZXDB_TIMEOUT = 30_000; |
| const INVALID_SERVER_PORT = -1; |
| |
| /** |
| * Manage the initial connection and the lifetime of the zxdb process (`fx debug`). |
| * The zxdb process communicates with the IDE debug adapter through a socket. |
| */ |
| export class ZxdbConsole { |
| fx: Fx; |
| zxdbProcess?: ZxdbProcess; |
| // Overwrite the default waittime (for debugging). |
| public retryWaitTimeMs?: number; |
| |
| public constructor(fx: Fx) { |
| this.fx = fx; |
| } |
| |
| // 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)); |
| }); |
| s.on('error', (err: Error) => reject(err)); |
| }); |
| } |
| |
| /** |
| * 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. |
| const timeout: number = timeoutOverride ?? |
| vscode.workspace.getConfiguration().get(CONFIG_CONNECT_TIMEOUT, DEFAULT_ZXDB_TIMEOUT); |
| |
| let socket: Net.Socket; |
| let done = false; |
| |
| const exitAndCleanUp = () => { |
| done = true; |
| if (socket) { |
| socket.destroy(); |
| } |
| }; |
| |
| const 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')); |
| } |
| }; |
| |
| const 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); |
| }); |
| |
| const 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(); |
| }); |
| }; |
| |
| const zxdbProcess = new ZxdbProcess(this.fx, session, portNumber, onCloseCallback); |
| this.zxdbProcess = zxdbProcess; |
| connect(); |
| |
| const 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 `fx debug` process and monitor its state and data. |
| * @param session vscode debug session. |
| * @param portNumber that the zxdb debugger should utilize. |
| * @returns |
| */ |
| constructor(fx: Fx, session: vscode.DebugSession, portNumber: number, |
| onCloseCallback: (errorMsg: string) => void) { |
| try { |
| logger.debug(`Starting new zxdb process connected to port ${portNumber}`, 'zxdb-console'); |
| this.zxdbProcess = fx.runStreaming(ZXDB_COMMAND_OPT.concat(['--debug-adapter-port', `${portNumber}`])); |
| this.zxdbProcess?.stdout.setEncoding('utf8'); |
| this.zxdbProcess?.stderr.setEncoding('utf8'); |
| if (!this.zxdbProcess) { |
| logger.warn('Cannot start zxdb, no path to ffx'); |
| return; |
| } |
| |
| let errorMsg = ''; |
| this.zxdbProcess.stdout.on('data', (data: unknown) => { |
| this.processOutput(session, 'stdout', String(data)); |
| }); |
| this.zxdbProcess.stderr.on('data', (data: unknown) => { |
| this.processOutput(session, 'stderr', String(data)); |
| errorMsg = `${errorMsg}${String(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 : String(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'); |
| } |
| } |
| } |