Series MapLesson 29 / 32
Final StretchOrdered learning track

Learn Java Io Modern Io Resource Boundaries Part 029 Process Io

15 min read2810 words
PrevNext
Lesson 2932 lesson track2832 Final Stretch

title: Learn Java IO, Modern IO, Streams, Buffers, Resources, Serialization & Data Boundaries - Part 029 description: Process IO in Java using ProcessBuilder, stdin/stdout/stderr handling, pipe deadlock avoidance, redirects, bounded capture, timeouts, cancellation, encoding, and production process-boundary design. series: learn-java-io-modern-io-resource-boundaries seriesTitle: Learn Java IO, Modern IO, Streams, Buffers, Resources, Serialization & Data Boundaries order: 29 partTitle: Process IO: stdin, stdout, stderr, and Deadlock Avoidance tags:

  • java
  • io
  • processbuilder
  • process
  • stdin
  • stdout
  • stderr
  • pipes
  • deadlock
  • resource-boundaries
  • series date: 2026-06-30

Part 029 — Process IO: stdin, stdout, stderr, and Deadlock Avoidance

Starting a process is easy. Owning its IO boundary correctly is the hard part.

Java gives you ProcessBuilder and Process to create and control native processes. That sounds simple until the child process writes enough data to fill a pipe, waits for the parent to read it, while the parent waits for the child to exit. That is the classic process IO deadlock.

This part treats process execution as a bidirectional IO boundary:

parent JVM  <---- stdin/stdout/stderr pipes ---->  child process

The mental model is not “run command and get string”. The correct model is:

A child process is a concurrent producer/consumer with finite OS pipe buffers, independent lifecycle, its own working directory, environment, encoding, exit status, and failure modes.

1. Learning Objectives

After this part, you should be able to:

  1. Explain why process IO deadlocks happen.
  2. Use ProcessBuilder without accidentally invoking a shell.
  3. Decide between inheritIO, redirect-to-file, concurrent drain, and bounded capture.
  4. Correctly handle stdin, stdout, and stderr lifecycles.
  5. Apply timeout, cancellation, and process-tree cleanup policies.
  6. Decode process output with explicit charset rules.
  7. Build a reusable process runner that prevents unbounded memory capture.
  8. Understand the difference between exit code failure and IO/protocol failure.
  9. Test process-boundary behavior without relying on fragile shell scripts.
  10. Review production process execution code for deadlock, leak, and data-loss risks.

2. Kaufman Skill Slice

The skill for this part is:

Given an external command, execute it from Java while preserving bounded memory, bounded time, deterministic IO ownership, and diagnosable failure behavior.

Break it down:

Sub-skillProduction question
Command constructionAre we executing an argument vector or a shell expression?
EnvironmentWhich variables are inherited, added, removed, or normalized?
Working directoryIs the child launched from a controlled directory?
Stdin policyDoes the child need input, EOF, a file, or no input?
Stdout policyIs output inherited, redirected, streamed, captured, parsed, or discarded?
Stderr policyIs stderr separate, merged, redirected, or captured with limit?
BackpressureCan the child block because the parent is not draining pipes?
TimeoutWhat happens if the child hangs?
CancellationDo we destroy the child only, or the process tree?
EncodingHow are bytes from stdout/stderr decoded?
Result modelWhat is returned: exit code, output, metrics, files, or parsed records?
CleanupAre streams, executors, temp files, and processes closed?

3. Process IO Mental Model

A process has at least three standard streams:

StreamFrom Java parent perspectiveCommon Java method
Child stdinParent writes to childprocess.getOutputStream()
Child stdoutParent reads from childprocess.getInputStream()
Child stderrParent reads diagnostics from childprocess.getErrorStream()

The names are easy to confuse:

process.getOutputStream()

means:

an OutputStream from the parent JVM into the child's standard input

Similarly:

process.getInputStream()

means:

an InputStream for the parent JVM to read from the child's standard output

Diagram:

Important invariant:

