1use std::time::Duration;
2
3#[cfg(feature = "async")]
4use tokio::io::AsyncReadExt;
5#[cfg(feature = "async")]
6use tokio::process::Command;
7use tracing::{debug, warn};
8
9use crate::Claude;
10use crate::error::{Error, Result};
11
12#[derive(Debug, Clone)]
14pub struct CommandOutput {
15 pub stdout: String,
16 pub stderr: String,
17 pub exit_code: i32,
18 pub success: bool,
19}
20
21#[cfg(feature = "async")]
27pub async fn run_claude(claude: &Claude, args: Vec<String>) -> Result<CommandOutput> {
28 run_claude_with_retry(claude, args, None).await
29}
30
31#[cfg(feature = "async")]
33pub async fn run_claude_with_retry(
34 claude: &Claude,
35 args: Vec<String>,
36 retry_override: Option<&crate::retry::RetryPolicy>,
37) -> Result<CommandOutput> {
38 let policy = retry_override.or(claude.retry_policy.as_ref());
39
40 match policy {
41 Some(policy) => {
42 crate::retry::with_retry(policy, || run_claude_once(claude, args.clone())).await
43 }
44 None => run_claude_once(claude, args).await,
45 }
46}
47
48#[cfg(feature = "async")]
54pub async fn run_claude_with_stdin_prompt(
55 claude: &Claude,
56 args: Vec<String>,
57 stdin_content: String,
58) -> Result<CommandOutput> {
59 run_claude_with_stdin_prompt_internal(claude, args, stdin_content).await
60}
61
62#[cfg(feature = "async")]
63async fn run_claude_with_stdin_prompt_internal(
64 claude: &Claude,
65 args: Vec<String>,
66 stdin_content: String,
67) -> Result<CommandOutput> {
68 let mut command_args = Vec::new();
69 command_args.extend(claude.global_args.clone());
70 command_args.extend(args);
71
72 debug!(binary = %claude.binary.display(), args = ?command_args, "executing claude command (stdin prompt)");
73
74 let binary = &claude.binary;
75 let env = &claude.env;
76 let working_dir = claude.working_dir.as_deref();
77
78 if let Some(timeout) = claude.timeout {
79 run_with_timeout_stdin(
80 binary,
81 &command_args,
82 env,
83 working_dir,
84 timeout,
85 stdin_content,
86 )
87 .await
88 } else {
89 run_internal_stdin(binary, &command_args, env, working_dir, stdin_content).await
90 }
91}
92
93#[cfg(feature = "async")]
94async fn run_internal_stdin(
95 binary: &std::path::Path,
96 args: &[String],
97 env: &std::collections::HashMap<String, String>,
98 working_dir: Option<&std::path::Path>,
99 stdin_content: String,
100) -> Result<CommandOutput> {
101 use tokio::io::AsyncWriteExt;
102
103 let mut cmd = Command::new(binary);
104 cmd.args(args);
105 cmd.stdin(std::process::Stdio::piped());
106 cmd.stdout(std::process::Stdio::piped());
107 cmd.stderr(std::process::Stdio::piped());
108 cmd.env_remove("CLAUDECODE");
109 cmd.env_remove("CLAUDE_CODE_ENTRYPOINT");
110
111 if let Some(dir) = working_dir {
112 cmd.current_dir(dir);
113 }
114
115 for (key, value) in env {
116 cmd.env(key, value);
117 }
118
119 let mut child = cmd.spawn().map_err(|e| Error::Io {
120 message: format!("failed to spawn claude: {e}"),
121 source: e,
122 working_dir: working_dir.map(|p| p.to_path_buf()),
123 })?;
124
125 if let Some(mut stdin) = child.stdin.take() {
127 stdin
128 .write_all(stdin_content.as_bytes())
129 .await
130 .map_err(|e| Error::Io {
131 message: format!("failed to write to claude stdin: {e}"),
132 source: e,
133 working_dir: working_dir.map(|p| p.to_path_buf()),
134 })?;
135 }
137
138 let mut stdout_handle = child.stdout.take().expect("stdout was piped");
139 let mut stderr_handle = child.stderr.take().expect("stderr was piped");
140
141 let (status, stdout_str, stderr_str) = tokio::join!(
142 child.wait(),
143 drain(&mut stdout_handle),
144 drain(&mut stderr_handle),
145 );
146
147 let status = status.map_err(|e| Error::Io {
148 message: "failed to wait for claude process".to_string(),
149 source: e,
150 working_dir: working_dir.map(|p| p.to_path_buf()),
151 })?;
152
153 let exit_code = status.code().unwrap_or(-1);
154
155 if !status.success() {
156 return Err(Error::from_command_failure(
157 format!("{} {}", binary.display(), args.join(" ")),
158 exit_code,
159 stdout_str,
160 stderr_str,
161 working_dir.map(|p| p.to_path_buf()),
162 ));
163 }
164
165 Ok(CommandOutput {
166 stdout: stdout_str,
167 stderr: stderr_str,
168 exit_code,
169 success: true,
170 })
171}
172
173#[cfg(feature = "async")]
174async fn run_with_timeout_stdin(
175 binary: &std::path::Path,
176 args: &[String],
177 env: &std::collections::HashMap<String, String>,
178 working_dir: Option<&std::path::Path>,
179 timeout: Duration,
180 stdin_content: String,
181) -> Result<CommandOutput> {
182 use tokio::io::AsyncWriteExt;
183
184 let mut cmd = Command::new(binary);
185 cmd.args(args);
186 cmd.stdin(std::process::Stdio::piped());
187 cmd.stdout(std::process::Stdio::piped());
188 cmd.stderr(std::process::Stdio::piped());
189 cmd.env_remove("CLAUDECODE");
190 cmd.env_remove("CLAUDE_CODE_ENTRYPOINT");
191
192 if let Some(dir) = working_dir {
193 cmd.current_dir(dir);
194 }
195
196 for (key, value) in env {
197 cmd.env(key, value);
198 }
199
200 let mut child = cmd.spawn().map_err(|e| Error::Io {
201 message: format!("failed to spawn claude: {e}"),
202 source: e,
203 working_dir: working_dir.map(|p| p.to_path_buf()),
204 })?;
205
206 if let Some(mut stdin) = child.stdin.take() {
208 stdin
209 .write_all(stdin_content.as_bytes())
210 .await
211 .map_err(|e| Error::Io {
212 message: format!("failed to write to claude stdin: {e}"),
213 source: e,
214 working_dir: working_dir.map(|p| p.to_path_buf()),
215 })?;
216 }
218
219 let mut stdout_handle = child.stdout.take().expect("stdout was piped");
220 let mut stderr_handle = child.stderr.take().expect("stderr was piped");
221
222 let wait_and_drain = async {
223 let (status, stdout_str, stderr_str) = tokio::join!(
224 child.wait(),
225 drain(&mut stdout_handle),
226 drain(&mut stderr_handle),
227 );
228 (status, stdout_str, stderr_str)
229 };
230
231 match tokio::time::timeout(timeout, wait_and_drain).await {
232 Ok((Ok(status), stdout, stderr)) => {
233 let exit_code = status.code().unwrap_or(-1);
234
235 if !status.success() {
236 return Err(Error::from_command_failure(
237 format!("{} {}", binary.display(), args.join(" ")),
238 exit_code,
239 stdout,
240 stderr,
241 working_dir.map(|p| p.to_path_buf()),
242 ));
243 }
244
245 Ok(CommandOutput {
246 stdout,
247 stderr,
248 exit_code,
249 success: true,
250 })
251 }
252 Ok((Err(e), _stdout, _stderr)) => Err(Error::Io {
253 message: "failed to wait for claude process".to_string(),
254 source: e,
255 working_dir: working_dir.map(|p| p.to_path_buf()),
256 }),
257 Err(_) => {
258 let _ = child.kill().await;
259 let drain_budget = Duration::from_millis(200);
260 let stdout_str = tokio::time::timeout(drain_budget, drain(&mut stdout_handle))
261 .await
262 .unwrap_or_default();
263 let stderr_str = tokio::time::timeout(drain_budget, drain(&mut stderr_handle))
264 .await
265 .unwrap_or_default();
266 if !stdout_str.is_empty() || !stderr_str.is_empty() {
267 warn!(
268 stdout = %stdout_str,
269 stderr = %stderr_str,
270 "partial output from timed-out process",
271 );
272 }
273 Err(Error::Timeout {
274 timeout_seconds: timeout.as_secs(),
275 })
276 }
277 }
278}
279
280#[cfg(feature = "async")]
281async fn run_claude_once(claude: &Claude, args: Vec<String>) -> Result<CommandOutput> {
282 let mut command_args = Vec::new();
283
284 command_args.extend(claude.global_args.clone());
286
287 command_args.extend(args);
289
290 debug!(binary = %claude.binary.display(), args = ?command_args, "executing claude command");
291
292 let output = if let Some(timeout) = claude.timeout {
293 run_with_timeout(
294 &claude.binary,
295 &command_args,
296 &claude.env,
297 claude.working_dir.as_deref(),
298 timeout,
299 )
300 .await?
301 } else {
302 run_internal(
303 &claude.binary,
304 &command_args,
305 &claude.env,
306 claude.working_dir.as_deref(),
307 )
308 .await?
309 };
310
311 Ok(output)
312}
313
314#[cfg(feature = "async")]
316pub async fn run_claude_allow_exit_codes(
317 claude: &Claude,
318 args: Vec<String>,
319 allowed_codes: &[i32],
320) -> Result<CommandOutput> {
321 let output = run_claude(claude, args).await;
322
323 match output {
324 Err(Error::CommandFailed {
325 exit_code,
326 stdout,
327 stderr,
328 ..
329 }) if allowed_codes.contains(&exit_code) => Ok(CommandOutput {
330 stdout,
331 stderr,
332 exit_code,
333 success: false,
334 }),
335 other => other,
336 }
337}
338
339#[cfg(feature = "async")]
340async fn run_internal(
341 binary: &std::path::Path,
342 args: &[String],
343 env: &std::collections::HashMap<String, String>,
344 working_dir: Option<&std::path::Path>,
345) -> Result<CommandOutput> {
346 let mut cmd = Command::new(binary);
347 cmd.args(args);
348
349 cmd.stdin(std::process::Stdio::null());
351
352 cmd.env_remove("CLAUDECODE");
354 cmd.env_remove("CLAUDE_CODE_ENTRYPOINT");
355
356 if let Some(dir) = working_dir {
357 cmd.current_dir(dir);
358 }
359
360 for (key, value) in env {
361 cmd.env(key, value);
362 }
363
364 let output = cmd.output().await.map_err(|e| Error::Io {
365 message: format!("failed to spawn claude: {e}"),
366 source: e,
367 working_dir: working_dir.map(|p| p.to_path_buf()),
368 })?;
369
370 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
371 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
372 let exit_code = output.status.code().unwrap_or(-1);
373
374 if !output.status.success() {
375 return Err(Error::from_command_failure(
376 format!("{} {}", binary.display(), args.join(" ")),
377 exit_code,
378 stdout,
379 stderr,
380 working_dir.map(|p| p.to_path_buf()),
381 ));
382 }
383
384 Ok(CommandOutput {
385 stdout,
386 stderr,
387 exit_code,
388 success: true,
389 })
390}
391
392#[cfg(feature = "async")]
404async fn run_with_timeout(
405 binary: &std::path::Path,
406 args: &[String],
407 env: &std::collections::HashMap<String, String>,
408 working_dir: Option<&std::path::Path>,
409 timeout: Duration,
410) -> Result<CommandOutput> {
411 let mut cmd = Command::new(binary);
412 cmd.args(args);
413 cmd.stdin(std::process::Stdio::null());
414 cmd.stdout(std::process::Stdio::piped());
415 cmd.stderr(std::process::Stdio::piped());
416 cmd.env_remove("CLAUDECODE");
417 cmd.env_remove("CLAUDE_CODE_ENTRYPOINT");
418
419 if let Some(dir) = working_dir {
420 cmd.current_dir(dir);
421 }
422
423 for (key, value) in env {
424 cmd.env(key, value);
425 }
426
427 let mut child = cmd.spawn().map_err(|e| Error::Io {
428 message: format!("failed to spawn claude: {e}"),
429 source: e,
430 working_dir: working_dir.map(|p| p.to_path_buf()),
431 })?;
432
433 let mut stdout = child.stdout.take().expect("stdout was piped");
434 let mut stderr = child.stderr.take().expect("stderr was piped");
435
436 let wait_and_drain = async {
441 let (status, stdout_str, stderr_str) =
442 tokio::join!(child.wait(), drain(&mut stdout), drain(&mut stderr));
443 (status, stdout_str, stderr_str)
444 };
445
446 match tokio::time::timeout(timeout, wait_and_drain).await {
447 Ok((Ok(status), stdout, stderr)) => {
448 let exit_code = status.code().unwrap_or(-1);
449
450 if !status.success() {
451 return Err(Error::from_command_failure(
452 format!("{} {}", binary.display(), args.join(" ")),
453 exit_code,
454 stdout,
455 stderr,
456 working_dir.map(|p| p.to_path_buf()),
457 ));
458 }
459
460 Ok(CommandOutput {
461 stdout,
462 stderr,
463 exit_code,
464 success: true,
465 })
466 }
467 Ok((Err(e), _stdout, _stderr)) => Err(Error::Io {
468 message: "failed to wait for claude process".to_string(),
469 source: e,
470 working_dir: working_dir.map(|p| p.to_path_buf()),
471 }),
472 Err(_) => {
473 let _ = child.kill().await;
479 let drain_budget = Duration::from_millis(200);
480 let stdout_str = tokio::time::timeout(drain_budget, drain(&mut stdout))
481 .await
482 .unwrap_or_default();
483 let stderr_str = tokio::time::timeout(drain_budget, drain(&mut stderr))
484 .await
485 .unwrap_or_default();
486 if !stdout_str.is_empty() || !stderr_str.is_empty() {
487 warn!(
488 stdout = %stdout_str,
489 stderr = %stderr_str,
490 "partial output from timed-out process",
491 );
492 }
493 Err(Error::Timeout {
494 timeout_seconds: timeout.as_secs(),
495 })
496 }
497 }
498}
499
500#[cfg(feature = "async")]
501async fn drain<R: AsyncReadExt + Unpin>(reader: &mut R) -> String {
502 let mut buf = Vec::new();
503 let _ = reader.read_to_end(&mut buf).await;
504 String::from_utf8_lossy(&buf).into_owned()
505}
506
507#[cfg(feature = "sync")]
511pub fn run_claude_sync(claude: &Claude, args: Vec<String>) -> Result<CommandOutput> {
512 run_claude_with_retry_sync(claude, args, None)
513}
514
515#[cfg(feature = "sync")]
517pub fn run_claude_with_retry_sync(
518 claude: &Claude,
519 args: Vec<String>,
520 retry_override: Option<&crate::retry::RetryPolicy>,
521) -> Result<CommandOutput> {
522 let policy = retry_override.or(claude.retry_policy.as_ref());
523
524 match policy {
525 Some(policy) => {
526 crate::retry::with_retry_sync(policy, || run_claude_once_sync(claude, args.clone()))
527 }
528 None => run_claude_once_sync(claude, args),
529 }
530}
531
532#[cfg(feature = "sync")]
537pub fn run_claude_with_stdin_prompt_sync(
538 claude: &Claude,
539 args: Vec<String>,
540 stdin_content: String,
541) -> Result<CommandOutput> {
542 let mut command_args = Vec::new();
543 command_args.extend(claude.global_args.clone());
544 command_args.extend(args);
545
546 debug!(binary = %claude.binary.display(), args = ?command_args, "executing claude command (stdin prompt, sync)");
547
548 if let Some(timeout) = claude.timeout {
549 run_with_timeout_stdin_sync(
550 &claude.binary,
551 &command_args,
552 &claude.env,
553 claude.working_dir.as_deref(),
554 timeout,
555 stdin_content,
556 )
557 } else {
558 run_internal_stdin_sync(
559 &claude.binary,
560 &command_args,
561 &claude.env,
562 claude.working_dir.as_deref(),
563 stdin_content,
564 )
565 }
566}
567
568#[cfg(feature = "sync")]
569fn run_internal_stdin_sync(
570 binary: &std::path::Path,
571 args: &[String],
572 env: &std::collections::HashMap<String, String>,
573 working_dir: Option<&std::path::Path>,
574 stdin_content: String,
575) -> Result<CommandOutput> {
576 use std::io::Write;
577 use std::process::{Command as StdCommand, Stdio};
578
579 let mut cmd = StdCommand::new(binary);
580 cmd.args(args);
581 cmd.stdin(Stdio::piped());
582 cmd.stdout(Stdio::piped());
583 cmd.stderr(Stdio::piped());
584 cmd.env_remove("CLAUDECODE");
585 cmd.env_remove("CLAUDE_CODE_ENTRYPOINT");
586
587 if let Some(dir) = working_dir {
588 cmd.current_dir(dir);
589 }
590
591 for (key, value) in env {
592 cmd.env(key, value);
593 }
594
595 let mut child = cmd.spawn().map_err(|e| Error::Io {
596 message: format!("failed to spawn claude: {e}"),
597 source: e,
598 working_dir: working_dir.map(|p| p.to_path_buf()),
599 })?;
600
601 if let Some(mut stdin) = child.stdin.take() {
603 stdin
604 .write_all(stdin_content.as_bytes())
605 .map_err(|e| Error::Io {
606 message: format!("failed to write to claude stdin: {e}"),
607 source: e,
608 working_dir: working_dir.map(|p| p.to_path_buf()),
609 })?;
610 stdin.flush().map_err(|e| Error::Io {
611 message: format!("failed to flush claude stdin: {e}"),
612 source: e,
613 working_dir: working_dir.map(|p| p.to_path_buf()),
614 })?;
615 }
617
618 let output = child.wait_with_output().map_err(|e| Error::Io {
619 message: "failed to wait for claude process".to_string(),
620 source: e,
621 working_dir: working_dir.map(|p| p.to_path_buf()),
622 })?;
623
624 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
625 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
626 let exit_code = output.status.code().unwrap_or(-1);
627
628 if !output.status.success() {
629 return Err(Error::from_command_failure(
630 format!("{} {}", binary.display(), args.join(" ")),
631 exit_code,
632 stdout,
633 stderr,
634 working_dir.map(|p| p.to_path_buf()),
635 ));
636 }
637
638 Ok(CommandOutput {
639 stdout,
640 stderr,
641 exit_code,
642 success: true,
643 })
644}
645
646#[cfg(feature = "sync")]
647fn run_with_timeout_stdin_sync(
648 binary: &std::path::Path,
649 args: &[String],
650 env: &std::collections::HashMap<String, String>,
651 working_dir: Option<&std::path::Path>,
652 timeout: Duration,
653 stdin_content: String,
654) -> Result<CommandOutput> {
655 use std::io::Write;
656 use std::process::{Command as StdCommand, Stdio};
657 use std::thread;
658 use wait_timeout::ChildExt;
659
660 let mut cmd = StdCommand::new(binary);
661 cmd.args(args);
662 cmd.stdin(Stdio::piped());
663 cmd.stdout(Stdio::piped());
664 cmd.stderr(Stdio::piped());
665 cmd.env_remove("CLAUDECODE");
666 cmd.env_remove("CLAUDE_CODE_ENTRYPOINT");
667
668 if let Some(dir) = working_dir {
669 cmd.current_dir(dir);
670 }
671
672 for (key, value) in env {
673 cmd.env(key, value);
674 }
675
676 let mut child = cmd.spawn().map_err(|e| Error::Io {
677 message: format!("failed to spawn claude: {e}"),
678 source: e,
679 working_dir: working_dir.map(|p| p.to_path_buf()),
680 })?;
681
682 if let Some(mut stdin) = child.stdin.take() {
684 stdin
685 .write_all(stdin_content.as_bytes())
686 .map_err(|e| Error::Io {
687 message: format!("failed to write to claude stdin: {e}"),
688 source: e,
689 working_dir: working_dir.map(|p| p.to_path_buf()),
690 })?;
691 stdin.flush().map_err(|e| Error::Io {
692 message: format!("failed to flush claude stdin: {e}"),
693 source: e,
694 working_dir: working_dir.map(|p| p.to_path_buf()),
695 })?;
696 }
698
699 let stdout = child.stdout.take().expect("stdout was piped");
700 let stderr = child.stderr.take().expect("stderr was piped");
701
702 let stdout_thread = thread::spawn(move || drain_sync(stdout));
703 let stderr_thread = thread::spawn(move || drain_sync(stderr));
704
705 match child.wait_timeout(timeout).map_err(|e| Error::Io {
706 message: "failed to wait for claude process".to_string(),
707 source: e,
708 working_dir: working_dir.map(|p| p.to_path_buf()),
709 })? {
710 Some(status) => {
711 let stdout = stdout_thread.join().unwrap_or_default();
712 let stderr = stderr_thread.join().unwrap_or_default();
713 let exit_code = status.code().unwrap_or(-1);
714
715 if !status.success() {
716 return Err(Error::from_command_failure(
717 format!("{} {}", binary.display(), args.join(" ")),
718 exit_code,
719 stdout,
720 stderr,
721 working_dir.map(|p| p.to_path_buf()),
722 ));
723 }
724
725 Ok(CommandOutput {
726 stdout,
727 stderr,
728 exit_code,
729 success: true,
730 })
731 }
732 None => {
733 let _ = child.kill();
734 let _ = child.wait();
735 let (stdout_str, stderr_str) =
736 join_with_deadline(stdout_thread, stderr_thread, Duration::from_millis(200));
737 if !stdout_str.is_empty() || !stderr_str.is_empty() {
738 warn!(
739 stdout = %stdout_str,
740 stderr = %stderr_str,
741 "partial output from timed-out process",
742 );
743 }
744 Err(Error::Timeout {
745 timeout_seconds: timeout.as_secs(),
746 })
747 }
748 }
749}
750
751#[cfg(feature = "sync")]
752fn run_claude_once_sync(claude: &Claude, args: Vec<String>) -> Result<CommandOutput> {
753 let mut command_args = Vec::new();
754 command_args.extend(claude.global_args.clone());
755 command_args.extend(args);
756
757 debug!(binary = %claude.binary.display(), args = ?command_args, "executing claude command (sync)");
758
759 if let Some(timeout) = claude.timeout {
760 run_with_timeout_sync(
761 &claude.binary,
762 &command_args,
763 &claude.env,
764 claude.working_dir.as_deref(),
765 timeout,
766 )
767 } else {
768 run_internal_sync(
769 &claude.binary,
770 &command_args,
771 &claude.env,
772 claude.working_dir.as_deref(),
773 )
774 }
775}
776
777#[cfg(feature = "sync")]
779pub fn run_claude_allow_exit_codes_sync(
780 claude: &Claude,
781 args: Vec<String>,
782 allowed_codes: &[i32],
783) -> Result<CommandOutput> {
784 match run_claude_sync(claude, args) {
785 Err(Error::CommandFailed {
786 exit_code,
787 stdout,
788 stderr,
789 ..
790 }) if allowed_codes.contains(&exit_code) => Ok(CommandOutput {
791 stdout,
792 stderr,
793 exit_code,
794 success: false,
795 }),
796 other => other,
797 }
798}
799
800#[cfg(feature = "sync")]
801fn run_internal_sync(
802 binary: &std::path::Path,
803 args: &[String],
804 env: &std::collections::HashMap<String, String>,
805 working_dir: Option<&std::path::Path>,
806) -> Result<CommandOutput> {
807 use std::process::{Command as StdCommand, Stdio};
808
809 let mut cmd = StdCommand::new(binary);
810 cmd.args(args);
811 cmd.stdin(Stdio::null());
812 cmd.env_remove("CLAUDECODE");
813 cmd.env_remove("CLAUDE_CODE_ENTRYPOINT");
814
815 if let Some(dir) = working_dir {
816 cmd.current_dir(dir);
817 }
818
819 for (key, value) in env {
820 cmd.env(key, value);
821 }
822
823 let output = cmd.output().map_err(|e| Error::Io {
824 message: format!("failed to spawn claude: {e}"),
825 source: e,
826 working_dir: working_dir.map(|p| p.to_path_buf()),
827 })?;
828
829 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
830 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
831 let exit_code = output.status.code().unwrap_or(-1);
832
833 if !output.status.success() {
834 return Err(Error::from_command_failure(
835 format!("{} {}", binary.display(), args.join(" ")),
836 exit_code,
837 stdout,
838 stderr,
839 working_dir.map(|p| p.to_path_buf()),
840 ));
841 }
842
843 Ok(CommandOutput {
844 stdout,
845 stderr,
846 exit_code,
847 success: true,
848 })
849}
850
851#[cfg(feature = "sync")]
858fn run_with_timeout_sync(
859 binary: &std::path::Path,
860 args: &[String],
861 env: &std::collections::HashMap<String, String>,
862 working_dir: Option<&std::path::Path>,
863 timeout: Duration,
864) -> Result<CommandOutput> {
865 use std::process::{Command as StdCommand, Stdio};
866 use std::thread;
867 use wait_timeout::ChildExt;
868
869 let mut cmd = StdCommand::new(binary);
870 cmd.args(args);
871 cmd.stdin(Stdio::null());
872 cmd.stdout(Stdio::piped());
873 cmd.stderr(Stdio::piped());
874 cmd.env_remove("CLAUDECODE");
875 cmd.env_remove("CLAUDE_CODE_ENTRYPOINT");
876
877 if let Some(dir) = working_dir {
878 cmd.current_dir(dir);
879 }
880
881 for (key, value) in env {
882 cmd.env(key, value);
883 }
884
885 let mut child = cmd.spawn().map_err(|e| Error::Io {
886 message: format!("failed to spawn claude: {e}"),
887 source: e,
888 working_dir: working_dir.map(|p| p.to_path_buf()),
889 })?;
890
891 let stdout = child.stdout.take().expect("stdout was piped");
896 let stderr = child.stderr.take().expect("stderr was piped");
897
898 let stdout_thread = thread::spawn(move || drain_sync(stdout));
899 let stderr_thread = thread::spawn(move || drain_sync(stderr));
900
901 match child.wait_timeout(timeout).map_err(|e| Error::Io {
902 message: "failed to wait for claude process".to_string(),
903 source: e,
904 working_dir: working_dir.map(|p| p.to_path_buf()),
905 })? {
906 Some(status) => {
907 let stdout = stdout_thread.join().unwrap_or_default();
908 let stderr = stderr_thread.join().unwrap_or_default();
909 let exit_code = status.code().unwrap_or(-1);
910
911 if !status.success() {
912 return Err(Error::from_command_failure(
913 format!("{} {}", binary.display(), args.join(" ")),
914 exit_code,
915 stdout,
916 stderr,
917 working_dir.map(|p| p.to_path_buf()),
918 ));
919 }
920
921 Ok(CommandOutput {
922 stdout,
923 stderr,
924 exit_code,
925 success: true,
926 })
927 }
928 None => {
929 let _ = child.kill();
934 let _ = child.wait();
935
936 let (stdout_str, stderr_str) =
937 join_with_deadline(stdout_thread, stderr_thread, Duration::from_millis(200));
938
939 if !stdout_str.is_empty() || !stderr_str.is_empty() {
940 warn!(
941 stdout = %stdout_str,
942 stderr = %stderr_str,
943 "partial output from timed-out process",
944 );
945 }
946
947 Err(Error::Timeout {
948 timeout_seconds: timeout.as_secs(),
949 })
950 }
951 }
952}
953
954#[cfg(feature = "sync")]
955fn drain_sync<R: std::io::Read>(mut reader: R) -> String {
956 let mut buf = Vec::new();
957 let _ = reader.read_to_end(&mut buf);
958 String::from_utf8_lossy(&buf).into_owned()
959}
960
961#[cfg(feature = "sync")]
967fn join_with_deadline(
968 stdout_thread: std::thread::JoinHandle<String>,
969 stderr_thread: std::thread::JoinHandle<String>,
970 budget: Duration,
971) -> (String, String) {
972 use std::sync::mpsc;
973 use std::thread;
974
975 let (tx, rx) = mpsc::channel::<(&'static str, String)>();
976
977 let tx_out = tx.clone();
978 let tx_err = tx;
979
980 thread::spawn(move || {
981 let s = stdout_thread.join().unwrap_or_default();
982 let _ = tx_out.send(("stdout", s));
983 });
984 thread::spawn(move || {
985 let s = stderr_thread.join().unwrap_or_default();
986 let _ = tx_err.send(("stderr", s));
987 });
988
989 let mut stdout = String::new();
990 let mut stderr = String::new();
991 let deadline = std::time::Instant::now() + budget;
992
993 for _ in 0..2 {
994 let now = std::time::Instant::now();
995 if now >= deadline {
996 break;
997 }
998 match rx.recv_timeout(deadline - now) {
999 Ok(("stdout", s)) => stdout = s,
1000 Ok(("stderr", s)) => stderr = s,
1001 Ok(_) => unreachable!(),
1002 Err(_) => break,
1003 }
1004 }
1005
1006 (stdout, stderr)
1007}