Remote: Stream remote server into WSL on /mnt copy failure (#57624)

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #47375


The current `crates/remote/src/transport/wsl.rs` remote transport code
tries to copy the remote-server from /mnt/c/* from within WSL. This
fails if you have automount disabled, or otherwise have your drive
unmounted.

How to reproduce this error:

Prerequisites: Windows with WSL2 and a WSL distro installed.

1. Inside the WSL distro, disable Windows automount — edit
`/etc/wsl.conf`:
```ini
[automount]
enabled = false
```

2. From a Windows shell: `wsl --shutdown`
3. Open the distro shell again and confirm `mount | grep /mnt/c` returns
nothing.
4. From Zed (any version before this PR), open a remote WSL project on
that distro for the first time (no cached
`~/.zed_server/zed-remote-server-*` inside the distro — `rm` it if
present).

The solution was to pipe the server in over stdin if the copy fails.
This is a fallback path similar to how ssh.rs has a fallback path if the
download fails there.

With this PR the logs look like this:
```
2026-05-24T18:51:37-07:00 INFO  [remote::transport::wsl] uploading remote server to WSL ".zed_server/download-72808-remote_server" (441187kb)
2026-05-24T18:51:37-07:00 WARN  [remote::transport::wsl] failed to upload remote server via /mnt, falling back to wsl.exe stdin: Command 'Command("wsl.exe" "--distribution" "Ubuntu" "--cd" "~" "--exec" "wslpath" "-u" "C:/Users/<user>/Documents/GitHub/zed/target/remote_server/x86_64-unknown-linux-musl/debug/remote_server")' failed: wslpath: C:/Users/<user>/Documents/GitHub/zed/target/remote_server/x86_64-unknown-linux-musl/debug/remote_server: Command 'Command("wsl.exe" "--distribution" "Ubuntu" "--cd" "~" "wslpath" "-u" "C:/Users/<user>/Documents/GitHub/zed/target/remote_server/x86_64-unknown-linux-musl/debug/remote_server")' failed: wslpath: C:/Users/<user>/Documents/GitHub/zed/target/remote_server/x86_64-unknown-linux-musl/debug/remote_server
2026-05-24T18:51:38-07:00 INFO  [remote::transport::wsl] uploaded remote server in 945.899ms
```

I considered making wsl just use curl/wget to download as a fallback, as
docker/ssh do, but that would lose the 'cache' effect of downloading
once on windows and sending to multiple WSL distros. Which seems to have
been the intent of the copy method in the first place.

I also considered using the network-drive style UNC mount to copy from
Windows to the WSL distro, but that could run into similar issues where
the user may have that disabled. It also seems to copy as WSL's default
user, rather than the --user specified to Zed for the distro. Currently
--user is not passed by default to wsl.exe, so that ends up being the
default user. However, it is settable through the settings.json, so some
rare users may still have an issue with ownership of the copied files.

I chose to make the new stdin option the fallback instead of the default
because the current code is working for most people, and it seemed like
a much larger change to replace the default.

For the tests there don't seem to be any tests for wsl.rs currently that
I can find, so it seemed like a lot of extra code needed to satisfy that
PR checkbox. wsl.rs and docker.rs have zero inline tests, and the ones
that are in ssh.rs don't cover the ensure/upload/download portion. The
integration tests seem to use RemoteClient::fake_server and bypass the
real transports.

For manual testing I tested these scenarios:
1. WSL distro with automount **on** AND existing binary: **still works**
2. WSL distro with automount **on** AND **no** existing binary: **still
works**
3. WSL distro with automount **off** AND existing binary: **still
works**
4. WSL distro with automount **off** AND **no** existing binary: **now
works**

I'm not all that proficient with rust so I used a coding agent to help,
but I reviewed every line and it all makes sense to me. Please let me
know if something looks off, I'm trying to learn.


Release Notes:

- Fixed remote server installation failing in WSL when automount is
disabled.

---------

Co-authored-by: Lukas Wirth <lukas@zed.dev>
This commit is contained in:
Travis McHenry 2026-08-04 01:54:56 -07:00 committed by GitHub
parent 4aad57fd1f
commit e717010c82
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -11,7 +11,10 @@ use gpui::{App, AppContext as _, AsyncApp, Task};
use release_channel::{AppVersion, ReleaseChannel};
use rpc::proto::Envelope;
use semver::Version;
use smol::fs;
use smol::{
fs,
io::{self, AsyncWriteExt as _},
};
use std::{
ffi::OsStr,
fmt::Write as _,
@ -297,6 +300,26 @@ impl WslRemoteConnection {
size / 1024
);
match self.copy_via_wslpath_and_cp(src_path, dst_path).await {
Ok(()) => {}
Err(cp_err) => {
log::warn!(
"failed to upload remote server via /mnt, falling back to wsl.exe stdin: {cp_err:#}"
);
delegate.set_status(Some("Streaming remote server into WSL"), cx);
self.stream_file_into_wsl(src_path, dst_path)
.await
.with_context(|| {
format!("failed to stream file into WSL after /mnt copy failed: {cp_err:#}")
})?;
}
}
log::info!("uploaded remote server in {:?}", t0.elapsed());
Ok(())
}
async fn copy_via_wslpath_and_cp(&self, src_path: &Path, dst_path: &RelPath) -> Result<()> {
let src_path_in_wsl = self.windows_path_to_wsl_path(src_path).await?;
let cp = self.shell_kind.prepend_command_prefix("cp");
self.run_wsl_command(
@ -312,9 +335,57 @@ impl WslRemoteConnection {
dst_path,
e
)
})
}
async fn stream_file_into_wsl(&self, src_path: &Path, dst_path: &RelPath) -> Result<()> {
let mut file = fs::File::open(src_path).await.with_context(|| {
format!(
"failed to open {} for streaming into WSL",
src_path.display()
)
})?;
log::info!("uploaded remote server in {:?}", t0.elapsed());
let dst_posix = dst_path.display(PathStyle::Unix);
let mut command = wsl_command_impl(
&self.connection_options,
"sh",
&["-c", "cat > \"$1\"", "zed-upload", dst_posix.as_ref()],
true,
);
command.kill_on_drop(true);
let mut child = command
.spawn()
.context("failed to spawn wsl.exe for stdin upload")?;
let mut stdin = child
.stdin
.take()
.context("wsl.exe child did not expose stdin")?;
let copy_result = io::copy(&mut file, &mut stdin).await;
let flush_result = stdin.flush().await;
drop(stdin);
let output = child
.output()
.await
.context("failed to await wsl.exe stdin-upload child")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!(
"wsl.exe stdin upload failed (status {:?}): {}",
output.status.code(),
stderr.trim()
);
}
copy_result.with_context(|| {
format!("failed to write {} into wsl.exe stdin", src_path.display())
})?;
flush_result.context("failed to flush wsl.exe stdin")?;
Ok(())
}