View Javadoc
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 }