Skip to content

Streaming APIs (phase 2): stream()/start() terminal methods on the fluent WinRMClient #111

Description

@bertysentry

Note (2026-07-27): reframed as phase 2 of the API modernization. Streaming is now designed as additional terminal methods on the fluent WinRMClient operation builders introduced by #131, rather than new methods on WindowsRemoteExecutor. The technical requirements below (lazy Pull, incremental decoding, lifecycle, cleanup) are unchanged.

Context

The public API currently materializes complete responses in memory:

  • WQL execution returns all rows after collecting the initial Enumerate response and every subsequent Pull response.
  • Command execution returns a result only after collecting all stdout and stderr into strings.

This is inconvenient and memory-intensive for large WQL result sets and long-running or verbose commands. Callers should be able to process rows and command output incrementally as WinRM responses arrive — the wire protocol is already streaming (WS-Enumeration Pull loop, Receive loop); only the client buffers.

This issue targets the lightweight implementation only: LightWinRMService / WsmanClient.

Depends on

Proposed API

The choice between "materialize everything" and "stream" is a different terminal method on the same operation builder — everything upstream (auth, TLS, namespace, working directory, timeout) is shared:

Operation Materialized (#131) Streaming (this issue)
WQL execute()WqlResult stream()Stream<WqlRow> (closeable)
Command execute()CommandResult start()RemoteProcess (closeable)

WQL: lazy Stream<WqlRow>

try (Stream<WqlRow> rows = client.wql("SELECT * FROM Win32_NTLogEvent").stream()) {
    rows.filter(r -> "Error".equals(r.string("Type")))
        .limit(100)
        .forEach(this::process);
}

Requirements:

Command: a Process-like handle

A Stream<String> of lines is too lossy for commands (two output channels, an exit code, a lifecycle). The shape Java developers already know is java.lang.Process:

try (RemoteProcess p = client.command("wevtutil qe System /f:text").start()) {
    try (BufferedReader out = p.stdout()) {        // Reader, decoded incrementally
        out.lines().forEach(this::process);
    }
    int exit = p.waitFor();                        // or waitFor(Duration)
}

Plus a callback middle ground for "tail the output but block until done", keeping execute() as the terminal:

client.command("longRunningThing.exe")
      .onStdout(chunk -> log.info(chunk))
      .onStderr(chunk -> log.warn(chunk))
      .execute();   // still returns CommandResult at the end

Requirements:

  • distinguish stdout from stderr and preserve ordering within each channel;
  • expose the eventual exit code and execution metadata;
  • support cancellation/close and reliably send the WinRM terminate Signal;
  • avoid deadlock or unbounded buffering if the caller consumes one channel more slowly than the other;
  • incremental decoding: the current receiveLoop deliberately accumulates raw bytes and decodes once at completion, because a multibyte character can be split across Stream chunks / Receive responses. Streaming output must use a stateful CharsetDecoder that carries partial-character state between chunks — not per-chunk new String(bytes);
  • flush chunks promptly as each Receive response is processed.

Semantics that must be defined up front

  • Timeout: for execute(), timeout = overall deadline (as today). For stream()/start(), an overall deadline makes long tails impossible — the builder timeout should mean inactivity timeout (max silence between chunks/pages), with waitFor(Duration) for callers who want a hard deadline. Whichever is chosen, it is documented in the Javadoc from day one; retrofitting timeout semantics is painful.
  • Connection pinning: a WinRMClient is one serial channel (one socket, stateful RC4, operations serialized). An open stream/handle pins the connection; other operations on that client block until it is closed. Do not fight this — document it (same contract as a JDBC ResultSet on its connection).

Backward compatibility

Acceptance criteria

  • WQL rows can be consumed before the complete enumeration has been retrieved.
  • Command stdout and stderr can be consumed before the remote command exits.
  • Memory usage is bounded by the active WSMan page/chunks rather than total result size.
  • Early close, cancellation, timeout, parse failure, and consumer failure clean up the enumeration/command (WS-Enumeration Release / terminate Signal) and release locks/connections.
  • Timeout semantics (inactivity vs deadline) are documented and enforced throughout stream consumption.
  • LightWinRMService / WsmanClient implements the streaming contract; the blocking execute() path is implemented on top of it.
  • Tests (FakeWsmanServer / WsmanProtocolTest style) cover multi-page enumeration, early termination, large output, interleaved stdout/stderr, split multibyte characters, timeout, cancellation, and resource cleanup.
  • Javadocs include try-with-resources examples for both streaming operations; README documents the streaming API.

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