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