Skip to content

feat(nfs): Windows support for hf-mount-nfs#140

Draft
XciD wants to merge 16 commits into
mainfrom
feat/windows-nfs-support
Draft

feat(nfs): Windows support for hf-mount-nfs#140
XciD wants to merge 16 commits into
mainfrom
feat/windows-nfs-support

Conversation

@XciD

@XciD XciD commented Apr 27, 2026

Copy link
Copy Markdown
Member

Summary

Make hf-mount-nfs build and run end-to-end on Windows. Closes #88 (NFS path; FUSE remains Unix-only).

Verified on Windows Server 2022 (Rust 1.95 + MSVC 2022) with the "Client for NFS" feature: datasets/XciD/hf-mount-poll-test mounts cleanly to drive Z: and files read back correctly through xet-core/CAS.

$ hf-mount-nfs.exe --hf-token ... --read-only repo datasets/XciD/hf-mount-poll-test Z:
INFO Mounting dataset/XciD/hf-mount-poll-test/main at "Z:" (read-only, backend=nfs)
INFO NFS server listening on 127.0.0.1:50567
INFO NFS mount active at Z:
INFO HTTP download: .gitattributes -> "/tmp/hf-mount-cache/staging/http_..."

PS> dir Z:\
.gitattributes   2975 -ar---
file_1.txt         32 -ar---
file_2.txt         34 -ar---
... (10 files)

PS> Get-Content Z:\file_1.txt
v3: file 1 modified at 08:58:59

What changed

Cross-platform plumbing:

  • daemon gated cfg(unix) with a Windows stub (src/daemon_windows.rs) preserving the public API. Daemon control commands return a clear "not supported" error on Windows.
  • virtual_fs replaces direct libc::pread/libc::pwrite on Arc<File> with cross-platform helpers built on std::os::unix::fs::FileExt::read_at (and std::os::windows::fs::FileExt::seek_read).
  • setup::raise_fd_limit is a no-op on Windows; uid/gid default to 0. Mount-point creation now uses ErrorKind::AlreadyExists instead of libc::EEXIST.
  • nfs.rs cfg-gates tokio::signal::unix::SignalKind::terminate and adds a Windows mount block driving mount.exe against \\127.0.0.1\!.

Windows mount discovery (the real blocker):

  • Windows mount.exe has no mountport= option to bypass portmapper; it always queries portmapper at 127.0.0.1:111 to discover MOUNT and NFS service ports. nfsserve doesn't expose one. Linux/macOS skip this via mountport=N.
  • Bundle a minimal RFC 1833 portmapper (program 100000 v2) that answers GETPORT(MOUNT v3) and GETPORT(NFS v3) with nfsserve's actual TCP port, on UDP and TCP. Private module in nfs.rs, cfg(windows) only.

Build / binary surface:

  • fuser moved to [target.'cfg(unix)'.dependencies]. pub mod fuse gated cfg(all(unix, feature = "fuse")).
  • hf-mount, hf-mount-fuse, hf-mount-fuse-sidecar print a clear "Unix-only" message and exit 1 on Windows.

Windows runtime requirements

  • "Client for NFS" feature enabled:
    • Server: Install-WindowsFeature -Name NFS-Client
    • Client (10/11): Enable-WindowsOptionalFeature -Online -FeatureName ServicesForNFS-ClientOnly,ClientForNFS-Infrastructure -All
  • Run as Administrator (port 111 is privileged for the bundled portmapper).
  • For non-admin user visibility in Explorer / "This PC": set EnableLinkedConnections and reboot, otherwise drives mounted by an elevated process are only visible to other elevated processes (MS doc):
    reg add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System /v EnableLinkedConnections /t REG_DWORD /d 1 /f
    
    Then reboot. After that, hf-mount-nfs.exe ... Z: in an admin shell exposes Z: to non-admin Explorer / apps.

Debug knob

  • HF_MOUNT_SKIP_AUTO_MOUNT=1 makes the binary spawn the NFS server + portmapper but skip the mount.exe invocation and the mount-disappearance poll, leaving the server up so mount.exe can be invoked manually for diagnosis.

Limitations

  • Daemon controller (hf-mount) and FUSE backend remain Unix-only.

