diff --git a/Cargo.toml b/Cargo.toml index 46c1126..59ee252 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,43 +3,46 @@ name = "whenever" description = "Whenever Automation Tool" readme = "README.md" license = "LGPL-2.1-or-later" -version = "1.3.1" +version = "1.3.2" authors = ["Francesco Garosi "] repository = "https://github.com/almostearthling/whenever/" edition = "2024" + [dependencies] -time = "0.3" +async-std = "1.12" +async-trait = "0.1" +bstr = "1.12" +cfgmap = { version = "0.4", features = ["from_toml"] } chrono = "0.4" -rand = "0.10" -regex = "1.7" -log = "0.4" -flexi_logger = "0.31" -nu-ansi-term = "0.50" +clap = { version = "4.1", features = ["derive"] } clokwerk = "0.4" +ctrlc = { version = "3.5", features = ["termination"] } +flexi_logger = "0.31" +futures = "0.3" +itertools = "0.15" +lazy_static = "1.5" listenfd = "1.0" -toml = "1.0" +log = "0.4" +minreq = { version = "3.0", features = ["https-native-tls"], optional = true } +nu-ansi-term = "0.50" +notify = "8.1" +parking_lot = "0.12" +rand = "0.10" +regex = "1.7" serde = "1.0" -lazy_static = "1.4" -subprocess = "1.1" -ctrlc = { version = "3.5", features = ["termination"] } -cfgmap = { version = "0.4", features = ["from_toml", "from_json"] } -clap = { version = "4.1", features = ["derive"] } -mlua = { version = "0.11", features = ["lua54", "vendored"] } -unique_id = "0.1" serde_json = "1.0" single-instance = "0.3" -notify = "8.1" -async-std = "1.12" -zbus = { version = "5.9", optional = true } -whoami = "2.1" -itertools = "0.15" -futures = "0.3" -async-trait = "0.1" +subprocess = "1.1" system-idle-time = "1.0" -parking_lot = "0.12" -bstr = "1.12" -minreq = { version = "3.0", features = ["https-native-tls"], optional = true } +time = "0.3" +toml = "1.0" +unique_id = "0.1" +whoami = "2.1" +zbus = { version = "5.17", optional = true } + +# the "lua54" feature can be modified to use a different Lua version +mlua = { version = "0.12", features = ["lua54", "vendored"] } [target.'cfg(windows)'.dependencies] wmi = { version = "0.18", optional = true } @@ -65,9 +68,10 @@ winresource = "0.1" # - cargo build --release --features windows_std (Windows) # # as the default configuration does not include any option. + [features] default = [] -# default = ["wmi", "dbus", "lua_sync", "lua_httpreq"] +# default = ["check_std"] dbus = ["dep:zbus"] wmi = ["dep:wmi"] lua_sync = [] @@ -80,10 +84,10 @@ check_std = ["wmi", "dbus", "lua_sync", "lua_httpreq"] # make the executable memory footprint as small as possible for release [profile.release] -strip = true # strip executable -opt-level = "z" # optimize for size +strip = true # strip executable +opt-level = "z" # optimize for size lto = true -panic = 'abort' # remove garbage strings from executable +panic = 'abort' # remove garbage strings from executable # end diff --git a/docs/65.lua.rst b/docs/65.lua.rst index f633e60..0434141 100644 --- a/docs/65.lua.rst +++ b/docs/65.lua.rst @@ -11,6 +11,11 @@ is running, it only needs to be initialized and provided the script that, in tur loaded at startup. Tasks (or checks) that would last even seconds, can be performed in a small fraction of the time. +.. note:: + The default *Lua* interpreter is *Lua v5.4*: this is intentional, as this version can be + considered stable and well supported. However the interpreter version can be changed at build + time by modifying the ``"lua54"`` entry in *Cargo.toml*. + The embedded interpreter makes the standard *Lua* library available for scripts, which includes: * string manipulation @@ -306,7 +311,7 @@ script from within **whenever**: for more complex needs, external commands can b anyway. .. note:: - The *http* module is only available when the ``lua_httpreq`` feature is enabled; however, + The *http* module is only available when the ``lua_httpreq`` feature is enabled; however, both the standard configurations and the provided binaries come with the feature enabled. diff --git a/docs/conf.py b/docs/conf.py index 501538f..cb38c17 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,7 +9,7 @@ project = "whenever" author = "Francesco Garosi" copyright = "2023-%Y, Francesco Garosi" -release = "1.3.1" +release = "1.3.2" html_logo = "graphics/metronome.png" diff --git a/src/cfghelp.rs b/src/cfghelp.rs index 58bbd14..c02d124 100644 --- a/src/cfghelp.rs +++ b/src/cfghelp.rs @@ -8,7 +8,7 @@ use cfgmap::CfgMap; use regex::Regex; -use crate::common::wres::{Error, Kind, Result}; +use crate::utility::result::{Error, Kind, Result}; use crate::constants::*; /// use this to specify that a configuration element is mandatory diff --git a/src/common.rs b/src/common.rs index 995a5a3..942fbaf 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,358 +1,4 @@ //! Common modules and other globally available items -//! -//! The common logging system is a simplified version of what is available in -//! the `log` crate, and all logging functions shall use this common module. -//! -//! Some notes on logging: -//! -//! * The log messages are composed by -//! - the timestamp -//! - the application name (see below) in brackets -//! - the log level -//! - the log message -//! * The log message in turn has the following form: -//! `context: [MSGTYPE] human readable message` -//! where -//! - the context is usually constructed with two space-separated strings -//! indicating the part of the program where a certain message is issued -//! - MSGTYPE (in square brackets) consists of two or more alphanumeric -//! strings, separated by slashes, whose first two are described below -//! and the further ones may depend on the first two -//! - the human readable message is an explanation of what happened. -//! -//! The first two elements in MSGTYPE indicate in which point of an operation -//! the event occurs, and the type of event. The first element can be one of: -//! -//! * _INIT_ if the event occurs in an initialization phase -//! * _START_ if the event occurs while starting something, service or process -//! * _PROC_ if the event occurs while processing or during some activity -//! * _END_ if the event occurs at the end of a service or process -//! * _HIST_ is a _trace level only_ message emitted to show history on GUI: -//! in this case _MSG_ is sent at the beginning of task execution, and -//! _OK_, _FAIL_ or _IND_ are sent at the end (resp. on success, -//! failure or _indeterminate_ outcome) -//! * _BUSY_ is also a _trace level only_ message emitted to allow a GUI to -//! show the application status (for instance using an icon in the -//! tray area): when there are one or more conditions busy, the second -//! element is _YES_, otherwise _NO_ -//! * _PAUSE_ another _trace level only_ message emitted to allow a GUI to -//! change application status (for instance using a tray icon) when -//! the scheduler is paused: useful because an _internal command_ -//! based task might pause the scheduler unattendedly -//! -//! while the second can be one of: -//! -//! * _OK_ for expected outcomes or behaviours -//! * _FAIL_ for unexpected outcomes or behaviours -//! * _IND_ for indeterminate outcomes -//! * _MSG_ if the human-readable part is exclusively informational -//! * _ERR_ (may be followed by a dash `-` and a code) for errors to be -//! notified -//! * _YES_ (only occurs for _BUSY_ or _PAUSE_ indicators) means: application -//! is busy or has been paused -//! * _NO_ (only occurs for _BUSY_ or _PAUSE_ indicators) means: application -//! is _not_ busy or has been resumed -//! -//! This should help using the log as a way of communicating to a wrapper -//! utility the state of the scheduler, and possibily give the opportunity to -//! organize communication to the user in a friendlier way. -//! -//! This module also contains common enums, traits, structs, and functions -//! shared between items that use the same technology. Shared collections are -//! organized in modules: -//! -//! * `cmditem` for assets common to command based tasks and conditions -//! * `luaitem` for assets common to Lua based tasks and conditions -//! * `dbusitem` for assets common to DBus based conditions and events -//! * `wmiitem` for assets common to WMI based conditions and events -//! * `wres` for the _whenever_ specific `Result`, that has automations -//! for conversion from many other result types -//! -//! in order to avoid behaviour discrepancies, and possibly to save some -//! memory by avoiding unnecessary duplications. - -use lazy_static::lazy_static; -use parking_lot::RwLock; - -// the following global flag is exposed here because it looks like there is -// no actual way to pass anything but a string as payload to the logger, so -// the common logging function should know whether the logger is initialized -// to return JSON message and build the JSON payload itself -lazy_static! { - static ref LOGGER_EMITS_JSON: RwLock = RwLock::new(false); -} - -#[allow(dead_code)] -/// Module for logging -/// -/// Exposes (publicly): -/// -/// * a function to universally log (`log`) -/// * a logger initialization function (`init`) -/// * the logging levels: _trace_ < _debug_ < _info_ < _warn_ < _error_, -/// provided as the `LogType` enumeration -pub mod logging { - use crate::constants::{APP_NAME, ERR_LOGGER_NOT_INITIALIZED}; - use flexi_logger::{DeferredNow, FileSpec, Logger, style}; - use log::Record; - use log::{debug, error, info, trace, warn}; - use nu_ansi_term::Style; - use serde_json::json; - use std::path::PathBuf; - - use super::LOGGER_EMITS_JSON; - - // time stamp format that is used by the provided format functions. - const NOW_FMT: &str = "%Y-%m-%dT%H:%M:%S%.3f"; - const NOW_FMT_FULL: &str = "%Y-%m-%dT%H:%M:%S%.6f"; - - // log formatters - fn log_format_plain( - w: &mut dyn std::io::Write, - now: &mut DeferredNow, - record: &Record, - ) -> Result<(), std::io::Error> { - write!( - w, - "[{}] ({APP_NAME}) {} {}", - now.format(NOW_FMT), - format_args!("{:5}", record.level()), - &record.args(), - ) - } - - fn log_format_json( - w: &mut dyn std::io::Write, - now: &mut DeferredNow, - record: &Record, - ) -> Result<(), std::io::Error> { - let header = json!({ - "application": APP_NAME, - "time": now.format(NOW_FMT_FULL).to_string(), - "level": record.level().to_string(), - }); - let payload = record.args(); - write!(w, "{{\"header\":{header},\"contents\":{payload}}}") - } - - fn log_format_colors( - w: &mut dyn std::io::Write, - now: &mut DeferredNow, - record: &Record, - ) -> Result<(), std::io::Error> { - let level = record.level(); - let bold = Style::new().bold(); - let dimmed = Style::new().dimmed(); - write!( - w, - "[{}] {} {} {}", - format_args!("{}", now.format(NOW_FMT)), - dimmed.paint(format!("({APP_NAME})")), - style(level).paint(format!("{:5}", level.to_string())), - bold.paint(record.args().to_string()), - ) - } - - /// Log levels (from most verbose to least) - pub enum LogType { - Trace, - Debug, - Info, - Warn, - Error, - } - - /// Logger initialization: if `filename` is not given, the log will be - /// sent to stdout and use color (and the `append` parameter will be - /// ignored); otherwise `filename` will be used as path for the log file: - /// causes an error if it's not possible to open the log file. - pub fn init( - level: LogType, - filename: Option, - append: bool, - logcolor: bool, // these three values are mutually - logplain: bool, // exclusive by construction of the - logjson: bool, // main `clap` parser - ) -> std::io::Result { - let level = match level { - LogType::Trace => "trace", - LogType::Debug => "debug", - LogType::Info => "info", - LogType::Warn => "warn", - LogType::Error => "error", - }; - - // the following line is to avoid other crates logging (e.g. `zbus`) - // so it can be commented out for debugging purposes and replaced with - // the subsequent commented out line. A reminder to documentation: - // https://docs.rs/flexi_logger/latest/flexi_logger/struct.LogSpecification.html - // FIXME: maybe we can choose the actual configuration string - // automatically according to the current build settings? - let logspec = format!("whenever={level}"); - // let logspec = format!("{level}"); - - let mut logger; - logger = Logger::try_with_str(logspec); - match logger { - Ok(l) => { - if let Some(fname) = filename { - let log_format = { - if logcolor { - log_format_plain - } else if logplain { - log_format_plain - } else if logjson { - *LOGGER_EMITS_JSON.write() = true; - log_format_json - } else { - log_format_plain - } - }; - let mut pb = PathBuf::from(&fname); - if pb.parent().is_none() - || pb.parent().unwrap().to_str().unwrap_or("").is_empty() - { - pb = { - let mut dir = PathBuf::from("."); - dir.push(pb); - dir - } - } - let fspec = FileSpec::try_from(&pb) - .map_err(|e| std::io::Error::other(e.to_string()))?; - logger = Ok( - l.log_to_file(fspec).format_for_files(log_format), // .write_mode(WriteMode::BufferAndFlush) - ); - if append { - logger = Ok(logger.unwrap().append()); - } - } else { - let log_format = { - if logcolor { - log_format_colors - } else if logplain { - log_format_plain - } else if logjson { - *LOGGER_EMITS_JSON.write() = true; - log_format_json - } else { - log_format_colors - } - }; - // in json mode to console we also support capture by pipes - // so that wrappers may use stdout to get updates; it also - // redirects the logger's own errors to a black hole in - // order to avoid polluting a wrapper - if logjson { - logger = Ok(l - .format_for_stdout(log_format) - .write_mode(flexi_logger::WriteMode::Direct) - .error_channel(flexi_logger::ErrorChannel::DevNull) - .log_to_stdout()); - } else { - logger = Ok(l.format_for_stdout(log_format).log_to_stdout()); - } - } - } - _ => { - return Err(std::io::Error::new( - std::io::ErrorKind::Unsupported, - ERR_LOGGER_NOT_INITIALIZED, - )); - } - } - if let Err(_e) = logger.unwrap().start() { - return Err(std::io::Error::new( - std::io::ErrorKind::Unsupported, - ERR_LOGGER_NOT_INITIALIZED, - )); - } - - Ok(true) - } - - /// Common log function. The parameters are granular in order to achieve - /// two benefits: the first is that for most of them a constant can be - /// used, thus reducing the possibility of non-conformant log messages - /// (which may arise on typos) and, to some extent, the executable size; - /// the second is that JSON log messages can be as fine-grained as - /// needed. The constants to be used are defined in _constants.rs_, and - /// in particular: - /// - /// * `emitter` is one of the `LOG_EMITTER_...` constants - /// * `action` is one of the `LOG_ACTION_...` constants - /// * `when` is one of the `LOG_WHEN_...` constants - /// * `status` is one of the `LOG_STATUS_...` constants - /// - /// while non-constant parameters must be defined as follows - /// - /// * `item` can be a tuple consisting of item _name_ and _id_ - /// * `message` is the only arbitrary string that can be passed - /// - /// This allows JSON messages to be easily interpretable by a wrapper - /// according to the hints given in the documentation. - pub fn log( - severity: LogType, - emitter: &str, - action: &str, - item: Option<(&str, i64)>, - when: &str, - status: &str, - message: &str, - ) { - let payload = if *LOGGER_EMITS_JSON.read() { - let context = if let Some((item, item_id)) = item { - json!({ - "emitter": emitter, - "action": action, - "item": item, - "item_id": item_id, - }) - } else { - json!({ - "emitter": emitter, - "action": action, - "item": null, - "item_id": null, - }) - }; - let message_type = json!({ - "when": when, - "status": status, - }); - json!({ - "context": context, - "message_type": message_type, - "message": message, - }) - .to_string() - } else { - let item_repr = if let Some((name, id)) = item { - format!(" {name}/{id}") - } else { - String::new() - }; - format!("{emitter} {action}{item_repr}: [{when}/{status}] {message}") - }; - match severity { - LogType::Trace => { - trace!("{payload}") - } - LogType::Debug => { - debug!("{payload}") - } - LogType::Info => { - info!("{payload}") - } - LogType::Warn => { - warn!("{payload}") - } - LogType::Error => { - error!("{payload}") - } - } - } -} #[allow(dead_code)] /// This module helps command based items perform common activities @@ -360,8 +6,8 @@ pub mod cmditem { use std::time::{Duration, SystemTime}; use subprocess::{Exec, ExitStatus}; - use crate::LogType; use crate::constants::*; + use crate::utility::logging::LogType; /// In case of failure, the reason will be one of the provided values #[derive(Debug, PartialEq)] @@ -1186,233 +832,6 @@ pub mod cmditem { } } -#[cfg(feature = "lua_sync")] -#[allow(dead_code)] -/// A module providing named mutexes that can be shared across all threads. -/// -/// This module implements a global set of named mutexes that can be locked -/// and released by any thread. -pub mod named_mutex { - // NOTE: originally this module was AI generated, however the poor guy - // kept making mistakes of all sorts, from completely reinventing common - // (or standard) libraries API, to writing failing tests, and so on. In - // the end, however, it made me discover some interesting libraries and - // rethink on how to implement the functionality by hand. Now the module - // is hand coded (apart from doc comments) - use lazy_static::lazy_static; - use parking_lot::{Condvar, Mutex}; - use std::collections::HashMap; - use std::sync::Arc; - use std::time::Duration; - - // global map of named mutexes - #[derive(Debug)] - struct SharedLock { - busy: Mutex, - notifier: Condvar, - } - - #[allow(dead_code)] - impl SharedLock { - pub fn new_free() -> Self { - SharedLock { - busy: Mutex::new(false), - notifier: Condvar::new(), - } - } - - // busy by default - pub fn new() -> Self { - SharedLock { - busy: Mutex::new(true), - notifier: Condvar::new(), - } - } - - // this reclaims a named mutex, possibly with a timeout: if able to - // capture it, then it changes its busy state to true and returns - // true to signal that it succeeded - pub fn claim(&self, timeout: Option) -> bool { - let mut busy = self.busy.lock(); - if *busy { - if let Some(timeout) = timeout { - if self.notifier.wait_for(&mut busy, timeout).timed_out() { - false - } else { - *busy = true; - true - } - } else { - self.notifier.wait(&mut busy); - *busy = true; - true - } - } else { - *busy = true; - true - } - } - - // free the mutex and signal the next waiting thread that it can go on; - // this fails only if there was nothing to release - pub fn free(&self) -> bool { - let mut busy = self.busy.lock(); - if *busy { - *busy = false; - self.notifier.notify_one(); - true - } else { - false - } - } - } - - lazy_static! { - static ref NMUTEX_MAP: Arc>>> = - Arc::new(Mutex::new(HashMap::new())); - } - - // add or retrieve a lock - fn get_slock(name: &str) -> Arc { - let map = NMUTEX_MAP.clone(); - let mut map = map.lock(); - let s1 = map - .entry(name.to_string()) - .or_insert_with(|| Arc::new(SharedLock::new_free())); - s1.clone() - } - - // the actual library, as per specification - - /// Attempts to acquire and lock a named mutex. - /// - /// If a mutex with the given `name` doesn't exist, it creates one and - /// locks it immediately. /// If a mutex with the given `name` exists, - /// it attempts to lock it. - /// - /// # Arguments - /// - /// * `name` - The name of the mutex to lock - /// * `timeout` - Maximum time to wait for the lock. - /// - `None`: Wait indefinitely - /// - `Some(duration)`: Wait for the specified duration - /// - /// # Returns - /// - /// Returns `true` if the mutex was successfully locked, `false` if the - /// timeout was exceeded. - /// - /// # Examples - /// - /// ```ignore - /// if namedmutex_lock("Mux01", None) { - /// println!("locked!"); - /// std::thread::sleep(std::time::Duration::from_millis(500)); - /// let _ = namedmutex_release("Mux01"); - /// } - /// ``` - /// - /// ```ignore - /// if !namedmutex_lock("Mux01", Some(Duration::from_millis(1000))) { - /// println!("could not lock the mutex"); - /// } - /// ``` - pub fn namedmutex_lock(name: &str, timeout: Option) -> bool { - let sl = &mut get_slock(name); - sl.claim(timeout) - } - - /// Releases a previously locked named mutex. - /// - /// # Arguments - /// - /// * `name` - The name of the mutex to release - /// - /// # Returns - /// - /// Returns `true` if the mutex was successfully released, `false` if a - /// mutex with the specified name was not found or was not locked by the - /// current thread. - /// - /// # Examples - /// - /// ```ignore - /// if namedmutex_lock("Mux01", None) { - /// // ... do some work ... - /// namedmutex_release("Mux01"); - /// } - /// ``` - pub fn namedmutex_release(name: &str) -> bool { - let sl = &mut get_slock(name); - sl.free() - } -} - -/// Provide a simplified HTTP(S) request for Lua -/// -/// The capability offered by this module can be added to a table so that it -/// works as a preloaded module -#[cfg(feature = "lua_httpreq")] -#[allow(dead_code)] -pub mod lua_httpreq { - use std::collections::HashMap; - - use bstr::BStr; - use minreq; - use mlua::IntoLua; - - use crate::constants::ERR_LUA_HTTPREQ_ERROR; - - /// Perform a request using the GET method: parameters must be urlencoded - /// directly in the `url` argument - pub fn request_get( - lua: &mlua::Lua, - url: &str, - headers: Option>, - ) -> mlua::Result<(mlua::Value, i64, mlua::Table)> { - let mut req = minreq::get(url); - if let Some(headers) = headers { - req = req.with_headers(headers); - } - - let resp = req - .send() - .map_err(|e| mlua::Error::runtime(format!("{ERR_LUA_HTTPREQ_ERROR}: {e}")))?; - Ok(( - BStr::new(&resp.as_bytes()).into_lua(lua)?, - resp.status_code as i64, - lua.create_table_from(resp.headers)?, - )) - } - - /// Perform a request using the POST method: parameters must be urlencoded - /// in the body, which is provided as a blob (reasonably a string) - pub fn request_post( - lua: &mlua::Lua, - url: &str, - body: Option<&[u8]>, - headers: Option>, - ) -> mlua::Result<(mlua::Value, i64, mlua::Table)> { - let mut req = minreq::post(url); - if let Some(headers) = headers { - req = req.with_headers(headers); - } - if let Some(body) = body { - req = req.with_body(body); - } - - let resp = req - .send() - .map_err(|e| mlua::Error::runtime(format!("{ERR_LUA_HTTPREQ_ERROR}: {e}")))?; - - Ok(( - BStr::new(&resp.as_bytes()).into_lua(lua)?, - resp.status_code as i64, - lua.create_table_from(resp.headers)?, - )) - } -} - #[allow(dead_code)] /// This module provides utilities for Lua based items pub mod luaitem { @@ -1487,6 +906,176 @@ pub mod luaitem { #[cfg(feature = "lua_sync")] pub use helper_sync::{LuaState, del_shared_state, get_shared_state, set_shared_state}; + // implement synchronization utilities: sleep and named mutexes + #[cfg(feature = "lua_sync")] + pub mod sync { + use crate::constants::*; + use crate::utility::named_mutex::*; + use std::thread; + use std::time::Duration; + + /// Sleep for a number of seconds: can be fractional + pub fn sleep(secs: f64) -> mlua::Result<()> { + let ms = (secs * 1000.0).round() as i64; + let ms = if ms < 0 { 0 } else { ms } as u64; + thread::sleep(Duration::from_millis(ms)); + Ok(()) + } + + /// Try to lock a named mutex possibly with a timeout + pub fn lock(name: String, timeout: Option) -> mlua::Result { + if RE_LUA_MUTEX_NAME.is_match(name.as_str()) { + if let Some(ms) = timeout { + let ms = (ms * 1000.0).round() as i64; + if ms >= 0 { + Ok(namedmutex_lock( + name.as_str(), + Some(Duration::from_millis(ms as u64)), + )) + } else { + Err(mlua::Error::runtime(ERR_LUA_INVALID_PARAMETER)) + } + } else { + Ok(namedmutex_lock(name.as_str(), None)) + } + } else { + Err(mlua::Error::runtime(ERR_LUA_INVALID_PARAMETER)) + } + } + + /// Release a locked named mutex + pub fn release(name: String) -> mlua::Result { + Ok(namedmutex_release(name.as_str())) + } + } + + // simple HTTP request functonality + #[cfg(feature = "lua_httpreq")] + pub mod httpreq { + use bstr::BStr; + use minreq; + use mlua::IntoLua; + use std::collections::HashMap; + + use crate::constants::*; + + /// Perform a request using the GET method: parameters must be urlencoded + /// directly in the `url` argument + fn request_get( + lua: &mlua::Lua, + url: &str, + headers: Option>, + ) -> mlua::Result<(mlua::Value, i64, mlua::Table)> { + let mut req = minreq::get(url); + if let Some(headers) = headers { + req = req.with_headers(headers); + } + + let resp = req + .send() + .map_err(|e| mlua::Error::runtime(format!("{ERR_LUA_HTTPREQ_ERROR}: {e}")))?; + Ok(( + BStr::new(&resp.as_bytes()).into_lua(lua)?, + resp.status_code as i64, + lua.create_table_from(resp.headers)?, + )) + } + + /// Perform a request using the POST method: parameters must be urlencoded + /// in the body, which is provided as a blob (reasonably a string) + fn request_post( + lua: &mlua::Lua, + url: &str, + body: Option<&[u8]>, + headers: Option>, + ) -> mlua::Result<(mlua::Value, i64, mlua::Table)> { + let mut req = minreq::post(url); + if let Some(headers) = headers { + req = req.with_headers(headers); + } + if let Some(body) = body { + req = req.with_body(body); + } + + let resp = req + .send() + .map_err(|e| mlua::Error::runtime(format!("{ERR_LUA_HTTPREQ_ERROR}: {e}")))?; + + Ok(( + BStr::new(&resp.as_bytes()).into_lua(lua)?, + resp.status_code as i64, + lua.create_table_from(resp.headers)?, + )) + } + + /// Lua specific HTTP GET utility + pub fn get( + lua: &mlua::Lua, + url: String, + headers: mlua::Value, + ) -> mlua::Result<(mlua::Value, i64, mlua::Table)> { + if headers.is_nil() { + Ok(request_get(lua, url.as_str(), None)?) + } else if headers.is_table() { + let mut h: HashMap = HashMap::new(); + for pair in headers + .as_table() + .unwrap() + .pairs::() + { + let (key, value) = pair?; + h.insert(key.to_string()?, value.to_string()?); + } + Ok(request_get(lua, url.as_str(), Some(h))?) + } else { + Err(mlua::Error::runtime(ERR_LUA_INVALID_PARAMETER)) + } + } + + /// Lua specific HTTP POST utility + pub fn post( + lua: &mlua::Lua, + url: String, + body: mlua::Value, + headers: mlua::Value, + ) -> mlua::Result<(mlua::Value, i64, mlua::Table)> { + if headers.is_nil() { + if body.is_nil() { + Ok(request_post(lua, url.as_str(), None, None)?) + } else { + Ok(request_post( + lua, + url.as_str(), + Some(body.to_string()?.as_bytes()), + None, + )?) + } + } else if headers.is_table() { + let mut h: HashMap = HashMap::new(); + for pair in headers + .as_table() + .unwrap() + .pairs::() + { + let (key, value) = pair?; + h.insert(key.to_string()?, value.to_string()?); + } + if body.is_nil() { + Ok(request_post(lua, url.as_str(), None, Some(h))?) + } else { + Ok(request_post( + lua, + url.as_str(), + Some(body.to_string()?.as_bytes()), + Some(h), + )?) + } + } else { + Err(mlua::Error::runtime(ERR_LUA_INVALID_PARAMETER)) + } + } + } + /// The possible values to be checked from Lua #[derive(Debug, Clone)] pub enum LuaValue { @@ -1544,8 +1133,8 @@ pub mod luaitem { #[cfg(feature = "dbus")] #[allow(dead_code)] pub mod dbusitem { - use crate::LogType; use crate::constants::*; + use crate::utility::logging::LogType; use cfgmap::CfgValue; use regex::Regex; use std::collections::HashMap; @@ -1672,7 +1261,7 @@ pub mod dbusitem { // zvariant is able to directly compare values zvariant::Value::Array(a) => { let v = zvariant::Value::from(*self); - a.iter().any(|x| v == *x) + a.contains(&v) } _ => false, } @@ -1870,11 +1459,8 @@ pub mod dbusitem { fn to_variant(&self) -> Option> { let mut a: Vec = Vec::new(); for item in self.iter() { - if let Some(v) = item.to_variant() { - a.push(v) - } else { - return None; - } + let v = item.to_variant()?; + a.push(v); } Some(zvariant::Value::new(a)) } @@ -1888,11 +1474,8 @@ pub mod dbusitem { fn to_variant(&self) -> Option> { let mut d: HashMap = HashMap::new(); for (key, item) in self.iter() { - if let Some(v) = item.to_variant() { - d.insert(key.clone(), v); - } else { - return None; - } + let v = item.to_variant()?; + d.insert(key.clone(), v); } Some(zvariant::Value::new(d)) } @@ -1922,15 +1505,8 @@ pub mod dbusitem { let map = self.as_map().unwrap(); let mut h: HashMap = HashMap::new(); for key in map.keys() { - if let Some(value) = map.get(key) { - if let Some(v) = value.to_variant() { - h.insert(key.clone(), v); - } else { - return None; - } - } else { - return None; - } + let v = map.get(key)?.to_variant()?; + h.insert(key.clone(), v); } Some(zvariant::Value::new(h)) } else { @@ -2775,8 +2351,8 @@ pub mod dbusitem { #[cfg(feature = "wmi")] #[allow(dead_code)] pub mod wmiitem { - use crate::LogType; use crate::constants::*; + use crate::utility::logging::LogType; use regex::Regex; use std::collections::HashMap; use std::hash::{Hash, Hasher}; @@ -3012,268 +2588,4 @@ pub mod wmiitem { } } -/// A common result type: catching errors from modules used throughout -/// the entire code. The corresponding error carries some information -/// about what went wrong. -#[allow(dead_code)] -pub mod wres { - use mlua; - use notify; - use std::{self, fmt, sync::PoisonError}; - - use crate::constants::{ERR_FAILED, ERR_LOCK_FAILED}; - - /// Types of specific errors - #[non_exhaustive] - #[derive(Debug, Clone)] - pub enum Kind { - Forbidden, - Unsupported, - Unavailable, - Unconverted, - Unparsed, - Busy, - Invalid, - Failed, - Empty, - // ... - Unknown, - } - - impl fmt::Display for Kind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match self { - Kind::Forbidden => "not permitted", - Kind::Unsupported => "not supported", - Kind::Unavailable => "not available", - Kind::Unconverted => "not converted", - Kind::Unparsed => "not parsed", - Kind::Busy => "resource busy", - Kind::Invalid => "invalid", - Kind::Failed => "failed", - Kind::Empty => "empty", - Kind::Unknown => "unknown", - } - ) - } - } - - /// Describes the origin of the error: if `Native` the error was originated - /// natively, otherwise the field is set by another error that is converted - /// into `Error` via a dedicated `From` trait implementation. - #[non_exhaustive] - #[derive(Debug, Clone, PartialEq)] - pub enum Origin { - Native, - Unit, - StdIo, - Notify, - Sync, - Lua, - - #[cfg(feature = "dbus")] - DBus, - - #[cfg(windows)] - #[cfg(feature = "wmi")] - Wmi, - - // ... - Unknown, - } - - impl fmt::Display for Origin { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match self { - Origin::Native => "self", - Origin::Unit => "unit", - Origin::StdIo => "io", - Origin::Notify => "fschange", - Origin::Sync => "sync", - Origin::Lua => "lua", - - #[cfg(feature = "dbus")] - Origin::DBus => "dbus", - - #[cfg(windows)] - #[cfg(feature = "wmi")] - Origin::Wmi => "wmi", - - // ... - Origin::Unknown => "unknown", - } - ) - } - } - - /// The error type that is used throughout the application: implementations - /// of the `From` trait are used to implicitly convert from other error - /// types, which in turn set the `origin` property. - #[derive(Debug, Clone)] - pub struct Error { - kind: Kind, - origin: Origin, - message: String, // freeform message: owned in order to avoid lifetime management - } - - impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.origin != Origin::Native { - write!(f, "{} ({}): {}", &self.kind, &self.origin, &self.message) - } else { - write!(f, "{}: {}", &self.kind, &self.message) - } - } - } - - // maybe the most important: From - impl From for Error { - fn from(e: std::io::Error) -> Self { - Self { - kind: match e.kind() { - std::io::ErrorKind::Unsupported => Kind::Unsupported, - std::io::ErrorKind::PermissionDenied => Kind::Forbidden, - std::io::ErrorKind::InvalidData => Kind::Invalid, - std::io::ErrorKind::InvalidInput => Kind::Invalid, - _ => Kind::Unknown, - }, - origin: Origin::StdIo, - message: e.to_string(), - } - } - } - - // notify (fschange) errors - impl From for Error { - fn from(e: notify::Error) -> Self { - Self { - kind: Kind::Failed, - origin: Origin::Notify, - message: e.to_string(), - } - } - } - - // Lua errors - impl From for Error { - fn from(e: mlua::Error) -> Self { - Self { - kind: Kind::Failed, - origin: Origin::Lua, - message: e.to_string(), - } - } - } - - // zbus errors - #[cfg(feature = "dbus")] - impl From for Error { - fn from(e: zbus::Error) -> Self { - Self { - kind: Kind::Failed, - origin: Origin::DBus, - message: e.to_string(), - } - } - } - - // wmi errors - #[cfg(windows)] - #[cfg(feature = "wmi")] - impl From for Error { - fn from(e: wmi::WMIError) -> Self { - let kind = match e { - wmi::WMIError::ConvertBoolError(_) - | wmi::WMIError::ConvertStringError(_) - | wmi::WMIError::ConvertLengthError(_) - | wmi::WMIError::ConvertDatetimeError(_) - | wmi::WMIError::ConvertDurationError(_) - | wmi::WMIError::ConvertVariantError(_) - | wmi::WMIError::ConvertError(_) => Kind::Unconverted, - wmi::WMIError::DeserializeValueError(_) - | wmi::WMIError::InvalidDeserializationVariantError(_) - | wmi::WMIError::SerdeError(_) => Kind::Invalid, - wmi::WMIError::ParseDatetimeError(_) - | wmi::WMIError::ParseFloatError(_) - | wmi::WMIError::ParseIntError(_) => Kind::Unparsed, - wmi::WMIError::UnimplementedArrayItem => Kind::Unavailable, - _ => Kind::Failed, - }; - Self { - kind, - origin: Origin::Wmi, - message: e.to_string(), - } - } - } - - // resource locking errors - impl From> for Error { - fn from(_: PoisonError) -> Self { - Self { - kind: Kind::Failed, - origin: Origin::Sync, - message: ERR_LOCK_FAILED.to_owned(), - } - } - } - - // errors based on the unit type - impl From<()> for Error { - fn from(_: ()) -> Self { - Self { - kind: Kind::Failed, - origin: Origin::Unit, - message: ERR_FAILED.to_owned(), - } - } - } - - // implements `Error` and provides access to properties - impl Error { - // this is used only to natively create an instance of `Error`: only - // conversions set the `origin` property to something different - pub fn new(kind: Kind, message: &str) -> Self { - Self { - kind, - origin: Origin::Native, - message: message.to_string(), - } - } - - // property access - pub fn kind(&self) -> &Kind { - &self.kind - } - - pub fn origin(&self) -> &Origin { - &self.origin - } - - pub fn message(&self) -> &str { - &self.message - } - } - - // possible last resort to allow conversions from pointers to errors - impl From> for Error { - fn from(e: Box) -> Self { - Self { - kind: Kind::Unknown, - origin: Origin::Unknown, - message: e.to_string(), - } - } - } - - /// Specific `Result` type that assumes `wres::Error` as its Err variant - pub type Result = std::result::Result; -} - // end. diff --git a/src/condition/base.rs b/src/condition/base.rs index 22d9549..0c09121 100644 --- a/src/condition/base.rs +++ b/src/condition/base.rs @@ -23,8 +23,8 @@ use std::time::Instant; -use crate::common::logging::{LogType, log}; -use crate::common::wres::{Error, Kind, Result}; +use crate::utility::logging::{LogType, log}; +use crate::utility::result::{Error, Kind, Result}; use crate::constants::*; use crate::task::registry::TaskRegistry; @@ -237,7 +237,7 @@ pub trait Condition: Send { assert!( self.get_id() != 0, "condition {} not registered", - self.get_name() + self.get_name(), ); // bail out if the condition has no associated tasks, if it @@ -422,7 +422,7 @@ pub trait Condition: Send { assert!( self.get_id() != 0, "condition {} not registered", - self.get_name() + self.get_name(), ); assert!( self.task_registry().is_some(), diff --git a/src/condition/bucket_cond.rs b/src/condition/bucket_cond.rs index 97b777e..9c894a9 100644 --- a/src/condition/bucket_cond.rs +++ b/src/condition/bucket_cond.rs @@ -20,8 +20,8 @@ use cfgmap::CfgMap; use regex::Regex; use super::base::Condition; -use crate::common::logging::{LogType, log}; -use crate::common::wres::Result; +use crate::utility::logging::{LogType, log}; +use crate::utility::result::Result; use crate::task::registry::TaskRegistry; use crate::{cfg_mandatory, constants::*}; diff --git a/src/condition/command_cond.rs b/src/condition/command_cond.rs index 114c1e6..20e8bfd 100644 --- a/src/condition/command_cond.rs +++ b/src/condition/command_cond.rs @@ -29,8 +29,8 @@ use cfgmap::CfgMap; use super::base::Condition; use crate::common::cmditem::*; -use crate::common::logging::{LogType, log}; -use crate::common::wres::{Error, Kind, Result}; +use crate::utility::logging::{LogType, log}; +use crate::utility::result::{Error, Kind, Result}; use crate::task::registry::TaskRegistry; use crate::{cfg_mandatory, constants::*}; diff --git a/src/condition/dbus_cond.rs b/src/condition/dbus_cond.rs index 3627755..53f6f78 100644 --- a/src/condition/dbus_cond.rs +++ b/src/condition/dbus_cond.rs @@ -22,8 +22,8 @@ use std::str::FromStr; use super::base::Condition; use crate::common::dbusitem::*; -use crate::common::logging::{LogType, log}; -use crate::common::wres::Result; +use crate::utility::logging::{LogType, log}; +use crate::utility::result::Result; use crate::task::registry::TaskRegistry; use crate::{cfg_mandatory, constants::*}; diff --git a/src/condition/idle_cond.rs b/src/condition/idle_cond.rs index cf44080..3d9f5b0 100644 --- a/src/condition/idle_cond.rs +++ b/src/condition/idle_cond.rs @@ -12,8 +12,8 @@ use cfgmap::CfgMap; use system_idle_time::get_idle_time; use super::base::Condition; -use crate::common::logging::{LogType, log}; -use crate::common::wres::Result; +use crate::utility::logging::{LogType, log}; +use crate::utility::result::Result; use crate::task::registry::TaskRegistry; use crate::{cfg_mandatory, constants::*}; diff --git a/src/condition/interval_cond.rs b/src/condition/interval_cond.rs index 07987c8..409fdeb 100644 --- a/src/condition/interval_cond.rs +++ b/src/condition/interval_cond.rs @@ -11,8 +11,8 @@ use std::time::{Duration, Instant}; use cfgmap::CfgMap; use super::base::Condition; -use crate::common::logging::{LogType, log}; -use crate::common::wres::Result; +use crate::utility::logging::{LogType, log}; +use crate::utility::result::Result; use crate::task::registry::TaskRegistry; use crate::{cfg_mandatory, constants::*}; diff --git a/src/condition/lua_cond.rs b/src/condition/lua_cond.rs index 1a414b6..1e47943 100644 --- a/src/condition/lua_cond.rs +++ b/src/condition/lua_cond.rs @@ -9,9 +9,6 @@ use std::collections::HashMap; use std::hash::{DefaultHasher, Hash, Hasher}; use std::time::{Duration, Instant, SystemTime}; -#[cfg(feature = "lua_sync")] -use std::thread; - use std::fs; use std::path::{Path, PathBuf}; @@ -21,20 +18,14 @@ use cfgmap::CfgMap; use mlua; use super::base::Condition; -use crate::common::logging::{LogType, log}; +use crate::utility::logging::{LogType, log}; use crate::common::luaitem::*; -use crate::common::wres::{Error, Kind, Result}; +use crate::utility::result::{Error, Kind, Result}; use crate::task::registry::TaskRegistry; use crate::{cfg_mandatory, constants::*}; use crate::cfghelp::*; -#[cfg(feature = "lua_sync")] -use crate::common::named_mutex::*; - -#[cfg(feature = "lua_httpreq")] -use crate::common::lua_httpreq; - /// _Lua_ script Based Condition /// /// This condition is verified when the underlying _Lua_ script execution @@ -960,16 +951,17 @@ impl Condition for LuaCondition { // decides whether or not to pollute the Lua environment also setting // the variables configured by the user if self.set_vars { - let _ = globals.set(LUAVAR_NAME_COND.as_str(), self.cond_name.to_string()); + globals.set(LUAVAR_NAME_COND.as_str(), self.cond_name.to_string())?; for varname in self.variables.keys() { if let Some(v) = self.variables.get(varname.as_str()) { - let res = match v { + if match v { LuaValue::LuaBoolean(x) => globals.set(varname.as_str(), *x), LuaValue::LuaNumber(x) => globals.set(varname.as_str(), *x), LuaValue::LuaString(x) => globals.set(varname.as_str(), x.as_str()), - }; - if res.is_err() { + } + .is_err() + { self.log( LogType::Warn, LOG_WHEN_START, @@ -1040,41 +1032,23 @@ impl Condition for LuaCondition { // the following features are optional #[cfg(feature = "lua_sync")] { + // this `use` is preferred for readability + use crate::common::luaitem; + // create synchronization functions in a table let syncftab = lua.create_table()?; let _ = syncftab.set( "sleep", - lua.create_function(move |_, secs: f64| { - let ms = (secs * 1000.0).round() as i64; - let ms = if ms < 0 { 0 } else { ms } as u64; - thread::sleep(Duration::from_millis(ms)); - Ok(()) - })?, + lua.create_function(|_, secs: f64| luaitem::sync::sleep(secs))?, ); // for no particular reason we enforce the mutex name to carry an // identifier-like name, otherwise an error is thrown let _ = syncftab.set( "lock", - lua.create_function(move |_, (name, timeout): (String, Option)| { - if RE_LUA_MUTEX_NAME.is_match(name.as_str()) { - if let Some(ms) = timeout { - let ms = (ms * 1000.0).round() as i64; - if ms >= 0 { - Ok(namedmutex_lock( - name.as_str(), - Some(Duration::from_millis(ms as u64)), - )) - } else { - Err(mlua::Error::runtime(ERR_LUA_INVALID_PARAMETER)) - } - } else { - Ok(namedmutex_lock(name.as_str(), None)) - } - } else { - Err(mlua::Error::runtime(ERR_LUA_INVALID_PARAMETER)) - } + lua.create_function(|_, (name, timeout): (String, Option)| { + luaitem::sync::lock(name, timeout) })?, ); @@ -1082,7 +1056,7 @@ impl Condition for LuaCondition { // and the unlock will simply fail and return `false` let _ = syncftab.set( "release", - lua.create_function(|_, name: String| Ok(namedmutex_release(name.as_str())))?, + lua.create_function(|_, name: String| luaitem::sync::release(name))?, ); // ... @@ -1130,28 +1104,16 @@ impl Condition for LuaCondition { #[cfg(feature = "lua_httpreq")] { + // this `use` is preferred for readability + use crate::common::luaitem; + // HTTP request capability let httpftab = lua.create_table()?; let _ = httpftab.set( "get", lua.create_function(|lua: &mlua::Lua, (url, headers): (String, mlua::Value)| { - if headers.is_nil() { - Ok(lua_httpreq::request_get(lua, url.as_str(), None)?) - } else if headers.is_table() { - let mut h: HashMap = HashMap::new(); - for pair in headers - .as_table() - .unwrap() - .pairs::() - { - let (key, value) = pair?; - h.insert(key.to_string()?, value.to_string()?); - } - Ok(lua_httpreq::request_get(lua, url.as_str(), Some(h))?) - } else { - Err(mlua::Error::runtime(ERR_LUA_INVALID_PARAMETER)) - } + luaitem::httpreq::get(lua, url, headers) })?, ); @@ -1159,40 +1121,7 @@ impl Condition for LuaCondition { "post", lua.create_function( |lua: &mlua::Lua, (url, body, headers): (String, mlua::Value, mlua::Value)| { - if headers.is_nil() { - if body.is_nil() { - Ok(lua_httpreq::request_post(lua, url.as_str(), None, None)?) - } else { - Ok(lua_httpreq::request_post( - lua, - url.as_str(), - Some(body.to_string()?.as_bytes()), - None, - )?) - } - } else if headers.is_table() { - let mut h: HashMap = HashMap::new(); - for pair in headers - .as_table() - .unwrap() - .pairs::() - { - let (key, value) = pair?; - h.insert(key.to_string()?, value.to_string()?); - } - if body.is_nil() { - Ok(lua_httpreq::request_post(lua, url.as_str(), None, Some(h))?) - } else { - Ok(lua_httpreq::request_post( - lua, - url.as_str(), - Some(body.to_string()?.as_bytes()), - Some(h), - )?) - } - } else { - Err(mlua::Error::runtime(ERR_LUA_INVALID_PARAMETER)) - } + luaitem::httpreq::post(lua, url, body, headers) }, )?, ); @@ -1289,26 +1218,25 @@ impl Condition for LuaCondition { let mut state_updated = false; if let Ok(table) = globals.get::(LUA_TABLE_STATE_PRIVATE) { for pair in table.pairs::() { - if let Ok((name, value)) = pair { - if let Ok(name) = lua.convert::(name) { - if RE_LUA_STATE_INDEX.is_match(name.as_str()) { - if let Ok(value) = lua.convert::(value) { - self.log( - LogType::Trace, - LOG_WHEN_PROC, - LOG_STATUS_MSG, - &format!("private state entry with index `{name}` set to {value}"), - ); - state.insert(name, value); - state_updated = true; - } - } else { - save_error = true; - break; - } - } else { - save_error = true; - break; + if pair.is_err() { + save_error = true; + break; + } + let (name, value) = pair.unwrap(); + if let Ok(name) = lua.convert::(name) + && RE_LUA_STATE_INDEX.is_match(name.as_str()) + { + if let Ok(value) = lua.convert::(value) { + self.log( + LogType::Trace, + LOG_WHEN_PROC, + LOG_STATUS_MSG, + &format!( + "private state entry with index `{name}` set to {value}" + ), + ); + state.insert(name, value); + state_updated = true; } } else { save_error = true; @@ -1351,27 +1279,24 @@ impl Condition for LuaCondition { LogType::Debug, LOG_WHEN_PROC, LOG_STATUS_MSG, - &format!("checking results: {}", &self.repr_checks()), + &format!("checking results: {}", self.repr_checks()), ); if self.expect_all { failure_reason = FailureReason::NoFailure; for (varname, value) in self.expected.iter() { if let Some(res) = match value { - LuaValue::LuaString(v) => { - let r: std::result::Result = - globals.get(varname.as_str()); - if let Ok(r) = r { Some(r == *v) } else { None } - } - LuaValue::LuaNumber(v) => { - let r: std::result::Result = - globals.get(varname.as_str()); - if let Ok(r) = r { Some(r == *v) } else { None } - } - LuaValue::LuaBoolean(v) => { - let r: std::result::Result = - globals.get(varname.as_str()); - if let Ok(r) = r { Some(r == *v) } else { None } - } + LuaValue::LuaString(v) => globals + .get(varname.as_str()) + .map(|x: String| Some(x == *v)) + .unwrap_or(None), + LuaValue::LuaNumber(v) => globals + .get(varname.as_str()) + .map(|x: f64| Some(x == *v)) + .unwrap_or(None), + LuaValue::LuaBoolean(v) => globals + .get(varname.as_str()) + .map(|x: bool| Some(x == *v)) + .unwrap_or(None), } { if !res { self.log( @@ -1401,35 +1326,30 @@ impl Condition for LuaCondition { } else { failure_reason = FailureReason::VariableMatch; for (varname, value) in self.expected.iter() { - if let Some(res) = match value { - LuaValue::LuaString(v) => { - let r: std::result::Result = - globals.get(varname.as_str()); - if let Ok(r) = r { Some(r == *v) } else { None } - } - LuaValue::LuaNumber(v) => { - let r: std::result::Result = - globals.get(varname.as_str()); - if let Ok(r) = r { Some(r == *v) } else { None } - } - LuaValue::LuaBoolean(v) => { - let r: std::result::Result = - globals.get(varname.as_str()); - if let Ok(r) = r { Some(r == *v) } else { None } - } + if match value { + LuaValue::LuaString(v) => globals + .get(varname.as_str()) + .map(|x: String| x == *v) + .unwrap_or(false), + LuaValue::LuaNumber(v) => globals + .get(varname.as_str()) + .map(|x: f64| x == *v) + .unwrap_or(false), + LuaValue::LuaBoolean(v) => globals + .get(varname.as_str()) + .map(|x: bool| x == *v) + .unwrap_or(false), } { - if res { - self.log( - LogType::Debug, - LOG_WHEN_END, - LOG_STATUS_MSG, - &format!( - "result match on at least one variable ({varname}): success" - ), - ); - failure_reason = FailureReason::NoFailure; - break; - } + self.log( + LogType::Debug, + LOG_WHEN_PROC, + LOG_STATUS_OK, + &format!( + "result match on at least one variable ({varname}): success" + ), + ); + failure_reason = FailureReason::NoFailure; + break; } } } diff --git a/src/condition/registry.rs b/src/condition/registry.rs index 298545b..22bdf51 100644 --- a/src/condition/registry.rs +++ b/src/condition/registry.rs @@ -16,8 +16,8 @@ use unique_id::Generator; use unique_id::sequence::SequenceGenerator; use super::base::{Condition, ConditionRef}; -use crate::common::logging::{LogType, log}; -use crate::common::wres::{Error, Kind, Result}; +use crate::utility::logging::{LogType, log}; +use crate::utility::result::{Error, Kind, Result}; use crate::constants::*; // module-wide values @@ -166,17 +166,9 @@ impl ConditionRegistry { let busy = busy.lock(); if *busy == 0 { if self.has_condition(&name) { - match self.remove_condition(&name) { - Ok(_) => { - return Ok(self.add_condition(cond_ref)); - } - _ => { - return Err(Error::new(Kind::Failed, ERR_CONDREG_CANNOT_PULL_COND)); - } - } - } else { - return Ok(self.add_condition(cond_ref)); + self.remove_condition(&name)?; } + Ok(self.add_condition(cond_ref)) } else { let queue = self.items_to_add.clone(); let mut queue = queue.lock(); @@ -192,9 +184,8 @@ impl ConditionRegistry { "registry busy: condition {name} set to be added when no conditions are busy", ), ); + Ok(true) } - - Ok(true) } /// Remove a named condition from the list and give it back stored in a Box. diff --git a/src/condition/time_cond.rs b/src/condition/time_cond.rs index 9cd9877..093aea1 100644 --- a/src/condition/time_cond.rs +++ b/src/condition/time_cond.rs @@ -25,8 +25,8 @@ use cfgmap::CfgMap; use chrono::prelude::*; use super::base::Condition; -use crate::common::logging::{LogType, log}; -use crate::common::wres::{Error, Kind, Result}; +use crate::utility::logging::{LogType, log}; +use crate::utility::result::{Error, Kind, Result}; use crate::task::registry::TaskRegistry; use crate::{cfg_mandatory, constants::*}; diff --git a/src/condition/wmi_cond.rs b/src/condition/wmi_cond.rs index b47e4fb..62ec8fb 100644 --- a/src/condition/wmi_cond.rs +++ b/src/condition/wmi_cond.rs @@ -22,9 +22,9 @@ use std::collections::HashMap; use wmi::{Variant, WMIConnection}; use super::base::Condition; -use crate::common::logging::{LogType, log}; +use crate::utility::logging::{LogType, log}; use crate::common::wmiitem::*; -use crate::common::wres::Result; +use crate::utility::result::Result; use crate::task::registry::TaskRegistry; use crate::{cfg_mandatory, constants::*}; diff --git a/src/config.rs b/src/config.rs index dd51832..3ecca3f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,8 +5,8 @@ use cfgmap::{CfgMap, CfgValue}; use std::fs; -use crate::common::logging::{LogType, log}; -use crate::common::wres::{Error, Kind, Result}; +use crate::utility::logging::{LogType, log}; +use crate::utility::result::{Error, Kind, Result}; use crate::condition::bucket_cond::ExecutionBucket; use crate::constants::*; diff --git a/src/event/base.rs b/src/event/base.rs index 26b2f9f..4365857 100644 --- a/src/event/base.rs +++ b/src/event/base.rs @@ -15,8 +15,8 @@ use async_trait::async_trait; -use crate::common::logging::{LogType, log}; -use crate::common::wres::{Error, Kind, Result}; +use crate::utility::logging::{LogType, log}; +use crate::utility::result::{Error, Kind, Result}; use crate::condition::bucket_cond::ExecutionBucket; use crate::condition::registry::ConditionRegistry; use crate::constants::*; diff --git a/src/event/dbus_event.rs b/src/event/dbus_event.rs index 532b980..b42a537 100644 --- a/src/event/dbus_event.rs +++ b/src/event/dbus_event.rs @@ -22,8 +22,8 @@ use zbus; use super::base::Event; use crate::common::dbusitem::*; -use crate::common::logging::{LogType, log}; -use crate::common::wres::Result; +use crate::utility::logging::{LogType, log}; +use crate::utility::result::Result; use crate::condition::bucket_cond::ExecutionBucket; use crate::condition::registry::ConditionRegistry; use crate::{cfg_mandatory, constants::*}; diff --git a/src/event/fschange_event.rs b/src/event/fschange_event.rs index 7be426e..6262bb7 100644 --- a/src/event/fschange_event.rs +++ b/src/event/fschange_event.rs @@ -20,8 +20,8 @@ use cfgmap::CfgMap; use notify::{self, Watcher}; use super::base::Event; -use crate::common::logging::{LogType, log}; -use crate::common::wres::Result; +use crate::utility::logging::{LogType, log}; +use crate::utility::result::Result; use crate::condition::bucket_cond::ExecutionBucket; use crate::condition::registry::ConditionRegistry; use crate::{cfg_mandatory, constants::*}; diff --git a/src/event/manual_event.rs b/src/event/manual_event.rs index f0ae9a7..f5de3a1 100644 --- a/src/event/manual_event.rs +++ b/src/event/manual_event.rs @@ -12,8 +12,8 @@ use cfgmap::CfgMap; use async_trait::async_trait; use super::base::Event; -use crate::common::logging::{LogType, log}; -use crate::common::wres::Result; +use crate::utility::logging::{LogType, log}; +use crate::utility::result::Result; use crate::condition::bucket_cond::ExecutionBucket; use crate::condition::registry::ConditionRegistry; use crate::{cfg_mandatory, constants::*}; diff --git a/src/event/registry.rs b/src/event/registry.rs index 62ede06..571c411 100644 --- a/src/event/registry.rs +++ b/src/event/registry.rs @@ -22,8 +22,8 @@ use unique_id::Generator; use unique_id::sequence::SequenceGenerator; use super::base::{Event, EventRef}; -use crate::common::logging::{LogType, log}; -use crate::common::wres::{Error, Kind, Result}; +use crate::utility::logging::{LogType, log}; +use crate::utility::result::{Error, Kind, Result}; use crate::constants::*; // module-wide values diff --git a/src/event/wmi_event.rs b/src/event/wmi_event.rs index 45e7465..182a28e 100644 --- a/src/event/wmi_event.rs +++ b/src/event/wmi_event.rs @@ -14,8 +14,8 @@ use async_trait::async_trait; use wmi::WMIConnection; use super::base::Event; -use crate::common::logging::{LogType, log}; -use crate::common::wres::Result; +use crate::utility::logging::{LogType, log}; +use crate::utility::result::Result; use crate::condition::bucket_cond::ExecutionBucket; use crate::condition::registry::ConditionRegistry; use crate::{cfg_mandatory, constants::*}; diff --git a/src/main.rs b/src/main.rs index 4d2fa77..f2906c8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,6 +25,7 @@ mod cfghelp; mod common; mod config; mod constants; +mod utility; // bring the registries in scope use condition::registry::ConditionRegistry; @@ -34,10 +35,10 @@ use task::registry::TaskRegistry; use condition::bucket_cond::ExecutionBucket; use task::internal_task::set_command_runner; -use crate::common::wres::{Error, Kind, Result}; -use common::logging::{LogType, init as log_init, log}; use config::*; use constants::*; +use utility::logging::{LogType, init as log_init, log}; +use utility::result::{Error, Kind, Result}; lazy_static! { // the global task registry: all conditions will be associated to this @@ -55,7 +56,7 @@ lazy_static! { // single instance name static ref INSTANCE_GUID: String = format!( "{APP_NAME}-{}-{APP_GUID}", - { if let Ok(s) = username() { s } else { String::from(STR_UNKNOWN_VALUE) }}, + username().unwrap_or(String::from(STR_UNKNOWN_VALUE)), ); // set this if the application must exit @@ -88,10 +89,10 @@ lazy_static! { // check whether an instance is already running, and return an error if so fn check_single_instance(instance: &SingleInstance) -> Result<()> { if !instance.is_single() { - return Err(Error::new(Kind::Forbidden, ERR_ALREADY_RUNNING)); + Err(Error::new(Kind::Forbidden, ERR_ALREADY_RUNNING)) + } else { + Ok(()) } - - Ok(()) } // execute a (very basic but working) scheduler tick: the call to this function @@ -204,7 +205,9 @@ fn sched_tick(rand_millis_range: Option) -> bool { None, LOG_WHEN_PROC, LOG_STATUS_MSG, - &format!("condition {name} tested (tasks executed unsuccessfully)"), + &format!( + "condition {name} tested (tasks executed unsuccessfully)" + ), ); } } @@ -405,12 +408,11 @@ fn set_suspended_condition(name: &str, suspended: bool) { // intervals might fire immediately; reset will always // succeed, so this construct to build the right log // message is only here for consistency - let info = - if CONDITION_REGISTRY.reset_condition(name, true).is_ok() { - "resumed and reset" - } else { - "resumed" - }; + let info = if CONDITION_REGISTRY.reset_condition(name, true).is_ok() { + "resumed and reset" + } else { + "resumed" + }; log( LogType::Info, LOG_EMITTER_MAIN, @@ -1086,11 +1088,10 @@ fn main() { exit_if_fails!(args.quiet, check_single_instance(&instance)); // now check that the config file name has been provided - if args.config.is_none() { + let config = args.config.unwrap_or_else(|| { eprintln!("{APP_NAME} error: configuration file not specified"); std::process::exit(2); - } - let config = args.config.unwrap(); + }); // configure the logger let level = match args.log_level { diff --git a/src/task/base.rs b/src/task/base.rs index 0b9a5a2..60f0f10 100644 --- a/src/task/base.rs +++ b/src/task/base.rs @@ -9,8 +9,8 @@ //! and read/write access to its ID in the form of an unsigned integer. A zero //! ID is used for _inactive_ tasks. -use crate::common::logging::{LogType, log}; -use crate::common::wres::Result; +use crate::utility::logging::{LogType, log}; +use crate::utility::result::Result; use crate::constants::*; /// Define the interface for `Task` objects diff --git a/src/task/command_task.rs b/src/task/command_task.rs index fb0d390..71db319 100644 --- a/src/task/command_task.rs +++ b/src/task/command_task.rs @@ -38,8 +38,8 @@ use cfgmap::CfgMap; // we implement the Task trait here in order to enqueue tasks use super::base::Task; use crate::common::cmditem::*; -use crate::common::logging::{LogType, log}; -use crate::common::wres::{Error, Kind, Result}; +use crate::utility::logging::{LogType, log}; +use crate::utility::result::{Error, Kind, Result}; use crate::{cfg_mandatory, constants::*}; use crate::cfghelp::*; diff --git a/src/task/internal_task.rs b/src/task/internal_task.rs index 42df088..b761b7f 100644 --- a/src/task/internal_task.rs +++ b/src/task/internal_task.rs @@ -16,8 +16,8 @@ use lazy_static::lazy_static; // we implement the Task trait here in order to enqueue tasks use super::base::Task; -use crate::common::logging::{LogType, log}; -use crate::common::wres::{Error, Kind, Result}; +use crate::utility::logging::{LogType, log}; +use crate::utility::result::{Error, Kind, Result}; use crate::{cfg_mandatory, constants::*}; use crate::cfghelp::*; diff --git a/src/task/lua_task.rs b/src/task/lua_task.rs index e09bd0b..7541297 100644 --- a/src/task/lua_task.rs +++ b/src/task/lua_task.rs @@ -9,12 +9,6 @@ use std::collections::HashMap; use std::hash::{DefaultHasher, Hash, Hasher}; use std::time::SystemTime; -#[cfg(feature = "lua_sync")] -use std::time::Duration; - -#[cfg(feature = "lua_sync")] -use std::thread; - use std::fs; use std::path::{Path, PathBuf}; @@ -25,19 +19,13 @@ use mlua; // we implement the Task trait here in order to enqueue tasks use super::base::Task; -use crate::common::logging::{LogType, log}; +use crate::utility::logging::{LogType, log}; use crate::common::luaitem::*; -use crate::common::wres::{Error, Kind, Result}; +use crate::utility::result::{Error, Kind, Result}; use crate::{cfg_mandatory, constants::*}; use crate::cfghelp::*; -#[cfg(feature = "lua_sync")] -use crate::common::named_mutex::*; - -#[cfg(feature = "lua_httpreq")] -use crate::common::lua_httpreq; - /// _Lua_ script Based Task /// /// This type of task runs a _Lua_ script and possibly matches one or more @@ -634,17 +622,18 @@ impl Task for LuaTask { // decides whether or not to pollute the Lua environment also setting // the variables configured by the user if self.set_vars { - let _ = globals.set(LUAVAR_NAME_COND.as_str(), trigger_name.to_string()); - let _ = globals.set(LUAVAR_NAME_TASK.as_str(), self.task_name.to_string()); + globals.set(LUAVAR_NAME_COND.as_str(), trigger_name.to_string())?; + globals.set(LUAVAR_NAME_TASK.as_str(), self.task_name.to_string())?; for varname in self.variables.keys() { if let Some(v) = self.variables.get(varname.as_str()) { - let res = match v { + if match v { LuaValue::LuaBoolean(x) => globals.set(varname.as_str(), *x), LuaValue::LuaNumber(x) => globals.set(varname.as_str(), *x), LuaValue::LuaString(x) => globals.set(varname.as_str(), x.as_str()), - }; - if res.is_err() { + } + .is_err() + { self.log( LogType::Warn, LOG_WHEN_START, @@ -720,41 +709,23 @@ impl Task for LuaTask { // the following features are optional #[cfg(feature = "lua_sync")] { + // this `use` is preferred for readability + use crate::common::luaitem; + // create synchronization functions in a table let syncftab = lua.create_table()?; let _ = syncftab.set( "sleep", - lua.create_function(move |_, secs: f64| { - let ms = (secs * 1000.0).round() as i64; - let ms = if ms < 0 { 0 } else { ms } as u64; - thread::sleep(Duration::from_millis(ms)); - Ok(()) - })?, + lua.create_function(|_, secs: f64| luaitem::sync::sleep(secs))?, ); // for no particular reason we enforce the mutex name to carry an // identifier-like name, otherwise an error is thrown let _ = syncftab.set( "lock", - lua.create_function(move |_, (name, timeout): (String, Option)| { - if RE_LUA_MUTEX_NAME.is_match(name.as_str()) { - if let Some(ms) = timeout { - let ms = (ms * 1000.0).round() as i64; - if ms >= 0 { - Ok(namedmutex_lock( - name.as_str(), - Some(Duration::from_millis(ms as u64)), - )) - } else { - Err(mlua::Error::runtime(ERR_LUA_INVALID_PARAMETER)) - } - } else { - Ok(namedmutex_lock(name.as_str(), None)) - } - } else { - Err(mlua::Error::runtime(ERR_LUA_INVALID_PARAMETER)) - } + lua.create_function(|_, (name, timeout): (String, Option)| { + luaitem::sync::lock(name, timeout) })?, ); @@ -762,7 +733,7 @@ impl Task for LuaTask { // and the unlock will simply fail and return `false` let _ = syncftab.set( "release", - lua.create_function(move |_, name: String| Ok(namedmutex_release(name.as_str())))?, + lua.create_function(|_, name: String| luaitem::sync::release(name))?, ); // ... @@ -810,28 +781,16 @@ impl Task for LuaTask { #[cfg(feature = "lua_httpreq")] { + // this `use` is preferred for readability + use crate::common::luaitem; + // HTTP request capability let httpftab = lua.create_table()?; let _ = httpftab.set( "get", lua.create_function(|lua: &mlua::Lua, (url, headers): (String, mlua::Value)| { - if headers.is_nil() { - Ok(lua_httpreq::request_get(lua, url.as_str(), None)?) - } else if headers.is_table() { - let mut h: HashMap = HashMap::new(); - for pair in headers - .as_table() - .unwrap() - .pairs::() - { - let (key, value) = pair?; - h.insert(key.to_string()?, value.to_string()?); - } - Ok(lua_httpreq::request_get(lua, url.as_str(), Some(h))?) - } else { - Err(mlua::Error::runtime(ERR_LUA_INVALID_PARAMETER)) - } + luaitem::httpreq::get(lua, url, headers) })?, ); @@ -839,40 +798,7 @@ impl Task for LuaTask { "post", lua.create_function( |lua: &mlua::Lua, (url, body, headers): (String, mlua::Value, mlua::Value)| { - if headers.is_nil() { - if body.is_nil() { - Ok(lua_httpreq::request_post(lua, url.as_str(), None, None)?) - } else { - Ok(lua_httpreq::request_post( - lua, - url.as_str(), - Some(body.to_string()?.as_bytes()), - None, - )?) - } - } else if headers.is_table() { - let mut h: HashMap = HashMap::new(); - for pair in headers - .as_table() - .unwrap() - .pairs::() - { - let (key, value) = pair?; - h.insert(key.to_string()?, value.to_string()?); - } - if body.is_nil() { - Ok(lua_httpreq::request_post(lua, url.as_str(), None, Some(h))?) - } else { - Ok(lua_httpreq::request_post( - lua, - url.as_str(), - Some(body.to_string()?.as_bytes()), - Some(h), - )?) - } - } else { - Err(mlua::Error::runtime(ERR_LUA_INVALID_PARAMETER)) - } + luaitem::httpreq::post(lua, url, body, headers) }, )?, ); @@ -969,26 +895,25 @@ impl Task for LuaTask { let mut state_updated = false; if let Ok(table) = globals.get::(LUA_TABLE_STATE_PRIVATE) { for pair in table.pairs::() { - if let Ok((name, value)) = pair { - if let Ok(name) = lua.convert::(name) { - if RE_LUA_STATE_INDEX.is_match(name.as_str()) { - if let Ok(value) = lua.convert::(value) { - self.log( - LogType::Trace, - LOG_WHEN_PROC, - LOG_STATUS_MSG, - &format!("private state entry with index `{name}` set to {value}"), - ); - state.insert(name, value); - state_updated = true; - } - } else { - save_error = true; - break; - } - } else { - save_error = true; - break; + if pair.is_err() { + save_error = true; + break; + } + let (name, value) = pair.unwrap(); + if let Ok(name) = lua.convert::(name) + && RE_LUA_STATE_INDEX.is_match(name.as_str()) + { + if let Ok(value) = lua.convert::(value) { + self.log( + LogType::Trace, + LOG_WHEN_PROC, + LOG_STATUS_MSG, + &format!( + "private state entry with index `{name}` set to {value}" + ), + ); + state.insert(name, value); + state_updated = true; } } else { save_error = true; @@ -1033,28 +958,25 @@ impl Task for LuaTask { LOG_STATUS_MSG, &format!( "(trigger: {trigger_name}) checking results: {}", - &self.repr_checks(), + self.repr_checks(), ), ); if self.expect_all { failure_reason = FailureReason::NoFailure; for (varname, value) in self.expected.iter() { if let Some(res) = match value { - LuaValue::LuaString(v) => { - let r: std::result::Result = - globals.get(varname.as_str()); - if let Ok(r) = r { Some(r == *v) } else { None } - } - LuaValue::LuaNumber(v) => { - let r: std::result::Result = - globals.get(varname.as_str()); - if let Ok(r) = r { Some(r == *v) } else { None } - } - LuaValue::LuaBoolean(v) => { - let r: std::result::Result = - globals.get(varname.as_str()); - if let Ok(r) = r { Some(r == *v) } else { None } - } + LuaValue::LuaString(v) => globals + .get(varname.as_str()) + .map(|x: String| Some(x == *v)) + .unwrap_or(None), + LuaValue::LuaNumber(v) => globals + .get(varname.as_str()) + .map(|x: f64| Some(x == *v)) + .unwrap_or(None), + LuaValue::LuaBoolean(v) => globals + .get(varname.as_str()) + .map(|x: bool| Some(x == *v)) + .unwrap_or(None), } { if !res { self.log( @@ -1082,37 +1004,33 @@ impl Task for LuaTask { } } } else { + // this case is simplified compared fo `expect_all` failure_reason = FailureReason::VariableMatch; for (varname, value) in self.expected.iter() { - if let Some(res) = match value { - LuaValue::LuaString(v) => { - let r: std::result::Result = - globals.get(varname.as_str()); - if let Ok(r) = r { Some(r == *v) } else { None } - } - LuaValue::LuaNumber(v) => { - let r: std::result::Result = - globals.get(varname.as_str()); - if let Ok(r) = r { Some(r == *v) } else { None } - } - LuaValue::LuaBoolean(v) => { - let r: std::result::Result = - globals.get(varname.as_str()); - if let Ok(r) = r { Some(r == *v) } else { None } - } + if match value { + LuaValue::LuaString(v) => globals + .get(varname.as_str()) + .map(|x: String| x == *v) + .unwrap_or(false), + LuaValue::LuaNumber(v) => globals + .get(varname.as_str()) + .map(|x: f64| x == *v) + .unwrap_or(false), + LuaValue::LuaBoolean(v) => globals + .get(varname.as_str()) + .map(|x: bool| x == *v) + .unwrap_or(false), } { - if res { - self.log( - LogType::Debug, - LOG_WHEN_PROC, - LOG_STATUS_OK, - &format!( - "(trigger: {trigger_name}) result match on at least one variable ({varname}): success" - ), - ); - failure_reason = FailureReason::NoFailure; - break; - } + self.log( + LogType::Debug, + LOG_WHEN_PROC, + LOG_STATUS_OK, + &format!( + "(trigger: {trigger_name}) result match on at least one variable ({varname}): success" + ), + ); + failure_reason = FailureReason::NoFailure; + break; } } } diff --git a/src/task/registry.rs b/src/task/registry.rs index ba64066..074d716 100644 --- a/src/task/registry.rs +++ b/src/task/registry.rs @@ -22,8 +22,8 @@ use unique_id::Generator; use unique_id::sequence::SequenceGenerator; use super::base::{Task, TaskRef}; -use crate::common::logging::{LogType, log}; -use crate::common::wres::{Error, Kind, Result}; +use crate::utility::logging::{LogType, log}; +use crate::utility::result::{Error, Kind, Result}; use crate::constants::*; // module-wide values @@ -128,7 +128,7 @@ impl TaskRegistry { /// # Arguments /// /// * `task_ref` - an object implementing the `base::Task` trait, provided - /// to the function as a `Box` aka `TaskRef` + /// to the function as a `Box` aka `TaskRef` /// /// # Returns /// @@ -160,17 +160,9 @@ impl TaskRegistry { let sessions = sessions.lock(); if *sessions == 0 { if self.has_task(&name) { - match self.remove_task(&name) { - Ok(_) => { - return Ok(self.add_task(task_ref)); - } - _ => { - return Err(Error::new(Kind::Failed, ERR_TASKREG_CANNOT_PULL_TASK)); - } - } - } else { - return Ok(self.add_task(task_ref)); + self.remove_task(&name)?; } + Ok(self.add_task(task_ref)) } else { let queue = self.items_to_add.clone(); let mut queue = queue.lock(); @@ -184,9 +176,8 @@ impl TaskRegistry { LOG_STATUS_OK, &format!("registry busy: task {name} set to be added when no tasks are running"), ); + Ok(true) } - - Ok(true) } /// Remove a named task from the list and give it back stored in a Box. @@ -313,7 +304,7 @@ impl TaskRegistry { ) -> Result>>> { assert!( self.has_all_tasks(names), - "some tasks not found in registry for condition `{trigger_name}`" + "some tasks not found in registry for condition `{trigger_name}`", ); let mut res: HashMap>> = HashMap::new(); @@ -507,7 +498,7 @@ impl TaskRegistry { ) -> Result>>> { assert!( self.has_all_tasks(names), - "some tasks not found in registry for condition `{trigger_name}`" + "some tasks not found in registry for condition `{trigger_name}`", ); // count the active running sessions: there can be more than a diff --git a/src/utility/logging.rs b/src/utility/logging.rs new file mode 100644 index 0000000..74bce14 --- /dev/null +++ b/src/utility/logging.rs @@ -0,0 +1,338 @@ +//! The common logging system is a simplified version of what is available in +//! the `log` crate, and all logging functions shall use this common module. +//! +//! Some notes on logging: +//! +//! * The log messages are composed by +//! - the timestamp +//! - the application name (see below) in brackets +//! - the log level +//! - the log message +//! * The log message in turn has the following form: +//! `context: [MSGTYPE] human readable message` +//! where +//! - the context is usually constructed with two space-separated strings +//! indicating the part of the program where a certain message is issued +//! - MSGTYPE (in square brackets) consists of two or more alphanumeric +//! strings, separated by slashes, whose first two are described below +//! and the further ones may depend on the first two +//! - the human readable message is an explanation of what happened. +//! +//! The first two elements in MSGTYPE indicate in which point of an operation +//! the event occurs, and the type of event. The first element can be one of: +//! +//! * _INIT_ if the event occurs in an initialization phase +//! * _START_ if the event occurs while starting something, service or process +//! * _PROC_ if the event occurs while processing or during some activity +//! * _END_ if the event occurs at the end of a service or process +//! * _HIST_ is a _trace level only_ message emitted to show history on GUI: +//! in this case _MSG_ is sent at the beginning of task execution, and +//! _OK_, _FAIL_ or _IND_ are sent at the end (resp. on success, +//! failure or _indeterminate_ outcome) +//! * _BUSY_ is also a _trace level only_ message emitted to allow a GUI to +//! show the application status (for instance using an icon in the +//! tray area): when there are one or more conditions busy, the second +//! element is _YES_, otherwise _NO_ +//! * _PAUSE_ another _trace level only_ message emitted to allow a GUI to +//! change application status (for instance using a tray icon) when +//! the scheduler is paused: useful because an _internal command_ +//! based task might pause the scheduler unattendedly +//! +//! while the second can be one of: +//! +//! * _OK_ for expected outcomes or behaviours +//! * _FAIL_ for unexpected outcomes or behaviours +//! * _IND_ for indeterminate outcomes +//! * _MSG_ if the human-readable part is exclusively informational +//! * _ERR_ (may be followed by a dash `-` and a code) for errors to be +//! notified +//! * _YES_ (only occurs for _BUSY_ or _PAUSE_ indicators) means: application +//! is busy or has been paused +//! * _NO_ (only occurs for _BUSY_ or _PAUSE_ indicators) means: application +//! is _not_ busy or has been resumed +//! +//! This should help using the log as a way of communicating to a wrapper +//! utility the state of the scheduler, and possibily give the opportunity to +//! organize communication to the user in a friendlier way. +//! +//! This module also contains common enums, traits, structs, and functions +//! shared between items that use the same technology. Shared collections are +//! organized in modules: +//! +//! * `cmditem` for assets common to command based tasks and conditions +//! * `luaitem` for assets common to Lua based tasks and conditions +//! * `dbusitem` for assets common to DBus based conditions and events +//! * `wmiitem` for assets common to WMI based conditions and events +//! * `wres` for the _whenever_ specific `Result`, that has automations +//! for conversion from many other result types +//! +//! in order to avoid behaviour discrepancies, and possibly to save some +//! memory by avoiding unnecessary duplications. + +use lazy_static::lazy_static; +use parking_lot::RwLock; + +use crate::constants::{APP_NAME, ERR_LOGGER_NOT_INITIALIZED}; +use flexi_logger::{DeferredNow, FileSpec, Logger, style}; +use log::Record; +use log::{debug, error, info, trace, warn}; +use nu_ansi_term::Style; +use serde_json::json; +use std::path::PathBuf; + +// the following global flag is exposed here because it looks like there is +// no actual way to pass anything but a string as payload to the logger, so +// the common logging function should know whether the logger is initialized +// to return JSON message and build the JSON payload itself +lazy_static! { + static ref LOGGER_EMITS_JSON: RwLock = RwLock::new(false); +} + +// time stamp format that is used by the provided format functions. +const NOW_FMT: &str = "%Y-%m-%dT%H:%M:%S%.3f"; +const NOW_FMT_FULL: &str = "%Y-%m-%dT%H:%M:%S%.6f"; + +// log formatters +fn log_format_plain( + w: &mut dyn std::io::Write, + now: &mut DeferredNow, + record: &Record, +) -> Result<(), std::io::Error> { + write!( + w, + "[{}] ({APP_NAME}) {} {}", + now.format(NOW_FMT), + format_args!("{:5}", record.level()), + record.args(), + ) +} + +fn log_format_json( + w: &mut dyn std::io::Write, + now: &mut DeferredNow, + record: &Record, +) -> Result<(), std::io::Error> { + let header = json!({ + "application": APP_NAME, + "time": now.format(NOW_FMT_FULL).to_string(), + "level": record.level().to_string(), + }); + let payload = record.args(); + write!(w, "{{\"header\":{header},\"contents\":{payload}}}") +} + +fn log_format_colors( + w: &mut dyn std::io::Write, + now: &mut DeferredNow, + record: &Record, +) -> Result<(), std::io::Error> { + let level = record.level(); + let bold = Style::new().bold(); + let dimmed = Style::new().dimmed(); + write!( + w, + "[{}] {} {} {}", + format_args!("{}", now.format(NOW_FMT)), + dimmed.paint(format!("({APP_NAME})")), + style(level).paint(format!("{:5}", level.to_string())), + bold.paint(record.args().to_string()), + ) +} + +/// Log levels (from most verbose to least) +pub enum LogType { + Trace, + Debug, + Info, + Warn, + Error, +} + +/// Logger initialization: if `filename` is not given, the log will be +/// sent to stdout and use color (and the `append` parameter will be +/// ignored); otherwise `filename` will be used as path for the log file: +/// causes an error if it's not possible to open the log file. +pub fn init( + level: LogType, + filename: Option, + append: bool, + logcolor: bool, // these three values are mutually + logplain: bool, // exclusive by construction of the + logjson: bool, // main `clap` parser +) -> std::io::Result { + let level = match level { + LogType::Trace => "trace", + LogType::Debug => "debug", + LogType::Info => "info", + LogType::Warn => "warn", + LogType::Error => "error", + }; + + // the following line is to avoid other crates logging (e.g. `zbus`) + // so it can be commented out for debugging purposes and replaced with + // the subsequent commented out line. A reminder to documentation: + // https://docs.rs/flexi_logger/latest/flexi_logger/struct.LogSpecification.html + // FIXME: maybe we can choose the actual configuration string + // automatically according to the current build settings? + let logspec = format!("whenever={level}"); + // let logspec = format!("{level}"); + + let mut logger; + logger = Logger::try_with_str(logspec); + match logger { + Ok(l) => { + if let Some(fname) = filename { + let log_format = { + if logcolor { + log_format_plain + } else if logplain { + log_format_plain + } else if logjson { + *LOGGER_EMITS_JSON.write() = true; + log_format_json + } else { + log_format_plain + } + }; + let mut pb = PathBuf::from(&fname); + if pb.parent().is_none() || pb.parent().unwrap().to_str().unwrap_or("").is_empty() { + pb = { + let mut dir = PathBuf::from("."); + dir.push(pb); + dir + } + } + let fspec = + FileSpec::try_from(&pb).map_err(|e| std::io::Error::other(e.to_string()))?; + logger = Ok( + l.log_to_file(fspec).format_for_files(log_format), // .write_mode(WriteMode::BufferAndFlush) + ); + if append { + logger = Ok(logger.unwrap().append()); + } + } else { + let log_format = { + if logcolor { + log_format_colors + } else if logplain { + log_format_plain + } else if logjson { + *LOGGER_EMITS_JSON.write() = true; + log_format_json + } else { + log_format_colors + } + }; + // in json mode to console we also support capture by pipes + // so that wrappers may use stdout to get updates; it also + // redirects the logger's own errors to a black hole in + // order to avoid polluting a wrapper + if logjson { + logger = Ok(l + .format_for_stdout(log_format) + .write_mode(flexi_logger::WriteMode::Direct) + .error_channel(flexi_logger::ErrorChannel::DevNull) + .log_to_stdout()); + } else { + logger = Ok(l.format_for_stdout(log_format).log_to_stdout()); + } + } + } + _ => { + return Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + ERR_LOGGER_NOT_INITIALIZED, + )); + } + } + if let Err(_e) = logger.unwrap().start() { + return Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + ERR_LOGGER_NOT_INITIALIZED, + )); + } + + Ok(true) +} + +/// Common log function. The parameters are granular in order to achieve +/// two benefits: the first is that for most of them a constant can be +/// used, thus reducing the possibility of non-conformant log messages +/// (which may arise on typos) and, to some extent, the executable size; +/// the second is that JSON log messages can be as fine-grained as +/// needed. The constants to be used are defined in _constants.rs_, and +/// in particular: +/// +/// * `emitter` is one of the `LOG_EMITTER_...` constants +/// * `action` is one of the `LOG_ACTION_...` constants +/// * `when` is one of the `LOG_WHEN_...` constants +/// * `status` is one of the `LOG_STATUS_...` constants +/// +/// while non-constant parameters must be defined as follows +/// +/// * `item` can be a tuple consisting of item _name_ and _id_ +/// * `message` is the only arbitrary string that can be passed +/// +/// This allows JSON messages to be easily interpretable by a wrapper +/// according to the hints given in the documentation. +pub fn log( + severity: LogType, + emitter: &str, + action: &str, + item: Option<(&str, i64)>, + when: &str, + status: &str, + message: &str, +) { + let payload = if *LOGGER_EMITS_JSON.read() { + let context = if let Some((item, item_id)) = item { + json!({ + "emitter": emitter, + "action": action, + "item": item, + "item_id": item_id, + }) + } else { + json!({ + "emitter": emitter, + "action": action, + "item": null, + "item_id": null, + }) + }; + let message_type = json!({ + "when": when, + "status": status, + }); + json!({ + "context": context, + "message_type": message_type, + "message": message, + }) + .to_string() + } else { + let item_repr = if let Some((name, id)) = item { + format!(" {name}/{id}") + } else { + String::new() + }; + format!("{emitter} {action}{item_repr}: [{when}/{status}] {message}") + }; + match severity { + LogType::Trace => { + trace!("{payload}") + } + LogType::Debug => { + debug!("{payload}") + } + LogType::Info => { + info!("{payload}") + } + LogType::Warn => { + warn!("{payload}") + } + LogType::Error => { + error!("{payload}") + } + } +} diff --git a/src/utility/mod.rs b/src/utility/mod.rs new file mode 100644 index 0000000..bf4b55e --- /dev/null +++ b/src/utility/mod.rs @@ -0,0 +1,3 @@ +pub mod logging; +pub mod named_mutex; +pub mod result; diff --git a/src/utility/named_mutex.rs b/src/utility/named_mutex.rs new file mode 100644 index 0000000..f8c150b --- /dev/null +++ b/src/utility/named_mutex.rs @@ -0,0 +1,162 @@ +//! A module providing named mutexes that can be shared across all threads. +//! +//! This module implements a global set of named mutexes that can be locked +//! and released by any thread. + +#![cfg(feature = "lua_sync")] +#![allow(dead_code)] + +// NOTE: originally this module was AI generated, however the poor guy +// kept making mistakes of all sorts, from completely reinventing common +// (or standard) libraries API, to writing failing tests, and so on. In +// the end, however, it made me discover some interesting libraries and +// rethink on how to implement the functionality by hand. Now the module +// is hand coded (apart from doc comments) + +use lazy_static::lazy_static; +use parking_lot::{Condvar, Mutex}; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; + +// global map of named mutexes +#[derive(Debug)] +struct SharedLock { + busy: Mutex, + notifier: Condvar, +} + +#[allow(dead_code)] +impl SharedLock { + pub fn new_free() -> Self { + SharedLock { + busy: Mutex::new(false), + notifier: Condvar::new(), + } + } + + // busy by default + pub fn new() -> Self { + SharedLock { + busy: Mutex::new(true), + notifier: Condvar::new(), + } + } + + // this reclaims a named mutex, possibly with a timeout: if able to + // capture it, then it changes its busy state to true and returns + // true to signal that it succeeded + pub fn claim(&self, timeout: Option) -> bool { + let mut busy = self.busy.lock(); + if *busy { + if let Some(timeout) = timeout { + if self.notifier.wait_for(&mut busy, timeout).timed_out() { + false + } else { + *busy = true; + true + } + } else { + self.notifier.wait(&mut busy); + *busy = true; + true + } + } else { + *busy = true; + true + } + } + + // free the mutex and signal the next waiting thread that it can go on; + // this fails only if there was nothing to release + pub fn free(&self) -> bool { + let mut busy = self.busy.lock(); + if *busy { + *busy = false; + self.notifier.notify_one(); + true + } else { + false + } + } +} + +lazy_static! { + static ref NMUTEX_MAP: Arc>>> = + Arc::new(Mutex::new(HashMap::new())); +} + +// add or retrieve a lock +fn get_slock(name: &str) -> Arc { + let map = NMUTEX_MAP.clone(); + let mut map = map.lock(); + let s1 = map + .entry(name.to_string()) + .or_insert_with(|| Arc::new(SharedLock::new_free())); + s1.clone() +} + +// the actual library, as per specification + +/// Attempts to acquire and lock a named mutex. +/// +/// If a mutex with the given `name` doesn't exist, it creates one and +/// locks it immediately. /// If a mutex with the given `name` exists, +/// it attempts to lock it. +/// +/// # Arguments +/// +/// * `name` - The name of the mutex to lock +/// * `timeout` - Maximum time to wait for the lock. +/// - `None`: Wait indefinitely +/// - `Some(duration)`: Wait for the specified duration +/// +/// # Returns +/// +/// Returns `true` if the mutex was successfully locked, `false` if the +/// timeout was exceeded. +/// +/// # Examples +/// +/// ```ignore +/// if namedmutex_lock("Mux01", None) { +/// println!("locked!"); +/// std::thread::sleep(std::time::Duration::from_millis(500)); +/// let _ = namedmutex_release("Mux01"); +/// } +/// ``` +/// +/// ```ignore +/// if !namedmutex_lock("Mux01", Some(Duration::from_millis(1000))) { +/// println!("could not lock the mutex"); +/// } +/// ``` +pub fn namedmutex_lock(name: &str, timeout: Option) -> bool { + let sl = &mut get_slock(name); + sl.claim(timeout) +} + +/// Releases a previously locked named mutex. +/// +/// # Arguments +/// +/// * `name` - The name of the mutex to release +/// +/// # Returns +/// +/// Returns `true` if the mutex was successfully released, `false` if a +/// mutex with the specified name was not found or was not locked by the +/// current thread. +/// +/// # Examples +/// +/// ```ignore +/// if namedmutex_lock("Mux01", None) { +/// // ... do some work ... +/// namedmutex_release("Mux01"); +/// } +/// ``` +pub fn namedmutex_release(name: &str) -> bool { + let sl = &mut get_slock(name); + sl.free() +} diff --git a/src/utility/result.rs b/src/utility/result.rs new file mode 100644 index 0000000..a9f3f0d --- /dev/null +++ b/src/utility/result.rs @@ -0,0 +1,261 @@ +//! A common result type: catching errors from modules used throughout +//! the entire code. The corresponding error carries some information +//! about what went wrong. + +use mlua; +use notify; +use std::{self, fmt, sync::PoisonError}; + +use crate::constants::{ERR_FAILED, ERR_LOCK_FAILED}; + +/// Types of specific errors +#[non_exhaustive] +#[derive(Debug, Clone)] +pub enum Kind { + Forbidden, + Unsupported, + Unavailable, + Unconverted, + Unparsed, + Busy, + Invalid, + Failed, + Empty, + // ... + Unknown, +} + +impl fmt::Display for Kind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Kind::Forbidden => "not permitted", + Kind::Unsupported => "not supported", + Kind::Unavailable => "not available", + Kind::Unconverted => "not converted", + Kind::Unparsed => "not parsed", + Kind::Busy => "resource busy", + Kind::Invalid => "invalid", + Kind::Failed => "failed", + Kind::Empty => "empty", + Kind::Unknown => "unknown", + } + ) + } +} + +/// Describes the origin of the error: if `Native` the error was originated +/// natively, otherwise the field is set by another error that is converted +/// into `Error` via a dedicated `From` trait implementation. +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq)] +pub enum Origin { + Native, + Unit, + StdIo, + Notify, + Sync, + Lua, + + #[cfg(feature = "dbus")] + DBus, + + #[cfg(windows)] + #[cfg(feature = "wmi")] + Wmi, + + // ... + Unknown, +} + +impl fmt::Display for Origin { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Origin::Native => "self", + Origin::Unit => "unit", + Origin::StdIo => "io", + Origin::Notify => "fschange", + Origin::Sync => "sync", + Origin::Lua => "lua", + + #[cfg(feature = "dbus")] + Origin::DBus => "dbus", + + #[cfg(windows)] + #[cfg(feature = "wmi")] + Origin::Wmi => "wmi", + + // ... + Origin::Unknown => "unknown", + } + ) + } +} + +/// The error type that is used throughout the application: implementations +/// of the `From` trait are used to implicitly convert from other error +/// types, which in turn set the `origin` property. +#[derive(Debug, Clone)] +pub struct Error { + kind: Kind, + origin: Origin, + message: String, // freeform message: owned in order to avoid lifetime management +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.origin != Origin::Native { + write!(f, "{} ({}): {}", self.kind, self.origin, self.message) + } else { + write!(f, "{}: {}", self.kind, self.message) + } + } +} + +// maybe the most important: From +impl From for Error { + fn from(e: std::io::Error) -> Self { + Self { + kind: match e.kind() { + std::io::ErrorKind::Unsupported => Kind::Unsupported, + std::io::ErrorKind::PermissionDenied => Kind::Forbidden, + std::io::ErrorKind::InvalidData => Kind::Invalid, + std::io::ErrorKind::InvalidInput => Kind::Invalid, + _ => Kind::Unknown, + }, + origin: Origin::StdIo, + message: e.to_string(), + } + } +} + +// notify (fschange) errors +impl From for Error { + fn from(e: notify::Error) -> Self { + Self { + kind: Kind::Failed, + origin: Origin::Notify, + message: e.to_string(), + } + } +} + +// Lua errors +impl From for Error { + fn from(e: mlua::Error) -> Self { + Self { + kind: Kind::Failed, + origin: Origin::Lua, + message: e.to_string(), + } + } +} + +// zbus errors +#[cfg(feature = "dbus")] +impl From for Error { + fn from(e: zbus::Error) -> Self { + Self { + kind: Kind::Failed, + origin: Origin::DBus, + message: e.to_string(), + } + } +} + +// wmi errors +#[cfg(windows)] +#[cfg(feature = "wmi")] +impl From for Error { + fn from(e: wmi::WMIError) -> Self { + let kind = match e { + wmi::WMIError::ConvertBoolError(_) + | wmi::WMIError::ConvertStringError(_) + | wmi::WMIError::ConvertLengthError(_) + | wmi::WMIError::ConvertDatetimeError(_) + | wmi::WMIError::ConvertDurationError(_) + | wmi::WMIError::ConvertVariantError(_) + | wmi::WMIError::ConvertError(_) => Kind::Unconverted, + wmi::WMIError::DeserializeValueError(_) + | wmi::WMIError::InvalidDeserializationVariantError(_) + | wmi::WMIError::SerdeError(_) => Kind::Invalid, + wmi::WMIError::ParseDatetimeError(_) + | wmi::WMIError::ParseFloatError(_) + | wmi::WMIError::ParseIntError(_) => Kind::Unparsed, + wmi::WMIError::UnimplementedArrayItem => Kind::Unavailable, + _ => Kind::Failed, + }; + Self { + kind, + origin: Origin::Wmi, + message: e.to_string(), + } + } +} + +// resource locking errors +impl From> for Error { + fn from(_: PoisonError) -> Self { + Self { + kind: Kind::Failed, + origin: Origin::Sync, + message: ERR_LOCK_FAILED.to_owned(), + } + } +} + +// errors based on the unit type +impl From<()> for Error { + fn from(_: ()) -> Self { + Self { + kind: Kind::Failed, + origin: Origin::Unit, + message: ERR_FAILED.to_owned(), + } + } +} + +// implements `Error` and provides access to properties +impl Error { + // this is used only to natively create an instance of `Error`: only + // conversions set the `origin` property to something different + pub fn new(kind: Kind, message: &str) -> Self { + Self { + kind, + origin: Origin::Native, + message: message.to_string(), + } + } + + // property access + pub fn kind(&self) -> &Kind { + &self.kind + } + + pub fn origin(&self) -> &Origin { + &self.origin + } + + pub fn message(&self) -> &str { + &self.message + } +} + +// possible last resort to allow conversions from pointers to errors +impl From> for Error { + fn from(e: Box) -> Self { + Self { + kind: Kind::Unknown, + origin: Origin::Unknown, + message: e.to_string(), + } + } +} + +/// Specific `Result` type that assumes `wres::Error` as its Err variant +pub type Result = std::result::Result;