Skip to content

Let system() and command-pipe children inherit the standard input - #577

Merged
bertysentry merged 5 commits into
mainfrom
575-spawned-processes-inherit-stdin
Aug 18, 2026
Merged

Let system() and command-pipe children inherit the standard input#577
bertysentry merged 5 commits into
mainfrom
575-spawned-processes-inherit-stdin

Conversation

@bertysentry

Copy link
Copy Markdown
Contributor

Fixes #575

What

The children of system(cmd) and of a command input pipe ("cmd" | getline) now inherit the JVM's standard input when Jawk reads the real standard input of the process — every CLI run — as POSIX requires and as gawk, mawk, and BWK awk behave:

$ echo hi | jawk 'BEGIN { "sort" | getline line; print "[" line "]" }'
[hi]

Terminal-aware commands work too: "stty size" | getline reaches the controlling terminal instead of failing with Inappropriate ioctl for device, which is what aborted patsie75/awk-demo at startup.

How

JRT.spawnProcess gains an inheritStandardInput flag mapping to ProcessBuilder.Redirect.INHERIT. The system() and command-input-pipe call sites pass standardInput == System.in; the output-pipe call site keeps the pipe as the child's standard input. Embedded executions bound to a custom Java input stream keep the closed-stdin behavior: a Java stream cannot be lent to another OS process, and handing over the host JVM's standard input would leak input the embedder never gave to Jawk.

Tests

SpawnedProcessStdinTest covers the three contracts: the pipe child and the system() child read Jawk's standard input across a real process boundary (CLI spawned in a fresh JVM with redirected stdin — the in-process builders cannot observe fd inheritance), and an embedded execution keeps the child's standard input closed. The scripts are passed via -f because Windows mangles embedded double quotes in inline command-line arguments.

mvn verify passes: all unit and compatibility tests, checkstyle, PMD, and SpotBugs clean. Documented in java-output.md and behavior-changes.md (Unreleased).

🤖 Generated with Claude Code

The children of system(cmd) and of a command input pipe
("cmd" | getline) now inherit the JVM's standard input when Jawk reads
the real standard input of the process (CLI runs), as POSIX requires
and as gawk, mawk, and BWK awk behave. Stdin filters and
terminal-aware commands such as "stty size" | getline now work.

Embedded executions bound to a custom Java input stream keep the
closed-stdin behavior: a Java stream cannot be lent to another OS
process, and handing over the host JVM's standard input would leak
input the embedder never gave to Jawk. Output pipes are unchanged: the
pipe itself remains the child's standard input.

Fixes #575

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1d317b1c8a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


