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.
Context
The public API currently materializes complete responses in memory:
Enumerateresponse and every subsequentPullresponse.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
Pullloop,Receiveloop); only the client buffers.This issue targets the lightweight implementation only:
LightWinRMService/WsmanClient.Depends on
WinRMClientAPI. Modern fluent client API: WinRMClient builder with per-operation builders #131 deliberately builds its internal seams stream-first (the Pull and Receive loops expose iterator/callback-shaped primitives, and the blockingexecute()is implemented as "drain the stream"), so this issue only adds new terminal methods on the existing operation builders.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:
execute()→WqlResultstream()→Stream<WqlRow>(closeable)execute()→CommandResultstart()→RemoteProcess(closeable)WQL: lazy
Stream<WqlRow>Requirements:
Pullrequests only as iteration advances (backpressure is free — it is a pull protocol);AutoCloseableand documented as such (same contract asFiles.lines());Releaseso the server frees the enumeration context — a nicety the client does not send today, which would also help with server-side operation quotas (e.g. 2008R2MaxConcurrentOperationsPerUser);pageSize(int)on the WQL builder for tuning (today's hardcodedMaxElements=32000becomes the default) — overlaps with Allow configuration of batch size (MaxElements) and pull timeout (MaxTime) for WQL/WinRM queries #86/WQL queries should support setting max elements #35;Command: a
Process-like handleA
Stream<String>of lines is too lossy for commands (two output channels, an exit code, a lifecycle). The shape Java developers already know isjava.lang.Process:Plus a callback middle ground for "tail the output but block until done", keeping
execute()as the terminal:Requirements:
Signal;receiveLoopdeliberately accumulates raw bytes and decodes once at completion, because a multibyte character can be split acrossStreamchunks /Receiveresponses. Streaming output must use a statefulCharsetDecoderthat carries partial-character state between chunks — not per-chunknew String(bytes);Receiveresponse is processed.Semantics that must be defined up front
execute(), timeout = overall deadline (as today). Forstream()/start(), an overall deadline makes long tails impossible — the builder timeout should mean inactivity timeout (max silence between chunks/pages), withwaitFor(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.WinRMClientis 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 JDBCResultSeton its connection).Backward compatibility
WindowsRemoteExecutor.executeWql(...)/executeCommand(...)and the static helpers remain source- and behavior-compatible. Where practical they become convenience collectors over the streaming primitives (drain-the-stream), so the two paths cannot drift apart.Acceptance criteria
Release/ terminateSignal) and release locks/connections.LightWinRMService/WsmanClientimplements the streaming contract; the blockingexecute()path is implemented on top of it.WsmanProtocolTeststyle) cover multi-page enumeration, early termination, large output, interleaved stdout/stderr, split multibyte characters, timeout, cancellation, and resource cleanup.