1 package org.metricshub.winrm;
2
3 /*-
4 * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
5 * WinRM Java Client
6 * ჻჻჻჻჻჻
7 * Copyright (C) 2023 - 2026 MetricsHub
8 * ჻჻჻჻჻჻
9 * Licensed under the Apache License, Version 2.0 (the "License");
10 * you may not use this file except in compliance with the License.
11 * You may obtain a copy of the License at
12 *
13 * http://www.apache.org/licenses/LICENSE-2.0
14 *
15 * Unless required by applicable law or agreed to in writing, software
16 * distributed under the License is distributed on an "AS IS" BASIS,
17 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 * See the License for the specific language governing permissions and
19 * limitations under the License.
20 * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
21 */
22
23 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
24 import java.util.concurrent.TimeoutException;
25 import org.metricshub.winrm.exceptions.WindowsRemoteException;
26
27 /**
28 * A cursor over the raw output of a running remote command, returned by
29 * {@link WindowsRemoteExecutor#startCommand(String, String, long)}. Each {@link #next()} is one
30 * WSMan Receive round trip yielding the output bytes exactly as the server handed them out —
31 * undecoded, because a multibyte character can be split across chunks; decode with a stateful
32 * {@link java.nio.charset.CharsetDecoder} (or accumulate the bytes and decode once at the end).
33 * <p>
34 * The cursor owns the executor's serial connection until the command completes or the cursor is
35 * closed: no other operation can run on the same executor while the cursor is open. Completion
36 * (a {@code null} return from {@link #next()}) sends the protocol's terminate Signal and releases
37 * the connection on its own; closing earlier sends the same Signal, which actually stops the
38 * still-running remote command. Always close the cursor — use try-with-resources.
39 * <p>
40 * A cursor is not thread-safe: advance and close it from one thread at a time.
41 */
42 public interface CommandCursor extends AutoCloseable {
43 /**
44 * Block until the remote command produces output (or completes), for at most one
45 * per-round-trip timeout.
46 *
47 * @return the next chunk of raw output — possibly empty — or {@code null} once the command has
48 * completed; the exit code is then available from {@link #exitCode()}
49 * @throws TimeoutException when the command produces no output for a whole per-round-trip
50 * timeout (the inactivity timeout of the stream)
51 * @throws WindowsRemoteException for any other failure while receiving
52 */
53 Chunk next() throws TimeoutException, WindowsRemoteException;
54
55 /**
56 * Bounded variant of {@link #next()}: block at most the given wait for output. When the
57 * command produces nothing in that window, an <b>empty</b> chunk is returned — a bounded poll
58 * expiring is not a failure, and the cursor remains fully usable — unlike {@link #next()},
59 * whose whole per-round-trip timeout counts as the stream's inactivity limit. Deadline-bounded
60 * waits (e.g. {@code RemoteProcess.waitFor(Duration)}) are built on this.
61 * <p>
62 * The default implementation does not bound the wait: it delegates to {@link #next()}.
63 *
64 * @param maxWaitMillis how long to block at most, capped by the cursor's per-round-trip timeout
65 * @return the next chunk of raw output — empty when the wait elapsed first — or {@code null}
66 * once the command has completed
67 * @throws TimeoutException when the server does not even answer the bounded request
68 * @throws WindowsRemoteException for any other failure while receiving
69 */
70 default Chunk poll(final long maxWaitMillis) throws TimeoutException, WindowsRemoteException {
71 return next();
72 }
73
74 /**
75 * Cadence variant of {@link #poll(long)}: ask the server to answer within
76 * {@code askMillis} — the polling cadence — while allowing the answer itself up to
77 * {@code maxWaitMillis} to arrive. A polling consumer (e.g. an interactive session pump)
78 * wants short idle rounds, but must not fail the stream when a loaded or distant server
79 * takes longer than one cadence to get its answer across; {@link #poll(long)} is exactly
80 * this call with {@code askMillis == maxWaitMillis}.
81 * <p>
82 * The default implementation delegates to {@link #poll(long)} with the full wait.
83 *
84 * @param askMillis when the server should answer at the latest — with output when it has
85 * any, with the protocol's "nothing yet" otherwise
86 * @param maxWaitMillis how long to block at most, capped by the cursor's per-round-trip
87 * timeout
88 * @return the next chunk of raw output — empty when nothing arrived — or {@code null} once
89 * the command has completed
90 * @throws TimeoutException when the server does not even answer the bounded request
91 * @throws WindowsRemoteException for any other failure while receiving
92 */
93 default Chunk poll(final long askMillis, final long maxWaitMillis) throws TimeoutException, WindowsRemoteException {
94 return poll(maxWaitMillis);
95 }
96
97 /**
98 * Feed standard input to the running command — the WSMan Send operation, carrying the bytes to
99 * the command's {@code stdin} stream. Input larger than one envelope's worth is split into
100 * several Send requests automatically. A Send is an ordinary request on the executor's serial
101 * connection: it alternates with {@link #next()}/{@link #poll(long)} on the caller's thread,
102 * it never runs concurrently with them.
103 * <p>
104 * The default implementation throws {@link UnsupportedOperationException}: only executors that
105 * support command input (such as the built-in lightweight backend) implement this method.
106 *
107 * @param data the input bytes (possibly empty — with {@code end}, a pure end-of-input Send)
108 * @param end {@code true} to mark the end of input: the command's stdin then reaches EOF, and
109 * no further input may be sent
110 * @throws IllegalStateException when the command has already completed or the cursor is closed
111 * @throws TimeoutException when the server does not answer the Send in time
112 * @throws WindowsRemoteException for any other failure while sending
113 */
114 default void send(final byte[] data, final boolean end) throws TimeoutException, WindowsRemoteException {
115 throw new UnsupportedOperationException(getClass().getName() + " does not support command input.");
116 }
117
118 /**
119 * Interrupt the command the way a console Ctrl+C would — the WSMan Signal operation with the
120 * {@code ctrl_c} code. Unlike {@link #close()}'s terminate Signal, it interrupts the command's
121 * child process without ending the command itself: the cursor stays fully usable. A no-op once
122 * the command has completed or the cursor is closed.
123 * <p>
124 * The default implementation throws {@link UnsupportedOperationException}: only executors that
125 * support it (such as the built-in lightweight backend) implement this method.
126 *
127 * @throws TimeoutException when the server does not answer the Signal in time
128 * @throws WindowsRemoteException for any other failure while signaling
129 */
130 default void interrupt() throws TimeoutException, WindowsRemoteException {
131 throw new UnsupportedOperationException(getClass().getName() + " does not support command interruption.");
132 }
133
134 /**
135 * Get the command's exit code.
136 *
137 * @return the exit code
138 * @throws IllegalStateException when the command has not completed yet — completion is
139 * observed as a {@code null} return from {@link #next()}
140 */
141 int exitCode();
142
143 /**
144 * Terminate the command (when it is still running) with the WinRM terminate Signal and release
145 * the executor's connection. Idempotent; a no-op when the command already completed. After an
146 * early close, {@link #next()} returns {@code null} without touching the connection again (and
147 * no exit code is available, since the command never completed). May throw an unchecked
148 * {@link org.metricshub.winrm.exceptions.WinRMClientException} when the Signal itself fails —
149 * the remote command may then still be running.
150 */
151 @Override
152 void close();
153
154 /** One Receive response's worth of raw output bytes, split by stream. */
155 final class Chunk {
156
157 private final byte[] stdout;
158 private final byte[] stderr;
159
160 /**
161 * Create a chunk over the given stream bytes (not copied: a chunk is a transient carrier
162 * between the protocol loop and the decoder, not a retained value).
163 *
164 * @param stdout the raw stdout bytes of this chunk (possibly empty, never null)
165 * @param stderr the raw stderr bytes of this chunk (possibly empty, never null)
166 */
167 @SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "Chunks are transient carriers on the output hot path; defensive copies "
168 +
169 "would double the allocation for no benefit")
170 public Chunk(final byte[] stdout, final byte[] stderr) {
171 this.stdout = stdout;
172 this.stderr = stderr;
173 }
174
175 /**
176 * Get the raw stdout bytes of this chunk.
177 *
178 * @return the stdout bytes, possibly empty
179 */
180 @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "Chunks are transient carriers on the output hot path; defensive copies "
181 +
182 "would double the allocation for no benefit")
183 public byte[] stdout() {
184 return stdout;
185 }
186
187 /**
188 * Get the raw stderr bytes of this chunk.
189 *
190 * @return the stderr bytes, possibly empty
191 */
192 @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "Chunks are transient carriers on the output hot path; defensive copies "
193 +
194 "would double the allocation for no benefit")
195 public byte[] stderr() {
196 return stderr;
197 }
198 }
199 }