use custom_error::custom_error; use std::borrow::Cow; use std::error::Error as stdError; use std::io::{BufRead, BufReader, Lines}; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; custom_error! {UnitdStderrParseError VersionNotFound = "Version string output not found", BuildSettingsNotFound = "Build settings not found" } #[derive(Debug, Clone)] pub struct UnitdConfigureOptions { pub version: Cow<'static, str>, pub all_flags: Cow<'static, str>, } impl UnitdConfigureOptions { pub fn new(unitd_path: &Path) -> Result> { fn parse_configure_settings_from_unitd_stderr_output( lines: &mut Lines, ) -> Result> { const VERSION_PREFIX: &str = "unit version: "; const CONFIGURED_AS_PREFIX: &str = "configured as "; const CONFIGURE_PREFIX: &str = "configured as ./configure "; fn aggregate_parsable_lines( mut accum: (Option, Option), line: String, ) -> (Option, Option) { if line.starts_with(VERSION_PREFIX) { accum.0 = line.strip_prefix(VERSION_PREFIX).map(|l| l.to_string()); } else if line.starts_with(CONFIGURED_AS_PREFIX) { accum.1 = line.strip_prefix(CONFIGURE_PREFIX).map(|l| l.to_string()); } accum } let options_lines = lines .filter_map(|line| line.ok()) .fold((None, None), aggregate_parsable_lines); if options_lines.0.is_none() { return Err(Box::new(UnitdStderrParseError::VersionNotFound) as Box); } else if options_lines.1.is_none() { return Err(Box::new(UnitdStderrParseError::BuildSettingsNotFound) as Box); } Ok(UnitdConfigureOptions { version: options_lines.0.unwrap().into(), all_flags: options_lines.1.unwrap().into(), }) } let program = unitd_path.as_os_str(); let child = Command::new(program) .arg("--version") .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn()?; let output = child.wait_with_output()?; let err = BufReader::new(&*output.stderr); parse_configure_settings_from_unitd_stderr_output(&mut err.lines()) } pub fn has_flag(&self, flag_name: &str) -> bool { self.all_flags .split_ascii_whitespace() .any(|flag| flag.starts_with(format!("--{}", flag_name).as_str())) } pub fn get_flag_value(&self, flag_name: &str) -> Option { self.all_flags .split_ascii_whitespace() .find(|flag| flag.starts_with(format!("--{}", flag_name).as_str())) .and_then(|flag| { let parts: Vec<&str> = flag.split('=').collect(); if parts.len() >= 2 { Some(parts[1].to_owned()) } else { None } }) } pub fn debug_enabled(&self) -> bool { self.has_flag("debug") } pub fn openssl_enabled(&self) -> bool { self.has_flag("openssl") } pub fn prefix_path(&self) -> Option> { self.get_flag_value("prefix") .map(PathBuf::from) .map(PathBuf::into_boxed_path) } fn join_to_prefix_path(&self, sub_path: S) -> Option> where S: Into, { self.prefix_path() .map(|path| path.join(sub_path.into()).into_boxed_path()) } pub fn default_control_api_socket_address(&self) -> Option { // If the socket address is specific configured in the configure options, we use // that. Otherwise, we use the default path as assumed to be unix:$prefix/control.unit.sock. match self.get_flag_value("control") { Some(socket_address) => Some(socket_address), None => { // Give up if the unitd is compiled with unix sockets disabled if self.has_flag("no-unix-sockets") { return None; } let socket_path = self.join_to_prefix_path("control.unit.sock"); socket_path.map(|path| format!("unix:{}", path.to_string_lossy())) } } } pub fn default_pid_path(&self) -> Option> { match self.get_flag_value("pid") { Some(pid_path) => self.join_to_prefix_path(pid_path), None => self.join_to_prefix_path("unit.pid"), } } pub fn default_log_path(&self) -> Option> { match self.get_flag_value("log") { Some(pid_path) => self.join_to_prefix_path(pid_path), None => self.join_to_prefix_path("unit.log"), } } pub fn default_modules_directory(&self) -> Option> { match self.get_flag_value("modules") { Some(modules_dir_name) => self.join_to_prefix_path(modules_dir_name), None => self.join_to_prefix_path("modules"), } } pub fn default_state_directory(&self) -> Option> { match self.get_flag_value("state") { Some(state_dir_name) => self.join_to_prefix_path(state_dir_name), None => self.join_to_prefix_path("state"), } } pub fn default_tmp_directory(&self) -> Option> { match self.get_flag_value("tmp") { Some(tmp_dir_name) => self.join_to_prefix_path(tmp_dir_name), None => self.join_to_prefix_path("tmp"), } } pub fn default_user(&self) -> Option { self.get_flag_value("user").map(String::from) } pub fn default_group(&self) -> Option { self.get_flag_value("group").map(String::from) } } #[cfg(test)] mod tests { use super::*; use crate::unitd_instance; use crate::unitd_instance::UNITD_PATH_ENV_KEY; #[test] fn can_detect_key() { let options = UnitdConfigureOptions { version: Default::default(), all_flags: Cow::from("--debug --openssl --prefix=/opt/unit"), }; assert!(options.has_flag("debug")); assert!(options.has_flag("openssl")); assert!(options.has_flag("prefix")); assert!(!options.has_flag("fobar")); } #[test] fn can_get_flag_value_by_key() { let expected = "/opt/unit"; let options = UnitdConfigureOptions { version: Default::default(), all_flags: Cow::from("--debug --openssl --prefix=/opt/unit"), }; let actual = options.get_flag_value("prefix"); assert_eq!(expected, actual.unwrap()) } #[test] fn can_get_prefix_path() { let expected: Box = Path::new("/opt/unit").into(); let options = UnitdConfigureOptions { version: Default::default(), all_flags: Cow::from("--debug --openssl --prefix=/opt/unit"), }; let actual = options.prefix_path(); assert_eq!(expected, actual.unwrap()) } #[test] fn can_parse_complicated_configure_options() { let expected: Box = Path::new("/usr").into(); let options = UnitdConfigureOptions { version: Default::default(), all_flags: Cow::from("--prefix=/usr --state=/var/lib/unit --control=unix:/var/run/control.unit.sock --pid=/var/run/unit.pid --log=/var/log/unit.log --tmp=/var/tmp --user=unit --group=unit --tests --openssl --modules=/usr/lib/unit/modules --libdir=/usr/lib/x86_64-linux-gnu --cc-opt='-g -O2 -fdebug-prefix-map=/data/builder/debuild/unit-1.28.0/pkg/deb/debuild/unit-1.28.0=. -specs=/usr/share/dpkg/no-pie-compile.specs -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC' --ld-opt='-Wl,-Bsymbolic-functions -specs=/usr/share/dpkg/no-pie-link.specs -Wl,-z,relro -Wl,-z,now -Wl,--as-needed -pie' "), }; let actual = options.prefix_path(); assert_eq!(expected, actual.unwrap()) } #[test] #[ignore] // run this one manually - not in CI fn can_run_unitd() { let specific_path = std::env::var(UNITD_PATH_ENV_KEY).map_err(|error| Box::new(error) as Box); let unitd_path = unitd_instance::find_executable_path(specific_path); let config_options = UnitdConfigureOptions::new(&unitd_path.unwrap()); match config_options { Ok(options) => { println!("{:?}", options) } Err(error) => panic!("{}", error), }; } }