@Test
public void commandInputPipeChildReadsJawkStandardInput() throws Exception {
String output = runCliJvmWithStdin(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Route CLI script tests through AwkTestSupport

Both new inheritance tests execute AWK scripts through the private runCliJvmWithStdin helper instead of AwkTestSupport, bypassing the repository's explicit requirement for every new script-running unit test. Add the required subprocess capability to AwkTestSupport and invoke it here so setup, assertions, and temporary-file handling remain centralized.

AGENTS.md reference: AGENTS.md:L23-L29

Useful? React with 👍 / 👎.

Comment thread src/main/java/io/jawk/jrt/JRT.java Outdated
* input
*/
private boolean spawnedProcessInheritsStandardInput() {
return standardInput == System.in;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Track native stdin instead of comparing System.in

When an embedding application calls System.setIn(customStream) and then runs Jawk with that System.in—including through the default Cli constructor or JSR-223 fallback—this identity check returns true even though Redirect.INHERIT connects the child to the process's native descriptor 0, not the replacement Java stream. The child can therefore block on or consume unrelated host input instead of receiving closed stdin as promised for custom Java streams; native-stdin eligibility needs to be carried explicitly from the real CLI entry path.

Useful? React with 👍 / 👎.

Comment on lines +109 to +113
try (InputStream out = process.getInputStream(); InputStream err = process.getErrorStream()) {
copy(out, stdout);
copy(err, stderr);
}
assertTrue("CLI JVM did not terminate", process.waitFor(30, TimeUnit.SECONDS));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply the timeout while draining the child process

If the spawned CLI hangs, or writes enough stderr to fill its pipe while stdout remains open, copy(out, stdout) blocks before waitFor(30, ...) is ever reached; the child can likewise block on the undrained stderr pipe. Thus the advertised timeout cannot prevent this test from hanging CI precisely when the subprocess behavior regresses. Drain both streams concurrently and apply the timeout before waiting for either stream to reach EOF, terminating the child on expiry.

Useful? React with 👍 / 👎.

- Capture the JVM's standard input when JRT is initialized and compare
  the bound input against that, so a stream installed later with
  System.setIn never qualifies for inheritance and the child's standard
  input fails closed instead of exposing the process's real
  descriptor 0.
- Move the fresh-JVM CLI runner into AwkTestSupport, where every
  script-running test helper lives.
- Redirect the child's standard output and error to files so no pipe
  can stall either side, enforce the timeout with waitFor alone, and
  kill the child when it expires.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bertysentry

Copy link
Copy Markdown
Contributor Author

@codex please review again

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eb3a2393b7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/main/java/io/jawk/jrt/JRT.java Outdated
* replacement never matches, so the child's standard input stays closed
* instead of silently exposing the host's real descriptor 0.
*/
private static final InputStream PROCESS_STANDARD_INPUT = System.in;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Track native stdin explicitly instead of capturing System.in

When an embedding application calls System.setIn(customStream) before the first active use of JRT, this initializer captures that replacement as PROCESS_STANDARD_INPUT; the subsequent identity check therefore permits Redirect.INHERIT, exposing the JVM's native descriptor 0 to system() or command-pipe children instead of keeping their stdin closed, potentially causing them to consume unrelated host input or block. The fresh evidence relative to the earlier finding is that the new class-initialization-time capture still occurs after an embedder may replace System.in; native-stdin eligibility needs to be passed explicitly from the real process CLI entry path.

Useful? React with 👍 / 👎.

No capture of System.in can establish that Jawk reads the standard
input of the JVM process: an embedder may replace the stream with
System.setIn at any point, including before JRT initializes. The
eligibility is now carried explicitly from Cli.main, the one caller
that can vouch for it, through a JRT setter. Every other path —
embedded API runs, tests, JSR-223 — leaves the flag unset, so the
children of system() and of command pipes keep a closed standard input
there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bertysentry

Copy link
Copy Markdown
Contributor Author

@codex please review again

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2dd51dc371

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// this CLI reads is the standard input of the JVM itself, the one
// stream a spawned child can inherit; embedders and tests reach
// run() through other paths and never set this.
cli.processStandardInput = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not assume every main invocation uses native stdin

When an embedder or test calls the public Cli.main after System.setIn(customStream), this unconditional flag makes system() and command-pipe children inherit the process's native descriptor 0 even though Jawk itself reads the replacement stream, so a child can block on or consume unrelated host input. The repository already invokes Cli.main programmatically in AwkTest.java, so main is not exclusively an OS launch path. The fresh evidence relative to the earlier finding is this new unconditional assignment; eligibility must only be enabled when the CLI input is actually backed by native stdin.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that no capture or flag can survive a programmatic Cli.main call after System.setIn — and there is no supported Java-side check that can tell the launcher-installed System.in from a replacement (the fd0 backing of a stream is not observable without deep reflection into java.io). So the boundary is now drawn as a documented contract instead: Cli.main carries process semantics — it is what the jawk launch of the JVM runs, and its children may inherit the process's standard input, as POSIX requires of awk. Code that replaces System.in must go through create(), the constructors, or the Awk API, where spawned children always get a closed standard input. The one programmatic Cli.main call in the test suite (AwkTest.compileTuplesViaCLI) now goes through create() accordingly, so nothing in the repository calls main programmatically anymore.

There is no Java-side check that can distinguish the
launcher-installed System.in from a replacement installed with
System.setIn, so the standard-input inheritance declared by Cli.main
is documented as part of its contract: main carries process semantics,
and code that replaces System.in must use create(), the constructors,
or the Awk API, where spawned children always get a closed standard
input. The one programmatic Cli.main call in the test suite now goes
through create() accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bertysentry

Copy link
Copy Markdown
Contributor Author

@codex please review again

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

Reviewed commit: 786cd0cf93

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@bertysentry

Copy link
Copy Markdown
Contributor Author

@copilot resolve the merge conflicts in this pull request

Co-authored-by: bertysentry <32521698+bertysentry@users.noreply.github.com>

Copilot AI commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved the merge conflict and pushed merge commit e1b326d.

@bertysentry
bertysentry merged commit adb9cbb into main Aug 18, 2026
@bertysentry
bertysentry deleted the 575-spawned-processes-inherit-stdin branch August 18, 2026 17:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Processes spawned by system() and command pipes cannot read Jawk's standard input

2 participants