The child process, the parent thread, and the OS pipe buffers are concurrent participants. If any participant waits while holding a full/empty pipe expectation, the system can deadlock.

4. The Classic Deadlock

This is wrong:

Process process = new ProcessBuilder("some-command").start();
int exitCode = process.waitFor();
String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
String error = new String(process.getErrorStream().readAllBytes(), StandardCharsets.UTF_8);

Why?

If the child writes enough stdout or stderr to fill the pipe buffer, it blocks. The parent is waiting for exit. The child cannot exit because it is blocked writing. The parent cannot start reading because it is waiting.

State diagram:

The fix is not “increase buffer size”. The fix is to choose a stream policy before starting the process.

5. ProcessBuilder Core Model

ProcessBuilder is a mutable builder for process attributes:

ProcessBuilder pb = new ProcessBuilder("git", "status", "--porcelain=v1");
pb.directory(Path.of("/repo/worktree").toFile());
pb.environment().put("LC_ALL", "C");
Process process = pb.start();

Key attributes:

AttributeMeaning
commandArgument vector used to create the process
environmentEnvironment variables for the child
directoryWorking directory for the child
redirectsWhere stdin/stdout/stderr are connected
error stream mergeWhether stderr is merged into stdout

Production rule:

Build commands as an argument vector, not as one shell string, unless shell semantics are explicitly required.

Prefer:

new ProcessBuilder("git", "log", "--oneline", "--", fileName)

Avoid:

new ProcessBuilder("sh", "-c", "git log --oneline -- " + fileName)

The second form introduces shell parsing, quoting, expansion, and injection risk. Sometimes shell is necessary, but it should be a conscious boundary decision.

6. Four Safe Output Strategies

There are four common ways to handle stdout/stderr safely.

6.1 Inherit IO

Use when the child is an interactive or diagnostic command and you want output to go directly to the parent process console:

Process process = new ProcessBuilder("java", "--version")
    .inheritIO()
    .start();

int exit = process.waitFor();

Pros:

  • Simple.
  • No pipe draining code.
  • Good for CLI tools.

Cons:

  • Output is not captured.
  • Hard to test.
  • Not suitable for server-side request handling.

6.2 Redirect to File

Use when output may be large and should not be stored in memory:

Path out = tempDir.resolve("tool.out");
Path err = tempDir.resolve("tool.err");

Process process = new ProcessBuilder("some-tool", "--large-output")
    .redirectOutput(out.toFile())
    .redirectError(err.toFile())
    .start();

int exit = process.waitFor();

Pros:

  • Handles large output.
  • Avoids unbounded heap usage.
  • Preserves diagnostic artifacts.

Cons:

  • Requires file lifecycle management.
  • Caller must inspect or stream files later.

6.3 Merge stderr into stdout

Use when ordering between stdout and stderr matters more than separation:

Process process = new ProcessBuilder("some-tool")
    .redirectErrorStream(true)
    .start();

byte[] combined = process.getInputStream().readAllBytes();
int exit = process.waitFor();

Careful: after redirectErrorStream(true), stderr is merged into stdout. The error stream is no longer independently meaningful.

Pros:

  • One stream to drain.
  • Preserves approximate interleaving.

Cons:

  • Loses stream distinction.
  • Parsing output becomes harder.

6.4 Concurrent Drain with Bounded Capture

Use when you need captured output but must avoid deadlock and memory blowup.

ExecutorService executor = Executors.newFixedThreadPool(2);

try {
    Process process = new ProcessBuilder("some-tool").start();

    Future<byte[]> stdout = executor.submit(() -> readLimited(process.getInputStream(), 1_000_000));
    Future<byte[]> stderr = executor.submit(() -> readLimited(process.getErrorStream(), 1_000_000));

    boolean finished = process.waitFor(30, TimeUnit.SECONDS);
    if (!finished) {
        process.destroyForcibly();
        throw new TimeoutException("process timed out");
    }

    int exit = process.exitValue();
    byte[] out = stdout.get(5, TimeUnit.SECONDS);
    byte[] err = stderr.get(5, TimeUnit.SECONDS);
} finally {
    executor.shutdownNow();
}

