@@ -4,6 +4,7 @@ mod tmp_files;
44pub ( crate ) mod users;
55
66use anyhow:: Result ;
7+ use lazy_errors:: prelude:: * ;
78use serde:: { Deserialize , Serialize } ;
89use std:: fs:: DirBuilder ;
910use std:: path:: { Path , PathBuf } ;
@@ -13,6 +14,14 @@ use thiserror::Error;
1314use crate :: activate:: etc_files:: FileTree ;
1415use crate :: { StorePath , STATE_FILE_NAME , SYSTEM_MANAGER_STATE_DIR } ;
1516
17+ /// Separates the partial result from the error in an ActivationResult,
18+ pub ( crate ) fn split_activation_result < R > ( result : ActivationResult < R > ) -> ( R , anyhow:: Result < ( ) > ) {
19+ match result {
20+ Ok ( r) => ( r, Ok ( ( ) ) ) ,
21+ Err ( ActivationError :: WithPartialResult { result, source } ) => ( result, Err ( source) ) ,
22+ }
23+ }
24+
1625#[ derive( Error , Debug ) ]
1726pub enum ActivationError < R > {
1827 #[ error( "" ) ]
@@ -77,60 +86,68 @@ pub fn activate(store_path: &StorePath, ephemeral: bool) -> Result<()> {
7786
7887 let state_file = & get_state_file ( ) ?;
7988 let old_state = State :: from_file ( state_file) ?;
89+ let mut errs = ErrorStash :: new ( || "Activation completed with errors" ) ;
8090
8191 log:: info!( "Activating etc files..." ) ;
8292
83- match etc_files:: activate ( store_path, old_state. file_tree , ephemeral) {
84- Ok ( etc_tree) => {
85- log:: info!( "Restarting sysinit-reactivation.target..." ) ;
86- services:: restart_sysinit_reactivation_target ( ) ?;
87-
88- // Restart userborn before tmpfiles so users exist when tmpfiles runs
89- if let Err ( e) = services:: restart_userborn_if_exists ( ) {
90- log:: error!( "Error restarting userborn.service: {e}" ) ;
91- }
92-
93- 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" ) ;
100- }
101-
102- 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 ( ) ) ;
120- }
121-
122- Ok ( ( ) )
93+ let ( etc_tree, etc_result) = split_activation_result ( etc_files:: activate (
94+ store_path,
95+ old_state. file_tree ,
96+ ephemeral,
97+ ) ) ;
98+ let etc_ok = etc_result. is_ok ( ) ;
99+ if let Err ( ref e) = etc_result {
100+ log:: error!( "Error during activation: {e:?}" ) ;
101+ }
102+ etc_result. or_stash ( & mut errs) ;
103+
104+ // Only run daemon reload, userborn, tmpfiles, and services when etc files
105+ // were fully applied. Partial etc results mean services may reference
106+ // missing config files.
107+ let services = if etc_ok {
108+ log:: info!( "Restarting sysinit-reactivation.target..." ) ;
109+ let sysinit_result = services:: restart_sysinit_reactivation_target ( ) ;
110+ if let Err ( ref e) = sysinit_result {
111+ log:: error!( "Error restarting sysinit-reactivation.target: {e}" ) ;
123112 }
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
129- } ;
130- final_state. write_to_file ( state_file) ?;
131- Ok ( ( ) )
113+ sysinit_result. or_stash ( & mut errs) ;
114+
115+ // Restart userborn before tmpfiles so users exist when tmpfiles runs
116+ let userborn_result = services:: restart_userborn_if_exists ( ) ;
117+ if let Err ( ref e) = userborn_result {
118+ log:: error!( "Error restarting userborn.service: {e}" ) ;
132119 }
133- }
120+ userborn_result. or_stash ( & mut errs) ;
121+
122+ log:: info!( "Activating tmp files..." ) ;
123+ let ( ( ) , tmp_result) = split_activation_result ( tmp_files:: activate ( & etc_tree) ) ;
124+ if let Err ( ref e) = tmp_result {
125+ log:: error!( "Error during activation of tmp files: {e}" ) ;
126+ }
127+ tmp_result. or_stash ( & mut errs) ;
128+
129+ log:: info!( "Activating systemd services..." ) ;
130+ let ( services, svc_result) = split_activation_result ( services:: activate (
131+ store_path,
132+ old_state. services ,
133+ ephemeral,
134+ ) ) ;
135+ if let Err ( ref e) = svc_result {
136+ log:: error!( "Error during activation: {e:?}" ) ;
137+ }
138+ svc_result. or_stash ( & mut errs) ;
139+ services
140+ } else {
141+ old_state. services
142+ } ;
143+
144+ let final_state = State {
145+ file_tree : etc_tree,
146+ services,
147+ } ;
148+ final_state. write_to_file ( state_file) . or_stash ( & mut errs) ;
149+
150+ Ok ( Result :: < ( ) , _ > :: from ( errs) ?)
134151}
135152
136153pub fn prepopulate ( store_path : & StorePath , ephemeral : bool ) -> Result < ( ) > {
@@ -146,36 +163,46 @@ pub fn prepopulate(store_path: &StorePath, ephemeral: bool) -> Result<()> {
146163
147164 let state_file = & get_state_file ( ) ?;
148165 let old_state = State :: from_file ( state_file) ?;
166+ let mut errs = ErrorStash :: new ( || "Pre-population completed with errors" ) ;
149167
150168 log:: info!( "Activating etc files..." ) ;
151169
152- match etc_files:: activate ( store_path, old_state. file_tree , ephemeral) {
153- Ok ( etc_tree) => {
154- 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
174- }
175- }
170+ let ( etc_tree, etc_result) = split_activation_result ( etc_files:: activate (
171+ store_path,
172+ old_state. file_tree ,
173+ ephemeral,
174+ ) ) ;
175+ let etc_ok = etc_result. is_ok ( ) ;
176+ if let Err ( ref e) = etc_result {
177+ log:: error!( "Error during activation: {e:?}" ) ;
176178 }
177- . write_to_file ( state_file) ?;
178- Ok ( ( ) )
179+ etc_result. or_stash ( & mut errs) ;
180+
181+ // Only register services when etc files were fully applied, preserving
182+ // old service state on etc failure to avoid persisting state from a
183+ // partial run.
184+ let services = if etc_ok {
185+ log:: info!( "Registering systemd services..." ) ;
186+ let ( services, svc_result) = split_activation_result ( services:: get_active_services (
187+ store_path,
188+ old_state. services ,
189+ ) ) ;
190+ if let Err ( ref e) = svc_result {
191+ log:: error!( "Error during activation: {e:?}" ) ;
192+ }
193+ svc_result. or_stash ( & mut errs) ;
194+ services
195+ } else {
196+ old_state. services
197+ } ;
198+
199+ let final_state = State {
200+ file_tree : etc_tree,
201+ services,
202+ } ;
203+ final_state. write_to_file ( state_file) . or_stash ( & mut errs) ;
204+
205+ Ok ( Result :: < ( ) , _ > :: from ( errs) ?)
179206}
180207
181208fn run_preactivation_assertions ( store_path : & StorePath ) -> Result < process:: ExitStatus > {
@@ -198,3 +225,38 @@ pub(crate) fn get_state_file() -> Result<PathBuf> {
198225 . create ( SYSTEM_MANAGER_STATE_DIR ) ?;
199226 Ok ( state_file)
200227}
228+
229+ #[ cfg( test) ]
230+ mod tests {
231+ use super :: * ;
232+
233+ #[ test]
234+ fn empty_stash_returns_ok ( ) {
235+ let errs = ErrorStash :: new ( || "Activation completed with errors" ) ;
236+ let result: std:: result:: Result < ( ) , lazy_errors:: prelude:: Error > = errs. into ( ) ;
237+ assert ! ( result. is_ok( ) ) ;
238+ }
239+
240+ #[ test]
241+ fn single_stashed_error_returns_err ( ) {
242+ let mut errs = ErrorStash :: new ( || "Activation completed with errors" ) ;
243+ Err :: < ( ) , _ > ( anyhow:: anyhow!( "userborn failed" ) ) . or_stash ( & mut errs) ;
244+ let result: std:: result:: Result < ( ) , lazy_errors:: prelude:: Error > = errs. into ( ) ;
245+ let err = result. unwrap_err ( ) ;
246+ let msg = format ! ( "{err:#}" ) ;
247+ assert ! ( msg. contains( "userborn failed" ) , "message was: {msg}" ) ;
248+ }
249+
250+ #[ test]
251+ fn multiple_stashed_errors_returns_combined_err ( ) {
252+ let mut errs = ErrorStash :: new ( || "Deactivation completed with errors" ) ;
253+ Err :: < ( ) , _ > ( anyhow:: anyhow!( "userborn failed" ) ) . or_stash ( & mut errs) ;
254+ Err :: < ( ) , _ > ( anyhow:: anyhow!( "tmpfiles 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( "Deactivation" ) , "message was: {msg}" ) ;
259+ assert ! ( msg. contains( "userborn failed" ) , "message was: {msg}" ) ;
260+ assert ! ( msg. contains( "tmpfiles failed" ) , "message was: {msg}" ) ;
261+ }
262+ }
0 commit comments