blob: 2ee5c0ddc1b473e722af9f57e5d42418bef81800 [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 vscode from 'vscode';
import {
CancellationToken,
DebugAdapterTracker,
DebugAdapterTrackerFactory,
DebugConfiguration,
ProviderResult,
WorkspaceFolder,
} from 'vscode';
import { DebugProtocol } from '@vscode/debugprotocol';
import { DEFAULT_SERVER_PORT, ZxdbConsole } from './console';
import * as logger from '../logger';
import { Ffx } from '../ffx';
import { Setup } from '../extension';
export let zxdbConsole: ZxdbConsole;
/**
* Initialize the singleton zxdbConsole instance.
* @param ffx instance to interact with ffx.
*/
function startZxdbConsole(ffx: Ffx) {
zxdbConsole = new ZxdbConsole(ffx);
}
/**
* register the zxdb DAP implementation and related commands
*/
export function setUpZxdb(ctx: vscode.ExtensionContext, setup: Setup) {
// TODO(fxbug.dev/100579): we should normalize this logging infrastructure so
// we don't have multiple logging setups.
startZxdbConsole(setup.ffx);
ctx.subscriptions.push(vscode.commands.registerCommand(
'fuchsia.internal.zxdb.pickProcess', (config) => {
let promptText: string = '';
if (config.request === 'launch') {
promptText =
'Launch(zxdb): Enter name of the process that will be launched.' +
' Hint: For components, it\'s usually the component name.';
} else {
promptText = 'Attach(zxdb): Enter name of the process to debug.' +
' The process should already be running or should be started separately.';
}
// TODO(fxbug.dev/100581): add code to run ps and give a list of process
// to pick from.
return vscode.window.showInputBox({
placeHolder: '(e.g. hello-world-test)',
value: '',
prompt: promptText
});
}));
ctx.subscriptions.push(vscode.commands.registerCommand(
'fuchsia.internal.zxdb.enterLaunchCommand', (config) => {
return vscode.window.showInputBox({
placeHolder: '(e.g. fx test hello-world-test)',
value: '',
prompt:
'Launch(zxdb): Enter launch command. This will be run in the vscode terminal.'
});
}));
// register a configuration provider for 'zxdb' debug type
const provider = new ZxdbConfigurationProvider(setup.ffx);
ctx.subscriptions.push(
vscode.debug.registerDebugConfigurationProvider('zxdb', provider));
let factory = new ZxdbDebugAdapterFactory();
ctx.subscriptions.push(vscode.Disposable.from(
vscode.debug.registerDebugAdapterDescriptorFactory('zxdb', factory)));
let dapLogger = new ZxdbDebugAdapterTrackerFactory();
ctx.subscriptions.push(vscode.Disposable.from(
vscode.debug.registerDebugAdapterTrackerFactory('zxdb', dapLogger)));
}
/**
* Formats and validates the input configuration. This provider is registered via
* debug.registerDebugConfigurationProvider.
*/
export class ZxdbConfigurationProvider implements vscode.DebugConfigurationProvider {
maxFilterNameLength = 32;
private ffx: Ffx;
constructor(ffx: Ffx) {
this.ffx = ffx;
}
/* This method is called just before launching debug session.
Final updates to debug configuration can be done here.
*/
resolveDebugConfigurationWithSubstitutedVariables(
folder: WorkspaceFolder | undefined, config: DebugConfiguration,
token?: CancellationToken): ProviderResult<DebugConfiguration> {
// Return null if launch.json is empty or missing.
if (!config.type && !config.request && !config.name) {
logger.error('launch not configured correctly:', 'zxdb', config);
void vscode.window.showErrorMessage(
'launch.json does not have any zxdb configuration. Initial configurations will be ' +
'added automatically, if not add it manually (Hint: Add Configuration -> zxdb...).\n' +
'Next, pick a configuration in the launch configuration dropdown to start debugging.');
return null;
}
// Trim the process name if it is too long.
if (config.process.length > this.maxFilterNameLength) {
logger.warn(
`Process name [${config.process}] too long. ` +
`Trimmed to ${config.process.substring(0, this.maxFilterNameLength)}`);
config.process = config.process.substring(0, this.maxFilterNameLength);
void vscode.window.showWarningMessage(`Process name too long. Trimmed to ${config.process}`);
}
// Check for a target device before attempting to attach or launch.
if (!this.ffx.targetDevice) {
logger.error('No device selected, launching failed');
void vscode.window.showErrorMessage(
'No target device selected. Please select a target device');
return null;
}
// Verify that the request is either 'attach' or 'launch'.
if (config.request === 'attach') {
logger.info(`Attaching to ${config.process}`);
} else if (config.request === 'launch') {
logger.info(`Launching process ${config.process}`, 'zxdb', config);
} else {
logger.error('Unknown launch config', 'zxdb', config);
void vscode.window.showErrorMessage('Unknown launch configuration.');
return null;
}
return config;
}
}
export class ZxdbDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory {
createDebugAdapterDescriptor(session: vscode.DebugSession):
ProviderResult<vscode.DebugAdapterDescriptor> {
// Start zxdb console and wait for it to be ready.
// Then create a client to start debugging in vscode.
zxdbConsole.createZxdbProcess(session);
return zxdbConsole.waitForZxdbConsole().then(() => {
logger.info('Creating debug adapter client.');
let client = new vscode.DebugAdapterServer(DEFAULT_SERVER_PORT);
return client;
}).catch(err => { logger.error(`Error starting debugger: ${err}`); return undefined; });
}
}
/**
* Track the communication between the editor and the debug adapter. This enables us to modify the
* state of zxdb editor variables ( e.g. to close the zxdb terminal), and to log the communication
* messages.
*/
class ZxdbDebugAdapterTracker implements DebugAdapterTracker {
ignoreError: boolean = false;
restart: boolean = false;
launchPID: number | undefined = undefined;
constructor(private readonly session: vscode.DebugSession) { this.session = session; }
public onWillStartSession() {
vscode.debug.activeDebugConsole.appendLine('will start session');
this.ignoreError = false;
logger.debug('Starting debug session:', 'dap-tracker', this.session.configuration);
}
public onWillReceiveMessage(message: any) {
let request: DebugProtocol.Request = message;
if (request.command === 'disconnect') {
// Ignore errors reported after this. The connection is closed by the
// backend and hence debug adapter reports connection loss errors which
// needs to be ignored. It might confuse users if these errors are shown.
this.ignoreError = true;
if (request.arguments && request.arguments.restart) {
this.restart = request.arguments.restart;
}
} else if (request.command === 'runInTerminal') {
let response: DebugProtocol.RunInTerminalResponse = message;
if (response.body.shellProcessId) {
this.launchPID = getChildPID(response.body.shellProcessId);
} else if (response.body.processId) {
this.launchPID = response.body.processId;
}
}
logger.debug('onWillReceiveMessage:', 'dap-tracker', message);
}
public onDidSendMessage(message: DebugProtocol.Message) {
logger.debug('onDidSendMessage', 'dap-tracker', message);
}
public onWillStopSession() {
logger.debug('onWillStopSession', 'dap-tracker');
if (!this.restart) {
zxdbConsole.destroyZxdbProcess();
}
destroyZxdbLaunch(this.launchPID);
this.launchPID = undefined;
}
public onError(error: Error) {
logger.error('onError', 'dap-tracker', error);
if (!this.ignoreError) {
void vscode.window.showErrorMessage(`Error: ${error.message}`);
}
}
public onExit(code: number | undefined, signal: string | undefined) {
logger.info('onExit', 'dap-tracker', code, signal);
}
}
export class ZxdbDebugAdapterTrackerFactory implements DebugAdapterTrackerFactory {
public createDebugAdapterTracker(session: vscode.DebugSession):
ProviderResult<DebugAdapterTracker> {
return new ZxdbDebugAdapterTracker(session);
}
}
// Methods to control launched process
/**
* Get the child PID of a parent process.
* @param pid of the parent process.
* @returns the PID of the child.
*/
function getChildPID(pid: number): number | undefined {
let childPID = undefined;
if (process.platform !== 'win32') {
childPID =
Number(require('child_process').execSync(`pgrep -P ${pid}`) + '');
logger.debug(`Child process ID: ${childPID}`);
}
return childPID;
}
/**
* Kill the zxdb process with the given PID.
* @param pid of zxdb instance.
*/
function destroyZxdbLaunch(pid: number | undefined) {
if (pid) {
logger.info(`zxdb launch (PID:${pid}) is destroyed.`);
process.kill(-pid, 2); // SIGINT
}
}