The important ideas:

  1. Drain stdout and stderr while the process is running.
  2. Bound the captured bytes.
  3. Apply timeout.
  4. Handle process cleanup.
  5. Handle gobbler failures separately from process exit code.

7. Bounded Capture Utility

Never expose an unbounded readAllBytes() on process output in production unless the command and maximum output are strictly controlled.

A simple bounded reader:

static byte[] readLimited(InputStream in, int maxBytes) throws IOException {
    ByteArrayOutputStream out = new ByteArrayOutputStream(Math.min(maxBytes, 8192));
    byte[] buffer = new byte[8192];
    int total = 0;

    while (true) {
        int n = in.read(buffer);
        if (n == -1) {
            return out.toByteArray();
        }
        if (n == 0) {
            continue;
        }
        if (total + n > maxBytes) {
            int allowed = maxBytes - total;
            if (allowed > 0) {
                out.write(buffer, 0, allowed);
            }
            throw new IOException("process output exceeded limit: " + maxBytes + " bytes");
        }
        out.write(buffer, 0, n);
        total += n;
    }
}

In more advanced systems, use a result that records truncation instead of throwing:

record CapturedBytes(byte[] bytes, boolean truncated, long observedBytes) {}

A truncating capture is often better for diagnostics:

capture first 64 KiB stdout
capture first 64 KiB stderr
stream full output to file if needed

8. Stdin Policy

A child process that reads stdin may block forever if the parent never writes or never closes stdin.

Wrong:

Process process = new ProcessBuilder("some-filter").start();
process.getOutputStream().write(inputBytes);
int exit = process.waitFor(); // child may wait for EOF

Better:

Process process = new ProcessBuilder("some-filter").start();

try (OutputStream stdin = process.getOutputStream()) {
    stdin.write(inputBytes);
} // close sends EOF to child stdin

int exit = process.waitFor();

But still not enough if stdout/stderr are not drained concurrently.

Production stdin modes:

ModeUsage
closed stdinChild should not read input
inherited stdinInteractive command
redirected from fileLarge input
streamed by parentDynamic input
bounded generated inputControlled generated payload

Closed stdin pattern:

Process process = new ProcessBuilder("tool").start();
process.getOutputStream().close();

If the child expects no input, closing stdin early is often clearer than leaving it open.

9. Timeout and Cancellation

A process can hang for many reasons:

  • waiting for stdin
  • blocked writing stdout/stderr
  • waiting on network
  • waiting on lock
  • CPU-bound loop
  • child process spawned grandchildren
  • external binary prompt waiting for user input

Use timeout:

boolean finished = process.waitFor(timeout.toMillis(), TimeUnit.MILLISECONDS);
if (!finished) {
    process.destroy();
    if (!process.waitFor(2, TimeUnit.SECONDS)) {
        process.destroyForcibly();
    }
    throw new TimeoutException("process timed out after " + timeout);
}

This kills the direct child process. It may not kill grandchildren on every platform.

For process trees, Java exposes ProcessHandle:

static void destroyTree(Process process) {
    ProcessHandle handle = process.toHandle();

    handle.descendants()
        .sorted(Comparator.comparingLong(ProcessHandle::pid).reversed())
        .forEach(ph -> ph.destroy());

    handle.destroy();
}

Then escalate:

static void destroyTreeForcibly(Process process) {
    ProcessHandle handle = process.toHandle();

    handle.descendants()
        .forEach(ProcessHandle::destroyForcibly);

    handle.destroyForcibly();
}

Caveat:

Process-tree semantics are platform-dependent. Treat cancellation as best-effort unless you control the child process design.

10. Encoding Boundary

Process output is bytes, not strings.

Bad:

String output = new String(bytes);

Better:

String output = new String(bytes, StandardCharsets.UTF_8);

