1use std::fmt;
5use std::time::{Duration, Instant};
6
7use tokio::process::Command;
8use tracing::{Instrument, Span, debug, field, info_span};
9
10use crate::Codex;
11use crate::error::{Error, Result};
12
13pub(crate) fn command_span(name: &'static str, codex: &Codex, args: &[String]) -> Span {
19 let working_dir = codex
20 .working_dir
21 .as_ref()
22 .map_or_else(|| "(inherited)".to_string(), |p| p.display().to_string());
23
24 info_span!(
25 parent: Span::current(),
26 "codex",
27 otel.name = name,
28 subcommand = args.first().map_or("(none)", String::as_str),
29 binary = %codex.binary.display(),
30 working_dir = %working_dir,
31 outcome = field::Empty,
32 exit_code = field::Empty,
33 duration_ms = field::Empty,
34 )
35}
36
37pub(crate) struct SpanOutcome {
47 span: Span,
48 started: Instant,
49 settled: bool,
50}
51
52impl SpanOutcome {
53 pub(crate) fn start(span: Span) -> Self {
54 Self {
55 span,
56 started: Instant::now(),
57 settled: false,
58 }
59 }
60
61 pub(crate) fn settle(&mut self, outcome: &'static str, exit_code: Option<i32>) {
62 self.settled = true;
63 self.span.record("outcome", outcome);
64 self.span
65 .record("duration_ms", self.started.elapsed().as_millis() as u64);
66 if let Some(code) = exit_code {
67 self.span.record("exit_code", code);
68 }
69 }
70
71 fn settle_from_ref(&mut self, result: &Result<CommandOutput>) {
72 self.settle_from(result);
73 }
74
75 fn settle_from(&mut self, result: &Result<CommandOutput>) {
76 match result {
77 Ok(output) => self.settle("ok", Some(output.exit_code)),
78 Err(Error::Timeout { .. }) => self.settle("timeout", None),
79 Err(e) => match e.exit_code() {
82 Some(code) => self.settle("failed", Some(code)),
83 None => self.settle("error", None),
84 },
85 }
86 }
87}
88
89impl Drop for SpanOutcome {
90 fn drop(&mut self) {
91 if !self.settled {
92 self.settle("cancelled", None);
93 }
94 }
95}
96
97#[cfg(unix)]
104pub(crate) fn own_process_group(cmd: &mut Command, enabled: bool) {
105 if enabled {
106 cmd.process_group(0);
107 }
108}
109
110#[cfg(not(unix))]
111pub(crate) fn own_process_group(_cmd: &mut Command, _enabled: bool) {}
112
113#[cfg(unix)]
122pub(crate) fn signal_group(pid: u32, signal: i32) {
123 let Ok(pid) = i32::try_from(pid) else {
124 return;
125 };
126 unsafe {
129 libc::kill(-pid, signal);
130 }
131}
132
133pub(crate) struct GroupKillGuard {
140 pid: Option<u32>,
141}
142
143impl GroupKillGuard {
144 pub(crate) fn new(pid: Option<u32>) -> Self {
145 Self { pid }
146 }
147
148 pub(crate) fn disarm(&mut self) {
150 self.pid = None;
151 }
152
153 #[cfg(unix)]
157 pub(crate) async fn terminate(&mut self, grace: Duration) {
158 let Some(pid) = self.pid.take() else {
159 return;
160 };
161 signal_group(pid, libc::SIGTERM);
162 tokio::time::sleep(grace).await;
163 signal_group(pid, libc::SIGKILL);
164 }
165
166 #[cfg(not(unix))]
169 pub(crate) async fn terminate(&mut self, _grace: Duration) {
170 let _ = self.pid.take();
171 }
172}
173
174impl Drop for GroupKillGuard {
175 fn drop(&mut self) {
176 if let Some(pid) = self.pid.take() {
179 #[cfg(unix)]
180 signal_group(pid, libc::SIGKILL);
181 #[cfg(not(unix))]
182 let _ = pid;
183 }
184 }
185}
186
187#[derive(Clone)]
192pub struct CommandOutput {
193 pub stdout: String,
195 pub stderr: String,
197 pub exit_code: i32,
199 pub success: bool,
201}
202
203const DEBUG_TRUNCATE_LEN: usize = 200;
204
205fn truncate_for_debug(s: &str) -> String {
206 if s.len() > DEBUG_TRUNCATE_LEN {
207 format!("{}... ({} bytes total)", &s[..DEBUG_TRUNCATE_LEN], s.len())
208 } else {
209 s.to_string()
210 }
211}
212
213impl fmt::Debug for CommandOutput {
214 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215 f.debug_struct("CommandOutput")
216 .field("stdout", &truncate_for_debug(&self.stdout))
217 .field("stderr", &truncate_for_debug(&self.stderr))
218 .field("exit_code", &self.exit_code)
219 .field("success", &self.success)
220 .finish()
221 }
222}
223
224pub async fn run_codex(codex: &Codex, args: Vec<String>) -> Result<CommandOutput> {
230 run_codex_with_retry(codex, args, None).await
231}
232
233pub async fn run_codex_with_retry(
235 codex: &Codex,
236 args: Vec<String>,
237 retry_override: Option<&crate::retry::RetryPolicy>,
238) -> Result<CommandOutput> {
239 let policy = retry_override.or(codex.retry_policy.as_ref());
240
241 match policy {
242 Some(policy) => {
243 let span = info_span!(
246 "codex.retry",
247 subcommand = args.first().map_or("(none)", String::as_str),
248 max_attempts = policy.max_attempts,
249 );
250 crate::retry::with_retry(policy, || run_codex_once(codex, args.clone()))
251 .instrument(span)
252 .await
253 }
254 None => run_codex_once(codex, args).await,
255 }
256}
257
258pub(crate) fn assemble_args(codex: &Codex, args: Vec<String>) -> Vec<String> {
266 let mut command_args = Vec::with_capacity(codex.global_args.len() + args.len());
267 let protects_rollout_budget = args
268 .windows(2)
269 .any(|pair| pair[0] == "-c" && crate::RolloutBudgetConfig::is_config_override(&pair[1]));
270 let mut global_args = codex.global_args.iter().peekable();
271 while let Some(arg) = global_args.next() {
272 if protects_rollout_budget
273 && matches!(arg.as_str(), "--enable" | "--disable")
274 && global_args
275 .peek()
276 .is_some_and(|feature| feature.as_str() == "rollout_budget")
277 {
278 global_args.next();
279 continue;
280 }
281 if protects_rollout_budget
282 && matches!(
283 arg.as_str(),
284 "--enable=rollout_budget" | "--disable=rollout_budget"
285 )
286 {
287 continue;
288 }
289 command_args.push(arg.clone());
290 }
291 command_args.extend(args);
292 command_args
293}
294
295pub(crate) fn command_string(codex: &Codex, args: Vec<String>) -> String {
297 let mut out = shell_quote(&codex.binary.display().to_string());
298 for arg in assemble_args(codex, args) {
299 out.push(' ');
300 out.push_str(&shell_quote(&arg));
301 }
302 out
303}
304
305pub(crate) fn shell_quote(arg: &str) -> String {
311 if arg.is_empty() {
312 return "''".to_string();
313 }
314 if arg.contains(|c: char| c.is_whitespace() || "\"'$\\`|;<>&()[]{}*?!~#".contains(c)) {
315 return format!("'{}'", arg.replace('\'', r"'\''"));
316 }
317 arg.to_string()
318}
319
320async fn run_codex_once(codex: &Codex, args: Vec<String>) -> Result<CommandOutput> {
321 let span = command_span("codex.exec", codex, &args);
322 let outcome_span = span.clone();
323 let command_args = assemble_args(codex, args);
324
325 async move {
326 debug!(binary = %codex.binary.display(), args = ?command_args, "executing codex command");
327
328 let mut outcome = SpanOutcome::start(outcome_span);
329 let result = match codex.timeout {
330 Some(timeout) => {
331 run_internal_inner(
332 SpawnSpec {
333 binary: &codex.binary,
334 args: &command_args,
335 env: &codex.env,
336 clear_env: codex.clear_env,
337 working_dir: codex.working_dir.as_deref(),
338 stdin_prompt: None,
339 process_group: codex.process_group,
340 },
341 Some(timeout_stop(timeout)),
342 codex.termination_grace,
343 )
344 .await
345 }
346 None => {
347 run_internal(
348 &codex.binary,
349 &command_args,
350 &codex.env,
351 codex.clear_env,
352 codex.working_dir.as_deref(),
353 codex.process_group,
354 )
355 .await
356 }
357 };
358 outcome.settle_from(&result);
359 result
360 }
361 .instrument(span)
362 .await
363}
364
365pub async fn run_codex_cancellable<C>(
381 codex: &Codex,
382 args: Vec<String>,
383 cancel: C,
384) -> Result<CommandOutput>
385where
386 C: std::future::Future<Output = ()> + Send,
387{
388 let span = command_span("codex.exec", codex, &args);
389 let outcome_span = span.clone();
390 let command_args = assemble_args(codex, args);
391
392 async move {
393 debug!(binary = %codex.binary.display(), args = ?command_args, "executing cancellable codex command");
394
395 let mut outcome = SpanOutcome::start(outcome_span);
396 let stop = cancellation_or_timeout(cancel, codex.timeout, codex.termination_grace);
397 let result = run_internal_inner(
398 SpawnSpec {
399 binary: &codex.binary,
400 args: &command_args,
401 env: &codex.env,
402 clear_env: codex.clear_env,
403 working_dir: codex.working_dir.as_deref(),
404 stdin_prompt: None,
405 process_group: codex.process_group,
406 },
407 Some(stop),
408 codex.termination_grace,
409 )
410 .await;
411
412 match &result {
413 Err(Error::Cancelled { .. }) => outcome.settle("cancelled", None),
414 other => outcome.settle_from_ref(other),
415 }
416 result
417 }
418 .instrument(span)
419 .await
420}
421
422pub async fn run_codex_allow_exit_codes(
424 codex: &Codex,
425 args: Vec<String>,
426 allowed_codes: &[i32],
427) -> Result<CommandOutput> {
428 let output = run_codex(codex, args).await;
429
430 match output {
431 Err(e)
435 if e.exit_code()
436 .is_some_and(|code| allowed_codes.contains(&code)) =>
437 {
438 let exit_code = e.exit_code().unwrap_or(-1);
439 let (stdout, stderr) = match &e {
440 Error::CommandFailed { stdout, stderr, .. } => (stdout.clone(), stderr.clone()),
441 Error::Auth { message, .. }
442 | Error::Config { message, .. }
443 | Error::NotTrustedDirectory { message, .. }
444 | Error::SessionNotFound { message, .. } => (String::new(), message.clone()),
445 _ => (String::new(), String::new()),
446 };
447 Ok(CommandOutput {
448 stdout,
449 stderr,
450 exit_code,
451 success: false,
452 })
453 }
454 other => other,
455 }
456}
457
458pub async fn run_codex_with_stdin_prompt(
470 codex: &Codex,
471 args: Vec<String>,
472 prompt: &str,
473) -> Result<CommandOutput> {
474 let span = command_span("codex.exec", codex, &args);
475 let outcome_span = span.clone();
476 let command_args = assemble_args(codex, args);
477
478 async move {
479 debug!(
480 binary = %codex.binary.display(),
481 args = ?command_args,
482 prompt_bytes = prompt.len(),
483 "executing codex command with a stdin prompt"
484 );
485
486 let mut outcome = SpanOutcome::start(outcome_span);
487 let stop = codex.timeout.map(timeout_stop);
488 let result = run_internal_inner(
489 SpawnSpec {
490 binary: &codex.binary,
491 args: &command_args,
492 env: &codex.env,
493 clear_env: codex.clear_env,
494 working_dir: codex.working_dir.as_deref(),
495 stdin_prompt: Some(prompt),
496 process_group: codex.process_group,
497 },
498 stop,
499 codex.termination_grace,
500 )
501 .await;
502 outcome.settle_from(&result);
503 result
504 }
505 .instrument(span)
506 .await
507}
508
509pub async fn run_codex_with_stdin_prompt_cancellable<C>(
514 codex: &Codex,
515 args: Vec<String>,
516 prompt: &str,
517 cancel: C,
518) -> Result<CommandOutput>
519where
520 C: std::future::Future<Output = ()> + Send,
521{
522 let span = command_span("codex.exec", codex, &args);
523 let outcome_span = span.clone();
524 let command_args = assemble_args(codex, args);
525
526 async move {
527 debug!(
528 binary = %codex.binary.display(),
529 args = ?command_args,
530 prompt_bytes = prompt.len(),
531 "executing cancellable codex command with a stdin prompt"
532 );
533
534 let mut outcome = SpanOutcome::start(outcome_span);
535 let stop = cancellation_or_timeout(cancel, codex.timeout, codex.termination_grace);
536 let result = run_internal_inner(
537 SpawnSpec {
538 binary: &codex.binary,
539 args: &command_args,
540 env: &codex.env,
541 clear_env: codex.clear_env,
542 working_dir: codex.working_dir.as_deref(),
543 stdin_prompt: Some(prompt),
544 process_group: codex.process_group,
545 },
546 Some(stop),
547 codex.termination_grace,
548 )
549 .await;
550
551 match &result {
552 Err(Error::Cancelled { .. }) => outcome.settle("cancelled", None),
553 other => outcome.settle_from_ref(other),
554 }
555 result
556 }
557 .instrument(span)
558 .await
559}
560
561async fn run_internal(
562 binary: &std::path::Path,
563 args: &[String],
564 env: &std::collections::HashMap<String, String>,
565 clear_env: bool,
566 working_dir: Option<&std::path::Path>,
567 process_group: bool,
568) -> Result<CommandOutput> {
569 run_internal_inner(
570 SpawnSpec {
571 binary,
572 args,
573 env,
574 clear_env,
575 working_dir,
576 stdin_prompt: None,
577 process_group,
578 },
579 None,
580 Duration::from_secs(0),
581 )
582 .await
583}
584
585#[derive(Clone, Copy, Debug)]
586enum StopReason {
587 Cancelled { grace_seconds: u64 },
588 Timeout { timeout_seconds: u64 },
589}
590
591impl StopReason {
592 fn into_error(self) -> Error {
593 match self {
594 Self::Cancelled { grace_seconds } => Error::Cancelled { grace_seconds },
595 Self::Timeout { timeout_seconds } => Error::Timeout { timeout_seconds },
596 }
597 }
598}
599
600type StopFuture<'a> = std::pin::Pin<Box<dyn std::future::Future<Output = StopReason> + Send + 'a>>;
601
602fn timeout_stop(timeout: Duration) -> StopFuture<'static> {
603 Box::pin(async move {
604 tokio::time::sleep(timeout).await;
605 StopReason::Timeout {
606 timeout_seconds: timeout.as_secs(),
607 }
608 })
609}
610
611fn cancellation_or_timeout<'a, C>(
612 cancel: C,
613 timeout: Option<Duration>,
614 grace: Duration,
615) -> StopFuture<'a>
616where
617 C: std::future::Future<Output = ()> + Send + 'a,
618{
619 Box::pin(async move {
620 match timeout {
621 Some(timeout) => tokio::select! {
622 () = cancel => StopReason::Cancelled {
623 grace_seconds: grace.as_secs(),
624 },
625 () = tokio::time::sleep(timeout) => StopReason::Timeout {
626 timeout_seconds: timeout.as_secs(),
627 },
628 },
629 None => {
630 cancel.await;
631 StopReason::Cancelled {
632 grace_seconds: grace.as_secs(),
633 }
634 }
635 }
636 })
637}
638
639struct SpawnSpec<'a> {
641 binary: &'a std::path::Path,
642 args: &'a [String],
643 env: &'a std::collections::HashMap<String, String>,
644 clear_env: bool,
646 working_dir: Option<&'a std::path::Path>,
647 stdin_prompt: Option<&'a str>,
649 process_group: bool,
651}
652
653async fn run_internal_inner(
654 spec: SpawnSpec<'_>,
655 stop: Option<StopFuture<'_>>,
656 grace: Duration,
657) -> Result<CommandOutput> {
658 let SpawnSpec {
659 binary,
660 args,
661 env,
662 clear_env,
663 working_dir,
664 stdin_prompt,
665 process_group,
666 } = spec;
667 let mut cmd = Command::new(binary);
668 cmd.args(args);
669
670 if stdin_prompt.is_some() {
673 cmd.stdin(std::process::Stdio::piped());
674 } else {
675 cmd.stdin(std::process::Stdio::null());
676 }
677
678 cmd.kill_on_drop(true);
682 own_process_group(&mut cmd, process_group);
683
684 if let Some(dir) = working_dir {
685 cmd.current_dir(dir);
686 }
687
688 apply_child_environment(&mut cmd, clear_env, env);
689
690 cmd.stdout(std::process::Stdio::piped());
694 cmd.stderr(std::process::Stdio::piped());
695
696 let mut child = cmd.spawn().map_err(|e| Error::Io {
697 message: format!("failed to spawn codex: {e}"),
698 source: e,
699 working_dir: working_dir.map(|p| p.to_path_buf()),
700 })?;
701
702 let mut group = GroupKillGuard::new(process_group.then(|| child.id()).flatten());
707 let child_stdin = child.stdin.take();
708 let mut child_stdout = child.stdout.take().expect("stdout was configured as piped");
709 let mut child_stderr = child.stderr.take().expect("stderr was configured as piped");
710
711 let write = async move {
712 let (Some(prompt), Some(mut stdin)) = (stdin_prompt, child_stdin) else {
713 return Ok(());
714 };
715 use tokio::io::AsyncWriteExt;
716 stdin
717 .write_all(prompt.as_bytes())
718 .await
719 .map_err(|e| Error::Io {
720 message: format!("failed to write the prompt to codex stdin: {e}"),
721 source: e,
722 working_dir: working_dir.map(|p| p.to_path_buf()),
723 })?;
724 stdin.shutdown().await.map_err(|e| Error::Io {
727 message: format!("failed to close codex stdin: {e}"),
728 source: e,
729 working_dir: working_dir.map(|p| p.to_path_buf()),
730 })
731 };
732
733 let read_stdout = async move {
738 use tokio::io::AsyncReadExt;
739 let mut bytes = Vec::new();
740 child_stdout
741 .read_to_end(&mut bytes)
742 .await
743 .map(|_| bytes)
744 .map_err(|e| Error::Io {
745 message: format!("failed to read codex stdout: {e}"),
746 source: e,
747 working_dir: working_dir.map(|p| p.to_path_buf()),
748 })
749 };
750 let read_stderr = async move {
751 use tokio::io::AsyncReadExt;
752 let mut bytes = Vec::new();
753 child_stderr
754 .read_to_end(&mut bytes)
755 .await
756 .map(|_| bytes)
757 .map_err(|e| Error::Io {
758 message: format!("failed to read codex stderr: {e}"),
759 source: e,
760 working_dir: working_dir.map(|p| p.to_path_buf()),
761 })
762 };
763 let wait = async { child.wait().await.map_err(|e| wait_error(e, working_dir)) };
764 let run = async {
765 let ((), stdout, stderr, status) = tokio::try_join!(write, read_stdout, read_stderr, wait)?;
766 Ok::<_, Error>((stdout, stderr, status))
767 };
768
769 let finished = match stop {
770 None => Ok(run.await),
771 Some(stop) => tokio::select! {
775 outcome = run => Ok(outcome),
776 reason = stop => Err(reason),
777 },
778 };
779
780 let (stdout, stderr, status) = match finished {
781 Ok(Ok(finished)) => finished,
782 Ok(Err(error)) => {
783 terminate_and_reap(&mut child, &mut group, grace, working_dir).await?;
787 return Err(error);
788 }
789 Err(reason) => {
790 terminate_and_reap(&mut child, &mut group, grace, working_dir).await?;
796 return Err(reason.into_error());
797 }
798 };
799
800 group.disarm();
802
803 let stdout = String::from_utf8_lossy(&stdout).to_string();
804 let stderr = String::from_utf8_lossy(&stderr).to_string();
805 let exit_code = status.code().unwrap_or(-1);
806
807 if !status.success() {
808 return Err(Error::from_command_failure(
809 format!("{} {}", binary.display(), args.join(" ")),
810 exit_code,
811 stdout,
812 stderr,
813 working_dir.map(|p| p.to_path_buf()),
814 ));
815 }
816
817 Ok(CommandOutput {
818 stdout,
819 stderr,
820 exit_code,
821 success: true,
822 })
823}
824
825async fn terminate_and_reap(
826 child: &mut tokio::process::Child,
827 group: &mut GroupKillGuard,
828 grace: Duration,
829 working_dir: Option<&std::path::Path>,
830) -> Result<()> {
831 group.terminate(grace).await;
832 if child
833 .try_wait()
834 .map_err(|e| wait_error(e, working_dir))?
835 .is_none()
836 && let Err(error) = child.start_kill()
837 && error.kind() != std::io::ErrorKind::InvalidInput
838 {
839 return Err(wait_error(error, working_dir));
840 }
841 child.wait().await.map_err(|e| wait_error(e, working_dir))?;
842 Ok(())
843}
844
845fn wait_error(error: std::io::Error, working_dir: Option<&std::path::Path>) -> Error {
846 Error::Io {
847 message: format!("failed to wait on codex: {error}"),
848 source: error,
849 working_dir: working_dir.map(|p| p.to_path_buf()),
850 }
851}
852
853pub(crate) fn apply_child_environment(
858 cmd: &mut Command,
859 clear_env: bool,
860 env: &std::collections::HashMap<String, String>,
861) {
862 if clear_env {
863 cmd.env_clear();
864 }
865 cmd.envs(env);
866}
867
868#[cfg(test)]
869mod tests {
870 use super::*;
871 use crate::CodexCommand;
872
873 #[test]
874 fn typed_rollout_budget_suppresses_conflicting_client_global_toggles() {
875 let codex = Codex::builder()
876 .binary("/bin/echo")
877 .config("features.rollout_budget=false")
878 .enable("rollout_budget")
879 .disable("rollout_budget")
880 .arg("--enable=rollout_budget")
881 .arg("--disable=rollout_budget")
882 .arg("--disable")
883 .arg("rollout_budget")
884 .enable("keep-enabled")
885 .disable("keep-disabled")
886 .build()
887 .expect("echo must exist");
888 let budget = crate::RolloutBudgetConfig::builder(10_000)
889 .build()
890 .expect("valid budget");
891 let expected = budget.config_override();
892 let opening = crate::ExecCommand::new("hi")
893 .rollout_budget(budget.clone())
894 .args();
895 let resumed = crate::ExecResumeCommand::new()
896 .session_id("thread")
897 .rollout_budget(budget)
898 .args();
899
900 for args in [opening, resumed] {
901 let assembled = assemble_args(&codex, args);
902 assert!(assembled.iter().any(|arg| arg == &expected));
903 assert!(
904 !assembled.windows(2).any(|pair| {
905 matches!(pair[0].as_str(), "--enable" | "--disable")
906 && pair[1] == "rollout_budget"
907 }),
908 "typed budget must suppress paired client toggles: {assembled:?}"
909 );
910 assert!(
911 !assembled.iter().any(|arg| {
912 matches!(
913 arg.as_str(),
914 "--enable=rollout_budget" | "--disable=rollout_budget"
915 )
916 }),
917 "typed budget must suppress equals-form client toggles: {assembled:?}"
918 );
919 assert!(
920 assembled
921 .windows(2)
922 .any(|pair| pair == ["--enable", "keep-enabled"])
923 );
924 assert!(
925 assembled
926 .windows(2)
927 .any(|pair| pair == ["--disable", "keep-disabled"])
928 );
929 }
930 }
931
932 fn make_output(stdout: &str, stderr: &str) -> CommandOutput {
933 CommandOutput {
934 stdout: stdout.to_string(),
935 stderr: stderr.to_string(),
936 exit_code: 0,
937 success: true,
938 }
939 }
940
941 #[test]
942 fn shell_quote_leaves_plain_words_alone() {
943 assert_eq!(shell_quote("exec"), "exec");
944 assert_eq!(shell_quote("--ephemeral"), "--ephemeral");
945 assert_eq!(shell_quote("model=gpt-5"), "model=gpt-5");
946 }
947
948 #[test]
949 fn shell_quote_wraps_anything_a_shell_would_read() {
950 assert_eq!(shell_quote("fix the tests"), "'fix the tests'");
951 assert_eq!(shell_quote("$HOME"), "'$HOME'");
952 assert_eq!(shell_quote("a;b"), "'a;b'");
953 assert_eq!(shell_quote("*.rs"), "'*.rs'");
954 assert_eq!(shell_quote("it's"), r"'it'\''s'");
955 }
956
957 #[test]
960 fn shell_quote_keeps_the_empty_argument_visible() {
961 assert_eq!(shell_quote(""), "''");
962 }
963
964 #[test]
965 fn debug_short_output_not_truncated() {
966 let output = make_output("hello", "world");
967 let debug = format!("{output:?}");
968 assert!(debug.contains("hello"));
969 assert!(debug.contains("world"));
970 assert!(!debug.contains("bytes total"));
971 }
972
973 #[test]
974 fn debug_long_output_truncated() {
975 let long = "x".repeat(300);
976 let output = make_output(&long, &long);
977 let debug = format!("{output:?}");
978 assert!(debug.contains("... (300 bytes total)"));
979 assert!(!debug.contains(&long));
980 }
981
982 #[cfg(unix)]
985 #[tokio::test]
986 async fn child_environment_is_inherited_by_default() {
987 let capture = crate::test_support::EnvCapture::new("env-default");
988 let codex = crate::test_support::env_capturing_codex(&capture)
989 .build()
990 .expect("bash must exist");
991
992 crate::ExecCommand::new("probe")
993 .execute(&codex)
994 .await
995 .unwrap();
996
997 let environment = capture.read();
998 assert_eq!(
999 environment.get("PATH"),
1000 Some(&std::env::var("PATH").expect("test process must have PATH"))
1001 );
1002 }
1003
1004 #[cfg(unix)]
1008 #[tokio::test]
1009 async fn cleared_environment_reaches_buffered_open_and_resume() {
1010 let capture = crate::test_support::EnvCapture::new("env-buffered");
1011 let opening_client = crate::test_support::env_capturing_codex(&capture)
1012 .clear_env()
1013 .env("CODEX_WRAPPER_EXPLICIT", "opening")
1014 .timeout(Duration::from_secs(2))
1015 .build()
1016 .expect("bash must exist");
1017
1018 crate::ExecCommand::new("probe")
1019 .execute(&opening_client)
1020 .await
1021 .unwrap();
1022 let opening_environment = capture.read();
1023 assert!(!opening_environment.contains_key("PATH"));
1024 assert_eq!(
1025 opening_environment
1026 .get("CODEX_WRAPPER_EXPLICIT")
1027 .map(String::as_str),
1028 Some("opening")
1029 );
1030 assert!(opening_environment.contains_key("CODEX_WRAPPER_ENV_CAPTURE"));
1031
1032 let resume_client = crate::test_support::env_capturing_codex(&capture)
1033 .clear_env()
1034 .env("CODEX_WRAPPER_EXPLICIT", "resume")
1035 .build()
1036 .expect("bash must exist");
1037 crate::ExecResumeCommand::new()
1038 .last()
1039 .execute(&resume_client)
1040 .await
1041 .unwrap();
1042 let resume_environment = capture.read();
1043 assert!(!resume_environment.contains_key("PATH"));
1044 assert_eq!(
1045 resume_environment
1046 .get("CODEX_WRAPPER_EXPLICIT")
1047 .map(String::as_str),
1048 Some("resume")
1049 );
1050 }
1051
1052 #[cfg(unix)]
1056 #[tokio::test]
1057 async fn cleared_environment_reaches_stdin_and_cancellable_runs() {
1058 let capture = crate::test_support::EnvCapture::new("env-specialized");
1059 let codex = crate::test_support::env_capturing_codex(&capture)
1060 .env("CODEX_WRAPPER_EXPLICIT", "specialized")
1061 .clear_env()
1062 .build()
1063 .expect("bash must exist");
1064
1065 crate::ExecCommand::new("stdin prompt")
1066 .prompt_via_stdin()
1067 .execute(&codex)
1068 .await
1069 .unwrap();
1070 let stdin_environment = capture.read();
1071 assert!(!stdin_environment.contains_key("PATH"));
1072 assert_eq!(
1073 stdin_environment
1074 .get("CODEX_WRAPPER_EXPLICIT")
1075 .map(String::as_str),
1076 Some("specialized")
1077 );
1078
1079 let never = std::future::pending::<()>();
1080 run_codex_cancellable(&codex, crate::ExecCommand::new("cancellable").args(), never)
1081 .await
1082 .unwrap();
1083 let cancellable_environment = capture.read();
1084 assert!(!cancellable_environment.contains_key("PATH"));
1085 assert_eq!(
1086 cancellable_environment
1087 .get("CODEX_WRAPPER_EXPLICIT")
1088 .map(String::as_str),
1089 Some("specialized")
1090 );
1091 }
1092
1093 #[cfg(unix)]
1094 #[tokio::test]
1095 async fn environment_values_do_not_leak_into_spawn_errors() {
1096 let secret = "spawn-error-must-not-leak-this";
1097 let codex = Codex::builder()
1098 .binary("/codex-wrapper/this-binary-does-not-exist")
1099 .clear_env()
1100 .env("CODEX_WRAPPER_SECRET", secret)
1101 .build()
1102 .unwrap();
1103
1104 let error = run_codex(&codex, vec!["exec".into()])
1105 .await
1106 .expect_err("the fake path must not spawn");
1107 assert!(!error.to_string().contains(secret));
1108 assert!(!format!("{error:?}").contains(secret));
1109 }
1110
1111 #[cfg(unix)]
1113 #[tokio::test]
1114 async fn timeout_kills_the_spawned_process() {
1115 use crate::test_support::{PidFile, blocking_codex, is_running_for_test};
1116
1117 let pid_file = PidFile::new("exec-timeout");
1118 let codex = blocking_codex(&pid_file)
1119 .timeout(Duration::from_millis(500))
1120 .build()
1121 .expect("bash must exist");
1122
1123 let result = run_codex(&codex, vec!["exec".into(), "probe".into()]).await;
1124 assert!(
1125 matches!(result, Err(Error::Timeout { .. })),
1126 "expected timeout error, got: {result:?}"
1127 );
1128
1129 let pid = pid_file.read_pid().await;
1130 assert!(
1131 !is_running_for_test(pid),
1132 "codex ({pid}) survived the timeout"
1133 );
1134 }
1135
1136 #[cfg(unix)]
1139 #[tokio::test]
1140 async fn cancellation_kills_the_spawned_process() {
1141 use crate::test_support::{PidFile, blocking_codex, wait_until_gone};
1142
1143 let pid_file = PidFile::new("exec-cancel");
1144 let codex = blocking_codex(&pid_file).build().expect("bash must exist");
1145
1146 let cancelled = tokio::time::timeout(
1147 Duration::from_millis(500),
1148 run_codex(&codex, vec!["exec".into(), "probe".into()]),
1149 )
1150 .await;
1151 assert!(
1152 cancelled.is_err(),
1153 "fake codex should still have been running, got: {cancelled:?}"
1154 );
1155
1156 let pid = pid_file.read_pid().await;
1157 assert!(
1158 wait_until_gone(pid).await,
1159 "codex ({pid}) survived the dropped future"
1160 );
1161 }
1162
1163 #[cfg(unix)]
1174 mod recorder {
1175 use std::cell::RefCell;
1176 use std::sync::{Arc, Mutex, Once};
1177
1178 use tracing::field::{Field, Visit};
1179 use tracing::span::{Attributes, Id, Record};
1180 use tracing::{Event, Metadata, Subscriber};
1181
1182 type Sink = Arc<Mutex<Vec<(String, String)>>>;
1183
1184 thread_local! {
1185 static SINK: RefCell<Option<Sink>> = const { RefCell::new(None) };
1186 }
1187
1188 struct Global;
1189
1190 impl Global {
1191 fn collect(f: impl FnOnce(&mut Vec<(String, String)>)) {
1192 SINK.with(|sink| {
1193 if let Some(sink) = sink.borrow().as_ref() {
1194 f(&mut sink.lock().unwrap());
1195 }
1196 });
1197 }
1198 }
1199
1200 impl Subscriber for Global {
1201 fn enabled(&self, _: &Metadata<'_>) -> bool {
1203 true
1204 }
1205 fn new_span(&self, attrs: &Attributes<'_>) -> Id {
1206 Self::collect(|fields| attrs.record(&mut Collect(fields)));
1207 Id::from_u64(1)
1208 }
1209 fn record(&self, _: &Id, values: &Record<'_>) {
1210 Self::collect(|fields| values.record(&mut Collect(fields)));
1211 }
1212 fn record_follows_from(&self, _: &Id, _: &Id) {}
1213 fn event(&self, _: &Event<'_>) {}
1214 fn enter(&self, _: &Id) {}
1215 fn exit(&self, _: &Id) {}
1216 }
1217
1218 struct Collect<'a>(&'a mut Vec<(String, String)>);
1219
1220 impl Visit for Collect<'_> {
1221 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
1222 self.0.push((field.name().into(), format!("{value:?}")));
1223 }
1224 fn record_str(&mut self, field: &Field, value: &str) {
1225 self.0.push((field.name().into(), value.into()));
1226 }
1227 fn record_i64(&mut self, field: &Field, value: i64) {
1228 self.0.push((field.name().into(), value.to_string()));
1229 }
1230 fn record_u64(&mut self, field: &Field, value: u64) {
1231 self.0.push((field.name().into(), value.to_string()));
1232 }
1233 }
1234
1235 pub(super) struct Recorder(Sink);
1236
1237 impl Recorder {
1238 pub(super) fn install() -> Self {
1240 static INIT: Once = Once::new();
1241 INIT.call_once(|| {
1242 let _ = tracing::subscriber::set_global_default(Global);
1243 });
1244 let sink: Sink = Arc::new(Mutex::new(Vec::new()));
1245 SINK.with(|slot| *slot.borrow_mut() = Some(Arc::clone(&sink)));
1246 Self(sink)
1247 }
1248
1249 pub(super) fn dump(&self) -> String {
1250 format!("{:?}", self.0.lock().unwrap())
1251 }
1252
1253 pub(super) fn value(&self, field: &str) -> Option<String> {
1254 self.0
1255 .lock()
1256 .unwrap()
1257 .iter()
1258 .rev()
1259 .find(|(name, _)| name == field)
1260 .map(|(_, value)| value.clone())
1261 }
1262 }
1263
1264 impl Drop for Recorder {
1265 fn drop(&mut self) {
1266 SINK.with(|slot| *slot.borrow_mut() = None);
1267 }
1268 }
1269 }
1270
1271 #[cfg(unix)]
1272 #[tokio::test]
1273 async fn span_records_the_subcommand_and_a_clean_outcome() {
1274 let recorder = recorder::Recorder::install();
1275
1276 let codex = Codex::builder()
1277 .binary("/bin/echo")
1278 .build()
1279 .expect("echo must exist");
1280 run_codex(&codex, vec!["exec".into()]).await.unwrap();
1281
1282 assert_eq!(recorder.value("subcommand").as_deref(), Some("exec"));
1283 assert_eq!(recorder.value("outcome").as_deref(), Some("ok"));
1284 assert_eq!(recorder.value("exit_code").as_deref(), Some("0"));
1285 assert!(recorder.value("duration_ms").is_some());
1286 }
1287
1288 #[cfg(unix)]
1291 #[tokio::test]
1292 async fn span_does_not_carry_the_prompt() {
1293 let recorder = recorder::Recorder::install();
1294
1295 let codex = Codex::builder()
1296 .binary("/bin/echo")
1297 .build()
1298 .expect("echo must exist");
1299 run_codex(&codex, vec!["exec".into(), "a very secret prompt".into()])
1300 .await
1301 .unwrap();
1302
1303 let recorded = format!("{:?}", recorder.value("subcommand"));
1304 assert!(!recorded.contains("secret"));
1305 for field in ["binary", "working_dir", "outcome", "exit_code"] {
1306 let value = recorder.value(field).unwrap_or_default();
1307 assert!(
1308 !value.contains("secret"),
1309 "{field} leaked the prompt: {value}"
1310 );
1311 }
1312 }
1313
1314 #[cfg(unix)]
1317 #[tokio::test]
1318 async fn a_cancelled_run_is_recorded_as_cancelled() {
1319 let recorder = recorder::Recorder::install();
1320
1321 let pid_file = crate::test_support::PidFile::new("span-cancel");
1322 let codex = crate::test_support::blocking_codex(&pid_file)
1323 .build()
1324 .expect("bash must exist");
1325
1326 let cancelled = tokio::time::timeout(
1327 Duration::from_millis(300),
1328 run_codex(&codex, vec!["exec".into()]),
1329 )
1330 .await;
1331 assert!(cancelled.is_err(), "the run should still have been going");
1332
1333 assert_eq!(
1334 recorder.value("outcome").as_deref(),
1335 Some("cancelled"),
1336 "recorded: {}",
1337 recorder.dump()
1338 );
1339 }
1340
1341 #[cfg(unix)]
1344 #[tokio::test]
1345 async fn a_timed_out_run_is_recorded_as_timeout() {
1346 let recorder = recorder::Recorder::install();
1347
1348 let pid_file = crate::test_support::PidFile::new("span-timeout");
1349 let codex = crate::test_support::blocking_codex(&pid_file)
1350 .timeout(Duration::from_millis(300))
1351 .build()
1352 .expect("bash must exist");
1353
1354 let result = run_codex(&codex, vec!["exec".into()]).await;
1355 assert!(matches!(result, Err(Error::Timeout { .. })), "{result:?}");
1356
1357 assert_eq!(recorder.value("outcome").as_deref(), Some("timeout"));
1358 }
1359
1360 #[cfg(unix)]
1365 fn failing_codex(case: &str) -> Codex {
1366 let script = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1367 .join("tests")
1368 .join("fake-codex-failure.sh");
1369 Codex::builder()
1370 .binary("/bin/bash")
1371 .arg(script.to_str().unwrap())
1372 .env("CODEX_WRAPPER_TEST_FAILURE", case)
1373 .build()
1374 .expect("bash must exist")
1375 }
1376
1377 #[cfg(unix)]
1380 #[tokio::test]
1381 async fn a_real_spawn_returns_a_classified_error() {
1382 use crate::error::FailureKind;
1383
1384 for (case, expected) in [
1385 ("auth", FailureKind::Auth),
1386 ("not-trusted", FailureKind::NotTrustedDirectory),
1387 ("config", FailureKind::Config),
1388 ("session", FailureKind::SessionNotFound),
1389 ("mystery", FailureKind::Unclassified),
1390 ] {
1391 let codex = failing_codex(case);
1392 let err = run_codex(&codex, vec!["exec".into()]).await.unwrap_err();
1393 assert_eq!(err.failure_kind(), Some(expected), "case {case}: {err}");
1394 }
1395 }
1396
1397 #[cfg(unix)]
1401 #[tokio::test]
1402 async fn a_classified_failure_is_not_retried() {
1403 let policy = crate::retry::RetryPolicy::new()
1404 .max_attempts(3)
1405 .initial_backoff(Duration::from_millis(1))
1406 .retry_on_exit_codes([1]);
1407
1408 let started = Instant::now();
1409 let codex = failing_codex("auth");
1410 let err = run_codex_with_retry(&codex, vec!["exec".into()], Some(&policy))
1411 .await
1412 .unwrap_err();
1413
1414 assert!(matches!(err, Error::Auth { .. }), "{err}");
1415 assert!(
1418 started.elapsed() < Duration::from_secs(2),
1419 "looks like it retried: {:?}",
1420 started.elapsed()
1421 );
1422 }
1423
1424 #[cfg(unix)]
1426 #[tokio::test]
1427 async fn an_unclassified_failure_still_retries() {
1428 let policy = crate::retry::RetryPolicy::new()
1429 .max_attempts(2)
1430 .initial_backoff(Duration::from_millis(1))
1431 .retry_on_exit_codes([1]);
1432
1433 let codex = failing_codex("mystery");
1434 let err = run_codex_with_retry(&codex, vec!["exec".into()], Some(&policy))
1435 .await
1436 .unwrap_err();
1437
1438 assert!(matches!(err, Error::CommandFailed { .. }), "{err}");
1439 }
1440
1441 #[cfg(unix)]
1444 #[tokio::test]
1445 async fn allowed_exit_codes_still_apply_to_a_classified_failure() {
1446 let codex = failing_codex("auth");
1447 let output = run_codex_allow_exit_codes(&codex, vec!["exec".into()], &[1])
1448 .await
1449 .expect("exit code 1 was allowed");
1450
1451 assert_eq!(output.exit_code, 1);
1452 assert!(!output.success);
1453 assert!(
1454 output.stderr.contains("401 Unauthorized"),
1455 "{}",
1456 output.stderr
1457 );
1458 }
1459
1460 #[cfg(unix)]
1467 fn spawning_codex(label: &str) -> (Codex, crate::test_support::PidFile) {
1468 let pid_file = crate::test_support::PidFile::new(label);
1469 let script = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1470 .join("tests")
1471 .join("fake-codex-spawns-child.sh");
1472 let codex = Codex::builder()
1473 .binary("/bin/bash")
1474 .arg(script.to_str().unwrap())
1475 .env(
1476 "CODEX_WRAPPER_TEST_PIDFILE",
1477 pid_file.path().to_str().unwrap(),
1478 )
1479 .build()
1480 .expect("bash must exist");
1481 (codex, pid_file)
1482 }
1483
1484 #[cfg(unix)]
1485 async fn read_pids(pid_file: &crate::test_support::PidFile) -> (u32, u32) {
1486 for _ in 0..200 {
1487 if let Ok(contents) = std::fs::read_to_string(pid_file.path()) {
1488 let parse = |prefix: &str| -> Option<u32> {
1489 contents
1490 .lines()
1491 .find_map(|l| l.strip_prefix(prefix))
1492 .and_then(|v| v.trim().parse().ok())
1493 };
1494 if let (Some(parent), Some(child)) = (parse("parent="), parse("child=")) {
1495 return (parent, child);
1496 }
1497 }
1498 tokio::time::sleep(Duration::from_millis(10)).await;
1499 }
1500 panic!("the fake codex never recorded both pids");
1501 }
1502
1503 #[cfg(unix)]
1506 #[tokio::test]
1507 async fn cancelling_kills_the_whole_process_group() {
1508 use crate::test_support::wait_until_gone;
1509
1510 let (codex, pid_file) = spawning_codex("group-drop");
1511
1512 let cancelled = tokio::time::timeout(
1513 Duration::from_millis(400),
1514 run_codex(&codex, vec!["exec".into()]),
1515 )
1516 .await;
1517 assert!(cancelled.is_err(), "the run should still have been going");
1518
1519 let (parent, child) = read_pids(&pid_file).await;
1520 assert!(wait_until_gone(parent).await, "codex ({parent}) survived");
1521 assert!(
1522 wait_until_gone(child).await,
1523 "the subprocess ({child}) survived the cancelled run"
1524 );
1525 }
1526
1527 #[cfg(unix)]
1530 #[tokio::test]
1531 async fn run_codex_cancellable_stops_the_group_gracefully() {
1532 use crate::test_support::is_running_for_test;
1533
1534 let (codex, pid_file) = spawning_codex("group-cancel");
1535 let codex = Codex::builder()
1536 .binary(codex.binary())
1537 .arg(
1538 std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1539 .join("tests")
1540 .join("fake-codex-spawns-child.sh")
1541 .to_str()
1542 .unwrap(),
1543 )
1544 .env(
1545 "CODEX_WRAPPER_TEST_PIDFILE",
1546 pid_file.path().to_str().unwrap(),
1547 )
1548 .termination_grace(Duration::from_millis(50))
1549 .build()
1550 .unwrap();
1551
1552 let cancel = async {
1553 tokio::time::sleep(Duration::from_millis(300)).await;
1554 };
1555 let result = run_codex_cancellable(&codex, vec!["exec".into()], cancel).await;
1556
1557 assert!(
1558 matches!(result, Err(Error::Cancelled { .. })),
1559 "expected a cancellation, got: {result:?}"
1560 );
1561
1562 let (parent, child) = read_pids(&pid_file).await;
1563 assert!(!is_running_for_test(parent), "codex ({parent}) survived");
1564 assert!(
1565 !is_running_for_test(child),
1566 "the subprocess ({child}) survived cancellation"
1567 );
1568 }
1569
1570 #[cfg(unix)]
1573 #[tokio::test]
1574 async fn timeout_stops_the_group_before_returning() {
1575 use crate::test_support::is_running_for_test;
1576
1577 let pid_file = crate::test_support::PidFile::new("group-timeout-settled");
1578 let script = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1579 .join("tests")
1580 .join("fake-codex-spawns-child.sh");
1581 let codex = Codex::builder()
1582 .binary("/bin/bash")
1583 .arg(script.to_str().unwrap())
1584 .env(
1585 "CODEX_WRAPPER_TEST_PIDFILE",
1586 pid_file.path().to_str().unwrap(),
1587 )
1588 .timeout(Duration::from_millis(300))
1589 .termination_grace(Duration::from_millis(10))
1590 .build()
1591 .unwrap();
1592
1593 let result = run_codex(&codex, vec!["exec".into()]).await;
1594 assert!(
1595 matches!(result, Err(Error::Timeout { .. })),
1596 "expected a timeout, got: {result:?}"
1597 );
1598
1599 let (parent, child) = read_pids(&pid_file).await;
1600 assert!(!is_running_for_test(parent), "codex ({parent}) survived");
1601 assert!(
1602 !is_running_for_test(child),
1603 "the subprocess ({child}) survived the timeout"
1604 );
1605 }
1606
1607 #[cfg(unix)]
1611 #[tokio::test]
1612 async fn stdin_write_failure_stops_and_reaps_before_returning() {
1613 use crate::test_support::is_running_for_test;
1614
1615 let pid_file = crate::test_support::PidFile::new("stdin-write-failure");
1616 let script = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1617 .join("tests")
1618 .join("fake-codex-closes-stdin-spawns-child.sh");
1619 let codex = Codex::builder()
1620 .binary("/bin/bash")
1621 .arg(script.to_str().unwrap())
1622 .env(
1623 "CODEX_WRAPPER_TEST_PIDFILE",
1624 pid_file.path().to_str().unwrap(),
1625 )
1626 .termination_grace(Duration::from_millis(10))
1627 .build()
1628 .unwrap();
1629
1630 let prompt = "x".repeat(4 * 1024 * 1024);
1631 let result = run_codex_with_stdin_prompt(
1632 &codex,
1633 crate::ExecCommand::from_stdin(&prompt).args(),
1634 &prompt,
1635 )
1636 .await;
1637 assert!(
1638 matches!(result, Err(Error::Io { ref message, .. }) if message.contains("stdin")),
1639 "expected a stdin error, got: {result:?}"
1640 );
1641
1642 let (parent, child) = read_pids(&pid_file).await;
1643 assert!(!is_running_for_test(parent), "codex ({parent}) survived");
1644 assert!(
1645 !is_running_for_test(child),
1646 "the subprocess ({child}) survived the stdin failure"
1647 );
1648 }
1649
1650 #[cfg(unix)]
1653 #[tokio::test]
1654 async fn a_run_that_finishes_first_is_not_cancelled() {
1655 let codex = Codex::builder()
1656 .binary("/bin/echo")
1657 .build()
1658 .expect("echo must exist");
1659
1660 let never = std::future::pending::<()>();
1661 let output = run_codex_cancellable(&codex, vec!["exec".into()], never)
1662 .await
1663 .unwrap();
1664 assert!(output.success);
1665 }
1666
1667 #[cfg(unix)]
1672 #[tokio::test]
1673 async fn opting_out_of_process_groups_leaves_the_subprocess() {
1674 use crate::test_support::wait_until_gone;
1675
1676 let pid_file = crate::test_support::PidFile::new("group-optout");
1677 let script = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1678 .join("tests")
1679 .join("fake-codex-spawns-child.sh");
1680 let codex = Codex::builder()
1681 .binary("/bin/bash")
1682 .arg(script.to_str().unwrap())
1683 .env(
1684 "CODEX_WRAPPER_TEST_PIDFILE",
1685 pid_file.path().to_str().unwrap(),
1686 )
1687 .process_group(false)
1688 .build()
1689 .expect("bash must exist");
1690
1691 let cancelled = tokio::time::timeout(
1692 Duration::from_millis(400),
1693 run_codex(&codex, vec!["exec".into()]),
1694 )
1695 .await;
1696 assert!(cancelled.is_err(), "the run should still have been going");
1697
1698 let (parent, child) = read_pids(&pid_file).await;
1699 assert!(
1700 wait_until_gone(parent).await,
1701 "kill_on_drop still reaps the direct child ({parent})"
1702 );
1703 assert!(
1706 crate::test_support::is_running_for_test(child),
1707 "with groups off, the subprocess ({child}) is expected to survive"
1708 );
1709 signal_group(child, libc::SIGKILL);
1711 unsafe { libc::kill(i32::try_from(child).unwrap_or(0), libc::SIGKILL) };
1712 }
1713}