Follow-ups (out of scope)

  • CI matrix entry for windows-2022 running at minimum cargo build --features nfs --bin hf-mount-nfs.
  • Document the Windows path in the README (Client for NFS install + Administrator + EnableLinkedConnections requirement).

@github-actions

Copy link
Copy Markdown
Contributor

POSIX Compliance (pjdfstest)

============================================================
  pjdfstest POSIX Compliance Results
------------------------------------------------------------
  Files: 130/130 passed    Tests: 832 total (0 subtests failed)
  Result: PASS
------------------------------------------------------------
  Category               Passed    Total   Status
  -------------------- -------- -------- --------
  chflags                     5        5       OK
  chmod                       8        8       OK
  chown                       6        6       OK
  ftruncate                  13       13       OK
  granular                    5        5       OK
  mkdir                       9        9       OK
  open                       19       19       OK
  posix_fallocate             1        1       OK
  rename                     10       10       OK
  rmdir                      11       11       OK
  symlink                    10       10       OK
  truncate                   13       13       OK
  unlink                     11       11       OK
  utimensat                   9        9       OK
============================================================

@github-actions

github-actions Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results

============================================================
  Benchmark — 50MB
------------------------------------------------------------
  Metric                                 FUSE          NFS
  ------------------------------ ------------ ------------
  Sequential read                    206.8 MB/s     303.9 MB/s
  Sequential re-read                2134.0 MB/s    2451.3 MB/s
  Range read (1MB@25MB)                0.5 ms         0.2 ms
  Random reads (100x4KB avg)           0.0 ms         0.0 ms
  Sequential write (FUSE)           1546.2 MB/s
  Close latency (CAS+Hub)            0.114 s
  Write end-to-end                   342.8 MB/s
  Dedup write                       1897.3 MB/s
  Dedup close latency                0.161 s
  Dedup end-to-end                   266.5 MB/s
============================================================
============================================================
  Benchmark — 200MB
------------------------------------------------------------
  Metric                                 FUSE          NFS
  ------------------------------ ------------ ------------
  Sequential read                    725.8 MB/s     418.5 MB/s
  Sequential re-read                2208.4 MB/s    2393.0 MB/s
  Range read (1MB@25MB)                0.2 ms         0.2 ms
  Random reads (100x4KB avg)           0.0 ms         0.0 ms
  Sequential write (FUSE)           1713.9 MB/s
  Close latency (CAS+Hub)            0.186 s
  Write end-to-end                   660.5 MB/s
  Dedup write                       1792.4 MB/s
  Dedup close latency                0.090 s
  Dedup end-to-end                   992.6 MB/s
============================================================
============================================================
  Benchmark — 500MB
------------------------------------------------------------
  Metric                                 FUSE          NFS
  ------------------------------ ------------ ------------
  Sequential read                    888.3 MB/s    1322.5 MB/s
  Sequential re-read                2213.7 MB/s    2409.0 MB/s
  Range read (1MB@25MB)                0.2 ms         0.2 ms
  Random reads (100x4KB avg)           0.0 ms         0.0 ms
  Sequential write (FUSE)           1548.5 MB/s
  Close latency (CAS+Hub)            0.139 s
  Write end-to-end                  1081.5 MB/s
  Dedup write                       1483.5 MB/s
  Dedup close latency                0.480 s
  Dedup end-to-end                   612.2 MB/s
============================================================
============================================================
  fio Benchmark Results
------------------------------------------------------------
  Job                        FUSE MB/s   NFS MB/s  FUSE IOPS   NFS IOPS
  ------------------------- ---------- ---------- ---------- ----------
  seq-read-100M                  505.0      480.8                      
  seq-reread-100M               2500.0       74.7                      
  rand-read-4k-100M                0.1        0.1         18         20
  seq-read-5x10M                 862.1      819.7                      
  rand-read-10x1M                  0.1        0.1         36         34
  Random Read Latency           FUSE avg      NFS avg
  ------------------------- ------------ ------------
  rand-read-4k-100M           55672.1 us   49427.6 us
  rand-read-10x1M             28143.5 us   29348.4 us
============================================================

XciD added 4 commits May 4, 2026 10:25
Make `hf-mount-nfs` build and run end-to-end on Windows, addressing #88.

