// This module defines unit settings parsing logig // // NOTE: ON UNIT PARSING LOGIC // when parsing a unit in /etc/vigil/units/ we shoul ignore // ANY broken/bad constructed toml unit configuration file // without "crashing" logic. // // NOTE: ON GENERAL SERVICE LOGGING // when vigil starts service, it should send out logs to vigil? // or logs in such init systems are just taking the stdout+stderr // of service and showing its output? idk 4 now, look into how its supposed to be use crate::{log_success, log_warning, RUNLEVEL_STATE}; use serde::Deserialize; use std::{ fs::{read_dir, read_to_string}, process::Child, }; /* [info] name = "name" description = "description" version = "version" [config] exec = "path to exec" runlevel = enum Runlevel restart=always|never|on-failure */ #[derive(Deserialize, PartialEq, Clone)] enum Restart { #[serde(alias = "always")] Always, #[serde(alias = "never")] Never, #[serde(alias = "on-failure")] OnFailure, } #[derive(Deserialize, PartialEq, Eq, Clone, Copy, Debug)] pub enum Runlevel { /// The system is shutting down, runlevel int: 0 #[serde(alias = "shutdown")] Shutdown, /// One-user system debug-mode, runlevel int: 1 #[serde(alias = "one-user")] OneUser, /// Multi-user CLI (TTY) with no network, runlevel int: 2 #[serde(alias = "multiuser-no-network")] MultiNoNetwork, /// Multi-user CLI with network, runlevel int: 3 #[serde(alias = "multiuser-network")] MultiNetwork, /// Multi-user mode with GUI, runlevel int: 5 #[serde(alias = "multiuser-network-gui")] MultiGUINetwork, /// Runlevel is not critical for running the service, runlevel int: 4 #[serde(alias = "undefined")] Undefined, /// System going too reboot, runlevel int: 6 #[serde(alias = "reboot")] Reboot, /// System going to halt #[serde(alias = "halt")] Halt, } #[allow(dead_code)] #[derive(Deserialize)] pub struct ServiceInfo { /// Service name name: String, /// Service description description: String, /// Service version version: String, } #[allow(dead_code)] #[derive(Deserialize)] pub struct ServiceConfig { /// Execution command, like ExecStart in sysd exec: String, /// Runlevel, like after=*.target runlevel: Runlevel, /// Restart service: Always|Never restart: Restart, } /// Main Unit-file struct #[allow(dead_code)] #[derive(Deserialize)] pub struct Unit { info: ServiceInfo, config: ServiceConfig, } /* impl Restart { pub fn as_str(&self) -> &'static str { match self { Restart::Always => "always", Restart::Never => "never", } } } */ /// Function: /// 1. starting services declared in `/etc/vigil/units/` /// 2. checks services for dropping and if ts true to restarting it max restart count: 3 /// /// Based on the global runlevel variable, should be runned as second thread // TODO: More logs && better errors processing && fix clippy warnings pub fn services_mainloop() -> Result<(), Box> { let unit_list = parse_all_units()?; let mut pids: Vec<(Child, String, Restart, u8)> = vec![]; let mut last_runlevel = Runlevel::Undefined; loop { let current_runlevel = { let guard = match RUNLEVEL_STATE.lock() { Ok(g) => g, Err(poisoned) => { log_warning("Mutex was poisoned. Recovering"); poisoned.into_inner() } }; *guard }; if current_runlevel != last_runlevel { log_success(&format!( "Runlevel changed: {:?} -> {:?}", last_runlevel, current_runlevel )); // Stops other runlevel services pids.retain_mut(|(child, exec, _, _)| { // TODO: Correct stop with SIGTERM + timeout match child.try_wait() { Ok(Some(_)) => { log_success(&format!("Service exited: {}", exec)); false } _ => true, } }); for unit in &unit_list { if unit.config.runlevel == current_runlevel { match std::process::Command::new(&unit.config.exec).spawn() { Ok(child) => { pids.push(( child, unit.config.exec.clone(), unit.config.restart.clone(), 0, )); log_success(&format!("Started: {}", unit.config.exec)); } Err(e) => { log_warning(&format!("Failed to start {}: {}", unit.config.exec, e)); } } } } last_runlevel = current_runlevel; if matches!(current_runlevel, Runlevel::Shutdown | Runlevel::Halt) { break Ok(()); } } pids.retain_mut(|entry| { let (child, exec, restart_policy, restart_count) = entry; match child.try_wait() { Ok(Some(status)) => { log_warning(&format!("Service exited: {} (status: {:?})", exec, status)); let should_restart = match *restart_policy { Restart::Always => true, Restart::OnFailure => !status.success(), Restart::Never => false, }; if should_restart { if *restart_count >= 3 { log_warning(&format!( "Maximum restart attempts (3) reached for {}, giving up", exec )); return false; } match std::process::Command::new(exec.as_str()).spawn() { Ok(new_child) => { *child = new_child; *restart_count += 1; log_success(&format!( "Restarted {} (attempt {})", exec, restart_count )); true } Err(e) => { log_warning(&format!("Failed to restart {}: {}", exec, e)); false } } } else { false } } Ok(None) => true, Err(e) => { log_warning(&format!("Failed to check status of {}: {}", exec, e)); false } } }); } } /* for i in 0..pids.len() { match pids[i].0.try_wait() { Ok(Some(status)) => { if pids[i].2 == Restart::Always && pids[i].3 < 3 && !status.success() { let new_child = std::process::Command::new(pids[i].1.clone()) .spawn() .map_err(|e| Box::new(e) as Box)?; log_warning(&format!( "One of units dropped with Error {};\n Restarting it. Attempt: {}", status, pids[i].3 + 1 )); pids.push(( new_child, pids[i].1.clone(), pids[i].2.clone(), pids[i].3 + 1, )); pids.remove(i); } else { log_warning(&format!( "Unit {} has reached maximum restart attempts or restart policy is not Always, not restarting.", pids[i].1 )); continue; } } Ok(None) => continue, Err(e) => log_warning(&format!("Error occurred while checking services: {}", e)), } } std::thread::sleep(Duration::from_secs(15)); } } */ fn parse_all_units() -> Result, Box> { let mut units: Vec = Vec::new(); for unit in read_dir("/etc/vigil/units") .map_err(|e| Box::new(e) as Box)? { let unit_path = unit .map_err(|e| Box::new(e) as Box)? .path(); let unit_str: String = match read_to_string(unit_path) { Ok(content) => content, Err(e) => { log_warning(&format!("Error while reading unit: {}", e)); continue; } }; let deserialized: Result = toml::from_str(&unit_str); match deserialized { Ok(unit) => { units.push(unit); } Err(e) => { log_warning(&format!("Error while parsing unit: {}", e)); continue; } }; } Ok(units) }