From 9bf0188905e5f916201c259243dccd5262bbf5d1 Mon Sep 17 00:00:00 2001 From: "Daniel P. Brice" Date: Mon, 18 Mar 2024 14:02:10 -0700 Subject: [PATCH] [DUX-2058] Fix bug in use of child_process (#64) --- src/activationcommand.ts | 4 +++- src/extension.ts | 2 +- src/formatter.ts | 2 +- src/onsaverunner.ts | 2 +- src/tags.ts | 6 ++--- src/utils.ts | 51 +++++++++++++++++++++++++++------------- 6 files changed, 44 insertions(+), 23 deletions(-) diff --git a/src/activationcommand.ts b/src/activationcommand.ts index ca43b15..50ca6c6 100644 --- a/src/activationcommand.ts +++ b/src/activationcommand.ts @@ -9,7 +9,9 @@ export function makeActivationCommand(parentOutput: IHierarchicalOutputChannel, const output = parentOutput.split() reveal && output.show(true) - const proc = AsyncProcess.make({ output, command, basedir }, () => { + const proc = AsyncProcess.spawn({ output, command, basedir }) + + proc.then(() => () => { parentOutput.appendLine(alloglot.ui.activateCommandDone(command)) }) diff --git a/src/extension.ts b/src/extension.ts index 4f1745c..967d517 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -71,7 +71,7 @@ export function deactivate(): Promise { } if (command) { - return AsyncProcess.make({ output: globalOutput, command, basedir }, () => undefined) + return AsyncProcess.exec({ output: globalOutput, command, basedir }, () => undefined) .then(() => { globalOutput?.appendLine(alloglot.ui.deactivateCommandDone(command)) return cleanup() diff --git a/src/formatter.ts b/src/formatter.ts index a942a1f..1344a02 100644 --- a/src/formatter.ts +++ b/src/formatter.ts @@ -51,7 +51,7 @@ export function makeFormatter(output: vscode.OutputChannel, config: LanguageConf document.lineAt(document.lineCount - 1).rangeIncludingLineBreak.end, ); - const proc = AsyncProcess.make( + const proc = AsyncProcess.exec( { output: verboseOutput ? output : undefined, command, basedir, stdin }, stdout => [new vscode.TextEdit(entireDocument, stdout)] ) diff --git a/src/onsaverunner.ts b/src/onsaverunner.ts index b724d05..1089851 100644 --- a/src/onsaverunner.ts +++ b/src/onsaverunner.ts @@ -17,7 +17,7 @@ export function makeOnSaveRunner(output: vscode.OutputChannel, config: LanguageC const refreshTags = (doc: vscode.TextDocument) => { if (doc.languageId === languageId) { const command = onSaveCommand.replace('${file}', doc.fileName) - disposal.insert(AsyncProcess.make({ output, command, basedir }, () => undefined)) + disposal.insert(AsyncProcess.exec({ output, command, basedir }, () => undefined)) } } diff --git a/src/tags.ts b/src/tags.ts index b9f1704..25cc9ea 100644 --- a/src/tags.ts +++ b/src/tags.ts @@ -229,7 +229,7 @@ namespace TagsSource { if (initTagsCommand) { const command = initTagsCommand - disposal.insert(AsyncProcess.make({ output, command, basedir }, () => undefined)) + disposal.insert(AsyncProcess.exec({ output, command, basedir }, () => undefined)) } const onSaveWatcher = (() => { @@ -238,7 +238,7 @@ namespace TagsSource { const refreshTags = (doc: vscode.TextDocument) => { if (doc.languageId === languageId) { const command = refreshTagsCommand.replace('${file}', doc.fileName) - disposal.insert(AsyncProcess.make({ output, command, basedir }, () => undefined)) + disposal.insert(AsyncProcess.exec({ output, command, basedir }, () => undefined)) } } @@ -274,7 +274,7 @@ namespace TagsSource { const command = `${grepPath} -P '${regexp.source}' ${tagsUri.fsPath} | head -n ${limit}` output?.appendLine(`Searching for ${regexp} in ${tagsUri.fsPath}...`) - return AsyncProcess.make({ output, command, basedir }, stdout => filterMap(stdout.split('\n'), line => parseTag(line, output))) + return AsyncProcess.exec({ output, command, basedir }, stdout => filterMap(stdout.split('\n'), line => parseTag(line, output))) } function parseTag(line: string, output?: vscode.OutputChannel): Tag | undefined { diff --git a/src/utils.ts b/src/utils.ts index 02548fc..d115dc7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import { exec } from 'child_process' +import * as child_process from 'child_process' import * as vscode from 'vscode' import { alloglot } from './config' @@ -37,11 +37,33 @@ export namespace AsyncProcess { } /** - * Create an {@link IAsyncProcess async process} that runs a command and returns a promise of the result. - * `make(spec, f)` computes the result by running the `f` on the process stdout. + * Create a (potentially) long-lived {@link IAsyncProcess async process}. + * `spawn(spec)` runs a command and returns a promise that resolves when the process exits. * Method `dispose()` kills the process and is idempotent. */ - export function make(spec: Spec, f: (stdout: string) => T): IAsyncProcess { + export function spawn(spec: Spec): IAsyncProcess { + return make(spec, (cmd, opts, resolve) => { + const proc = child_process.spawn(cmd, [], {...opts, shell: true}) + proc.on('exit', resolve) + return proc + }) + } + + /** + * Create a short-lived {@link IAsyncProcess async process} that runs a command and returns a promise of the result. + * `exec(spec, f)` computes the result by running the `f` on the process stdout. + * Method `dispose()` kills the process and is idempotent. + */ + export function exec(spec: Spec, f: (stdout: string) => T): IAsyncProcess { + return make(spec, (cmd, opts, resolve) => { + return child_process.exec(cmd, opts, (error, stdout, stderr) => { + !stdout && spec.output?.appendLine(alloglot.ui.commandNoOutput(spec.command)) + resolve(f(stdout)) + }) + }) + } + + function make(spec: Spec, makeProc: (command: string, opts: {cwd?: string, signal?: AbortSignal}, resolve: (t: T) => void) => child_process.ChildProcess): IAsyncProcess { let controller: AbortController | undefined = new AbortController() const { signal } = controller @@ -50,23 +72,20 @@ export namespace AsyncProcess { // giving this an `any` signature allows us to add a `dispose` method. // it's a little bit jank, but i don't know how else to do it. - const asyncProc: any = new Promise((resolve, reject) => { + const asyncProc: any = new Promise((resolve, reject) => { output?.appendLine(alloglot.ui.runningCommand(command, cwd)) try { - const proc = exec(command, { cwd, signal }, (error, stdout, stderr) => { - if (error) { - output?.appendLine(alloglot.ui.errorRunningCommand(command, error)) - reject(error) - } + const proc = makeProc(command, { cwd, signal }, resolve) - stderr && output?.appendLine(alloglot.ui.commandLogs(command, stderr)) - !stdout && output?.appendLine(alloglot.ui.commandNoOutput(command)) - - resolve(f(stdout)) + proc.on('error', error => { + output?.appendLine(alloglot.ui.errorRunningCommand(command, error)) + reject(error) }) - proc.stdout?.on('data', chunk => output?.append(stripAnsi(chunk))) + proc.stdout?.on('data', chunk => output?.append(stripAnsi(chunk.toString()))) + proc.stderr?.on('data', chunk => output?.append(stripAnsi(chunk.toString()))) + stdin && proc.stdin?.write(stdin) proc.stdin?.end() } catch (err) { @@ -90,7 +109,7 @@ export namespace AsyncProcess { asyncProc.then(() => output?.appendLine(alloglot.ui.ranCommand(command))) - return asyncProc as IAsyncProcess + return asyncProc } const stripAnsi: (raw: string) => string = require('strip-ansi').default