1use crate::{
2 RemoteArch, RemoteOs, RemotePlatform,
3 json_log::LogRecord,
4 protocol::{MESSAGE_LEN_SIZE, message_len_from_buffer, read_message_with_len, write_message},
5};
6use anyhow::{Context as _, Result};
7use futures::{
8 AsyncReadExt as _, FutureExt as _, StreamExt as _,
9 channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender},
10};
11use gpui::{AppContext as _, AsyncApp, Task};
12use rpc::proto::Envelope;
13use smol::process::Child;
14
15pub mod docker;
16pub mod ssh;
17pub mod wsl;
18
19/// Parses the output of `uname -sm` to determine the remote platform.
20/// Takes the last line to skip possible shell initialization output.
21fn parse_platform(output: &str) -> Result<RemotePlatform> {
22 let output = output.trim();
23 let uname = output.rsplit_once('\n').map_or(output, |(_, last)| last);
24 let Some((os, arch)) = uname.split_once(" ") else {
25 anyhow::bail!("unknown uname: {uname:?}")
26 };
27
28 let os = match os {
29 "Darwin" => RemoteOs::MacOs,
30 "Linux" => RemoteOs::Linux,
31 _ => anyhow::bail!(
32 "Prebuilt remote servers are not yet available for {os:?}. See https://zed.dev/docs/remote-development"
33 ),
34 };
35
36 // exclude armv5,6,7 as they are 32-bit.
37 let arch = if arch.starts_with("armv8")
38 || arch.starts_with("armv9")
39 || arch.starts_with("arm64")
40 || arch.starts_with("aarch64")
41 {
42 RemoteArch::Aarch64
43 } else if arch.starts_with("x86") {
44 RemoteArch::X86_64
45 } else {
46 anyhow::bail!(
47 "Prebuilt remote servers are not yet available for {arch:?}. See https://zed.dev/docs/remote-development"
48 )
49 };
50
51 Ok(RemotePlatform { os, arch })
52}
53
54/// Parses the output of `echo $SHELL` to determine the remote shell.
55/// Takes the last line to skip possible shell initialization output.
56fn parse_shell(output: &str, fallback_shell: &str) -> String {
57 let output = output.trim();
58 let shell = output.rsplit_once('\n').map_or(output, |(_, last)| last);
59 if shell.is_empty() {
60 log::error!("$SHELL is not set, falling back to {fallback_shell}");
61 fallback_shell.to_owned()
62 } else {
63 shell.to_owned()
64 }
65}
66
67fn handle_rpc_messages_over_child_process_stdio(
68 mut remote_proxy_process: Child,
69 incoming_tx: UnboundedSender<Envelope>,
70 mut outgoing_rx: UnboundedReceiver<Envelope>,
71 mut connection_activity_tx: Sender<()>,
72 cx: &AsyncApp,
73) -> Task<Result<i32>> {
74 let mut child_stderr = remote_proxy_process.stderr.take().unwrap();
75 let mut child_stdout = remote_proxy_process.stdout.take().unwrap();
76 let mut child_stdin = remote_proxy_process.stdin.take().unwrap();
77
78 let mut stdin_buffer = Vec::new();
79 let mut stdout_buffer = Vec::new();
80 let mut stderr_buffer = Vec::new();
81 let mut stderr_offset = 0;
82
83 let stdin_task = cx.background_spawn(async move {
84 while let Some(outgoing) = outgoing_rx.next().await {
85 write_message(&mut child_stdin, &mut stdin_buffer, outgoing).await?;
86 }
87 anyhow::Ok(())
88 });
89
90 let stdout_task = cx.background_spawn({
91 let mut connection_activity_tx = connection_activity_tx.clone();
92 async move {
93 loop {
94 stdout_buffer.resize(MESSAGE_LEN_SIZE, 0);
95 let len = child_stdout.read(&mut stdout_buffer).await?;
96
97 if len == 0 {
98 return anyhow::Ok(());
99 }
100
101 if len < MESSAGE_LEN_SIZE {
102 child_stdout.read_exact(&mut stdout_buffer[len..]).await?;
103 }
104
105 let message_len = message_len_from_buffer(&stdout_buffer);
106 let envelope =
107 read_message_with_len(&mut child_stdout, &mut stdout_buffer, message_len)
108 .await?;
109 connection_activity_tx.try_send(()).ok();
110 incoming_tx.unbounded_send(envelope).ok();
111 }
112 }
113 });
114
115 let stderr_task: Task<anyhow::Result<()>> = cx.background_spawn(async move {
116 loop {
117 stderr_buffer.resize(stderr_offset + 1024, 0);
118
119 let len = child_stderr
120 .read(&mut stderr_buffer[stderr_offset..])
121 .await?;
122 if len == 0 {
123 return anyhow::Ok(());
124 }
125
126 stderr_offset += len;
127 let mut start_ix = 0;
128 while let Some(ix) = stderr_buffer[start_ix..stderr_offset]
129 .iter()
130 .position(|b| b == &b'\n')
131 {
132 let line_ix = start_ix + ix;
133 let content = &stderr_buffer[start_ix..line_ix];
134 start_ix = line_ix + 1;
135 if let Ok(record) = serde_json::from_slice::<LogRecord>(content) {
136 record.log(log::logger())
137 } else {
138 eprintln!("(remote) {}", String::from_utf8_lossy(content));
139 }
140 }
141 stderr_buffer.drain(0..start_ix);
142 stderr_offset -= start_ix;
143
144 connection_activity_tx.try_send(()).ok();
145 }
146 });
147
148 cx.background_spawn(async move {
149 let result = futures::select! {
150 result = stdin_task.fuse() => {
151 result.context("stdin")
152 }
153 result = stdout_task.fuse() => {
154 result.context("stdout")
155 }
156 result = stderr_task.fuse() => {
157 result.context("stderr")
158 }
159 };
160 let status = remote_proxy_process.status().await?.code().unwrap_or(1);
161 if status != 0 {
162 anyhow::bail!("Remote server exited with status {status}");
163 }
164 match result {
165 Ok(_) => Ok(status),
166 Err(error) => Err(error),
167 }
168 })
169}
170
171#[cfg(debug_assertions)]
172async fn build_remote_server_from_source(
173 platform: &crate::RemotePlatform,
174 delegate: &dyn crate::RemoteClientDelegate,
175 cx: &mut AsyncApp,
176) -> Result<Option<std::path::PathBuf>> {
177 use smol::process::{Command, Stdio};
178 use std::env::VarError;
179 use std::path::Path;
180 use util::command::new_smol_command;
181
182 // By default, we make building remote server from source opt-out and we do not force artifact compression
183 // for quicker builds.
184 let build_remote_server =
185 std::env::var("ZED_BUILD_REMOTE_SERVER").unwrap_or("nocompress".into());
186
187 if let "false" | "no" | "off" | "0" = &*build_remote_server {
188 return Ok(None);
189 }
190
191 async fn run_cmd(command: &mut Command) -> Result<()> {
192 let output = command
193 .kill_on_drop(true)
194 .stderr(Stdio::inherit())
195 .output()
196 .await?;
197 anyhow::ensure!(
198 output.status.success(),
199 "Failed to run command: {command:?}: output: {}",
200 String::from_utf8_lossy(&output.stderr)
201 );
202 Ok(())
203 }
204
205 let use_musl = !build_remote_server.contains("nomusl");
206 let triple = format!(
207 "{}-{}",
208 platform.arch,
209 match platform.os {
210 RemoteOs::Linux =>
211 if use_musl {
212 "unknown-linux-musl"
213 } else {
214 "unknown-linux-gnu"
215 },
216 RemoteOs::MacOs => "apple-darwin",
217 RemoteOs::Windows if cfg!(windows) => "pc-windows-msvc",
218 RemoteOs::Windows => "pc-windows-gnu",
219 }
220 );
221 let mut rust_flags = match std::env::var("RUSTFLAGS") {
222 Ok(val) => val,
223 Err(VarError::NotPresent) => String::new(),
224 Err(e) => {
225 log::error!("Failed to get env var `RUSTFLAGS` value: {e}");
226 String::new()
227 }
228 };
229 if platform.os == RemoteOs::Linux && use_musl {
230 rust_flags.push_str(" -C target-feature=+crt-static");
231
232 if let Ok(path) = std::env::var("ZED_ZSTD_MUSL_LIB") {
233 rust_flags.push_str(&format!(" -C link-arg=-L{path}"));
234 }
235 }
236 if build_remote_server.contains("mold") {
237 rust_flags.push_str(" -C link-arg=-fuse-ld=mold");
238 }
239
240 if platform.arch.as_str() == std::env::consts::ARCH
241 && platform.os.as_str() == std::env::consts::OS
242 {
243 delegate.set_status(Some("Building remote server binary from source"), cx);
244 log::info!("building remote server binary from source");
245 run_cmd(
246 new_smol_command("cargo")
247 .current_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/../.."))
248 .args([
249 "build",
250 "--package",
251 "remote_server",
252 "--features",
253 "debug-embed",
254 "--target-dir",
255 "target/remote_server",
256 "--target",
257 &triple,
258 ])
259 .env("RUSTFLAGS", &rust_flags),
260 )
261 .await?;
262 } else {
263 if which("zig", cx).await?.is_none() {
264 anyhow::bail!(if cfg!(not(windows)) {
265 "zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup)"
266 } else {
267 "zig not found on $PATH, install zig (use `winget install -e --id zig.zig` or see https://ziglang.org/learn/getting-started or use zigup)"
268 });
269 }
270
271 let rustup = which("rustup", cx)
272 .await?
273 .context("rustup not found on $PATH, install rustup (see https://rustup.rs/)")?;
274 delegate.set_status(Some("Adding rustup target for cross-compilation"), cx);
275 log::info!("adding rustup target");
276 run_cmd(
277 new_smol_command(rustup)
278 .args(["target", "add"])
279 .arg(&triple),
280 )
281 .await?;
282
283 if which("cargo-zigbuild", cx).await?.is_none() {
284 delegate.set_status(Some("Installing cargo-zigbuild for cross-compilation"), cx);
285 log::info!("installing cargo-zigbuild");
286 run_cmd(new_smol_command("cargo").args(["install", "--locked", "cargo-zigbuild"]))
287 .await?;
288 }
289
290 delegate.set_status(
291 Some(&format!(
292 "Building remote binary from source for {triple} with Zig"
293 )),
294 cx,
295 );
296 log::info!("building remote binary from source for {triple} with Zig");
297 run_cmd(
298 new_smol_command("cargo")
299 .args([
300 "zigbuild",
301 "--package",
302 "remote_server",
303 "--features",
304 "debug-embed",
305 "--target-dir",
306 "target/remote_server",
307 "--target",
308 &triple,
309 ])
310 .env("RUSTFLAGS", &rust_flags),
311 )
312 .await?;
313 };
314 let bin_path = Path::new("target")
315 .join("remote_server")
316 .join(&triple)
317 .join("debug")
318 .join("remote_server")
319 .with_extension(if platform.os.is_windows() { "exe" } else { "" });
320
321 let path = if !build_remote_server.contains("nocompress") {
322 delegate.set_status(Some("Compressing binary"), cx);
323
324 #[cfg(not(target_os = "windows"))]
325 {
326 run_cmd(new_smol_command("gzip").args(["-f", &bin_path.to_string_lossy()])).await?;
327 }
328
329 #[cfg(target_os = "windows")]
330 {
331 // On Windows, we use 7z to compress the binary
332
333 let seven_zip = which("7z.exe",cx)
334 .await?
335 .context("7z.exe not found on $PATH, install it (e.g. with `winget install -e --id 7zip.7zip`) or, if you don't want this behaviour, set $env:ZED_BUILD_REMOTE_SERVER=\"nocompress\"")?;
336 let gz_path = format!("target/remote_server/{}/debug/remote_server.gz", triple);
337 if smol::fs::metadata(&gz_path).await.is_ok() {
338 smol::fs::remove_file(&gz_path).await?;
339 }
340 run_cmd(new_smol_command(seven_zip).args([
341 "a",
342 "-tgzip",
343 &gz_path,
344 &bin_path.to_string_lossy(),
345 ]))
346 .await?;
347 }
348
349 let mut archive_path = bin_path;
350 archive_path.set_extension("gz");
351 std::env::current_dir()?.join(archive_path)
352 } else {
353 bin_path
354 };
355
356 Ok(Some(path))
357}
358
359#[cfg(debug_assertions)]
360async fn which(
361 binary_name: impl AsRef<str>,
362 cx: &mut AsyncApp,
363) -> Result<Option<std::path::PathBuf>> {
364 let binary_name = binary_name.as_ref().to_string();
365 let binary_name_cloned = binary_name.clone();
366 let res = cx
367 .background_spawn(async move { which::which(binary_name_cloned) })
368 .await;
369 match res {
370 Ok(path) => Ok(Some(path)),
371 Err(which::Error::CannotFindBinaryPath) => Ok(None),
372 Err(err) => Err(anyhow::anyhow!(
373 "Failed to run 'which' to find the binary '{binary_name}': {err}"
374 )),
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 #[test]
383 fn test_parse_platform() {
384 let result = parse_platform("Linux x86_64\n").unwrap();
385 assert_eq!(result.os, RemoteOs::Linux);
386 assert_eq!(result.arch, RemoteArch::X86_64);
387
388 let result = parse_platform("Darwin arm64\n").unwrap();
389 assert_eq!(result.os, RemoteOs::MacOs);
390 assert_eq!(result.arch, RemoteArch::Aarch64);
391
392 let result = parse_platform("Linux x86_64").unwrap();
393 assert_eq!(result.os, RemoteOs::Linux);
394 assert_eq!(result.arch, RemoteArch::X86_64);
395
396 let result = parse_platform("some shell init output\nLinux aarch64\n").unwrap();
397 assert_eq!(result.os, RemoteOs::Linux);
398 assert_eq!(result.arch, RemoteArch::Aarch64);
399
400 let result = parse_platform("some shell init output\nLinux aarch64").unwrap();
401 assert_eq!(result.os, RemoteOs::Linux);
402 assert_eq!(result.arch, RemoteArch::Aarch64);
403
404 assert_eq!(
405 parse_platform("Linux armv8l\n").unwrap().arch,
406 RemoteArch::Aarch64
407 );
408 assert_eq!(
409 parse_platform("Linux aarch64\n").unwrap().arch,
410 RemoteArch::Aarch64
411 );
412 assert_eq!(
413 parse_platform("Linux x86_64\n").unwrap().arch,
414 RemoteArch::X86_64
415 );
416
417 let result = parse_platform(
418 r#"Linux x86_64 - What you're referring to as Linux, is in fact, GNU/Linux...\n"#,
419 )
420 .unwrap();
421 assert_eq!(result.os, RemoteOs::Linux);
422 assert_eq!(result.arch, RemoteArch::X86_64);
423
424 assert!(parse_platform("Windows x86_64\n").is_err());
425 assert!(parse_platform("Linux armv7l\n").is_err());
426 }
427
428 #[test]
429 fn test_parse_shell() {
430 assert_eq!(parse_shell("/bin/bash\n", "sh"), "/bin/bash");
431 assert_eq!(parse_shell("/bin/zsh\n", "sh"), "/bin/zsh");
432
433 assert_eq!(parse_shell("/bin/bash", "sh"), "/bin/bash");
434 assert_eq!(
435 parse_shell("some shell init output\n/bin/bash\n", "sh"),
436 "/bin/bash"
437 );
438 assert_eq!(
439 parse_shell("some shell init output\n/bin/bash", "sh"),
440 "/bin/bash"
441 );
442 assert_eq!(parse_shell("", "sh"), "sh");
443 assert_eq!(parse_shell("\n", "sh"), "sh");
444 }
445}