WinRM Java Client 3.1.01
-
Home
- Usage
Remote Files
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 reads it as it arrives. No SMB, no extra port, no share. File Transfers[1] covers copying files to the host, and how downloads work. The standalone jar exposes remote file access as the ls, stat, cat, and get subcommands — see the Command-Line Client[2] manual.
Reading a file
client.file(path)[3] designates a file on the remote host; nothing is sent until a terminal is called:
// Whole file, bytes (byte-exact: nothing converted, a BOM is kept)
byte[] content = client.file("C:\\Windows\\Temp\\collect.bin").readBytes();
// Whole file, text — the charset is always explicit, never guessed (UTF-8 keeps a BOM as U+FEFF)
String text = client.file("C:\\inetpub\\logs\\u_ex260729.log").readText(StandardCharsets.UTF_8);
// Integrity: the whole file's digest, as lowercase hex (MD5, SHA1, SHA256, SHA384, SHA512);
// only the digest is transferred, and offset/length do not apply
String sha256 = client.file("C:\\Windows\\Temp\\collect.ps1").digest("SHA256");
Byte ranges and tails
offset(long) and length(long) select a byte range, served by a seek on the host — reading the end of a multi-gigabyte log costs the same as reading the start of a small one:
// 64 KiB from the 1 GiB position
byte[] chunk = client.file("D:\\logs\\huge.log").offset(1_073_741_824L).length(65_536).readBytes();
// The last 8 KiB — a negative offset counts from the end
byte[] tail = client.file("D:\\logs\\huge.log").offset(-8192).readBytes();
| Setting | Result |
|---|---|
offset(-n) |
starts at max(0, size - n): a file shorter than n is read whole. The size is read by the same remote invocation that seeks, so a growing log is tailed from its current end. |
offset(-8192).length(1024) |
the first 1 KiB of the last 8 KiB |
| offset past the end | empty — not an error |
length beyond the end |
what exists |
length(0), an empty file |
empty |
Ranges are byte ranges: a range boundary can split a multibyte character, which then decodes to U+FFFD at the edges of a readText. Reads are not snapshots: a file that changes between two reads is read as it is at each read.
Streaming large files
readBytes() and readText() hold the whole content in memory, so they are capped: 64 MiB by default, adjustable with maxBytes(long) — reading more fails with a clear exception instead of an OutOfMemoryError (the cap is sent to the host too, so no more than one byte past it is transferred). For anything larger, stream:
try (InputStream in = client.file("D:\\logs\\huge.log").openStream()) {
in.transferTo(out);
}
try (BufferedReader reader = client.file("D:\\logs\\huge.log").openReader(StandardCharsets.UTF_8)) {
reader.lines().filter(line -> line.contains("ERROR")).forEach(System.out::println);
}
The stream passes the content on as it arrives: memory stays bounded whatever the file size. Like a RemoteProcess[4], it must be closed — it holds the client's connection until it reaches its end or is closed (other operations on the same client wait meanwhile), 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
downloadTo(Path) writes the whole file to a local file — digest-verified, skipped when the local copy is already identical, and atomic: the destination is replaced in one step, never left half-written. client.downloadFile(remote, local) does the same with the client's timeout:
long bytes = client.file("C:\\Windows\\Temp\\collect.log").downloadTo(Path.of("collect.log"));
How it works, its timeout and its speed are described in Downloading a file[5].
Timeouts
The blocking terminals (readBytes, readText, downloadTo, digest, info, exists, and execute() on a directory listing) run under a wall-clock deadline: the client's timeout, or timeout(Duration) on the request — raise it for large reads, downloads and big trees. openStream()/openReader() and a listing's stream() use the inactivity semantics of the other streaming terminals: the timeout bounds the silence between two blocks, not the whole operation. See Timeouts and Errors[6].
Errors and requirements
- 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 (
pagefile.sys, live registry hives) fails with a sharing violation. - Every failure is a
WinRMClientExceptionnaming its cause: path not found, a directory where a file is expected (or a file given tolist()), access denied, sharing violation, PowerShell not available, or PowerShell in Constrained Language Mode. - A script rejected at startup by the host's per-user operation quota (as low as 15 concurrent operations on Windows Server 2008 R2) has not run: it is retried with escalating delays (5, 10, 15, 20 seconds) for as long as the timeout allows, like the steps of a file transfer[1].
- The host needs PowerShell 2.0 or later in
FullLanguagemode (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 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,500 characters for a read, 1,150 for
info(), and 450 forlist(), whose walker script is the largest (roughly half as many with accented letters, a third with CJK characters) — all above the classic 260-characterMAX_PATH. A longer path fails with aWinRMClientExceptionbefore anything is sent.
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.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 under 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 at most 32 KiB at a time, about 60 times per second — about 2 MB/s at best. The reader's script writes the file's raw bytes 64 KiB at a time, so it runs at that ceiling; a command that writes line by line (Write-Output, type) gets about 0.3 MB/s. Raising the host's MaxEnvelopeSizekb does not help. Parallel reads, each on its own WinRMClient, add up.
File properties
info() returns the properties of a file or directory — or an empty Optional when the path does not exist, which is not an error. exists() is info().isPresent().
Optional<RemoteFileInfo> info = client.file("C:\\Windows\\Temp\\collect.log").info();
info.ifPresent(i -> System.out.println(i.size() + " bytes, modified " + i.lastModified()));
boolean there = client.file("C:\\Windows\\Temp\\collect.log").exists();
RemoteFileInfo[7] is an immutable value:
| Accessor | Content |
|---|---|
path(), name() |
the full path as reported by the host, and its last element |
isDirectory() |
a directory (including a junction or a directory symbolic link) |
size() |
the size in bytes, 0 for a directory |
lastModified(), created(), lastAccessed() |
UTC timestamps, 100 ns precision |
isHidden(), isSystem(), isReadOnly(), isArchive(), isReparsePoint() |
the attribute flags |
attributes() |
the raw Windows FileAttributes value, for anything else |
Genuine failures still throw a WinRMClientException: access denied on the path itself, an invalid path, PowerShell unavailable or constrained.
Listing a directory
list() prepares the listing of a directory (RemoteDirectoryListing[8]); set its filters, then execute() it into a RemoteFileList[9] or stream() it:
RemoteFileList logs = client.file("C:\\inetpub\\logs").list()
.glob("*.log")
.recursive() // off by default
.maxDepth(3)
.filesOnly() // or directoriesOnly()
.modifiedAfter(Instant.now().minus(Duration.ofDays(1)))
.minSize(1024L)
.execute();
logs.entries(); // List<RemoteFileInfo>
logs.inaccessible(); // List<String>: directories that could not be read
// Large trees stream: memory stays bounded whatever the size of the tree
try (Stream<RemoteFileInfo> tree = client.file("D:\\data").list()
.recursive()
.onInaccessible(path -> System.err.println("skipped " + path))
.stream()) {
tree.filter(RemoteFileInfo::isDirectory).forEach(System.out::println);
}
| Setting | Effect |
|---|---|
| (none) | the directory's own entries |
recursive() |
the whole tree, depth-first |
maxDepth(n) |
the tree down to depth n (1 = the directory's own entries); implies recursive() |
glob(pattern) |
entries whose name matches: * any sequence, ? one character, everything else literal, case-insensitive, whole name (*.log matches app.log, not app.log.1) |
filesOnly(), directoriesOnly() |
one kind of entry |
modifiedAfter(instant), modifiedBefore(instant) |
last write time strictly after / before |
minSize(bytes), maxSize(bytes) |
file size bounds, inclusive; directories have no size and are not filtered by them |
onInaccessible(consumer) |
told of each directory that could not be read, as the host reports it |
timeout(duration) |
wall-clock deadline for execute(), inactivity timeout for stream() |
Every filter is evaluated on the host: a big tree is not shipped over the wire just to be discarded locally. The filters select what is reported, not where the walk goes: with recursive(), every subdirectory is traversed, whatever the glob or the type filter.
Like the other streaming terminals, a stream() must be closed (try-with-resources): it holds the client's connection until it is exhausted or closed (other operations on the same client wait: to read or digest each entry, execute() the listing first), and closing it early stops the remote walk. Failures are thrown from the stream's operations, when the host reports them. The host signals it is alive every second while it walks, so the inactivity timeout only trips when the host is unresponsive — or when reading a single directory takes longer than the timeout.
Links and inaccessible directories
- Reparse points — junctions, symbolic links, mount points — are reported (with
isReparsePoint()true) but never descended into: a junction looping back to its parent, like the legacyC:\Documents and Settings-style junctions, cannot make a walk run forever. Listing a junction explicitly lists its target. - A subdirectory that cannot be read (typically access denied) does not stop the walk: its path is reported through
onInaccessible(...)andRemoteFileList.inaccessible(), and its content is missing from the entries. The directory being listed is different: when it cannot be read, the listing fails. - An administrator's WinRM session holds the backup privilege, enabled, and directory enumeration uses backup semantics: permissions that deny reading a directory do not stop an administrator. Inaccessible directories are mostly met with non-administrator accounts.
Listing performance
The walk runs in the host's PowerShell and streams one short text line per reported entry. Measured over HTTP with NTLM encryption: C:\Windows\System32 (16,000 entries) in 2.3 seconds, the whole of C:\Windows (126,000 entries) in 15 seconds on Windows Server 2022; the same System32 in 5.5 seconds on Windows Server 2008 R2 (PowerShell 2.0). With selective filters, only the walk itself counts: a recursive *.dll over 10 MB in System32 takes under a second on 2022.
How the metadata travels
A small script walks the tree with the .NET DirectoryInfo API — on every PowerShell version from 2.0 — and writes one ASCII line per entry: the attributes, size and timestamps as plain integers, and only the path base64-encoded (UTF-8). Non-ASCII names therefore round-trip exactly, whatever the remote console code page, and a truncated or malformed line fails with a clear exception instead of producing a half-populated entry.
Where PowerShell is constrained, SELECT queries on CIM_DataFile and CIM_Directory through client.wql(...)[10] can serve metadata instead, much more slowly: an unindexed CIM_DataFile query on a large tree can take minutes, and a recursive listing needs one query per directory.
Paths and limitations
- Long paths: paths longer than the classic 260-character
MAX_PATHwork where PowerShell runs on .NET Framework 4.6.2 or later (Windows Server 2016 and later out of the box; older versions with WMF 5.1 and an updated .NET): the scripts use\\?\-prefixed paths there, and report paths without the prefix. On older hosts (e.g. Windows Server 2008 R2 with PowerShell 2.0), a path over 260 characters fails with an explicit “path too long” error. The directory given tolist()is limited to about 450 characters (see the requirements); the entries a listing reports can be of any length. - UNC paths (
\\server\share\...) are a second hop: the host must authenticate to the file server with your credentials, which NTLM does not allow. They need Kerberos credential delegation[11] (allowDelegation()): without it, access to a UNC path typically fails with access denied. lastAccessed()is only as good as the host keeps it: many Windows versions disable or delay last-access updates.
See also
- File Transfers[1] — copying local files to the host, and how downloads work
- Remote Commands[12] — the command shell the reads and listings ride
- WQL Queries[10] — the WMI alternative for file metadata
- [1] file-transfers.html
- [2] cli.html#remote-files
- [3] apidocs/org/metricshub/winrm/RemoteFile.html
- [4] commands.html#streaming-the-output
- [5] file-transfers.html#downloading-a-file
- [6] timeouts-and-errors.html
- [7] apidocs/org/metricshub/winrm/RemoteFileInfo.html
- [8] apidocs/org/metricshub/winrm/RemoteDirectoryListing.html
- [9] apidocs/org/metricshub/winrm/RemoteFileList.html
- [10] wql.html
- [11] authentication.html#credential-delegation
- [12] commands.html
