1#[cfg(test)]
3mod lifetime_tests;
4mod observe;
5mod result;
6mod worker;
7use crate::{
8 process,
9 project::{VerifyOptions, selection::Selection},
10 store::{self, Receipts},
11 types::{FailureCode, RequestKey, RunId, RunStatus},
12};
13pub use observe::{Counts, RunObservation, State, status, view, wait};
14pub use result::{Finding, ResultCursor, ResultState, RunResultPage, RunVerdict, result};
15use serde::{Deserialize, Serialize};
16use std::path::{Path, PathBuf};
17pub(crate) use worker::{execute, supervise};
18
19#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(default)]
22pub struct RunModes {
23 pub speculate: bool,
24 pub defer_human: bool,
25}
26impl RunModes {
27 pub(crate) fn is_default(&self) -> bool {
28 self == &Self::default()
29 }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "snake_case")]
34pub enum Activity {
35 Preparing,
36 Executing,
37 Publishing,
38 Waiting,
39 Cancelling,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(tag = "state", rename_all = "snake_case")]
44pub enum Worker {
45 Starting {
46 launcher: process::ChildIdentity,
47 },
48 Admitted {
49 owner: process::ChildIdentity,
50 },
51 Legacy,
53}
54#[derive(Debug, Clone, Serialize, Deserialize)]
55#[serde(rename_all = "camelCase")]
56pub struct Control {
57 pub worker: Worker,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub supervisor: Option<process::ChildIdentity>,
61 pub activity: Activity,
62 #[serde(default)]
63 pub cancellation_requested: bool,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub failure_code: Option<FailureCode>,
66}
67impl Control {
68 pub(crate) fn starting() -> Result<Self, String> {
69 Ok(Self {
70 worker: Worker::Starting {
71 launcher: process::child_identity(std::process::id()).map_err(|e| e.to_string())?,
72 },
73 supervisor: None,
74 activity: Activity::Preparing,
75 cancellation_requested: false,
76 failure_code: None,
77 })
78 }
79 pub(crate) fn legacy() -> Self {
80 Self {
81 worker: Worker::Legacy,
82 supervisor: None,
83 activity: Activity::Executing,
84 cancellation_requested: false,
85 failure_code: None,
86 }
87 }
88}
89
90#[derive(Debug, Clone)]
92pub struct Context {
93 pub(crate) repo: PathBuf,
94 pub(crate) state: PathBuf,
95 executable: PathBuf,
96}
97impl Context {
98 pub fn new(repo: &Path, state: Option<&Path>, executable: &Path) -> Result<Self, String> {
99 Ok(Self {
100 repo: crate::workspace::canonical_target(repo).map_err(|e| e.to_string())?,
101 state: crate::workspace::canonical_target(&store::state_dir(state)?)
102 .map_err(|e| e.to_string())?,
103 executable: executable.to_path_buf(),
104 })
105 }
106 pub fn discover(repo: &Path, state: Option<&Path>) -> Result<Self, String> {
108 let executable = process::artifactize_executable()?;
109 Self::new(repo, state, &executable)
110 }
111}
112
113pub async fn start(
114 context: &Context,
115 selection: &Selection,
116 options: &VerifyOptions,
117 request_key: Option<RequestKey>,
118) -> Result<RunObservation, String> {
119 if let Some(key) = &request_key
121 && context.state.join(store::DATABASE).is_file()
122 {
123 let receipts = Receipts::open(&context.state, &context.repo).await?;
124 let result = receipts.keyed_run(&context.repo, key).await;
125 let closed = receipts.close().await;
126 if let Some(id) = result.and_then(|id| closed.map(|()| id))? {
127 return status(context, &id).await;
128 }
129 }
130 let (id, accepted) = crate::project::verify::reserve(
131 &context.repo,
132 Some(&context.state),
133 selection,
134 options,
135 request_key.as_ref(),
136 )
137 .await?;
138 if accepted {
139 let receipts = Receipts::open(&context.state, &context.repo).await?;
140 let result = async {
141 let launch =
142 process::launch_run(&context.executable, &id, &context.repo, &context.state);
143 let admitted = match launch {
144 Ok(identity) => receipts.admit(&id, identity).await,
145 Err(error) => Err(error.to_string()),
146 };
147 if let Err(error) = admitted {
148 worker::fail(&receipts, &id, FailureCode::SpawnFailed, error.clone()).await?;
149 return Err(format!("Run {id} handoff failed: {error}"));
150 }
151 Ok(())
152 }
153 .await;
154 let closed = receipts.close().await;
155 result.and(closed)?;
156 }
157 status(context, &id).await
158}
159
160#[derive(Debug, Clone, Serialize)]
161pub struct CancelAcknowledgement {
162 pub run: RunId,
163 pub state: State,
164 pub cancellation_requested: bool,
165}
166pub async fn cancel(context: &Context, id: &RunId) -> Result<CancelAcknowledgement, String> {
167 let current = view(context, id).await?;
168 let receipts = Receipts::open(&context.state, &context.repo).await?;
169 let result = receipts.request_cancel(¤t.run.id).await;
170 let closed = receipts.close().await;
171 let run = result.and_then(|run| closed.map(|()| run))?;
172 Ok(CancelAcknowledgement {
173 run: id.clone(),
174 state: observe::state(&run, ¤t.requests),
175 cancellation_requested: run
176 .control
177 .as_ref()
178 .is_some_and(|control| control.cancellation_requested),
179 })
180}
181
182pub(crate) async fn reconcile(
183 state: &Path,
184 view: store::RunView,
185) -> Result<store::RunView, String> {
186 if view.run.status() == RunStatus::Running
187 && let Some(control) = &view.run.control
188 {
189 let owner = match control.worker {
190 Worker::Starting { launcher } => Some(launcher),
191 Worker::Admitted { owner } => Some(owner),
192 Worker::Legacy => None,
193 };
194 if owner.is_some() && !process::is_alive(owner).map_err(|e| e.to_string())? {
195 worker::recover(state, &view).await?;
196 return store::read_run(state, &view.run.id).await;
197 }
198 }
199 Ok(view)
200}