| /* eslint-disable @typescript-eslint/no-explicit-any */ | |
| /*--------------------------------------------------------- | |
| * Copyright 2021 The Go Authors. All rights reserved. | |
| * Licensed under the MIT License. See LICENSE in the project root for license information. | |
| *--------------------------------------------------------*/ | |
| import { ChildProcess, ChildProcessWithoutNullStreams, spawn } from 'child_process'; | |
| import stream = require('stream'); | |
| import vscode = require('vscode'); | |
| import { OutputEvent, TerminatedEvent } from 'vscode-debugadapter'; | |
| import getPort = require('get-port'); | |
| import path = require('path'); | |
| import * as fs from 'fs'; | |
| import * as net from 'net'; | |
| import { DebugProtocol } from 'vscode-debugprotocol'; | |
| import { getWorkspaceFolderPath } from './util'; | |
| import { getEnvPath, getBinPathFromEnvVar } from './utils/pathUtils'; | |
| import { GoExtensionContext } from './context'; | |
| import { createRegisterCommand } from './commands'; | |
| export function activate(ctx: vscode.ExtensionContext, goCtx: GoExtensionContext) { | |
| const debugOutputChannel = vscode.window.createOutputChannel('Go Debug', { log: true }); | |
| ctx.subscriptions.push(debugOutputChannel); | |
| const factory = new GoDebugAdapterDescriptorFactory(debugOutputChannel); | |
| ctx.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory('go', factory)); | |
| if ('dispose' in factory) { | |
| ctx.subscriptions.push(factory); | |
| } | |
| const tracker = new GoDebugAdapterTrackerFactory(debugOutputChannel); | |
| ctx.subscriptions.push(vscode.debug.registerDebugAdapterTrackerFactory('go', tracker)); | |
| if ('dispose' in tracker) { | |
| ctx.subscriptions.push(tracker); | |
| } | |
| const registerCommand = createRegisterCommand(ctx, goCtx); | |
| registerCommand('go.debug.toggleHideSystemGoroutines', () => toggleHideSystemGoroutines); | |
| } | |
| class GoDebugAdapterDescriptorFactory implements vscode.DebugAdapterDescriptorFactory { | |
| constructor(private outputChannel: vscode.LogOutputChannel) {} | |
| public createDebugAdapterDescriptor( | |
| session: vscode.DebugSession, | |
| executable: vscode.DebugAdapterExecutable | undefined | |
| ): vscode.ProviderResult<vscode.DebugAdapterDescriptor> { | |
| if (session.configuration.debugAdapter === 'dlv-dap') { | |
| return this.createDebugAdapterDescriptorDlvDap(session.configuration); | |
| } | |
| return executable; | |
| } | |
| public async dispose() { | |
| console.log('GoDebugAdapterDescriptorFactory.dispose'); | |
| } | |
| private async createDebugAdapterDescriptorDlvDap( | |
| configuration: vscode.DebugConfiguration | |
| ): Promise<vscode.ProviderResult<vscode.DebugAdapterDescriptor>> { | |
| const logger = this.outputChannel; | |
| logger.debug(`Config: ${JSON.stringify(configuration)}`); | |
| if (configuration.port) { | |
| const host = configuration.host ?? '127.0.0.1'; | |
| logger.info(`Connecting to DAP server at ${host}:${configuration.port}`); | |
| return new vscode.DebugAdapterServer(configuration.port, host); | |
| } | |
| const d = new DelveDAPOutputAdapter(configuration, logger); | |
| return new vscode.DebugAdapterInlineImplementation(d); | |
| } | |
| } | |
| class GoDebugAdapterTrackerFactory implements vscode.DebugAdapterTrackerFactory { | |
| constructor(private outputChannel: vscode.LogOutputChannel) {} | |
| createDebugAdapterTracker(session: vscode.DebugSession) { | |
| const logger = this.outputChannel; | |
| let requestsSent = 0; | |
| let responsesReceived = 0; | |
| return { | |
| onWillStartSession: () => | |
| logger.debug(`session ${session.id} will start with ${JSON.stringify(session.configuration)}`), | |
| onWillReceiveMessage: (message: any) => { | |
| logger.trace(`client -> ${JSON.stringify(message)}`); | |
| requestsSent++; | |
| }, | |
| onDidSendMessage: (message: any) => { | |
| logger.trace(`client <- ${JSON.stringify(message)}`); | |
| responsesReceived++; | |
| }, | |
| onError: (error: Error) => logger.error(`error: ${error}`), | |
| onWillStopSession: () => { | |
| if ( | |
| session.configuration.debugAdapter === 'dlv-dap' && | |
| session.configuration.mode === 'remote' && | |
| requestsSent > 0 && | |
| responsesReceived === 0 // happens when the rpc server doesn't understand DAP | |
| ) { | |
| const err = | |
| 'Expected to connect to external `dlv --headless` server @ v1.7.3 or later via DAP. Older versions fail with "error layer=rpc rpc:invalid character \'C\' looking for beginning of value" logged to the terminal.\n'; | |
| logger.warn(err); | |
| vscode.window.showErrorMessage(err); | |
| } | |
| logger.debug(`session ${session.id} will stop\n`); | |
| }, | |
| onExit: (code: number | undefined, signal: string | undefined) => | |
| logger.info(`debug adapter exited: (code: ${code}, signal: ${signal})`) | |
| }; | |
| } | |
| dispose() {} | |
| } | |
| const TWO_CRLF = '\r\n\r\n'; | |
| type ILogger = Pick<vscode.LogOutputChannel, 'error' | 'info' | 'debug' | 'trace'>; | |
| // Proxies DebugProtocolMessage exchanges between VSCode and a remote | |
| // process or server connected through a duplex stream, after its | |
| // start method is called. | |
| export class ProxyDebugAdapter implements vscode.DebugAdapter { | |
| private messageEmitter = new vscode.EventEmitter<vscode.DebugProtocolMessage>(); | |
| // connection from/to server (= dlv dap) | |
| private readable?: stream.Readable; | |
| private writable?: stream.Writable; | |
| protected logger: ILogger; | |
| private terminated = false; | |
| constructor(logger: ILogger) { | |
| this.logger = logger; | |
| this.onDidSendMessage = this.messageEmitter.event; | |
| } | |
| // Implement vscode.DebugAdapter (VSCodeDebugAdapter) interface. | |
| // Client will call handleMessage to send messages, and | |
| // listen on onDidSendMessage to receive messages. | |
| onDidSendMessage: vscode.Event<vscode.DebugProtocolMessage>; | |
| async handleMessage(message: vscode.DebugProtocolMessage): Promise<void> { | |
| await this.sendMessageToServer(message); | |
| } | |
| // Methods for proxying. | |
| protected sendMessageToClient(msg: vscode.DebugProtocolMessage) { | |
| this.messageEmitter.fire(msg); | |
| } | |
| protected sendMessageToServer(message: vscode.DebugProtocolMessage): void { | |
| const json = JSON.stringify(message) ?? ''; | |
| if (this.writable) { | |
| this.writable.write( | |
| `Content-Length: ${Buffer.byteLength(json, 'utf8')}${TWO_CRLF}${json}`, | |
| 'utf8', | |
| (err) => { | |
| if (err) { | |
| this.logger?.error(`error sending message: ${err}`); | |
| this.sendMessageToClient(new TerminatedEvent()); | |
| } | |
| } | |
| ); | |
| } else { | |
| this.logger?.error(`stream is closed; dropping ${json}`); | |
| } | |
| } | |
| public async start(readable: stream.Readable, writable: stream.Writable) { | |
| if (this.readable || this.writable) { | |
| throw new Error('start was called more than once'); | |
| } | |
| this.readable = readable; | |
| this.writable = writable; | |
| this.readable.on('data', (data: Buffer) => { | |
| this.handleDataFromServer(data); | |
| }); | |
| this.readable.once('close', () => { | |
| this.readable = undefined; | |
| }); | |
| this.readable.on('error', (err) => { | |
| if (this.terminated) { | |
| return; | |
| } | |
| this.terminated = true; | |
| if (err) { | |
| this.logger?.error(`connection error: ${err}`); | |
| this.sendMessageToClient(new OutputEvent(`connection error: ${err}\n`, 'console')); | |
| } | |
| this.sendMessageToClient(new TerminatedEvent()); | |
| }); | |
| } | |
| async dispose() { | |
| this.writable?.end(); // no more write. | |
| } | |
| private rawData = Buffer.alloc(0); | |
| private contentLength = -1; | |
| // Implements parsing of the DAP protocol. We cannot use ProtocolClient | |
| // from the vscode-debugadapter package, because it's not exported and | |
| // is not meant for external usage. | |
| // See https://github.com/microsoft/vscode-debugadapter-node/issues/232 | |
| private handleDataFromServer(data: Buffer): void { | |
| this.rawData = Buffer.concat([this.rawData, data]); | |
| // eslint-disable-next-line no-constant-condition | |
| while (true) { | |
| if (this.contentLength >= 0) { | |
| if (this.rawData.length >= this.contentLength) { | |
| const message = this.rawData.toString('utf8', 0, this.contentLength); | |
| this.rawData = this.rawData.slice(this.contentLength); | |
| this.contentLength = -1; | |
| if (message.length > 0) { | |
| const rawMessage = JSON.parse(message); | |
| this.sendMessageToClient(rawMessage); | |
| } | |
| continue; // there may be more complete messages to process | |
| } | |
| } else { | |
| const idx = this.rawData.indexOf(TWO_CRLF); | |
| if (idx !== -1) { | |
| const header = this.rawData.toString('utf8', 0, idx); | |
| const lines = header.split('\r\n'); | |
| for (const line of lines) { | |
| const pair = line.split(/: +/); | |
| if (pair[0] === 'Content-Length') { | |
| this.contentLength = +pair[1]; | |
| } | |
| } | |
| this.rawData = this.rawData.slice(idx + TWO_CRLF.length); | |
| continue; | |
| } | |
| } | |
| break; | |
| } | |
| } | |
| } | |
| // DelveDAPOutputAdapter is a ProxyDebugAdapter that proxies between | |
| // VSCode and a dlv dap process spawned and managed by this adapter. | |
| // It turns the process's stdout/stderrr into OutputEvent. | |
| export class DelveDAPOutputAdapter extends ProxyDebugAdapter { | |
| constructor(private configuration: vscode.DebugConfiguration, logger: ILogger) { | |
| super(logger); | |
| } | |
| private connected?: Promise<{ connected: boolean; reason?: any }>; | |
| private dlvDapServer?: ChildProcess; | |
| private socket?: net.Socket; | |
| private terminatedOnError = false; | |
| protected sendMessageToClient(message: vscode.DebugProtocolMessage) { | |
| const m = message as any; | |
| if (m.type === 'request') { | |
| this.logger.debug(`do not forward reverse request: dropping ${JSON.stringify(m)}`); | |
| return; | |
| } | |
| super.sendMessageToClient(message); | |
| } | |
| protected async sendMessageToServer(message: vscode.DebugProtocolMessage): Promise<void> { | |
| const m = message as any; | |
| if (m.type === 'response') { | |
| this.logger.debug(`do not forward reverse request response: dropping ${JSON.stringify(m)}`); | |
| return; | |
| } | |
| if (!this.connected) { | |
| if (m.type === 'request' && m.command === 'initialize') { | |
| this.connected = this.launchDelveDAP(); | |
| } else { | |
| this.connected = Promise.resolve({ | |
| connected: false, | |
| reason: `the first message must be an initialize request, got ${JSON.stringify(m)}` | |
| }); | |
| } | |
| } | |
| const { connected, reason } = await this.connected; | |
| if (connected) { | |
| super.sendMessageToServer(message); | |
| return; | |
| } | |
| const errMsg = `Couldn't start dlv dap:\n${reason}`; | |
| if (this.terminatedOnError) { | |
| this.terminatedOnError = true; | |
| this.outputEvent('stderr', errMsg); | |
| this.sendMessageToClient(new TerminatedEvent()); | |
| } | |
| if (m.type === 'request') { | |
| const req = message as DebugProtocol.Request; | |
| this.sendMessageToClient({ | |
| seq: 0, | |
| type: 'response', | |
| request_seq: req.seq, | |
| success: false, | |
| command: req.command, | |
| message: errMsg | |
| }); | |
| } | |
| } | |
| async dispose(timeoutMS?: number) { | |
| // NOTE: OutputEvents from here may not show up in DEBUG CONSOLE | |
| // because the debug session is terminating. | |
| await super.dispose(); | |
| if (!this.dlvDapServer) { | |
| return; | |
| } | |
| if (this.connected === undefined) { | |
| return; | |
| } | |
| this.connected = undefined; | |
| const dlvDapServer = this.dlvDapServer; | |
| this.dlvDapServer = undefined; | |
| if (!dlvDapServer) { | |
| return; | |
| } | |
| if (dlvDapServer.exitCode !== null) { | |
| this.logger?.info( | |
| `dlv dap process(${dlvDapServer.pid}) already exited (exit code: ${dlvDapServer.exitCode})` | |
| ); | |
| return; | |
| } | |
| await new Promise<void>((resolve) => { | |
| if (timeoutMS === undefined || timeoutMS < 0) { | |
| timeoutMS = 1_000; | |
| } | |
| const exitTimeoutToken = setTimeout(() => { | |
| this.logger?.error(`dlv dap process (${dlvDapServer.pid}) isn't responding. Killing...`); | |
| dlvDapServer.kill('SIGINT'); // Don't use treekill but let dlv handle cleaning up the child processes. | |
| }, timeoutMS); | |
| dlvDapServer.on('exit', (code, signal) => { | |
| clearTimeout(exitTimeoutToken); | |
| if (code || signal) { | |
| this.logger?.error( | |
| `dlv dap process(${dlvDapServer.pid}) exited (exit code: ${code} signal: ${signal})` | |
| ); | |
| } | |
| resolve(); | |
| }); | |
| }); | |
| } | |
| private async launchDelveDAP() { | |
| try { | |
| const { dlvDapServer, socket } = await this.startDapServer(this.configuration); | |
| this.dlvDapServer = dlvDapServer; | |
| this.socket = socket; | |
| this.start(this.socket, this.socket); | |
| } catch (err) { | |
| return { connected: false, reason: err }; | |
| } | |
| this.logger?.debug(`Running dlv dap server: pid=${this.dlvDapServer?.pid}`); | |
| return { connected: true }; | |
| } | |
| protected outputEvent(dest: string, output: string, data?: any) { | |
| this.sendMessageToClient(new OutputEvent(output, dest, data)); | |
| } | |
| async startDapServer( | |
| configuration: vscode.DebugConfiguration | |
| ): Promise<{ dlvDapServer?: ChildProcess; socket: net.Socket }> { | |
| const log = (msg: string) => this.outputEvent('stdout', msg); | |
| const logErr = (msg: string) => this.outputEvent('stderr', msg); | |
| const logConsole = (msg: string) => { | |
| this.outputEvent('console', msg); | |
| // Some log messages generated after vscode stops the debug session | |
| // may not appear in the DEBUG CONSOLE. For easier debugging, log | |
| // the messages through the logger that prints to Go Debug output | |
| // channel. | |
| this.logger?.trace(msg); | |
| }; | |
| // If a port has been specified, assume there is an already | |
| // running dap server to connect to. Otherwise, we start the dlv dap server. | |
| const dlvExternallyLaunched = !!configuration.port; | |
| if ( | |
| !dlvExternallyLaunched && | |
| (configuration.console === 'integratedTerminal' || configuration.console === 'externalTerminal') | |
| ) { | |
| return this.startDAPServerWithClientAddrFlag(configuration, logErr); | |
| } | |
| const host = configuration.host || '127.0.0.1'; | |
| const port = configuration.port || (await getPort()); | |
| const dlvDapServer = dlvExternallyLaunched | |
| ? undefined | |
| : await spawnDlvDapServerProcess(configuration, host, port, log, logErr, logConsole); | |
| const socket = await new Promise<net.Socket>((resolve, reject) => { | |
| // eslint-disable-next-line prefer-const | |
| let timer: NodeJS.Timeout; | |
| const s = net.createConnection(port, host, () => { | |
| clearTimeout(timer); | |
| resolve(s); | |
| }); | |
| timer = setTimeout(() => { | |
| reject('connection timeout'); | |
| s?.destroy(); | |
| }, 1000); | |
| }); | |
| return { dlvDapServer, socket }; | |
| } | |
| async startDAPServerWithClientAddrFlag( | |
| launchAttachArgs: vscode.DebugConfiguration, | |
| logErr: (msg: string) => void | |
| ): Promise<{ dlvDapServer?: ChildProcessWithoutNullStreams; socket: net.Socket }> { | |
| // This is called only when launchAttachArgs.console === 'integratedTerminal' | 'externalTerminal' currently. | |
| const console = (launchAttachArgs as any).console === 'externalTerminal' ? 'external' : 'integrated'; | |
| const { dlvArgs, dlvPath, dir, env } = getSpawnConfig(launchAttachArgs, logErr); | |
| // logDest - unlike startDAPServer that relies on piping log messages to a file descriptor | |
| // using --log-dest, we can pass the user-specified logDest directly to the flag. | |
| const logDest = launchAttachArgs.logDest; | |
| if (logDest) { | |
| dlvArgs.push(`--log-dest=${logDest}`); | |
| } | |
| dlvArgs.unshift(dlvPath); | |
| if (launchAttachArgs.asRoot === true && process.platform !== 'win32') { | |
| const sudo = getSudo(); | |
| if (sudo) { | |
| dlvArgs.unshift(sudo); | |
| } else { | |
| throw new Error('Failed to find "sudo" utility'); | |
| } | |
| } | |
| try { | |
| const port = await getPort(); | |
| const rendezvousServerPromise = waitForDAPServer(port, 30_000, this.logger); | |
| dlvArgs.push(`--client-addr=:${port}`); | |
| super.sendMessageToClient({ | |
| seq: 0, | |
| type: 'request', | |
| command: 'runInTerminal', | |
| arguments: { | |
| kind: console, | |
| title: `Go Debug Terminal (${launchAttachArgs.name})`, | |
| cwd: dir, | |
| args: dlvArgs, | |
| env: env | |
| } | |
| }); | |
| const socket = await rendezvousServerPromise; | |
| return { socket }; | |
| } catch (err) { | |
| logErr(`Failed to launch dlv: ${err}`); | |
| throw new Error('cannot launch dlv dap. See DEBUG CONSOLE'); | |
| } | |
| } | |
| } | |
| let sudoPath: string | null | undefined = undefined; | |
| function getSudo(): string | null { | |
| if (sudoPath === undefined) { | |
| sudoPath = getBinPathFromEnvVar('sudo', getEnvPath(), false); | |
| } | |
| return sudoPath; | |
| } | |
| function waitForDAPServer(port: number, timeoutMs: number, logger: ILogger): Promise<net.Socket> { | |
| return new Promise((resolve, reject) => { | |
| // eslint-disable-next-line prefer-const | |
| let s: net.Server | undefined; | |
| const timeoutToken = setTimeout(() => { | |
| if (s?.listening) { | |
| s.close(); | |
| } | |
| reject(new Error('timed out while waiting for DAP in reverse mode to connect')); | |
| }, timeoutMs); | |
| s = net.createServer({ pauseOnConnect: true }, (socket) => { | |
| logger.debug( | |
| `connected: ${port} (remote: ${socket.remoteAddress}:${socket.remotePort} local: ${socket.localAddress}:${socket.localPort})` | |
| ); | |
| clearTimeout(timeoutToken); | |
| s?.close(); // accept no more connection | |
| socket.resume(); | |
| resolve(socket); | |
| }); | |
| s.on('error', (err) => { | |
| logger.error(`connection error ${err}`); | |
| reject(err); | |
| }); | |
| s.maxConnections = 1; | |
| s.listen(port); | |
| }); | |
| } | |
| function spawnDlvDapServerProcess( | |
| launchAttachArgs: vscode.DebugConfiguration, | |
| host: string, | |
| port: number, | |
| log: (msg: string) => void, | |
| logErr: (msg: string) => void, | |
| logConsole: (msg: string) => void | |
| ): Promise<ChildProcess> { | |
| const { dlvArgs, dlvPath, dir, env } = getSpawnConfig(launchAttachArgs, logErr); | |
| // env does not include process.env. Construct the new env for process spawning | |
| // by combining process.env. | |
| const envForSpawn = env ? Object.assign({}, process.env, env) : undefined; | |
| dlvArgs.push(`--listen=${host}:${port}`); | |
| const onWindows = process.platform === 'win32'; | |
| if (!onWindows) { | |
| dlvArgs.push('--log-dest=3'); | |
| } | |
| const logDest = launchAttachArgs.logDest; | |
| if (typeof logDest === 'number') { | |
| logErr(`Using a file descriptor for 'logDest' (${logDest}) is not allowed.\n`); | |
| throw new Error('Using a file descriptor for `logDest` is not allowed.'); | |
| } | |
| if (logDest && !path.isAbsolute(logDest)) { | |
| logErr( | |
| `Using a relative path for 'logDest' (${logDest}) is not allowed.\nSee https://code.visualstudio.com/docs/editor/variables-reference if you want workspace-relative path.\n` | |
| ); | |
| throw new Error('Using a relative path for `logDest` is not allowed'); | |
| } | |
| if (logDest && onWindows) { | |
| logErr( | |
| 'Using `logDest` or `--log-dest` is not supported on windows yet. See https://github.com/golang/vscode-go/issues/1472.' | |
| ); | |
| throw new Error('Using `logDest` on windows is not allowed'); | |
| } | |
| const logDestStream = logDest ? fs.createWriteStream(logDest) : undefined; | |
| logConsole(`Starting: ${dlvPath} ${dlvArgs.join(' ')} from ${dir}\n`); | |
| // TODO(hyangah): In module-module workspace mode, the program should be build in the super module directory | |
| // where go.work (gopls.mod) file is present. Where dlv runs determines the build directory currently. Two options: | |
| // 1) launch dlv in the super-module module directory and adjust launchArgs.cwd (--wd). | |
| // 2) introduce a new buildDir launch attribute. | |
| return new Promise<ChildProcess>((resolve, reject) => { | |
| const p = spawn(dlvPath, dlvArgs, { | |
| cwd: dir, | |
| env: envForSpawn, | |
| stdio: onWindows ? ['pipe', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe', 'pipe'] // --log-dest=3 if !onWindows. | |
| }); | |
| let started = false; | |
| const timeoutToken: NodeJS.Timer = setTimeout(() => { | |
| logConsole(`Delve DAP server (PID: ${p.pid}) is not responding`); | |
| reject(new Error('timed out while waiting for DAP server to start')); | |
| }, 30_000); | |
| const stopWaitingForServerToStart = () => { | |
| clearTimeout(timeoutToken); | |
| started = true; | |
| resolve(p); | |
| }; | |
| p.stdout.on('data', (chunk) => { | |
| const msg = chunk.toString(); | |
| if (!started && msg.startsWith('DAP server listening at:')) { | |
| stopWaitingForServerToStart(); | |
| } | |
| log(msg); | |
| }); | |
| p.stderr.on('data', (chunk) => { | |
| logErr(chunk.toString()); | |
| }); | |
| p.stdio[3]?.on('data', (chunk) => { | |
| const msg = chunk.toString(); | |
| if (!started && msg.startsWith('DAP server listening at:')) { | |
| stopWaitingForServerToStart(); | |
| } | |
| if (logDestStream) { | |
| // always false on windows. | |
| // write to the specified file. | |
| logDestStream?.write(chunk, (err) => { | |
| if (err) { | |
| logConsole(`Error writing to ${logDest}: ${err}, log may be incomplete.`); | |
| } | |
| }); | |
| } else { | |
| logConsole(msg); | |
| } | |
| }); | |
| p.stdio[3]?.on('close', () => { | |
| // always false on windows. | |
| logDestStream?.end(); | |
| }); | |
| p.on('close', (code, signal) => { | |
| // TODO: should we watch 'exit' instead? | |
| // NOTE: log messages here may not appear in DEBUG CONSOLE if the termination of | |
| // the process was triggered by debug adapter's dispose when dlv dap doesn't | |
| // respond to disconnect on time. In that case, it's possible that the session | |
| // is in the middle of teardown and DEBUG CONSOLE isn't accessible. Check | |
| // Go Debug output channel. | |
| if (typeof code === 'number') { | |
| // The process exited on its own. | |
| logConsole(`dlv dap (${p.pid}) exited with code: ${code}\n`); | |
| } else if (code === null && signal) { | |
| logConsole(`dlv dap (${p.pid}) was killed by signal: ${signal}\n`); | |
| } else { | |
| logConsole(`dlv dap (${p.pid}) terminated with code: ${code} signal: ${signal}\n`); | |
| } | |
| }); | |
| p.on('error', (err) => { | |
| if (err) { | |
| logConsole(`Error: ${err}\n`); | |
| } | |
| }); | |
| }); | |
| } | |
| // getSpawnConfig returns the dlv args, directory, and dlv path necessary to spawn the dlv command. | |
| // It also returns `env` that is the additional environment variables users want to run the dlv | |
| // and the debuggee (i.e., go.toolsEnvVars, launch configuration's env and envFile) with. | |
| function getSpawnConfig(launchAttachArgs: vscode.DebugConfiguration, logErr: (msg: string) => void) { | |
| // launchArgsEnv is user-requested env vars (envFiles + env + toolsEnvVars). | |
| const env = launchAttachArgs.env; | |
| const dlvPath = launchAttachArgs.dlvToolPath ?? 'dlv'; | |
| if (!fs.existsSync(dlvPath)) { | |
| const envPath = getEnvPath(); | |
| logErr( | |
| `Couldn't find ${dlvPath} at the Go tools path, ${process.env['GOPATH']}${ | |
| env['GOPATH'] ? ', ' + env['GOPATH'] : '' | |
| } or ${envPath}\n` + | |
| 'Follow the setup instruction in https://github.com/golang/vscode-go/blob/master/docs/debugging.md#getting-started.\n' | |
| ); | |
| throw new Error('Cannot find Delve debugger (dlv dap)'); | |
| } | |
| let dir = getWorkspaceFolderPath(); | |
| if (launchAttachArgs.request === 'launch' && launchAttachArgs['__buildDir']) { | |
| // __buildDir is the directory determined during resolving debug config | |
| dir = launchAttachArgs['__buildDir']; | |
| } | |
| const dlvArgs = new Array<string>(); | |
| dlvArgs.push('dap'); | |
| // When duplicate flags are specified, | |
| // dlv doesn't mind but accepts the last flag value. | |
| if (launchAttachArgs.dlvFlags && launchAttachArgs.dlvFlags.length > 0) { | |
| dlvArgs.push(...launchAttachArgs.dlvFlags); | |
| } | |
| if (launchAttachArgs.showLog) { | |
| dlvArgs.push('--log=' + launchAttachArgs.showLog.toString()); | |
| // Only add the log output flag if we have already added the log flag. | |
| // Otherwise, delve complains. | |
| if (launchAttachArgs.logOutput) { | |
| dlvArgs.push('--log-output=' + launchAttachArgs.logOutput); | |
| } | |
| } | |
| return { dlvArgs, dlvPath, dir, env }; | |
| } | |
| // toggleHideSystemGoroutineCustomRequest is a helper function extracted | |
| // for testing the command. | |
| export async function toggleHideSystemGoroutinesCustomRequest( | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| cr: (command: string, args?: any) => Thenable<any> | |
| ) { | |
| const debugConsole = vscode.debug.activeDebugConsole; | |
| try { | |
| const response = await cr('evaluate', { | |
| expression: 'dlv config -list hideSystemGoroutines', | |
| context: 'context' | |
| }); | |
| let update = 'false'; | |
| if (response?.result?.indexOf('false') >= 0) { | |
| update = 'true'; | |
| } | |
| await cr('evaluate', { | |
| expression: `dlv config hideSystemGoroutines ${update}`, | |
| context: 'context' | |
| }); | |
| } catch (err) { | |
| if (err instanceof Error && err.message.indexOf('debuggee is running') >= 0) { | |
| debugConsole.appendLine('Cannot toggle hideSystemGoroutines while debuggee is running'); | |
| return; | |
| } | |
| debugConsole.appendLine(`Error toggling hideSystemGoroutines: ${err}`); | |
| } | |
| } | |
| const toggleHideSystemGoroutines = () => { | |
| const ds = vscode.debug.activeDebugSession; | |
| if (ds) { | |
| toggleHideSystemGoroutinesCustomRequest((command, args) => { | |
| return ds.customRequest(command, args); | |
| }); | |
| } | |
| }; |