corundum_framework_package/
services.rs1#[derive(Clone, Copy, Debug)]
9pub struct Services {
10 #[cfg(feature = "service")]
11 list: &'static [fn() -> corundum_framework_service::ServiceSpec],
12}
13
14impl Services {
15 pub const NONE: Services = Services {
17 #[cfg(feature = "service")]
18 list: &[],
19 };
20
21 #[cfg(feature = "service")]
23 pub const fn new(
24 services: &'static [fn() -> corundum_framework_service::ServiceSpec],
25 ) -> Services {
26 Services { list: services }
27 }
28
29 #[cfg(feature = "service")]
31 pub(crate) fn is_empty(&self) -> bool {
32 self.list.is_empty()
33 }
34
35 #[cfg(feature = "service")]
37 pub(crate) fn specs(&self) -> impl Iterator<Item = corundum_framework_service::ServiceSpec> {
38 self.list.iter().map(|service| service())
39 }
40}
41
42pub(crate) trait ServiceDriver: Send + Sync {
46 fn stop_running(&self) -> Result<(), String>;
49 fn restart_stopped(&self);
52}
53
54impl ServiceDriver for () {
56 fn stop_running(&self) -> Result<(), String> {
57 Ok(())
58 }
59
60 fn restart_stopped(&self) {}
61}
62
63#[cfg(feature = "service")]
64pub(crate) use with_services::*;
65
66#[cfg(not(feature = "service"))]
67pub(crate) fn driver(
69 _spec: &'static crate::start::PackageSpec,
70) -> std::sync::Arc<dyn ServiceDriver> {
71 std::sync::Arc::new(())
72}
73
74#[cfg(feature = "service")]
75mod with_services {
76 use std::fs;
77 use std::path::{Path, PathBuf};
78 use std::sync::Arc;
79
80 use corundum_framework_platform::dirs::AppDirs;
81 use corundum_framework_service::{
82 Environment, ServiceManager, ServiceState, SystemManager, start, state, stop,
83 };
84
85 use super::ServiceDriver;
86 use crate::start::PackageSpec;
87
88 pub(crate) const RESTART_MARKER: &str = "services-to-restart";
91
92 pub(crate) fn driver(spec: &'static PackageSpec) -> Arc<dyn ServiceDriver> {
94 Arc::new(System {
95 spec,
96 data: AppDirs::new(spec.app_name).map(|dirs| dirs.data().to_owned()),
97 })
98 }
99
100 struct System {
102 spec: &'static PackageSpec,
103 data: Option<PathBuf>,
105 }
106
107 impl ServiceDriver for System {
108 fn stop_running(&self) -> Result<(), String> {
109 if self.spec.services.is_empty() {
110 return Ok(());
111 }
112 let data = self
115 .data
116 .as_deref()
117 .ok_or("no data directory to note the services to restart in")?;
118 stop_running(self.spec, data, &Environment::detect(), &SystemManager).map(|_| ())
119 }
120
121 fn restart_stopped(&self) {
122 if let Some(data) = &self.data {
123 restart_stopped(self.spec, data, &Environment::detect(), &SystemManager);
124 }
125 }
126 }
127
128 pub(crate) fn stop_running(
137 spec: &PackageSpec,
138 data: &Path,
139 env: &Environment,
140 manager: &dyn ServiceManager,
141 ) -> Result<Vec<&'static str>, String> {
142 let mut running = Vec::new();
143 for service in spec.services.specs() {
144 match state(&service, env, manager) {
145 Ok(ServiceState::Running) => running.push(service),
146 Ok(_) => {}
147 Err(e) => {
148 tracing::warn!(
149 "could not tell whether {} runs, stopping it anyway: {e}",
150 service.app_name
151 );
152 running.push(service);
153 }
154 }
155 }
156 if running.is_empty() {
157 return Ok(Vec::new());
158 }
159 let names: String = running
160 .iter()
161 .map(|service| format!("{}\n", service.app_name))
162 .collect();
163 let marker = data.join(RESTART_MARKER);
164 fs::create_dir_all(data)
165 .and_then(|()| fs::write(&marker, names))
166 .map_err(|e| format!("could not write {}: {e}", marker.display()))?;
167 for service in &running {
168 let _ = stop(service, env, manager);
170 }
171 Ok(running.iter().map(|service| service.app_name).collect())
172 }
173
174 pub(crate) fn restart_stopped(
180 spec: &PackageSpec,
181 data: &Path,
182 env: &Environment,
183 manager: &dyn ServiceManager,
184 ) {
185 let marker = data.join(RESTART_MARKER);
186 let names = match fs::read(&marker) {
187 Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
188 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return,
189 Err(e) => {
190 tracing::warn!("could not read {}: {e}", marker.display());
191 String::new()
192 }
193 };
194 for name in names.lines().map(str::trim).filter(|name| !name.is_empty()) {
195 let Some(service) = spec
196 .services
197 .specs()
198 .find(|service| service.app_name == name)
199 else {
200 tracing::warn!(
201 "{name:?} was stopped for an update but is no service of the app now"
202 );
203 continue;
204 };
205 if let Err(e) = start(&service, env, manager) {
206 tracing::warn!("could not start {name} again after the update: {e}");
207 }
208 }
209 if let Err(e) = fs::remove_file(&marker) {
210 tracing::warn!("could not remove {}: {e}", marker.display());
211 }
212 }
213
214 #[cfg(any(windows, test))]
217 pub(crate) fn uninstall_all(
218 spec: &PackageSpec,
219 env: &Environment,
220 manager: &dyn ServiceManager,
221 ) {
222 for service in spec.services.specs() {
223 if let Err(e) = corundum_framework_service::uninstall(&service, env, manager) {
224 tracing::warn!("could not uninstall {}: {e}", service.app_name);
225 }
226 }
227 }
228
229 #[cfg(test)]
230 mod tests {
231 use std::fs;
232 use std::path::PathBuf;
233
234 use corundum_framework_service::{
235 Environment, Os, RecordingManager, ServiceSpec, unit_path,
236 };
237
238 use super::*;
239 use crate::services::Services;
240
241 fn demo() -> ServiceSpec {
242 ServiceSpec {
243 app_name: "demo",
244 description: "Demo daemon".into(),
245 args: vec!["daemon".into()],
246 legacy_labels: vec![],
247 post_install: None,
248 }
249 }
250
251 fn idle() -> ServiceSpec {
252 ServiceSpec {
253 app_name: "idle",
254 ..demo()
255 }
256 }
257
258 fn package(services: &'static [fn() -> ServiceSpec]) -> PackageSpec {
259 PackageSpec {
260 app_name: "demo",
261 on_installed: None,
262 on_uninstalling: None,
263 before_apply: None,
264 cli: None,
265 services: Services::new(services),
266 }
267 }
268
269 fn home() -> PathBuf {
270 PathBuf::from("/home/user")
271 }
272
273 fn linux() -> Environment {
274 Environment {
275 euid: 1000,
276 os: Os::Linux,
277 home: Some(home()),
278 }
279 }
280
281 fn command(parts: &[&str]) -> Vec<String> {
282 parts.iter().map(|part| (*part).to_owned()).collect()
283 }
284
285 fn installed() -> RecordingManager {
288 RecordingManager::new()
289 .with_unit(
290 unit_path("demo", &home()),
291 "[Service]\nExecStart=/usr/bin/demo daemon\n",
292 )
293 .with_unit(
294 unit_path("idle", &home()),
295 "[Service]\nExecStart=/usr/bin/idle daemon\n",
296 )
297 .with_file("/usr/bin/demo")
298 .with_file("/usr/bin/idle")
299 .answering("idle", "inactive")
301 .answering("is-active", "active")
302 }
303
304 fn marker(data: &Path) -> PathBuf {
305 data.join(RESTART_MARKER)
306 }
307
308 #[test]
309 fn a_running_service_is_stopped_and_marked() {
310 let data = tempfile::tempdir().unwrap();
311 let manager = installed();
312 let stopped = stop_running(&package(&[demo]), data.path(), &linux(), &manager).unwrap();
313 assert_eq!(stopped, ["demo"]);
314 assert_eq!(fs::read_to_string(marker(data.path())).unwrap(), "demo\n");
315 assert!(manager.calls().commands.contains(&command(&[
316 "systemctl",
317 "--user",
318 "stop",
319 "demo"
320 ])));
321 }
322
323 #[test]
324 fn only_the_spec_s_running_services_are_stopped() {
325 let data = tempfile::tempdir().unwrap();
326 let manager = installed();
327 let stopped =
328 stop_running(&package(&[demo, idle]), data.path(), &linux(), &manager).unwrap();
329 assert_eq!(stopped, ["demo"]);
330 assert_eq!(fs::read_to_string(marker(data.path())).unwrap(), "demo\n");
331 let commands = manager.calls().commands;
332 assert!(!commands.contains(&command(&["systemctl", "--user", "stop", "idle"])));
333
334 let data = tempfile::tempdir().unwrap();
337 let manager = installed();
338 let stopped = stop_running(&package(&[]), data.path(), &linux(), &manager).unwrap();
339 assert!(stopped.is_empty());
340 assert!(manager.calls().commands.is_empty());
341 assert!(!marker(data.path()).exists());
342 }
343
344 #[test]
345 fn the_marked_services_start_and_the_marker_goes() {
346 let data = tempfile::tempdir().unwrap();
347 fs::write(marker(data.path()), "demo\n").unwrap();
348 let manager = RecordingManager::new();
349 restart_stopped(&package(&[demo, idle]), data.path(), &linux(), &manager);
350 assert_eq!(
351 manager.calls().commands,
352 [command(&["systemctl", "--user", "start", "demo"])]
353 );
354 assert!(!marker(data.path()).exists());
355 }
356
357 #[test]
358 fn a_marker_naming_an_unknown_service_is_dropped() {
359 let data = tempfile::tempdir().unwrap();
360 fs::write(marker(data.path()), b"gone\n\xff\n").unwrap();
361 let manager = RecordingManager::new();
362 restart_stopped(&package(&[demo]), data.path(), &linux(), &manager);
363 assert!(manager.calls().commands.is_empty());
364 assert!(!marker(data.path()).exists());
365
366 restart_stopped(&package(&[demo]), data.path(), &linux(), &manager);
368 assert!(manager.calls().commands.is_empty());
369 }
370
371 #[test]
372 fn a_failed_start_is_logged_and_the_marker_still_goes() {
373 let data = tempfile::tempdir().unwrap();
374 fs::write(marker(data.path()), "demo\nidle\n").unwrap();
375 let manager = RecordingManager::failing_on("start");
376 restart_stopped(&package(&[demo, idle]), data.path(), &linux(), &manager);
377 assert_eq!(manager.calls().commands.len(), 2);
379 assert!(!marker(data.path()).exists());
380 }
381
382 #[test]
383 fn a_service_whose_state_cannot_be_read_is_stopped_and_marked() {
384 let data = tempfile::tempdir().unwrap();
387 let env = Environment {
388 home: None,
389 ..linux()
390 };
391 let manager = installed();
392 let stopped = stop_running(&package(&[demo]), data.path(), &env, &manager).unwrap();
393 assert_eq!(stopped, ["demo"]);
394 assert_eq!(fs::read_to_string(marker(data.path())).unwrap(), "demo\n");
395 assert!(manager.calls().commands.contains(&command(&[
396 "systemctl",
397 "--user",
398 "stop",
399 "demo"
400 ])));
401 }
402
403 #[test]
404 fn a_marker_that_cannot_be_written_stops_nothing() {
405 let data = tempfile::tempdir().unwrap();
408 let unwritable = data.path().join("file");
409 fs::write(&unwritable, "").unwrap();
410 let manager = installed();
411 let err = stop_running(&package(&[demo]), &unwritable, &linux(), &manager).unwrap_err();
412 assert!(err.contains(RESTART_MARKER), "{err}");
413 assert!(!manager.calls().commands.contains(&command(&[
414 "systemctl",
415 "--user",
416 "stop",
417 "demo"
418 ])));
419 }
420
421 #[test]
422 fn uninstall_all_uninstalls_each_service() {
423 let manager = RecordingManager::new();
424 uninstall_all(&package(&[demo, idle]), &linux(), &manager);
425 assert_eq!(
426 manager.calls().removed,
427 [unit_path("demo", &home()), unit_path("idle", &home())]
428 );
429 }
430 }
431}