But even UTF-8 may be wrong if the child uses platform encoding or locale-specific output.

Production strategy:

  1. Configure the child to emit stable machine-readable UTF-8 output when possible.
  2. Set locale/environment variables where appropriate.
  3. Prefer machine-readable flags such as JSON, NUL-delimited output, or stable porcelain modes.
  4. Decode with explicit charset.
  5. Treat decoding failures as protocol failures.

Example:

ProcessBuilder pb = new ProcessBuilder("git", "status", "--porcelain=v1", "-z");
pb.environment().put("LC_ALL", "C");

The -z option in tools that support it is valuable because filenames may contain newlines. This is not Java-specific; it is a boundary design principle:

Prefer unambiguous machine protocols over human-readable text when a process output is consumed by code.

11. Exit Code Is Not the Whole Result

A process execution result has multiple channels of information:

record ProcessResult(
    int exitCode,
    byte[] stdout,
    byte[] stderr,
    Duration duration,
    boolean timedOut,
    boolean stdoutTruncated,
    boolean stderrTruncated
) {}

Exit code alone cannot tell you:

  • whether stdout capture was truncated
  • whether stderr capture failed
  • whether decoding failed
  • whether timeout killed the process
  • whether output parsing failed
  • whether the process wrote a valid partial artifact

Process boundary states:

A robust process runner should not collapse all non-happy-path states into “exit code != 0”.

12. Redirects

ProcessBuilder.Redirect lets you connect standard streams to files.

Examples:

ProcessBuilder pb = new ProcessBuilder("tool");
pb.redirectInput(inputFile.toFile());
pb.redirectOutput(outputFile.toFile());
pb.redirectError(errorFile.toFile());

Append mode:

pb.redirectOutput(ProcessBuilder.Redirect.appendTo(logFile.toFile()));

Discard output:

pb.redirectOutput(ProcessBuilder.Redirect.DISCARD);
pb.redirectError(ProcessBuilder.Redirect.DISCARD);

Use discard only when diagnostics are truly irrelevant. In production, silently discarding stderr often makes incidents harder to debug.

Better compromise:

large output -> file
small diagnostic summary -> bounded capture

13. Merging stderr

redirectErrorStream(true) merges stderr into stdout.

ProcessBuilder pb = new ProcessBuilder("tool");
pb.redirectErrorStream(true);
Process process = pb.start();
byte[] combined = process.getInputStream().readAllBytes();

Use it when:

  • you need approximate chronological output
  • the tool writes progress to stderr but data to stdout is not machine-consumed
  • separation does not matter

Avoid it when:

  • stdout is a machine-readable protocol
  • stderr is diagnostic only
  • you must distinguish data from error messages

A common production bug:

pb.redirectErrorStream(true);
parseJson(process.getInputStream());

If the child writes warnings to stderr, merged output is no longer valid JSON.

14. Process Pipelines

Java supports starting pipelines with ProcessBuilder.startPipeline.

Conceptual example:

List<ProcessBuilder> pipeline = List.of(
    new ProcessBuilder("cat", "input.txt"),
    new ProcessBuilder("grep", "ERROR"),
    new ProcessBuilder("sort")
);

List<Process> processes = ProcessBuilder.startPipeline(pipeline);

This creates OS-level pipe connections between processes.

Important:

  1. You still need to consume the final stdout.
  2. You still need to consume stderr for each process unless redirected/merged.
  3. You need to check each exit code, not just the last process.
  4. Cancellation should consider the whole pipeline.

Pipeline failure model:

Pipeline result should preserve per-process status:

record PipelineResult(List<Integer> exitCodes, byte[] stdout, List<byte[]> stderrs) {}

15. Interactive Process Trap

Some commands are interactive by default:

  • password prompts
  • confirmation prompts
  • pagers
  • editors
  • shell commands waiting for stdin
  • tools that change behavior if stdout is a terminal

In server-side Java, avoid interactive commands. Disable prompts explicitly:

