Implement idle watcher for X11

This commit is contained in:
Demmie 2023-04-19 19:35:11 -04:00
parent 65ce731f03
commit 5c1b98d75f
No known key found for this signature in database
GPG Key ID: B06DAA3D432C6E9A
9 changed files with 195 additions and 31 deletions

76
Cargo.lock generated
View File

@ -194,7 +194,7 @@ source = "git+https://github.com/ActivityWatch/aw-server-rust#1f4f07d86c06de89de
dependencies = [
"aw-models",
"chrono",
"gethostname",
"gethostname 0.4.1",
"reqwest",
"serde",
"serde_json",
@ -220,12 +220,13 @@ dependencies = [
"chrono",
"clap",
"fern",
"gethostname",
"gethostname 0.4.1",
"log",
"serde_json",
"wayland-backend",
"wayland-client",
"wayland-scanner",
"x11rb",
"zbus",
]
@ -713,6 +714,16 @@ dependencies = [
"version_check",
]
[[package]]
name = "gethostname"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "gethostname"
version = "0.4.1"
@ -1008,6 +1019,15 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "memoffset"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
dependencies = [
"autocfg",
]
[[package]]
name = "memoffset"
version = "0.7.1"
@ -1053,6 +1073,19 @@ dependencies = [
"tempfile",
]
[[package]]
name = "nix"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4"
dependencies = [
"autocfg",
"bitflags",
"cfg-if",
"libc",
"memoffset 0.6.5",
]
[[package]]
name = "nix"
version = "0.26.2"
@ -1062,7 +1095,7 @@ dependencies = [
"bitflags",
"cfg-if",
"libc",
"memoffset",
"memoffset 0.7.1",
"pin-utils",
"static_assertions",
]
@ -1927,7 +1960,7 @@ dependencies = [
"cc",
"downcast-rs",
"io-lifetimes",
"nix",
"nix 0.26.2",
"scoped-tls",
"smallvec",
"wayland-sys",
@ -1940,7 +1973,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85bde68449abab1a808e5227b6e295f4ae3680911eb7711b4a2cb90141edb780"
dependencies = [
"bitflags",
"nix",
"nix 0.26.2",
"wayland-backend",
"wayland-scanner",
]
@ -2002,6 +2035,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "winapi-wsapoll"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
@ -2188,6 +2230,28 @@ dependencies = [
"winapi",
]
[[package]]
name = "x11rb"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdf3c79412dd91bae7a7366b8ad1565a85e35dd049affc3a6a2c549e97419617"
dependencies = [
"gethostname 0.2.3",
"nix 0.25.1",
"winapi",
"winapi-wsapoll",
"x11rb-protocol",
]
[[package]]
name = "x11rb-protocol"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0b1513b141123073ce54d5bb1d33f801f17508fbd61e02060b1214e96d39c56"
dependencies = [
"nix 0.25.1",
]
[[package]]
name = "zbus"
version = "3.11.1"
@ -2211,7 +2275,7 @@ dependencies = [
"futures-sink",
"futures-util",
"hex",
"nix",
"nix 0.26.2",
"once_cell",
"ordered-stream",
"rand",

View File

@ -15,3 +15,4 @@ zbus = "3.11.1"
clap = "4.2.1"
log = { version = "0.4.17", features = ["std"] }
fern = { version = "0.6.2", features = ["colored"] }
x11rb = { version = "0.11.1", features = ["screensaver"] }

View File

@ -1,13 +1,13 @@
use std::path::PathBuf;
use std::{path::PathBuf, time::Duration};
use clap::{arg, value_parser, Command};
pub struct Config {
pub port: u32,
pub host: String,
pub idle_timeout: u32,
pub poll_time_idle: u32,
pub poll_time_window: u32,
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,
}
@ -40,13 +40,16 @@ impl Config {
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 poll_seconds_idle = *matches.get_one::<u32>("poll-time-idle").unwrap();
let poll_seconds_window = *matches.get_one::<u32>("poll-time-window").unwrap();
let idle_timeout_seconds = *matches.get_one::<u32>("idle-timeout").unwrap();
Self {
port: *matches.get_one("port").unwrap(),
host: String::clone(matches.get_one("host").unwrap()),
idle_timeout: *matches.get_one("idle-timeout").unwrap(),
poll_time_idle: *matches.get_one("poll-time-idle").unwrap(),
poll_time_window: *matches.get_one("poll-time-window").unwrap(),
idle_timeout: Duration::from_secs(u64::from(idle_timeout_seconds)),
poll_time_idle: Duration::from_secs(u64::from(poll_seconds_idle)),
poll_time_window: Duration::from_secs(u64::from(poll_seconds_window)),
idle_bucket_name,
active_window_bucket_name,
}

View File

@ -11,7 +11,6 @@ use std::env::temp_dir;
use std::path::Path;
use std::sync::{mpsc::channel, Arc, Mutex};
use std::thread;
use std::time;
use zbus::blocking::{Connection, ConnectionBuilder};
use zbus::dbus_interface;
@ -196,9 +195,7 @@ impl Watcher for WindowWatcher {
if let Err(error) = send_active_window(client, &active_window) {
error!("Error on sending active window heartbeat: {error}");
}
thread::sleep(time::Duration::from_secs(u64::from(
client.config.poll_time_window,
)));
thread::sleep(client.config.poll_time_window);
}
}
}

View File

@ -10,6 +10,7 @@ mod wl_bindings;
mod wl_connection;
mod wl_foreign_toplevel;
mod wl_kwin_idle;
mod x11_screensaver_idle;
use config::Config;
use fern::colors::{Color, ColoredLevelConfig};
@ -18,6 +19,7 @@ use report_client::ReportClient;
use std::env;
use std::{error::Error, str::FromStr, sync::Arc, thread};
use wl_kwin_idle::IdleWatcher as WlKwinIdleWatcher;
use x11_screensaver_idle::IdleWatcher as X11IdleWatcher;
use crate::wl_foreign_toplevel::WindowWatcher as WlrForeignToplevelWindowWatcher;
@ -59,7 +61,8 @@ macro_rules! watcher {
};
}
const IDLE_WATCHERS: &[WatcherConstructor] = &[watcher!(WlKwinIdleWatcher)];
const IDLE_WATCHERS: &[WatcherConstructor] =
&[watcher!(WlKwinIdleWatcher), watcher!(X11IdleWatcher)];
const ACTIVE_WINDOW_WATCHERS: &[WatcherConstructor] = &[
watcher!(WlrForeignToplevelWindowWatcher),
@ -103,8 +106,14 @@ fn main() -> Result<(), BoxedError> {
"Sending to server {}:{}",
client.config.host, client.config.port
);
info!("Idle timeout: {} seconds", client.config.idle_timeout);
info!("Polling period: {} seconds", client.config.poll_time_idle);
info!(
"Idle timeout: {} seconds",
client.config.idle_timeout.as_secs()
);
info!(
"Polling period: {} seconds",
client.config.poll_time_idle.as_secs()
);
let mut thread_handlers = Vec::new();

View File

@ -39,7 +39,7 @@ impl ReportClient {
data,
};
let pulsetime = f64::from(self.config.idle_timeout + self.config.poll_time_idle);
let pulsetime = (self.config.idle_timeout + self.config.poll_time_idle).as_secs_f64();
self.client
.heartbeat(&self.config.idle_bucket_name, &event, pulsetime)
.map_err(|_| "Failed to send heartbeat")?;
@ -58,7 +58,7 @@ impl ReportClient {
data,
};
let interval_margin: f64 = f64::from(self.config.poll_time_idle + 1);
let interval_margin = self.config.poll_time_idle.as_secs_f64() + 1.0;
self.client
.heartbeat(
&self.config.active_window_bucket_name,

View File

@ -1,5 +1,5 @@
use std::collections::HashMap;
use std::{sync::Arc, thread, time};
use std::{sync::Arc, thread};
use crate::{wl_connection::subscribe_state, Watcher};
@ -160,9 +160,7 @@ impl Watcher for WindowWatcher {
error!("Error on iteration: {e}");
}
thread::sleep(time::Duration::from_secs(u64::from(
client.config.poll_time_window,
)));
thread::sleep(client.config.poll_time_window);
}
}
}

