Skip to content

Commit 6be53d9

Browse files
committed
feat: return non-zero exit code on activation errors
Use lazy_errors to accumulate errors during activation and return a non-zero exit code if any errors were encountered, while still applying as many changes as possible.
1 parent 42acdac commit 6be53d9

9 files changed

Lines changed: 223 additions & 117 deletions

File tree

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ dbus = "0.9.7"
1818
env_logger = "0.11.0"
1919
im = { version = "15.1.0", features = ["serde"] }
2020
itertools = "0.14.0"
21+
lazy_errors = "0.10.1"
2122
log = "0.4.17"
2223
nix = { version = "0.31.0", features = ["hostname", "user"] }
2324
regex = "1.11.1"

crates/system-manager-engine/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ dbus.workspace = true
2121
env_logger.workspace = true
2222
im.workspace = true
2323
itertools.workspace = true
24+
lazy_errors.workspace = true
2425
log.workspace = true
2526
nix.workspace = true
2627
regex.workspace = true

crates/system-manager-engine/src/activate.rs

Lines changed: 134 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,34 @@ mod tmp_files;
44
pub(crate) mod users;
55

66
use anyhow::Result;
7+
use lazy_errors::prelude::*;
78
use serde::{Deserialize, Serialize};
89
use std::fs::DirBuilder;
910
use std::path::{Path, PathBuf};
10-
use std::{fs, io, process};
11+
use std::{fmt, fs, io, process};
1112
use thiserror::Error;
1213

1314
use crate::activate::etc_files::FileTree;
1415
use crate::{StorePath, STATE_FILE_NAME, SYSTEM_MANAGER_STATE_DIR};
1516