Verified on Windows Server 2022 (m5.xlarge EC2) with Rust 1.95 + MSVC 2022
and the "Client for NFS" feature: a HuggingFace dataset mounts cleanly to a
drive letter and files read back correctly through xet-core/CAS.

Cross-platform plumbing:
- `daemon` is gated `cfg(unix)`; a Windows stub (`src/daemon_windows.rs`)
  preserves the public API so callers compile unchanged. Daemon control
  commands (`stop`/`status`/`daemonize`) return a clear "not supported" error.
- `virtual_fs` replaces direct `libc::pread`/`libc::pwrite` on `Arc<File>`
  with cross-platform helpers built on `std::os::unix::fs::FileExt::read_at`
  / `std::os::windows::fs::FileExt::seek_read` (and the write equivalents).
- `setup::raise_fd_limit` is a no-op on Windows; uid/gid default to 0.
  Mount-point creation now uses `ErrorKind::AlreadyExists` instead of
  `libc::EEXIST` for portability.
- `nfs.rs` cfg-gates `tokio::signal::unix::SignalKind::terminate` (Unix only)
  and adds a Windows mount block that drives `mount.exe` with NFS-client
  options against `\\127.0.0.1\!`.

Windows mount discovery (the actual blocker):
- Windows `mount.exe` has no `mountport=` option to bypass portmapper, so it
  always queries portmapper at 127.0.0.1:111 to discover MOUNT and NFS
  service ports. nfsserve doesn't expose one. Linux/macOS skip this via
  `mountport=N`.
- Bundle a minimal RFC 1833 portmapper (program 100000 v2) that answers
  GETPORT(MOUNT v3) and GETPORT(NFS v3) with nfsserve's actual TCP port,
  serving both UDP and TCP on 127.0.0.1:111. Implemented as a private module
  inside `nfs.rs` and only compiled `cfg(windows)`. Binding port 111
  requires Administrator on Windows.

Build/binary surface:
- `fuser` is moved to `[target.'cfg(unix)'.dependencies]` so the `fuse`
  feature has no effect on Windows; `pub mod fuse` is gated `cfg(all(unix,
  feature = "fuse"))`.
- `hf-mount`, `hf-mount-fuse`, `hf-mount-fuse-sidecar` print a clear
  "Unix-only" message and exit 1 on Windows.

Limitations:
- Windows requires the "Client for NFS" feature
  (`Install-WindowsFeature -Name NFS-Client`) and Administrator privileges
  (port 111 is privileged).
- The neutral `hf-mount` daemon controller and FUSE backend remain
  Unix-only.
- Drop the `mod imp { pub fn run() }` wrapper in `hf-mount.rs` and gate at
  item level instead, removing the 500-line re-indent.