View File

@ -5,7 +5,7 @@ use super::wl_bindings;
use super::wl_connection::{subscribe_state, WlEventConnection};
use super::BoxedError;
use chrono::{DateTime, Duration, Utc};
use std::{sync::Arc, thread, time};
use std::{sync::Arc, thread};
use wayland_client::{
globals::GlobalListContents,
protocol::{wl_registry, wl_seat::WlSeat},
@ -131,9 +131,10 @@ impl Watcher for IdleWatcher {
}
fn watch(&mut self, client: &Arc<ReportClient>) {
let timeout = u32::try_from(client.config.idle_timeout.as_secs() * 1000);
let mut idle_state = IdleState::new(
self.connection
.get_kwin_idle_timeout(client.config.idle_timeout * 1000)
.get_kwin_idle_timeout(timeout.unwrap())
.unwrap(),
Arc::clone(client),
);
@ -149,9 +150,7 @@ impl Watcher for IdleWatcher {
} else if let Err(e) = idle_state.send_ping() {
error!("Error on idle iteration: {e}");
}
thread::sleep(time::Duration::from_secs(u64::from(
client.config.poll_time_idle,
)));
thread::sleep(client.config.poll_time_idle);
}
}
}

View File

@ -0,0 +1,93 @@
use std::{env, sync::Arc, thread};
use chrono::{Duration, Utc};
use x11rb::connection::Connection;
use x11rb::protocol::screensaver::ConnectionExt;
use x11rb::rust_connection::RustConnection;
use crate::{report_client::ReportClient, BoxedError, Watcher};
pub struct IdleWatcher {
connection: RustConnection,
screen_root: u32,
}
impl IdleWatcher {
fn seconds_since_last_input(&self) -> Result<u32, BoxedError> {
let a = self.connection.screensaver_query_info(self.screen_root)?;
let b = a.reply()?;
Ok(b.ms_since_user_input / 1000)
}
fn run(&self, is_idle: bool, client: &Arc<ReportClient>) -> Result<bool, BoxedError> {
// The logic is rewritten from the original Python code:
// https://github.com/ActivityWatch/aw-watcher-afk/blob/ef531605cd8238e00138bbb980e5457054e05248/aw_watcher_afk/afk.py#L73
let duration_1ms: Duration = Duration::milliseconds(1);
let duration_zero: Duration = Duration::zero();
let seconds_since_input = self.seconds_since_last_input()?;
let now = Utc::now();
let time_since_input = Duration::seconds(i64::from(seconds_since_input));
let last_input = now - time_since_input;
let mut is_idle_again = is_idle;
if is_idle && u64::from(seconds_since_input) < client.config.idle_timeout.as_secs() {
debug!("No longer idle");
client.ping(is_idle, last_input, duration_zero)?;
is_idle_again = false;
// ping with timestamp+1ms with the next event (to ensure the latest event gets retrieved by get_event)
client.ping(is_idle, last_input + duration_1ms, duration_zero)?;
} else if !is_idle && u64::from(seconds_since_input) >= client.config.idle_timeout.as_secs()
{
debug!("Idle again");
client.ping(is_idle, last_input, duration_zero)?;
is_idle_again = true;
// ping with timestamp+1ms with the next event (to ensure the latest event gets retrieved by get_event)
client.ping(is_idle, last_input + duration_1ms, time_since_input)?;
} else {
// Send a heartbeat if no state change was made
if is_idle {
trace!("Reporting as idle");
client.ping(is_idle, last_input, time_since_input)?;
} else {
trace!("Reporting as not idle");
client.ping(is_idle, last_input, duration_zero)?;
}
}
Ok(is_idle_again)
}
}
impl Watcher for IdleWatcher {
fn new() -> Result<Self, BoxedError> {
if env::var("DISPLAY").is_err() {
warn!("DISPLAY is not set, setting to the default value \":0\"");
env::set_var("DISPLAY", ":0");
}
let (connection, screen_num) = x11rb::connect(None)?;
let screen_root = connection.setup().roots[screen_num].root;
Ok(IdleWatcher {
connection,
screen_root,
})
}
fn watch(&mut self, client: &Arc<ReportClient>) {
info!("Starting idle watcher");
let mut is_idle = false;
loop {
match self.run(is_idle, client) {
Ok(is_idle_again) => {
is_idle = is_idle_again;
}
Err(e) => error!("Error on idle iteration: {e}"),
};
thread::sleep(client.config.poll_time_idle);
}
}
}