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 <contact@blob42.xyz>
one-off-cmd
blob42 3 months ago
parent b2785a2f96
commit c57e92e7d5

110
Cargo.lock generated

@ -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"

@ -1,3 +1,4 @@
- match multiple patterns
- cmd exec:
[x] repeat on cmd failure
[x] disable profile on cmd failure

@ -1,7 +1,5 @@
#![allow(dead_code)]
#![allow(unused_variables)]
pub mod process;
pub mod state;
pub mod sched;
pub mod config;

@ -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(())

@ -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<Instant>,
last_seen: Option<Instant>,
last_refresh: Option<Instant>,
prev_refresh: Option<Instant>,
prev_state: Option<ProcState>,
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<Instant>,
last_seen: Option<Instant>,
last_refresh: Option<Instant>,
prev_refresh: Option<Instant>,
state: ProcState,
prev_state: Option<ProcState>,
lifetime: ProcLifetime,
pids: Vec<usize>,
}
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::<Vec<usize>>();
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::<Vec<usize>>();
// 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::<Vec<usize>>();
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::State> {
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::State> {
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()));
}
}

@ -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<ProfileJob>,
}
#[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<T>
where
T: StateTracker + StateMatcher,
{
profile: Profile,
process: Process,
object: T,
}
impl ProfileJob {
pub fn new(profile: Profile) -> Self {
impl ProfileJob<Process> {
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<Process> {
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<Box<dyn Job>>,
}
static PROCESS_REFRESH_SPECS: OnceLock<RefreshKind> = 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<Profile>) -> Self {
pub fn new() -> Self {
debug!("Using sampling rate of {:?}.", Self::SAMPLING_RATE);
let jobs: Vec<ProfileJob> = 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<Profile>) -> Self
{
let mut jobs: Vec<Box<dyn Job>> = 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()
}
}

@ -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<Self::State>;
// state is exiting
fn exiting(&self) -> Option<Self::State> {
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;
}

@ -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};

@ -1,6 +1,4 @@
#!/bin/sh
while true; do
sleep 1
done
sleep 300

@ -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();
}

@ -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'"]

Loading…
Cancel
Save