Skip to content

Command stdin and interactive shell: stdin() builders, RemoteProcess.stdin(), and a CLI shell subcommand - #144

Merged
bertysentry merged 17 commits into
mainfrom
136-command-stdin-interactive-shell
Jul 29, 2026
Merged

Command stdin and interactive shell: stdin() builders, RemoteProcess.stdin(), and a CLI shell subcommand#144
bertysentry merged 17 commits into
mainfrom
136-command-stdin-interactive-shell

Conversation

@bertysentry

Copy link
Copy Markdown
Contributor

Closes #136 — phase 3 of the API modernization, built directly on the bounded-poll primitive of #111 (PR #134).

Phase 1 — stdin for commands (library)

  • New WSMan Send envelope (rsp:Send, base64 stdin stream, End="true" on the final chunk), the ctrl_c Signal code next to terminate, and a send(byte[], end) / interrupt() seam on WsmanClient.RemoteCommand and CommandCursor. Input larger than one envelope is split automatically (96 KiB raw per Send, under the advertised MaxEnvelopeSize after base64).
  • stdin(String | Path | InputStream) on CommandRequest: pre-supplied input, delivered in full right after startup and ending with the End mark so the remote stdin reaches EOF. Supplying input switches the command to pipe semantics (WINRS_CONSOLEMODE_STDIN=FALSE) — filters like sort consume it and terminate, exactly like a local < file redirection; without it the historical console semantics (TRUE) are kept. Works with both execute() and start().
  • RemoteProcess.stdin() completes the java.lang.Process shape: written text buffers locally, flush() emits one WSMan Send, close() sends the final chunk with End="true". Writes and reads alternate on the caller's thread — same serial connection, no concurrency — and the classic pipe deadlock is documented as the caller's to avoid, like Process.
  • RemoteProcess.interrupt(): the ctrl_c Signal — interrupts the remote child like a console Ctrl+C without ending the command or the session.
  • RemoteProcess.poll(Duration): one bounded protocol round trip (the building block of the shell pump below); returns as soon as the server answers, expiry is not a failure.
  • Cleanup discipline from Streaming APIs (phase 2): stream()/start() terminals on the fluent WinRMClient #134 carries over: closing stdin after completion or after an early close is a silent local no-op — no stray requests, no revived transport, and a known exit code is never hidden behind a cleanup failure (input still buffered at that point is discarded, like a java.lang.Process pipe's).

Phase 2 — interactive shell subcommand (CLI)

java -jar winrm-java-standalone.jar -h server -u 'DOMAIN\user' -pf pw.txt shell
  • Starts cmd.exe remotely (console-mode stdin, like winrs) and bridges it to the local terminal until it exits. The pump is single-threaded on the wire: drain queued local input → Send → bounded poll → forward decoded stdout/stderr; the only helper thread reads local stdin into a queue. An idle session never trips the inactivity timeout — every poll completes with output or the protocol's "nothing yet" fault.
  • Local EOF (Ctrl+Z / Ctrl+D) → Send with End="true"; Ctrl+C is rerouted (reflective sun.misc.Signal, graceful fallback when absent) to the WSMan ctrl_c Signal, interrupting the remote child without killing the session; the remote exit code follows the existing CLI exit-code contract.
  • command bonus: piped/redirected local stdin is forwarded as the remote command's stdin — winrm-java ... command sort < data.txt just works (console detection is injected, so tests stay deterministic).

