| /* eslint-disable @typescript-eslint/no-unused-vars */ |
| /* eslint-disable no-async-promise-executor */ |
| /*--------------------------------------------------------- |
| * Copyright 2021 The Go Authors. All rights reserved. |
| * Licensed under the MIT License. See LICENSE in the project root for license information. |
| *--------------------------------------------------------*/ |
| |
| import cp = require('child_process'); |
| import { QuickPickItem } from 'vscode'; |
| import { getBinPath } from './util'; |
| import { lsofDarwinCommand, parseLsofProcesses } from './utils/lsofProcessParser'; |
| import { getEnvPath, getCurrentGoRoot } from './utils/pathUtils'; |
| import { parsePsProcesses, psDarwinCommand, psLinuxCommand } from './utils/psProcessParser'; |
| import { parseWmicProcesses, wmicCommand } from './utils/wmicProcessParser'; |
| import vscode = require('vscode'); |
| |
| export async function pickProcess(): Promise<string> { |
| const allProcesses = await getAllProcesses(); |
| const id = await processPicker(allProcesses); |
| return id; |
| } |
| |
| export async function pickProcessByName(name: string): Promise<string> { |
| const allProcesses = await getAllProcesses(); |
| const matches = allProcesses.filter((item) => item.processName === name); |
| if (matches.length === 1) { |
| return matches[0].id; |
| } |
| const id = await processPicker(allProcesses, name); |
| return id; |
| } |
| |
| export async function pickGoProcess(): Promise<string> { |
| const allProcesses = await getGoProcesses(); |
| const id = await processPicker(allProcesses); |
| return id; |
| } |
| |
| async function processPicker(processes: AttachItem[], name?: string): Promise<string> { |
| // We need to use createQuickPick instead of showQuickPick |
| // to set the starting value for the menu. |
| const menu = vscode.window.createQuickPick<AttachItem>(); |
| if (name) { |
| menu.value = name; |
| } |
| menu.items = processes; |
| menu.placeholder = 'Choose a process to attach to'; |
| menu.matchOnDescription = true; |
| menu.matchOnDetail = true; |
| return new Promise<string>(async (resolve, reject) => { |
| menu.onDidAccept(() => { |
| if (menu.selectedItems.length !== 1) { |
| reject(new Error('No process selected.')); |
| } |
| const selectedId = menu.selectedItems[0].id; |
| resolve(selectedId); |
| }); |
| // The quickpick menu can be hidden either explicitly or |
| // through other UI interactions. When the quick pick menu is |
| // missing we want to keep the debugging setup process from |
| // hanging, so we reject the selection. |
| menu.onDidHide(() => { |
| reject(new Error('No process selected.')); |
| }); |
| |
| menu.show(); |
| }).finally(() => menu.dispose()); |
| } |
| |
| // Modified from: |
| // https://github.com/microsoft/vscode-python/blob/main/src/client/debugger/extension/attachQuickPick/provider.ts |
| // - This extension adds a function for identifying the Go processes (getGoProcesses) |
| export interface AttachItem extends QuickPickItem { |
| id: string; |
| processName?: string; |
| commandLine?: string; |
| isGo?: boolean; |
| executable?: string; |
| } |
| |
| export interface ProcessListCommand { |
| command: string; |
| args: string[]; |
| } |
| |
| async function getGoProcesses(): Promise<AttachItem[]> { |
| const processes = await getAllProcesses(); |
| if (process.platform === 'darwin') { |
| // The executable paths are not set for darwin. |
| // It appears there is no standard way to get the executable |
| // path for a running process on darwin os. This implementation |
| // may not work for all processes. |
| const lsofOutput = await runCommand(lsofDarwinCommand); |
| if (lsofOutput.err) { |
| // We weren't able to run lsof, return all processes found. |
| return processes; |
| } |
| const darwinExes = parseLsofProcesses(lsofOutput.stdout); |
| // Merge the executable path to the processes obtained using ps. |
| mergeExecutableAttachItem(processes, darwinExes); |
| } |
| |
| // Run 'go version' on all executable paths to find 'go' processes |
| const goRuntimePath = getBinPath('go'); |
| if (!goRuntimePath) { |
| vscode.window.showErrorMessage( |
| `Failed to run "go version" as the "go" binary cannot be found in either GOROOT(${getCurrentGoRoot()}) or PATH(${getEnvPath()})` |
| ); |
| return processes; |
| } |
| const args = ['version']; |
| processes.forEach((item) => { |
| if (item.executable) { |
| args.push(item.executable); |
| } |
| }); |
| |
| const { stdout } = await runCommand({ command: goRuntimePath, args }); |
| |
| // Parse the executable paths from stdout. Ignore stderr, since we expect many to fail. |
| const goProcessExecutables: string[] = parseGoVersionOutput(stdout); |
| |
| const goProcesses: AttachItem[] = []; |
| processes.forEach((item) => { |
| if (item.executable && goProcessExecutables.indexOf(item.executable) >= 0) { |
| item.isGo = true; |
| goProcesses.push(item); |
| } |
| }); |
| |
| return goProcesses; |
| } |
| |
| export function parseGoVersionOutput(stdout: string): string[] { |
| const goProcessExes: string[] = []; |
| const goVersionRegexp = /: go\d+\.\d+(\.\d+)?$/; |
| |
| const lines = stdout.toString().split('\n'); |
| lines.forEach((line) => { |
| const match = line.match(goVersionRegexp); |
| if (match && match.length > 0) { |
| const exe = line.substr(0, line.length - match[0].length); |
| goProcessExes.push(exe); |
| } |
| }); |
| return goProcessExes; |
| } |
| |
| export const compareByProcessId = (a: AttachItem, b: AttachItem) => { |
| return parseInt(a.id, 10) - parseInt(b.id, 10); |
| }; |
| |
| export function mergeExecutableAttachItem(processes: AttachItem[], addlAttachItemInfo: AttachItem[]) { |
| processes.sort(compareByProcessId); |
| addlAttachItemInfo.sort(compareByProcessId); |
| let aIdx = 0; |
| let pIdx = 0; |
| while (aIdx < addlAttachItemInfo.length && pIdx < processes.length) { |
| const aAttachItem = addlAttachItemInfo[aIdx]; |
| const pAttachItem = processes[pIdx]; |
| if (aAttachItem.id === pAttachItem.id) { |
| pAttachItem.executable = aAttachItem.executable; |
| aIdx++; |
| pIdx++; |
| continue; |
| } |
| |
| if (compareByProcessId(pAttachItem, aAttachItem) > 0) { |
| aIdx++; |
| } else { |
| pIdx++; |
| } |
| } |
| } |
| |
| async function getAllProcesses(): Promise<AttachItem[]> { |
| let processCmd: ProcessListCommand; |
| switch (process.platform) { |
| case 'win32': |
| processCmd = wmicCommand; |
| break; |
| case 'darwin': |
| processCmd = psDarwinCommand; |
| break; |
| case 'linux': |
| processCmd = psLinuxCommand; |
| break; |
| default: |
| // Other operating systems are not supported. |
| throw new Error( |
| `'pickProcess' and 'pickGoProcess' are not supported for ${process.platform}. Set process id in launch.json instead.` |
| ); |
| } |
| |
| const { stdout } = await runCommand(processCmd); |
| |
| return process.platform === 'win32' ? parseWmicProcesses(stdout) : parsePsProcesses(stdout); |
| } |
| |
| async function runCommand( |
| processCmd: ProcessListCommand |
| ): Promise<{ err: cp.ExecException | null; stdout: string; stderr: string }> { |
| return await new Promise<{ err: cp.ExecException | null; stdout: string; stderr: string }>((resolve) => { |
| cp.execFile(processCmd.command, processCmd.args, (err, stdout, stderr) => { |
| resolve({ err, stdout, stderr }); |
| }); |
| }); |
| } |