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