1use crate::{
2 RemoteArch, RemoteClientDelegate, RemoteOs, RemotePlatform,
3 remote_client::{CommandTemplate, Interactive, RemoteConnection, RemoteConnectionOptions},
4 transport::{parse_platform, parse_shell},
5};
6use anyhow::{Context, Result, anyhow, bail};
7use async_trait::async_trait;
8use collections::HashMap;
9use futures::channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender};
10use gpui::{App, AppContext as _, AsyncApp, Task};
11use release_channel::{AppVersion, ReleaseChannel};
12use rpc::proto::Envelope;
13use semver::Version;
14use smol::fs;
15use std::{
16 ffi::OsStr,
17 fmt::Write as _,
18 path::{Path, PathBuf},
19 sync::Arc,
20 time::Instant,
21};
22
23use util::{
24 command::Stdio,
25 paths::{PathStyle, RemotePathBuf},
26 rel_path::RelPath,
27 shell::{Shell, ShellKind},
28 shell_builder::ShellBuilder,
29};
30
31#[derive(
32 Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
33)]
34pub struct WslConnectionOptions {
35 pub distro_name: String,
36 pub user: Option<String>,
37}
38
39impl From<settings::WslConnection> for WslConnectionOptions {
40 fn from(val: settings::WslConnection) -> Self {
41 WslConnectionOptions {
42 distro_name: val.distro_name,
43 user: val.user,
44 }
45 }
46}
47
48#[derive(Debug)]
49pub(crate) struct WslRemoteConnection {
50 remote_binary_path: Option<Arc<RelPath>>,
51 platform: RemotePlatform,
52 shell: String,
53 shell_kind: ShellKind,
54 default_system_shell: String,
55 has_wsl_interop: bool,
56 connection_options: WslConnectionOptions,
57}
58
59impl WslRemoteConnection {
60 pub(crate) async fn new(
61 connection_options: WslConnectionOptions,
62 delegate: Arc<dyn RemoteClientDelegate>,
63 cx: &mut AsyncApp,
64 ) -> Result<Self> {
65 log::info!(
66 "Connecting to WSL distro {} with user {:?}",
67 connection_options.distro_name,
68 connection_options.user
69 );
70 let (release_channel, version) =
71 cx.update(|cx| (ReleaseChannel::global(cx), AppVersion::global(cx)));
72
73 let mut this = Self {
74 connection_options,
75 remote_binary_path: None,
76 platform: RemotePlatform {
77 os: RemoteOs::Linux,
78 arch: RemoteArch::X86_64,
79 },
80 shell: String::new(),
81 shell_kind: ShellKind::Posix,
82 default_system_shell: String::from("/bin/sh"),
83 has_wsl_interop: false,
84 };
85 delegate.set_status(Some("Detecting WSL environment"), cx);
86 this.shell = this
87 .detect_shell()
88 .await
89 .context("failed detecting shell")?;
90 log::info!("Remote shell discovered: {}", this.shell);
91 this.shell_kind = ShellKind::new(&this.shell, false);
92 this.has_wsl_interop = this.detect_has_wsl_interop().await.unwrap_or_default();
93 log::info!(
94 "Remote has wsl interop {}",
95 if this.has_wsl_interop {
96 "enabled"
97 } else {
98 "disabled"
99 }
100 );
101 this.platform = this
102 .detect_platform()
103 .await
104 .context("failed detecting platform")?;
105 log::info!("Remote platform discovered: {:?}", this.platform);
106 this.remote_binary_path = Some(
107 this.ensure_server_binary(&delegate, release_channel, version, cx)
108 .await
109 .context("failed ensuring server binary")?,
110 );
111 log::debug!("Detected WSL environment: {this:#?}");
112
113 Ok(this)
114 }
115
116 async fn detect_platform(&self) -> Result<RemotePlatform> {
117 let program = self.shell_kind.prepend_command_prefix("uname");
118 let output = self.run_wsl_command_with_output(&program, &["-sm"]).await?;
119 parse_platform(&output)
120 }
121
122 async fn detect_shell(&self) -> Result<String> {
123 const DEFAULT_SHELL: &str = "sh";
124 match self
125 .run_wsl_command_with_output("sh", &["-c", "echo $SHELL"])
126 .await
127 {
128 Ok(output) => Ok(parse_shell(&output, DEFAULT_SHELL)),
129 Err(e) => {
130 log::error!("Failed to detect remote shell: {e}");
131 Ok(DEFAULT_SHELL.to_owned())
132 }
133 }
134 }
135
136 async fn detect_has_wsl_interop(&self) -> Result<bool> {
137 let interop = match self
138 .run_wsl_command_with_output("cat", &["/proc/sys/fs/binfmt_misc/WSLInterop"])
139 .await
140 {
141 Ok(interop) => interop,
142 Err(err) => self
143 .run_wsl_command_with_output("cat", &["/proc/sys/fs/binfmt_misc/WSLInterop-late"])
144 .await
145 .inspect_err(|err2| log::error!("Failed to detect wsl interop: {err}; {err2}"))?,
146 };
147 Ok(interop.contains("enabled"))
148 }
149
150 async fn windows_path_to_wsl_path(&self, source: &Path) -> Result<String> {
151 windows_path_to_wsl_path_impl(&self.connection_options, source).await
152 }
153
154 async fn run_wsl_command_with_output(&self, program: &str, args: &[&str]) -> Result<String> {
155 run_wsl_command_with_output_impl(&self.connection_options, program, args).await
156 }
157
158 async fn run_wsl_command(&self, program: &str, args: &[&str]) -> Result<()> {
159 run_wsl_command_impl(wsl_command_impl(
160 &self.connection_options,
161 program,
162 args,
163 false,
164 ))
165 .await
166 .map(|_| ())
167 }
168
169 async fn ensure_server_binary(
170 &self,
171 delegate: &Arc<dyn RemoteClientDelegate>,
172 release_channel: ReleaseChannel,
173 version: Version,
174 cx: &mut AsyncApp,
175 ) -> Result<Arc<RelPath>> {
176 let version_str = match release_channel {
177 ReleaseChannel::Dev => "build".to_string(),
178 _ => version.to_string(),
179 };
180
181 let binary_name = format!(
182 "zed-remote-server-{}-{}",
183 release_channel.dev_name(),
184 version_str
185 );
186
187 let dst_path =
188 paths::remote_server_dir_relative().join(RelPath::unix(&binary_name).unwrap());
189
190 if let Some(parent) = dst_path.parent() {
191 let parent = parent.display(PathStyle::Posix);
192 let mkdir = self.shell_kind.prepend_command_prefix("mkdir");
193 self.run_wsl_command(&mkdir, &["-p", &parent])
194 .await
195 .map_err(|e| anyhow!("Failed to create directory: {}", e))?;
196 }
197
198 let binary_exists_on_server = self
199 .run_wsl_command(&dst_path.display(PathStyle::Posix), &["version"])
200 .await
201 .is_ok();
202
203 #[cfg(any(debug_assertions, feature = "build-remote-server-binary"))]
204 if let Some(remote_server_path) = super::build_remote_server_from_source(
205 &self.platform,
206 delegate.as_ref(),
207 binary_exists_on_server,
208 cx,
209 )
210 .await?
211 {
212 let tmp_path = paths::remote_server_dir_relative().join(
213 &RelPath::unix(&format!(
214 "download-{}-{}",
215 std::process::id(),
216 remote_server_path.file_name().unwrap().to_string_lossy()
217 ))
218 .unwrap(),
219 );
220 self.upload_file(&remote_server_path, &tmp_path, delegate, cx)
221 .await?;
222 self.extract_and_install(&tmp_path, &dst_path, delegate, cx)
223 .await?;
224 return Ok(dst_path);
225 }
226
227 if binary_exists_on_server {
228 return Ok(dst_path);
229 }
230
231 let wanted_version = match release_channel {
232 ReleaseChannel::Nightly | ReleaseChannel::Dev => None,
233 _ => Some(cx.update(|cx| AppVersion::global(cx))),
234 };
235
236 let src_path = delegate
237 .download_server_binary_locally(self.platform, release_channel, wanted_version, cx)
238 .await?;
239
240 let tmp_path = format!(
241 "{}.{}.gz",
242 dst_path.display(PathStyle::Posix),
243 std::process::id()
244 );
245 let tmp_path = RelPath::unix(&tmp_path).unwrap();
246
247 self.upload_file(&src_path, &tmp_path, delegate, cx).await?;
248 self.extract_and_install(&tmp_path, &dst_path, delegate, cx)
249 .await?;
250
251 Ok(dst_path)
252 }
253
254 async fn upload_file(
255 &self,
256 src_path: &Path,
257 dst_path: &RelPath,
258 delegate: &Arc<dyn RemoteClientDelegate>,
259 cx: &mut AsyncApp,
260 ) -> Result<()> {
261 delegate.set_status(Some("Uploading remote server"), cx);
262
263 if let Some(parent) = dst_path.parent() {
264 let parent = parent.display(PathStyle::Posix);
265 let mkdir = self.shell_kind.prepend_command_prefix("mkdir");
266 self.run_wsl_command(&mkdir, &["-p", &parent])
267 .await
268 .context("Failed to create directory when uploading file")?;
269 }
270
271 let t0 = Instant::now();
272 let src_stat = fs::metadata(&src_path)
273 .await
274 .with_context(|| format!("source path does not exist: {}", src_path.display()))?;
275 let size = src_stat.len();
276 log::info!(
277 "uploading remote server to WSL {:?} ({}kb)",
278 dst_path,
279 size / 1024
280 );
281
282 let src_path_in_wsl = self.windows_path_to_wsl_path(src_path).await?;
283 let cp = self.shell_kind.prepend_command_prefix("cp");
284 self.run_wsl_command(
285 &cp,
286 &["-f", &src_path_in_wsl, &dst_path.display(PathStyle::Posix)],
287 )
288 .await
289 .map_err(|e| {
290 anyhow!(
291 "Failed to copy file {}({}) to WSL {:?}: {}",
292 src_path.display(),
293 src_path_in_wsl,
294 dst_path,
295 e
296 )
297 })?;
298
299 log::info!("uploaded remote server in {:?}", t0.elapsed());
300 Ok(())
301 }
302
303 async fn extract_and_install(
304 &self,
305 tmp_path: &RelPath,
306 dst_path: &RelPath,
307 delegate: &Arc<dyn RemoteClientDelegate>,
308 cx: &mut AsyncApp,
309 ) -> Result<()> {
310 delegate.set_status(Some("Extracting remote server"), cx);
311
312 let tmp_path_str = tmp_path.display(PathStyle::Posix);
313 let dst_path_str = dst_path.display(PathStyle::Posix);
314
315 // Build extraction script with proper error handling
316 let script = if tmp_path_str.ends_with(".gz") {
317 let uncompressed = tmp_path_str.trim_end_matches(".gz");
318 format!(
319 "set -e; gunzip -f '{}' && chmod 755 '{}' && mv -f '{}' '{}'",
320 tmp_path_str, uncompressed, uncompressed, dst_path_str
321 )
322 } else {
323 format!(
324 "set -e; chmod 755 '{}' && mv -f '{}' '{}'",
325 tmp_path_str, tmp_path_str, dst_path_str
326 )
327 };
328
329 self.run_wsl_command("sh", &["-c", &script])
330 .await
331 .map_err(|e| anyhow!("Failed to extract server binary: {}", e))?;
332 Ok(())
333 }
334}
335
336#[async_trait(?Send)]
337impl RemoteConnection for WslRemoteConnection {
338 fn start_proxy(
339 &self,
340 unique_identifier: String,
341 reconnect: bool,
342 incoming_tx: UnboundedSender<Envelope>,
343 outgoing_rx: UnboundedReceiver<Envelope>,
344 connection_activity_tx: Sender<()>,
345 delegate: Arc<dyn RemoteClientDelegate>,
346 cx: &mut AsyncApp,
347 ) -> Task<Result<i32>> {
348 delegate.set_status(Some("Starting proxy"), cx);
349
350 let Some(remote_binary_path) = &self.remote_binary_path else {
351 return Task::ready(Err(anyhow!("Remote binary path not set")));
352 };
353
354 let mut proxy_args = vec![];
355 for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] {
356 if let Some(value) = std::env::var(env_var).ok() {
357 // We don't quote the value here as it seems excessive and may result in invalid envs for the
358 // proxy server. For example, `RUST_LOG='debug'` will result in a warning "invalid logging spec 'debug'', ignoring it"
359 // in the proxy server. Therefore, we pass the env vars as is.
360 proxy_args.push(format!("{}={}", env_var, value));
361 }
362 }
363
364 proxy_args.push(remote_binary_path.display(PathStyle::Posix).into_owned());
365 proxy_args.push("proxy".to_owned());
366 proxy_args.push("--identifier".to_owned());
367 proxy_args.push(unique_identifier);
368
369 if reconnect {
370 proxy_args.push("--reconnect".to_owned());
371 }
372
373 let proxy_process =
374 match wsl_command_impl(&self.connection_options, "env", &proxy_args, true)
375 .kill_on_drop(true)
376 .spawn()
377 {
378 Ok(process) => process,
379 Err(error) => {
380 return Task::ready(Err(anyhow!("failed to spawn remote server: {}", error)));
381 }
382 };
383
384 super::handle_rpc_messages_over_child_process_stdio(
385 proxy_process,
386 incoming_tx,
387 outgoing_rx,
388 connection_activity_tx,
389 cx,
390 )
391 }
392
393 fn upload_directory(
394 &self,
395 src_path: PathBuf,
396 dest_path: RemotePathBuf,
397 cx: &App,
398 ) -> Task<Result<()>> {
399 cx.background_spawn({
400 let options = self.connection_options.clone();
401 async move {
402 let wsl_src = windows_path_to_wsl_path_impl(&options, &src_path).await?;
403 let command = wsl_command_impl(
404 &options,
405 "cp",
406 &["-r", &wsl_src, &dest_path.to_string()],
407 true,
408 );
409 run_wsl_command_impl(command).await.map_err(|e| {
410 anyhow!(
411 "failed to upload directory {} -> {}: {}",
412 src_path.display(),
413 dest_path,
414 e
415 )
416 })?;
417
418 Ok(())
419 }
420 })
421 }
422
423 async fn kill(&self) -> Result<()> {
424 Ok(())
425 }
426
427 fn has_been_killed(&self) -> bool {
428 false
429 }
430
431 fn shares_network_interface(&self) -> bool {
432 true
433 }
434
435 fn build_command(
436 &self,
437 program: Option<String>,
438 args: &[String],
439 env: &HashMap<String, String>,
440 working_dir: Option<String>,
441 port_forward: Option<(u16, String, u16)>,
442 _interactive: Interactive,
443 ) -> Result<CommandTemplate> {
444 if port_forward.is_some() {
445 bail!("WSL shares the network interface with the host system");
446 }
447
448 let shell_kind = self.shell_kind;
449 let working_dir = working_dir
450 .map(|working_dir| RemotePathBuf::new(working_dir, PathStyle::Posix).to_string())
451 .unwrap_or("~".to_string());
452
453 let mut exec = String::from("exec env ");
454
455 for (key, value) in env.iter() {
456 let assignment = format!("{key}={value}");
457 let assignment = shell_kind.try_quote(&assignment).context("shell quoting")?;
458 write!(exec, "{assignment} ")?;
459 }
460
461 if let Some(program) = program {
462 write!(
463 exec,
464 "{}",
465 shell_kind
466 .try_quote_prefix_aware(&program)
467 .context("shell quoting")?
468 )?;
469 for arg in args {
470 let arg = shell_kind.try_quote(&arg).context("shell quoting")?;
471 write!(exec, " {}", &arg)?;
472 }
473 } else {
474 write!(&mut exec, "{} -l", self.shell)?;
475 }
476 let (command, args) =
477 ShellBuilder::new(&Shell::Program(self.shell.clone()), false).build(Some(exec), &[]);
478
479 let mut wsl_args = if let Some(user) = &self.connection_options.user {
480 vec![
481 "--distribution".to_string(),
482 self.connection_options.distro_name.clone(),
483 "--user".to_string(),
484 user.clone(),
485 "--cd".to_string(),
486 working_dir,
487 "--".to_string(),
488 command,
489 ]
490 } else {
491 vec![
492 "--distribution".to_string(),
493 self.connection_options.distro_name.clone(),
494 "--cd".to_string(),
495 working_dir,
496 "--".to_string(),
497 command,
498 ]
499 };
500 wsl_args.extend(args);
501
502 Ok(CommandTemplate {
503 program: "wsl.exe".to_string(),
504 args: wsl_args,
505 env: HashMap::default(),
506 })
507 }
508
509 fn build_forward_ports_command(
510 &self,
511 _: Vec<(u16, String, u16)>,
512 ) -> anyhow::Result<CommandTemplate> {
513 Err(anyhow!("WSL shares a network interface with the host"))
514 }
515
516 fn connection_options(&self) -> RemoteConnectionOptions {
517 RemoteConnectionOptions::Wsl(self.connection_options.clone())
518 }
519
520 fn path_style(&self) -> PathStyle {
521 PathStyle::Posix
522 }
523
524 fn shell(&self) -> String {
525 self.shell.clone()
526 }
527
528 fn default_system_shell(&self) -> String {
529 self.default_system_shell.clone()
530 }
531
532 fn has_wsl_interop(&self) -> bool {
533 self.has_wsl_interop
534 }
535}
536
537/// `wslpath` is a executable available in WSL, it's a linux binary.
538/// So it doesn't support Windows style paths.
539async fn sanitize_path(path: &Path) -> Result<String> {
540 let path = smol::fs::canonicalize(path)
541 .await
542 .with_context(|| format!("Failed to canonicalize path {}", path.display()))?;
543 let path_str = path.to_string_lossy();
544
545 let sanitized = path_str.strip_prefix(r"\\?\").unwrap_or(&path_str);
546 Ok(sanitized.replace('\\', "/"))
547}
548
549fn run_wsl_command_with_output_impl(
550 options: &WslConnectionOptions,
551 program: &str,
552 args: &[&str],
553) -> impl Future<Output = Result<String>> + use<> {
554 let exec_command = wsl_command_impl(options, program, args, true);
555 let command = wsl_command_impl(options, program, args, false);
556 async move {
557 match run_wsl_command_impl(exec_command).await {
558 Ok(res) => Ok(res),
559 Err(exec_err) => match run_wsl_command_impl(command).await {
560 Ok(res) => Ok(res),
561 Err(e) => Err(e.context(exec_err)),
562 },
563 }
564 }
565}
566
567impl WslConnectionOptions {
568 pub fn abs_windows_path_to_wsl_path(
569 &self,
570 source: &Path,
571 ) -> impl Future<Output = Result<String>> + use<> {
572 let path_str = source.to_string_lossy();
573
574 let source = path_str.strip_prefix(r"\\?\").unwrap_or(&*path_str);
575 let source = source.replace('\\', "/");
576 run_wsl_command_with_output_impl(self, "wslpath", &["-u", &source])
577 }
578}
579
580async fn windows_path_to_wsl_path_impl(
581 options: &WslConnectionOptions,
582 source: &Path,
583) -> Result<String> {
584 let source = sanitize_path(source).await?;
585 run_wsl_command_with_output_impl(options, "wslpath", &["-u", &source]).await
586}
587
588/// Converts a WSL/POSIX path to a Windows path using `wslpath -w`.
589///
590/// For example, `/home/user/project` becomes `\\wsl.localhost\Ubuntu\home\user\project`
591#[cfg(target_os = "windows")]
592pub fn wsl_path_to_windows_path(
593 options: &WslConnectionOptions,
594 wsl_path: &Path,
595) -> impl Future<Output = Result<PathBuf>> + use<> {
596 let wsl_path_str = wsl_path.to_string_lossy().to_string();
597 let command = wsl_command_impl(options, "wslpath", &["-w", &wsl_path_str], true);
598 async move {
599 let windows_path = run_wsl_command_impl(command).await?;
600 Ok(PathBuf::from(windows_path))
601 }
602}
603
604fn run_wsl_command_impl(
605 mut command: util::command::Command,
606) -> impl Future<Output = Result<String>> {
607 async move {
608 let output = command
609 .output()
610 .await
611 .with_context(|| format!("Failed to run command '{:?}'", command))?;
612
613 if !output.status.success() {
614 return Err(anyhow!(
615 "Command '{:?}' failed: {}",
616 command,
617 String::from_utf8_lossy(&output.stderr).trim()
618 ));
619 }
620
621 Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned())
622 }
623}
624
625/// Creates a new `wsl.exe` command that runs the given program with the given arguments.
626///
627/// If `exec` is true, the command will be executed in the WSL environment without spawning a new shell.
628fn wsl_command_impl(
629 options: &WslConnectionOptions,
630 program: &str,
631 args: &[impl AsRef<OsStr>],
632 exec: bool,
633) -> util::command::Command {
634 let mut command = util::command::new_command("wsl.exe");
635
636 if let Some(user) = &options.user {
637 command.arg("--user").arg(user);
638 }
639
640 command
641 .stdin(Stdio::piped())
642 .stdout(Stdio::piped())
643 .stderr(Stdio::piped())
644 .arg("--distribution")
645 .arg(&options.distro_name)
646 .arg("--cd")
647 .arg("~");
648
649 if exec {
650 command.arg("--exec");
651 }
652
653 command.arg(program).args(args);
654
655 log::debug!("wsl {:?}", command);
656 command
657}