| // 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 |
| } |
| } |