Bounded-poll fix (latent #134 bug exposed by the pump)

The WSMan service clamps OperationTimeout below 500 ms up to 500 ms (measured on a real Windows Server 2008 R2 host): a wire poll with a 200–500 ms budget always lost the race between the server's "nothing yet" fault (answered at the floor, not at the requested time) and its own socket cut, killing the connection. FakeWsmanServer answers instantly, so no fake-server test could catch this. Wire polls now require a 750 ms budget (shorter waits are waited out locally, protocol untouched) and never ask the server for less than the 500 ms floor; the shell poll cadence is 1 s — it paces idle rounds only, output still echoes back with sub-second latency because a poll returns as soon as output exists.

Tests & docs

  • FakeWsmanServer learns Send: stdin chunks are decrypted, decoded, and exposed with their End flag (stdinChunks()); FakeWsmanResponses.sendResponse() added.
  • New CommandStdinTest (9 tests): content, chunking (both the 64 KiB feed buffers and the 96 KiB envelope split), End flag, console-mode option on the wire, a scripted write → flush → read REPL round trip, early close with stdin open, stdin after completion, ctrl_c on the wire.
  • New InteractiveShellTest (4 tests): scripted pump rounds — input line → Send, scripted output → local stdout, output-before-input ordering, EOF → End="true", Ctrl+C → ctrl_c Signal with the session surviving, exit-code propagation.
  • Verified live against a real Windows Server 2008 R2 host: printf 'pear\napple\n' | ... command sort returns the sorted lines, and a full shell session (banner, ver, echo %USERNAME%, exit 42) propagates exit code 42.
  • Docs: cli.md (shell manual section, stdin forwarding, timeout semantics for shell), commands.md (stdin API, pipe-vs-console semantics, deadlock caveat), README one-liners.

mvn verify site green: 164 tests + IT, checkstyle / PMD / SpotBugs all at zero.

🤖 Generated with Claude Code

…stdin(), CLI shell subcommand (#136)

Phase 1 - stdin for commands (library):
- New WSMan Send envelope (rsp:Send, base64 stdin stream, End="true" on the
  final chunk) and a send(byte[], end) / interrupt() seam on RemoteCommand
  and CommandCursor; input larger than one envelope is chunked automatically.
- stdin(String|Path|InputStream) on CommandRequest: pre-supplied input,
  delivered in full right after startup and ending with the End mark; it
  switches the remote stdin to pipe semantics (WINRS_CONSOLEMODE_STDIN=FALSE)
  so filters like sort see EOF. Works with execute() and start().
- RemoteProcess.stdin() completes the java.lang.Process shape: flush() emits
  one WSMan Send, close() marks the end of input. RemoteProcess.interrupt()
  sends the new ctrl_c Signal (a console Ctrl+C that leaves the session
  alive), and RemoteProcess.poll(Duration) exposes one bounded round trip.
- Cleanup discipline from #134 carries over: closing stdin after completion
  or an early close is a silent local no-op - no stray requests, no revived
  transport, no exit code hidden behind cleanup failures.

Phase 2 - interactive shell subcommand (CLI):
- winrm-java ... shell starts cmd.exe remotely and bridges it to the local
  terminal until it exits: a single-threaded pump (drain queued local input
  -> Send -> bounded poll -> forward output) with one helper thread reading
  local stdin, line-oriented like winrs. Local EOF sends End="true"; Ctrl+C
  is rerouted (sun.misc.Signal, reflective with graceful fallback) to the
  WSMan ctrl_c Signal; the remote exit code uses the CLI exit-code contract.
- command forwards piped/redirected local stdin as the remote command's
  stdin: winrm-java ... command sort < data.txt just works.

Bounded-poll fix exposed by the pump (latent from #134): the WSMan service
clamps OperationTimeout below 500 ms up to 500 ms (measured on Windows
Server 2008 R2), so a wire poll shorter than the floor plus transit slack
always lost the race between the "nothing yet" fault and its own socket cut.
Wire polls now require a 750 ms budget (shorter waits are waited out
locally) and never ask the server for less than the 500 ms floor.

FakeWsmanServer learns Send (decodes and records stdin chunks with their End
flag); new CommandStdinTest and InteractiveShellTest cover content, chunking,
End, console-mode option on the wire, a scripted REPL round trip, Ctrl+C,
exit-code propagation, and the cleanup invariants. Verified live against a
real Windows Server 2008 R2 host (piped sort and a full shell session).

Closes #136

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: 5db84956ad

ℹ️ 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/org/metricshub/winrm/CommandRequest.java Outdated
Comment thread src/main/java/org/metricshub/winrm/cli/InteractiveShell.java
Comment thread src/main/java/org/metricshub/winrm/light/WsmanClient.java
Comment thread src/main/java/org/metricshub/winrm/RemoteProcess.java Outdated
… End tracking, shell timeout floor

- stdin() (no argument) on CommandRequest declares interactive input for
  start(): pipe semantics (WINRS_CONSOLEMODE_STDIN=FALSE) without
  pre-supplied content, so RemoteProcess.stdin().close() actually delivers
  EOF to filters like sort. execute() rejects the declaration up front - it
  cannot take interactive input, and a command waiting on a never-fed pipe
  would hang for a whole timeout. Plain start() keeps the historical console
  semantics (what the interactive CLI shell wants).
- New ChunkEncoder (the mirror image of ChunkDecoder) carries the charset
  encoder state across stdin flushes: a byte-order mark is emitted once, not
  per flush, and a surrogate pair split by a flush boundary is withheld and
  completed by the next write instead of becoming two replacements.
- RemoteCommand.send() tracks the delivered End mark and rejects later sends
  locally, so direct CommandCursor users cannot append input after EOF.
- The CLI rejects a shell --timeout below 1000 ms: it caps each bounded poll
  of the session pump, and a poll below the WSMan 500 ms floor plus transit
  slack never reaches the wire - the shell would spin locally without ever
  fetching output.

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: e9a471a4bd

ℹ️ 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/org/metricshub/winrm/WindowsRemoteExecutor.java Outdated
Comment thread src/main/java/org/metricshub/winrm/light/WsmanClient.java
…or timeouts keep polling

- WindowsRemoteExecutor.startCommand(4-arg) default now delegates console-mode
  requests to the historical three-argument variant, so a pre-existing
  executor that overrides only that variant keeps working for ordinary
  commands after the upgrade; only pipe-mode stdin is unsupported by default.
  The three-argument default throws again (as before this PR), and
  LightWinRMService overrides both variants explicitly.
- The bounded poll's cap by the per-round-trip timeout no longer pushes a
  caller's wait below the wire-poll minimum: a process configured with an
  inactivity timeout under 750 ms could otherwise never issue a Receive from
  poll()/waitFor(Duration) and never observe completion. Same clamp for the
  bounded completion Signal.

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: d23a612d8c

ℹ️ 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/org/metricshub/winrm/cli/InteractiveShell.java
Comment thread src/main/java/org/metricshub/winrm/cli/InteractiveShell.java Outdated
… shell input batches

- The shell pump no longer fails the session when a line (or the local EOF)
  gets queued while the final Done-carrying Receive is in flight: the
  cursor-level IllegalStateException is treated as "the command is gone,
  stop forwarding", and the next poll reports the exit code normally.
- Redirected input is forwarded in bounded batches (32 KiB of characters per
  round, the rest stays queued) so a fire hose of input cannot starve the
  output side of the pump or balloon the local buffers, and the reader
  thread's queue is capped (1024 lines) for backpressure toward the local
  stdin.

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: 6a4f6566f4

ℹ️ 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/org/metricshub/winrm/light/WsmanClient.java Outdated
Comment thread src/main/java/org/metricshub/winrm/CommandRequest.java Outdated
Comment thread src/main/java/org/metricshub/winrm/RemoteProcess.java
…ssed cleanup, closed writer

- Send and the ctrl_c Signal now go through the same streaming timeout
  translation as command startup and Receive: a server staying quiet for a
  whole inactivity timeout surfaces as the documented WinRMTimeoutException
  (CLI exit 124), not as a generic connection or protocol failure.
- A failed pre-supplied stdin delivery is no longer masked when the cleanup
  terminate Signal fails too: the original exception wins, the cleanup
  failure travels as suppressed.
- With pre-supplied stdin, the RemoteProcess.stdin() writer is now actually
  closed at construction: writes are rejected immediately instead of being
  buffered into the void by the BufferedWriter layer.

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: 76d1cd5586

ℹ️ 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/org/metricshub/winrm/cli/WinRmCli.java Outdated
Comment thread src/main/java/org/metricshub/winrm/cli/WinRmCli.java Outdated
…l cadence from response transit

- Stdin forwarding no longer keys on System.console() alone, which is also
  null when only the OUTPUT is redirected (command hostname > result.txt) and
  would then hang the CLI consuming an interactive terminal. The input itself
  is probed: bytes already waiting at startup can only come from a pipe or a
  redirected file; an untouched terminal reports nothing available.
- New cadence variant of the bounded poll, poll(ask, maxWait): the server is
  asked to answer within the polling cadence, but the answer itself may take
  up to the inactivity timeout to arrive - one slow answer from a loaded
  server no longer kills the interactive session at the 1-second cadence.
  RemoteProcess.poll(Duration) now uses it (the wait is the cadence, the
  request timeout governs transit); waitFor(Duration) keeps the hard bound
  for deadlines, including against a dead peer.

Verified live against a real Windows Server 2008 R2 host again (interactive
shell session, exit-code propagation).

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: 494b652a0c

ℹ️ 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/org/metricshub/winrm/cli/WinRmCli.java
Comment thread src/main/java/org/metricshub/winrm/cli/InteractiveShell.java Outdated
… oversized shell input records

- Stdin redirection detection no longer relies on buffered bytes alone: a
  seekable descriptor 0 is a redirected file (catching empty files and the
  null device - verified live: `command sort < /dev/null` now completes
  immediately instead of hanging), while waiting bytes still catch pipes.
  The one undetectable case - a pipe whose producer writes only after the
  CLI started - gets the new explicit -i/--stdin option (command subcommand
  only), which forces forwarding regardless of detection.
- The shell pump now splits an input record larger than one round's budget:
  the surplus leads the next round instead of being encoded and sent whole,
  so a giant newline-free record (minified JSON, base64) keeps memory
  bounded and the input/output rounds alternating.

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: 6dfdd05280

ℹ️ 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/org/metricshub/winrm/RemoteProcess.java Outdated
Comment thread src/main/java/org/metricshub/winrm/cli/InteractiveShell.java Outdated
…at the reader

- The stdin ChunkEncoder is now created on first actual input rather than
  with the RemoteProcess: a legal decode-only charset (x-JISAutoDetect,
  ISO-2022-CN) throws on Charset.newEncoder(), which previously failed
  start() AFTER the cursor was acquired - leaking the running command and
  the client's serial connection - even for callers that only read output.
  The failure now hits exactly the flush that actually needs an encoder,
  and a bare close of the untouched writer stays silent.
- The shell's local reader thread no longer materializes whole lines
  (BufferedReader.readLine() would heap out on a giant newline-free record
  before any downstream bound applied): it reads and queues bounded pieces
  (4 KiB), normalizing every line-ending flavor (LF, CRLF, lone CR) to the
  CRLF the remote cmd.exe expects. The pump's InputSource seam moves from
  lines to terminator-included pieces accordingly, so the byte-bounded
  backpressure (256-piece queue) actually bounds memory by characters.

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: 71e299dcc8

ℹ️ 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/org/metricshub/winrm/CommandRequest.java
stdin() (no argument) now discards previously pre-supplied input, so a
reusable request switched to interactive mode actually behaves
interactively: start() no longer pre-sends the stale source, and execute()
rejects the declaration as documented.

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

Copy link
Copy Markdown
Contributor Author

@codex please review again

The shell subcommand now runs `cmd.exe /Q` (cmd's command echo off) over
pipe-mode stdin (WINRS_CONSOLEMODE_STDIN=FALSE, the winrs -noecho
equivalent): the forwarded input is never repeated back by the remote side
- the local terminal already shows what the user types, and the output
stream carries the prompts and command output only. Pipe-mode stdin also
turns the local end-of-input into a real EOF: cmd.exe exits on it, so a
piped session ends cleanly without an explicit `exit`.

Verified live against a real Windows Server 2008 R2 host: no input echo,
prompts and output intact, exit-code propagation unchanged, and an
EOF-only session terminates with the shell's exit code.

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: f244e14cf1

ℹ️ 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/org/metricshub/winrm/cli/InteractiveShell.java Outdated
Comment thread src/main/java/org/metricshub/winrm/cli/InteractiveShell.java Outdated
- The shell's local input is no longer decoded with Charset.defaultCharset(),
  which is UTF-8 on modern JDKs even when a Windows console feeds System.in
  with its OEM code page (mangling non-ASCII commands). The real encoding is
  probed in order: stdin.encoding (recent JDKs), sun.stdin.encoding (Windows
  consoles on older JDKs), Console.charset() (Java 17+, reached reflectively),
  native.encoding (Java 18+), then the process default.
- A local stdin read failure no longer masquerades as a normal end of input
  (which closed the remote stdin, let the shell run the truncated input, and
  reported success): the failure travels through the queue and fails the
  bridge, so the CLI exits nonzero with the actual error.

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: a3f81a559a

ℹ️ 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/org/metricshub/winrm/cli/WinRmCli.java Outdated
…iguration

The ctrl_c-requires-console-mode claim is refuted by a live probe against a
real Windows Server 2008 R2 host: with WINRS_CONSOLEMODE_STDIN=FALSE, the
WSMan ctrl_c Signal interrupted a 29-second ping after 0.6 s ("Control-C"
in the output) and the session kept running - the Signal is delivered as a
console ctrl event to the command's process group regardless of the stdin
mode option. Production keeps pipe-mode stdin (echo-free, real EOF).

The valid part of the finding: the pump tests started a plain cmd.exe with
console-mode stdin instead of what the shell subcommand actually runs. They
now start the production configuration (cmd.exe /Q over pipe-mode stdin)
through a shared helper.

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. Can't wait for the next one!

Reviewed commit: e0477bad0b

ℹ️ 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 and others added 2 commits July 29, 2026 13:48
…din, ANSI-encoded

Typing "echo testé" in the shell produced "test????". Diagnosed against a real
Windows host by probing every combination of stdin mode and input encoding:

- cmd.exe reading its COMMAND LINES from a PIPED stdin under console code page
  65001 decodes them one byte at a time, so every non-ASCII byte becomes
  U+FFFD. No client-side encoding fixes it: UTF-8, CP1252, CP850 and CP437 all
  come back corrupted. This is what the echo-suppression change (pipe mode)
  exposed.
- With CONSOLE-mode stdin the WinRM service converts the bytes itself, using
  the remote machine's ANSI code page: the same line encoded in windows-1252
  round-trips intact on a French host, and output stays UTF-8 (full Unicode).
- DATA piped to an ordinary program (sort, findstr) is unaffected in either
  mode - it never goes through cmd's parser - so the stdin(...) builders keep
  pipe semantics and UTF-8, verified intact.

The shell therefore goes back to console-mode stdin and encodes what the user
types with the remote ANSI code page, queried once per session from
Win32_OperatingSystem.CodeSet (fallback windows-1252). Echo stays off: cmd.exe
/Q alone suppresses it, and console-mode stdin never echoes. Console mode has
no end of input, so the pump now translates the local EOF into an "exit"
command; the remote exit code still propagates.

New CommandRequest.stdinCharset(Charset) carries the asymmetry: output follows
the console code page (UTF-8), console-mode input follows the ANSI one.

Verified live on Windows Server 2008 R2: "echo testé français" echoes back
intact, no input echo, EOF-only session exits 0, "exit 9" propagates 9.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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: 674b9a96af

ℹ️ 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/org/metricshub/winrm/cli/InteractiveShell.java
…se of the mangled input

The previous fix was wrong about the mechanism and only helped on Windows
Server 2008 R2. Probing both a 2008 R2 and a 2022 host, in both stdin modes,
with four input encodings each, gives one consistent answer: the culprit is
console code page 65001 itself. A remote cmd.exe decodes the command lines it
reads from its standard input ONE BYTE AT A TIME under 65001, so every
non-ASCII byte becomes U+FFFD whatever the client sends - console mode only
appeared to work on 2008 R2 because that version's service converted the bytes
with the ANSI page first. Under ANY single-byte console code page (1252, 850,
437) every combination round-trips intact on both hosts.

The shell therefore runs under the remote machine's ANSI code page, queried
once per session from Win32_OperatingSystem.CodeSet (fallback 1252), with the
matching charset on both directions. That page must be set when the shell is
created, so the console code page is now a client-level setting
(WinRMClient.Builder.consoleCodePage, plumbed to WINRS_CODEPAGE) and the CLI
opens the session on a second connection once the probe has answered.

Pipe-mode stdin comes back with it: under a single-byte page it carries
non-ASCII fine, cmd.exe /Q keeps the echo off, and the local end-of-input is a
real EOF again (no synthetic "exit" needed).

Everything else keeps code page 65001 and UTF-8: wql, command, and data piped
to a program never go through cmd's command-line parser.

Verified live on Windows Server 2022 (French) and 2008 R2: "echo testé
français" round-trips intact, banner correct, no echo, EOF exits, exit codes
propagate.

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: 8087838597

ℹ️ 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/org/metricshub/winrm/WinRMClient.java Outdated
Comment thread src/main/java/org/metricshub/winrm/light/WsmanClient.java Outdated
…ls within the configured timeout

- The consoleCodePage Javadoc had been inserted between the timeout Javadoc
  and timeout(Duration), leaving that public method undocumented. Moved back.
- A bounded poll's budget is capped by the per-round-trip timeout STRICTLY
  again: raising it to the wire minimum made a poll outlast the inactivity
  tolerance its caller configured, contradicting both the CommandCursor and
  RemoteProcess contracts. A timeout below the protocol floor therefore keeps
  bounded polls local (no wire), which the Javadoc and the test now state
  explicitly - the unbounded paths (waitFor, the readers) still advance such a
  process, and the CLI's shell already rejects timeouts under 1 s.

Re-verified live on Windows Server 2022: "echo testé français" intact, exit
code propagated.

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: 991c6dc6b3

ℹ️ 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/org/metricshub/winrm/cli/WinRmCli.java Outdated
…one decision

charsetOfCodePage() could fall back to windows-1252 while the shell was still
created under the code page the host reported. The dangerous case is a host
configured for UTF-8: it reports 65001 as its ANSI page, so the session would
have run under exactly the code page an interactive cmd.exe cannot read
command lines from - with a mismatched charset on top, corrupting both
directions.

sessionEncoding() now returns the code page AND the charset together, so they
can never disagree, and refuses two answers by falling back on both counts:
65001 (unusable for a shell by construction) and any page this JVM has no
charset for.

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. Another round soon, please!

Reviewed commit: 19d78873d0

ℹ️ 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
bertysentry merged commit 12b61a1 into main Jul 29, 2026
5 checks passed
@bertysentry
bertysentry deleted the 136-command-stdin-interactive-shell branch July 29, 2026 13:41
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.

Command stdin and interactive shell: stdin() on the builders, RemoteProcess.stdin(), and a CLI shell subcommand

1 participant