blob: 8262a1934726866c12f9c3e18edbcb18d49815c8 [file] [log] [blame]
// 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');
}
}
}