17+
pub(crate) fn collect_activation_result_err<R, F, M>(
18+
res: ActivationResult<R>,
19+
err_stash: &mut ErrorStash<F, M>,
20+
) -> ActivationResult<R>
21+
where
22+
M: fmt::Display,
23+
F: FnOnce() -> M,
24+
{
25+
res.map_err(|e| {
26+
let ActivationError::WithPartialResult {
27+
result: _,
28+
ref source,
29+
} = e;
30+
err_stash.push(source.to_string());
31+
e
32+
})
33+
}
34+
1635
#[derive(Error, Debug)]
1736
pub enum ActivationError<R> {
1837
#[error("")]
@@ -77,60 +96,68 @@ pub fn activate(store_path: &StorePath, ephemeral: bool) -> Result<()> {
7796

7897
let state_file = &get_state_file()?;
7998
let old_state = State::from_file(state_file)?;
99+
let mut errs = ErrorStash::new(|| "Activation completed with errors");
80100

81101
log::info!("Activating etc files...");
82102

83-
match etc_files::activate(store_path, old_state.file_tree, ephemeral) {
103+
let etc_result = collect_activation_result_err(
104+
etc_files::activate(store_path, old_state.file_tree, ephemeral),
105+
&mut errs,
106+
);
107+
if let Err(ref e) = etc_result {
108+
log::error!("Error during activation: {e:?}");
109+
}
110+
111+
// Only run daemon reload, userborn, tmpfiles, and services when etc files
112+
// were fully applied. Partial etc results mean services may reference
113+
// missing config files.
114+
let (etc_tree, services) = match etc_result {
84115
Ok(etc_tree) => {
85116
log::info!("Restarting sysinit-reactivation.target...");
86-
services::restart_sysinit_reactivation_target()?;
117+
let sysinit_result = services::restart_sysinit_reactivation_target();
118+
if let Err(ref e) = sysinit_result {
119+
log::error!("Error restarting sysinit-reactivation.target: {e}");
120+
}
121+
sysinit_result.or_stash(&mut errs);
87122

88123
// Restart userborn before tmpfiles so users exist when tmpfiles runs
89-
if let Err(e) = services::restart_userborn_if_exists() {
124+
let userborn_result = services::restart_userborn_if_exists();
125+
if let Err(ref e) = userborn_result {
90126
log::error!("Error restarting userborn.service: {e}");
91127
}
128+
userborn_result.or_stash(&mut errs);
92129

93130
log::info!("Activating tmp files...");
94-
let tmp_result = tmp_files::activate(&etc_tree);
95-
if let Err(e) = &tmp_result {
96-
log::error!("Error during activation of tmp files");
97-
log::error!("{e}");
98-
} else {
99-
log::debug!("Successfully created tmp files");
131+
let tmp_result =
132+
collect_activation_result_err(tmp_files::activate(&etc_tree), &mut errs);
133+
if let Err(ref e) = tmp_result {
134+
log::error!("Error during activation of tmp files: {e}");
100135
}
101136

102137
log::info!("Activating systemd services...");
103-
let final_state = match services::activate(store_path, old_state.services, ephemeral) {
104-
Ok(services) => State {
105-
file_tree: etc_tree,
106-
services,
107-
},
108-
Err(ActivationError::WithPartialResult { result, source }) => {
109-
log::error!("Error during activation: {source:?}");
110-
State {
111-
file_tree: etc_tree,
112-
services: result,
113-
}
114-
}
115-
};
116-
final_state.write_to_file(state_file)?;
117-
118-
if let Err(e) = tmp_result {
119-
return Err(e.into());
138+
let svc_result = collect_activation_result_err(
139+
services::activate(store_path, old_state.services, ephemeral),
140+
&mut errs,
141+
);
142+
if let Err(ref e) = svc_result {
143+
log::error!("Error during activation: {e:?}");
120144
}
121-
122-
Ok(())
123-
}
124-
Err(ActivationError::WithPartialResult { result, source }) => {
125-
log::error!("Error during activation: {source:?}");
126-
let final_state = State {
127-
file_tree: result,
128-
..old_state
145+
let services = match svc_result {
146+
Ok(s) => s,
147+
Err(ActivationError::WithPartialResult { result, .. }) => result,
129148
};
130-
final_state.write_to_file(state_file)?;
131-
Ok(())
149+
(etc_tree, services)
132150
}
133-
}
151+
Err(ActivationError::WithPartialResult { result, .. }) => (result, old_state.services),
152+
};
153+
154+
let final_state = State {
155+
file_tree: etc_tree,
156+
services,
157+
};
158+
final_state.write_to_file(state_file).or_stash(&mut errs);
159+
160+
Ok(Result::<(), _>::from(errs)?)
134161
}
135162

136163
pub fn prepopulate(store_path: &StorePath, ephemeral: bool) -> Result<()> {
@@ -146,36 +173,47 @@ pub fn prepopulate(store_path: &StorePath, ephemeral: bool) -> Result<()> {
146173

147174
let state_file = &get_state_file()?;
148175
let old_state = State::from_file(state_file)?;
176+
let mut errs = ErrorStash::new(|| "Pre-population completed with errors");
149177

150178
log::info!("Activating etc files...");
151179

152-
match etc_files::activate(store_path, old_state.file_tree, ephemeral) {
180+
let etc_result = collect_activation_result_err(
181+
etc_files::activate(store_path, old_state.file_tree, ephemeral),
182+
&mut errs,
183+
);
184+
if let Err(ref e) = etc_result {
185+
log::error!("Error during activation: {e:?}");
186+
}
187+
188+
// Only register services when etc files were fully applied, preserving
189+
// old service state on etc failure to avoid persisting state from a
190+
// partial run.
191+
let (etc_tree, services) = match etc_result {
153192
Ok(etc_tree) => {
154193
log::info!("Registering systemd services...");
155-
match services::get_active_services(store_path, old_state.services) {
156-
Ok(services) => State {
157-
file_tree: etc_tree,
158-
services,
159-
},
160-
Err(ActivationError::WithPartialResult { result, source }) => {
161-
log::error!("Error during activation: {source:?}");
162-
State {
163-
file_tree: etc_tree,
164-
services: result,
165-
}
166-
}
167-
}
168-
}
169-
Err(ActivationError::WithPartialResult { result, source }) => {
170-
log::error!("Error during activation: {source:?}");
171-
State {
172-
file_tree: result,
173-
..old_state
194+
let svc_result = collect_activation_result_err(
195+
services::get_active_services(store_path, old_state.services),
196+
&mut errs,
197+
);
198+
if let Err(ref e) = svc_result {
199+
log::error!("Error during activation: {e:?}");
174200
}
201+
let services = match svc_result {
202+
Ok(s) => s,
203+
Err(ActivationError::WithPartialResult { result, .. }) => result,
204+
};
205+
(etc_tree, services)
175206
}
176-
}
177-
.write_to_file(state_file)?;
178-
Ok(())
207+
Err(ActivationError::WithPartialResult { result, .. }) => (result, old_state.services),
208+
};
209+
210+
let final_state = State {
211+
file_tree: etc_tree,
212+
services,
213+
};
214+
final_state.write_to_file(state_file).or_stash(&mut errs);
215+
216+
Ok(Result::<(), _>::from(errs)?)
179217
}
180218

181219
fn run_preactivation_assertions(store_path: &StorePath) -> Result<process::ExitStatus> {
@@ -198,3 +236,38 @@ pub(crate) fn get_state_file() -> Result<PathBuf> {
198236
.create(SYSTEM_MANAGER_STATE_DIR)?;
199237
Ok(state_file)
200238
}
239+
240+
#[cfg(test)]
241+
mod tests {
242+
use super::*;
243+
244+
#[test]
245+
fn empty_stash_returns_ok() {
246+
let errs = ErrorStash::new(|| "Activation completed with errors");
247+
let result: std::result::Result<(), lazy_errors::prelude::Error> = errs.into();
248+
assert!(result.is_ok());
249+
}
250+
251+
#[test]
252+
fn single_stashed_error_returns_err() {
253+
let mut errs = ErrorStash::new(|| "Activation completed with errors");
254+
Err::<(), _>(anyhow::anyhow!("userborn failed")).or_stash(&mut errs);
255+
let result: std::result::Result<(), lazy_errors::prelude::Error> = errs.into();
256+
let err = result.unwrap_err();
257+
let msg = format!("{err:#}");
258+
assert!(msg.contains("userborn failed"), "message was: {msg}");
259+
}
260+
261+
#[test]
262+
fn multiple_stashed_errors_returns_combined_err() {
263+
let mut errs = ErrorStash::new(|| "Deactivation completed with errors");
264+
Err::<(), _>(anyhow::anyhow!("userborn failed")).or_stash(&mut errs);
265+
Err::<(), _>(anyhow::anyhow!("tmpfiles failed")).or_stash(&mut errs);
266+
let result: std::result::Result<(), lazy_errors::prelude::Error> = errs.into();
267+
let err = result.unwrap_err();
268+
let msg = format!("{err:#}");
269+
assert!(msg.contains("Deactivation"), "message was: {msg}");
270+
assert!(msg.contains("userborn failed"), "message was: {msg}");
271+
assert!(msg.contains("tmpfiles failed"), "message was: {msg}");
272+
}
273+
}

0 commit comments

Comments
 (0)