- Rename `imp::run` -> `imp::main` in `hf-mount-fuse-sidecar.rs`.
- Restructure `unmount_nfs` to flat `linux`/`macos`/`windows` sibling cfgs
  (matches `is_mounted`'s style, drops the nested cfg blocks).
- Replace the 200ms sleep before `mount.exe` with a deterministic readiness
  signal: `portmapper::start()` binds UDP+TCP synchronously, then spawns the
  accept loops, returning a JoinHandle. Caller awaits bind, then proceeds.
- Drop the misleading `std::mem::forget(portmapper_handle)` (forgetting a
  JoinHandle is a no-op for the task lifecycle in tokio); abort cleanly in
  the shutdown path instead.
- Replace `format!("nolock,...")` with `String::from(...)` + `push_str`
  (clippy `useless_format`).
- Hoist the inline `async { #[cfg(unix)] {...} #[cfg(not(unix))] {...} }`
  into a named `sigterm_fut` future at the top of the select scope.
- Inline the `read_u32`/`write_u32` helpers in the portmapper into direct
  `u32::from_be_bytes` / `to_be_bytes` calls (already used elsewhere in the
  module).
Drop the inline RFC 1833 portmapper module and depend on the matching
upstream feature in nfsserve (huggingface/nfsserve#44, branch
feat/portmap-listener via [patch.crates-io]).

Net: -147 lines locally; the portmapper now lives where it logically
belongs (next to nfsserve's existing portmap_handlers and rpc/xdr code,
where future maintainers will look first).

Drop the patch.crates-io entry once nfsserve cuts a release containing
the new module.
`tokio::pin!(server_handle)` plus dropping it at function end does not
cancel a tokio task — the JoinHandle's Drop is a no-op for the underlying
task. The NFS accept loop in `nfsserve::handle_forever()` would keep
running after `mount_nfs` returned, leaking the bound TCP port until
process exit. Caught by codex review on the Windows port PR.

Now: explicit `.abort(); let _ = .await;` in the shutdown sequence.
@XciD XciD force-pushed the feat/windows-nfs-support branch from eafd0f5 to 3386adb Compare May 4, 2026 08:26
- Add build-windows job to release.yml (matches Linux/macOS jobs)
- Add windows-build.yml that builds hf-mount-nfs.exe and hf-mount.exe
  on push to feat/windows-** and on PRs touching src/Cargo, uploads
  the binaries as a workflow artifact for direct download.
@XciD XciD force-pushed the feat/windows-nfs-support branch from 3386adb to 44fcef8 Compare May 4, 2026 08:26
XciD added 11 commits May 4, 2026 10:55
Setting the env var keeps the NFS server + portmapper running and
prints the exact mount.exe command to run manually. Lets us debug
which option mount.exe rejects without the binary tearing the server
down on the first failure.
…T set

Without this, the 2s poll sees the un-mounted drive letter and tears
down the server, defeating the whole point of the skip flag.
The daemon controller (hf-mount) is Unix-only — its Windows main()
just prints 'Unix-only' and exits 1. No reason to ship the binary.
The cfg(unix) wrap was unnecessary churn — hf-mount-fuse-sidecar
declares 'required-features = ["fuse"]', and the fuse feature
depends on fuser which is a [target.'cfg(unix)'.dependencies], so
on Windows the binary is naturally skipped (the feature can't be
enabled). Reverts the 859-line indentation diff.
Only DaemonGuard::from_env / notify_ready are needed by hf-mount-nfs
on Windows; the full controller is Unix-only and not built. 49 lines
of stub trimmed to 8 inline lines.
…red-openssl

- Use tokio::process::Command instead of std::process::Command for the
  mount.exe call so it doesn't block a tokio worker thread.
- Build the mount.exe command string once instead of duplicating it in
  the info log and the error message.
- Drop --features vendored-openssl from Windows CI builds: native-tls
  uses Schannel on Windows, openssl-sys is not in the dep tree, so the
  feature was just compiling OpenSSL from source for nothing.
Adds Windows under 'System dependencies' with Client for NFS install
commands (Server vs 10/11), Administrator requirement, and the
EnableLinkedConnections registry tweak for non-admin Explorer
visibility. Notes that only hf-mount-nfs.exe is built on Windows.
Required by the new non-blocking mount.exe spawn.
Adds three small helpers to tests/common/mod.rs:
- nfs_mount_point(slug): returns /tmp/hf-mount-nfs-{slug}-{pid} on Unix,
  Z: on Windows (relies on --test-threads=1 for drive-letter reuse).
- nfs_binary_path(): appends .exe on Windows.
- nfs_is_mounted(): /proc/mounts on Linux, mount cmd on macOS,
  std::fs::metadata on Windows.

unmount_nfs is cfg-gated to use 'sudo umount' on Unix and 'umount.exe -f'
on Windows (no sudo). create_dir_all on the mount point is skipped on
Windows since drive letters aren't directories.

The 4 mount_point bindings in tests/nfs_ops.rs swap their ad-hoc
format!() calls for nfs_mount_point(). bench.rs / fio_bench.rs left as
Unix-only paths since benches don't run on Windows CI.
Same NFS integration tests as the Linux smoke job, runs on
windows-2022 with the Client for NFS feature enabled. Builds a
fresh hf-mount-nfs.exe (cached via Swatinem/rust-cache) then
exercises the cross-platform helpers added in the previous commit.
These tests use libc::dup + as_raw_fd which are Unix-only, breaking
the Windows compile of tests/common/fs_tests.rs. The fcntl-style FD
duplication semantics they exercise don't exist on Windows anyway.
@XciD XciD mentioned this pull request May 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows support

1 participant