Skip to content

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

Description

@bertysentry

Phase 3 of the API modernization, building directly on the streaming terminals of #111 (PR #134). The bounded-poll primitive built there (CommandCursor.poll / RemoteCommand.pollChunk) is the enabler: it is exactly the "ask for output, but only for a moment" operation an input pump needs.

Context

The shell we create already declares <rsp:InputStreams>stdin</rsp:InputStreams>, but the client never sends any input: commands that read stdin hang or see an open, never-fed pipe. The WSMan shell protocol has first-class support for this — the Send operation (.../windows/shell/Send) carries base64 <rsp:Stream Name="stdin" CommandId="..."> chunks, with End="true" on the final chunk signalling EOF.

With input, the natural end-game is an interactive session: a shell subcommand on the CLI that behaves like winrs, letting the user drive cmd.exe on the remote host live.

Phase 1 — stdin for commands (library)

Proposed API

Pre-supplied input on the existing builders (works with both execute() and start()):

CommandResult result = client.command("sort")
    .stdin(Path.of("data.txt"))          // also stdin(InputStream), stdin(String)
    .execute();

Process-style interactive input on the streaming handle, completing the java.lang.Process shape:

try (RemoteProcess p = client.command("some-repl.exe").start()) {
    try (BufferedWriter in = p.stdin()) {      // OutputStream/Writer, encoded with the command charset
        in.write("first request\n");
        in.flush();                            // flush() emits a WSMan Send
        System.out.println(p.stdout().readLine());
    }                                          // close() sends the final chunk with End="true" (EOF)
    p.waitFor();
}

Requirements

  • New Send envelope in Envelopes (rsp:Send / stream stdin / optional End="true"), and a send(byte[] data, boolean end) seam on CommandCursor + RemoteCommand.
  • Same serial connection, no concurrency: a Send is just another request under the handle's connection permit. RemoteProcess stays single-threaded — writes and reads alternate on the caller's thread, like java.lang.Process pipes. Document the classic deadlock (blocking on a read while the remote blocks on input) as the caller's to avoid, exactly like Process.
  • WINRS_CONSOLEMODE_STDIN becomes configurable: FALSE (pipe semantics) when stdin is supplied programmatically, TRUE kept as today's default otherwise, and TRUE for the interactive shell below.
  • Chunking: input split to respect MaxEnvelopeSize (153 600), like ShellFileCopy does for file payloads.
  • Encoding: input encoded with the same charset used to decode output (detected code page or explicit charset(...)); ChunkDecoder has the decode side, encoding needs no state (chunk on byte boundaries after encoding, not before).
  • Cleanup discipline from Streaming APIs (phase 2): stream()/start() terminals on the fluent WinRMClient #134 carries over: closing stdin, early close, cancellation — no path may leave the connection desynced, revive a closed client, or hide a known exit code behind cleanup failures.
  • FakeWsmanServer learns Send: decrypt and record stdin chunks so tests can assert content, ordering, End flag, and console-mode option on the wire.

Phase 2 — interactive shell subcommand (CLI)

java -jar winrm-java-standalone.jar -h server -u 'DOMAIN\user' -pf pw.txt shell

Starts cmd.exe in the remote shell (console-mode stdin) and bridges it to the local terminal until the remote shell exits.

Design

  • Single-threaded protocol pump built on the bounded poll: loop = drain queued local input → Sendpoll(shortBudget) → write decoded stdout/stderr to the local streams. Latency is the poll cadence (~100–250 ms). A polling session never trips the inactivity timeout while idle — every round trip completes with output or the protocol's "nothing yet" fault.
  • One local helper thread only, reading local stdin into a queue (blocking System.in reads must not stall the pump). The WinRM connection itself stays strictly serial.
  • Line-oriented, like winrs: with zero dependencies there is no raw-terminal/PTY mode in pure Java, so input is line-buffered by the local terminal. No ANSI/completion guarantees — document this clearly in the manual.
  • Signals: local EOF (Ctrl+Z/Ctrl+D) → Send with End="true"; Ctrl+C → WSMan Signal with the ctrl_c code (new constant next to terminate), interrupting the remote child without killing the session.
  • Exit code: the remote cmd.exe exit code is propagated using the existing CLI exit-code contract.
  • command subcommand bonus: when local stdin is not a console (piped/redirected), forward it as the command's stdin — winrm-java ... command sort < data.txt just works.
  • Docs: new sections in the CLI manual (shell subcommand, stdin forwarding, limitations) and in commands.md for the library API; README one-liner.

Acceptance criteria

  • A command can consume programmatic stdin end-to-end (content, chunking, End flag, console-mode option asserted against FakeWsmanServer).
  • RemoteProcess.stdin() supports a write → flush → read round trip against a scripted REPL exchange.
  • Early close with stdin open, cancellation mid-Send, and client-closed-while-handle-open stay clean (no stray requests, no revived transport — same invariants as Streaming APIs (phase 2): stream()/start() terminals on the fluent WinRMClient #134).
  • The shell pump is testable headlessly: scripted exchange (input line → Send on the wire → scripted output → local stdout), EOF → End="true", Ctrl+C → ctrl_c Signal, exit-code propagation.
  • Timeout semantics documented: per-round-trip bounds apply; an idle interactive session does not die of inactivity.
  • mvn verify site green; cli.md updated.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions