diff --git a/README.md b/README.md index b27c369..8692d81 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,12 @@ With `--distro`, `wsldrive` starts `wsldrived` in the distro over `wsl.exe`, wai listen, mounts, and tears the agent down on exit (the agent also self-terminates if the Windows side goes away). Ctrl+C unmounts cleanly. +Add `--hvsocket` to use the **Hyper-V socket transport** instead of loopback TCP — the agent listens +on `vsock` and the client connects over `AF_HYPERV`, bypassing the localhost relay. This is what makes +the WSL→Windows direction fast (see `bench/RESULTS.md`). One-time setup: run +`scripts/register-hvsocket.ps1` elevated and `wsl --shutdown`. The WSL VM GUID is discovered via +`hcsdiag` (Hyper-V admin) or passed with `--vm-guid`. + Drop a `.wsldriveignore` at the served root (gitignore-style: `node_modules/`, `*.log`, `/build`, ...) to exclude directories from the mounted view and from sync — handy for keeping build output and vendored trees off the drive. diff --git a/src/platform/win/wsl_launch.cpp b/src/platform/win/wsl_launch.cpp index c6f0a86..f2b91f6 100644 --- a/src/platform/win/wsl_launch.cpp +++ b/src/platform/win/wsl_launch.cpp @@ -4,7 +4,9 @@ #include +#include #include +#include namespace wsld::platform { @@ -25,6 +27,52 @@ std::string sh_quote(std::string_view s) { } // namespace +namespace { +// Extracts the first GUID-shaped token (8-4-4-4-12 hex) from a string. +std::string first_guid(std::string_view s) { + auto is_hex = [](char c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); + }; + for (std::size_t i = 0; i + 36 <= s.size(); ++i) { + bool ok = true; + for (std::size_t j = 0; j < 36 && ok; ++j) { + const char c = s[i + j]; + if (j == 8 || j == 13 || j == 18 || j == 23) + ok = c == '-'; + else + ok = is_hex(c); + } + if (ok) return std::string(s.substr(i, 36)); + } + return {}; +} +} // namespace + +std::string discover_wsl_vm_guid() { + std::FILE* pipe = _popen("hcsdiag list 2>&1", "r"); + if (pipe == nullptr) return {}; + std::string out; + char buf[512]; + while (std::fgets(buf, sizeof(buf), pipe) != nullptr) out += buf; + _pclose(pipe); + // hcsdiag prints one compute system per block; the WSL utility VM's line + // contains "WSL". Prefer a line mentioning WSL; fall back to the first GUID. + std::size_t start = 0; + std::string fallback; + for (std::size_t i = 0; i <= out.size(); ++i) { + if (i == out.size() || out[i] == '\n') { + const std::string_view line(out.data() + start, i - start); + const std::string g = first_guid(line); + if (!g.empty()) { + if (line.find("WSL") != std::string_view::npos) return g; + if (fallback.empty()) fallback = g; + } + start = i + 1; + } + } + return fallback; +} + std::string build_wsl_command(const WslAgentSpec& spec) { const std::string agent = spec.agent_path.empty() ? "wsldrived" : spec.agent_path; std::string cmd = "wsl.exe"; @@ -33,7 +81,8 @@ std::string build_wsl_command(const WslAgentSpec& spec) { cmd += " -- "; cmd += sh_quote(agent); cmd += " --root " + sh_quote(spec.wsl_root); - cmd += " --listen tcp://127.0.0.1:" + std::to_string(spec.port); + const std::string listen = spec.listen.empty() ? ("tcp://127.0.0.1:" + std::to_string(spec.port)) : spec.listen; + cmd += " --listen " + listen; cmd += " --exit-when-idle"; // agent terminates when this client's socket closes return cmd; } @@ -62,7 +111,7 @@ void WslAgent::stop() { // listen port), then the wsl.exe client. const std::string kill = "wsl.exe" + (spec_.distro.empty() ? std::string() : " -d " + spec_.distro) + " -- pkill -f " + [&] { - std::string pat = "wsldrived.*127.0.0.1:" + std::to_string(spec_.port); + std::string pat = "wsldrived.*:" + std::to_string(spec_.port); std::string q = "'"; for (char c : pat) q += (c == '\'') ? std::string("'\\''") : std::string(1, c); q.push_back('\''); diff --git a/src/platform/win/wsl_launch.hpp b/src/platform/win/wsl_launch.hpp index 610a948..00c4b6b 100644 --- a/src/platform/win/wsl_launch.hpp +++ b/src/platform/win/wsl_launch.hpp @@ -12,9 +12,15 @@ struct WslAgentSpec { std::string distro; // e.g. "Ubuntu"; empty = the default distro std::string agent_path; // path to wsldrived inside the distro (default: "wsldrived" on PATH) std::string wsl_root; // the ext4 directory to serve - std::uint32_t port = 0; // loopback TCP port the agent listens on + std::uint32_t port = 0; // port the agent listens on + std::string listen; // full listen endpoint; empty = tcp://127.0.0.1: }; +/// Best-effort discovery of the running WSL2 utility VM's GUID (via `hcsdiag +/// list`). Returns "" if it cannot be determined (e.g. not elevated / not a +/// Hyper-V administrator). Needed to reach the guest over AF_HYPERV. +[[nodiscard]] std::string discover_wsl_vm_guid(); + /// Builds the `wsl.exe ...` command line that launches the agent. Pure, so the /// argument construction can be unit-tested without spawning anything. [[nodiscard]] std::string build_wsl_command(const WslAgentSpec& spec); diff --git a/src/tools/wsldrive_main.cpp b/src/tools/wsldrive_main.cpp index 14b0f96..0680eff 100644 --- a/src/tools/wsldrive_main.cpp +++ b/src/tools/wsldrive_main.cpp @@ -45,6 +45,7 @@ void usage() { #ifdef WSLDRIVE_HAVE_WINFSP " wsldrive mount --distro --wsl-root [--agent ] [--port N]\n" " (auto-launch the agent in the distro)\n" + " add --hvsocket [--vm-guid ] (use the Hyper-V socket transport)\n" #endif , stderr); @@ -157,6 +158,8 @@ int main(int argc, char** argv) { std::uint32_t port = 51789; bool have_distro = false; bool writeback = false; + bool hvsocket = false; + std::string vm_guid; for (int i = 3; i < argc; ++i) { const std::string_view a = argv[i]; auto val = [&]() -> std::string { return (i + 1 < argc) ? argv[++i] : std::string(); }; @@ -164,6 +167,10 @@ int main(int argc, char** argv) { connect = val(); else if (a == "--listen") listen = val(); + else if (a == "--hvsocket") + hvsocket = true; + else if (a == "--vm-guid") + vm_guid = val(); else if (a == "--writeback") writeback = true; else if (a == "--distro") { @@ -189,12 +196,33 @@ int main(int argc, char** argv) { std::fprintf(stderr, "wsldrive: --wsl-root is required with --distro\n"); return 2; } - if (auto r = agent.start({.distro = distro, .agent_path = agent_path, .wsl_root = wsl_root, .port = port}); !r) { + // WSL2 routes hvsocket host->guest: the agent listens on vsock and this + // client connects to the guest VM over AF_HYPERV. Needs a registered vsock + // port (scripts/register-hvsocket.ps1) and the VM's GUID. + if (hvsocket && port == 51789) port = 5700; // a registered vsock port by default + wsld::platform::WslAgentSpec spec{.distro = distro, .agent_path = agent_path, .wsl_root = wsl_root, .port = port}; + if (hvsocket) { + std::string guid = vm_guid.empty() ? wsld::platform::discover_wsl_vm_guid() : vm_guid; + if (guid.empty()) { + std::fprintf(stderr, + "wsldrive: could not determine the WSL VM GUID for --hvsocket.\n" + " Pass --vm-guid (from an elevated `hcsdiag list`), or run elevated.\n"); + return 2; + } + spec.listen = "vsock://any:" + std::to_string(port); + connect = "hv://{" + guid + "}:" + std::to_string(port); + } + if (auto r = agent.start(spec); !r) { std::fprintf(stderr, "wsldrive: failed to launch agent in distro '%s'\n", distro.c_str()); return 1; } - connect = "tcp://127.0.0.1:" + std::to_string(port); - std::printf("launched agent in %s, waiting for it to listen...\n", distro.empty() ? "(default distro)" : distro.c_str()); + if (!hvsocket) connect = "tcp://127.0.0.1:" + std::to_string(port); + std::printf("launched agent in %s over %s, waiting for it to listen...\n", + distro.empty() ? "(default distro)" : distro.c_str(), hvsocket ? "hvsocket" : "tcp"); + std::fflush(stdout); + // A hvsocket connect() blocks for a long OS timeout if the service is not + // yet listening (unlike TCP's fast refusal), so let the agent bind first. + if (hvsocket) std::this_thread::sleep_for(std::chrono::milliseconds(2500)); } else #endif if (connect.empty() && listen.empty()) { @@ -214,6 +242,8 @@ int main(int argc, char** argv) { (void)wsl_root; (void)agent_path; (void)port; + (void)hvsocket; + (void)vm_guid; (void)have_distro; std::string source; wsld::Result sock = wsld::fail(wsld::Errc::ConnectionClosed); diff --git a/tests/agent_test.cpp b/tests/agent_test.cpp index e35aaed..911cc9a 100644 --- a/tests/agent_test.cpp +++ b/tests/agent_test.cpp @@ -497,6 +497,12 @@ TEST(WslLaunch, BuildsCommand) { // A single quote in a path is escaped so the distro shell keeps it literal. const std::string q = platform::build_wsl_command({.distro = "", .agent_path = "", .wsl_root = "/a'b", .port = 1}); EXPECT_NE(q.find("'/a'\\''b'"), std::string::npos); + + // An explicit listen endpoint (e.g. vsock for the Hyper-V socket transport) is used verbatim. + const std::string v = platform::build_wsl_command( + {.distro = "Ubuntu", .agent_path = "", .wsl_root = "/x", .port = 5700, .listen = "vsock://any:5700"}); + EXPECT_NE(v.find("--listen vsock://any:5700"), std::string::npos); + EXPECT_EQ(v.find("tcp://"), std::string::npos); } TEST(Win32Watcher, ReportsEventsAndStopsCleanly) {