1use std::process::ExitStatus;
2use std::sync::Arc;
3use std::sync::Mutex;
4use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
5
6use anyhow::{Result, bail};
7use oxdock_fs::GuardedPath;
8use oxdock_parser::{Arg, AssertTarget, Step, StepKind, Value, guard_option_allows};
9use oxdock_process::{BackgroundHandle, CommandStdin, ProcessManager};
10
11fn exit_status_from_code(code: i32) -> ExitStatus {
13 #[cfg(unix)]
14 {
15 use std::os::unix::process::ExitStatusExt;
16 ExitStatus::from_raw(code << 8)
17 }
18 #[cfg(windows)]
19 {
20 use std::os::windows::process::ExitStatusExt;
21 ExitStatus::from_raw(code as u32)
22 }
23}
24
25use super::capture::SpillBuffer;
26use super::handlers;
27use super::io::{ExactCapture, SlidingWindow, StreamHandle};
28use super::state::{ExecState, TaskEntry, TaskPhase};
29
30pub(super) struct ThreadJoinHandle {
33 join: Option<std::thread::JoinHandle<Result<()>>>,
34 cancel_token: Arc<AtomicBool>,
35 active_process: Arc<Mutex<Option<Box<dyn BackgroundHandle>>>>,
36 thread_error: Option<anyhow::Error>,
38}
39
40impl ThreadJoinHandle {
41 pub(super) fn new(
42 join: std::thread::JoinHandle<Result<()>>,
43 cancel_token: Arc<AtomicBool>,
44 active_process: Arc<Mutex<Option<Box<dyn BackgroundHandle>>>>,
45 ) -> Self {
46 Self {
47 join: Some(join),
48 cancel_token,
49 active_process,
50 thread_error: None,
51 }
52 }
53
54 fn reap(&mut self) {
56 if self.join.is_none() {
57 return;
58 }
59 let handle = self.join.take().unwrap();
60 match handle.join() {
61 Ok(Ok(())) => {}
62 Ok(Err(e)) => {
63 self.thread_error = Some(e);
64 }
65 Err(panic) => {
66 let msg = if let Some(s) = panic.downcast_ref::<&str>() {
67 s.to_string()
68 } else if let Some(s) = panic.downcast_ref::<String>() {
69 s.clone()
70 } else {
71 "thread panicked".to_string()
72 };
73 self.thread_error = Some(anyhow::anyhow!("{msg}"));
74 }
75 }
76 }
77}
78
79impl BackgroundHandle for ThreadJoinHandle {
80 fn try_wait(&mut self) -> Result<Option<ExitStatus>> {
81 if let Some(join) = &self.join {
82 if join.is_finished() {
83 self.reap();
84 } else {
85 return Ok(None);
86 }
87 }
88 if let Some(ref err) = self.thread_error {
89 Err(anyhow::anyhow!("{err}"))
90 } else {
91 Ok(Some(exit_status_from_code(0)))
92 }
93 }
94
95 fn kill(&mut self) -> Result<()> {
96 self.cancel_token.store(true, Ordering::SeqCst);
98 if let Ok(mut guard) = self.active_process.lock()
100 && let Some(ref mut proc) = *guard
101 {
102 let _ = proc.kill();
103 }
104 self.reap();
106 Ok(())
107 }
108
109 fn wait(&mut self) -> Result<ExitStatus> {
110 self.reap();
111 if let Some(ref err) = self.thread_error {
112 Err(anyhow::anyhow!("{err}"))
113 } else {
114 Ok(exit_status_from_code(0))
115 }
116 }
117}
118
119impl Drop for ThreadJoinHandle {
120 fn drop(&mut self) {
121 let _ = self.kill();
122 }
123}
124
125static ASSERT_GENERATION: AtomicUsize = AtomicUsize::new(0);
129
130#[derive(Debug)]
137pub(super) enum Flow {
138 Done,
139 Break { idx: usize },
140 Continue { idx: usize },
141 Return { idx: usize, value: Value },
142}
143
144pub(super) fn allocate_assert_generation() -> usize {
145 ASSERT_GENERATION.fetch_add(1, Ordering::Relaxed)
146}
147
148#[derive(Clone, Copy, PartialEq, Eq)]
150pub(super) enum AssertStream {
151 Stdout,
152 Stderr,
153}
154
155fn extract_stream_needle(kind: &StepKind) -> Option<(AssertStream, &Arg)> {
159 let step = match kind {
160 StepKind::WithIo { cmd, .. } => cmd.as_ref(),
161 other => other,
162 };
163 match step {
164 StepKind::AssertContains { haystack, needle } => match haystack {
165 AssertTarget::Stdout => Some((AssertStream::Stdout, needle)),
166 AssertTarget::Stderr => Some((AssertStream::Stderr, needle)),
167 _ => None,
168 },
169 _ => None,
170 }
171}
172
173fn needs_exact_stdout(kind: &StepKind) -> bool {
176 let step = match kind {
177 StepKind::WithIo { cmd, .. } => cmd.as_ref(),
178 other => other,
179 };
180 matches!(
181 step,
182 StepKind::AssertEq {
183 actual: AssertTarget::Stdout,
184 ..
185 }
186 )
187}
188
189pub(super) enum ResolvedAssertTarget {
192 Value(Value),
193 Stdout,
194 Stderr,
195 Pipe(Vec<u8>),
196}
197
198pub(super) fn resolve_assert_target<P: ProcessManager>(
202 target: &AssertTarget,
203 cx: &mut StepCtx<'_, P>,
204) -> Result<ResolvedAssertTarget> {
205 match target {
206 AssertTarget::Value(arg) => Ok(ResolvedAssertTarget::Value(
207 super::args::evaluate_assert_operand(arg, cx)?,
208 )),
209 AssertTarget::Stdout => Ok(ResolvedAssertTarget::Stdout),
210 AssertTarget::Stderr => Ok(ResolvedAssertTarget::Stderr),
211 AssertTarget::Pipe(name) => {
212 let bytes = cx.state.io.peek_pipe_content(name).map_err(|e| {
213 anyhow::anyhow!("step pipe assertion cannot read pipe {name:?}: {e}")
214 })?;
215 Ok(ResolvedAssertTarget::Pipe(bytes))
216 }
217 }
218}
219
220pub(super) fn pre_register_assertions<P: ProcessManager>(
227 state: &mut ExecState<P>,
228 steps: &[Step],
229 generation: usize,
230) -> Result<()> {
231 let mut windows = match state.assert_windows.lock() {
232 Ok(guard) => guard,
233 Err(_) => bail!("assert_windows poisoned"),
234 };
235 let mut stderr_windows = match state.assert_windows_stderr.lock() {
236 Ok(guard) => guard,
237 Err(_) => bail!("assert_windows_stderr poisoned"),
238 };
239 let mut exact = match state.exact_stdout.lock() {
240 Ok(guard) => guard,
241 Err(_) => bail!("exact_stdout poisoned"),
242 };
243 for (idx, step) in steps.iter().enumerate() {
244 if let Some((stream, arg)) = extract_stream_needle(&step.kind) {
245 let resolved = super::args::resolve_arg_state(arg, state)?;
246 let map = match stream {
247 AssertStream::Stdout => &mut windows,
248 AssertStream::Stderr => &mut stderr_windows,
249 };
250 map.insert((generation, idx), SlidingWindow::new(resolved.into_bytes()));
251 }
252 if needs_exact_stdout(&step.kind) {
253 exact.entry(generation).or_insert_with(ExactCapture::new);
254 }
255 }
256 Ok(())
257}
258
259#[allow(clippy::collapsible_if)]
265pub(super) fn sync_iteration_assert_needles<P: ProcessManager>(
266 state: &ExecState<P>,
267 steps: &[Step],
268 generation: usize,
269) -> Result<()> {
270 let mut windows = match state.assert_windows.lock() {
271 Ok(guard) => guard,
272 Err(_) => bail!("assert_windows poisoned"),
273 };
274 let mut stderr_windows = match state.assert_windows_stderr.lock() {
275 Ok(guard) => guard,
276 Err(_) => bail!("assert_windows_stderr poisoned"),
277 };
278 for (idx, step) in steps.iter().enumerate() {
279 if let Some((stream, arg)) = extract_stream_needle(&step.kind) {
280 let map = match stream {
281 AssertStream::Stdout => &mut windows,
282 AssertStream::Stderr => &mut stderr_windows,
283 };
284 if let Some(w) = map.get_mut(&(generation, idx)) {
285 let resolved = super::args::resolve_arg_state(arg, state)?;
286 w.update_needle(resolved.into_bytes());
287 }
288 }
289 }
290 Ok(())
291}
292
293pub struct StepCtx<'a, P: ProcessManager> {
306 pub(super) state: &'a mut ExecState<P>,
307 pub(super) process: &'a mut P,
308 pub(super) stdin: CommandStdin,
309 pub(super) expose_stdin: bool,
310 pub(super) out: Option<StreamHandle>,
311 pub(super) err: Option<StreamHandle>,
312}
313
314impl<'a, P: ProcessManager> StepCtx<'a, P> {
315 pub fn get_var(&self, key: &str) -> Option<Value> {
317 self.state.get_var(key)
318 }
319
320 pub fn get_env(&self, key: &str) -> Option<String> {
322 self.state.envs.get(key).cloned()
323 }
324
325 pub fn cwd(&self) -> &GuardedPath {
327 &self.state.cwd
328 }
329}
330
331#[allow(clippy::too_many_arguments)]
332pub(super) fn execute_steps<P: ProcessManager>(
333 state: &mut ExecState<P>,
334 process: &mut P,
335 steps: &[Step],
336 stdin: CommandStdin,
337 expose_stdin: bool,
338 out: Option<StreamHandle>,
339 err: Option<StreamHandle>,
340 wait_at_end: bool,
341) -> Result<Flow> {
342 let generation = allocate_assert_generation();
343 let flow = execute_steps_inner(
344 state,
345 process,
346 generation,
347 steps,
348 stdin,
349 expose_stdin,
350 out,
351 err,
352 wait_at_end,
353 )?;
354 let mut windows = match state.assert_windows.lock() {
356 Ok(guard) => guard,
357 Err(_) => bail!("assert_windows poisoned"),
358 };
359 windows.retain(|(g, _), _| *g != generation);
360 let mut stderr_windows = match state.assert_windows_stderr.lock() {
361 Ok(guard) => guard,
362 Err(_) => bail!("assert_windows_stderr poisoned"),
363 };
364 stderr_windows.retain(|(g, _), _| *g != generation);
365 let mut exact = match state.exact_stdout.lock() {
366 Ok(guard) => guard,
367 Err(_) => bail!("exact_stdout poisoned"),
368 };
369 exact.retain(|g, _| *g != generation);
370 Ok(flow)
371}
372
373#[allow(clippy::too_many_arguments)]
376pub(super) fn execute_single_step_with_generation<P: ProcessManager>(
377 state: &mut ExecState<P>,
378 process: &mut P,
379 cmd: &StepKind,
380 generation: usize,
381 idx: usize,
382 stdin: CommandStdin,
383 expose_stdin: bool,
384 out: Option<StreamHandle>,
385 err: Option<StreamHandle>,
386) -> Result<Flow> {
387 let mut cx = StepCtx {
388 state,
389 process,
390 stdin,
391 expose_stdin,
392 out,
393 err,
394 };
395 match cmd {
399 StepKind::FuncDef { .. }
400 | StepKind::Call { .. }
401 | StepKind::Return { .. }
402 | StepKind::While { .. }
403 | StepKind::Break
404 | StepKind::Continue
405 | StepKind::For { .. }
406 | StepKind::If { .. }
407 | StepKind::Timeout { .. }
408 | StepKind::WithIo { .. }
409 | StepKind::AssignCapture { .. } => {
410 return dispatch_flow_step(cmd, &mut cx, generation, idx);
411 }
412 _ => {}
413 }
414 match cmd {
415 StepKind::Run(arg) => {
416 let cmd = super::args::resolve_arg(arg, &mut cx)?;
417 let cmd = super::args::expand_dsl_vars(&cmd, cx.state);
418 handlers::run(&mut cx, idx, &cmd)
419 }
420 StepKind::RunExec { argv } => {
421 let resolved = handlers::resolve_run_exec_argv(argv, &mut cx)?;
422 handlers::run_argv(&mut cx, idx, &resolved)
423 }
424 StepKind::Echo(arg) => {
425 let msg = super::args::resolve_arg(arg, &mut cx)?;
426 handlers::echo(&mut cx, &msg)
427 }
428 StepKind::AsyncBlock { .. } => handlers::dispatch_async_block(cmd, &mut cx),
429 StepKind::Workdir(arg) => {
430 let path = super::args::resolve_arg(arg, &mut cx)?;
431 handlers::workdir(&mut cx, idx, &path)
432 }
433 StepKind::Workspace(target) => handlers::workspace(&mut cx, target),
434 StepKind::Env { key, value } => {
435 let resolved = super::args::resolve_arg(value, &mut cx)?;
436 handlers::env(&mut cx, key, &resolved)
437 }
438 StepKind::InheritEnv { keys } => {
439 handlers::inherit_env(&mut cx, keys)?;
440 sync_iteration_assert_needles(
441 cx.state,
442 &[Step {
443 guard: None,
444 kind: cmd.clone(),
445 scope_enter: 0,
446 scope_exit: 0,
447 }],
448 generation,
449 )?;
450 Ok(())
451 }
452 StepKind::Copy {
453 from_current_workspace,
454 from,
455 to,
456 } => {
457 let from_resolved = super::args::resolve_arg(from, &mut cx)?;
458 let to_resolved = super::args::resolve_arg(to, &mut cx)?;
459 handlers::copy(
460 &mut cx,
461 idx,
462 *from_current_workspace,
463 &from_resolved,
464 &to_resolved,
465 )
466 }
467 StepKind::CopyGit {
468 rev,
469 from,
470 to,
471 include_dirty,
472 } => {
473 let rev_resolved = super::args::resolve_arg(rev, &mut cx)?;
474 let from_resolved = super::args::resolve_arg(from, &mut cx)?;
475 let to_resolved = super::args::resolve_arg(to, &mut cx)?;
476 handlers::copy_git(
477 &mut cx,
478 idx,
479 &rev_resolved,
480 &from_resolved,
481 &to_resolved,
482 *include_dirty,
483 )
484 }
485 StepKind::HashSha256 { path } => {
486 let path_resolved = super::args::resolve_arg(path, &mut cx)?;
487 handlers::hash_sha256(&mut cx, idx, &path_resolved)
488 }
489 StepKind::Symlink { from, to } => {
490 let from_resolved = super::args::resolve_arg(from, &mut cx)?;
491 let to_resolved = super::args::resolve_arg(to, &mut cx)?;
492 handlers::symlink(&mut cx, idx, &from_resolved, &to_resolved)
493 }
494 StepKind::Mkdir(arg) => {
495 let path = super::args::resolve_arg(arg, &mut cx)?;
496 handlers::mkdir(&mut cx, idx, &path)
497 }
498 StepKind::Ls(arg) => {
499 let resolved = super::args::resolve_arg_opt(arg, &mut cx)?;
500 handlers::ls(&mut cx, idx, &resolved)
501 }
502 StepKind::Cwd => handlers::cwd(&mut cx, idx),
503 StepKind::Read(arg) => {
504 let resolved = super::args::resolve_arg_opt(arg, &mut cx)?;
505 handlers::read(&mut cx, idx, &resolved)
506 }
507 StepKind::ReadLine { var } => handlers::read_line(&mut cx, idx, var),
508 StepKind::Write { path, contents } => {
509 let path_resolved = super::args::resolve_arg(path, &mut cx)?;
510 let contents_resolved = super::args::resolve_arg_opt(contents, &mut cx)?;
511 handlers::write(&mut cx, idx, &path_resolved, contents_resolved.as_deref())
512 }
513 StepKind::Append { path, contents } => {
514 let path_resolved = super::args::resolve_arg(path, &mut cx)?;
515 let contents_resolved = super::args::resolve_arg_opt(contents, &mut cx)?;
516 handlers::append(&mut cx, idx, &path_resolved, contents_resolved.as_deref())
517 }
518 StepKind::Expand { path, overrides } => {
519 let path_resolved = super::args::resolve_arg_opt(path, &mut cx)?;
520 let overrides_resolved = super::args::resolve_overrides(overrides, &mut cx)?;
521 handlers::replace(&mut cx, idx, &path_resolved, &overrides_resolved)
522 }
523 StepKind::AssertEq {
524 hash,
525 actual,
526 expected,
527 } => {
528 let target = resolve_assert_target(actual, &mut cx)?;
529 let expected_resolved = match expected {
530 Some(e) => Some(super::args::evaluate_assert_operand(e, &mut cx)?),
531 None => None,
532 };
533 handlers::assert_eq(
534 &mut cx,
535 idx,
536 generation,
537 idx,
538 hash,
539 &target,
540 expected_resolved.as_ref(),
541 )
542 }
543 StepKind::AssertContains { haystack, needle } => {
544 let target = resolve_assert_target(haystack, &mut cx)?;
545 handlers::assert_contains(&mut cx, idx, generation, idx, &target, needle)
546 }
547 StepKind::WithIoBlock { .. } => {
548 bail!("WITH_IO block should have been expanded during parsing")
549 }
550 StepKind::Exit(code) => {
551 let code = super::args::resolve_arg_as_int(code, &mut cx)?;
552 handlers::exit(&mut cx, code)
553 }
554 StepKind::Assign {
555 var,
556 decl_type,
557 expr,
558 } => handlers::assign(&mut cx, var, decl_type.clone(), expr),
559 StepKind::Set { var, expr } => handlers::set_var_value(&mut cx, var, expr),
560 StepKind::AssignAsync {
561 var,
562 decl_type,
563 body,
564 } => handlers::dispatch_assign_async(var, decl_type.clone(), body, &mut cx),
565 StepKind::Await { var } => handlers::dispatch_await(var, &mut cx),
566 StepKind::AwaitCapture {
567 out_var,
568 out_type,
569 task_var,
570 } => handlers::dispatch_await_capture(out_var, out_type.clone(), task_var, &mut cx),
571 StepKind::Cancel { var } => handlers::dispatch_cancel(var, &mut cx),
572 StepKind::Sleep { duration } => {
573 let duration = super::args::resolve_arg_as_duration(duration, &mut cx)?;
574 handlers::sleep(&mut cx, idx, &duration)
575 }
576 StepKind::FuncDef { .. }
577 | StepKind::Call { .. }
578 | StepKind::Return { .. }
579 | StepKind::While { .. }
580 | StepKind::Break
581 | StepKind::Continue
582 | StepKind::For { .. }
583 | StepKind::If { .. }
584 | StepKind::Timeout { .. }
585 | StepKind::WithIo { .. }
586 | StepKind::AssignCapture { .. } => {
587 unreachable!("compound steps dispatch before this match")
588 }
589 }?;
590 Ok(Flow::Done)
591}
592
593#[allow(clippy::too_many_arguments)]
594fn execute_steps_inner<P: ProcessManager>(
595 state: &mut ExecState<P>,
596 process: &mut P,
597 generation: usize,
598 steps: &[Step],
599 stdin: CommandStdin,
600 expose_stdin: bool,
601 out: Option<StreamHandle>,
602 err: Option<StreamHandle>,
603 wait_at_end: bool,
604) -> Result<Flow> {
605 pre_register_assertions(state, steps, generation)?;
607
608 for (idx, step) in steps.iter().enumerate() {
609 if state.cancel_token.load(Ordering::SeqCst) {
611 bail!("ASYNC task cancelled");
612 }
613 if step.scope_enter > 0 {
614 for _ in 0..step.scope_enter {
615 state.push_scope();
616 }
617 }
618
619 let should_run = guard_option_allows(step.guard.as_ref(), &state.envs);
620 let flow_result: Result<Flow> = if !should_run {
621 Ok(Flow::Done)
622 } else {
623 let mut cx = StepCtx {
624 state,
625 process,
626 stdin: stdin.clone(),
627 expose_stdin,
628 out: out.clone(),
629 err: err.clone(),
630 };
631 let flow_result: Result<Flow> = match &step.kind {
634 StepKind::FuncDef { .. }
635 | StepKind::Call { .. }
636 | StepKind::Return { .. }
637 | StepKind::While { .. }
638 | StepKind::Break
639 | StepKind::Continue
640 | StepKind::For { .. }
641 | StepKind::If { .. }
642 | StepKind::Timeout { .. }
643 | StepKind::WithIo { .. }
644 | StepKind::AssignCapture { .. } => {
645 dispatch_flow_step(&step.kind, &mut cx, generation, idx)
646 }
647 _ => {
648 match &step.kind {
649 StepKind::InheritEnv { keys } => {
650 handlers::inherit_env(&mut cx, keys)?;
651 sync_iteration_assert_needles(cx.state, steps, generation)?;
652 Ok(())
653 }
654 StepKind::Workdir(arg) => {
655 let path = super::args::resolve_arg(arg, &mut cx)?;
656 handlers::workdir(&mut cx, idx, &path)
657 }
658 StepKind::Workspace(target) => handlers::workspace(&mut cx, target),
659 StepKind::Env { key, value } => {
660 let resolved = super::args::resolve_arg(value, &mut cx)?;
661 handlers::env(&mut cx, key, &resolved)?;
662 sync_iteration_assert_needles(cx.state, steps, generation)?;
663 Ok(())
664 }
665 StepKind::Run(arg) => {
666 let cmd = super::args::resolve_arg(arg, &mut cx)?;
667 let cmd = super::args::expand_dsl_vars(&cmd, cx.state);
668 handlers::run(&mut cx, idx, &cmd)
669 }
670 StepKind::RunExec { argv } => {
671 let resolved = handlers::resolve_run_exec_argv(argv, &mut cx)?;
672 handlers::run_argv(&mut cx, idx, &resolved)
673 }
674 StepKind::Echo(arg) => {
675 let msg = super::args::resolve_arg(arg, &mut cx)?;
676 handlers::echo(&mut cx, &msg)
677 }
678 StepKind::AsyncBlock { .. } => {
679 handlers::dispatch_async_block(&step.kind, &mut cx)
680 }
681 StepKind::Copy {
682 from_current_workspace,
683 from,
684 to,
685 } => {
686 let from_resolved = super::args::resolve_arg(from, &mut cx)?;
687 let to_resolved = super::args::resolve_arg(to, &mut cx)?;
688 handlers::copy(
689 &mut cx,
690 idx,
691 *from_current_workspace,
692 &from_resolved,
693 &to_resolved,
694 )
695 }
696 StepKind::CopyGit {
697 rev,
698 from,
699 to,
700 include_dirty,
701 } => {
702 let rev_resolved = super::args::resolve_arg(rev, &mut cx)?;
703 let from_resolved = super::args::resolve_arg(from, &mut cx)?;
704 let to_resolved = super::args::resolve_arg(to, &mut cx)?;
705 handlers::copy_git(
706 &mut cx,
707 idx,
708 &rev_resolved,
709 &from_resolved,
710 &to_resolved,
711 *include_dirty,
712 )
713 }
714 StepKind::HashSha256 { path } => {
715 let path_resolved = super::args::resolve_arg(path, &mut cx)?;
716 handlers::hash_sha256(&mut cx, idx, &path_resolved)
717 }
718 StepKind::Symlink { from, to } => {
719 let from_resolved = super::args::resolve_arg(from, &mut cx)?;
720 let to_resolved = super::args::resolve_arg(to, &mut cx)?;
721 handlers::symlink(&mut cx, idx, &from_resolved, &to_resolved)
722 }
723 StepKind::Mkdir(arg) => {
724 let path = super::args::resolve_arg(arg, &mut cx)?;
725 handlers::mkdir(&mut cx, idx, &path)
726 }
727 StepKind::Ls(arg) => {
728 let resolved = super::args::resolve_arg_opt(arg, &mut cx)?;
729 handlers::ls(&mut cx, idx, &resolved)
730 }
731 StepKind::Cwd => handlers::cwd(&mut cx, idx),
732 StepKind::Read(arg) => {
733 let resolved = super::args::resolve_arg_opt(arg, &mut cx)?;
734 handlers::read(&mut cx, idx, &resolved)
735 }
736 StepKind::ReadLine { var } => handlers::read_line(&mut cx, idx, var),
737 StepKind::Write { path, contents } => {
738 let path_resolved = super::args::resolve_arg(path, &mut cx)?;
739 let contents_resolved =
740 super::args::resolve_arg_opt(contents, &mut cx)?;
741 handlers::write(
742 &mut cx,
743 idx,
744 &path_resolved,
745 contents_resolved.as_deref(),
746 )
747 }
748 StepKind::Append { path, contents } => {
749 let path_resolved = super::args::resolve_arg(path, &mut cx)?;
750 let contents_resolved =
751 super::args::resolve_arg_opt(contents, &mut cx)?;
752 handlers::append(
753 &mut cx,
754 idx,
755 &path_resolved,
756 contents_resolved.as_deref(),
757 )
758 }
759 StepKind::Expand { path, overrides } => {
760 let path_resolved = super::args::resolve_arg_opt(path, &mut cx)?;
761 let overrides_resolved =
762 super::args::resolve_overrides(overrides, &mut cx)?;
763 handlers::replace(&mut cx, idx, &path_resolved, &overrides_resolved)
764 }
765 StepKind::AssertEq {
766 hash,
767 actual,
768 expected,
769 } => {
770 let target = resolve_assert_target(actual, &mut cx)?;
771 let expected_resolved = match expected {
772 Some(e) => Some(super::args::evaluate_assert_operand(e, &mut cx)?),
773 None => None,
774 };
775 handlers::assert_eq(
776 &mut cx,
777 idx,
778 generation,
779 idx,
780 hash,
781 &target,
782 expected_resolved.as_ref(),
783 )
784 }
785 StepKind::AssertContains { haystack, needle } => {
786 let target = resolve_assert_target(haystack, &mut cx)?;
787 handlers::assert_contains(
788 &mut cx, idx, generation, idx, &target, needle,
789 )
790 }
791 StepKind::WithIoBlock { .. } => {
792 bail!("WITH_IO block should have been expanded during parsing")
793 }
794 StepKind::Exit(code) => {
795 let code = super::args::resolve_arg_as_int(code, &mut cx)?;
796 handlers::exit(&mut cx, code)
797 }
798 StepKind::Assign {
799 var,
800 decl_type,
801 expr,
802 } => handlers::assign(&mut cx, var, decl_type.clone(), expr),
803 StepKind::Set { var, expr } => handlers::set_var_value(&mut cx, var, expr),
804 StepKind::AssignAsync {
805 var,
806 decl_type,
807 body,
808 } => handlers::dispatch_assign_async(var, decl_type.clone(), body, &mut cx),
809 StepKind::Await { var } => handlers::dispatch_await(var, &mut cx),
810 StepKind::AwaitCapture {
811 out_var,
812 out_type,
813 task_var,
814 } => handlers::dispatch_await_capture(
815 out_var,
816 out_type.clone(),
817 task_var,
818 &mut cx,
819 ),
820 StepKind::Cancel { var } => handlers::dispatch_cancel(var, &mut cx),
821 StepKind::Sleep { duration } => {
822 let duration = super::args::resolve_arg_as_duration(duration, &mut cx)?;
823 handlers::sleep(&mut cx, idx, &duration)
824 }
825 StepKind::FuncDef { .. }
826 | StepKind::Call { .. }
827 | StepKind::Return { .. }
828 | StepKind::While { .. }
829 | StepKind::Break
830 | StepKind::Continue
831 | StepKind::For { .. }
832 | StepKind::If { .. }
833 | StepKind::Timeout { .. }
834 | StepKind::WithIo { .. }
835 | StepKind::AssignCapture { .. } => {
836 unreachable!("compound steps dispatch in the outer match")
837 }
838 }?;
839 Ok(Flow::Done)
840 }
841 };
842 flow_result
843 };
844
845 let restore_result = restore_scopes(state, step.scope_exit);
846 let expiry_drained = if let Some(expiry) = state.keeper_expiry.as_mut() {
851 expiry.expire_step(steps, idx)
852 } else {
853 false
854 };
855 if expiry_drained {
856 state.keeper_expiry = None;
857 }
858 let flow = flow_result?;
859 restore_result?;
860 match flow {
861 Flow::Done => {}
862 Flow::Break { .. } | Flow::Continue { .. } | Flow::Return { .. } => {
863 return Ok(flow);
864 }
865 }
866 }
867
868 let reap_named = !state.inside_async;
876 let has_bg = !state.bg_children.is_empty();
877 let named_pending = |state: &ExecState<P>| {
878 reap_named
879 && state
880 .named_tasks
881 .lock()
882 .unwrap_or_else(|e| e.into_inner())
883 .values()
884 .any(|entry| !entry.state.lock().unwrap_or_else(|e| e.into_inner()).reaped)
885 };
886 let has_named = named_pending(state);
887 if wait_at_end && (has_bg || has_named) {
888 loop {
889 let mut failed_status: Option<anyhow::Error> = None;
890
891 if failed_status.is_none() && state.cancel_token.load(Ordering::SeqCst) {
896 failed_status = Some(anyhow::anyhow!("ASYNC task cancelled"));
897 }
898
899 let mut i = 0;
901 while i < state.bg_children.len() {
902 match state.bg_children[i].try_wait() {
903 Ok(Some(status)) => {
904 if !status.success() && failed_status.is_none() {
905 failed_status =
906 Some(anyhow::anyhow!("ASYNC process exited with status {status}"));
907 break;
908 }
909 state.bg_children.swap_remove(i);
910 }
911 Ok(None) => {
912 i += 1;
913 }
914 Err(e) => {
915 if failed_status.is_none() {
916 failed_status = Some(e);
917 }
918 break;
919 }
920 }
921 }
922
923 if failed_status.is_none() && reap_named {
927 let entries: Vec<(u64, Arc<TaskEntry>)> = {
928 let named = state.named_tasks.lock().unwrap_or_else(|e| e.into_inner());
929 named
930 .iter()
931 .map(|(id, entry)| (*id, Arc::clone(entry)))
932 .collect()
933 };
934 for (id, entry) in &entries {
935 enum Poll {
936 Pending,
937 CompletedOk { sink: Option<Arc<SpillBuffer>> },
938 CompletedErr(anyhow::Error),
939 }
940 let poll = {
941 let mut guard = entry.state.lock().unwrap_or_else(|e| e.into_inner());
942 match guard.phase {
943 TaskPhase::Running | TaskPhase::Awaiting => {
944 let take_sink = matches!(guard.phase, TaskPhase::Running);
948 match guard.handle.as_mut() {
949 Some(handle) => match handle.try_wait() {
950 Ok(Some(status)) => {
951 let _ = guard.handle.take();
952 guard.phase = TaskPhase::Completed;
953 let sink =
954 if take_sink { guard.sink.take() } else { None };
955 if status.success() {
956 Poll::CompletedOk { sink }
957 } else {
958 Poll::CompletedErr(anyhow::anyhow!(
959 "named ASYNC task {id} exited with status {status}"
960 ))
961 }
962 }
963 Ok(None) => Poll::Pending,
964 Err(e) => {
965 let _ = guard.handle.take();
966 guard.phase = TaskPhase::Completed;
967 Poll::CompletedErr(e)
968 }
969 },
970 None => Poll::Pending,
973 }
974 }
975 TaskPhase::Cancelled | TaskPhase::Completed => Poll::Pending,
976 }
977 };
978 match poll {
979 Poll::Pending => {}
980 Poll::CompletedOk { sink } => {
981 entry.finish_teardown();
982 if let Some(sink) = sink
983 && let Err(e) = forward_task_sink(&sink, &out, *id)
984 {
985 if failed_status.is_none() {
986 failed_status = Some(e);
987 }
988 break;
989 }
990 }
991 Poll::CompletedErr(e) => {
992 entry.finish_teardown();
993 if failed_status.is_none() {
994 failed_status = Some(e);
995 }
996 break;
997 }
998 }
999 }
1000 }
1001
1002 if let Some(err) = failed_status {
1007 for survivor in state.bg_children.iter_mut() {
1008 let _ = survivor.kill();
1009 }
1010 if reap_named {
1011 let entries: Vec<Arc<TaskEntry>> = {
1012 let named = state.named_tasks.lock().unwrap_or_else(|e| e.into_inner());
1013 named.values().cloned().collect()
1014 };
1015 let mut to_kill: Vec<(Arc<TaskEntry>, Box<dyn BackgroundHandle>)> = Vec::new();
1016 for entry in &entries {
1017 let mut guard = entry.state.lock().unwrap_or_else(|e| e.into_inner());
1018 match guard.phase {
1019 TaskPhase::Running | TaskPhase::Awaiting => {
1020 guard.phase = TaskPhase::Cancelled;
1021 if let Some(handle) = guard.handle.take() {
1022 to_kill.push((Arc::clone(entry), handle));
1023 }
1024 }
1025 TaskPhase::Cancelled | TaskPhase::Completed => {}
1026 }
1027 }
1028 for (entry, mut handle) in to_kill {
1029 let _ = handle.kill();
1030 entry.finish_teardown();
1031 }
1032 }
1033 state.bg_children.clear();
1034 return Err(err);
1035 }
1036
1037 let bg_empty = state.bg_children.is_empty();
1038 if bg_empty && !named_pending(state) {
1039 return Ok(Flow::Done);
1040 }
1041 if reap_named {
1045 let unreaped: Vec<Arc<TaskEntry>> = {
1046 let named = state.named_tasks.lock().unwrap_or_else(|e| e.into_inner());
1047 named
1048 .values()
1049 .filter(|entry| {
1050 let guard = entry.state.lock().unwrap_or_else(|e| e.into_inner());
1051 matches!(guard.phase, TaskPhase::Cancelled) && !guard.reaped
1052 })
1053 .cloned()
1054 .collect()
1055 };
1056 for entry in &unreaped {
1057 entry.wait_reaped();
1058 }
1059 }
1060 std::thread::sleep(std::time::Duration::from_millis(10));
1061 }
1062 }
1063
1064 Ok(Flow::Done)
1065}
1066
1067fn forward_task_sink(sink: &Arc<SpillBuffer>, out: &Option<StreamHandle>, id: u64) -> Result<()> {
1072 let bytes = sink
1073 .drain_bytes()
1074 .map_err(|e| anyhow::anyhow!("named ASYNC task {id} output drain failed: {e}"))?;
1075 if !bytes.is_empty() {
1076 super::io::write_stdout(out.clone(), |writer| {
1077 writer
1078 .write_all(&bytes)
1079 .map_err(|e| anyhow::anyhow!("named ASYNC task {id} output forward failed: {e}"))?;
1080 Ok(())
1081 })?;
1082 }
1083 Ok(())
1084}
1085
1086fn restore_scopes<P: ProcessManager>(state: &mut ExecState<P>, count: usize) -> Result<()> {
1087 for _ in 0..count {
1088 state.pop_scope()?;
1089 }
1090 Ok(())
1091}
1092
1093#[allow(clippy::too_many_arguments)]
1098pub(super) fn execute_scoped_steps<P: ProcessManager>(
1099 state: &mut ExecState<P>,
1100 process: &mut P,
1101 steps: &[Step],
1102 stdin: CommandStdin,
1103 expose_stdin: bool,
1104 out: Option<StreamHandle>,
1105 err: Option<StreamHandle>,
1106 wait_at_end: bool,
1107) -> Result<Flow> {
1108 state.push_scope();
1109 let res = execute_steps(
1110 state,
1111 process,
1112 steps,
1113 stdin,
1114 expose_stdin,
1115 out,
1116 err,
1117 wait_at_end,
1118 );
1119 let pop_res = state.pop_scope();
1122 match (res, pop_res) {
1123 (Ok(flow), Ok(())) => Ok(flow),
1124 (Err(e), _) => Err(e),
1125 (Ok(_), Err(e)) => Err(e),
1126 }
1127}
1128
1129fn dispatch_flow_step<P: ProcessManager>(
1133 cmd: &StepKind,
1134 cx: &mut StepCtx<'_, P>,
1135 generation: usize,
1136 idx: usize,
1137) -> Result<Flow> {
1138 match cmd {
1139 StepKind::FuncDef { name, params, body } => {
1140 handlers::define_func(cx, name, params, body)?;
1141 Ok(Flow::Done)
1142 }
1143 StepKind::Call { name, args } => {
1144 let _ = handlers::call_func_value(cx, idx, name, args)?;
1145 Ok(Flow::Done)
1146 }
1147 StepKind::Return { expr } => handlers::handle_return(cx, idx, expr),
1148 StepKind::While { cond, body } => handlers::while_loop(cx, idx, cond, body),
1149 StepKind::Break => Ok(Flow::Break { idx }),
1150 StepKind::Continue => Ok(Flow::Continue { idx }),
1151 StepKind::For {
1152 key_var,
1153 key_type,
1154 var,
1155 var_type,
1156 in_expr,
1157 body,
1158 } => handlers::for_loop(
1159 cx,
1160 key_var.as_deref(),
1161 key_type.clone(),
1162 var,
1163 var_type.clone(),
1164 in_expr,
1165 body,
1166 ),
1167 StepKind::If {
1168 cond,
1169 then_body,
1170 else_ifs,
1171 else_body,
1172 } => handlers::if_then(cx, cond, then_body, else_ifs, else_body),
1173 StepKind::Timeout { duration, body } => {
1174 let duration = super::args::resolve_arg_as_duration(duration, cx)?;
1175 handlers::timeout(cx, idx, &duration, body)
1176 }
1177 StepKind::WithIo { bindings, cmd } => handlers::with_io(cx, generation, idx, bindings, cmd),
1178 StepKind::AssignCapture {
1179 var,
1180 decl_type,
1181 cmd,
1182 } => handlers::assign_capture(cx, generation, idx, var, decl_type.clone(), cmd),
1183 _ => {
1184 unreachable!("dispatch_flow_step handles only compound steps")
1185 }
1186 }
1187}