Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
53 changes: 51 additions & 2 deletions src/platform/win/wsl_launch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

#include <windows.h>

#include <cstdio>
#include <string>
#include <string_view>

namespace wsld::platform {

Expand All @@ -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";
Expand All @@ -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;
}
Expand Down Expand Up @@ -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('\'');
Expand Down
8 changes: 7 additions & 1 deletion src/platform/win/wsl_launch.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:<port>
};

/// 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);
Expand Down
36 changes: 33 additions & 3 deletions src/tools/wsldrive_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ void usage() {
#ifdef WSLDRIVE_HAVE_WINFSP
" wsldrive mount <drive> --distro <name> --wsl-root <path> [--agent <path>] [--port N]\n"
" (auto-launch the agent in the distro)\n"
" add --hvsocket [--vm-guid <guid>] (use the Hyper-V socket transport)\n"
#endif
,
stderr);
Expand Down Expand Up @@ -157,13 +158,19 @@ 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(); };
if (a == "--connect")
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") {
Expand All @@ -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 <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()) {
Expand All @@ -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<wsld::net::Socket> sock = wsld::fail(wsld::Errc::ConnectionClosed);
Expand Down
6 changes: 6 additions & 0 deletions tests/agent_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading