@ -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 ( ) > c ond . 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 . l ast_refresh = Some ( Instant ::now ( ) ) ;
let last_seen = p . l ast_refresh. unwrap ( ) ;
p . update_ state( ) ;
assert! ( p . matches( cond_seen . clone ( ) ) , "should match user condition" ) ;
p . l ifetime. l ast_refresh = Some ( Instant ::now ( ) ) ;
let last_seen = p . l ifetime. l ast_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 . l ifetime. l ast_refresh = Some ( Instant ::now ( ) ) ;
p . update_ inner_ state( ) ;
// 5+2 = 7
assert! ( p . l ast_seen. unwrap ( ) . elapsed ( ) = = Duration ::from_secs ( 7 ) ) ;
assert! ( ! p . matches( cond_seen . clone ( ) ) ) ;
assert! ( p . first_seen. unwrap ( ) = = first_seen . unwrap ( ) ) ;
assert! ( p . l ifetime. l ast_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 . l ast_refresh = Some ( Instant ::now ( ) ) ;
p . prev_refresh = t1 ;
p . update_ state( ) ;
p . l ifetime. l ast_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 . l ast_refresh = Some ( Instant ::now ( ) ) ;
p . update_ state( ) ;
p . l ifetime. l ast_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 . l ast_seen, p . last_refresh ) ;
assert! ( ! p . matches( cond_not_seen . clone ( ) ) ) ;
assert_eq! ( p . l ifetime. l ast_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 . l ast_refresh = Some ( Instant ::now ( ) ) ;
p . update_ state( ) ;
assert! ( matches! ( p . state, ProcState ::NotSeen ) ) ;
p . l ifetime. l ast_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 . l ast_refresh = Some ( Instant ::now ( ) ) ;
p . update_ state( ) ;
assert! ( matches! ( p . state, ProcState ::NotSeen ) ) ;
assert! ( p . matches( cond_not_seen . clone ( ) ) ) ;
}
p . l ifetime. l ast_refresh = Some ( Instant ::now ( ) ) ;
p . update_ inner_ state( ) ;
assert! ( matches! ( p . lifetime. state, ProcState ::NotSeen ) ) ;
assert! ( p . lifetime. matches( cond_not_seen . clone ( ) ) ) ;
}
}