brintos

brintos / llvm-project-archived public Read only

0
0
Text · 6.9 KiB · 4e34896 Raw
213 lines · typescript
1import { FSWatcher, watch as chokidarWatch } from 'chokidar';2import * as child_process from "node:child_process";3import * as path from "path";4import { isDeepStrictEqual } from "util";5import * as vscode from "vscode";6 7/**8 * Represents a running lldb-dap process that is accepting connections (i.e. in "server mode").9 *10 * Handles startup of the process if it isn't running already as well as prompting the user11 * to restart when arguments have changed.12 */13export class LLDBDapServer implements vscode.Disposable {14  private serverProcess?: child_process.ChildProcessWithoutNullStreams;15  private serverInfo?: Promise<{ host: string; port: number }>;16  private serverSpawnInfo?: string[];17  // Detects changes to the lldb-dap executable file since the server's startup.18  private serverFileWatcher?: FSWatcher;19  // Indicates whether the lldb-dap executable file has changed since the server's startup.20  private serverFileChanged?: boolean;21 22  constructor() {23    vscode.commands.registerCommand(24      "lldb-dap.getServerProcess",25      () => this.serverProcess,26    );27  }28 29  /**30   * Starts the server with the provided options. The server will be restarted or reused as31   * necessary.32   *33   * @param dapPath the path to the debug adapter executable34   * @param args the list of arguments to provide to the debug adapter35   * @param options the options to provide to the debug adapter process36   * @returns a promise that resolves with the host and port information or `undefined` if unable to launch the server.37   */38  async start(39    dapPath: string,40    args: string[],41    options?: child_process.SpawnOptionsWithoutStdio,42    connectionTimeoutSeconds?: number,43  ): Promise<{ host: string; port: number } | undefined> {44    // Both the --connection and --connection-timeout arguments are subject to the shouldContinueStartup() check.45    const connectionTimeoutArgs =46      connectionTimeoutSeconds && connectionTimeoutSeconds > 047        ? ["--connection-timeout", `${connectionTimeoutSeconds}`]48        : [];49    const dapArgs = [50      ...args,51      "--connection",52      "listen://localhost:0",53      ...connectionTimeoutArgs,54    ];55    if (!(await this.shouldContinueStartup(dapPath, dapArgs, options?.env))) {56      return undefined;57    }58 59    if (this.serverInfo) {60      return this.serverInfo;61    }62 63    this.serverInfo = new Promise((resolve, reject) => {64      const process = child_process.spawn(dapPath, dapArgs, options);65      process.on("error", (error) => {66        reject(error);67        this.cleanUp(process);68      });69      process.on("exit", (code, signal) => {70        let errorMessage = "Server process exited early";71        if (code !== undefined) {72          errorMessage += ` with code ${code}`;73        } else if (signal !== undefined) {74          errorMessage += ` due to signal ${signal}`;75        }76        reject(new Error(errorMessage));77        this.cleanUp(process);78      });79      process.stdout.setEncoding("utf8").on("data", (data) => {80        const connection = /connection:\/\/\[([^\]]+)\]:(\d+)/.exec(81          data.toString(),82        );83        if (connection) {84          const host = connection[1];85          const port = Number(connection[2]);86          resolve({ host, port });87          process.stdout.removeAllListeners();88        }89      });90      this.serverProcess = process;91      this.serverSpawnInfo = this.getSpawnInfo(dapPath, dapArgs, options?.env);92      this.serverFileChanged = false;93      this.serverFileWatcher = chokidarWatch(dapPath);94      this.serverFileWatcher95        .on('change', () => this.serverFileChanged = true)96        .on('unlink', () => this.serverFileChanged = true);97    });98    return this.serverInfo;99  }100 101  /**102   * Checks to see if the server needs to be restarted. If so, it will prompt the user103   * to ask if they wish to restart.104   *105   * @param dapPath the path to the debug adapter106   * @param args the arguments for the debug adapter107   * @returns whether or not startup should continue depending on user input108   */109  private async shouldContinueStartup(110    dapPath: string,111    args: string[],112    env: NodeJS.ProcessEnv | { [key: string]: string } | undefined,113  ): Promise<boolean> {114    if (115      !this.serverProcess ||116      !this.serverInfo ||117      !this.serverSpawnInfo ||118      !this.serverFileWatcher ||119      this.serverFileChanged === undefined120    ) {121      return true;122    }123 124    const changeTLDR = [];125    const changeDetails = [];126 127    if (this.serverFileChanged) {128      changeTLDR.push("an old binary");129    }130 131    const newSpawnInfo = this.getSpawnInfo(dapPath, args, env);132    if (!isDeepStrictEqual(this.serverSpawnInfo, newSpawnInfo)) {133      changeTLDR.push("different arguments");134      changeDetails.push(`135The previous lldb-dap server was started with:136 137${this.serverSpawnInfo.join(" ")}138 139The new lldb-dap server will be started with:140 141${newSpawnInfo.join(" ")}142`143      );144    }145 146    // If the server hasn't changed, continue startup without killing it.147    if (changeTLDR.length === 0) {148      return true;149    }150 151    // The server has changed. Prompt the user to restart it.152    const userInput = await vscode.window.showInformationMessage(153      "The lldb-dap server has changed. Would you like to restart the server?",154      {155        modal: true,156        detail: `An existing lldb-dap server (${this.serverProcess.pid}) is running with ${changeTLDR.map(s => `*${s}*`).join(" and ")}.157${changeDetails.join("\n")}158Restarting the server will interrupt any existing debug sessions and start a new server.`,159      },160      "Restart",161      "Use Existing",162    );163    switch (userInput) {164      case "Restart":165        this.dispose();166        return true;167      case "Use Existing":168        return true;169      case undefined:170        return false;171    }172  }173 174  dispose() {175    if (!this.serverProcess) {176      return;177    }178    this.serverProcess.kill();179    this.cleanUp(this.serverProcess);180  }181 182  cleanUp(process: child_process.ChildProcessWithoutNullStreams) {183    // If the following don't equal, then the fields have already been updated184    // (either a new process has started, or the fields were already cleaned185    // up), and so the cleanup should be skipped.186    if (this.serverProcess === process) {187      this.serverProcess = undefined;188      this.serverInfo = undefined;189      this.serverSpawnInfo = undefined;190      this.serverFileWatcher?.close();191      this.serverFileWatcher = undefined;192      this.serverFileChanged = undefined;193    }194  }195 196  getSpawnInfo(197    path: string,198    args: string[],199    env: NodeJS.ProcessEnv | { [key: string]: string } | undefined,200  ): string[] {201    return [202      path,203      ...args,204      ...Object.entries(env ?? {})205        // Filter and sort to avoid restarting the server just because the206        // order of env changed or the log path changed.207        .filter((entry) => String(entry[0]) !== "LLDBDAP_LOG")208        .sort()209        .map((entry) => String(entry[0]) + "=" + String(entry[1])),210    ];211  }212}213