Extract watchers into module

This commit is contained in:
Demmie 2023-05-06 01:22:19 -04:00
parent 1740ec00c6
commit 63bfe4181e
No known key found for this signature in database
GPG Key ID: B06DAA3D432C6E9A
28 changed files with 274 additions and 239 deletions

View File

@ -12,33 +12,17 @@ jobs:
fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@nightly
with:
profile: minimal
toolchain: stable
components: rustfmt
override: true
default: true
- name: cargo fmt
uses: actions-rs/cargo@v1
with:
command: fmt
args: -- --check
- run: cargo fmt --check --workspace --all
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@nightly
with:
toolchain: stable
override: true
components: clippy
profile: minimal
default: true
components: clippy
- uses: Swatinem/rust-cache@v2
- name: Run cargo check
uses: actions-rs/cargo@v1
with:
command: clippy
args: -- -D warnings
- run: cargo clippy --locked --all-targets --all-features --workspace -- -D warnings

39
Cargo.lock generated
View File

@ -62,9 +62,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.70"
version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
[[package]]
name = "async-broadcast"
@ -220,26 +220,16 @@ dependencies = [
[[package]]
name = "awatcher"
version = "0.1.0"
version = "0.0.1"
dependencies = [
"anyhow",
"aw-client-rust",
"chrono",
"clap",
"dirs 5.0.0",
"fern",
"gethostname 0.4.1",
"log",
"regex",
"serde",
"serde_default",
"serde_json",
"toml",
"wayland-backend",
"wayland-client",
"wayland-scanner",
"x11rb",
"zbus",
"watchers",
]
[[package]]
@ -2062,6 +2052,27 @@ version = "0.2.84"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
[[package]]
name = "watchers"
version = "0.0.1"
dependencies = [
"anyhow",
"aw-client-rust",
"chrono",
"dirs 5.0.0",
"log",
"regex",
"serde",
"serde_default",
"serde_json",
"toml",
"wayland-backend",
"wayland-client",
"wayland-scanner",
"x11rb",
"zbus",
]
[[package]]
name = "wayland-backend"
version = "0.1.1"

View File

@ -1,6 +1,6 @@
[package]
name = "awatcher"
version = "0.1.0"
version = "0.0.1"
authors = ["Demmie <2e3s19@gmail.com>"]
edition = "2021"
@ -8,32 +8,24 @@ edition = "2021"
name = "awatcher"
path = "src/main.rs"
[lib]
name = "awatcher"
crate-type = ["lib"]
path = "src/lib.rs"
[workspace]
members = ["watchers"]
[workspace.dependencies]
anyhow = "1.0.70"
log = { version = "0.4.17", features = ["std"] }
[dependencies]
aw-client-rust = { git = "https://github.com/ActivityWatch/aw-server-rust" }
watchers = { path = "./watchers", default-features = false }
gethostname = "0.4.1"
wayland-client = "0.30.1"
wayland-scanner = "0.30"
wayland-backend = "0.1"
x11rb = { version = "0.11.1", features = ["screensaver"] }
zbus = {version = "3.11.1", optional = true}
chrono = "0.4.24"
toml = "0.7.3"
clap = { version = "4.2.1", features = ["string"] }
log = { version = "0.4.17", features = ["std"] }
fern = { version = "0.6.2", features = ["colored"] }
dirs = "5.0.0"
serde = { version = "1.0.160", features = ["derive"] }
serde_default = "0.1.0"
serde_json = "1.0.95"
regex = "1.8.1"
anyhow = "1.0.70"
log = { workspace = true }
anyhow = { workspace = true }
[features]
default = ["gnome", "kwin_window"]
gnome = ["zbus"]
kwin_window = ["zbus"]
gnome = ["watchers/gnome"]
kwin_window = ["watchers/kwin_window"]

View File

@ -1,92 +1,140 @@
mod defaults;
mod file_config;
mod filters;
use std::path::Path;
use std::path::PathBuf;
use self::filters::{Filter, Replacement};
use clap::{arg, value_parser, Arg, ArgAction, Command};
use file_config::FileConfig;
use clap::parser::ValueSource;
use clap::{arg, value_parser, Arg, ArgAction, ArgMatches, Command};
use fern::colors::{Color, ColoredLevelConfig};
use log::LevelFilter;
use std::{path::PathBuf, time::Duration};
use watchers::config::defaults;
use watchers::config::Config;
use watchers::config::FileConfig;
pub struct Config {
pub port: u32,
pub host: String,
pub idle_timeout: Duration,
pub poll_time_idle: Duration,
pub poll_time_window: Duration,
pub idle_bucket_name: String,
pub active_window_bucket_name: String,
pub no_server: bool,
pub verbosity: LevelFilter,
filters: Vec<Filter>,
pub fn setup_logger(verbosity: LevelFilter) -> Result<(), fern::InitError> {
fern::Dispatch::new()
.format(|out, message, record| {
let colors = ColoredLevelConfig::new()
.info(Color::Green)
.debug(Color::Blue)
.trace(Color::Cyan);
out.finish(format_args!(
"[{} {} {}] {}",
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S%.6f"),
colors.color(record.level()),
record.target(),
message
));
})
.level(log::LevelFilter::Error)
.level_for("watchers", verbosity)
.level_for("awatcher", verbosity)
.chain(std::io::stdout())
.apply()?;
Ok(())
}
impl Config {
pub fn from_cli() -> anyhow::Result<Self> {
let matches = Command::new("Activity Watcher")
.version("0.0.1")
.about("A set of ActivityWatch desktop watchers")
.args([
arg!(-c --config <FILE> "Custom config file").value_parser(value_parser!(PathBuf)),
arg!(--port <PORT> "Custom server port")
.value_parser(value_parser!(u32))
.default_value(defaults::port().to_string()),
arg!(--host <HOST> "Custom server host")
.value_parser(value_parser!(String))
.default_value(defaults::host()),
arg!(--"idle-timeout" <SECONDS> "Time of inactivity to consider the user idle")
.value_parser(value_parser!(u32))
.default_value(defaults::idle_timeout_seconds().to_string()),
arg!(--"poll-time-idle" <SECONDS> "Period between sending heartbeats to the server for idle activity")
.value_parser(value_parser!(u32))
.default_value(defaults::poll_time_idle_seconds().to_string()),
arg!(--"poll-time-window" <SECONDS> "Period between sending heartbeats to the server for idle activity")
.value_parser(value_parser!(u32))
.default_value(defaults::poll_time_window_seconds().to_string()),
arg!(--"no-server" "Don't communicate to the ActivityWatch server")
.value_parser(value_parser!(bool))
.action(ArgAction::SetTrue),
Arg::new("verbosity")
.short('v')
.help("Verbosity level: -v for warnings, -vv for info, -vvv for debug, -vvvv for trace")
.action(ArgAction::Count),
])
.get_matches();
pub fn from_cli() -> anyhow::Result<Config> {
let matches = Command::new("Activity Watcher")
.version("0.0.1")
.about("A set of ActivityWatch desktop watchers")
.args([
arg!(-c --config <FILE> "Custom config file").value_parser(value_parser!(PathBuf)),
arg!(--port <PORT> "Custom server port")
.value_parser(value_parser!(u32))
.default_value(defaults::port().to_string()),
arg!(--host <HOST> "Custom server host")
.value_parser(value_parser!(String))
.default_value(defaults::host()),
arg!(--"idle-timeout" <SECONDS> "Time of inactivity to consider the user idle")
.value_parser(value_parser!(u32))
.default_value(defaults::idle_timeout_seconds().to_string()),
arg!(--"poll-time-idle" <SECONDS> "Period between sending heartbeats to the server for idle activity")
.value_parser(value_parser!(u32))
.default_value(defaults::poll_time_idle_seconds().to_string()),
arg!(--"poll-time-window" <SECONDS> "Period between sending heartbeats to the server for idle activity")
.value_parser(value_parser!(u32))
.default_value(defaults::poll_time_window_seconds().to_string()),
arg!(--"no-server" "Don't communicate to the ActivityWatch server")
.value_parser(value_parser!(bool))
.action(ArgAction::SetTrue),
Arg::new("verbosity")
.short('v')
.help("Verbosity level: -v for warnings, -vv for info, -vvv for debug, -vvvv for trace")
.action(ArgAction::Count),
])
.get_matches();
let config = FileConfig::new_with_cli(&matches)?;
let config = new_with_cli(&matches)?;
let hostname = gethostname::gethostname().into_string().unwrap();
let idle_bucket_name = format!("aw-watcher-afk_{hostname}");
let active_window_bucket_name = format!("aw-watcher-window_{hostname}");
let verbosity = match matches.get_count("verbosity") {
0 => LevelFilter::Error,
1 => LevelFilter::Warn,
2 => LevelFilter::Info,
3 => LevelFilter::Debug,
_ => LevelFilter::Trace,
};
let hostname = gethostname::gethostname().into_string().unwrap();
let idle_bucket_name = format!("aw-watcher-afk_{hostname}");
let active_window_bucket_name = format!("aw-watcher-window_{hostname}");
let verbosity = match matches.get_count("verbosity") {
0 => LevelFilter::Error,
1 => LevelFilter::Warn,
2 => LevelFilter::Info,
3 => LevelFilter::Debug,
_ => LevelFilter::Trace,
};
Ok(Self {
port: config.server.port,
host: config.server.host,
idle_timeout: config.client.get_idle_timeout(),
poll_time_idle: config.client.get_poll_time_idle(),
poll_time_window: config.client.get_poll_time_window(),
idle_bucket_name,
active_window_bucket_name,
filters: config.client.filters,
no_server: *matches.get_one("no-server").unwrap(),
verbosity,
})
}
Ok(Config {
port: config.server.port,
host: config.server.host,
idle_timeout: config.client.get_idle_timeout(),
poll_time_idle: config.client.get_poll_time_idle(),
poll_time_window: config.client.get_poll_time_window(),
idle_bucket_name,
active_window_bucket_name,
filters: config.client.filters,
no_server: *matches.get_one("no-server").unwrap(),
verbosity,
})
}
pub fn window_data_replacement(&self, app_id: &str, title: &str) -> Replacement {
for filter in &self.filters {
if let Some(replacement) = filter.replacement(app_id, title) {
return replacement;
pub fn new_with_cli(matches: &ArgMatches) -> anyhow::Result<FileConfig> {
let mut config_path = None;
if matches.contains_id("config") {
let config_file = matches.get_one::<String>("config");
if let Some(path) = config_file {
if let Err(e) = std::fs::metadata(path) {
warn!("Invalid config filename, using the default config: {e}");
} else {
config_path = Some(Path::new(path).to_path_buf());
}
}
}
let mut config = FileConfig::new(config_path)?;
Replacement::default()
merge_cli(&mut config, matches);
Ok(config)
}
fn merge_cli(config: &mut FileConfig, matches: &ArgMatches) {
get_arg_value(
"poll-time-idle",
matches,
&mut config.client.poll_time_idle_seconds,
);
get_arg_value(
"poll-time-window",
matches,
&mut config.client.poll_time_window_seconds,
);
get_arg_value(
"idle-timeout",
matches,
&mut config.client.idle_timeout_seconds,
);
get_arg_value("port", matches, &mut config.server.port);
get_arg_value("host", matches, &mut config.server.host);
}
fn get_arg_value<T>(id: &str, matches: &ArgMatches, config_value: &mut T)
where
T: Clone + Send + Sync + 'static,
{
if let Some(ValueSource::CommandLine) = matches.value_source(id) {
let value = &mut matches.get_one::<T>(id).unwrap().clone();
std::mem::swap(config_value, value);
}
}

View File

@ -1,6 +0,0 @@
#[macro_use]
extern crate log;
pub mod config;
pub mod report_client;
pub mod watchers;

View File

@ -4,41 +4,14 @@
extern crate log;
mod config;
mod report_client;
mod watchers;
use config::Config;
use fern::colors::{Color, ColoredLevelConfig};
use log::LevelFilter;
use report_client::ReportClient;
use std::{sync::Arc, thread};
use std::sync::Arc;
use watchers::ConstructorFilter;
fn setup_logger(verbosity: LevelFilter) -> Result<(), fern::InitError> {
fern::Dispatch::new()
.format(|out, message, record| {
let colors = ColoredLevelConfig::new()
.info(Color::Green)
.debug(Color::Blue)
.trace(Color::Cyan);
out.finish(format_args!(
"[{} {} {}] {}",
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S%.6f"),
colors.color(record.level()),
record.target(),
message
));
})
.level(log::LevelFilter::Error)
.level_for("awatcher", verbosity)
.chain(std::io::stdout())
.apply()?;
Ok(())
}
use watchers::ReportClient;
fn main() -> anyhow::Result<()> {
let config = Config::from_cli()?;
setup_logger(config.verbosity)?;
let config = config::from_cli()?;
config::setup_logger(config.verbosity)?;
let client = ReportClient::new(config)?;
let client = Arc::new(client);
@ -69,19 +42,13 @@ fn main() -> anyhow::Result<()> {
let mut thread_handlers = Vec::new();
let idle_watcher = watchers::IDLE.filter_first_supported();
if let Some(mut watcher) = idle_watcher {
let thread_client = Arc::clone(&client);
let idle_handler = thread::spawn(move || watcher.watch(&thread_client));
if let Some(idle_handler) = watchers::IDLE.run_first_supported(&client) {
thread_handlers.push(idle_handler);
} else {
warn!("No supported idle handler is found");
}
let window_watcher = watchers::ACTIVE_WINDOW.filter_first_supported();
if let Some(mut watcher) = window_watcher {
let thread_client = Arc::clone(&client);
let active_window_handler = thread::spawn(move || watcher.watch(&thread_client));
if let Some(active_window_handler) = watchers::ACTIVE_WINDOW.run_first_supported(&client) {
thread_handlers.push(active_window_handler);
} else {
warn!("No supported active window handler is found");

32
watchers/Cargo.toml Normal file
View File

@ -0,0 +1,32 @@
[package]
name = "watchers"
version = "0.0.1"
authors = ["Demmie <2e3s19@gmail.com>"]
edition = "2021"
[lib]
name = "watchers"
crate-type = ["lib"]
path = "src/lib.rs"
[dependencies]
aw-client-rust = { git = "https://github.com/ActivityWatch/aw-server-rust" }
wayland-client = "0.30.1"
wayland-scanner = "0.30"
wayland-backend = "0.1"
x11rb = { version = "0.11.1", features = ["screensaver"] }
zbus = {version = "3.11.1", optional = true}
chrono = "0.4.24"
toml = "0.7.3"
dirs = "5.0.0"
serde = { version = "1.0.160", features = ["derive"] }
serde_default = "0.1.0"
serde_json = "1.0.95"
regex = "1.8.1"
log = { workspace = true }
anyhow = { workspace = true }
[features]
default = ["gnome", "kwin_window"]
gnome = ["zbus"]
kwin_window = ["zbus"]

33
watchers/src/config.rs Normal file
View File

@ -0,0 +1,33 @@
pub mod defaults;
mod file_config;
mod filters;
use self::filters::{Filter, Replacement};
pub use file_config::FileConfig;
use log::LevelFilter;
use std::time::Duration;
pub struct Config {
pub port: u32,
pub host: String,
pub idle_timeout: Duration,
pub poll_time_idle: Duration,
pub poll_time_window: Duration,
pub idle_bucket_name: String,
pub active_window_bucket_name: String,
pub no_server: bool,
pub verbosity: LevelFilter,
pub filters: Vec<Filter>,
}
impl Config {
pub fn window_data_replacement(&self, app_id: &str, title: &str) -> Replacement {
for filter in &self.filters {
if let Some(replacement) = filter.replacement(app_id, title) {
return replacement;
}
}
Replacement::default()
}
}

View File

@ -1,12 +1,7 @@
use anyhow::{anyhow, Context};
use clap::{parser::ValueSource, ArgMatches};
use serde::Deserialize;
use serde_default::DefaultFromSerde;
use std::{
io::ErrorKind,
path::{Path, PathBuf},
time::Duration,
};
use std::{io::ErrorKind, path::PathBuf, time::Duration};
use crate::config::defaults;
@ -24,11 +19,11 @@ pub struct ServerConfig {
#[serde(rename_all = "kebab-case")]
pub struct ClientConfig {
#[serde(default = "defaults::idle_timeout_seconds")]
idle_timeout_seconds: u32,
pub idle_timeout_seconds: u32,
#[serde(default = "defaults::poll_time_idle_seconds")]
poll_time_idle_seconds: u32,
pub poll_time_idle_seconds: u32,
#[serde(default = "defaults::poll_time_window_seconds")]
poll_time_window_seconds: u32,
pub poll_time_window_seconds: u32,
#[serde(default)]
pub filters: Vec<Filter>,
}
@ -118,53 +113,4 @@ impl FileConfig {
Ok(config)
}
pub fn new_with_cli(matches: &ArgMatches) -> anyhow::Result<Self> {
let mut config_path = None;
if matches.contains_id("config") {
let config_file = matches.get_one::<String>("config");
if let Some(path) = config_file {
if let Err(e) = std::fs::metadata(path) {
warn!("Invalid config filename, using the default config: {e}");
} else {
config_path = Some(Path::new(path).to_path_buf());
}
}
}
let mut config = Self::new(config_path)?;
config.merge_cli(matches);
Ok(config)
}
fn merge_cli(&mut self, matches: &ArgMatches) {
get_arg_value(
"poll-time-idle",
matches,
&mut self.client.poll_time_idle_seconds,
);
get_arg_value(
"poll-time-window",
matches,
&mut self.client.poll_time_window_seconds,
);
get_arg_value(
"idle-timeout",
matches,
&mut self.client.idle_timeout_seconds,
);
get_arg_value("port", matches, &mut self.server.port);
get_arg_value("host", matches, &mut self.server.host);
}
}
fn get_arg_value<T>(id: &str, matches: &ArgMatches, config_value: &mut T)
where
T: Clone + Send + Sync + 'static,
{
if let Some(ValueSource::CommandLine) = matches.value_source(id) {
let value = &mut matches.get_one::<T>(id).unwrap().clone();
std::mem::swap(config_value, value);
}
}

12
watchers/src/lib.rs Normal file
View File

@ -0,0 +1,12 @@
#[macro_use]
extern crate log;
pub mod config;
mod report_client;
mod watchers;
pub use report_client::ReportClient;
pub use watchers::ConstructorFilter;
pub use watchers::Watcher;
pub use watchers::ACTIVE_WINDOW;
pub use watchers::IDLE;

View File

@ -14,7 +14,10 @@ mod x11_screensaver_idle;
mod x11_window;
use crate::report_client::ReportClient;
use std::sync::Arc;
use std::{
sync::Arc,
thread::{self, JoinHandle},
};
pub trait Watcher: Send {
fn new() -> anyhow::Result<Self>
@ -30,6 +33,8 @@ type WatcherConstructors = [WatcherConstructor];
pub trait ConstructorFilter {
fn filter_first_supported(&self) -> Option<BoxedWatcher>;
fn run_first_supported(&self, client: &Arc<ReportClient>) -> Option<JoinHandle<()>>;
}
impl ConstructorFilter for WatcherConstructors {
@ -42,6 +47,17 @@ impl ConstructorFilter for WatcherConstructors {
}
})
}
fn run_first_supported(&self, client: &Arc<ReportClient>) -> Option<JoinHandle<()>> {
let idle_watcher = self.filter_first_supported();
if let Some(mut watcher) = idle_watcher {
let thread_client = Arc::clone(client);
let idle_handler = thread::spawn(move || watcher.watch(&thread_client));
Some(idle_handler)
} else {
None
}
}
}
macro_rules! watcher {