Skip to content

Commit 27a6583

Browse files
authored
Merge pull request #154 from konard/issue-153-39dda3815f4e
fix(shell): drain stdout/stderr to prevent command hanging on large output
2 parents 8494910 + 4ce98ef commit 27a6583

File tree

1 file changed

+18
-12
lines changed

1 file changed

+18
-12
lines changed

packages/lib/src/shell/command-runner.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,24 +47,28 @@ export const runCommandWithExitCodes = <E>(
4747
yield* _(ensureExitCode(numericExitCode, okExitCodes, onFailure))
4848
})
4949

50-
// CHANGE: run a command and return the exit code
51-
// WHY: enable status checks without throwing on non-zero exits
50+
// CHANGE: run a command and return the exit code, draining stdout/stderr to prevent buffer deadlock
51+
// WHY: piped stdout/stderr fill the OS buffer (~64 KB) causing the child process to hang indefinitely
5252
// QUOTE(ТЗ): "система авторизации"
5353
// REF: user-request-2026-01-28-auth
5454
// SOURCE: n/a
5555
// FORMAT THEOREM: forall cmd: exitCode(cmd) = n
5656
// PURITY: SHELL
5757
// EFFECT: Effect<number, PlatformError, CommandExecutor>
58-
// INVARIANT: stdout/stderr are suppressed for status checks
58+
// INVARIANT: stdout/stderr are drained asynchronously so the child process never blocks
5959
// COMPLEXITY: O(command)
6060
export const runCommandExitCode = (
6161
spec: RunCommandSpec
6262
): Effect.Effect<number, PlatformError, CommandExecutor.CommandExecutor> =>
63-
Effect.map(
64-
Command.exitCode(
65-
buildCommand(spec, "pipe", "pipe", "pipe")
66-
),
67-
Number
63+
Effect.scoped(
64+
Effect.gen(function*(_) {
65+
const executor = yield* _(CommandExecutor.CommandExecutor)
66+
const process = yield* _(executor.start(buildCommand(spec, "pipe", "pipe", "pipe")))
67+
yield* _(Effect.forkDaemon(Stream.runDrain(process.stdout)))
68+
yield* _(Effect.forkDaemon(Stream.runDrain(process.stderr)))
69+
const exitCode = yield* _(process.exitCode)
70+
return Number(exitCode)
71+
})
6872
)
6973

7074
const collectUint8Array = (chunks: Chunk.Chunk<Uint8Array>): Uint8Array =>
@@ -75,15 +79,15 @@ const collectUint8Array = (chunks: Chunk.Chunk<Uint8Array>): Uint8Array =>
7579
return next
7680
})
7781

78-
// CHANGE: run a command and capture stdout
79-
// WHY: allow auth flows to retrieve tokens from CLI tools
82+
// CHANGE: run a command and capture stdout, draining stderr to prevent buffer deadlock
83+
// WHY: if stderr fills the OS buffer (~64 KB) the child process hangs; drain it asynchronously
8084
// QUOTE(ТЗ): "система авторизации"
8185
// REF: user-request-2026-01-28-auth
8286
// SOURCE: n/a
8387
// FORMAT THEOREM: forall cmd: capture(cmd) -> stdout(cmd)
8488
// PURITY: SHELL
8589
// EFFECT: Effect<string, E | PlatformError, CommandExecutor>
86-
// INVARIANT: stderr is captured but ignored for output
90+
// INVARIANT: stderr is drained asynchronously; stdout is fully collected before returning
8791
// COMPLEXITY: O(command)
8892
export const runCommandCapture = <E>(
8993
spec: RunCommandSpec,
@@ -94,11 +98,13 @@ export const runCommandCapture = <E>(
9498
Effect.gen(function*(_) {
9599
const executor = yield* _(CommandExecutor.CommandExecutor)
96100
const process = yield* _(executor.start(buildCommand(spec, "pipe", "pipe", "pipe")))
101+
yield* _(Effect.forkDaemon(Stream.runDrain(process.stderr)))
97102
const bytes = yield* _(
98103
pipe(process.stdout, Stream.runCollect, Effect.map((chunks) => collectUint8Array(chunks)))
99104
)
100105
const exitCode = yield* _(process.exitCode)
101-
yield* _(ensureExitCode(Number(exitCode), okExitCodes, onFailure))
106+
const numericExitCode = Number(exitCode)
107+
yield* _(ensureExitCode(numericExitCode, okExitCodes, onFailure))
102108
return new TextDecoder("utf-8").decode(bytes)
103109
})
104110
)

0 commit comments

Comments
 (0)