1use std::fs;
2use std::io;
3use std::path::{Path, PathBuf};
4use std::process::Command;
5
6use super::model::{
7 GeneratedArtifact, GeneratedArtifactKind, SchedulerCommandOutput, SchedulerExecutionReport,
8 SchedulerExecutionStatus, SchedulerExecutionStep, SchedulerExecutionTotals,
9 SchedulerOperationPlan, SchedulerPlanStep,
10};
11
12pub trait SchedulerOperationBackend {
13 fn ensure_dir(&mut self, path: &Path) -> Result<(), String>;
14 fn write_file(&mut self, path: &Path, contents: &str) -> Result<(), String>;
15 fn set_executable(&mut self, path: &Path) -> Result<(), String>;
16 fn remove_file(&mut self, path: &Path) -> Result<RemoveFileOutcome, String>;
17 fn run_command(&mut self, argv: &[String]) -> Result<SchedulerCommandOutput, String>;
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum RemoveFileOutcome {
22 Removed,
23 NotFound,
24}
25
26#[derive(Debug, Default)]
27pub struct RealSchedulerOperationBackend;
28
29pub fn execute_scheduler_operation(
30 plan: &SchedulerOperationPlan,
31 backend: &mut impl SchedulerOperationBackend,
32) -> SchedulerExecutionReport {
33 let mut steps = Vec::with_capacity(plan.steps.len());
34 let mut totals = SchedulerExecutionTotals::default();
35 let mut blocked = false;
36
37 for step in &plan.steps {
38 let execution_step = if blocked {
39 SchedulerExecutionStep {
40 step: step.clone(),
41 status: SchedulerExecutionStatus::Blocked,
42 message: Some("blocked by an earlier failed scheduler operation step".to_string()),
43 command_output: None,
44 }
45 } else {
46 execute_step(step, &plan.artifacts, backend)
47 };
48
49 if execution_step.status == SchedulerExecutionStatus::Failed {
50 blocked = true;
51 }
52 increment_totals(&mut totals, execution_step.status);
53 steps.push(execution_step);
54 }
55
56 SchedulerExecutionReport {
57 command: plan.command,
58 operation: plan.operation,
59 dry_run: false,
60 platform: plan.platform,
61 artifacts: plan.artifacts.clone(),
62 steps,
63 totals,
64 }
65}
66
67impl SchedulerOperationBackend for RealSchedulerOperationBackend {
68 fn ensure_dir(&mut self, path: &Path) -> Result<(), String> {
69 fs::create_dir_all(path).map_err(error_message)
70 }
71
72 fn write_file(&mut self, path: &Path, contents: &str) -> Result<(), String> {
73 write_file_atomically(path, contents).map_err(error_message)
74 }
75
76 fn set_executable(&mut self, path: &Path) -> Result<(), String> {
77 set_executable(path).map_err(error_message)
78 }
79
80 fn remove_file(&mut self, path: &Path) -> Result<RemoveFileOutcome, String> {
81 match fs::remove_file(path) {
82 Ok(()) => Ok(RemoveFileOutcome::Removed),
83 Err(error) if error.kind() == io::ErrorKind::NotFound => {
84 Ok(RemoveFileOutcome::NotFound)
85 }
86 Err(error) => Err(error_message(error)),
87 }
88 }
89
90 fn run_command(&mut self, argv: &[String]) -> Result<SchedulerCommandOutput, String> {
91 let Some((program, args)) = argv.split_first() else {
92 return Err("command argv must not be empty".to_string());
93 };
94 let output = Command::new(program)
95 .args(args)
96 .output()
97 .map_err(error_message)?;
98 Ok(SchedulerCommandOutput {
99 exit_code: output.status.code(),
100 stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
101 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
102 })
103 }
104}
105
106fn execute_step(
107 step: &SchedulerPlanStep,
108 artifacts: &[GeneratedArtifact],
109 backend: &mut impl SchedulerOperationBackend,
110) -> SchedulerExecutionStep {
111 match step {
112 SchedulerPlanStep::EnsureDir { path } => match backend.ensure_dir(path) {
113 Ok(()) => applied(step),
114 Err(message) => failed(step, message, None),
115 },
116 SchedulerPlanStep::WriteFile {
117 path,
118 artifact_kind,
119 } => execute_write_file(step, path, *artifact_kind, artifacts, backend),
120 SchedulerPlanStep::SetExecutable { path } => match backend.set_executable(path) {
121 Ok(()) => applied(step),
122 Err(message) => failed(step, message, None),
123 },
124 SchedulerPlanStep::RemoveFile { path } => match backend.remove_file(path) {
125 Ok(RemoveFileOutcome::Removed) => applied(step),
126 Ok(RemoveFileOutcome::NotFound) => SchedulerExecutionStep {
127 step: step.clone(),
128 status: SchedulerExecutionStatus::Skipped,
129 message: Some("file was not present".to_string()),
130 command_output: None,
131 },
132 Err(message) => failed(step, message, None),
133 },
134 SchedulerPlanStep::RunCommand { argv } => execute_run_command(step, argv, backend),
135 }
136}
137
138fn execute_write_file(
139 step: &SchedulerPlanStep,
140 path: &Path,
141 artifact_kind: GeneratedArtifactKind,
142 artifacts: &[GeneratedArtifact],
143 backend: &mut impl SchedulerOperationBackend,
144) -> SchedulerExecutionStep {
145 let Some(artifact) = artifacts
146 .iter()
147 .find(|artifact| artifact.kind == artifact_kind && artifact.intended_install_path == path)
148 else {
149 return failed(
150 step,
151 format!(
152 "missing generated artifact for {} at {}",
153 super::model::artifact_kind_label(artifact_kind),
154 path.display()
155 ),
156 None,
157 );
158 };
159
160 match backend.write_file(path, &artifact.contents) {
161 Ok(()) => applied(step),
162 Err(message) => failed(step, message, None),
163 }
164}
165
166fn execute_run_command(
167 step: &SchedulerPlanStep,
168 argv: &[String],
169 backend: &mut impl SchedulerOperationBackend,
170) -> SchedulerExecutionStep {
171 match backend.run_command(argv) {
172 Ok(output) if output.exit_code == Some(0) => SchedulerExecutionStep {
173 step: step.clone(),
174 status: SchedulerExecutionStatus::Applied,
175 message: None,
176 command_output: Some(output),
177 },
178 Ok(output) => {
179 let message = match output.exit_code {
180 Some(code) => format!("command exited with status {code}"),
181 None => "command terminated without an exit status".to_string(),
182 };
183 failed(step, message, Some(output))
184 }
185 Err(message) => failed(step, message, None),
186 }
187}
188
189fn applied(step: &SchedulerPlanStep) -> SchedulerExecutionStep {
190 SchedulerExecutionStep {
191 step: step.clone(),
192 status: SchedulerExecutionStatus::Applied,
193 message: None,
194 command_output: None,
195 }
196}
197
198fn failed(
199 step: &SchedulerPlanStep,
200 message: String,
201 command_output: Option<SchedulerCommandOutput>,
202) -> SchedulerExecutionStep {
203 SchedulerExecutionStep {
204 step: step.clone(),
205 status: SchedulerExecutionStatus::Failed,
206 message: Some(message),
207 command_output,
208 }
209}
210
211fn increment_totals(totals: &mut SchedulerExecutionTotals, status: SchedulerExecutionStatus) {
212 match status {
213 SchedulerExecutionStatus::Applied => totals.applied += 1,
214 SchedulerExecutionStatus::Skipped => totals.skipped += 1,
215 SchedulerExecutionStatus::Failed => totals.failed += 1,
216 SchedulerExecutionStatus::Blocked => totals.blocked += 1,
217 }
218}
219
220fn write_file_atomically(path: &Path, contents: &str) -> io::Result<()> {
221 let temp_path = temp_path_for(path)?;
222 fs::write(&temp_path, contents)?;
223 match fs::rename(&temp_path, path) {
224 Ok(()) => Ok(()),
225 Err(rename_error) => replace_file_after_rename_failure(path, &temp_path, rename_error),
226 }
227}
228
229fn replace_file_after_rename_failure(
230 path: &Path,
231 temp_path: &Path,
232 rename_error: io::Error,
233) -> io::Result<()> {
234 match fs::remove_file(path) {
235 Ok(()) => match fs::rename(temp_path, path) {
236 Ok(()) => Ok(()),
237 Err(error) => {
238 let _ = fs::remove_file(temp_path);
239 Err(error)
240 }
241 },
242 Err(error) if error.kind() == io::ErrorKind::NotFound => {
243 let _ = fs::remove_file(temp_path);
244 Err(rename_error)
245 }
246 Err(error) => {
247 let _ = fs::remove_file(temp_path);
248 Err(error)
249 }
250 }
251}
252
253fn temp_path_for(path: &Path) -> io::Result<PathBuf> {
254 let Some(file_name) = path.file_name() else {
255 return Err(io::Error::new(
256 io::ErrorKind::InvalidInput,
257 "write-file path must include a file name",
258 ));
259 };
260 let mut temp_name = file_name.to_os_string();
261 temp_name.push(format!(".tmp.{}", std::process::id()));
262 Ok(path.with_file_name(temp_name))
263}
264
265#[cfg(unix)]
266fn set_executable(path: &Path) -> io::Result<()> {
267 use std::os::unix::fs::PermissionsExt;
268
269 let metadata = fs::metadata(path)?;
270 let mut permissions = metadata.permissions();
271 permissions.set_mode(permissions.mode() | 0o111);
272 fs::set_permissions(path, permissions)
273}
274
275#[cfg(not(unix))]
276fn set_executable(path: &Path) -> io::Result<()> {
277 let _ = fs::metadata(path)?;
278 Ok(())
279}
280
281fn error_message(error: io::Error) -> String {
282 error.to_string()
283}