From c57e92e7d5c0c4de9bd3f8a4c6d4ae04d482ebfd Mon Sep 17 00:00:00 2001 From: blob42 Date: Sat, 13 Jul 2024 20:09:50 +0200 Subject: [PATCH] StateMatcher and StateTracker traits - future possiblity to have multipler matchers such as memory, and cpu usage and their corresponding trackers - updated tests - Scheduler can take dyn jobs Signed-off-by: blob42 --- Cargo.lock | 110 ++++++------- TODO.md | 1 + src/lib.rs | 4 +- src/main.rs | 2 +- src/process.rs | 373 +++++++++++++++++++++++++++++-------------- src/sched.rs | 151 ++++++++++-------- src/state.rs | 24 +++ src/watch.rs | 6 +- tests/5382952proc.sh | 4 +- tests/process.rs | 29 ++-- tests/test.toml | 10 +- 11 files changed, 445 insertions(+), 269 deletions(-) create mode 100644 src/state.rs diff --git a/Cargo.lock b/Cargo.lock index b21b8e2..0b52e9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,9 +74,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "cfg-if" @@ -86,9 +86,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.5.7" +version = "4.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" +checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" dependencies = [ "clap_builder", "clap_derive", @@ -96,9 +96,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.7" +version = "4.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" +checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942" dependencies = [ "anstream", "anstyle", @@ -108,9 +108,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.5" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" +checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" dependencies = [ "heck", "proc-macro2", @@ -184,9 +184,9 @@ dependencies = [ [[package]] name = "either" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "env_filter" @@ -457,9 +457,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.85" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -614,18 +614,18 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", @@ -658,9 +658,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.66" +version = "2.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" dependencies = [ "proc-macro2", "quote", @@ -669,9 +669,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.30.12" +version = "0.30.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "732ffa00f53e6b2af46208fba5718d9662a421049204e156328b66791ffa15ae" +checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3" dependencies = [ "cfg-if", "core-foundation-sys", @@ -684,18 +684,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" dependencies = [ "proc-macro2", "quote", @@ -711,7 +711,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.14", + "toml_edit 0.22.15", ] [[package]] @@ -736,9 +736,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.14" +version = "0.22.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" +checksum = "d59a3a72298453f564e2b111fa896f8d07fabb36f51f06d7e875fc5e0b5a3ef1" dependencies = [ "indexmap", "serde", @@ -794,7 +794,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ "windows-core", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -803,7 +803,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -821,7 +821,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -841,18 +841,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -863,9 +863,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -875,9 +875,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -887,15 +887,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -905,9 +905,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -917,9 +917,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -929,9 +929,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -941,9 +941,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" diff --git a/TODO.md b/TODO.md index 7eb9b32..81e496e 100644 --- a/TODO.md +++ b/TODO.md @@ -1,3 +1,4 @@ +- match multiple patterns - cmd exec: [x] repeat on cmd failure [x] disable profile on cmd failure diff --git a/src/lib.rs b/src/lib.rs index 4531b47..4d95f35 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,5 @@ -#![allow(dead_code)] -#![allow(unused_variables)] - pub mod process; +pub mod state; pub mod sched; pub mod config; diff --git a/src/main.rs b/src/main.rs index 3222a2f..cfa1321 100644 --- a/src/main.rs +++ b/src/main.rs @@ -62,7 +62,7 @@ fn main() -> anyhow::Result<()> { let program_cfg = config::read_config(cli.config).context("missing config file")?; - let mut scheduler = Scheduler::new(program_cfg.profiles); + let mut scheduler = Scheduler::from_profiles(program_cfg.profiles); //TODO: own thread scheduler.run(); Ok(()) diff --git a/src/process.rs b/src/process.rs index 56d2e8f..95097ba 100644 --- a/src/process.rs +++ b/src/process.rs @@ -4,11 +4,16 @@ use std::{fmt::Display, time::Duration}; use log::debug; use memchr; use serde::Deserialize; -use sysinfo::System; +use crate::state::{StateMatcher, StateTracker}; + +#[cfg(test)] +use mock_instant::thread_local::Instant; #[cfg(not(test))] use std::time::Instant; + + #[derive(Debug, Clone, PartialEq)] pub enum ProcState { NeverSeen, @@ -16,12 +21,45 @@ pub enum ProcState { NotSeen, } +#[derive(Debug, Clone)] +pub struct ProcLifetime { + first_seen: Option, + last_seen: Option, + last_refresh: Option, + prev_refresh: Option, + prev_state: Option, + state: ProcState, +} + + + +impl ProcLifetime { + pub fn new() -> ProcLifetime { + Self { + first_seen: None, + last_seen: None, + last_refresh: None, + prev_refresh: None, + prev_state: None, + state: ProcState::NeverSeen, + } + } + + // /// the state is entering or exiting a Seen/NotSeen state + // pub fn is_switching(&self) -> bool { + // (matches!(self.state, ProcState::Seen) + // && matches!(self.prev_state, Some(ProcState::NotSeen))) || + // (matches!(self.state, ProcState::NotSeen) + // && matches!(self.prev_state, Some(ProcState::Seen))) + // } +} + impl Display for ProcState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let state = match self { - Self::NeverSeen => "never_seen", - Self::Seen => "seen", - Self::NotSeen => "not_seen", + ProcState::NeverSeen => "never_seen", + ProcState::Seen => "seen", + ProcState::NotSeen => "not_seen", }; write!(f, "{state}") } @@ -47,158 +85,236 @@ impl ProcCondition { } } - #[derive(Debug)] pub struct Process { pattern: String, - first_seen: Option, - last_seen: Option, - last_refresh: Option, - prev_refresh: Option, - state: ProcState, - prev_state: Option, + lifetime: ProcLifetime, pids: Vec, } impl Process { - pub fn from_pattern(pat: String) -> Self { + + pub fn build(pat: String, state_matcher: ProcLifetime) -> Self { Self { pattern: pat, - first_seen: None, - last_seen: None, - last_refresh: None, - prev_refresh: None, - state: ProcState::NeverSeen, - prev_state: None, + lifetime: state_matcher, pids: vec![], } } - pub fn refresh(&mut self, sysinfo: &System, last_refresh: Instant) { - self.pids = sysinfo.processes_by_name(&self.pattern) - .map(|p| p.pid().into()) - .collect::>(); - self.prev_refresh = self.last_refresh; - self.last_refresh = Some(last_refresh); - - self.update_state(); + pub fn from_pattern(pat: String) -> Self { + Self { + pattern: pat, + lifetime: ProcLifetime::new(), + pids: vec![], + } } - fn update_state(&mut self) { - + // TODO!: remove + // pub fn refresh(&mut self, sysinfo: &System, last_refresh: Instant) -> ProcLifetime { + // self.pids = sysinfo + // .processes_by_name(&self.pattern) + // .map(|p| p.pid().into()) + // .collect::>(); + // self.lifetime.prev_refresh = self.lifetime.last_refresh; + // self.lifetime.last_refresh = Some(last_refresh); + // + // self.update_state(); + // self.lifetime.clone() + // } + + fn update_inner_state(&mut self) { if self.pids.is_empty() { // no change if process still never seen - if !matches!(self.state, ProcState::NeverSeen) { - self.prev_state = Some(self.state.clone()); - self.state = ProcState::NotSeen; + if !matches!(self.state(), ProcState::NeverSeen) { + self.lifetime.prev_state = Some(self.lifetime.state.clone()); + self.lifetime.state = ProcState::NotSeen; debug!("<{}>: process disappread", self.pattern); } else { - self.prev_state = Some(ProcState::NeverSeen); + self.lifetime.prev_state = Some(ProcState::NeverSeen); debug!("<{}>: never seen so far", self.pattern); } // process found } else { - match self.state { + match self.state() { ProcState::NeverSeen => { - self.first_seen = self.last_refresh; + self.lifetime.first_seen = self.lifetime.last_refresh; debug!("<{}>: process seen first time", self.pattern); - }, + } ProcState::NotSeen => { debug!("<{}>: process reappeared", self.pattern); - }, + } ProcState::Seen => { debug!("<{}>: process still running", self.pattern); } } - self.prev_state = Some(self.state.clone()); - self.state = ProcState::Seen; - self.last_seen = self.last_refresh; - + self.lifetime.prev_state = Some(self.lifetime.state.clone()); + self.lifetime.state = ProcState::Seen; + self.lifetime.last_seen = self.lifetime.last_refresh; } } /// matches processes on the full path to the executable fn matches_exe(&self, info: &sysinfo::System) -> bool { - info.processes().values().filter_map(|proc| { - let finder = memchr::memmem::Finder::new(&self.pattern); - proc.exe().and_then(|exe_name| finder.find(exe_name.as_os_str().as_bytes())) - }).next().is_some() + info.processes() + .values() + .filter_map(|proc| { + let finder = memchr::memmem::Finder::new(&self.pattern); + proc.exe() + .and_then(|exe_name| finder.find(exe_name.as_os_str().as_bytes())) + }) + .next() + .is_some() } /// matches processes on the full command line fn matches_cmdline(&self, info: &sysinfo::System) -> bool { - info.processes().values().filter_map(|proc| { - let finder = memchr::memmem::Finder::new(&self.pattern); - finder.find(proc.cmd().join(" ").as_bytes()) - }).next().is_some() + info.processes() + .values() + .filter_map(|proc| { + let finder = memchr::memmem::Finder::new(&self.pattern); + finder.find(proc.cmd().join(" ").as_bytes()) + }) + .next() + .is_some() } /// matches processes the command name only fn matches_name(&self, info: &sysinfo::System) -> bool { info.processes_by_name(&self.pattern).next().is_some() - } fn matches_pattern(&self, info: &sysinfo::System, match_by: ProcessMatchBy) -> bool { match match_by { - ProcessMatchBy::ExePath => {self.matches_exe(info) } - ProcessMatchBy::Cmdline => {self.matches_cmdline(info)} - ProcessMatchBy::Name => {self.matches_name(info)}, + ProcessMatchBy::ExePath => self.matches_exe(info), + ProcessMatchBy::Cmdline => self.matches_cmdline(info), + ProcessMatchBy::Name => self.matches_name(info), } + } + + // pub fn matches(&self, c: ProcCondition) -> bool { + // match c { + // ProcCondition::Seen(_) => { + // if !matches!(self.get_state(), ProcState::Seen) { + // return false; + // }; + // if let Some(first_seen) = self.lifetime.first_seen { + // first_seen.elapsed() > c.span() + // } else { + // false + // } + // } + // ProcCondition::NotSeen(span) => { + // if !matches!(self.get_state(), ProcState::NotSeen | ProcState::NeverSeen) { + // false + // } else if let Some(last_seen) = self.lifetime.last_seen { + // last_seen.elapsed() > c.span() + // } else { + // matches!(self.get_state(), ProcState::NeverSeen) + // && self.get_state() == self.lifetime.prev_state.clone().unwrap() + // && self.lifetime.prev_refresh.is_some() + // && self.lifetime.prev_refresh.unwrap().elapsed() > span + // } + // } + // } + // } +} + + +impl StateTracker for Process { + /// updates the state and return a copy of the new state + fn update_state(&mut self, info: &sysinfo::System, t_refresh: Instant) -> impl StateMatcher { + self.pids = info + .processes_by_name(&self.pattern) + .map(|p| p.pid().into()) + .collect::>(); + self.lifetime.prev_refresh = self.lifetime.last_refresh; + self.lifetime.last_refresh = Some(t_refresh); + + self.update_inner_state(); + self.lifetime.clone() + } +} + +impl StateMatcher for Process { + type Condition = ProcCondition; + type State = ProcState; + + fn matches(&self, c: Self::Condition) -> bool { + self.lifetime.matches(c) + } + + fn state(&self) -> Self::State { + self.lifetime.state.clone() } - pub fn matches(&self, c: ProcCondition) -> bool { - match c { - ProcCondition::Seen(span) => { + fn prev_state(&self) -> Option { + self.lifetime.prev_state.clone() + } +} + +impl StateMatcher for ProcLifetime { + type Condition = ProcCondition; + type State = ProcState; + + fn matches(&self, cond: Self::Condition) -> bool { + match cond { + ProcCondition::Seen(_) => { if !matches!(self.state, ProcState::Seen) { return false; }; if let Some(first_seen) = self.first_seen { - first_seen.elapsed() > c.span() + first_seen.elapsed() > cond.span() } else { false } - }, + } ProcCondition::NotSeen(span) => { if !matches!(self.state, ProcState::NotSeen | ProcState::NeverSeen) { false } else if let Some(last_seen) = self.last_seen { - last_seen.elapsed() > c.span() - } else { matches!(self.state, ProcState::NeverSeen) && - self.state == self.prev_state.clone().unwrap() && - self.prev_refresh.is_some() && - self.prev_refresh.unwrap().elapsed() > span - + last_seen.elapsed() > cond.span() + } else { + matches!(self.state, ProcState::NeverSeen) + && self.prev_state.is_some() + && self.state == self.prev_state.clone().unwrap() + && self.prev_refresh.is_some() + && self.prev_refresh.unwrap().elapsed() > span } - } + } } } + + fn state(&self) -> Self::State { + self.state.clone() + } + + fn prev_state(&self) -> Option { + self.prev_state.clone() + } } enum ProcessMatchBy { ExePath, Cmdline, - Name + Name, } - - #[cfg(test)] -use mock_instant::global::Instant; - #[allow(unused_imports)] mod test { - use mock_instant::global::MockClock; - use crate::sched::Scheduler; use super::*; + use sysinfo::System; + use crate::{sched::Scheduler, state::*}; + use mock_instant::thread_local::MockClock; #[test] fn default_process() { let pat = "foo"; let p = Process::from_pattern(pat.into()); - assert!(matches!(p.state, ProcState::NeverSeen)) - } + assert!(matches!(p.state(), ProcState::NeverSeen)) + } // default process pattern matching (name) #[test] @@ -215,10 +331,11 @@ mod test { let mut not_p_match = Process::from_pattern("foobar_234324".into()); let mut sys = System::new(); sys.refresh_specifics(Scheduler::process_refresh_specs()); - p_match.refresh(&sys, Instant::now()); + + p_match.update_state(&sys, Instant::now()); assert!(!p_match.pids.is_empty()); - not_p_match.refresh(&sys, Instant::now()); + not_p_match.update_state(&sys, Instant::now()); assert!(not_p_match.pids.is_empty()); target.kill() @@ -233,103 +350,111 @@ mod test { fn match_pattern_cmdline() { todo!(); } - #[test] fn cond_seen_since() { + MockClock::set_time(Duration::ZERO); let cond_seen = ProcCondition::Seen(Duration::from_secs(5)); let mut p = Process::from_pattern("foo".into()); - p.last_refresh = Some(Instant::now()); - + p.lifetime.last_refresh = Some(Instant::now()); // no process detected initially // let mut action = proc_state.update(cond_seen.clone(), !detected, last_refresh); - assert!(matches!(p.state, ProcState::NeverSeen)); - assert!(!p.matches(cond_seen.clone())); + assert!(matches!(p.state(), ProcState::NeverSeen)); + assert!(!p.lifetime.matches(cond_seen.clone())); MockClock::advance(Duration::from_secs(2)); // process detected p.pids = vec![1]; - p.last_refresh = Some(Instant::now()); - let first_seen = p.last_refresh; - p.update_state(); - assert!(matches!(p.state, ProcState::Seen), "should be detected"); - assert!(!p.matches(cond_seen.clone()), "should match user condition"); + p.lifetime.last_refresh = Some(Instant::now()); + let first_seen = p.lifetime.last_refresh; + p.update_inner_state(); + assert!( + matches!(p.lifetime.state, ProcState::Seen), + "should be detected" + ); + assert!(!p.lifetime.matches(cond_seen.clone()), "should match user condition"); // process exceeded condition MockClock::advance(Duration::from_secs(6)); - p.last_refresh = Some(Instant::now()); - let last_seen = p.last_refresh.unwrap(); - p.update_state(); - assert!(p.matches(cond_seen.clone()), "should match user condition"); + p.lifetime.last_refresh = Some(Instant::now()); + let last_seen = p.lifetime.last_refresh.unwrap(); + p.update_inner_state(); + assert!(p.lifetime.matches(cond_seen.clone()), "should match user condition"); // process disappread MockClock::advance(Duration::from_secs(2)); p.pids = vec![]; - p.last_refresh = Some(Instant::now()); - p.update_state(); - assert!(matches!(p.state, ProcState::NotSeen), "should be not seen"); - assert!(p.last_seen.unwrap().elapsed() == last_seen.elapsed()); - assert!(!p.matches(cond_seen.clone()), "should not match user condition"); + p.lifetime.last_refresh = Some(Instant::now()); + p.update_inner_state(); + assert!( + matches!(p.lifetime.state, ProcState::NotSeen), + "should be not seen" + ); + assert!(p.lifetime.last_seen.unwrap().elapsed() == last_seen.elapsed()); + assert!( + !p.lifetime.matches(cond_seen.clone()), + "should not match user condition" + ); // process still not seen MockClock::advance(Duration::from_secs(5)); - p.last_refresh = Some(Instant::now()); - p.update_state(); + p.lifetime.last_refresh = Some(Instant::now()); + p.update_inner_state(); // 5+2 = 7 - assert!(p.last_seen.unwrap().elapsed() == Duration::from_secs(7)); - assert!(!p.matches(cond_seen.clone())); - assert!(p.first_seen.unwrap() == first_seen.unwrap()); + assert!(p.lifetime.last_seen.unwrap().elapsed() == Duration::from_secs(7)); + assert!(!p.lifetime.matches(cond_seen.clone())); + assert!(p.lifetime.first_seen.unwrap() == first_seen.unwrap()); } #[test] fn test_not_seen_since() { + MockClock::set_time(Duration::ZERO); let cond_not_seen = ProcCondition::NotSeen(Duration::from_secs(5)); let mut p = Process::from_pattern("foo".into()); - p.last_refresh = Some(Instant::now()); - let t1 = p.last_refresh; - p.update_state(); - assert!(matches!(p.state, ProcState::NeverSeen)); - assert!(p.last_refresh.is_some()); - + p.lifetime.last_refresh = Some(Instant::now()); + let t1 = p.lifetime.last_refresh; + p.update_inner_state(); + assert!(matches!(p.lifetime.state, ProcState::NeverSeen)); + assert!(p.lifetime.last_refresh.is_some()); // // Case 1: The process is never seen and the condition timeout is exceeded, triggering Run. MockClock::advance(Duration::from_secs(7)); - p.last_refresh = Some(Instant::now()); - p.prev_refresh = t1; - p.update_state(); + p.lifetime.last_refresh = Some(Instant::now()); + p.lifetime.prev_refresh = t1; + p.update_inner_state(); assert!(p.pids.is_empty(), "no pid should be detected"); - assert!(p.matches(cond_not_seen.clone())); - assert!(matches!(p.state, ProcState::NeverSeen)); + assert!(p.lifetime.matches(cond_not_seen.clone())); + assert!(matches!(p.lifetime.state, ProcState::NeverSeen)); MockClock::advance(Duration::from_secs(5)); // Case 2: A process is already running then disappears, the Run is triggered after the timeout. p.pids = vec![1]; - p.last_refresh = Some(Instant::now()); - p.update_state(); + p.lifetime.last_refresh = Some(Instant::now()); + p.update_inner_state(); assert!( - matches!(p.state, ProcState::Seen), + matches!(p.lifetime.state, ProcState::Seen), "process found but state is {} ", - p.state + p.state() ); - assert_eq!(p.last_seen, p.last_refresh); - assert!(!p.matches(cond_not_seen.clone())); + assert_eq!(p.lifetime.last_seen, p.lifetime.last_refresh); + assert!(!p.lifetime.matches(cond_not_seen.clone())); // process disappears MockClock::advance(Duration::from_secs(1)); p.pids = vec![]; - p.last_refresh = Some(Instant::now()); - p.update_state(); - assert!(matches!(p.state, ProcState::NotSeen)); + p.lifetime.last_refresh = Some(Instant::now()); + p.update_inner_state(); + assert!(matches!(p.lifetime.state, ProcState::NotSeen)); // Process now exceeded the absent limit and matches user cond MockClock::advance(Duration::from_secs(6)); - p.last_refresh = Some(Instant::now()); - p.update_state(); - assert!(matches!(p.state, ProcState::NotSeen)); - assert!(p.matches(cond_not_seen.clone())); - } + p.lifetime.last_refresh = Some(Instant::now()); + p.update_inner_state(); + assert!(matches!(p.lifetime.state, ProcState::NotSeen)); + assert!(p.lifetime.matches(cond_not_seen.clone())); + } } diff --git a/src/sched.rs b/src/sched.rs index af5cad4..2b80f2c 100644 --- a/src/sched.rs +++ b/src/sched.rs @@ -1,15 +1,21 @@ use serde::Deserialize; use std::{process::Command, sync::OnceLock, thread::sleep, time::Duration}; -#[cfg(not(test))] -use std::time::Instant; - use log::{debug, error, trace}; + #[cfg(test)] -use mock_instant::global::Instant; +use mock_instant::thread_local::Instant; + +#[cfg(not(test))] +use std::time::Instant; use sysinfo::{ProcessRefreshKind, RefreshKind, System, UpdateKind}; +use crate::{ + process::{ProcLifetime, ProcState}, + state::{StateMatcher, StateTracker}, +}; + use super::process::{ProcCondition, Process}; /// CmdSchedule is the base configuration unit, it can be defined one or many times. @@ -34,12 +40,6 @@ pub struct CmdSchedule { disabled: bool, } -pub struct Scheduler { - system_info: System, - //FIX: - jobs: Vec, -} - #[derive(Debug, Deserialize, Clone)] pub struct Profile { /// pattern of process name to match against @@ -67,43 +67,50 @@ fn default_watch_interval() -> Duration { Duration::from_secs(5) } -pub(crate) struct ProfileJob { +/// A job that can run in the scheduler +trait Job { + fn update(&mut self, sysinfo: &System, last_refresh: Instant); +} + +pub(crate) struct ProfileJob +where + T: StateTracker + StateMatcher, +{ profile: Profile, - process: Process, + object: T, } -impl ProfileJob { - pub fn new(profile: Profile) -> Self { +impl ProfileJob { + + pub fn from_profile(profile: Profile) -> Self { let pattern = profile.pattern.clone(); Self { profile, - process: Process::from_pattern(pattern), + object: Process::build(pattern, ProcLifetime::new()), } } +} + + + +impl Job for ProfileJob { - pub(crate) fn update_state(&mut self, sysinfo: &System, last_refresh: Instant) { - // let detected = sysinfo.processes_by_name(&self.conf.pattern).count() > 0; - // let detected = match sysinfo.processes_by_name(&self.conf.pattern).count() { - // 0 => Event::NotDetected, - // _ => Event::Detected(last_refresh), - // }; - - self.process.refresh(sysinfo, last_refresh); - - let enabled_cmds: Vec<_> = self - .profile - .commands - .iter_mut() - .filter(|c| !c.disabled) - .collect(); - // dbg!(&enabled_cmds); - - for cmd in enabled_cmds { - // let action = self - // .state - // .update(cmd.condition.clone(), detected, last_refresh); - - if self.process.matches(cmd.condition.clone()) { + fn update(&mut self, sysinfo: &System, last_refresh: Instant) { + // if we are entering or exiting the seen/not_seen state + { + let _ = self.object.update_state(sysinfo, last_refresh); + if (matches!(self.object.state(), ProcState::Seen) + && matches!(self.object.prev_state(), Some(ProcState::NotSeen))) || + (matches!(self.object.state(), ProcState::NotSeen) + && matches!(self.object.prev_state(), Some(ProcState::Seen))) + { + dbg!("run exec_end !"); + } + } + + // only process enabled commands + for cmd in self.profile.commands.iter_mut().filter(|c| !c.disabled) { + if self.object.matches(cmd.condition.clone()) { let out = Command::new(&cmd.exec[0]).args(&cmd.exec[1..]).output(); match out { @@ -118,7 +125,7 @@ impl ProfileJob { } } Err(e) => { - error!("failed to run cmd for {}", self.profile.pattern); + error!("{}: failed to run cmd for: {}", self.profile.pattern, e); cmd.disabled = true } } @@ -131,62 +138,74 @@ impl ProfileJob { } } +pub struct Scheduler { + system_info: System, + jobs: Vec>, +} + static PROCESS_REFRESH_SPECS: OnceLock = OnceLock::new(); -impl Scheduler { +impl Scheduler +{ const SAMPLING_RATE: Duration = Duration::from_secs(3); pub fn process_refresh_specs() -> RefreshKind { - *PROCESS_REFRESH_SPECS.get_or_init(||{ + *PROCESS_REFRESH_SPECS.get_or_init(|| { + let process_refresh_kind = ProcessRefreshKind::new() + .with_cmd(UpdateKind::Always) + .with_cwd(UpdateKind::Always) + .with_exe(UpdateKind::Always); - let process_refresh_kind = ProcessRefreshKind::new() - .with_cmd(UpdateKind::Always) - .with_cwd(UpdateKind::Always) - .with_exe(UpdateKind::Always); - - RefreshKind::new().with_processes(process_refresh_kind) + RefreshKind::new().with_processes(process_refresh_kind) }) } - pub fn new(profiles: Vec) -> Self { + pub fn new() -> Self { debug!("Using sampling rate of {:?}.", Self::SAMPLING_RATE); - let jobs: Vec = profiles - .iter() - .map(|p| ProfileJob::new(p.clone())) - .collect(); + Self { + system_info: System::new(), + jobs: Vec::new(), + } + } + + // NOTE: when other types of (matcher, tracker) will be available for other resources: + // Define type of profile in an enum and call the concrete version of the generic implmentation + pub fn from_profiles(profiles: Vec) -> Self + { + let mut jobs: Vec> = Vec::with_capacity(profiles.len()); + profiles.into_iter() + .map(ProfileJob::from_profile) + .for_each(|pj| jobs.push(Box::new(pj))); Self { system_info: System::new(), - jobs: profiles - .into_iter() - .map(ProfileJob::new) - .collect(), + jobs, + } } fn refresh_proc_info(&mut self) { - self.system_info.refresh_specifics(Self::process_refresh_specs()); + self.system_info + .refresh_specifics(Self::process_refresh_specs()); } pub fn run(&mut self) { loop { self.refresh_proc_info(); - // iterate over all watched processes and find matching ones in system info - // - // Process detections cases: - // - seen pattern + process exists - // - not seen pattern + process exists - // - seen pattern + no process - // - not seen pattern + no process - self.jobs .iter_mut() - .for_each(|j| j.update_state(&self.system_info, Instant::now())); + .for_each(|job| job.update(&self.system_info, Instant::now())); trace!("refresh sysinfo"); sleep(Self::SAMPLING_RATE); } } } + +impl Default for Scheduler { + fn default() -> Self { + Self::new() + } +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..bbe97bb --- /dev/null +++ b/src/state.rs @@ -0,0 +1,24 @@ + +#[cfg(not(test))] +use std::time::Instant; + +#[cfg(test)] +use mock_instant::thread_local::Instant; + +pub trait StateMatcher { + type Condition; + type State; + + fn matches(&self, c: Self::Condition) -> bool; + fn state(&self) -> Self::State; + fn prev_state(&self) -> Option; + + // state is exiting + fn exiting(&self) -> Option { + self.prev_state().filter(|_s|{ ! matches!(self.state(), _s) }) + } +} + +pub trait StateTracker { + fn update_state(&mut self, info: &sysinfo::System, t_refresh: Instant) -> impl StateMatcher; +} diff --git a/src/watch.rs b/src/watch.rs index 3d8625f..9509909 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -29,13 +29,15 @@ //! - `process`: Contains the definition and implementation of `ProcessWatch`, `ProcCondition`, and `ProcessWatchConfig`. use std::{collections::HashMap, thread::sleep, time::Duration}; -#[cfg(not(test))] -use std::time::Instant; use log::trace; + #[cfg(test)] use mock_instant::global::Instant; +#[cfg(not(test))] +use std::time::Instant; + use serde::{Deserialize, Deserializer}; use std::process::Command; use sysinfo::{Pid, Process, ProcessRefreshKind, RefreshKind, System, UpdateKind}; diff --git a/tests/5382952proc.sh b/tests/5382952proc.sh index 3ac27dd..2ddc10e 100755 --- a/tests/5382952proc.sh +++ b/tests/5382952proc.sh @@ -1,6 +1,4 @@ #!/bin/sh -while true; do - sleep 1 -done +sleep 300 diff --git a/tests/process.rs b/tests/process.rs index 4f006db..d099b56 100644 --- a/tests/process.rs +++ b/tests/process.rs @@ -1,6 +1,6 @@ use std::time::{Duration, Instant}; -use pswatch::{process::{self, ProcCondition}, sched::Scheduler}; +use pswatch::{process::{self, ProcCondition, ProcLifetime}, sched::Scheduler, state::*}; use rstest::rstest; use sysinfo::System; @@ -34,14 +34,14 @@ fn match_cond_seen( let pat = "538295"; let mut p = process::Process::from_pattern(pat.into()); - p.refresh(&s, Instant::now()); + p.update_state(&s, Instant::now()); let cond = ProcCondition::Seen(cond_span); std::thread::sleep(test_span); s.refresh_specifics(Scheduler::process_refresh_specs()); - p.refresh(&s, Instant::now()); + p.update_state(&s, Instant::now()); // process exceeded cond assert_eq!(p.matches(cond), should_match, @@ -74,13 +74,13 @@ fn match_cond_not_seen( let mut p = process::Process::from_pattern(pat.into()); s.refresh_specifics(Scheduler::process_refresh_specs()); let t1 = Instant::now(); - p.refresh(&s, t1); + p.update_state(&s, t1); std::thread::sleep(test_span); s.refresh_specifics(Scheduler::process_refresh_specs()); - p.refresh(&s, Instant::now()); + p.update_state(&s, Instant::now()); // process exceeded cond let d = t1.elapsed().as_millis(); @@ -89,12 +89,14 @@ fn match_cond_not_seen( cond_span.as_millis() ,d); } -// cond: not seen for 400ms +// cond: not seen // start state: seen // test state: not seen for `test_span` // (cond_span, test_span, should_match) #[rstest] +// REVIEW: #[case((400, 200), false)] +#[case((200, 400), false)] #[test] fn match_cond_not_seen_2( #[case] spans: (u64, u64), @@ -111,24 +113,31 @@ fn match_cond_not_seen_2( .spawn() .unwrap(); + let _ = std::process::Command::new("pkill") + .args(["-9", "-f", "5382952proc.sh"]) + .spawn() + .unwrap(); + + let pat = "538295"; let mut p = process::Process::from_pattern(pat.into()); s.refresh_specifics(Scheduler::process_refresh_specs()); let t1 = Instant::now(); - p.refresh(&s, t1); + p.update_state(&s, t1); std::thread::sleep(test_span); s.refresh_specifics(Scheduler::process_refresh_specs()); - p.refresh(&s, Instant::now()); + p.update_state(&s, Instant::now()); + dbg!(&p); // process exceeded cond let d = t1.elapsed().as_millis(); assert_eq!(p.matches(cond), should_match, - "process is not absent long enough. \ncondition: not_seen({}ms) > observation: not_seen: {}ms", - cond_span.as_millis() ,d); + "\nnot_seen condition should match. \ncondition: not_seen ({}ms) > observation: {}: {}ms", + cond_span.as_millis() , p.state(), d); let _ = target.kill(); } diff --git a/tests/test.toml b/tests/test.toml index e2cdecd..84fdbe6 100644 --- a/tests/test.toml +++ b/tests/test.toml @@ -7,18 +7,18 @@ regex = false condition = {not_seen = "5s"} # one off command -exec = ["sh", "-c", "echo not seen !"] +exec = ["sh", "-c", "notify-send bar"] # when exec_end is defined the schedule behaves like a toggle -# exec_end +exec_end = ["sh", "-c", "notify-send 'bar end'"] [[profiles]] pattern = "foo" regex = false [[profiles.commands]] -# lifetime = {not_seen = "3m"} -condition = {not_seen = "5s"} +condition = {seen = "5s"} # one off command -exec = ["sh", "-c", "echo not seen !"] +exec = ["sh", "-c", "notify-send foo"] +exec_end = ["sh", "-c", "notify-send 'foo end'"]