Skip to repository content1120 lines · 35.3 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:07:23.952Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
darwin.rs
1use mach2::exception_types::{
2 EXC_MASK_ALL, EXCEPTION_DEFAULT, exception_behavior_t, exception_mask_t,
3};
4use mach2::port::{MACH_PORT_NULL, mach_port_t};
5use mach2::thread_status::{THREAD_STATE_NONE, thread_state_flavor_t};
6use smol::Async;
7use std::collections::BTreeMap;
8use std::ffi::{CString, OsStr, OsString};
9use std::io;
10use std::os::fd::AsRawFd;
11use std::os::unix::ffi::{OsStrExt, OsStringExt};
12use std::os::unix::io::FromRawFd;
13use std::path::{Path, PathBuf};
14use std::process::{ExitStatus, Output};
15use std::ptr;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum Stdio {
19 /// A new pipe should be arranged to connect the parent and child processes.
20 #[default]
21 Piped,
22 /// The child inherits from the corresponding parent descriptor.
23 Inherit,
24 /// This stream will be ignored (redirected to `/dev/null`).
25 Null,
26}
27
28impl Stdio {
29 pub fn piped() -> Self {
30 Self::Piped
31 }
32
33 pub fn inherit() -> Self {
34 Self::Inherit
35 }
36
37 pub fn null() -> Self {
38 Self::Null
39 }
40}
41
42unsafe extern "C" {
43 fn posix_spawnattr_setexceptionports_np(
44 attr: *mut libc::posix_spawnattr_t,
45 mask: exception_mask_t,
46 new_port: mach_port_t,
47 behavior: exception_behavior_t,
48 new_flavor: thread_state_flavor_t,
49 ) -> libc::c_int;
50
51 fn posix_spawn_file_actions_addchdir_np(
52 file_actions: *mut libc::posix_spawn_file_actions_t,
53 path: *const libc::c_char,
54 ) -> libc::c_int;
55
56 fn posix_spawn_file_actions_addinherit_np(
57 file_actions: *mut libc::posix_spawn_file_actions_t,
58 filedes: libc::c_int,
59 ) -> libc::c_int;
60
61 static environ: *const *mut libc::c_char;
62}
63
64#[derive(Debug)]
65pub struct Command {
66 program: OsString,
67 args: Vec<OsString>,
68 envs: BTreeMap<OsString, Option<OsString>>,
69 env_clear: bool,
70 current_dir: Option<PathBuf>,
71 stdin_cfg: Option<Stdio>,
72 stdout_cfg: Option<Stdio>,
73 stderr_cfg: Option<Stdio>,
74 kill_on_drop: bool,
75}
76
77impl Command {
78 pub fn new(program: impl AsRef<OsStr>) -> Self {
79 Self {
80 program: program.as_ref().to_owned(),
81 args: Vec::new(),
82 envs: BTreeMap::new(),
83 env_clear: false,
84 current_dir: None,
85 stdin_cfg: None,
86 stdout_cfg: None,
87 stderr_cfg: None,
88 kill_on_drop: false,
89 }
90 }
91
92 pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
93 self.args.push(arg.as_ref().to_owned());
94 self
95 }
96
97 pub fn args<I, S>(&mut self, args: I) -> &mut Self
98 where
99 I: IntoIterator<Item = S>,
100 S: AsRef<OsStr>,
101 {
102 self.args
103 .extend(args.into_iter().map(|a| a.as_ref().to_owned()));
104 self
105 }
106
107 pub fn get_args(&self) -> impl Iterator<Item = &OsStr> {
108 self.args.iter().map(|s| s.as_os_str())
109 }
110
111 pub fn env(&mut self, key: impl AsRef<OsStr>, val: impl AsRef<OsStr>) -> &mut Self {
112 self.envs
113 .insert(key.as_ref().to_owned(), Some(val.as_ref().to_owned()));
114 self
115 }
116
117 pub fn envs<I, K, V>(&mut self, vars: I) -> &mut Self
118 where
119 I: IntoIterator<Item = (K, V)>,
120 K: AsRef<OsStr>,
121 V: AsRef<OsStr>,
122 {
123 for (key, val) in vars {
124 self.envs
125 .insert(key.as_ref().to_owned(), Some(val.as_ref().to_owned()));
126 }
127 self
128 }
129
130 pub fn env_remove(&mut self, key: impl AsRef<OsStr>) -> &mut Self {
131 let key = key.as_ref().to_owned();
132 if self.env_clear {
133 self.envs.remove(&key);
134 } else {
135 self.envs.insert(key, None);
136 }
137 self
138 }
139
140 pub fn env_clear(&mut self) -> &mut Self {
141 self.env_clear = true;
142 self.envs.clear();
143 self
144 }
145
146 pub fn current_dir(&mut self, dir: impl AsRef<Path>) -> &mut Self {
147 self.current_dir = Some(dir.as_ref().to_owned());
148 self
149 }
150
151 pub fn stdin(&mut self, cfg: Stdio) -> &mut Self {
152 self.stdin_cfg = Some(cfg);
153 self
154 }
155
156 pub fn stdout(&mut self, cfg: Stdio) -> &mut Self {
157 self.stdout_cfg = Some(cfg);
158 self
159 }
160
161 pub fn stderr(&mut self, cfg: Stdio) -> &mut Self {
162 self.stderr_cfg = Some(cfg);
163 self
164 }
165
166 pub fn kill_on_drop(&mut self, kill_on_drop: bool) -> &mut Self {
167 self.kill_on_drop = kill_on_drop;
168 self
169 }
170
171 pub fn spawn(&mut self) -> io::Result<Child> {
172 let current_dir = self
173 .current_dir
174 .as_deref()
175 .unwrap_or_else(|| Path::new("."));
176
177 // Optimization: if no environment modifications were requested, pass None
178 // to spawn_posix so it uses the `environ` global directly, avoiding a
179 // full copy of the environment. This matches std::process::Command behavior.
180 let envs = if self.env_clear || !self.envs.is_empty() {
181 let mut result = BTreeMap::<OsString, OsString>::new();
182 if !self.env_clear {
183 for (key, val) in std::env::vars_os() {
184 result.insert(key, val);
185 }
186 }
187 for (key, maybe_val) in &self.envs {
188 if let Some(val) = maybe_val {
189 result.insert(key.clone(), val.clone());
190 } else {
191 result.remove(key);
192 }
193 }
194 Some(result.into_iter().collect::<Vec<_>>())
195 } else {
196 None
197 };
198
199 spawn_posix_spawn(
200 &self.program,
201 &self.args,
202 current_dir,
203 envs.as_deref(),
204 self.stdin_cfg.unwrap_or_default(),
205 self.stdout_cfg.unwrap_or_default(),
206 self.stderr_cfg.unwrap_or_default(),
207 self.kill_on_drop,
208 )
209 }
210
211 pub async fn output(&mut self) -> io::Result<Output> {
212 self.stdin_cfg.get_or_insert(Stdio::null());
213 self.stdout_cfg.get_or_insert(Stdio::piped());
214 self.stderr_cfg.get_or_insert(Stdio::piped());
215
216 let child = self.spawn()?;
217 child.output().await
218 }
219
220 pub async fn status(&mut self) -> io::Result<ExitStatus> {
221 let mut child = self.spawn()?;
222 child.status().await
223 }
224
225 pub fn get_program(&self) -> &OsStr {
226 self.program.as_os_str()
227 }
228}
229
230#[derive(Debug)]
231pub struct Child {
232 inner: smol::process::Child,
233 pub stdin: Option<Async<std::fs::File>>,
234 pub stdout: Option<Async<std::fs::File>>,
235 pub stderr: Option<Async<std::fs::File>>,
236}
237
238impl Child {
239 pub fn id(&self) -> u32 {
240 self.inner.id()
241 }
242
243 pub fn kill(&mut self) -> io::Result<()> {
244 self.inner.kill()
245 }
246
247 pub fn try_status(&mut self) -> io::Result<Option<ExitStatus>> {
248 self.inner.try_status()
249 }
250
251 pub fn status(
252 &mut self,
253 ) -> impl std::future::Future<Output = io::Result<ExitStatus>> + Send + 'static {
254 self.stdin.take();
255 self.inner.status()
256 }
257
258 pub async fn output(mut self) -> io::Result<Output> {
259 use futures_lite::AsyncReadExt;
260
261 let status = self.status();
262
263 let stdout = self.stdout.take();
264 let stdout_future = async move {
265 let mut data = Vec::new();
266 if let Some(mut stdout) = stdout {
267 stdout.read_to_end(&mut data).await?;
268 }
269 io::Result::Ok(data)
270 };
271
272 let stderr = self.stderr.take();
273 let stderr_future = async move {
274 let mut data = Vec::new();
275 if let Some(mut stderr) = stderr {
276 stderr.read_to_end(&mut data).await?;
277 }
278 io::Result::Ok(data)
279 };
280
281 let (stdout_data, stderr_data) =
282 futures_lite::future::try_zip(stdout_future, stderr_future).await?;
283 let status = status.await?;
284
285 Ok(Output {
286 status,
287 stdout: stdout_data,
288 stderr: stderr_data,
289 })
290 }
291}
292
293fn spawn_posix_spawn(
294 program: &OsStr,
295 args: &[OsString],
296 current_dir: &Path,
297 envs: Option<&[(OsString, OsString)]>,
298 stdin_cfg: Stdio,
299 stdout_cfg: Stdio,
300 stderr_cfg: Stdio,
301 kill_on_drop: bool,
302) -> io::Result<Child> {
303 // posix_spawnp resolves programs against the parent's cwd/PATH, not the child's.
304 let resolved_program = if program.as_bytes().contains(&b'/') {
305 std::path::absolute(current_dir.join(program)).map_or_else(
306 |_| program.as_bytes().to_vec(),
307 |p| p.into_os_string().into_vec(),
308 )
309 } else {
310 envs.and_then(|e| {
311 e.iter()
312 .find(|(k, _)| k.as_os_str() == OsStr::new("PATH"))
313 .and_then(|(_, v)| which::which_in(program, Some(v.as_os_str()), current_dir).ok())
314 })
315 .map_or_else(
316 || program.as_bytes().to_vec(),
317 |path| path.into_os_string().into_vec(),
318 )
319 };
320 let program_cstr = CString::new(resolved_program).map_err(|_| invalid_input_error())?;
321 let argv0_cstr = CString::new(program.as_bytes()).map_err(|_| invalid_input_error())?;
322
323 let current_dir_cstr =
324 CString::new(current_dir.as_os_str().as_bytes()).map_err(|_| invalid_input_error())?;
325
326 let mut argv_cstrs = vec![argv0_cstr];
327 for arg in args {
328 let cstr = CString::new(arg.as_bytes()).map_err(|_| invalid_input_error())?;
329 argv_cstrs.push(cstr);
330 }
331 let mut argv_ptrs: Vec<*mut libc::c_char> = argv_cstrs
332 .iter()
333 .map(|s| s.as_ptr() as *mut libc::c_char)
334 .collect();
335 argv_ptrs.push(ptr::null_mut());
336
337 let envp: Vec<CString> = if let Some(envs) = envs {
338 envs.iter()
339 .map(|(key, value)| {
340 let mut env_str = key.as_bytes().to_vec();
341 env_str.push(b'=');
342 env_str.extend_from_slice(value.as_bytes());
343 CString::new(env_str)
344 })
345 .collect::<Result<Vec<_>, _>>()
346 .map_err(|_| invalid_input_error())?
347 } else {
348 Vec::new()
349 };
350 let mut envp_ptrs: Vec<*mut libc::c_char> = envp
351 .iter()
352 .map(|s| s.as_ptr() as *mut libc::c_char)
353 .collect();
354 envp_ptrs.push(ptr::null_mut());
355
356 let (stdin_read, stdin_write) = match stdin_cfg {
357 Stdio::Piped => {
358 let (r, w) = create_pipe()?;
359 (Some(r), Some(w))
360 }
361 Stdio::Null => {
362 let fd = open_dev_null(libc::O_RDONLY)?;
363 (Some(fd), None)
364 }
365 Stdio::Inherit => (None, None),
366 };
367
368 let (stdout_read, stdout_write) = match stdout_cfg {
369 Stdio::Piped => {
370 let (r, w) = create_pipe()?;
371 (Some(r), Some(w))
372 }
373 Stdio::Null => {
374 let fd = open_dev_null(libc::O_WRONLY)?;
375 (None, Some(fd))
376 }
377 Stdio::Inherit => (None, None),
378 };
379
380 let (stderr_read, stderr_write) = match stderr_cfg {
381 Stdio::Piped => {
382 let (r, w) = create_pipe()?;
383 (Some(r), Some(w))
384 }
385 Stdio::Null => {
386 let fd = open_dev_null(libc::O_WRONLY)?;
387 (None, Some(fd))
388 }
389 Stdio::Inherit => (None, None),
390 };
391
392 let mut attr: libc::posix_spawnattr_t = ptr::null_mut();
393 let mut file_actions: libc::posix_spawn_file_actions_t = ptr::null_mut();
394
395 unsafe {
396 cvt_nz(libc::posix_spawnattr_init(&mut attr))?;
397 cvt_nz(libc::posix_spawn_file_actions_init(&mut file_actions))?;
398
399 // The Rust runtime sets SIGPIPE to SIG_IGN before `main`, and ignored
400 // dispositions survive exec, so without this children would never die
401 // from writing to a closed pipe. Reset it to SIG_DFL, like std does
402 // (rust-lang/rust#101077). Like std, we don't touch the signal mask,
403 // so deliberately blocked signals (e.g. via `nohup`) stay blocked.
404 let mut default_set: libc::sigset_t = std::mem::zeroed();
405 if libc::sigemptyset(&mut default_set) == -1 {
406 return Err(io::Error::last_os_error());
407 }
408 if libc::sigaddset(&mut default_set, libc::SIGPIPE) == -1 {
409 return Err(io::Error::last_os_error());
410 }
411 cvt_nz(libc::posix_spawnattr_setsigdefault(&mut attr, &default_set))?;
412
413 cvt_nz(libc::posix_spawnattr_setflags(
414 &mut attr,
415 (libc::POSIX_SPAWN_CLOEXEC_DEFAULT | libc::POSIX_SPAWN_SETSIGDEF) as libc::c_short,
416 ))?;
417
418 cvt_nz(posix_spawnattr_setexceptionports_np(
419 &mut attr,
420 EXC_MASK_ALL,
421 MACH_PORT_NULL,
422 EXCEPTION_DEFAULT as exception_behavior_t,
423 THREAD_STATE_NONE,
424 ))?;
425
426 cvt_nz(posix_spawn_file_actions_addchdir_np(
427 &mut file_actions,
428 current_dir_cstr.as_ptr(),
429 ))?;
430
431 // With POSIX_SPAWN_CLOEXEC_DEFAULT, any fd without a file action is
432 // closed in the child, so inheriting a stdio fd requires an explicit
433 // addinherit_np action; without one the child's fd 0/1/2 would start
434 // out closed and could be silently reused by its first open().
435 if let Some(fd) = &stdin_read {
436 cvt_nz(libc::posix_spawn_file_actions_adddup2(
437 &mut file_actions,
438 fd.as_raw_fd(),
439 libc::STDIN_FILENO,
440 ))?;
441 }
442 if stdin_read.is_some() || stdin_cfg == Stdio::Inherit {
443 cvt_nz(posix_spawn_file_actions_addinherit_np(
444 &mut file_actions,
445 libc::STDIN_FILENO,
446 ))?;
447 }
448
449 if let Some(fd) = &stdout_write {
450 cvt_nz(libc::posix_spawn_file_actions_adddup2(
451 &mut file_actions,
452 fd.as_raw_fd(),
453 libc::STDOUT_FILENO,
454 ))?;
455 }
456 if stdout_write.is_some() || stdout_cfg == Stdio::Inherit {
457 cvt_nz(posix_spawn_file_actions_addinherit_np(
458 &mut file_actions,
459 libc::STDOUT_FILENO,
460 ))?;
461 }
462
463 if let Some(fd) = &stderr_write {
464 cvt_nz(libc::posix_spawn_file_actions_adddup2(
465 &mut file_actions,
466 fd.as_raw_fd(),
467 libc::STDERR_FILENO,
468 ))?;
469 }
470 if stderr_write.is_some() || stderr_cfg == Stdio::Inherit {
471 cvt_nz(posix_spawn_file_actions_addinherit_np(
472 &mut file_actions,
473 libc::STDERR_FILENO,
474 ))?;
475 }
476
477 let mut pid: libc::pid_t = 0;
478
479 let spawn_result = libc::posix_spawnp(
480 &mut pid,
481 program_cstr.as_ptr(),
482 &file_actions,
483 &attr,
484 argv_ptrs.as_ptr(),
485 if envs.is_some() {
486 envp_ptrs.as_ptr()
487 } else {
488 environ
489 },
490 );
491
492 libc::posix_spawnattr_destroy(&mut attr);
493 libc::posix_spawn_file_actions_destroy(&mut file_actions);
494
495 cvt_nz(spawn_result)?;
496
497 let inner = smol::process::Child::adopt_raw_pid(pid as u32, true, kill_on_drop)?;
498
499 Ok(Child {
500 inner,
501 stdin: stdin_write.map(Async::new).transpose()?,
502 stdout: stdout_read.map(Async::new).transpose()?,
503 stderr: stderr_read.map(Async::new).transpose()?,
504 })
505 }
506}
507
508fn create_pipe() -> io::Result<(std::fs::File, std::fs::File)> {
509 let mut fds: [libc::c_int; 2] = [0; 2];
510 unsafe {
511 let result = libc::pipe(fds.as_mut_ptr());
512 if result == -1 {
513 let error = io::Error::last_os_error();
514 return Err(error);
515 }
516
517 // Set close-on-exec on both ends of the pipe.
518 //
519 // Without this, unrelated spawns elsewhere in the process (e.g.
520 // `smol::process` or `async_process`, which on Apple platforms use
521 // `posix_spawn` *without* `POSIX_SPAWN_CLOEXEC_DEFAULT`) would inherit
522 // these file descriptors and keep the pipes open even after we drop our
523 // side.
524 for &fd in &fds {
525 let result = libc::ioctl(fd, libc::FIOCLEX);
526 if result == -1 {
527 let error = io::Error::last_os_error();
528 libc::close(fds[0]);
529 libc::close(fds[1]);
530 return Err(error);
531 }
532 }
533
534 Ok((
535 std::fs::File::from_raw_fd(fds[0]),
536 std::fs::File::from_raw_fd(fds[1]),
537 ))
538 }
539}
540
541fn open_dev_null(flags: libc::c_int) -> io::Result<std::fs::File> {
542 // Set close-on-exec for this pipe, for the same reason as in `create_pipe`.
543 let fd = unsafe {
544 libc::open(
545 c"/dev/null".as_ptr() as *const libc::c_char,
546 flags | libc::O_CLOEXEC,
547 )
548 };
549 if fd == -1 {
550 return Err(io::Error::last_os_error());
551 }
552 Ok(unsafe { std::fs::File::from_raw_fd(fd) })
553}
554
555/// Zero means `Ok()`, all other values are treated as raw OS errors. Does not look at `errno`.
556/// Mirrored after Rust's std `cvt_nz` function.
557fn cvt_nz(error: libc::c_int) -> io::Result<()> {
558 if error == 0 {
559 Ok(())
560 } else {
561 Err(io::Error::from_raw_os_error(error))
562 }
563}
564
565fn invalid_input_error() -> io::Error {
566 io::Error::new(
567 io::ErrorKind::InvalidInput,
568 "invalid argument: path or argument contains null byte",
569 )
570}
571
572#[cfg(test)]
573mod tests {
574 use std::os::unix::process::ExitStatusExt as _;
575
576 use super::*;
577 use futures_lite::AsyncWriteExt;
578
579 // Verifies that pipes returned by `create_pipe` aren't visible to unrelated
580 // child processes spawned via `std::process::Command`. On macOS, `std`
581 // uses `posix_spawn` without `POSIX_SPAWN_CLOEXEC_DEFAULT`, so any
582 // non-CLOEXEC fd in the parent leaks into the child. Without
583 // `FD_CLOEXEC` on our pipe fds, an unrelated spawn (a terminal, the crash
584 // handler, etc.) running concurrently with a piped git child would hold
585 // git's stdin write end open and deadlock the git child on `read()`.
586 #[test]
587 fn test_create_pipe_not_inherited_by_unrelated_spawn() {
588 let (read_file, write_file) = create_pipe().expect("create_pipe failed");
589 let read_fd = read_file.as_raw_fd();
590 let write_fd = write_file.as_raw_fd();
591
592 // Probe with the exact fds returned by `create_pipe` (no dup), since
593 // duping with `F_DUPFD` would lose CLOEXEC and `F_DUPFD_CLOEXEC` would
594 // unconditionally set it, either of which would defeat the test.
595 #[allow(clippy::disallowed_methods)]
596 let output = std::process::Command::new("/bin/sh")
597 .arg("-c")
598 .arg(format!(
599 "for fd in {read_fd} {write_fd}; do \
600 if [ -e /dev/fd/$fd ]; then \
601 echo $fd WAS INHERITED; \
602 else \
603 echo $fd WAS NOT INHERITED; \
604 fi; \
605 done; \
606 echo DONE"
607 ))
608 .output()
609 .expect("failed to spawn sh");
610
611 let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
612
613 assert_eq!(
614 stdout,
615 format!("{read_fd} WAS NOT INHERITED\n{write_fd} WAS NOT INHERITED\nDONE\n")
616 );
617 }
618
619 fn wait_until_gone(pid: u32) {
620 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
621 loop {
622 let result = unsafe { libc::kill(pid as libc::pid_t, 0) };
623 if result == -1 && io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH) {
624 return;
625 }
626 assert!(
627 std::time::Instant::now() < deadline,
628 "process {pid} was not reaped"
629 );
630 std::thread::sleep(std::time::Duration::from_millis(10));
631 }
632 }
633
634 #[test]
635 fn test_drop_reaps_child() {
636 smol::block_on(async {
637 let child = Command::new("/usr/bin/true")
638 .spawn()
639 .expect("failed to spawn command");
640 let pid = child.id();
641 drop(child);
642 wait_until_gone(pid);
643 });
644 }
645
646 #[test]
647 fn test_kill_on_drop_kills_and_reaps_child() {
648 smol::block_on(async {
649 let child = Command::new("/bin/sleep")
650 .arg("60")
651 .kill_on_drop(true)
652 .spawn()
653 .expect("failed to spawn command");
654 let pid = child.id();
655 drop(child);
656 wait_until_gone(pid);
657 });
658 }
659
660 #[test]
661 fn test_spawn_echo() {
662 smol::block_on(async {
663 let output = Command::new("/bin/echo")
664 .args(["-n", "hello world"])
665 .output()
666 .await
667 .expect("failed to run command");
668
669 assert!(output.status.success());
670 assert_eq!(output.stdout, b"hello world");
671 });
672 }
673
674 #[test]
675 fn test_spawn_cat_stdin() {
676 smol::block_on(async {
677 let mut child = Command::new("/bin/cat")
678 .stdin(Stdio::piped())
679 .stdout(Stdio::piped())
680 .spawn()
681 .expect("failed to spawn");
682
683 if let Some(ref mut stdin) = child.stdin {
684 stdin
685 .write_all(b"hello from stdin")
686 .await
687 .expect("failed to write");
688 stdin.close().await.expect("failed to close");
689 }
690 drop(child.stdin.take());
691
692 let output = child.output().await.expect("failed to get output");
693 assert!(output.status.success());
694 assert_eq!(output.stdout, b"hello from stdin");
695 });
696 }
697
698 #[test]
699 fn test_spawn_stderr() {
700 smol::block_on(async {
701 let output = Command::new("/bin/sh")
702 .args(["-c", "echo error >&2"])
703 .output()
704 .await
705 .expect("failed to run command");
706
707 assert!(output.status.success());
708 assert_eq!(output.stderr, b"error\n");
709 });
710 }
711
712 #[test]
713 fn test_spawn_exit_code() {
714 smol::block_on(async {
715 let output = Command::new("/bin/sh")
716 .args(["-c", "exit 42"])
717 .output()
718 .await
719 .expect("failed to run command");
720
721 assert!(!output.status.success());
722 assert_eq!(output.status.code(), Some(42));
723 });
724 }
725
726 #[test]
727 fn test_spawn_current_dir() {
728 smol::block_on(async {
729 let output = Command::new("/bin/pwd")
730 .current_dir("/tmp")
731 .output()
732 .await
733 .expect("failed to run command");
734
735 assert!(output.status.success());
736 let pwd = String::from_utf8_lossy(&output.stdout);
737 assert!(pwd.trim() == "/tmp" || pwd.trim() == "/private/tmp");
738 });
739 }
740
741 #[test]
742 fn test_spawn_env() {
743 smol::block_on(async {
744 let output = Command::new("/bin/sh")
745 .args(["-c", "echo $MY_TEST_VAR"])
746 .env("MY_TEST_VAR", "test_value")
747 .output()
748 .await
749 .expect("failed to run command");
750
751 assert!(output.status.success());
752 assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "test_value");
753 });
754 }
755
756 #[test]
757 fn test_spawn_status() {
758 smol::block_on(async {
759 let status = Command::new("/usr/bin/true")
760 .status()
761 .await
762 .expect("failed to run command");
763
764 assert!(status.success());
765
766 let status = Command::new("/usr/bin/false")
767 .status()
768 .await
769 .expect("failed to run command");
770
771 assert!(!status.success());
772 });
773 }
774
775 #[test]
776 fn test_env_remove_removes_set_env() {
777 smol::block_on(async {
778 let output = Command::new("/bin/sh")
779 .args(["-c", "echo ${MY_VAR:-unset}"])
780 .env("MY_VAR", "set_value")
781 .env_remove("MY_VAR")
782 .output()
783 .await
784 .expect("failed to run command");
785
786 assert!(output.status.success());
787 assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "unset");
788 });
789 }
790
791 #[test]
792 fn test_env_remove_removes_inherited_env() {
793 smol::block_on(async {
794 // SAFETY: This test is single-threaded and we clean up the var at the end
795 unsafe { std::env::set_var("TEST_INHERITED_VAR", "inherited_value") };
796
797 let output = Command::new("/bin/sh")
798 .args(["-c", "echo ${TEST_INHERITED_VAR:-unset}"])
799 .env_remove("TEST_INHERITED_VAR")
800 .output()
801 .await
802 .expect("failed to run command");
803
804 assert!(output.status.success());
805 assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "unset");
806
807 // SAFETY: Cleaning up test env var
808 unsafe { std::env::remove_var("TEST_INHERITED_VAR") };
809 });
810 }
811
812 #[test]
813 fn test_env_after_env_remove() {
814 smol::block_on(async {
815 let output = Command::new("/bin/sh")
816 .args(["-c", "echo ${MY_VAR:-unset}"])
817 .env_remove("MY_VAR")
818 .env("MY_VAR", "new_value")
819 .output()
820 .await
821 .expect("failed to run command");
822
823 assert!(output.status.success());
824 assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "new_value");
825 });
826 }
827
828 #[test]
829 fn test_env_remove_after_env_clear() {
830 smol::block_on(async {
831 let output = Command::new("/bin/sh")
832 .args(["-c", "echo ${MY_VAR:-unset}"])
833 .env_clear()
834 .env("MY_VAR", "set_value")
835 .env_remove("MY_VAR")
836 .output()
837 .await
838 .expect("failed to run command");
839
840 assert!(output.status.success());
841 assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "unset");
842 });
843 }
844
845 #[test]
846 fn test_stdio_null_stdin() {
847 smol::block_on(async {
848 let child = Command::new("/bin/cat")
849 .stdin(Stdio::null())
850 .stdout(Stdio::piped())
851 .spawn()
852 .expect("failed to spawn");
853
854 let output = child.output().await.expect("failed to get output");
855 assert!(output.status.success());
856 assert!(
857 output.stdout.is_empty(),
858 "stdin from /dev/null should produce no output from cat"
859 );
860 });
861 }
862
863 #[test]
864 fn test_stdio_null_stdout() {
865 smol::block_on(async {
866 let mut child = Command::new("/bin/echo")
867 .args(["hello"])
868 .stdout(Stdio::null())
869 .spawn()
870 .expect("failed to spawn");
871
872 assert!(
873 child.stdout.is_none(),
874 "stdout should be None when Stdio::null() is used"
875 );
876
877 let status = child.status().await.expect("failed to get status");
878 assert!(status.success());
879 });
880 }
881
882 #[test]
883 fn test_stdio_null_stderr() {
884 smol::block_on(async {
885 let mut child = Command::new("/bin/sh")
886 .args(["-c", "echo error >&2"])
887 .stderr(Stdio::null())
888 .spawn()
889 .expect("failed to spawn");
890
891 assert!(
892 child.stderr.is_none(),
893 "stderr should be None when Stdio::null() is used"
894 );
895
896 let status = child.status().await.expect("failed to get status");
897 assert!(status.success());
898 });
899 }
900
901 #[test]
902 fn test_stdio_piped_stdin() {
903 smol::block_on(async {
904 let mut child = Command::new("/bin/cat")
905 .stdin(Stdio::piped())
906 .stdout(Stdio::piped())
907 .spawn()
908 .expect("failed to spawn");
909
910 assert!(
911 child.stdin.is_some(),
912 "stdin should be Some when Stdio::piped() is used"
913 );
914
915 if let Some(ref mut stdin) = child.stdin {
916 stdin
917 .write_all(b"piped input")
918 .await
919 .expect("failed to write");
920 stdin.close().await.expect("failed to close");
921 }
922 drop(child.stdin.take());
923
924 let output = child.output().await.expect("failed to get output");
925 assert!(output.status.success());
926 assert_eq!(output.stdout, b"piped input");
927 });
928 }
929
930 #[test]
931 fn test_bare_program_resolved_via_custom_path() {
932 let temp_dir = tempfile::tempdir().expect("failed to create temp dir");
933
934 let link_path = temp_dir.path().join("zed-test-echo");
935 std::os::unix::fs::symlink("/bin/echo", &link_path).expect("failed to create symlink");
936
937 let custom_path = temp_dir.path().to_string_lossy().into_owned();
938
939 smol::block_on(async {
940 let output = Command::new("zed-test-echo")
941 .args(["-n", "from-custom-path"])
942 .env("PATH", &custom_path)
943 .output()
944 .await
945 .expect("failed to spawn with custom PATH");
946
947 assert!(output.status.success());
948 assert_eq!(output.stdout, b"from-custom-path");
949 });
950 }
951
952 #[test]
953 fn test_bare_program_preserves_argv0_when_resolved_via_custom_path() {
954 let temp_dir = tempfile::tempdir().expect("failed to create temp dir");
955
956 let link_path = temp_dir.path().join("zed-test-sh");
957 std::os::unix::fs::symlink("/bin/sh", &link_path).expect("failed to create symlink");
958
959 let custom_path = temp_dir.path().to_string_lossy().into_owned();
960
961 smol::block_on(async {
962 let output = Command::new("zed-test-sh")
963 .args(["-c", "printf %s \"$0\""])
964 .env("PATH", &custom_path)
965 .output()
966 .await
967 .expect("failed to spawn with custom PATH");
968
969 assert!(output.status.success());
970 assert_eq!(output.stdout, b"zed-test-sh");
971 });
972 }
973
974 #[test]
975 fn test_bare_program_with_custom_path_falls_back_when_not_found() {
976 smol::block_on(async {
977 let result = Command::new("zed-nonexistent-binary-xyz")
978 .env("PATH", "/nonexistent/path")
979 .spawn();
980
981 assert!(result.is_err());
982 });
983 }
984
985 #[test]
986 fn test_bare_program_with_custom_env_no_path_key() {
987 smol::block_on(async {
988 let output = Command::new("echo")
989 .args(["-n", "from-inherited-path"])
990 .env("ZED_TEST_VAR", "test")
991 .output()
992 .await
993 .expect("failed to spawn with custom env but no PATH override");
994
995 assert!(output.status.success());
996 assert_eq!(output.stdout, b"from-inherited-path");
997 });
998 }
999
1000 #[test]
1001 fn test_relative_path_resolved_against_current_dir() {
1002 let temp_dir = tempfile::tempdir().expect("failed to create temp dir");
1003
1004 let link_path = temp_dir.path().join("zed-test-echo");
1005 std::os::unix::fs::symlink("/bin/echo", &link_path).expect("failed to create symlink");
1006
1007 let relative_path = "./zed-test-echo";
1008
1009 smol::block_on(async {
1010 let output = Command::new(relative_path)
1011 .args(["-n", "from-relative-path"])
1012 .current_dir(temp_dir.path())
1013 .env("PATH", "/nonexistent/path")
1014 .output()
1015 .await
1016 .expect("failed to spawn with relative path");
1017
1018 assert!(output.status.success());
1019 assert_eq!(output.stdout, b"from-relative-path");
1020 });
1021 }
1022
1023 #[test]
1024 fn test_absolute_path_passes_through_unchanged() {
1025 smol::block_on(async {
1026 let output = Command::new("/bin/echo")
1027 .args(["-n", "from-absolute-path"])
1028 .env("PATH", "/nonexistent/path")
1029 .output()
1030 .await
1031 .expect("failed to spawn with absolute path");
1032
1033 assert!(output.status.success());
1034 assert_eq!(output.stdout, b"from-absolute-path");
1035 });
1036 }
1037
1038 #[test]
1039 fn test_stdio_inherit_keeps_stdio_open() {
1040 smol::block_on(async {
1041 let status = Command::new("/bin/sh")
1042 .args([
1043 "-c",
1044 "[ -e /dev/fd/0 ] && [ -e /dev/fd/1 ] && [ -e /dev/fd/2 ]",
1045 ])
1046 .stdin(Stdio::inherit())
1047 .stdout(Stdio::inherit())
1048 .stderr(Stdio::inherit())
1049 .status()
1050 .await
1051 .expect("failed to run command");
1052
1053 assert!(
1054 status.success(),
1055 "stdio fds should be open in the child when using Stdio::inherit()"
1056 );
1057 });
1058 }
1059
1060 #[test]
1061 fn test_child_sigpipe_disposition_matches_std() {
1062 const SCRIPT: &str = "kill -s PIPE $$; echo survived";
1063
1064 #[allow(clippy::disallowed_methods)]
1065 let std_output = std::process::Command::new("/bin/sh")
1066 .args(["-c", SCRIPT])
1067 .output()
1068 .expect("failed to run std command");
1069
1070 let our_output = smol::block_on(async {
1071 Command::new("/bin/sh")
1072 .args(["-c", SCRIPT])
1073 .output()
1074 .await
1075 .expect("failed to run command")
1076 });
1077
1078 assert_eq!(
1079 std_output.status.signal(),
1080 Some(libc::SIGPIPE),
1081 "expected std to reset SIGPIPE to SIG_DFL in children; did its default change?"
1082 );
1083 assert_eq!(
1084 our_output.status.signal(),
1085 std_output.status.signal(),
1086 "child SIGPIPE disposition diverges from std::process::Command"
1087 );
1088 assert_eq!(our_output.stdout, std_output.stdout);
1089 }
1090
1091 #[test]
1092 fn test_stdio_fds_closed_on_error() {
1093 fn count_open_fds() -> usize {
1094 let limit = unsafe { libc::getdtablesize() };
1095 (0..limit)
1096 .filter(|&fd| unsafe { libc::fcntl(fd, libc::F_GETFD) } != -1)
1097 .count()
1098 }
1099
1100 const ATTEMPTS: usize = 100;
1101 const SLACK: usize = 32;
1102
1103 let before = count_open_fds();
1104 for _ in 0..ATTEMPTS {
1105 Command::new("/bin/binarythatdoesnotexist")
1106 .stdin(Stdio::piped())
1107 .stdout(Stdio::piped())
1108 .stderr(Stdio::piped())
1109 .spawn()
1110 .expect_err("spawn should fail for a nonexistent binary");
1111 }
1112 let after = count_open_fds();
1113
1114 assert!(
1115 after <= before + SLACK,
1116 "fd leak detected: {before} open fds before, {after} after {ATTEMPTS} failed spawns"
1117 );
1118 }
1119}
1120