You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The current public API has aged poorly compared to what modern Java libraries offer:
WinRMWqlExecutor.executeWql(...) and WinRMCommandExecutor.execute(...) take 10–11 positional parameters, most of them nullable. Call sites are unreadable (null, null, null, 30000, null, null) and every new option is a breaking change or another overload.
One connection per call: the static helpers authenticate (full NTLM/Kerberos handshake), run a single operation, and tear everything down. Callers who poll pay the handshake every time unless they drop down to the semi-internal WinRMExecutorFactory / WindowsRemoteExecutor layer.
Primitive-obsessed signatures: long timeouts in milliseconds, Integer port, results as List<List<String>> with a parallel headers list.
Global configuration via system properties (org.metricshub.winrm.tls.insecure) — process-wide, cannot differ per host.
Three checked exceptions per method, with WqlQuerySyntaxException leaking out of command execution wrapped in IOException.
Proposal: a two-level builder API
Follow the pattern modern Java clients converged on (java.net.http.HttpClient, AWS SDK v2, Elasticsearch Java client):
Level 2 — operation builders: per-call settings (query/command, namespace, working directory, per-call timeout), terminated by execute().
try (WinRMClientclient = WinRMClient.builder("server01.acme.com")
.https() // default: http; port inferred (5985/5986)
.credentials("ACME\\admin", password) // char[], same wipeable semantics as today
.authentication(AuthScheme.KERBEROS, AuthScheme.NTLM) // ordered fallback; default NTLM
.ticketCache(path) // optional
.trustAllCertificates() // per-client, replaces the system property
.timeout(Duration.ofSeconds(30)) // default for all operations
.build()) {
// WQLWqlResultservices = client.wql("SELECT Name, State FROM Win32_Service")
.namespace("root\\cimv2") // default: root/cimv2
.timeout(Duration.ofSeconds(10)) // overrides client default
.execute();
for (WqlRowrow : services) { // Iterable<WqlRow>Stringname = row.string("Name");
}
services.columns(); // List<String>, query order preserved// CommandCommandResultresult = client.command("CSCRIPT script.vbs")
.workingDirectory("C:\\Temp")
.upload(Path.of("script.vbs")) // explicit, not a magic string-rewrite list
.execute();
result.stdout(); result.stderr(); result.exitCode(); result.elapsed(); // Duration
}
Design decisions
The client is the connection, and it is reusable — one authentication, N operations, explicit lifecycle via try-with-resources. Exposes what the light backend already does well (session reuse, stale-connection detection). Document thread-safety honestly: safe to share, operations serialized per connection.
java.time.Duration everywhere, never long millis. elapsed() on results returns Duration too.
No new positional statics. The one-shot case stays a one-liner through the same builder (WinRMClient.builder(host).credentials(u, p).build().wql("...").execute()), so there is no second API to learn.
Unchecked exceptions with a hierarchy: WinRMClientException extends RuntimeException, with subtypes WinRMAuthenticationException, WinRMFaultException (carrying the WSMan fault code + detail text as fields, since callers do contains() matching on fault text today), WinRMTimeoutException, WqlSyntaxException. The legacy checked types remain untouched on the legacy API.
Typed result objects: WqlResult (iterable rows + columns()), WqlRow (thin wrapper over the ordered map with string(name), get(name), asMap()), CommandResult with exitCode() (a process exit code, not an HTTP status).
File transfer becomes first-class: keep upload(path) on the command builder (command rewritten as today via ShellFileCopy), and also expose a direct client.uploadFile(localPath, remotePath).
Per-client TLS configuration replaces the system property: trustAllCertificates(), plus sslContext(SSLContext) for custom trust stores. The system property keeps working as a global fallback for one release; the builder wins.
Naming and placement: new API in the root package as the obvious front door — org.metricshub.winrm.WinRMClient, WqlResult, WqlRow, CommandResult, and a new root-package AuthScheme enum (org.metricshub.winrm.service.client.auth.AuthenticationEnum is a CXF-era path that should not appear in new signatures).
Stream-first internal seams: even though this phase ships only blocking terminal methods, the internal WsmanClient seams (Pull loop, Receive loop) should expose iterator/callback-shaped primitives so that execute() is implemented as "drain the stream". Phase 2 (Streaming APIs (phase 2): stream()/start() terminal methods on the fluent WinRMClient #111) then adds stream()/start() terminal methods without re-plumbing.
Java 11 baseline (maven.compiler.release=11): no records or sealed interfaces — plain immutable classes with concise accessor style (stdout(), not getStdout()).
WinRMClient is a facade over the existing internals (WinRMExecutorFactory / LightWinRMService), so behavior stays identical.
Backward compatibility
Purely additive — fits a 2.1.0.
WinRMWqlExecutor.executeWql(...) and WinRMCommandExecutor.execute(...) keep working unchanged; they can eventually be reimplemented as one-liners over the new API.
Deprecation of the legacy statics (@Deprecated without forRemoval) only once the new API has shipped and downstream consumers (MetricsHub) have migrated — a signpost, not a threat.
Acceptance criteria
The whole example above compiles and works against a real WinRM host (NTLM over HTTP and HTTPS, Kerberos over HTTPS, ordered fallback).
One client instance can run many WQL queries and commands over a single authenticated session.
All builder options have sensible defaults; only hostname and credentials are mandatory.
New exception hierarchy is thrown consistently; WSMan fault code and detail text are programmatically accessible.
Unit tests cover the builders (defaults, validation) and the facade wiring; protocol behavior is already covered by WsmanProtocolTest and must not regress.
README leads with the new API; the legacy API moves to a "Legacy API" section.
Context
The current public API has aged poorly compared to what modern Java libraries offer:
WinRMWqlExecutor.executeWql(...)andWinRMCommandExecutor.execute(...)take 10–11 positional parameters, most of them nullable. Call sites are unreadable (null, null, null, 30000, null, null) and every new option is a breaking change or another overload.WinRMExecutorFactory/WindowsRemoteExecutorlayer.longtimeouts in milliseconds,Integerport, results asList<List<String>>with a parallelheaderslist.org.metricshub.winrm.tls.insecure) — process-wide, cannot differ per host.WqlQuerySyntaxExceptionleaking out of command execution wrapped inIOException.Proposal: a two-level builder API
Follow the pattern modern Java clients converged on (
java.net.http.HttpClient, AWS SDK v2, Elasticsearch Java client):AutoCloseableclient.execute().Design decisions
java.time.Durationeverywhere, neverlongmillis.elapsed()on results returnsDurationtoo.WinRMClient.builder(host).credentials(u, p).build().wql("...").execute()), so there is no second API to learn.WinRMClientException extends RuntimeException, with subtypesWinRMAuthenticationException,WinRMFaultException(carrying the WSMan fault code + detail text as fields, since callers docontains()matching on fault text today),WinRMTimeoutException,WqlSyntaxException. The legacy checked types remain untouched on the legacy API.WqlResult(iterable rows +columns()),WqlRow(thin wrapper over the ordered map withstring(name),get(name),asMap()),CommandResultwithexitCode()(a process exit code, not an HTTP status).upload(path)on the command builder (command rewritten as today viaShellFileCopy), and also expose a directclient.uploadFile(localPath, remotePath).trustAllCertificates(), plussslContext(SSLContext)for custom trust stores. The system property keeps working as a global fallback for one release; the builder wins.org.metricshub.winrm.WinRMClient,WqlResult,WqlRow,CommandResult, and a new root-packageAuthSchemeenum (org.metricshub.winrm.service.client.auth.AuthenticationEnumis a CXF-era path that should not appear in new signatures).WsmanClientseams (Pull loop, Receive loop) should expose iterator/callback-shaped primitives so thatexecute()is implemented as "drain the stream". Phase 2 (Streaming APIs (phase 2): stream()/start() terminal methods on the fluent WinRMClient #111) then addsstream()/start()terminal methods without re-plumbing.pageSize(int)setsMaxElementson Enumerate/Pull (today's hardcoded 32000 becomes the default) andpullTimeout(Duration)sets the PullOperationTimeout/MaxTime.OptimizeEnumerationis already always sent since 2.0.0. The streaming variant (Streaming APIs (phase 2): stream()/start() terminal methods on the fluent WinRMClient #111) reuses the same knobs.Constraints
maven.compiler.release=11): no records or sealed interfaces — plain immutable classes with concise accessor style (stdout(), notgetStdout()).WinRMClientis a facade over the existing internals (WinRMExecutorFactory/LightWinRMService), so behavior stays identical.Backward compatibility
WinRMWqlExecutor.executeWql(...)andWinRMCommandExecutor.execute(...)keep working unchanged; they can eventually be reimplemented as one-liners over the new API.@DeprecatedwithoutforRemoval) only once the new API has shipped and downstream consumers (MetricsHub) have migrated — a signpost, not a threat.Acceptance criteria
WsmanProtocolTestand must not regress.Related issues
MaxElements/MaxTime) via thepageSize/pullTimeoutbuilder options — the implementing PR should reference it.stream()for WQL,start()for commands) — Streaming APIs (phase 2): stream()/start() terminal methods on the fluent WinRMClient #111.