--yes
--batch
--non-interactive
--no-pager
--quiet

Also remember:

A pipe is not a terminal. Some programs behave differently when stdout/stderr are not attached to a TTY.

Java ProcessBuilder does not provide a full pseudo-terminal abstraction. If you need TTY behavior, you need a dedicated library or platform-specific integration.

16. Environment Boundary

Environment variables are part of the process contract.

ProcessBuilder pb = new ProcessBuilder("tool");
Map<String, String> env = pb.environment();
env.put("LC_ALL", "C");
env.put("NO_COLOR", "1");
env.remove("AWS_PROFILE");

Guidelines:

ConcernPractice
Locale-sensitive outputSet stable locale variables where applicable
Color codesDisable color for machine parsing
CredentialsDo not accidentally inherit secrets if child does not need them
ProxiesDecide whether proxy env vars should be inherited
PATH lookupPrefer absolute executable path for critical commands
Case sensitivityEnvironment behavior differs across platforms

A deterministic process runner should allow an explicit environment policy:

enum EnvironmentPolicy {
    INHERIT_ALL,
    INHERIT_SELECTED,
    EMPTY_WITH_ALLOWLIST
}

17. Working Directory Boundary

Working directory affects:

  • relative file paths
  • config discovery
  • tool behavior
  • generated artifact location
  • security boundaries
  • reproducibility

Set it explicitly:

ProcessBuilder pb = new ProcessBuilder("git", "status", "--porcelain=v1");
pb.directory(repoDir.toFile());

Do not rely on the JVM process working directory in production.

If a child writes files, prefer:

work root / execution id / staging files

Example:

/var/app/work/process-runs/2026-06-30T10-15-30Z-8f3a/
  input/
  output/
  logs/
  tmp/

This makes cleanup, diagnostics, and retry behavior easier.

18. Cross-Platform Concerns

Process execution is OS-sensitive.

ConcernLinux/macOSWindows
executable lookupPATH, executable bitPATHEXT, .exe, .bat, .cmd
shell/bin/sh, bash, zshcmd.exe, PowerShell
path separator/\ and / in some APIs
line endings\noften \r\n
signalsPOSIX signalsdifferent process control model
environment caseusually case-sensitiveoften case-insensitive semantics

Avoid assuming shell syntax is portable:

rm -rf path

is not portable Java process logic.

Prefer Java APIs for filesystem work. Use external processes only when they provide capability you cannot reasonably implement in Java.

19. Production ProcessRunner Design

A useful process runner should make unsafe choices hard.

record CommandSpec(
    List<String> command,
    Path workingDirectory,
    Map<String, String> environmentOverrides,
    StdinSpec stdin,
    OutputSpec stdout,
    OutputSpec stderr,
    Duration timeout
) {}

sealed interface StdinSpec permits NoStdin, BytesStdin, FileStdin {}
record NoStdin() implements StdinSpec {}
record BytesStdin(byte[] bytes) implements StdinSpec {}
record FileStdin(Path path) implements StdinSpec {}

sealed interface OutputSpec permits InheritOutput, FileOutput, CaptureOutput, DiscardOutput {}
record InheritOutput() implements OutputSpec {}
record FileOutput(Path path, boolean append) implements OutputSpec {}
record CaptureOutput(int maxBytes) implements OutputSpec {}
record DiscardOutput() implements OutputSpec {}

This makes policies explicit:

CommandSpec spec = new CommandSpec(
    List.of("git", "status", "--porcelain=v1", "-z"),
    repo,
    Map.of("LC_ALL", "C"),
    new NoStdin(),
    new CaptureOutput(256_000),
    new CaptureOutput(64_000),
    Duration.ofSeconds(10)
);

Better than:

run("git status");

20. Minimal Safe Runner Skeleton

This is intentionally incomplete but shows the structure:

public final class ProcessRunner {
    private final ExecutorService ioExecutor;

    public ProcessRunner(ExecutorService ioExecutor) {
        this.ioExecutor = Objects.requireNonNull(ioExecutor);
    }

