diff --git a/src/main/java/org/metricshub/winrm/RemoteFile.java b/src/main/java/org/metricshub/winrm/RemoteFile.java
index 8a39f15..a5d0e88 100644
--- a/src/main/java/org/metricshub/winrm/RemoteFile.java
+++ b/src/main/java/org/metricshub/winrm/RemoteFile.java
@@ -72,15 +72,16 @@
* }
*
* The content travels through the WinRM connection itself (no SMB, no extra port): a small
- * PowerShell script on the host writes the bytes base64-encoded, which is binary-safe and
- * independent of the remote console code page. It is designed for configuration files, logs and
- * small data files — not a bulk transport: base64 through a command shell is far slower
- * than SMB. The host needs PowerShell (2.0 or later) in {@code FullLanguage} mode.
+ * PowerShell script on the host writes the file's raw bytes to its standard output stream, past
+ * any text conversion, so every byte arrives as stored whatever the remote console code page. It
+ * is designed for configuration files, logs and small data files — not a bulk transport:
+ * the WinRM service forwards a command's output at about 2 MB/s, far slower than SMB. The host
+ * needs PowerShell (2.0 or later) in {@code FullLanguage} mode.
*
* The file is opened with a share mode that tolerates other writers, so a log being written by a
* running service can be read; a file held with an exclusive lock (e.g. {@code pagefile.sys})
* fails with a sharing violation. A read writes nothing on the host: its script travels on the
- * command line, which limits the path to about 1,450 characters (fewer with non-Latin
+ * command line, which limits the path to about 1,500 characters (fewer with non-Latin
* characters) — a longer path fails before anything is sent. Ranges are byte ranges, and
* reads are not snapshots: a
* file that grows or shrinks between two reads is read as it is at each read.
@@ -257,10 +258,10 @@ public String readText(final Charset charset) {
}
/**
- * Open the content (the whole file, or the configured range) as a stream, decoded as it
- * arrives: memory is bounded by one transfer block, not by the file, and there is no size cap.
- * The file is opened before this method returns, so a file that cannot be read fails here; a
- * failure midway is reported by {@code read()} (never as a silently short read).
+ * Open the content (the whole file, or the configured range) as a stream, passed on as it
+ * arrives: memory is bounded by one protocol response, not by the file, and there is no size
+ * cap. The file is opened before this method returns, so a file that cannot be read fails here;
+ * a failure midway is reported by {@code read()} (never as a silently short read).
*
* The stream must be closed — use try-with-resources. It holds the client's connection
* until it reaches its end or is closed; closing it early stops the remote read. Failures are
@@ -314,7 +315,7 @@ public BufferedReader openReader(final Charset charset) {
* {@link #maxBytes(long)} settings do not apply.
*
* The timeout is a wall-clock deadline for the whole download, and the transfer runs at about
- * 1.5 MB/s: a large file needs a raised {@link #timeout(Duration)}. A deadline that fires while
+ * 1.8 MB/s: a large file needs a raised {@link #timeout(Duration)}. A deadline that fires while
* the verified file is being moved onto the destination lets that move complete: the download
* then succeeds. Downloads are not resumable — one that fails or times out starts over.
*
diff --git a/src/main/java/org/metricshub/winrm/RemoteFiles.java b/src/main/java/org/metricshub/winrm/RemoteFiles.java
index 9762ac7..8e143a6 100644
--- a/src/main/java/org/metricshub/winrm/RemoteFiles.java
+++ b/src/main/java/org/metricshub/winrm/RemoteFiles.java
@@ -38,15 +38,17 @@
/**
* The remote file access primitive: small PowerShell scripts that open a remote file and write
- * what the caller asked for as base64 lines on stdout, and the {@link InputStream} that
- * decodes those lines as the output chunks arrive. The metadata scripts ({@link #infoScript},
- * {@link #listScript}) write one ASCII record per entry instead, parsed by {@link #parseEntry}
- * and {@link #parseInaccessible}: plain integers, and only the path base64-encoded.
+ * what the caller asked for (its bytes, its digest) raw on stdout, and the
+ * {@link InputStream} that passes those bytes on as the output chunks arrive. The metadata scripts
+ * ({@link #infoScript}, {@link #listScript}) write one ASCII record per entry instead, parsed by
+ * {@link #parseEntry} and {@link #parseInaccessible}: plain integers, and only the path
+ * base64-encoded.
*
- * Base64 is what makes a text console a binary-safe channel: every byte value survives, and the
- * output is pure ASCII, so recovering the exact bytes never depends on the remote console code
- * page. Each line is an independent base64 block, decoded on its own. The caller-supplied path is
- * embedded base64-encoded too (UTF-8), so no path ever needs quoting or escaping.
+ * The bytes are written to {@code [Console]::OpenStandardOutput()}, a stream no text writer, code
+ * page or byte order mark touches, and the WinRM service forwards a command's stdout as it is:
+ * every byte value arrives as written (verified on Windows Server 2008 R2 with PowerShell 2.0,
+ * 2019 and 2022). The caller-supplied path is embedded base64-encoded (UTF-8), so no path ever
+ * needs quoting or escaping.
*
* Failures are reported by the script's exit code, checked when the output ends (see the
* {@code EXIT_*} constants), and turned into a {@link WinRMClientException} naming the cause.
@@ -77,14 +79,11 @@ private RemoteFiles() {}
static final int EXIT_COMMAND_NOT_FOUND = 9009;
/**
- * Bytes read and written per base64 line: 49,149, a multiple of 3 so a full block encodes
- * without padding, into 65,532 characters — with the newline, a 65,533-byte line that fits in
- * exactly two of the WinRM service's output reads. The service's shell plugin reads a
- * command's stdout pipe with at most one 32 KiB read per timer tick (64 per second at the
- * default 15.625 ms), so each line should fill whole reads: a 57 KiB block (a 77,825-byte
- * line) needs three, and is measurably slower.
+ * Bytes read and written per block: 64 KiB, exactly two of the WinRM service's output reads.
+ * The service's shell plugin reads a command's stdout pipe with at most one 32 KiB read per
+ * timer tick (64 per second at the default 15.625 ms), so each write should fill whole reads.
*/
- static final int BLOCK_SIZE = 49_149;
+ static final int BLOCK_SIZE = 64 * 1024;
/**
* The hash algorithms {@link #digestScript(String, String)} accepts: the names .NET's
@@ -193,12 +192,12 @@ private RemoteFiles() {}
/**
* Read a byte range: resolve a negative offset from the size of the open stream (so a
* growing log is tailed from its current end), seek, and write at most {@code length} bytes
- * ({@code -1}: to the end) as one base64 line per block. Seeking past the end is legal and
- * reads nothing. {@code %d} are the offset, the length and the block size.
+ * ({@code -1}: to the end), block by block. Seeking past the end is legal and reads nothing.
+ * {@code %d} are the offset, the length and the block size.
*
- * Each line goes to the raw stdout stream as ONE write: the WinRM service reads the pipe at
+ * Each block goes to the raw stdout stream as ONE write: the WinRM service reads the pipe at
* most once per timer tick, taking only what is in the pipe, and {@code [Console]::Out} (an
- * auto-flushing writer with a small buffer) cuts a line into 256-byte writes that leave each
+ * auto-flushing writer with a small buffer) cuts its output into 256-byte writes that leave each
* read with about 4 KiB — 5 to 6 times slower (measured on Windows 2008 R2 and 2022).
*/
private static final String READ_RANGE = "try{$n=[long]%d;$l=[long]%d;" +
@@ -206,24 +205,23 @@ private RemoteFiles() {}
"$f.Position=$n;$b=New-Object byte[] %d;$o=[Console]::OpenStandardOutput();" +
"while($l -ne 0){$c=$b.Length;if($l -gt 0 -and $l -lt $c){$c=[int]$l};" +
"$r=$f.Read($b,0,$c);if($r -le 0){break};" +
- "$a=[Text.Encoding]::ASCII.GetBytes([Convert]::ToBase64String($b,0,$r)+\"`n\");" +
- "$o.Write($a,0,$a.Length);if($l -gt 0){$l-=$r}};" +
+ "$o.Write($b,0,$r);if($l -gt 0){$l-=$r}};" +
"$o.Flush()}catch{fail $_.Exception}finally{$f.Close()}";
- /** Hash the whole file and write the raw digest as one base64 line. {@code %s} is the algorithm. */
+ /** Hash the whole file and write the raw digest. {@code %s} is the algorithm. */
private static final String DIGEST = "try{$h=[Security.Cryptography.HashAlgorithm]::Create('%s');" +
- "[Console]::Out.WriteLine([Convert]::ToBase64String($h.ComputeHash($f)))}catch{fail $_.Exception}finally{$f.Close()}";
+ "$y=$h.ComputeHash($f);$o=[Console]::OpenStandardOutput();$o.Write($y,0,$y.Length)}" +
+ "catch{fail $_.Exception}finally{$f.Close()}";
/**
- * What a download is verified against, as one base64 line of {@link #PROBE_LENGTH} bytes: the
- * number of bytes hashed (8 bytes, little-endian), then their SHA-256 digest. The count is the
- * stream position after hashing, not the file length read beforehand, so both describe the same
- * bytes even when the file changes during the probe.
+ * What a download is verified against, {@link #PROBE_LENGTH} raw bytes: the number of bytes
+ * hashed (8 bytes, little-endian), then their SHA-256 digest. The count is the stream position
+ * after hashing, not the file length read beforehand, so both describe the same bytes even when
+ * the file changes during the probe.
*/
- private static final String PROBE = "try{$h=[Security.Cryptography.HashAlgorithm]::Create('SHA256');$d=$h.ComputeHash($f);"
- +
- "[Console]::Out.WriteLine([Convert]::ToBase64String([byte[]]([BitConverter]::GetBytes($f.Position)+$d)))}" +
- "catch{fail $_.Exception}finally{$f.Close()}";
+ private static final String PROBE = "try{$h=[Security.Cryptography.HashAlgorithm]::Create('SHA256');" +
+ "$d=$h.ComputeHash($f);$y=[byte[]]([BitConverter]::GetBytes($f.Position)+$d);" +
+ "$o=[Console]::OpenStandardOutput();$o.Write($y,0,$y.Length)}catch{fail $_.Exception}finally{$f.Close()}";
/** The length of the {@link #probeScript(String)} output: an 8-byte size and a 32-byte SHA-256 digest. */
static final int PROBE_LENGTH = 40;
@@ -432,19 +430,19 @@ private static WinRMClientException malformed(final String line, final Throwable
}
/**
- * Start the script and return the stream of the bytes it writes. The first line is fetched
+ * Start the script and return the stream of the bytes it writes. The first bytes are fetched
* before returning, so a file that cannot be opened fails here, not on the first read.
*
* @param client the client to run the script on
* @param path the remote file, for the error messages
- * @param script the script, from {@link #readScript} or {@link #digestScript}
+ * @param script the script, from {@link #readScript}, {@link #digestScript} or {@link #probeScript}
* @param timeout the inactivity timeout of the stream
- * @return the decoded stream; it must be closed
+ * @return the stream; it must be closed
*/
static InputStream open(final WinRMClient client, final String path, final String script, final Duration timeout) {
final RemoteProcess process = start(client, path, script, timeout);
try {
- return new DecodingStream(process, path, client.hostname());
+ return new ContentStream(process, path, client.hostname());
} catch (final RuntimeException e) {
process.close();
throw e;
@@ -641,23 +639,21 @@ static WinRMClientException failure(
}
/**
- * The bytes a script writes, decoded line by line as the output arrives: memory is bounded by
- * one line, not by the file. The exit code is checked when the output ends, so a failure
- * midway is reported instead of looking like a short file.
+ * The bytes a script writes, passed on chunk by chunk as the output arrives: memory is bounded
+ * by one protocol response, not by the file. The exit code is checked when the output ends, so
+ * a failure midway is reported instead of looking like a short file.
*/
- private static final class DecodingStream extends InputStream {
+ private static final class ContentStream extends InputStream {
private final RemoteProcess process;
- private final BufferedReader stdout;
private final String path;
private final String hostname;
private byte[] block = new byte[0];
private int position;
private boolean ended;
- private DecodingStream(final RemoteProcess process, final String path, final String hostname) {
+ private ContentStream(final RemoteProcess process, final String path, final String hostname) {
this.process = process;
- this.stdout = process.stdout();
this.path = path;
this.hostname = hostname;
fill();
@@ -690,35 +686,16 @@ public int available() {
/** Make sure unread bytes are buffered: {@code false} at the end of a successful output. */
private boolean fill() {
- while (position == block.length) {
- if (ended) {
- return false;
- }
- final String line = readLine(stdout);
- if (line == null) {
+ if (position == block.length && !ended) {
+ final byte[] chunk = process.readStdoutChunk();
+ if (chunk == null) {
end();
- return false;
- }
- final String data = line.strip();
- if (data.isEmpty()) {
- continue;
- }
- try {
- block = Base64.getDecoder().decode(data);
- } catch (final IllegalArgumentException e) {
- throw new WinRMClientException(
- String.format(
- "Unexpected output while reading remote file %s on %s: %s",
- path,
- hostname,
- data.length() > 80 ? data.substring(0, 80) + "..." : data
- ),
- e
- );
+ } else {
+ block = chunk;
+ position = 0;
}
- position = 0;
}
- return true;
+ return position < block.length;
}
/** The output ended: collect the exit code, release the connection, report a failure. */
diff --git a/src/main/java/org/metricshub/winrm/RemoteProcess.java b/src/main/java/org/metricshub/winrm/RemoteProcess.java
index 357449c..6d8eba4 100644
--- a/src/main/java/org/metricshub/winrm/RemoteProcess.java
+++ b/src/main/java/org/metricshub/winrm/RemoteProcess.java
@@ -29,6 +29,7 @@
import java.io.Writer;
import java.nio.charset.Charset;
import java.time.Duration;
+import java.util.ArrayDeque;
import java.util.concurrent.TimeoutException;
import org.metricshub.winrm.exceptions.WinRMTimeoutException;
import org.metricshub.winrm.exceptions.WindowsRemoteException;
@@ -98,6 +99,10 @@ public final class RemoteProcess implements AutoCloseable {
private final StringBuilder stdoutPending = new StringBuilder();
private final StringBuilder stderrPending = new StringBuilder();
+ // Undecoded stdout chunks not read yet, once readStdoutChunk() took stdout as raw bytes: null
+ // while stdout is decoded text.
+ private ArrayDeque stdoutChunks;
+
// Written input that has not been flushed to the host yet.
private final StringBuilder stdinPending = new StringBuilder();
@@ -166,6 +171,26 @@ public BufferedReader stderr() {
return stderr;
}
+ /**
+ * Read the next chunk of the standard output as raw bytes, bypassing the charset decoder: for
+ * output that is not text, like the content of a remote file (see {@link RemoteFiles}). The
+ * first call switches stdout to raw bytes for good, so make it before anything reads or waits
+ * for output; the {@link #stdout()} reader then stays empty.
+ *
+ * @return the bytes as they arrived, never empty, or {@code null} at the end of the output
+ * @throws WinRMTimeoutException when the command stays silent for a whole inactivity timeout
+ * @throws org.metricshub.winrm.exceptions.WinRMClientException for any other failure
+ */
+ synchronized byte[] readStdoutChunk() {
+ if (stdoutChunks == null) {
+ stdoutChunks = new ArrayDeque<>();
+ }
+ while (stdoutChunks.isEmpty() && !finished) {
+ fetchOnce();
+ }
+ return stdoutChunks.poll();
+ }
+
/**
* Get the standard input of the remote command. Written text is buffered locally until
* {@code flush()}, which carries it to the host as one WSMan Send (encoded with the request's
@@ -410,7 +435,11 @@ private void absorb(final CommandCursor.Chunk chunk) {
stdoutPending.append(stdoutDecoder.finish());
stderrPending.append(stderrDecoder.finish());
} else {
- stdoutPending.append(stdoutDecoder.decode(chunk.stdout()));
+ if (stdoutChunks == null) {
+ stdoutPending.append(stdoutDecoder.decode(chunk.stdout()));
+ } else if (chunk.stdout().length > 0) {
+ stdoutChunks.add(chunk.stdout());
+ }
stderrPending.append(stderrDecoder.decode(chunk.stderr()));
}
}
diff --git a/src/site/markdown/cli.md b/src/site/markdown/cli.md
index 6c32d4b..2ca8397 100644
--- a/src/site/markdown/cli.md
+++ b/src/site/markdown/cli.md
@@ -310,7 +310,7 @@ there, or run the redirection in cmd.exe. PowerShell 7.4 and later keep the byte
When the output is closed early — `... cat 'D:\logs\huge.log' | head` — `cat` stops the remote
read instead of transferring the rest of the file for nobody, and exits with `74`. The transfer
-runs at about 1.5 MB/s (see [Read performance](files.html#read-performance)): logs and
+runs at about 2 MB/s (see [Read performance](files.html#read-performance)): logs and
configuration files, not bulk data.
### `get`
@@ -319,8 +319,8 @@ configuration files, not bulk data.
[digest-verified, atomic, and skipped when the local copy is already identical](file-transfers.html#downloading-a-file).
Without a local path, the file is written in the current directory under its remote name; an
existing directory receives it under its remote name too. Nothing is printed on success.
-`--timeout` is the deadline of the whole download: at about 1.5 MB/s, the default 60 seconds
-covers files up to about 80 MB — raise it for larger ones.
+`--timeout` is the deadline of the whole download: at about 1.8 MB/s, the default 60 seconds
+covers files up to about 100 MB — raise it for larger ones.
### Quoting remote paths
diff --git a/src/site/markdown/file-transfers.md b/src/site/markdown/file-transfers.md
index 27567c3..d614f1d 100644
--- a/src/site/markdown/file-transfers.md
+++ b/src/site/markdown/file-transfers.md
@@ -243,7 +243,7 @@ reads need — PowerShell 2.0 or later in `FullLanguage` mode, and read access t
The timeout of a download is a **wall-clock deadline for the whole transfer**: the client's
timeout for `downloadFile(...)` (30 seconds by default), or `timeout(Duration)` on the request.
-A large file needs a raised timeout — at the speed below, 30 seconds is about 40 MB. When the
+A large file needs a raised timeout — at the speed below, 30 seconds is about 50 MB. When the
deadline fires, the exception says how far the transfer got:
```text
@@ -257,9 +257,9 @@ place lets the move complete, and the download succeeds.
### Download performance
-Measured over HTTP with NTLM encryption, a download runs at about **1.35–1.45 MB/s**: 20 MiB in
-14.7 seconds on Windows Server 2022, 15.6 seconds on Windows Server 2008 R2 (PowerShell 2.0), and
-64 MiB in 46 seconds on 2022 — probe included. A small file costs under a second (two PowerShell
+Measured over HTTP with NTLM encryption, a download runs at about **1.7–1.9 MB/s**: 20 MiB in
+12 seconds on Windows Server 2022 and 2008 R2 (PowerShell 2.0), 13 seconds on 2019, and 64 MiB in
+35 seconds on 2022 — probe included. A small file costs under a second (two PowerShell
invocations: the probe and the read), and skipping an identical copy about 0.4 seconds. The limit
is on the host, in the way the WinRM service forwards a command's output — see
[Read performance](files.html#read-performance).
diff --git a/src/site/markdown/files.md b/src/site/markdown/files.md
index 4113fea..ec2272a 100644
--- a/src/site/markdown/files.md
+++ b/src/site/markdown/files.md
@@ -7,7 +7,7 @@ description: How the WinRM Java Client reads files and lists directories on the
WinRM has no file-access operation of its own (nothing like SFTP's `READ`), so the client reads
remote files and lists directories **through the WinRM command shell**: a small PowerShell script
-does the work on the host and writes the result in an encoding-proof form, and the client decodes
+does the work on the host and writes the result in an encoding-proof form, and the client reads
it as it arrives. No SMB, no extra port, no share. This is the reverse direction of
[File Transfers](file-transfers.html). The standalone jar exposes it as the `ls`, `stat`, `cat`,
and `get` subcommands — see the [Command-Line Client](cli.html#remote-files) manual.
@@ -70,11 +70,11 @@ try (BufferedReader reader = client.file("D:\\logs\\huge.log").openReader(Standa
}
```
-The stream decodes the content block by block as it arrives (48 KiB per block): memory stays
-bounded whatever the file size. Like a [`RemoteProcess`](commands.html), **it must be closed** —
-it holds the client's connection until it reaches its end or is closed, and closing it early stops
-the remote read. The file is opened before `openStream()` returns, so a missing file fails there;
-a failure midway is reported by `read()`, never as a silently short read.
+The stream passes the content on as it arrives: memory stays bounded whatever the file size.
+Like a [`RemoteProcess`](commands.html), **it must be closed** — it holds the client's connection
+until it reaches its end or is closed, and closing it early stops the remote read. The file is
+opened before `openStream()` returns, so a missing file fails there; a failure midway is reported
+by `read()`, never as a silently short read.
## Downloading to a local file
@@ -115,10 +115,12 @@ operation. See [Timeouts and Errors](timeouts-and-errors.html).
* The host needs **PowerShell 2.0 or later in `FullLanguage` mode** (Windows Server 2008 R2 and
later ship it). When AppLocker or WDAC puts PowerShell in Constrained Language Mode, the .NET
calls the scripts rely on are blocked: remote file access is then not available.
-* Non-ASCII paths and content are safe: the path travels base64-encoded (UTF-8) inside the script
- and the content comes back base64-encoded, so neither depends on the remote console code page.
+* Non-ASCII paths and binary content are safe: the path travels base64-encoded (UTF-8) inside the
+ script, and the content comes back as the file's raw bytes, written straight to the standard
+ output stream, past any text conversion. Neither depends on the remote console code page: every
+ byte value arrives as stored, verified on Windows Server 2008 R2 (PowerShell 2.0), 2019 and 2022.
* Remote file access never writes anything on the host: its scripts travel on the command line,
- which limits the path to about **1,450 characters** for a read, **1,150** for `info()`, and
+ which limits the path to about **1,500 characters** for a read, **1,150** for `info()`, and
**450** for `list()`, whose walker script is the largest (roughly half as many with accented
letters, a third with CJK characters) — all above the classic 260-character `MAX_PATH`. A
longer path fails with a `WinRMClientException` before anything is sent.
@@ -126,17 +128,18 @@ operation. See [Timeouts and Errors](timeouts-and-errors.html).
## Read performance
The mechanism is designed for **configuration files, logs and small data files — not bulk data**.
-Measured over HTTP with NTLM encryption, `openStream()` reads about **1.4–1.5 MB/s**: a 20 MiB
-file in 14–15 seconds, on Windows Server 2008 R2 (PowerShell 2.0), 2019 and 2022 (PowerShell 5.1)
-alike. A small file costs about a second, mostly the PowerShell startup. For gigabytes, use SMB.
+Measured over HTTP with NTLM encryption, `openStream()` reads about **1.8–2.0 MB/s**: a 20 MiB
+file in 10.6–12 seconds, on Windows Server 2008 R2 (PowerShell 2.0), 2019 and 2022
+(PowerShell 5.1) alike. A small file costs about a second, mostly the PowerShell startup. For
+gigabytes, use SMB.
The limit is on the host, not in the network or the client: the WinRM service reads a command's
output pipe itself, **at most 32 KiB per read and about 60 reads per second** per output stream —
-about 2 MB/s of output whatever the command. The reader is shaped for it: each block of the file
-travels as one 65,533-byte base64 line that fills exactly two reads (a larger 77,825-byte line
-needing three reads was 20–25% slower). Neither the client's `MaxEnvelopeSize` nor the host's
-`MaxEnvelopeSizekb` changes the limit — raising them only makes each response larger and
-proportionally slower to come.
+about 2 MB/s of output whatever the command. The reader runs at that limit: the host writes the
+file's raw bytes 64 KiB at a time, so every read is a full one and carries nothing but content
+(base64 text would carry only 3 bytes of the file in 4, and measured a third slower). Neither the
+client's `MaxEnvelopeSize` nor the host's `MaxEnvelopeSizekb` changes the limit — raising them
+only makes each response larger and proportionally slower to come.
The same limit applies to any command output: a command that writes its output in small pieces —
line by line, like `Write-Output` or `type` — gets only what accumulated in the 4 KiB pipe at each
diff --git a/src/test/java/org/metricshub/winrm/RemoteFileTest.java b/src/test/java/org/metricshub/winrm/RemoteFileTest.java
index 19b1fea..f815054 100644
--- a/src/test/java/org/metricshub/winrm/RemoteFileTest.java
+++ b/src/test/java/org/metricshub/winrm/RemoteFileTest.java
@@ -64,9 +64,9 @@
import org.metricshub.winrm.light.FakeWsmanServer;
/**
- * Client-side tests of {@link RemoteFile} against {@link FakeWsmanServer}: the scripted
- * base64-line output is decoded incrementally, exit codes become the documented exceptions, and
- * the request settings reach the script. The scripts themselves run in
+ * Client-side tests of {@link RemoteFile} against {@link FakeWsmanServer}: the scripted raw
+ * output is passed on as it arrives, exit codes become the documented exceptions, and the request
+ * settings reach the script. The scripts themselves run in
* {@link RemoteFilesScriptTest}.
*/
class RemoteFileTest {
@@ -102,18 +102,22 @@ private static String b64(final byte[] bytes) {
return Base64.getEncoder().encodeToString(bytes);
}
- private static String stdout(final String text) {
- return stream("stdout", COMMAND_ID, text.getBytes(StandardCharsets.US_ASCII));
+ private static String stdout(final byte[] bytes) {
+ return stream("stdout", COMMAND_ID, bytes);
+ }
+
+ private static byte[] ascii(final String text) {
+ return text.getBytes(StandardCharsets.US_ASCII);
}
/** Script a whole read: shell creation, command, the given Receive chunks (the last one completes), Signal. */
- private void enqueueRead(final int exitCode, final String stderr, final String... chunks) {
+ private void enqueueRead(final int exitCode, final String stderr, final byte[]... chunks) {
server.enqueue(200, envelope(resourceCreated("SHELL-1")));
enqueueCommand(exitCode, stderr, chunks);
}
/** Script a command on the existing shell: command, the given Receive chunks (the last one completes), Signal. */
- private void enqueueCommand(final int exitCode, final String stderr, final String... chunks) {
+ private void enqueueCommand(final int exitCode, final String stderr, final byte[]... chunks) {
server.enqueue(200, envelope(commandResponse(COMMAND_ID)));
for (int i = 0; i < chunks.length; i++) {
final boolean last = i == chunks.length - 1;
@@ -144,21 +148,18 @@ private List sentScripts() {
}
/** The probe output announcing the given content: its size (8 bytes, little-endian), then its SHA-256 digest. */
- private static String probe(final byte[] content) throws Exception {
+ private static byte[] probe(final byte[] content) throws Exception {
return probe(content.length, content);
}
/** The probe output announcing the given size and the SHA-256 digest of the given content. */
- private static String probe(final long size, final byte[] content) throws Exception {
- return b64(
- ByteBuffer
- .allocate(RemoteFiles.PROBE_LENGTH)
- .order(ByteOrder.LITTLE_ENDIAN)
- .putLong(size)
- .put(MessageDigest.getInstance("SHA-256").digest(content))
- .array()
- ) +
- "\r\n";
+ private static byte[] probe(final long size, final byte[] content) throws Exception {
+ return ByteBuffer
+ .allocate(RemoteFiles.PROBE_LENGTH)
+ .order(ByteOrder.LITTLE_ENDIAN)
+ .putLong(size)
+ .put(MessageDigest.getInstance("SHA-256").digest(content))
+ .array();
}
/** The files in the directory, sorted. */
@@ -169,15 +170,14 @@ private static List files(final Path directory) throws IOException {
}
@Test
- void readBytesDecodesLinesSplitAcrossReceiveChunks() {
+ void readBytesJoinsTheChunksWhereverTheyAreCut() {
final byte[] content = new byte[300];
for (int i = 0; i < content.length; i++) {
content[i] = (byte) i;
}
- final String line1 = b64(Arrays.copyOfRange(content, 0, 150));
- final String line2 = b64(Arrays.copyOfRange(content, 150, 300));
- // The first line is cut in the middle by the protocol chunking.
- enqueueRead(0, null, line1.substring(0, 17), line1.substring(17) + "\r\n" + line2 + "\r\n");
+ // One block written by the host, cut in the middle by the protocol chunking, with an empty
+ // Receive in between.
+ enqueueRead(0, null, Arrays.copyOfRange(content, 0, 17), new byte[0], Arrays.copyOfRange(content, 17, 300));
try (WinRMClient client = client()) {
assertArrayEquals(content, client.file(PATH).readBytes());
@@ -191,7 +191,7 @@ void readBytesDecodesLinesSplitAcrossReceiveChunks() {
@Test
void rangeSettingsReachTheScript() {
- enqueueRead(0, null, b64("tail".getBytes(StandardCharsets.US_ASCII)) + "\r\n");
+ enqueueRead(0, null, ascii("tail"));
try (WinRMClient client = client()) {
assertEquals("tail", client.file(PATH).offset(-8192).length(1024).readText(StandardCharsets.US_ASCII));
}
@@ -200,7 +200,7 @@ void rangeSettingsReachTheScript() {
@Test
void openStreamHasNoCapAndReadsToTheEnd() throws Exception {
- enqueueRead(0, null, b64(new byte[] { 1, 2, 3 }) + "\r\n", b64(new byte[] { 4, 5 }) + "\r\n");
+ enqueueRead(0, null, new byte[] { 1, 2, 3 }, new byte[] { 4, 5 });
try (WinRMClient client = client(); InputStream in = client.file(PATH).openStream()) {
assertArrayEquals(new byte[] { 1, 2, 3, 4, 5 }, in.readAllBytes());
}
@@ -208,17 +208,16 @@ void openStreamHasNoCapAndReadsToTheEnd() throws Exception {
}
@Test
- void readTextKeepsTheBomAndOpenReaderDecodesACharacterSplitAcrossLines() throws Exception {
- final byte[] bom = { (byte) 0xEF, (byte) 0xBB, (byte) 0xBF };
- enqueueRead(0, null, b64(bom) + "\r\n" + b64("é".getBytes(StandardCharsets.UTF_8)) + "\r\n");
+ void readTextKeepsTheBomAndOpenReaderDecodesACharacterSplitAcrossChunks() throws Exception {
+ enqueueRead(0, null, new byte[] { (byte) 0xEF, (byte) 0xBB, (byte) 0xBF, (byte) 0xC3, (byte) 0xA9 });
try (WinRMClient client = client()) {
assertEquals("\uFEFFé", client.file(PATH).readText(StandardCharsets.UTF_8));
}
- // "é" is C3 A9: its two bytes arrive on two separate lines (two transfer blocks).
+ // "é" is C3 A9: its two bytes arrive in two separate chunks.
server.close();
server = new FakeWsmanServer(DOMAIN, USER, PASSWORD);
- enqueueRead(0, null, b64(new byte[] { 'a', (byte) 0xC3 }) + "\r\n", b64(new byte[] { (byte) 0xA9, 'b' }) + "\r\n");
+ enqueueRead(0, null, new byte[] { 'a', (byte) 0xC3 }, new byte[] { (byte) 0xA9, 'b' });
try (WinRMClient client = client(); BufferedReader reader = client.file(PATH).openReader(StandardCharsets.UTF_8)) {
assertEquals("aéb", reader.readLine());
}
@@ -226,7 +225,7 @@ void readTextKeepsTheBomAndOpenReaderDecodesACharacterSplitAcrossLines() throws
@Test
void theSizeCapIsSentToTheHostAndEnforced() {
- enqueueRead(0, null, b64(new byte[] { 1, 2, 3, 4, 5 }) + "\r\n");
+ enqueueRead(0, null, new byte[] { 1, 2, 3, 4, 5 });
try (WinRMClient client = client()) {
final WinRMClientException e = assertThrows(
WinRMClientException.class,
@@ -239,7 +238,7 @@ void theSizeCapIsSentToTheHostAndEnforced() {
@Test
void aMissingFileFailsWhenTheStreamIsOpened() {
- enqueueRead(RemoteFiles.EXIT_NOT_FOUND, "Could not find file", "");
+ enqueueRead(RemoteFiles.EXIT_NOT_FOUND, "Could not find file", new byte[0]);
try (WinRMClient client = client()) {
final RemoteFile file = client.file(PATH);
final WinRMClientException e = assertThrows(WinRMClientException.class, file::openStream);
@@ -262,7 +261,7 @@ void exitCodesBecomeExplicitFailures() {
void anExactLengthReadStillChecksTheExitCode() {
// The host sends exactly the requested bytes, then fails (e.g. while closing the file):
// readBytes() must drain to the end of the output instead of returning a full buffer.
- enqueueRead(1, "The device is not ready", b64(new byte[] { 1, 2, 3 }) + "\r\n");
+ enqueueRead(1, "The device is not ready", new byte[] { 1, 2, 3 });
try (WinRMClient client = client()) {
final RemoteFile file = client.file(PATH).length(3);
final WinRMClientException e = assertThrows(WinRMClientException.class, file::readBytes);
@@ -273,18 +272,18 @@ void anExactLengthReadStillChecksTheExitCode() {
@Test
void aPathTooLongForTheCommandLineIsRefusedBeforeAnythingIsSent() {
try (WinRMClient client = client()) {
- final RemoteFile file = client.file("C:\\" + "a".repeat(1500));
+ final RemoteFile file = client.file("C:\\" + "a".repeat(1550));
final WinRMClientException e = assertThrows(WinRMClientException.class, file::readBytes);
assertTrue(e.getMessage().contains("too long"), e.getMessage());
// Just under the limit still fits: the script is not uploaded as a file.
- assertTrue(CommandRequest.encodePowerShell(RemoteFiles.readScript("C:\\" + "a".repeat(1450), -8192, 1)) != null);
+ assertTrue(CommandRequest.encodePowerShell(RemoteFiles.readScript("C:\\" + "a".repeat(1500), -8192, 1)) != null);
}
assertEquals(0, server.decryptedRequests().size());
}
@Test
void aFailureMidwayIsNotASilentlyShortRead() throws Exception {
- enqueueRead(1, "The device is not ready", b64(new byte[] { 1, 2, 3 }) + "\r\n");
+ enqueueRead(1, "The device is not ready", new byte[] { 1, 2, 3 });
try (WinRMClient client = client(); InputStream in = client.file(PATH).openStream()) {
assertEquals(1, in.read());
final WinRMClientException e = assertThrows(WinRMClientException.class, in::readAllBytes);
@@ -292,18 +291,9 @@ void aFailureMidwayIsNotASilentlyShortRead() throws Exception {
}
}
- @Test
- void unexpectedOutputIsReported() {
- enqueueRead(0, null, "WARNING: not base64!\r\n");
- try (WinRMClient client = client()) {
- final WinRMClientException e = assertThrows(WinRMClientException.class, () -> client.file(PATH).readBytes());
- assertTrue(e.getMessage().contains("Unexpected output"), e.getMessage());
- }
- }
-
@Test
void digestIsLowercaseHex() {
- enqueueRead(0, null, b64(new byte[] { (byte) 0xAB, 0x01, (byte) 0xFF }) + "\r\n");
+ enqueueRead(0, null, new byte[] { (byte) 0xAB, 0x01, (byte) 0xFF });
try (WinRMClient client = client()) {
assertEquals("ab01ff", client.file(PATH).digest("SHA256"));
}
@@ -343,7 +333,7 @@ void aStartRejectedByTheOperationQuotaIsRetried() {
500,
fault("2150859174", "The maximum number of concurrent operations for this user has been exceeded.")
);
- enqueueCommand(0, null, b64("ok".getBytes(StandardCharsets.US_ASCII)) + "\r\n");
+ enqueueCommand(0, null, ascii("ok"));
try (WinRMClient client = client()) {
assertEquals("ok", client.file(PATH).readText(StandardCharsets.US_ASCII));
}
@@ -374,7 +364,7 @@ void downloadReplacesADifferentLocalFile(@TempDir final Path directory) throws E
final byte[] content = "the new content".getBytes(StandardCharsets.US_ASCII);
final Path local = Files.write(directory.resolve("copy.bin"), "previous".getBytes(StandardCharsets.US_ASCII));
enqueueRead(0, null, probe(content));
- enqueueCommand(0, null, b64(content) + "\r\n");
+ enqueueCommand(0, null, content);
try (WinRMClient client = client()) {
assertEquals(content.length, client.downloadFile(PATH, local));
}
@@ -390,7 +380,7 @@ void aReplacedFileKeepsItsPosixPermissions(@TempDir final Path directory) throws
Files.setPosixFilePermissions(local, ownerOnly);
final byte[] content = "password=new".getBytes(StandardCharsets.US_ASCII);
enqueueRead(0, null, probe(content));
- enqueueCommand(0, null, b64(content) + "\r\n");
+ enqueueCommand(0, null, content);
try (WinRMClient client = client()) {
client.downloadFile(PATH, local);
}
@@ -414,7 +404,7 @@ void aStreamPathIsNotDownloadedIntoADirectory(@TempDir final Path directory) {
void aSizeMismatchFailsEvenWhenTheDigestMatches(@TempDir final Path directory) throws Exception {
final byte[] content = { 1, 2, 3 };
enqueueRead(0, null, probe(999, content));
- enqueueCommand(0, null, b64(content) + "\r\n");
+ enqueueCommand(0, null, content);
final Path local = directory.resolve("copy.bin");
try (WinRMClient client = client()) {
final RemoteFile file = client.file(PATH);
@@ -431,7 +421,7 @@ void aLongDestinationNameStillFitsTheStagingFile(@TempDir final Path directory)
final byte[] content = { 1, 2, 3 };
final Path local = directory.resolve("a".repeat(240) + ".bin");
enqueueRead(0, null, probe(content));
- enqueueCommand(0, null, b64(content) + "\r\n");
+ enqueueCommand(0, null, content);
try (WinRMClient client = client()) {
assertEquals(3, client.downloadFile(PATH, local));
}
@@ -461,12 +451,7 @@ void downloadWritesTheVerifiedContent(@TempDir final Path directory) throws Exce
content[i] = (byte) i;
}
enqueueRead(0, null, probe(content));
- enqueueCommand(
- 0,
- null,
- b64(Arrays.copyOfRange(content, 0, 150)) + "\r\n",
- b64(Arrays.copyOfRange(content, 150, 300)) + "\r\n"
- );
+ enqueueCommand(0, null, Arrays.copyOfRange(content, 0, 150), Arrays.copyOfRange(content, 150, 300));
final Path local = directory.resolve("copy.bin");
try (WinRMClient client = client()) {
// The range settings do not apply: the whole file is downloaded.
@@ -496,7 +481,7 @@ void anIdenticalLocalFileIsNotDownloadedAgain(@TempDir final Path directory) thr
void aDirectoryDestinationReceivesTheRemoteFileName(@TempDir final Path directory) throws Exception {
final byte[] content = { 1, 2, 3 };
enqueueRead(0, null, probe(content));
- enqueueCommand(0, null, b64(content) + "\r\n");
+ enqueueCommand(0, null, content);
try (WinRMClient client = client()) {
assertEquals(3, client.downloadFile(PATH, directory));
}
@@ -507,7 +492,7 @@ void aDirectoryDestinationReceivesTheRemoteFileName(@TempDir final Path director
void aDigestMismatchFailsWithoutWritingTheDestination(@TempDir final Path directory) throws Exception {
// The file changed between the probe and the read.
enqueueRead(0, null, probe("expected".getBytes(StandardCharsets.US_ASCII)));
- enqueueCommand(0, null, b64("modified".getBytes(StandardCharsets.US_ASCII)) + "\r\n");
+ enqueueCommand(0, null, ascii("modified"));
final Path local = directory.resolve("copy.bin");
try (WinRMClient client = client()) {
final RemoteFile file = client.file(PATH);
@@ -519,7 +504,7 @@ void aDigestMismatchFailsWithoutWritingTheDestination(@TempDir final Path direct
@Test
void aMalformedProbeIsReported(@TempDir final Path directory) {
- enqueueRead(0, null, b64(new byte[] { 1, 2, 3 }) + "\r\n");
+ enqueueRead(0, null, new byte[] { 1, 2, 3 });
try (WinRMClient client = client()) {
final RemoteFile file = client.file(PATH);
final WinRMClientException e = assertThrows(WinRMClientException.class, () -> file.downloadTo(directory));
@@ -548,10 +533,10 @@ void aTimeoutMidTransferLeavesTheDestinationAsItWas(@TempDir final Path director
enqueueRead(0, null, probe(content));
server
.enqueue(200, envelope(commandResponse(COMMAND_ID)))
- .enqueue(200, envelope(receiveResponse(stdout(b64(new byte[600]) + "\r\n"), null)))
+ .enqueue(200, envelope(receiveResponse(stdout(new byte[600]), null)))
.enqueueDelayed(
200,
- envelope(receiveResponse(stdout(b64(new byte[400]) + "\r\n"), done(COMMAND_ID, 0))),
+ envelope(receiveResponse(stdout(new byte[400]), done(COMMAND_ID, 0))),
4_000
)
.enqueue(200, envelope(signalResponse()));
diff --git a/src/test/java/org/metricshub/winrm/RemoteFilesScriptTest.java b/src/test/java/org/metricshub/winrm/RemoteFilesScriptTest.java
index 78b3cfe..ebac9c2 100644
--- a/src/test/java/org/metricshub/winrm/RemoteFilesScriptTest.java
+++ b/src/test/java/org/metricshub/winrm/RemoteFilesScriptTest.java
@@ -25,7 +25,6 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
-import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
@@ -53,14 +52,14 @@
/**
* Runs the PowerShell scripts of {@link RemoteFiles} in the local {@code powershell.exe},
- * exactly as the remote shell would ({@code -EncodedCommand}), and decodes their output the way
- * the client does: the actual script semantics (seek, negative offsets, share mode, exit codes)
- * are only testable against a real PowerShell. Windows only.
+ * exactly as the remote shell would ({@code -EncodedCommand}), and reads their output the way the
+ * client does: the actual script semantics (seek, negative offsets, share mode, exit codes, raw
+ * output) are only testable against a real PowerShell. Windows only.
*/
@EnabledOnOs(OS.WINDOWS)
class RemoteFilesScriptTest {
- /** Every byte value, then enough random bytes to span several transfer blocks. */
+ /** Every byte value, then enough random bytes to span several transfer blocks, ending with a CR. */
private static byte[] content;
@TempDir
@@ -77,11 +76,12 @@ static void createFile() throws Exception {
final byte[] random = new byte[content.length - 256];
new Random(42).nextBytes(random);
System.arraycopy(random, 0, content, 256, random.length);
+ content[content.length - 1] = '\r';
file = directory.resolve("données-漢字.bin");
Files.write(file, content);
}
- /** The outcome of a script: the decoded bytes and the exit code. */
+ /** The outcome of a script: the bytes it wrote on stdout and the exit code. */
private static final class Outcome {
final byte[] bytes;
@@ -94,12 +94,23 @@ private static final class Outcome {
}
private static Outcome run(final String script) throws Exception {
- final Listing raw = exec(script);
- final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
- for (final String line : raw.lines) {
- bytes.write(Base64.getDecoder().decode(line));
+ final String encoded = Base64.getEncoder().encodeToString(script.getBytes(StandardCharsets.UTF_16LE));
+ final Process process = new ProcessBuilder(
+ "powershell.exe",
+ "-NoProfile",
+ "-NonInteractive",
+ "-EncodedCommand",
+ encoded
+ )
+ .redirectError(ProcessBuilder.Redirect.DISCARD)
+ .start();
+ process.getOutputStream().close();
+ final byte[] stdout = process.getInputStream().readAllBytes();
+ if (!process.waitFor(60, TimeUnit.SECONDS)) {
+ process.destroyForcibly();
+ throw new AssertionError("powershell.exe did not complete");
}
- return new Outcome(bytes.toByteArray(), raw.exitCode);
+ return new Outcome(stdout, process.exitValue());
}
/** The outcome of a metadata script: its non-blank stdout lines and the exit code. */
@@ -139,28 +150,13 @@ private static String relative(final String path) {
}
private static Listing exec(final String script) throws Exception {
- final String encoded = Base64.getEncoder().encodeToString(script.getBytes(StandardCharsets.UTF_16LE));
- final Process process = new ProcessBuilder(
- "powershell.exe",
- "-NoProfile",
- "-NonInteractive",
- "-EncodedCommand",
- encoded
- )
- .redirectError(ProcessBuilder.Redirect.DISCARD)
- .start();
- process.getOutputStream().close();
- final String stdout = new String(process.getInputStream().readAllBytes(), StandardCharsets.US_ASCII);
- if (!process.waitFor(60, TimeUnit.SECONDS)) {
- process.destroyForcibly();
- throw new AssertionError("powershell.exe did not complete");
- }
+ final Outcome outcome = run(script);
final List lines = Arrays
- .stream(stdout.split("\r?\n"))
+ .stream(new String(outcome.bytes, StandardCharsets.US_ASCII).split("\r?\n"))
.filter(l -> !l.isBlank())
.map(String::strip)
.collect(Collectors.toList());
- return new Listing(lines, process.exitValue());
+ return new Listing(lines, outcome.exitCode);
}
private static byte[] read(final long offset, final long length) throws Exception {
diff --git a/src/test/java/org/metricshub/winrm/cli/WinRmCliTest.java b/src/test/java/org/metricshub/winrm/cli/WinRmCliTest.java
index 316c450..15f33c8 100644
--- a/src/test/java/org/metricshub/winrm/cli/WinRmCliTest.java
+++ b/src/test/java/org/metricshub/winrm/cli/WinRmCliTest.java
@@ -803,12 +803,9 @@ void catWritesTheRemoteBytesUnconverted() throws Exception {
System
.arraycopy(new byte[]
{ '\r', '\n', '\n', '\r', (byte) 0xC3, 0x28, (byte) 0xFF, (byte) 0xFE, 0x1A }, 0, content, 256, 9);
- final String lines = b64(Arrays.copyOfRange(content, 0, 200)) + "\r\n"
- + b64(Arrays.copyOfRange(content, 200, content.length)) +
- "\r\n";
try (FakeWsmanServer server = new FakeWsmanServer("FAKE", "user", "secret")) {
enqueueShellCreation(server);
- enqueueScript(server, 0, lines.substring(0, 100), lines.substring(100));
+ enqueueScript(server, 0, Arrays.copyOfRange(content, 0, 100), Arrays.copyOfRange(content, 100, content.length));
enqueueShellDeletion(server);
final Invocation invocation = invokeAgainst(server, "cat", "C:\\Windows\\Temp\\collect.bin");
@@ -825,7 +822,7 @@ void catWritesTheRemoteBytesUnconverted() throws Exception {
void catReadsARangeAndDecodesTextWithTheGivenCharset() throws Exception {
try (FakeWsmanServer server = new FakeWsmanServer("FAKE", "user", "secret")) {
enqueueShellCreation(server);
- enqueueScript(server, 0, b64(new byte[] { 'c', 'a', 'f', (byte) 0xE9 }) + "\r\n");
+ enqueueScript(server, 0, new byte[] { 'c', 'a', 'f', (byte) 0xE9 });
enqueueShellDeletion(server);
final Invocation invocation = invokeAgainst(
@@ -856,8 +853,8 @@ void aClosedStandardOutputStopsTheRemoteRead() throws Exception {
.enqueue(200, FakeWsmanResponses.envelope(FakeWsmanResponses.commandResponse(COMMAND_ID)))
.enqueue(
200,
- FakeWsmanResponses.envelope(FakeWsmanResponses.receiveResponse(stdoutStream(b64(new byte[]
- { 1, 2, 3 }) + "\r\n"), null))
+ FakeWsmanResponses.envelope(FakeWsmanResponses.receiveResponse(stdoutStream(new byte[]
+ { 1, 2, 3 }), null))
)
// ...when the closed output makes the CLI stop it: the terminate Signal, which a real
// host answers only when its OperationTimeout expires, the read being blocked writing
@@ -893,7 +890,7 @@ void getDownloadsIntoAnExistingDirectoryUnderTheRemoteName(@TempDir final Path d
try (FakeWsmanServer server = new FakeWsmanServer("FAKE", "user", "secret")) {
enqueueShellCreation(server);
enqueueScript(server, 0, probe(content));
- enqueueScript(server, 0, b64(content) + "\r\n");
+ enqueueScript(server, 0, content);
enqueueShellDeletion(server);
final Invocation invocation = invokeAgainst(
@@ -962,27 +959,33 @@ private static String json(final String path, final String mode, final int attri
}
/** The probe output of a download: the size (8 bytes, little-endian), then the SHA-256 digest. */
- private static String probe(final byte[] content) throws Exception {
- return b64(
- ByteBuffer
- .allocate(40)
- .order(ByteOrder.LITTLE_ENDIAN)
- .putLong(content.length)
- .put(MessageDigest.getInstance("SHA-256").digest(content))
- .array()
- ) +
- "\r\n";
+ private static byte[] probe(final byte[] content) throws Exception {
+ return ByteBuffer
+ .allocate(40)
+ .order(ByteOrder.LITTLE_ENDIAN)
+ .putLong(content.length)
+ .put(MessageDigest.getInstance("SHA-256").digest(content))
+ .array();
}
- private static String stdoutStream(final String text) {
- return FakeWsmanResponses.stream("stdout", COMMAND_ID, text.getBytes(StandardCharsets.US_ASCII));
+ private static String stdoutStream(final byte[] bytes) {
+ return FakeWsmanResponses.stream("stdout", COMMAND_ID, bytes);
+ }
+
+ /** {@link #enqueueScript(FakeWsmanServer, int, byte[]...)} for a script writing text: the metadata records. */
+ private static void enqueueScript(final FakeWsmanServer server, final int exitCode, final String... chunks) {
+ enqueueScript(
+ server,
+ exitCode,
+ Arrays.stream(chunks).map(chunk -> chunk.getBytes(StandardCharsets.US_ASCII)).toArray(byte[][]::new)
+ );
}
/**
* Script one remote file script on the existing shell: the command, one Receive per stdout
* chunk (the last one completes with the exit code), and the Signal ending it.
*/
- private static void enqueueScript(final FakeWsmanServer server, final int exitCode, final String... chunks) {
+ private static void enqueueScript(final FakeWsmanServer server, final int exitCode, final byte[]... chunks) {
server.enqueue(200, FakeWsmanResponses.envelope(FakeWsmanResponses.commandResponse(COMMAND_ID)));
for (int i = 0; i < chunks.length; i++) {
final boolean last = i == chunks.length - 1;