1use crate::{
2 RemoteClientDelegate, RemotePlatform,
3 remote_client::{CommandTemplate, RemoteConnection, RemoteConnectionOptions},
4};
5use anyhow::{Result, anyhow, bail};
6use async_trait::async_trait;
7use collections::HashMap;
8use futures::channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender};
9use gpui::{App, AppContext as _, AsyncApp, SemanticVersion, Task};
10use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
11use rpc::proto::Envelope;
12use smol::{fs, process};
13use std::{
14 fmt::Write as _,
15 path::{Path, PathBuf},
16 process::Stdio,
17 sync::Arc,
18 time::Instant,
19};
20use util::paths::{PathStyle, RemotePathBuf};
21
22#[derive(Debug, Clone, PartialEq, Eq, Hash)]
23pub struct WslConnectionOptions {
24 pub distro_name: String,
25 pub user: Option<String>,
26}
27
28pub(crate) struct WslRemoteConnection {
29 remote_binary_path: Option<RemotePathBuf>,
30 platform: RemotePlatform,
31 shell: String,
32 default_system_shell: String,
33 connection_options: WslConnectionOptions,
34}
35
36impl WslRemoteConnection {
37 pub(crate) async fn new(
38 connection_options: WslConnectionOptions,
39 delegate: Arc<dyn RemoteClientDelegate>,
40 cx: &mut AsyncApp,
41 ) -> Result<Self> {
42 log::info!(
43 "Connecting to WSL distro {} with user {:?}",
44 connection_options.distro_name,
45 connection_options.user
46 );
47 let (release_channel, version, commit) = cx.update(|cx| {
48 (
49 ReleaseChannel::global(cx),
50 AppVersion::global(cx),
51 AppCommitSha::try_global(cx),
52 )
53 })?;
54
55 let mut this = Self {
56 connection_options,
57 remote_binary_path: None,
58 platform: RemotePlatform { os: "", arch: "" },
59 shell: String::new(),
60 default_system_shell: String::from("/bin/sh"),
61 };
62 delegate.set_status(Some("Detecting WSL environment"), cx);
63 this.platform = this.detect_platform().await?;
64 this.shell = this.detect_shell().await?;
65 this.remote_binary_path = Some(
66 this.ensure_server_binary(&delegate, release_channel, version, commit, cx)
67 .await?,
68 );
69
70 Ok(this)
71 }
72
73 async fn detect_platform(&self) -> Result<RemotePlatform> {
74 let arch_str = self.run_wsl_command("uname", &["-m"]).await?;
75 let arch_str = arch_str.trim().to_string();
76 let arch = match arch_str.as_str() {
77 "x86_64" => "x86_64",
78 "aarch64" | "arm64" => "aarch64",
79 _ => "x86_64",
80 };
81 Ok(RemotePlatform { os: "linux", arch })
82 }
83
84 async fn detect_shell(&self) -> Result<String> {
85 Ok(self
86 .run_wsl_command("sh", &["-c", "echo $SHELL"])
87 .await
88 .ok()
89 .and_then(|shell_path| {
90 Path::new(shell_path.trim())
91 .file_name()
92 .map(|it| it.to_str().unwrap().to_owned())
93 })
94 .unwrap_or_else(|| "bash".to_string()))
95 }
96
97 async fn windows_path_to_wsl_path(&self, source: &Path) -> Result<String> {
98 windows_path_to_wsl_path_impl(&self.connection_options, source).await
99 }
100
101 fn wsl_command(&self, program: &str, args: &[&str]) -> process::Command {
102 wsl_command_impl(&self.connection_options, program, args)
103 }
104
105 async fn run_wsl_command(&self, program: &str, args: &[&str]) -> Result<String> {
106 run_wsl_command_impl(&self.connection_options, program, args).await
107 }
108
109 async fn ensure_server_binary(
110 &self,
111 delegate: &Arc<dyn RemoteClientDelegate>,
112 release_channel: ReleaseChannel,
113 version: SemanticVersion,
114 commit: Option<AppCommitSha>,
115 cx: &mut AsyncApp,
116 ) -> Result<RemotePathBuf> {
117 let version_str = match release_channel {
118 ReleaseChannel::Nightly => {
119 let commit = commit.map(|s| s.full()).unwrap_or_default();
120 format!("{}-{}", version, commit)
121 }
122 ReleaseChannel::Dev => "build".to_string(),
123 _ => version.to_string(),
124 };
125
126 let binary_name = format!(
127 "zed-remote-server-{}-{}",
128 release_channel.dev_name(),
129 version_str
130 );
131
132 let dst_path = RemotePathBuf::new(
133 paths::remote_wsl_server_dir_relative().join(binary_name),
134 PathStyle::Posix,
135 );
136
137 if let Some(parent) = dst_path.parent() {
138 self.run_wsl_command("mkdir", &["-p", &parent.to_string()])
139 .await
140 .map_err(|e| anyhow!("Failed to create directory: {}", e))?;
141 }
142
143 #[cfg(debug_assertions)]
144 if let Some(remote_server_path) =
145 super::build_remote_server_from_source(&self.platform, delegate.as_ref(), cx).await?
146 {
147 let tmp_path = RemotePathBuf::new(
148 paths::remote_wsl_server_dir_relative().join(format!(
149 "download-{}-{}",
150 std::process::id(),
151 remote_server_path.file_name().unwrap().to_string_lossy()
152 )),
153 PathStyle::Posix,
154 );
155 self.upload_file(&remote_server_path, &tmp_path, delegate, cx)
156 .await?;
157 self.extract_and_install(&tmp_path, &dst_path, delegate, cx)
158 .await?;
159 return Ok(dst_path);
160 }
161
162 if self
163 .run_wsl_command(&dst_path.to_string(), &["version"])
164 .await
165 .is_ok()
166 {
167 return Ok(dst_path);
168 }
169
170 delegate.set_status(Some("Installing remote server"), cx);
171
172 let wanted_version = match release_channel {
173 ReleaseChannel::Nightly | ReleaseChannel::Dev => None,
174 _ => Some(cx.update(|cx| AppVersion::global(cx))?),
175 };
176
177 let src_path = delegate
178 .download_server_binary_locally(self.platform, release_channel, wanted_version, cx)
179 .await?;
180
181 let tmp_path = RemotePathBuf::new(
182 PathBuf::from(format!("{}.{}.gz", dst_path, std::process::id())),
183 PathStyle::Posix,
184 );
185
186 self.upload_file(&src_path, &tmp_path, delegate, cx).await?;
187 self.extract_and_install(&tmp_path, &dst_path, delegate, cx)
188 .await?;
189
190 Ok(dst_path)
191 }
192
193 async fn upload_file(
194 &self,
195 src_path: &Path,
196 dst_path: &RemotePathBuf,
197 delegate: &Arc<dyn RemoteClientDelegate>,
198 cx: &mut AsyncApp,
199 ) -> Result<()> {
200 delegate.set_status(Some("Uploading remote server to WSL"), cx);
201
202 if let Some(parent) = dst_path.parent() {
203 self.run_wsl_command("mkdir", &["-p", &parent.to_string()])
204 .await
205 .map_err(|e| anyhow!("Failed to create directory when uploading file: {}", e))?;
206 }
207
208 let t0 = Instant::now();
209 let src_stat = fs::metadata(&src_path).await?;
210 let size = src_stat.len();
211 log::info!(
212 "uploading remote server to WSL {:?} ({}kb)",
213 dst_path,
214 size / 1024
215 );
216
217 let src_path_in_wsl = self.windows_path_to_wsl_path(src_path).await?;
218 self.run_wsl_command("cp", &["-f", &src_path_in_wsl, &dst_path.to_string()])
219 .await
220 .map_err(|e| {
221 anyhow!(
222 "Failed to copy file {}({}) to WSL {:?}: {}",
223 src_path.display(),
224 src_path_in_wsl,
225 dst_path,
226 e
227 )
228 })?;
229
230 log::info!("uploaded remote server in {:?}", t0.elapsed());
231 Ok(())
232 }
233
234 async fn extract_and_install(
235 &self,
236 tmp_path: &RemotePathBuf,
237 dst_path: &RemotePathBuf,
238 delegate: &Arc<dyn RemoteClientDelegate>,
239 cx: &mut AsyncApp,
240 ) -> Result<()> {
241 delegate.set_status(Some("Extracting remote server"), cx);
242
243 let tmp_path_str = tmp_path.to_string();
244 let dst_path_str = dst_path.to_string();
245
246 // Build extraction script with proper error handling
247 let script = if tmp_path_str.ends_with(".gz") {
248 let uncompressed = tmp_path_str.trim_end_matches(".gz");
249 format!(
250 "set -e; gunzip -f '{}' && chmod 755 '{}' && mv -f '{}' '{}'",
251 tmp_path_str, uncompressed, uncompressed, dst_path_str
252 )
253 } else {
254 format!(
255 "set -e; chmod 755 '{}' && mv -f '{}' '{}'",
256 tmp_path_str, tmp_path_str, dst_path_str
257 )
258 };
259
260 self.run_wsl_command("sh", &["-c", &script])
261 .await
262 .map_err(|e| anyhow!("Failed to extract server binary: {}", e))?;
263 Ok(())
264 }
265}
266
267#[async_trait(?Send)]
268impl RemoteConnection for WslRemoteConnection {
269 fn start_proxy(
270 &self,
271 unique_identifier: String,
272 reconnect: bool,
273 incoming_tx: UnboundedSender<Envelope>,
274 outgoing_rx: UnboundedReceiver<Envelope>,
275 connection_activity_tx: Sender<()>,
276 delegate: Arc<dyn RemoteClientDelegate>,
277 cx: &mut AsyncApp,
278 ) -> Task<Result<i32>> {
279 delegate.set_status(Some("Starting proxy"), cx);
280
281 let Some(remote_binary_path) = &self.remote_binary_path else {
282 return Task::ready(Err(anyhow!("Remote binary path not set")));
283 };
284
285 let mut proxy_command = format!(
286 "exec {} proxy --identifier {}",
287 remote_binary_path, unique_identifier
288 );
289
290 if reconnect {
291 proxy_command.push_str(" --reconnect");
292 }
293
294 for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] {
295 if let Some(value) = std::env::var(env_var).ok() {
296 proxy_command = format!("{}='{}' {}", env_var, value, proxy_command);
297 }
298 }
299 let proxy_process = match self
300 .wsl_command("sh", &["-lc", &proxy_command])
301 .kill_on_drop(true)
302 .spawn()
303 {
304 Ok(process) => process,
305 Err(error) => {
306 return Task::ready(Err(anyhow!("failed to spawn remote server: {}", error)));
307 }
308 };
309
310 super::handle_rpc_messages_over_child_process_stdio(
311 proxy_process,
312 incoming_tx,
313 outgoing_rx,
314 connection_activity_tx,
315 cx,
316 )
317 }
318
319 fn upload_directory(
320 &self,
321 src_path: PathBuf,
322 dest_path: RemotePathBuf,
323 cx: &App,
324 ) -> Task<Result<()>> {
325 cx.background_spawn({
326 let options = self.connection_options.clone();
327 async move {
328 let wsl_src = windows_path_to_wsl_path_impl(&options, &src_path).await?;
329
330 run_wsl_command_impl(&options, "cp", &["-r", &wsl_src, &dest_path.to_string()])
331 .await
332 .map_err(|e| {
333 anyhow!(
334 "failed to upload directory {} -> {}: {}",
335 src_path.display(),
336 dest_path.to_string(),
337 e
338 )
339 })?;
340
341 Ok(())
342 }
343 })
344 }
345
346 async fn kill(&self) -> Result<()> {
347 Ok(())
348 }
349
350 fn has_been_killed(&self) -> bool {
351 false
352 }
353
354 fn shares_network_interface(&self) -> bool {
355 true
356 }
357
358 fn build_command(
359 &self,
360 program: Option<String>,
361 args: &[String],
362 env: &HashMap<String, String>,
363 working_dir: Option<String>,
364 port_forward: Option<(u16, String, u16)>,
365 ) -> Result<CommandTemplate> {
366 if port_forward.is_some() {
367 bail!("WSL shares the network interface with the host system");
368 }
369
370 let working_dir = working_dir
371 .map(|working_dir| RemotePathBuf::new(working_dir.into(), PathStyle::Posix).to_string())
372 .unwrap_or("~".to_string());
373
374 let mut script = String::new();
375
376 for (k, v) in env.iter() {
377 write!(&mut script, "{}='{}' ", k, v).unwrap();
378 }
379
380 if let Some(program) = program {
381 let command = shlex::try_quote(&program)?;
382 script.push_str(&command);
383 for arg in args {
384 let arg = shlex::try_quote(&arg)?;
385 script.push_str(" ");
386 script.push_str(&arg);
387 }
388 } else {
389 write!(&mut script, "exec {} -l", self.shell).unwrap();
390 }
391
392 let wsl_args = if let Some(user) = &self.connection_options.user {
393 vec![
394 "--distribution".to_string(),
395 self.connection_options.distro_name.clone(),
396 "--user".to_string(),
397 user.clone(),
398 "--cd".to_string(),
399 working_dir,
400 "--".to_string(),
401 self.shell.clone(),
402 "-c".to_string(),
403 shlex::try_quote(&script)?.to_string(),
404 ]
405 } else {
406 vec![
407 "--distribution".to_string(),
408 self.connection_options.distro_name.clone(),
409 "--cd".to_string(),
410 working_dir,
411 "--".to_string(),
412 self.shell.clone(),
413 "-c".to_string(),
414 shlex::try_quote(&script)?.to_string(),
415 ]
416 };
417
418 Ok(CommandTemplate {
419 program: "wsl.exe".to_string(),
420 args: wsl_args,
421 env: HashMap::default(),
422 })
423 }
424
425 fn connection_options(&self) -> RemoteConnectionOptions {
426 RemoteConnectionOptions::Wsl(self.connection_options.clone())
427 }
428
429 fn path_style(&self) -> PathStyle {
430 PathStyle::Posix
431 }
432
433 fn shell(&self) -> String {
434 self.shell.clone()
435 }
436
437 fn default_system_shell(&self) -> String {
438 self.default_system_shell.clone()
439 }
440}
441
442/// `wslpath` is a executable available in WSL, it's a linux binary.
443/// So it doesn't support Windows style paths.
444async fn sanitize_path(path: &Path) -> Result<String> {
445 let path = smol::fs::canonicalize(path).await?;
446 let path_str = path.to_string_lossy();
447
448 let sanitized = path_str.strip_prefix(r"\\?\").unwrap_or(&path_str);
449 Ok(sanitized.replace('\\', "/"))
450}
451
452async fn windows_path_to_wsl_path_impl(
453 options: &WslConnectionOptions,
454 source: &Path,
455) -> Result<String> {
456 let source = sanitize_path(source).await?;
457 run_wsl_command_impl(options, "wslpath", &["-u", &source]).await
458}
459
460fn wsl_command_impl(
461 options: &WslConnectionOptions,
462 program: &str,
463 args: &[&str],
464) -> process::Command {
465 let mut command = util::command::new_smol_command("wsl.exe");
466
467 if let Some(user) = &options.user {
468 command.arg("--user").arg(user);
469 }
470
471 command
472 .stdin(Stdio::piped())
473 .stdout(Stdio::piped())
474 .stderr(Stdio::piped())
475 .arg("--distribution")
476 .arg(&options.distro_name)
477 .arg("--cd")
478 .arg("~")
479 .arg(program)
480 .args(args);
481
482 command
483}
484
485async fn run_wsl_command_impl(
486 options: &WslConnectionOptions,
487 program: &str,
488 args: &[&str],
489) -> Result<String> {
490 let output = wsl_command_impl(options, program, args).output().await?;
491
492 if !output.status.success() {
493 return Err(anyhow!(
494 "Command '{}' failed: {}",
495 program,
496 String::from_utf8_lossy(&output.stderr).trim()
497 ));
498 }
499
500 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
501}