    public ProcessResult run(List<String> command, Path cwd, Duration timeout) throws Exception {
        ProcessBuilder pb = new ProcessBuilder(command);
        pb.directory(cwd.toFile());

        Process process = pb.start();
        Instant started = Instant.now();

        Future<CapturedBytes> stdout = ioExecutor.submit(() -> capture(process.getInputStream(), 256_000));
        Future<CapturedBytes> stderr = ioExecutor.submit(() -> capture(process.getErrorStream(), 64_000));

        try (OutputStream stdin = process.getOutputStream()) {
            // Close stdin to signal EOF because this runner does not provide input.
        }

        boolean finished = process.waitFor(timeout.toMillis(), TimeUnit.MILLISECONDS);
        if (!finished) {
            process.destroy();
            if (!process.waitFor(2, TimeUnit.SECONDS)) {
                process.destroyForcibly();
            }
            return ProcessResult.timedOut(command, Duration.between(started, Instant.now()));
        }

        int exitCode = process.exitValue();
        CapturedBytes out = stdout.get(5, TimeUnit.SECONDS);
        CapturedBytes err = stderr.get(5, TimeUnit.SECONDS);

        return ProcessResult.completed(
            command,
            exitCode,
            out,
            err,
            Duration.between(started, Instant.now())
        );
    }

    private static CapturedBytes capture(InputStream in, int maxBytes) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream(Math.min(maxBytes, 8192));
        byte[] buffer = new byte[8192];
        long observed = 0;
        boolean truncated = false;

        while (true) {
            int n = in.read(buffer);
            if (n == -1) {
                return new CapturedBytes(out.toByteArray(), truncated, observed);
            }

            observed += n;
            int remaining = maxBytes - out.size();
            if (remaining > 0) {
                out.write(buffer, 0, Math.min(n, remaining));
            } else {
                truncated = true;
            }

            if (observed > maxBytes) {
                truncated = true;
            }
        }
    }
}

A production implementation should also handle:

  • redirects
  • stdin bytes/files
  • process tree cancellation
  • executor saturation
  • startup failure
  • stdout/stderr drain failure
  • structured logging
  • result redaction
  • temporary working directory lifecycle

21. Startup Failure vs Runtime Failure

pb.start() can fail before a child exists:

try {
    Process process = pb.start();
} catch (IOException e) {
    // Executable not found, permission issue, invalid working directory, etc.
}

This is different from:

exitCode != 0

Failure taxonomy:

FailureExampleResult category
Startup failureexecutable not foundno child process
IO setup failureredirect file not writableno useful child result
Runtime non-zero exitcommand failedchild result with exit code
Timeoutchild did not finishcancellation result
Drain failurestdout reader failedboundary failure
Parse failureinvalid JSON outputprotocol failure

Good APIs preserve these distinctions.

22. Logging and Redaction

Command logging is useful but dangerous.

Avoid logging secrets:

curl -H Authorization: Bearer ...

Instead, model redaction explicitly:

record CommandArgument(String value, boolean sensitive) {}

Or keep command and display command separate:

record CommandSpec(List<String> argv, List<String> displayArgv) {}

Log:

  • executable name
  • working directory
  • timeout
  • exit code
  • duration
  • output size/truncation
  • diagnostic file path

Be careful with:

  • full stdout/stderr
  • environment variables
  • command arguments containing tokens
  • generated temporary paths that reveal sensitive tenant/case data

23. Process Boundary Patterns

23.1 Tool adapter pattern

Wrap each external tool behind a typed interface:

interface PdfTextExtractor {
    ExtractedText extract(Path pdf) throws IOException, InterruptedException;
}

Internally it may call pdftotext, but the rest of the application sees a typed contract.

Benefits:

  • isolates process IO complexity
  • centralizes timeout
  • centralizes version detection
  • simplifies testing
  • prevents command construction spread across codebase

23.2 Staged output pattern

