diff --git a/mod.test.ts b/mod.test.ts index cb89cd5..f2bd7cd 100644 --- a/mod.test.ts +++ b/mod.test.ts @@ -95,7 +95,7 @@ Deno.test("should not get stderr when set to writer", async () => { assertThrows( () => output.stderr, Error, - `Stderr was not piped (was streamed). Call .stderr(\"piped\") or .stderr(\"inheritPiped\") when building the command.`, + `Stderr was streamed to another source and is no longer available.`, ); }); @@ -374,14 +374,11 @@ Deno.test("should handle boolean list 'and'", async () => { Deno.test("should support custom command handlers", async () => { const builder = new CommandBuilder() - .registerCommand("zardoz-speaks", (context) => { + .registerCommand("zardoz-speaks", async (context) => { if (context.args.length != 1) { - context.stderr.writeLine("zardoz-speaks: expected 1 argument"); - return { - code: 1, - }; + return context.error("zardoz-speaks: expected 1 argument"); } - context.stdout.writeLine(`zardoz speaks to ${context.args[0]}`); + await context.stdout.writeLine(`zardoz speaks to ${context.args[0]}`); return { code: 0, }; @@ -802,7 +799,7 @@ Deno.test("piping to stdin", async () => { .stderr("piped") .noThrow(); assertEquals(result.code, 1); - assertEquals(result.stderr, "stdin pipe broken. Error: Exited with code: 1\n"); + assertEquals(result.stderr, "stdin pipe broken. Exited with code: 1\n"); } }); @@ -862,13 +859,9 @@ Deno.test("piping to a writable that throws", async () => { throw new Error("failed"); }, }); - await assertRejects( - async () => { - await $`echo 1`.stdout(writableStream); - }, - Error, - "failed", - ); + const result = await $`echo 1`.stdout(writableStream).stderr("piped").noThrow(); + assertEquals(result.code, 1); + assertEquals(result.stderr, "echo: failed\n"); }); Deno.test("piping stdout/stderr to a file", async () => { @@ -1030,7 +1023,7 @@ Deno.test("streaming api errors while streaming", async () => { .stdout("piped") .stderr("piped") .spawn(); - assertEquals(result.stderr, "stdin pipe broken. Error: Exited with code: 1\n"); + assertEquals(result.stderr, "stdin pipe broken. Exited with code: 1\n"); assertEquals(result.stdout, "1\n2\n"); } }); diff --git a/src/command.ts b/src/command.ts index cd64eca..1ba9b2c 100644 --- a/src/command.ts +++ b/src/command.ts @@ -18,6 +18,7 @@ import { Delay } from "./common.ts"; import { Buffer, colors, path, readerFromStreamReader } from "./deps.ts"; import { CapturingBufferWriter, + CapturingBufferWriterSync, InheritStaticTextBypassWriter, NullPipeWriter, PipedBuffer, @@ -25,12 +26,14 @@ import { ShellPipeReaderKind, ShellPipeWriter, ShellPipeWriterKind, + Writer, WriterSync, } from "./pipes.ts"; import { parseCommand, spawn } from "./shell.ts"; import { isShowingProgressBars } from "./console/progress/interval.ts"; import { PathRef } from "./path.ts"; import { RequestBuilder } from "./request.ts"; +import { writerFromStreamWriter } from "https://deno.land/std@0.213.0/streams/writer_from_stream_writer.ts"; type BufferStdio = "inherit" | "null" | "streamed" | Buffer; @@ -823,59 +826,42 @@ export function parseAndSpawnCommand(state: CommandBuilderState) { } const combinedBuffer = new Buffer(); return [ - new CapturingBufferWriter(stdoutBuffer, combinedBuffer), - new CapturingBufferWriter(stderrBuffer, combinedBuffer), + getCapturingBuffer(stdoutBuffer, combinedBuffer), + getCapturingBuffer(stderrBuffer, combinedBuffer), combinedBuffer, ] as const; } return [stdoutBuffer, stderrBuffer, undefined] as const; - function getOutputBuffer(innerWriter: WriterSync, { kind, options }: ShellPipeWriterKindWithOptions) { + function getCapturingBuffer(buffer: Writer | WriterSync, combinedBuffer: Buffer) { + if ("write" in buffer) { + return new CapturingBufferWriter(buffer, combinedBuffer); + } else { + return new CapturingBufferWriterSync(buffer, combinedBuffer); + } + } + + function getOutputBuffer(inheritWriter: WriterSync, { kind, options }: ShellPipeWriterKindWithOptions) { if (typeof kind === "object") { if (kind instanceof PathRef) { const file = kind.openSync({ write: true, truncate: true, create: true }); disposables.push(file); return file; } else if (kind instanceof WritableStream) { - // this is sketch - const writer = kind.getWriter(); - const promiseMap = new Map>(); - let hadError = false; - let foundErr: unknown = undefined; - let index = 0; + const streamWriter = kind.getWriter(); asyncDisposables.push({ async [Symbol.asyncDispose]() { - await Promise.all(promiseMap.values()); - if (foundErr) { - throw foundErr; - } - if (!options?.preventClose && !hadError) { - await writer.close(); + streamWriter.releaseLock(); + if (!options?.preventClose) { + try { + await kind.close(); + } catch { + // ignore, the stream have errored + } } }, }); - return { - writeSync(buffer: Uint8Array) { - if (foundErr) { - const errorToThrow = foundErr; - foundErr = undefined; - throw errorToThrow; - } - const newIndex = index++; - promiseMap.set( - newIndex, - writer.write(buffer).catch((err) => { - if (err != null) { - foundErr = err; - hadError = true; - } - }).finally(() => { - promiseMap.delete(newIndex); - }), - ); - return buffer.length; - }, - }; + return writerFromStreamWriter(streamWriter); } else { return kind; } @@ -883,14 +869,14 @@ export function parseAndSpawnCommand(state: CommandBuilderState) { switch (kind) { case "inherit": if (hasProgressBars) { - return new InheritStaticTextBypassWriter(innerWriter); + return new InheritStaticTextBypassWriter(inheritWriter); } else { return "inherit"; } case "piped": return new PipedBuffer(); case "inheritPiped": - return new CapturingBufferWriter(innerWriter, new Buffer()); + return new CapturingBufferWriterSync(inheritWriter, new Buffer()); case "null": return "null"; default: { @@ -902,9 +888,17 @@ export function parseAndSpawnCommand(state: CommandBuilderState) { } function finalizeCommandResultBuffer( - buffer: PipedBuffer | "inherit" | "null" | CapturingBufferWriter | InheritStaticTextBypassWriter | WriterSync, + buffer: + | PipedBuffer + | "inherit" + | "null" + | CapturingBufferWriter + | CapturingBufferWriterSync + | InheritStaticTextBypassWriter + | Writer + | WriterSync, ): BufferStdio { - if (buffer instanceof CapturingBufferWriter) { + if (buffer instanceof CapturingBufferWriterSync || buffer instanceof CapturingBufferWriter) { return buffer.getBuffer(); } else if (buffer instanceof InheritStaticTextBypassWriter) { buffer.flush(); // this is line buffered, so flush anything left @@ -920,7 +914,15 @@ export function parseAndSpawnCommand(state: CommandBuilderState) { } function finalizeCommandResultBufferForError( - buffer: PipedBuffer | "inherit" | "null" | CapturingBufferWriter | InheritStaticTextBypassWriter | WriterSync, + buffer: + | PipedBuffer + | "inherit" + | "null" + | CapturingBufferWriter + | CapturingBufferWriterSync + | InheritStaticTextBypassWriter + | Writer + | WriterSync, error: Error, ) { if (buffer instanceof InheritStaticTextBypassWriter) { @@ -1013,7 +1015,7 @@ export class CommandResult { /** Raw stderr bytes. */ get stderrBytes(): Uint8Array { - if (this.#stdout === "streamed") { + if (this.#stderr === "streamed") { throw new Error( `Stderr was streamed to another source and is no longer available.`, ); diff --git a/src/command_handler.ts b/src/command_handler.ts index d99f7a8..d297fcd 100644 --- a/src/command_handler.ts +++ b/src/command_handler.ts @@ -1,15 +1,15 @@ import { ExecuteResult } from "./result.ts"; import type { KillSignal } from "./command.ts"; -import { Reader, WriterSync } from "./pipes.ts"; +import { Reader } from "./pipes.ts"; /** Used to read from stdin. */ export type CommandPipeReader = "inherit" | "null" | Reader; /** Used to write to stdout or stderr. */ -export interface CommandPipeWriter extends WriterSync { - writeSync(p: Uint8Array): number; - writeText(text: string): void; - writeLine(text: string): void; +export interface CommandPipeWriter { + write(p: Uint8Array): Promise | number; + writeText(text: string): Promise | void; + writeLine(text: string): Promise | void; } /** Context of the currently executing command. */ @@ -21,6 +21,10 @@ export interface CommandContext { get stdout(): CommandPipeWriter; get stderr(): CommandPipeWriter; get signal(): KillSignal; + /// Helper function for writing a line to stderr and returning a 1 exit code. + error(message: string): Promise | ExecuteResult; + /// Helper function for writing a line to stderr and returning the provided exit code. + error(code: number, message: string): Promise | ExecuteResult; } /** Handler for executing a command. */ diff --git a/src/commands/cat.ts b/src/commands/cat.ts index 70426f3..85ec2fc 100644 --- a/src/commands/cat.ts +++ b/src/commands/cat.ts @@ -14,8 +14,7 @@ export async function catCommand( const code = await executeCat(context); return { code }; } catch (err) { - context.stderr.writeLine(`cat: ${err?.message ?? err}`); - return { code: 1 }; + return context.error(`cat: ${err?.message ?? err}`); } } @@ -29,8 +28,14 @@ async function executeCat(context: CommandContext) { if (typeof context.stdin === "object") { // stdin is a Reader while (!context.signal.aborted) { const size = await context.stdin.read(buf); - if (!size || size === 0) break; - else context.stdout.writeSync(buf.slice(0, size)); + if (!size || size === 0) { + break; + } else { + const maybePromise = context.stdout.write(buf.slice(0, size)); + if (maybePromise instanceof Promise) { + await maybePromise; + } + } } exitCode = context.signal.abortedExitCode ?? 0; } else { @@ -44,15 +49,24 @@ async function executeCat(context: CommandContext) { while (!context.signal.aborted) { // NOTE: rust supports cancellation here const size = file.readSync(buf); - if (!size || size === 0) break; - else context.stdout.writeSync(buf.slice(0, size)); + if (!size || size === 0) { + break; + } else { + const maybePromise = context.stdout.write(buf.slice(0, size)); + if (maybePromise instanceof Promise) { + await maybePromise; + } + } } exitCode = context.signal.abortedExitCode ?? 0; } catch (err) { - context.stderr.writeLine(`cat ${path}: ${err}`); + const maybePromise = context.stderr.writeLine(`cat ${path}: ${err?.message ?? err}`); + if (maybePromise instanceof Promise) { + await maybePromise; + } exitCode = 1; } finally { - if (file) file.close(); + file?.close(); } } } @@ -62,10 +76,15 @@ async function executeCat(context: CommandContext) { export function parseCatArgs(args: string[]): CatFlags { const paths = []; for (const arg of parseArgKinds(args)) { - if (arg.kind === "Arg") paths.push(arg.arg); - else bailUnsupported(arg); // for now, we don't support any arguments + if (arg.kind === "Arg") { + paths.push(arg.arg); + } else { + bailUnsupported(arg); // for now, we don't support any arguments + } } - if (paths.length === 0) paths.push("-"); + if (paths.length === 0) { + paths.push("-"); + } return { paths }; } diff --git a/src/commands/cd.ts b/src/commands/cd.ts index 3d2c515..02badf9 100644 --- a/src/commands/cd.ts +++ b/src/commands/cd.ts @@ -13,8 +13,7 @@ export async function cdCommand(context: CommandContext): Promise }], }; } catch (err) { - context.stderr.writeLine(`cd: ${err?.message ?? err}`); - return { code: 1 }; + return context.error(`cd: ${err?.message ?? err}`); } } diff --git a/src/commands/cp_mv.ts b/src/commands/cp_mv.ts index f7b04a7..a6d9218 100644 --- a/src/commands/cp_mv.ts +++ b/src/commands/cp_mv.ts @@ -11,8 +11,7 @@ export async function cpCommand( await executeCp(context.cwd, context.args); return { code: 0 }; } catch (err) { - context.stderr.writeLine(`cp: ${err?.message ?? err}`); - return { code: 1 }; + return context.error(`cp: ${err?.message ?? err}`); } } @@ -101,8 +100,7 @@ export async function mvCommand( await executeMove(context.cwd, context.args); return { code: 0 }; } catch (err) { - context.stderr.writeLine(`mv: ${err?.message ?? err}`); - return { code: 1 }; + return context.error(`mv: ${err?.message ?? err}`); } } diff --git a/src/commands/echo.ts b/src/commands/echo.ts index 43e5a33..f7b2165 100644 --- a/src/commands/echo.ts +++ b/src/commands/echo.ts @@ -1,7 +1,19 @@ import { CommandContext } from "../command_handler.ts"; import { ExecuteResult } from "../result.ts"; -export function echoCommand(context: CommandContext): ExecuteResult { - context.stdout.writeLine(context.args.join(" ")); - return { code: 0 }; +export function echoCommand(context: CommandContext): ExecuteResult | Promise { + try { + const maybePromise = context.stdout.writeLine(context.args.join(" ")); + if (maybePromise instanceof Promise) { + return maybePromise.then(() => ({ code: 0 })).catch((err) => handleFailure(context, err)); + } else { + return { code: 0 }; + } + } catch (err) { + return handleFailure(context, err); + } +} + +function handleFailure(context: CommandContext, err: any) { + return context.error(`echo: ${err?.message ?? err}`); } diff --git a/src/commands/exit.ts b/src/commands/exit.ts index 8640b06..992219a 100644 --- a/src/commands/exit.ts +++ b/src/commands/exit.ts @@ -1,7 +1,7 @@ import { CommandContext } from "../command_handler.ts"; -import { ExitExecuteResult } from "../result.ts"; +import { ExecuteResult } from "../result.ts"; -export function exitCommand(context: CommandContext): ExitExecuteResult { +export function exitCommand(context: CommandContext): ExecuteResult | Promise { try { const code = parseArgs(context.args); return { @@ -9,12 +9,7 @@ export function exitCommand(context: CommandContext): ExitExecuteResult { code, }; } catch (err) { - context.stderr.writeLine(`exit: ${err?.message ?? err}`); - // impl. note: bash returns 2 on exit parse failure, deno_task_shell returns 1 - return { - kind: "exit", - code: 2, - }; + return context.error(2, `exit: ${err?.message ?? err}`); } } diff --git a/src/commands/mkdir.ts b/src/commands/mkdir.ts index 53b4191..e603a9c 100644 --- a/src/commands/mkdir.ts +++ b/src/commands/mkdir.ts @@ -11,8 +11,7 @@ export async function mkdirCommand( await executeMkdir(context.cwd, context.args); return { code: 0 }; } catch (err) { - context.stderr.writeLine(`mkdir: ${err?.message ?? err}`); - return { code: 1 }; + return context.error(`mkdir: ${err?.message ?? err}`); } } diff --git a/src/commands/printenv.ts b/src/commands/printenv.ts index 6186bcd..56e639c 100644 --- a/src/commands/printenv.ts +++ b/src/commands/printenv.ts @@ -1,7 +1,7 @@ import { CommandContext } from "../command_handler.ts"; import { ExecuteResult } from "../result.ts"; -export function printEnvCommand(context: CommandContext): ExecuteResult { +export function printEnvCommand(context: CommandContext): ExecuteResult | Promise { // windows expects env vars to be upcased let args; if (Deno.build.os === "windows") { @@ -12,17 +12,22 @@ export function printEnvCommand(context: CommandContext): ExecuteResult { try { const result = executePrintEnv(context.env, args); - context.stdout.writeLine(result); - if (args.some((arg) => context.env[arg] === undefined)) { - return { code: 1 }; + const code = args.some((arg) => context.env[arg] === undefined) ? 1 : 0; + const maybePromise = context.stdout.writeLine(result); + if (maybePromise instanceof Promise) { + return maybePromise.then(() => ({ code })).catch((err) => handleError(context, err)); + } else { + return { code }; } - return { code: 0 }; } catch (err) { - context.stderr.writeLine(`printenv: ${err?.message ?? err}`); - return { code: 1 }; + return handleError(context, err); } } +function handleError(context: CommandContext, err: any): ExecuteResult | Promise { + return context.error(`printenv: ${err?.message ?? err}`); +} + /** * follows printenv on linux: * - if arguments are provided, return a string containing a list of all env variables as `repeat(KEY=VALUE\n)` diff --git a/src/commands/pwd.ts b/src/commands/pwd.ts index 2f76a49..8b5195e 100644 --- a/src/commands/pwd.ts +++ b/src/commands/pwd.ts @@ -3,17 +3,25 @@ import { path } from "../deps.ts"; import { ExecuteResult } from "../result.ts"; import { bailUnsupported, parseArgKinds } from "./args.ts"; -export function pwdCommand(context: CommandContext): ExecuteResult { +export function pwdCommand(context: CommandContext): ExecuteResult | Promise { try { const output = executePwd(context.cwd, context.args); - context.stdout.writeLine(output); - return { code: 0 }; + const maybePromise = context.stdout.writeLine(output); + const result = { code: 0 }; + if (maybePromise instanceof Promise) { + return maybePromise.then(() => result).catch((err) => handleError(context, err)); + } else { + return result; + } } catch (err) { - context.stderr.writeLine(`pwd: ${err?.message ?? err}`); - return { code: 1 }; + return handleError(context, err); } } +function handleError(context: CommandContext, err: any) { + return context.error(`pwd: ${err?.message ?? err}`); +} + function executePwd(cwd: string, args: string[]) { const flags = parseArgs(args); if (flags.logical) { diff --git a/src/commands/rm.ts b/src/commands/rm.ts index 455937e..0b31cc5 100644 --- a/src/commands/rm.ts +++ b/src/commands/rm.ts @@ -10,8 +10,7 @@ export async function rmCommand( await executeRemove(context.cwd, context.args); return { code: 0 }; } catch (err) { - context.stderr.writeLine(`rm: ${err?.message ?? err}`); - return { code: 1 }; + return context.error(`rm: ${err?.message ?? err}`); } } diff --git a/src/commands/sleep.ts b/src/commands/sleep.ts index 8de2dbe..05e6297 100644 --- a/src/commands/sleep.ts +++ b/src/commands/sleep.ts @@ -26,8 +26,7 @@ export async function sleepCommand(context: CommandContext): Promise { try { return { code: 0, changes: parseNames(context.args).map((name) => ({ kind: "unsetvar", name })), }; } catch (err) { - context.stderr.writeLine(`unset: ${err?.message ?? err}`); - return { code: 1 }; + return context.error(`unset: ${err?.message ?? err}`); } } diff --git a/src/pipes.ts b/src/pipes.ts index f540862..44208e3 100644 --- a/src/pipes.ts +++ b/src/pipes.ts @@ -1,6 +1,6 @@ import { type FsFileWrapper, PathRef } from "./path.ts"; import { logger } from "./console/logger.ts"; -import { Buffer, writeAllSync } from "./deps.ts"; +import { Buffer, writeAll, writeAllSync } from "./deps.ts"; import type { RequestBuilder } from "./request.ts"; import type { CommandBuilder } from "./command.ts"; @@ -11,11 +11,25 @@ export interface Reader { read(p: Uint8Array): Promise; } +/** `Deno.ReaderSync` stream. */ +export interface ReaderSync { + readSync(p: Uint8Array): number | null; +} + /** `Deno.WriterSync` stream. */ export interface WriterSync { writeSync(p: Uint8Array): number; } +/** `Deno.Writer` stream. */ +export interface Writer { + write(p: Uint8Array): Promise; +} + +export type PipeReader = Reader | ReaderSync; + +export type PipeWriter = Writer | WriterSync; + /** `Deno.Closer` */ export interface Closer { close(): void; @@ -58,11 +72,11 @@ export class NullPipeWriter implements WriterSync { } } -export class ShellPipeWriter implements WriterSync { +export class ShellPipeWriter { #kind: ShellPipeWriterKind; - #inner: WriterSync; + #inner: PipeWriter; - constructor(kind: ShellPipeWriterKind, inner: WriterSync) { + constructor(kind: ShellPipeWriterKind, inner: PipeWriter) { this.#kind = kind; this.#inner = inner; } @@ -71,12 +85,24 @@ export class ShellPipeWriter implements WriterSync { return this.#kind; } - writeSync(p: Uint8Array) { - return this.#inner.writeSync(p); + write(p: Uint8Array) { + if ("write" in this.#inner) { + return this.#inner.write(p); + } else { + return this.#inner.writeSync(p); + } + } + + writeAll(data: Uint8Array) { + if ("write" in this.#inner) { + return writeAll(this.#inner, data); + } else { + return writeAllSync(this.#inner, data); + } } writeText(text: string) { - return writeAllSync(this, encoder.encode(text)); + return this.writeAll(encoder.encode(text)); } writeLine(text: string) { @@ -84,7 +110,27 @@ export class ShellPipeWriter implements WriterSync { } } -export class CapturingBufferWriter implements WriterSync { +export class CapturingBufferWriter implements Writer { + #buffer: Buffer; + #innerWriter: Writer; + + constructor(innerWriter: Writer, buffer: Buffer) { + this.#innerWriter = innerWriter; + this.#buffer = buffer; + } + + getBuffer() { + return this.#buffer; + } + + async write(p: Uint8Array) { + const nWritten = await this.#innerWriter.write(p); + this.#buffer.writeSync(p.slice(0, nWritten)); + return nWritten; + } +} + +export class CapturingBufferWriterSync implements WriterSync { #buffer: Buffer; #innerWriter: WriterSync; diff --git a/src/shell.ts b/src/shell.ts index c4cbd55..462fa71 100644 --- a/src/shell.ts +++ b/src/shell.ts @@ -233,34 +233,6 @@ function cloneEnv(env: Env) { return result; } -// todo: delete -// export class DisposableCollection { -// #disposables: Disposable[] = []; - -// add(disposable: Disposable) { -// this.#disposables.push(disposable); -// } - -// disposeHandlingErrors(stderr: ShellPipeWriter): ExecuteResult { -// if (this.#disposables.length > 0) { -// const errors = []; -// for (const disposable of this.#disposables) { -// try { -// disposable[Symbol.dispose](); -// } catch (err) { -// errors.push(err); -// } -// } -// if (errors.length > 0) { -// const error = new AggregateError(errors); -// stderr.writeLine("failed disposing context. " + error); -// return { code: 1 }; -// } -// } -// return { code: 0 }; -// } -// } - interface ContextOptions { stdin: CommandPipeReader; stdout: ShellPipeWriter; @@ -394,9 +366,33 @@ export class Context { get signal() { return context.signal; }, + error(codeOrText: number | string, maybeText?: string): Promise | ExecuteResult { + return context.error(codeOrText, maybeText); + }, }; } + error(text: string): Promise | ExecuteResult; + error(code: number, text: string): Promise | ExecuteResult; + error(codeOrText: number | string, maybeText: string | undefined): Promise | ExecuteResult; + error(codeOrText: number | string, maybeText?: string): Promise | ExecuteResult { + let code: number; + let text: string; + if (typeof codeOrText === "number") { + code = codeOrText; + text = maybeText!; + } else { + code = 1; + text = codeOrText; + } + const maybePromise = this.stderr.writeLine(text); + if (maybePromise instanceof Promise) { + return maybePromise.then(() => ({ code })); + } else { + return { code }; + } + } + withInner(opts: Partial>) { return new Context({ stdin: opts.stdin ?? this.stdin, @@ -646,8 +642,7 @@ async function executeCommand(command: Command, context: Context): Promise { - function handleFileOpenError(outputPath: string, err: unknown): ExecuteResult { - context.stderr.writeLine(`failed opening file for redirect (${outputPath}). ${err}`); - return { code: 1 }; + function handleFileOpenError(outputPath: string, err: any) { + return context.error(`failed opening file for redirect (${outputPath}). ${err?.message ?? err}`); } const fd = resolveRedirectFd(redirect, context); @@ -684,14 +678,12 @@ async function resolveRedirectPipe( const words = await evaluateWordParts(redirect.ioFile, context); // edge case that's not supported if (words.length === 0) { - context.stderr.writeLine("redirect path must be 1 argument, but found 0"); - return { code: 1 }; + return context.error("redirect path must be 1 argument, but found 0"); } else if (words.length > 1) { - context.stderr.writeLine( + return context.error( `redirect path must be 1 argument, but found ${words.length} (${words.join(" ")}). ` + `Did you mean to quote it (ex. "${words.join(" ")}")?`, ); - return { code: 1 }; } switch (redirect.op.kind) { @@ -741,18 +733,16 @@ async function resolveRedirectPipe( } } -function resolveRedirectFd(redirect: Redirect, context: Context): ExecuteResult | 1 | 2 { +function resolveRedirectFd(redirect: Redirect, context: Context): ExecuteResult | Promise | 1 | 2 { const maybeFd = redirect.maybeFd; if (maybeFd == null) { return 1; // stdout } if (maybeFd.kind === "stdoutStderr") { - context.stderr.writeLine("redirecting to both stdout and stderr is not implemented"); - return { code: 1 }; + return context.error("redirecting to both stdout and stderr is not implemented"); } if (maybeFd.fd !== 1 && maybeFd.fd !== 2) { - context.stderr.writeLine(`only redirecting to stdout (1) and stderr (2) is supported`); - return { code: 1 }; + return context.error(`only redirecting to stdout (1) and stderr (2) is supported`); } else { return maybeFd.fd; } @@ -820,13 +810,16 @@ async function executeCommandArgs(commandArgs: string[], context: Context): Prom const completeSignal = completeController.signal; let stdinError: unknown | undefined; const stdinPromise = writeStdin(context.stdin, p, completeSignal) - .catch((err) => { + .catch(async (err) => { // don't surface anything because it's already been aborted if (completeSignal.aborted) { return; } - context.stderr.writeLine(`stdin pipe broken. ${err}`); + const maybePromise = context.stderr.writeLine(`stdin pipe broken. ${err?.message ?? err}`); + if (maybePromise != null) { + await maybePromise; + } stdinError = err; // kill the sub process try { @@ -922,7 +915,7 @@ async function pipeReaderToWritable(reader: Reader, writable: WritableStream, - writer: WriterSync, + writer: ShellPipeWriter, signal: AbortSignal | KillSignal, ) { const reader = readable.getReader(); @@ -931,20 +924,16 @@ async function pipeReadableToWriterSync( if (result.done) { break; } - writeAllSync(result.value); - } - - function writeAllSync(arr: Uint8Array) { - let nwritten = 0; - while (nwritten < arr.length && !signal.aborted) { - nwritten += writer.writeSync(arr.subarray(nwritten)); + const maybePromise = writer.writeAll(result.value); + if (maybePromise) { + await maybePromise; } } } async function pipeReaderToWriterSync( reader: Reader, - writer: WriterSync, + writer: ShellPipeWriter, signal: AbortSignal | KillSignal, ) { const buffer = new Uint8Array(1024); @@ -953,13 +942,9 @@ async function pipeReaderToWriterSync( if (bytesRead == null || bytesRead === 0) { break; } - writeAllSync(buffer.slice(0, bytesRead)); - } - - function writeAllSync(arr: Uint8Array) { - let nwritten = 0; - while (nwritten < arr.length && !signal.aborted) { - nwritten += writer.writeSync(arr.subarray(nwritten)); + const maybePromise = writer.writeAll(buffer.slice(0, bytesRead)); + if (maybePromise) { + await maybePromise; } } } @@ -1058,13 +1043,11 @@ async function executePipeSequence(sequence: PipeSequence, context: Context): Pr break; } case "stdoutstderr": { - context.stderr.writeLine(`piping to both stdout and stderr is not implemented (ex. |&)`); - return { code: 1 }; + return context.error(`piping to both stdout and stderr is not implemented (ex. |&)`); } default: { const _assertNever: never = nextInner.op; - context.stderr.writeLine(`not implemented pipe sequence op: ${nextInner.op}`); - return { code: 1 }; + return context.error(`not implemented pipe sequence op: ${nextInner.op}`); } } nextInner = nextInner.next;