use log::{debug, error}; use super::status::StatusResult; use super::{Result, ServiceManager, ServiceManagerError}; use std::os::unix::fs::PermissionsExt; use std::{ fs, io, process::{Command, ExitStatus, Stdio}, }; static SYSTEMCTL: &str = "systemctl"; // TODO(ppacher): add support for kdesudo and gksudo enum SudoCommand { Pkexec, Gksu, } impl From<std::process::Output> for ServiceManagerError { fn from(output: std::process::Output) -> Self { let msg = String::from_utf8(output.stderr) .ok() .filter(|s| !s.trim().is_empty()) .or_else(|| { String::from_utf8(output.stdout) .ok() .filter(|s| !s.trim().is_empty()) }) .unwrap_or_else(|| format!("Failed to run `systemctl`")); ServiceManagerError::Other(output.status, msg) } } /// System Service manager implementation for Linux based distros. pub struct SystemdServiceManager {} impl SystemdServiceManager { /// Checks if systemctl is available in /sbin/ /bin, /usr/bin or /usr/sbin. /// /// Note that we explicitly check those paths to avoid returning true in case /// there's a systemctl binary in the cwd and PATH includes . since this may /// pose a security risk of running an untrusted binary with root privileges. pub fn is_installed() -> bool { let paths = vec![ "/sbin/systemctl", "/bin/systemctl", "/usr/sbin/systemctl", "/usr/bin/systemctl", ]; for path in paths { debug!("checking for systemctl at path {}", path); match fs::metadata(path) { Ok(md) => { debug!("found systemctl at path {} ", path); if md.is_file() && md.permissions().mode() & 0o111 != 0 { return true; } error!( "systemctl binary found but invalid permissions: {}", md.permissions().mode().to_string() ); } Err(err) => { error!( "failed to check systemctl binary at {}: {}", path, err.to_string() ); continue; } }; } error!("failed to find systemctl binary"); false } } impl ServiceManager for SystemdServiceManager { fn status(&self) -> super::Result<StatusResult> { let name = "portmaster.service"; let result = systemctl("is-active", name, false); match result { // If `systemctl is-active` returns without an error code and stdout matches "active" (just to guard againt // unhandled cases), the service can be considered running. Ok(stdout) => { let mut copy = stdout.to_owned(); trim_newline(&mut copy); if copy != "active" { // make sure the output is as we expected Err(ServiceManagerError::Other(ExitStatus::default(), stdout)) } else { Ok(StatusResult::Running) } } Err(e) => { if let ServiceManagerError::Other(_err, ref output) = e { let mut copy = output.to_owned(); trim_newline(&mut copy); if copy == "inactive" { return Ok(StatusResult::Stopped); } } else { error!("failed to run 'systemctl is-active': {}", e.to_string()); } // Failed to check if the unit is running match systemctl("cat", name, false) { // "systemctl cat" seems to no have stable exit codes so we need // to check the output if it looks like "No files found for yyyy.service" // At least, the exit code are not documented for systemd v255 (newest at the time of writing) Err(ServiceManagerError::Other(status, msg)) => { if msg.contains("No files found for") { Ok(StatusResult::NotFound) } else { Err(ServiceManagerError::Other(status, msg)) } } // Any other error type means something went completely wrong while running systemctl altogether. Err(e) => Err(e), // Fine, systemctl cat worked so if the output is "inactive" we know the service is installed // but stopped. Ok(_) => { // Unit seems to be installed so check the output of result let mut stderr = e.to_string(); trim_newline(&mut stderr); if stderr == "inactive" { Ok(StatusResult::Stopped) } else { Err(e) } } } } } } fn start(&self) -> Result<StatusResult> { let name = "portmaster.service"; // This time we need to run as root through pkexec or similar binaries like kdesudo/gksudo. systemctl("start", name, true)?; // Check the status again to be sure it's started now self.status() } } fn systemctl( cmd: &str, unit: &str, run_as_root: bool, ) -> std::result::Result<String, ServiceManagerError> { let output = run(run_as_root, SYSTEMCTL, vec![cmd, unit])?; // The command have been able to run (i.e. has been spawned and executed by the kernel). // We now need to check the exit code and "stdout/stderr" output in case of an error. if output.status.success() { Ok(String::from_utf8(output.stdout)?) } else { Err(output.into()) } } fn run<'a>(root: bool, cmd: &'a str, args: Vec<&'a str>) -> std::io::Result<std::process::Output> { // clone the args vector so we can insert the actual command in case we're running // through pkexec or friends. This is just callled a couple of times on start-up // so cloning the vector does not add any mentionable performance impact here and it's better // than expecting a mutalble vector in the first place. let mut args = args.to_vec(); let mut command = match root { true => { // if we run through pkexec and friends we need to append cmd as the second argument. args.insert(0, cmd); match get_sudo_cmd() { Ok(cmd) => { match cmd { SudoCommand::Pkexec => { // disable the internal text-based prompt agent from pkexec because it won't work anyway. args.insert(0, "--disable-internal-agent"); Command::new("/usr/bin/pkexec") } SudoCommand::Gksu => { args.insert(0, "--message=Please enter your password:"); args.insert(1, "--sudo-mode"); Command::new("/usr/bin/gksudo") } } } Err(err) => return Err(err), } } false => Command::new(cmd), }; command.env("LC_ALL", "C"); command .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); command.args(args).output() } fn trim_newline(s: &mut String) { if s.ends_with('\n') { s.pop(); if s.ends_with('\r') { s.pop(); } } } fn get_sudo_cmd() -> std::result::Result<SudoCommand, std::io::Error> { if let Ok(_) = fs::metadata("/usr/bin/pkexec") { return Ok(SudoCommand::Pkexec); } if let Ok(_) = fs::metadata("/usr/bin/gksudo") { return Ok(SudoCommand::Gksu); } Err(std::io::Error::new( io::ErrorKind::NotFound, "failed to detect sudo command", )) }