Files
Lumotia/crates/core/src/process_watch.rs

123 lines
4.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Lightweight meeting-process detection.
//!
//! Scope (per Jake's ideology note): single signal only — poll the process
//! list and match user-editable patterns. No mic-activity heuristic, no
//! calendar integration. If the user opts in, we surface a non-modal toast
//! so they can decide to start recording. We never start recording
//! ourselves from this signal.
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System};
/// Reusable wrapper around a `sysinfo::System` whose process table is
/// refreshed in place on every poll, instead of allocating a fresh one.
///
/// On a busy host (~300 processes), `System::new_with_specifics` followed by
/// `refresh_processes` walks `/proc` cold and costs ~50100 ms; reusing the
/// same instance reuses sysinfo's per-process bookkeeping so subsequent
/// refreshes are dominated by diffing rather than allocation. The Tauri
/// host holds one of these behind a `Mutex` for the meeting-detection
/// command to call every 15 s.
pub struct ProcessLister {
system: System,
}
impl Default for ProcessLister {
fn default() -> Self {
Self::new()
}
}
impl ProcessLister {
pub fn new() -> Self {
Self {
system: System::new_with_specifics(
RefreshKind::nothing().with_processes(ProcessRefreshKind::nothing()),
),
}
}
/// Refresh the process table in place and return the current
/// lowercased executable names.
pub fn snapshot(&mut self) -> Vec<String> {
self.system.refresh_processes(ProcessesToUpdate::All, true);
self.system
.processes()
.values()
.map(|process| process.name().to_string_lossy().to_lowercase())
.collect()
}
}
/// Snapshot the current process list's executable/command names. Lowercased
/// for case-insensitive pattern matching.
///
/// Convenience wrapper that allocates a fresh `ProcessLister` per call.
/// Hot paths (the meeting-detection poller) should hold a long-lived
/// `ProcessLister` and call `snapshot()` directly to avoid the per-call
/// allocation of `System`'s internal bookkeeping.
pub fn list_running_process_names() -> Vec<String> {
ProcessLister::new().snapshot()
}
/// Match a snapshot of process names against case-insensitive substring
/// `patterns`. Returns the set of patterns that matched at least once, in
/// input order, deduped. Empty / whitespace-only patterns are skipped so
/// a stray blank entry in the user's list never matches everything.
pub fn match_meeting_patterns(process_names: &[String], patterns: &[String]) -> Vec<String> {
let mut matches: Vec<String> = Vec::new();
for raw_pattern in patterns {
let needle = raw_pattern.trim().to_lowercase();
if needle.is_empty() {
continue;
}
if process_names.iter().any(|name| name.contains(&needle))
&& !matches.iter().any(|existing| existing == &needle)
{
matches.push(needle);
}
}
matches
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn matches_are_case_insensitive_substrings() {
let processes = vec![
"Zoom Meeting".to_lowercase(),
"firefox".to_lowercase(),
"Microsoft Teams".to_lowercase(),
];
let patterns = vec!["ZOOM".into(), "teams".into(), "discord".into()];
let got = match_meeting_patterns(&processes, &patterns);
assert_eq!(got, vec!["zoom", "teams"]);
}
#[test]
fn empty_and_whitespace_patterns_are_ignored() {
let processes = vec!["anything".to_lowercase()];
let patterns = vec!["".into(), " ".into()];
assert!(match_meeting_patterns(&processes, &patterns).is_empty());
}
#[test]
fn matches_are_deduped() {
let processes = vec!["zoomclient".into(), "zoomhelper".into()];
let patterns = vec!["zoom".into(), "zoom".into()];
assert_eq!(match_meeting_patterns(&processes, &patterns), vec!["zoom"]);
}
#[test]
fn list_running_returns_something_on_this_host() {
// Smoke check — this is the test host and always has running procs.
let names = list_running_process_names();
assert!(!names.is_empty(), "expected at least one running process");
}
}