External tools often write output files. Use staging:

run-dir/
  input.pdf
  output.tmp
  stderr.log
  manifest.json

Only move output.tmp to committed location after:

  1. process exit code success
  2. output file exists
  3. output file validates
  4. size/format constraints pass
  5. optional checksum computed

23.3 Version probe pattern

On startup or first use:

tool --version

Validate:

  • executable exists
  • version is supported
  • output format is recognized
  • runtime permission is available

Do not discover this inside the first user request.

23.4 Quarantine failure pattern

For regulated or audit-heavy systems, keep failed run artifacts:

failed-runs/<execution-id>/
  command.json
  stdout.head
  stderr.head
  input.manifest
  generated-files/

This supports incident diagnosis without re-running non-deterministic tools.

24. Process IO Anti-Patterns

Anti-patternWhy it fails
waitFor() before draining streamsDeadlock when pipe fills
Unbounded readAllBytes()Heap exhaustion from large output
Merging stderr into machine stdoutCorrupts protocol parsing
Passing one shell stringQuoting and injection problems
Leaving stdin openChild waits for EOF
Ignoring stderrLost diagnostics
No timeoutRequest/thread/resource leak
Killing only child when it spawns childrenOrphan processes
Relying on default charsetPlatform-dependent decoding
Relying on parent cwdDeployment-dependent behavior
Logging full command/envSecret leakage
Treating non-zero exit as only failureMisses timeout, IO, and parse failures

25. Deliberate Practice

Exercise 1 — Deadlock reproduction

Write a child program that prints more data than a pipe buffer to stderr and then exits.

Parent program:

Process process = new ProcessBuilder("java", "SpamStderr").start();
process.waitFor();

Observe that it may hang.

Then fix it by draining stderr concurrently.

Exercise 2 — Bounded capture

Build a process runner that captures only the first 64 KiB of stdout and stderr, while tracking the total observed bytes.

Acceptance criteria:

  • no unbounded readAllBytes()
  • no process deadlock
  • result marks truncation
  • timeout enforced
  • stdin closed when unused

Exercise 3 — Protocol-safe parsing

Run a tool that emits machine-readable stdout and diagnostic stderr.

Acceptance criteria:

  • stdout parsed independently
  • stderr captured separately
  • merged stderr test fails intentionally
  • decoding charset is explicit

Exercise 4 — Staged artifact generation

Execute an external process that writes an output file.

Acceptance criteria:

  • output written to staging directory
  • exit code checked
  • output validated
  • temp output atomically moved to committed location
  • failed runs are quarantined

26. Review Checklist

Use this checklist for every process execution code review:

[ ] Command is argument-vector based, not shell-string based, unless shell is explicit.
[ ] Working directory is explicit.
[ ] Environment policy is explicit.
[ ] Stdin policy is explicit: closed, inherited, file, or streamed.
[ ] Stdout policy is explicit: inherited, redirected, captured, streamed, or discarded.
[ ] Stderr policy is explicit and does not corrupt machine-readable stdout.
[ ] Stdout and stderr cannot deadlock the child.
[ ] Captured output is bounded or redirected to file.
[ ] Timeout is enforced.
[ ] Cancellation policy is defined.
[ ] Process tree behavior is considered.
[ ] Charset is explicit for text output.
[ ] Exit code, timeout, IO failure, and parse failure are distinct.
[ ] Temporary files/directories are cleaned up or quarantined intentionally.
[ ] Sensitive command arguments and environment variables are redacted.

27. Mental Model Summary

Process IO is not a utility call. It is a concurrent boundary.

The safe default is:

argument vector
+ explicit cwd
+ explicit environment policy
+ explicit stdin policy
+ concurrent drain or redirect stdout/stderr
+ bounded capture
+ timeout
+ cleanup
+ typed result

If you remember only one invariant:

Never wait for a child process while ignoring the pipes it may need you to drain.

References

Lesson Recap

You just completed lesson 29 in final stretch. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.