1use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
19use std::io;
20use std::sync::Arc;
21use std::sync::atomic::{AtomicBool, Ordering};
22use std::time::{Duration, Instant};
23
24use crate::prover_config::{GroupKind, ProverPlan, ProverTask, ToDriverCommand};
25use crate::report_cli_error::report_cli_error_and_exit;
26use crate::toolchain_config::ToolchainConfig;
27
28use log::{debug, info, trace, warn};
29use signal_hook::consts::signal::{SIGHUP, SIGINT, SIGTERM};
30use signal_hook::low_level as siglow;
31use std::os::unix::process::CommandExt;
32use std::process::Stdio;
33
34use serde::Serialize;
35
36#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)]
37pub enum IndefiniteReason {
38 GroupCriteriaAlreadyMet,
39 IndefiniteChildren,
40 Timeout,
41 Cleanup,
42}
43
44#[derive(Copy, Clone, Debug, PartialEq, Eq)]
45pub enum TaskOutcome {
46 Normal { success: bool },
47 Indefinite { reason: IndefiniteReason },
48}
49
50impl TaskOutcome {
51 fn normal(success: bool) -> Self {
52 TaskOutcome::Normal { success }
53 }
54
55 fn indefinite(reason: IndefiniteReason) -> Self {
56 TaskOutcome::Indefinite { reason }
57 }
58
59 fn success() -> Self {
60 TaskOutcome::normal(true)
61 }
62
63 fn failed() -> Self {
64 TaskOutcome::normal(false)
65 }
66
67 #[allow(dead_code)]
68 fn timeout() -> Self {
69 TaskOutcome::indefinite(IndefiniteReason::Timeout)
70 }
71
72 fn is_success(&self) -> bool {
73 match self {
74 TaskOutcome::Normal { success } => *success,
75 _ => false,
76 }
77 }
78
79 fn is_failed(&self) -> bool {
80 match self {
81 TaskOutcome::Normal { success } => !*success,
82 _ => false,
83 }
84 }
85
86 fn is_indefinite(&self) -> bool {
87 match self {
88 TaskOutcome::Indefinite { .. } => true,
89 _ => false,
90 }
91 }
92}
93
94impl serde::Serialize for TaskOutcome {
95 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
96 where
97 S: serde::Serializer,
98 {
99 let label: &str = match self {
100 TaskOutcome::Normal { success: true } => "Success",
101 TaskOutcome::Normal { success: false } => "Failed",
102 TaskOutcome::Indefinite { reason } => match reason {
103 IndefiniteReason::GroupCriteriaAlreadyMet => "GroupCriteriaAlreadyMet",
104 IndefiniteReason::IndefiniteChildren => "IndefiniteChildren",
105 IndefiniteReason::Timeout => "Timeout",
106 IndefiniteReason::Cleanup => "Cleanup",
107 },
108 };
109 serializer.serialize_str(label)
110 }
111}
112
113#[derive(Copy, Clone, Debug)]
114enum TaskState {
115 NotStarted,
116 Running { pid: i32 },
117 Completed { outcome: TaskOutcome },
118}
119
120#[derive(Debug)]
121struct TaskNode {
122 task: ProverTask,
123 timeout_ms: Option<u64>,
124 task_id: Option<String>,
125 state: TaskState,
126 completed_stdout: Option<String>,
128 completed_stderr: Option<String>,
129}
130
131#[derive(Debug)]
132struct GroupNode {
133 kind: GroupKind,
134 children: Vec<NodeId>,
135 outcome: Option<TaskOutcome>,
136 keep_running_till_finish: bool,
139 first_winner_outcome: Option<TaskOutcome>,
142}
143
144#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
145struct NodeId(usize);
146
147#[derive(Debug)]
148enum NodeInner {
149 Task(TaskNode),
150 Group(GroupNode),
151}
152
153#[derive(Debug)]
154struct Node {
155 id: NodeId,
156 parent: Option<NodeId>,
157 inner: NodeInner,
158}
159
160#[derive(Debug)]
164pub struct Scheduler {
174 nodes: Vec<Node>,
175 root: NodeId,
176 task_ids: Vec<NodeId>,
178 running: HashSet<NodeId>,
180 pid_to_node: HashMap<i32, NodeId>,
182 round_robin: VecDeque<NodeId>,
184 max_procs: usize,
186 json_files: HashMap<NodeId, tempfile::NamedTempFile>,
188 outputs: HashMap<NodeId, (tempfile::NamedTempFile, tempfile::NamedTempFile, String)>,
190 cmdlines: HashMap<NodeId, String>,
192 cancel_flag: Arc<AtomicBool>,
194 deadlines: BTreeMap<Instant, Vec<NodeId>>,
195}
196
197#[derive(Debug, Serialize)]
198#[serde(untagged)]
199pub enum ProverReportNode {
200 Task {
201 task_id: Option<String>,
202 cmdline: Option<String>,
203 outcome: Option<TaskOutcome>,
204 stdout: Option<String>,
205 stderr: Option<String>,
206 },
207 Group {
208 kind: GroupKind,
209 outcome: Option<TaskOutcome>,
210 tasks: Vec<ProverReportNode>,
211 },
212}
213
214impl ProverReportNode {
215 pub fn find_task_node<'a>(&'a self, task_id: &str) -> Option<&'a ProverReportNode> {
216 match self {
217 ProverReportNode::Task { task_id: tid, .. } => {
218 if let Some(t) = tid {
219 if t == task_id {
220 return Some(self);
221 }
222 }
223 None
224 }
225 ProverReportNode::Group { tasks, .. } => {
226 for t in tasks {
227 if let Some(found) = t.find_task_node(task_id) {
228 return Some(found);
229 }
230 }
231 None
232 }
233 }
234 }
235}
236
237#[derive(Serialize)]
238pub struct ProverReport {
239 pub success: bool,
240 pub plan: ProverReportNode,
241}
242
243impl Scheduler {
244 pub fn new(plan: ProverPlan, max_procs: usize, cancel_flag: Arc<AtomicBool>) -> Scheduler {
250 log::info!("prover: new scheduler");
251 let mut builder = PlanBuilder::default();
252 let root = builder.build_plan(None, plan);
253 let nodes = builder.nodes;
254 let mut task_ids = Vec::new();
256 let mut round_robin = VecDeque::new();
257 for n in &nodes {
258 if matches!(n.inner, NodeInner::Task(_)) {
259 task_ids.push(n.id);
260 round_robin.push_back(n.id);
261 }
262 }
263 let mut sched = Scheduler {
264 nodes,
265 root,
266 task_ids,
267 running: HashSet::new(),
268 pid_to_node: HashMap::new(),
269 round_robin,
270 max_procs: max_procs.max(1),
271 json_files: HashMap::new(),
272 outputs: HashMap::new(),
273 cmdlines: HashMap::new(),
274 cancel_flag,
275 deadlines: BTreeMap::new(),
276 };
277 for n in &sched.nodes {
280 if let NodeInner::Task(t) = &n.inner {
281 let cmd = t.task.to_command();
282 let program = cmd.get_program().to_string_lossy().into_owned();
283 let args = cmd
284 .get_args()
285 .map(|s| s.to_string_lossy().into_owned())
286 .collect::<Vec<_>>();
287 let cmdline = if args.is_empty() {
288 program.clone()
289 } else {
290 format!("{} {}", program, args.join(" "))
291 };
292 sched.cmdlines.insert(n.id, cmdline);
293 }
294 }
295 info!(
296 "prover: scheduler initialized: tasks={}, max_procs={}",
297 sched.task_ids.len(),
298 sched.max_procs
299 );
300 sched
301 }
302
303 pub fn run(&mut self) -> io::Result<bool> {
309 loop {
310 if self.cancel_flag.load(Ordering::Relaxed) {
312 warn!("prover: cancellation requested; cleaning up and exiting");
313 let _ = self.cleanup_tasks();
314 return Ok(false);
315 }
316
317 self.check_and_handle_timeouts()?;
319
320 if let Some(root_outcome) = self.node_outcome(self.root) {
322 self.cleanup_tasks()?;
323 return Ok(root_outcome.is_success());
324 }
325
326 if self.running.len() < self.max_procs {
328 self.fill_slots()?;
329 }
330
331 if self.running.is_empty() {
332 panic!("prover: no running tasks remain and root unresolved; should not happen");
335 }
336
337 if let Err(_e) = self.wait_for_one_child_and_handle() {
339 return Err(io::Error::new(io::ErrorKind::Other, "waitpid failed"));
340 }
341 }
342 }
343
344 fn check_and_handle_timeouts(&mut self) -> io::Result<()> {
345 while let Some(entry) = self.deadlines.first_entry() {
346 let now = Instant::now();
347 if now < *entry.key() {
348 break;
349 }
350 let tids = entry.get().iter().copied().collect::<Vec<_>>();
351 entry.remove();
352 for tid in tids {
353 log::info!("timeout task={:?}", tid);
354 self.cancel_subtree(tid, IndefiniteReason::Timeout)?;
355 self.on_child_plan_finished(tid)?;
356 }
357 }
358 Ok(())
359 }
360
361 fn is_task_runnable(&self, task_id: NodeId) -> bool {
366 let node = &self.nodes[task_id.0];
367 match &node.inner {
368 NodeInner::Task(t) => matches!(t.state, TaskState::NotStarted),
369 _ => false,
370 }
371 }
372
373 fn is_task_completed(&self, task_id: NodeId) -> bool {
375 let node = &self.nodes[task_id.0];
376 match &node.inner {
377 NodeInner::Task(t) => matches!(t.state, TaskState::Completed { .. }),
378 _ => false,
379 }
380 }
381
382 fn is_task_indefinite(&self, task_id: NodeId) -> bool {
386 let node = &self.nodes[task_id.0];
387 match &node.inner {
388 NodeInner::Task(t) => matches!(
389 t.state,
390 TaskState::Completed {
391 outcome: TaskOutcome::Indefinite { .. },
392 }
393 ),
394 _ => false,
395 }
396 }
397
398 fn fill_slots(&mut self) -> io::Result<()> {
403 let len = self.round_robin.len();
404 for _ in 0..len {
405 if let Some(tid) = self.round_robin.pop_front() {
406 if self.is_task_runnable(tid) {
407 self.spawn_task(tid)?;
408 } else {
409 assert!(!self.is_task_completed(tid));
411 self.round_robin.push_back(tid);
412 }
413 if self.running.len() >= self.max_procs {
414 break;
415 }
416 } else {
417 break;
418 }
419 }
420 trace!(
421 "prover: scheduling iteration complete; running={}/{}",
422 self.running.len(),
423 self.max_procs
424 );
425 Ok(())
426 }
427
428 fn spawn_task(&mut self, task_id: NodeId) -> io::Result<()> {
435 let node = &mut self.nodes[task_id.0];
436 let (mut cmd, stdout_tmp, stderr_tmp, json_tmp, json_path, timeout_ms, cmdline) =
438 match &mut node.inner {
439 NodeInner::Task(t) => match t.state {
440 TaskState::NotStarted => {
441 let mut cmd = t.task.to_command();
442 let json_tmp = tempfile::Builder::new().suffix(".json").tempfile()?;
444 let json_path = json_tmp.path().to_path_buf();
445 cmd.arg("--output_json").arg(&json_path);
446 let json_path = json_path.display().to_string();
447
448 let stdout_tmp = tempfile::Builder::new().suffix(".stdout").tempfile()?;
450 let stderr_tmp = tempfile::Builder::new().suffix(".stderr").tempfile()?;
451 let stdout_file = stdout_tmp.as_file().try_clone()?;
452 let stderr_file = stderr_tmp.as_file().try_clone()?;
453 cmd.stdout(Stdio::from(stdout_file));
454 cmd.stderr(Stdio::from(stderr_file));
455
456 let program = cmd.get_program().to_string_lossy().into_owned();
458 let args = cmd
459 .get_args()
460 .map(|s| s.to_string_lossy().into_owned())
461 .collect::<Vec<_>>();
462 let cmdline = if args.is_empty() {
463 program.clone()
464 } else {
465 format!("{} {}", program, args.join(" "))
466 };
467
468 unsafe {
471 cmd.pre_exec(|| {
472 let rc = libc::setpgid(0, 0);
473 if rc != 0 {
474 return Err(io::Error::new(
475 io::ErrorKind::Other,
476 "Failed to setpgid",
477 ));
478 }
479 Ok(())
480 })
481 };
482
483 let timeout_ms = t.timeout_ms;
484
485 (
486 cmd, stdout_tmp, stderr_tmp, json_tmp, json_path, timeout_ms, cmdline,
487 )
488 }
489 _ => return Ok(()),
490 },
491 _ => return Ok(()),
492 };
493
494 let child = cmd.spawn()?;
495 let pid_i32 = child.id() as i32;
496
497 self.outputs
499 .insert(task_id, (stdout_tmp, stderr_tmp, cmdline.clone()));
500 self.json_files.insert(task_id, json_tmp);
501 if let Some(timeout_ms) = timeout_ms {
502 let deadline = Instant::now() + Duration::from_millis(timeout_ms);
503 self.deadlines.entry(deadline).or_default().push(task_id);
504 }
505
506 if let NodeInner::Task(t) = &mut self.nodes[task_id.0].inner {
507 t.state = TaskState::Running { pid: pid_i32 };
508 debug!(
509 "prover: spawned task, cmdline=\"{}\" pid={} json_out={}",
510 self.cmdlines.get(&task_id).unwrap(),
511 pid_i32,
512 json_path
513 );
514 }
515 self.running.insert(task_id);
516 self.pid_to_node.insert(pid_i32, task_id);
517 Ok(())
518 }
519
520 fn remove_tasks_from_queues(&mut self, tids: &[NodeId]) {
522 use std::collections::HashSet;
523 let to_remove: HashSet<NodeId> = tids.iter().copied().collect();
524 let mut new_rr = VecDeque::with_capacity(self.round_robin.len());
526 while let Some(id) = self.round_robin.pop_front() {
527 if !to_remove.contains(&id) {
528 new_rr.push_back(id);
529 }
530 }
531 self.round_robin = new_rr;
532 self.task_ids.retain(|id| !to_remove.contains(id));
534 }
535
536 fn remove_task_from_queues(&mut self, tid: NodeId) {
538 self.remove_tasks_from_queues(&[tid]);
539 }
540
541 fn collect_subtree_tasks_rec(&self, id: NodeId, out: &mut Vec<NodeId>) {
543 match &self.nodes[id.0].inner {
544 NodeInner::Task(_) => out.push(id),
545 NodeInner::Group(g) => {
546 for &cid in &g.children {
547 self.collect_subtree_tasks_rec(cid, out);
548 }
549 }
550 }
551 }
552
553 fn prune_subtree_tasks(&mut self, id: NodeId) {
555 let mut tids = Vec::new();
556 self.collect_subtree_tasks_rec(id, &mut tids);
557 self.remove_tasks_from_queues(&tids);
558 trace!("prover: pruned {} tasks from queues", tids.len());
559 }
560
561 fn wait_for_one_child_and_handle(&mut self) -> io::Result<()> {
568 let pids: Vec<i32> = self.pid_to_node.keys().copied().collect();
571 let mut handled_any = false;
572
573 for pid in pids {
574 let mut status: libc::c_int = 0;
575 let rc = unsafe { libc::waitpid(pid, &mut status as *mut libc::c_int, libc::WNOHANG) };
576 if rc == 0 {
577 continue; }
579 if rc < 0 {
580 let err = io::Error::last_os_error();
581 if let Some(raw) = err.raw_os_error() {
582 if raw == libc::EINTR {
583 continue;
584 }
585 if raw == libc::ECHILD {
586 if let Some(tid) = self.pid_to_node.remove(&pid) {
588 self.remove_task_bookkeeping(tid);
589 self.on_child_plan_finished(tid)?;
590 }
591 continue;
592 }
593 }
594 return Err(io::Error::new(io::ErrorKind::Other, "waitpid failed"));
595 }
596
597 let term_signal = if libc::WIFSIGNALED(status) {
599 Some(libc::WTERMSIG(status))
600 } else {
601 None
602 };
603
604 if let Some(tid) = self.pid_to_node.remove(&pid) {
605 let was_indefinite = self.is_task_indefinite(tid);
607 if was_indefinite {
608 debug!("prover: reaped indefinite task pid={}", pid);
609 handled_any = true;
610 continue;
611 }
612
613 if let Some(sig) = term_signal {
614 warn!(
615 "prover: task terminated by signal {} pid={} cmdline=\"{}\"",
616 sig,
617 pid,
618 self.cmdlines.get(&tid).unwrap()
619 );
620 }
621
622 let success = self.read_success_from_json(tid);
623
624 let mut captured_stdout: Option<String> = None;
626 let mut captured_stderr: Option<String> = None;
627 if let Some((stdout_tmp, stderr_tmp, _cmdline)) = self.outputs.remove(&tid) {
628 let read_lossy = |p: &std::path::Path| -> String {
629 match std::fs::read(p) {
630 Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
631 Err(_) => String::new(),
632 }
633 };
634 captured_stdout = Some(read_lossy(stdout_tmp.path()));
635 captured_stderr = Some(read_lossy(stderr_tmp.path()));
636 }
637
638 if let NodeInner::Task(t) = &mut self.nodes[tid.0].inner {
639 t.state = TaskState::Completed {
640 outcome: TaskOutcome::normal(success),
641 };
642 t.completed_stdout = captured_stdout;
643 t.completed_stderr = captured_stderr;
644 debug!(
645 "prover: task completed, cmdline=\"{}\" pid={} success={}",
646 self.cmdlines.get(&tid).unwrap(),
647 pid,
648 success
649 );
650 } else {
651 panic!("not a task node");
652 }
653 self.remove_task_bookkeeping(tid);
654 self.on_child_plan_finished(tid)?;
655 handled_any = true;
656 break; }
658 }
659
660 if !handled_any {
661 let mut ts = libc::timespec {
663 tv_sec: 0,
664 tv_nsec: 100 * 1000000,
665 };
666 unsafe { libc::nanosleep(&ts, &mut ts) };
667 }
668
669 Ok(())
670 }
671
672 fn read_success_from_json(&self, tid: NodeId) -> bool {
675 let path = match self.json_files.get(&tid) {
676 Some(tf) => tf.path(),
677 None => return false,
678 };
679 let data = match std::fs::read_to_string(path) {
680 Ok(s) => s,
681 Err(_) => return false,
682 };
683 let v: serde_json::Value = match serde_json::from_str(&data) {
684 Ok(v) => v,
685 Err(_) => return false,
686 };
687
688 v.get("success").and_then(|b| b.as_bool()).unwrap_or(false)
689 }
690
691 fn on_child_plan_finished(&mut self, node_id: NodeId) -> io::Result<()> {
694 let mut cur_id = node_id;
696 let mut parent = self.nodes[cur_id.0].parent;
697 while let Some(pid) = parent {
698 self.resolve_group_child(pid, cur_id)?;
699 cur_id = pid;
700 parent = self.nodes[pid.0].parent;
701 }
702 Ok(())
703 }
704
705 fn get_group_node(&self, id: NodeId) -> &GroupNode {
706 match &self.nodes[id.0].inner {
707 NodeInner::Group(g) => g,
708 _ => panic!("not a group node"),
709 }
710 }
711
712 fn get_group_node_mut(&mut self, id: NodeId) -> &mut GroupNode {
713 match &mut self.nodes[id.0].inner {
714 NodeInner::Group(g) => g,
715 _ => panic!("not a group node"),
716 }
717 }
718
719 fn resolve_group_child(&mut self, gid: NodeId, child_id: NodeId) -> io::Result<()> {
723 let (kind, idx, num_children, finished, success, failed, indefinite, keep_running) = {
724 let g = self.get_group_node(gid);
725 if g.outcome.is_some() {
726 return Ok(());
727 }
728 let num_children = g.children.len();
729 let idx = g
730 .children
731 .iter()
732 .position(|&id| id == child_id)
733 .expect("child must be in group's children");
734 let mut finished = 0;
735 let mut success = 0;
736 let mut failed = 0;
737 let mut indefinite = 0;
738 for &cid in &g.children {
739 let child_outcomes = self.node_outcome(cid);
740 match child_outcomes {
741 Some(TaskOutcome::Normal { success: true }) => {
742 finished += 1;
743 success += 1;
744 }
745 Some(TaskOutcome::Normal { success: false }) => {
746 finished += 1;
747 failed += 1;
748 }
749 Some(TaskOutcome::Indefinite { .. }) => {
750 finished += 1;
751 indefinite += 1;
752 }
753 None => {}
754 }
755 }
756 (
757 g.kind,
758 idx,
759 num_children,
760 finished,
761 success,
762 failed,
763 indefinite,
764 g.keep_running_till_finish,
765 )
766 };
767
768 let child_outcome = self.node_outcome(child_id);
771
772 match kind {
773 GroupKind::First => {
774 let g = self.get_group_node_mut(gid);
775 if let Some(co) = child_outcome {
776 if !co.is_indefinite() {
777 g.first_winner_outcome.get_or_insert(co);
778 }
779 }
780 if finished == num_children {
781 if let Some(winner) = g.first_winner_outcome {
782 info!("prover: group resolved kind=first outcome={:?}", winner);
783 return self.finalize_group_resolution(gid, winner, None);
784 } else {
785 return self.finalize_group_resolution(
786 gid,
787 TaskOutcome::indefinite(IndefiniteReason::IndefiniteChildren),
788 None,
789 );
790 }
791 }
792 if !keep_running {
793 if let Some(co) = child_outcome {
794 if !co.is_indefinite() {
795 info!("prover: group resolved kind=first result={:?}", co);
796 return self.finalize_group_resolution(gid, co, Some(idx));
797 }
798 }
799 }
800 return Ok(());
801 }
802 GroupKind::Any => {
803 if finished == num_children {
804 let outcome = if success > 0 {
805 TaskOutcome::success()
806 } else if indefinite > 0 {
807 TaskOutcome::indefinite(IndefiniteReason::IndefiniteChildren)
808 } else {
809 TaskOutcome::failed()
810 };
811 info!("prover: group resolved kind=any result={:?}", outcome);
812 return self.finalize_group_resolution(gid, outcome, None);
813 }
814 if !keep_running {
815 if let Some(co) = child_outcome {
816 if co.is_success() {
817 info!("prover: group resolved kind=any result=true");
818 return self.finalize_group_resolution(
819 gid,
820 TaskOutcome::success(),
821 Some(idx),
822 );
823 }
824 }
825 }
826 Ok(())
827 }
828 GroupKind::All => {
829 if finished == num_children {
830 let outcome = if failed > 0 {
831 TaskOutcome::failed()
832 } else if indefinite > 0 {
833 TaskOutcome::indefinite(IndefiniteReason::IndefiniteChildren)
834 } else {
835 TaskOutcome::success()
836 };
837 info!("prover: group resolved kind=all result={:?}", outcome);
838 return self.finalize_group_resolution(gid, outcome, None);
839 }
840 if !keep_running {
841 if let Some(co) = child_outcome {
842 if co.is_failed() {
843 info!("prover: group resolved kind=all result=false");
844 return self.finalize_group_resolution(
845 gid,
846 TaskOutcome::failed(),
847 Some(idx),
848 );
849 }
850 }
851 }
852 Ok(())
853 }
854 }
855 }
856
857 fn finalize_group_resolution(
858 &mut self,
859 gid: NodeId,
860 outcome: TaskOutcome,
861 cancel_siblings_of: Option<usize>,
862 ) -> io::Result<()> {
863 if let NodeInner::Group(g) = &mut self.nodes[gid.0].inner {
864 g.outcome = Some(outcome);
865 }
866 if let Some(idx) = cancel_siblings_of {
867 self.cancel_group_siblings(gid, idx)?;
868 }
869 self.prune_subtree_tasks(gid);
871 Ok(())
872 }
873
874 fn cancel_group_siblings(&mut self, gid: NodeId, exclude_child_idx: usize) -> io::Result<()> {
879 let cancel_ids: Vec<NodeId> = match &self.nodes[gid.0].inner {
881 NodeInner::Group(g) => g
882 .children
883 .iter()
884 .enumerate()
885 .filter_map(|(i, &cid)| {
886 if i != exclude_child_idx {
887 Some(cid)
888 } else {
889 None
890 }
891 })
892 .collect(),
893 _ => vec![],
894 };
895 if !cancel_ids.is_empty() {
896 warn!(
897 "prover: canceling {} sibling subtree(s) for group (excluding child idx={})",
898 cancel_ids.len(),
899 exclude_child_idx
900 );
901 }
902 for cid in cancel_ids.iter().copied() {
903 self.cancel_subtree(cid, IndefiniteReason::GroupCriteriaAlreadyMet)?;
904 }
905 Ok(())
906 }
907
908 fn set_outcome(&mut self, id: NodeId, outcome: TaskOutcome) {
909 match &mut self.nodes[id.0].inner {
910 NodeInner::Task(t) => t.state = TaskState::Completed { outcome },
911 NodeInner::Group(g) => g.outcome = Some(outcome),
912 }
913 }
914
915 fn cancel_subtree(
924 &mut self,
925 id: NodeId,
926 indefinite_reason: IndefiniteReason,
927 ) -> io::Result<()> {
928 match &self.nodes[id.0].inner {
929 NodeInner::Task(t) => {
930 let prior_state = t.state.clone();
931 if matches!(prior_state, TaskState::Completed { .. }) {
932 return Ok(());
933 }
934 self.set_outcome(id, TaskOutcome::indefinite(indefinite_reason));
935 match prior_state {
936 TaskState::NotStarted => {
937 debug!("prover: canceled (not-started) task");
938 }
939 TaskState::Running { pid } => {
940 unsafe {
941 let _ = libc::kill(-pid, libc::SIGKILL);
942 }
943 info!("prover: canceled running task pid={}", pid);
944 }
945 TaskState::Completed { .. } => {
946 }
948 }
949 self.remove_task_bookkeeping(id);
950 }
951 NodeInner::Group(g) => {
952 let prior_outcome = g.outcome.clone();
953 if matches!(prior_outcome, Some(..)) {
954 return Ok(());
955 }
956 let kind = g.kind;
957 let children = g.children.clone();
958 for cid in children {
959 self.cancel_subtree(cid, IndefiniteReason::GroupCriteriaAlreadyMet)?;
960 }
961 self.set_outcome(
962 id,
963 TaskOutcome::indefinite(IndefiniteReason::GroupCriteriaAlreadyMet),
964 );
965 self.prune_subtree_tasks(id);
967 warn!("prover: canceled group subtree kind={:?} ", kind);
968 }
969 }
970 Ok(())
971 }
972
973 fn node_outcome(&self, id: NodeId) -> Option<TaskOutcome> {
980 match &self.nodes[id.0].inner {
981 NodeInner::Task(t) => match t.state {
982 TaskState::Completed { outcome } => Some(outcome),
983 _ => None,
984 },
985 NodeInner::Group(g) => g.outcome,
986 }
987 }
988
989 fn remove_task_bookkeeping(&mut self, id: NodeId) {
993 self.running.remove(&id);
994 self.remove_task_from_queues(id);
995 let _ = self.json_files.remove(&id);
996 let _ = self.outputs.remove(&id);
997 }
998
999 fn build_report_node(&self, id: NodeId) -> ProverReportNode {
1000 match &self.nodes[id.0].inner {
1001 NodeInner::Task(t) => {
1002 let task_id = t.task_id.clone();
1003 let cmdline = self.cmdlines.get(&id).cloned();
1004 let outcome = self.node_outcome(id);
1005 let stdout = t.completed_stdout.clone();
1006 let stderr = t.completed_stderr.clone();
1007 ProverReportNode::Task {
1008 task_id,
1009 cmdline,
1010 outcome,
1011 stdout,
1012 stderr,
1013 }
1014 }
1015 NodeInner::Group(g) => {
1016 let outcome = g.outcome;
1017 let tasks = g
1018 .children
1019 .iter()
1020 .map(|&cid| self.build_report_node(cid))
1021 .collect();
1022 ProverReportNode::Group {
1023 kind: g.kind,
1024 outcome,
1025 tasks,
1026 }
1027 }
1028 }
1029 }
1030
1031 pub fn cleanup_tasks(&mut self) -> io::Result<()> {
1038 self.cancel_flag.store(true, Ordering::Relaxed);
1040 info!("prover: cleanup starting (best-effort) ");
1041 let _ = self.cancel_subtree(self.root, IndefiniteReason::Cleanup);
1042
1043 while !self.pid_to_node.is_empty() {
1045 let pids: Vec<i32> = self.pid_to_node.keys().copied().collect();
1046 let mut reaped_any = false;
1047
1048 for pid in pids {
1049 let mut status: libc::c_int = 0;
1050 let rc =
1051 unsafe { libc::waitpid(pid, &mut status as *mut libc::c_int, libc::WNOHANG) };
1052 if rc == 0 {
1053 continue;
1054 }
1055 if rc < 0 {
1056 let err = io::Error::last_os_error();
1057 if let Some(raw) = err.raw_os_error() {
1058 if raw == libc::EINTR {
1059 continue;
1060 }
1061 if raw == libc::ECHILD {
1062 self.pid_to_node.remove(&pid);
1064 reaped_any = true;
1065 continue;
1066 }
1067 }
1068 warn!("prover: waitpid during cleanup failed: {}", err);
1069 self.pid_to_node.remove(&pid);
1071 reaped_any = true;
1072 continue;
1073 }
1074
1075 self.pid_to_node.remove(&pid);
1077 trace!("prover: reaped child pid={} during cleanup", pid);
1078 reaped_any = true;
1079 }
1080
1081 if !reaped_any {
1082 let mut ts = libc::timespec {
1084 tv_sec: 0,
1085 tv_nsec: 100 * 1000000,
1086 };
1087 unsafe { libc::nanosleep(&ts, &mut ts) };
1088 }
1089 }
1090
1091 warn!("prover: cleanup complete");
1092 Ok(())
1093 }
1094}
1095
1096#[derive(Default)]
1097struct PlanBuilder {
1099 nodes: Vec<Node>,
1100}
1101
1102impl PlanBuilder {
1103 fn push(&mut self, parent: Option<NodeId>, inner: NodeInner) -> NodeId {
1104 let id = NodeId(self.nodes.len());
1105 self.nodes.push(Node { id, parent, inner });
1106 id
1107 }
1108
1109 fn build_plan(&mut self, parent: Option<NodeId>, plan: ProverPlan) -> NodeId {
1112 match plan {
1113 ProverPlan::Task {
1114 task,
1115 timeout_ms,
1116 task_id,
1117 } => self.push(
1118 parent,
1119 NodeInner::Task(TaskNode {
1120 task,
1121 timeout_ms,
1122 task_id,
1123 state: TaskState::NotStarted,
1124 completed_stdout: None,
1125 completed_stderr: None,
1126 }),
1127 ),
1128 ProverPlan::Group {
1129 kind,
1130 tasks,
1131 keep_running_till_finish,
1132 } => {
1133 let gid = self.push(
1134 parent,
1135 NodeInner::Group(GroupNode {
1136 kind,
1137 children: Vec::new(),
1138 outcome: None,
1139 keep_running_till_finish,
1140 first_winner_outcome: None,
1141 }),
1142 );
1143 let child_ids: Vec<NodeId> = tasks
1144 .into_iter()
1145 .map(|t| self.build_plan(Some(gid), t))
1146 .collect();
1147 if let NodeInner::Group(g) = &mut self.nodes[gid.0].inner {
1148 g.children = child_ids;
1149 }
1150 gid
1151 }
1152 }
1153 }
1154}
1155
1156pub fn run_prover_plan(plan: ProverPlan, max_procs: usize) -> io::Result<ProverReport> {
1160 let cancel_flag = Arc::new(AtomicBool::new(false));
1162 let mut sig_ids = Vec::new();
1163 for sig in [SIGINT, SIGTERM, SIGHUP] {
1164 let flag = Arc::clone(&cancel_flag);
1165 match unsafe { siglow::register(sig, move || flag.store(true, Ordering::Relaxed)) } {
1167 Ok(id) => sig_ids.push(id),
1168 Err(e) => warn!("prover: failed to register signal {}: {}", sig, e),
1169 }
1170 }
1171
1172 let mut sched = Scheduler::new(plan, max_procs, cancel_flag);
1173 info!("prover: run starting");
1174 let run_res = sched.run();
1175
1176 for id in sig_ids.drain(..) {
1178 let _ = siglow::unregister(id);
1179 }
1180
1181 let result = match run_res {
1182 Ok(v) => v,
1183 Err(e) => {
1184 let _ = sched.cleanup_tasks();
1185 return Err(e);
1186 }
1187 };
1188 let plan_node = sched.build_report_node(sched.root);
1189 Ok(ProverReport {
1190 success: result,
1191 plan: plan_node,
1192 })
1193}
1194
1195fn write_error_json(
1200 output_json_path: &Option<String>,
1201 message: &str,
1202 exception: &str,
1203 stage: &str,
1204) {
1205 if let Some(path) = output_json_path {
1206 let obj = serde_json::json!({
1207 "success": false,
1208 "error": {
1209 "message": message,
1210 "exception": exception,
1211 "stage": stage,
1212 }
1213 });
1214 if let Err(write_err) = std::fs::write(path, obj.to_string()) {
1215 warn!(
1216 "prover: failed writing output_json error to {}: {}",
1217 path, write_err
1218 );
1219 }
1220 }
1221}
1222
1223fn fail_and_exit(
1225 output_json_path: &Option<String>,
1226 stage: &str,
1227 message: &str,
1228 exception: &str,
1229 extra_details: &[(&str, &str)],
1230) -> ! {
1231 write_error_json(output_json_path, message, exception, stage);
1232 let mut details: Vec<(&str, &str)> = Vec::with_capacity(2 + extra_details.len());
1234 details.push(("stage", stage));
1235 details.push(("exception", exception));
1236 details.extend_from_slice(extra_details);
1237 report_cli_error_and_exit(message, Some("prover"), details)
1238}
1239
1240pub fn handle_prover(matches: &clap::ArgMatches, _config: &Option<ToolchainConfig>) {
1247 let cores: usize = matches
1248 .get_one::<String>("cores")
1249 .map(|s| s.parse::<usize>().unwrap_or(1))
1250 .unwrap_or(1)
1251 .max(1);
1252 let plan_path = matches
1253 .get_one::<String>("plan_json_file")
1254 .expect("plan_json_file arg missing");
1255 let output_json_path = matches
1256 .get_one::<String>("output_json")
1257 .map(|s| s.to_string());
1258
1259 info!(
1260 "prover: starting with cores={} plan_source= {}",
1261 cores,
1262 if plan_path == "-" { "stdin" } else { plan_path }
1263 );
1264
1265 let plan_json = if plan_path == "-" {
1266 use std::io::Read;
1267 let mut buf = String::new();
1268 if let Err(e) = std::io::stdin().read_to_string(&mut buf) {
1269 fail_and_exit(
1270 &output_json_path,
1271 "read-plan",
1272 "Failed to read plan from stdin",
1273 &e.to_string(),
1274 &[],
1275 );
1276 }
1277 buf
1278 } else {
1279 match std::fs::read_to_string(plan_path) {
1280 Ok(s) => s,
1281 Err(e) => {
1282 fail_and_exit(
1283 &output_json_path,
1284 "read-plan",
1285 "Failed to read plan JSON file",
1286 &e.to_string(),
1287 &[("path", plan_path)],
1288 );
1289 }
1290 }
1291 };
1292
1293 let plan: ProverPlan = match serde_json::from_str(&plan_json) {
1294 Ok(p) => p,
1295 Err(e) => {
1296 fail_and_exit(
1297 &output_json_path,
1298 "parse-plan",
1299 "Failed to parse ProverPlan JSON",
1300 &e.to_string(),
1301 &[],
1302 );
1303 }
1304 };
1305
1306 match run_prover_plan(plan, cores) {
1307 Ok(report) => {
1308 if let Some(path) = &output_json_path {
1309 let s = serde_json::to_string(&report).unwrap();
1312 if let Err(e) = std::fs::write(path, s) {
1313 warn!("prover: failed writing output_json to {}: {}", path, e);
1314 }
1315 } else {
1316 fn print_task_outputs(node: &ProverReportNode) {
1318 match node {
1319 ProverReportNode::Task {
1320 cmdline,
1321 stdout,
1322 stderr,
1323 ..
1324 } => {
1325 println!("{}", "-".repeat(80));
1326 println!("{}", cmdline.as_deref().unwrap_or(""));
1327 let out = stdout.as_deref().unwrap_or("");
1328 let err = stderr.as_deref().unwrap_or("");
1329 if out.is_empty() {
1330 println!(">>>> stdout:");
1331 } else {
1332 println!(">>>> stdout:\n{}", out);
1333 }
1334 if err.is_empty() {
1335 println!(">>>> stderr:");
1336 } else {
1337 println!(">>>> stderr:\n{}", err);
1338 }
1339 println!("{}", "-".repeat(80));
1340 }
1341 ProverReportNode::Group { tasks, .. } => {
1342 for t in tasks {
1343 print_task_outputs(t);
1344 }
1345 }
1346 }
1347 }
1348 print_task_outputs(&report.plan);
1349 }
1350 if report.success {
1351 println!("Overall: success");
1352 std::process::exit(0)
1353 } else {
1354 println!("Overall: failure");
1355 std::process::exit(1)
1356 }
1357 }
1358 Err(e) => {
1359 let exc = e.to_string();
1360 fail_and_exit(&output_json_path, "run", "prover run failed", &exc, &[]);
1362 }
1363 }
1364}