Skip to content

Commit

Permalink
[DUX-2058] Fix bug in use of child_process (#64)
Browse files Browse the repository at this point in the history
  • Loading branch information
friedbrice authored Mar 18, 2024
1 parent 414acc1 commit 9bf0188
Show file tree
Hide file tree
Showing 6 changed files with 44 additions and 23 deletions.
4 changes: 3 additions & 1 deletion src/activationcommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
})

Expand Down
2 changes: 1 addition & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export function deactivate(): Promise<void> {
}

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()
Expand Down
2 changes: 1 addition & 1 deletion src/formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
)
Expand Down
2 changes: 1 addition & 1 deletion src/onsaverunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (() => {
Expand All @@ -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))
}
}

Expand Down Expand Up @@ -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 {
Expand Down
51 changes: 35 additions & 16 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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<T>(spec: Spec, f: (stdout: string) => T): IAsyncProcess<T> {
export function spawn(spec: Spec): IAsyncProcess<void> {
return make<void>(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<T>(spec: Spec, f: (stdout: string) => T): IAsyncProcess<T> {
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<T>(spec: Spec, makeProc: (command: string, opts: {cwd?: string, signal?: AbortSignal}, resolve: (t: T) => void) => child_process.ChildProcess): IAsyncProcess<T> {
let controller: AbortController | undefined = new AbortController()
const { signal } = controller

Expand All @@ -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<T>((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) {
Expand All @@ -90,7 +109,7 @@ export namespace AsyncProcess {

asyncProc.then(() => output?.appendLine(alloglot.ui.ranCommand(command)))

return asyncProc as IAsyncProcess<T>
return asyncProc
}

const stripAnsi: (raw: string) => string = require('strip-ansi').default
Expand Down

0 comments on commit 9bf0188

Please sign in to comment.