mirror of
https://github.com/2e3s/awatcher.git
synced 2025-06-02 09:30:20 +00:00
Add an ability to run external modules
This commit is contained in:
parent
864d1dc364
commit
ae1e320176
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
/target
|
||||
src/bundle/logo.argb32
|
||||
lcov.info
|
||||
|
50
Cargo.lock
generated
50
Cargo.lock
generated
@ -239,7 +239,7 @@ dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"event-listener 3.0.0",
|
||||
"futures-lite",
|
||||
"rustix 0.38.15",
|
||||
"rustix 0.38.25",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
@ -447,7 +447,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "awatcher"
|
||||
version = "0.2.2-beta2"
|
||||
version = "0.2.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"aw-datastore",
|
||||
@ -459,6 +459,9 @@ dependencies = [
|
||||
"ksni",
|
||||
"log",
|
||||
"open",
|
||||
"rstest",
|
||||
"serde",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"toml 0.8.1",
|
||||
"watchers",
|
||||
@ -1729,7 +1732,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
|
||||
dependencies = [
|
||||
"hermit-abi 0.3.3",
|
||||
"rustix 0.38.15",
|
||||
"rustix 0.38.25",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
@ -1839,9 +1842,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.148"
|
||||
version = "0.2.150"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
|
||||
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
|
||||
|
||||
[[package]]
|
||||
name = "libdbus-sys"
|
||||
@ -1881,9 +1884,9 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.7"
|
||||
version = "0.4.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128"
|
||||
checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
@ -2543,6 +2546,15 @@ dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.3"
|
||||
@ -2871,14 +2883,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.15"
|
||||
version = "0.38.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2f9da0cbd88f9f09e7814e388301c8414c51c62aa6ce1e4b5c551d49d96e531"
|
||||
checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e"
|
||||
dependencies = [
|
||||
"bitflags 2.4.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.7",
|
||||
"linux-raw-sys 0.4.11",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
@ -2986,9 +2998,9 @@ checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.188"
|
||||
version = "1.0.193"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
|
||||
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@ -3007,9 +3019,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.188"
|
||||
version = "1.0.193"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
|
||||
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.67",
|
||||
"quote 1.0.33",
|
||||
@ -3250,14 +3262,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.8.0"
|
||||
version = "3.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef"
|
||||
checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"fastrand 2.0.1",
|
||||
"redox_syscall 0.3.5",
|
||||
"rustix 0.38.15",
|
||||
"redox_syscall 0.4.1",
|
||||
"rustix 0.38.25",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
@ -3809,7 +3821,7 @@ checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
|
||||
|
||||
[[package]]
|
||||
name = "watchers"
|
||||
version = "0.2.2-beta2"
|
||||
version = "0.2.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
|
10
Cargo.toml
10
Cargo.toml
@ -18,12 +18,17 @@ image = { version = "0.24.6" }
|
||||
members = ["watchers"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.2-beta2"
|
||||
version = "0.2.3"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.75"
|
||||
log = { version = "0.4.20", features = ["std"] }
|
||||
tokio = { version = "1.32.0" }
|
||||
serde = "1.0.193"
|
||||
|
||||
[dev-dependencies]
|
||||
rstest = "0.18.2"
|
||||
tempfile = "3.8.1"
|
||||
|
||||
[dependencies]
|
||||
watchers = { path = "./watchers", default-features = false }
|
||||
@ -39,12 +44,13 @@ ksni = {version = "0.2.1", optional = true}
|
||||
aw-server = { git = "https://github.com/ActivityWatch/aw-server-rust", optional = true, rev = "448312d" }
|
||||
aw-datastore = { git = "https://github.com/ActivityWatch/aw-server-rust", optional = true, rev = "448312d" }
|
||||
open = { version = "5.0.0", optional = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
|
||||
[features]
|
||||
default = ["gnome", "kwin_window"]
|
||||
gnome = ["watchers/gnome"]
|
||||
kwin_window = ["watchers/kwin_window"]
|
||||
bundle = ["ksni", "aw-server", "aw-datastore", "open"]
|
||||
bundle = ["ksni", "aw-server", "aw-datastore", "open", "serde"]
|
||||
|
||||
[package.metadata.deb]
|
||||
features = ["bundle"]
|
||||
|
10
README.md
10
README.md
@ -24,11 +24,13 @@ The binaries for the bundle, bundled DEB and ActivityWatch watchers replacement
|
||||
|
||||
### Bundle with built-in ActivityWatch
|
||||
|
||||
This is a single binary to run **awatcher** with the server without changing system and ActivityWatch configuration,
|
||||
when only tracking activity windows and idle state is needed.
|
||||
This is a single binary to run **awatcher** with the server without changing system and ActivityWatch configuration.
|
||||
The bundle is **aw-server-rust** and **awatcher** as a single executable.
|
||||
The data storage is compatible with ActivityWatch and **aw-server-rust** (**aw-server** has a different storage),
|
||||
so this can later be run as a module for ActivityWatch.
|
||||
The data storage is compatible with ActivityWatch and **aw-server-rust** (**aw-server** has a different storage), so this can later be run as a module for ActivityWatch.
|
||||
|
||||
External modules are run like in the original ActivityWatch distribution
|
||||
by looking at `$PATH` and running all binaries which start with `aw-`.
|
||||
They are controled from the tray, no additional configuration is necessary.
|
||||
|
||||
## Supported environments
|
||||
|
||||
|
@ -1,33 +1,11 @@
|
||||
mod menu;
|
||||
mod modules;
|
||||
mod server;
|
||||
|
||||
pub use menu::Tray;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::path::PathBuf;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
fn get_config_watchers(config_path: &Path) -> Option<Vec<String>> {
|
||||
let mut config_path = config_path.parent()?.to_path_buf();
|
||||
config_path.push("bundle-config.toml");
|
||||
debug!("Reading bundle config at {}", config_path.display());
|
||||
|
||||
let config_content = std::fs::read_to_string(&config_path).ok()?;
|
||||
let toml_content: toml::Value = toml::from_str(&config_content).ok()?;
|
||||
|
||||
trace!("Bundle config: {toml_content:?}");
|
||||
|
||||
Some(
|
||||
toml_content
|
||||
.get("watchers")?
|
||||
.get("autostart")?
|
||||
.as_array()?
|
||||
.iter()
|
||||
.filter_map(|value| value.as_str())
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn run(
|
||||
host: String,
|
||||
port: u32,
|
||||
@ -35,22 +13,14 @@ pub async fn run(
|
||||
no_tray: bool,
|
||||
shutdown_sender: UnboundedSender<()>,
|
||||
) {
|
||||
let watchers: Vec<String> =
|
||||
get_config_watchers(config_file.parent().unwrap()).unwrap_or_default();
|
||||
|
||||
for watcher in &watchers {
|
||||
debug!("Starting an external watcher {}", watcher);
|
||||
let _ = Command::new(watcher).spawn();
|
||||
}
|
||||
let manager = modules::Manager::new(
|
||||
&std::env::var("PATH").unwrap_or_default(),
|
||||
config_file.parent().unwrap(),
|
||||
);
|
||||
|
||||
if !no_tray {
|
||||
let service = ksni::TrayService::new(Tray::new(
|
||||
host,
|
||||
port,
|
||||
config_file,
|
||||
shutdown_sender,
|
||||
watchers,
|
||||
));
|
||||
let tray = Tray::new(host, port, config_file, shutdown_sender, manager);
|
||||
let service = ksni::TrayService::new(tray);
|
||||
service.spawn();
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,17 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
#[derive(Debug)]
|
||||
use super::modules::Manager;
|
||||
|
||||
pub struct Tray {
|
||||
server_host: String,
|
||||
server_port: u32,
|
||||
config_file: PathBuf,
|
||||
shutdown_sender: UnboundedSender<()>,
|
||||
watchers: Vec<String>,
|
||||
watchers_manager: Manager,
|
||||
checks: HashMap<PathBuf, bool>,
|
||||
}
|
||||
|
||||
impl Tray {
|
||||
@ -17,14 +20,21 @@ impl Tray {
|
||||
server_port: u32,
|
||||
config_file: PathBuf,
|
||||
shutdown_sender: UnboundedSender<()>,
|
||||
watchers: Vec<String>,
|
||||
watchers_manager: Manager,
|
||||
) -> Self {
|
||||
let checks = watchers_manager
|
||||
.path_watchers
|
||||
.iter()
|
||||
.map(|watcher| (watcher.path().to_owned(), watcher.started()))
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
server_host,
|
||||
server_port,
|
||||
config_file,
|
||||
shutdown_sender,
|
||||
watchers,
|
||||
watchers_manager,
|
||||
checks,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -38,9 +48,14 @@ impl ksni::Tray for Tray {
|
||||
}]
|
||||
}
|
||||
|
||||
fn id(&self) -> String {
|
||||
"awatcher-bundle".into()
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
"Awatcher".into()
|
||||
}
|
||||
|
||||
fn menu(&self) -> Vec<ksni::MenuItem<Self>> {
|
||||
let mut watchers_submenu: Vec<ksni::MenuItem<Self>> = vec![
|
||||
ksni::menu::CheckmarkItem {
|
||||
@ -59,12 +74,23 @@ impl ksni::Tray for Tray {
|
||||
}
|
||||
.into(),
|
||||
];
|
||||
for watcher in &self.watchers {
|
||||
for watcher in &self.watchers_manager.path_watchers {
|
||||
let path = watcher.path().to_owned();
|
||||
|
||||
watchers_submenu.push(
|
||||
ksni::menu::CheckmarkItem {
|
||||
label: watcher.clone(),
|
||||
enabled: false,
|
||||
checked: true,
|
||||
label: watcher.name(),
|
||||
enabled: true,
|
||||
checked: watcher.started(),
|
||||
activate: Box::new(move |this: &mut Self| {
|
||||
let current_checked = *this.checks.get(&path).unwrap_or(&false);
|
||||
this.checks.insert(path.clone(), !current_checked);
|
||||
if current_checked {
|
||||
this.watchers_manager.stop_watcher(&path);
|
||||
} else {
|
||||
this.watchers_manager.start_watcher(&path);
|
||||
}
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
@ -76,13 +102,11 @@ impl ksni::Tray for Tray {
|
||||
label: "ActivityWatch".into(),
|
||||
// https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html
|
||||
icon_name: "document-properties".into(),
|
||||
activate: {
|
||||
let url = format!("http://{}:{}", self.server_host, self.server_port);
|
||||
activate: Box::new(move |this: &mut Self| {
|
||||
let url = format!("http://{}:{}", this.server_host, this.server_port);
|
||||
|
||||
Box::new(move |_| {
|
||||
open::that(&url).unwrap();
|
||||
})
|
||||
},
|
||||
open::that(url).unwrap();
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
@ -108,13 +132,9 @@ impl ksni::Tray for Tray {
|
||||
ksni::menu::StandardItem {
|
||||
label: "Exit".into(),
|
||||
icon_name: "application-exit".into(),
|
||||
activate: {
|
||||
let shutdown_sender = self.shutdown_sender.clone();
|
||||
|
||||
Box::new(move |_| {
|
||||
shutdown_sender.send(()).unwrap();
|
||||
})
|
||||
},
|
||||
activate: Box::new(move |this: &mut Self| {
|
||||
this.shutdown_sender.send(()).unwrap();
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
|
335
src/bundle/modules.rs
Normal file
335
src/bundle/modules.rs
Normal file
@ -0,0 +1,335 @@
|
||||
// This repeats the functionality of aw-qt from ActivityWatch.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
struct Watchers {
|
||||
#[serde(default)]
|
||||
autostart: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
struct BundleConfig {
|
||||
watchers: Watchers,
|
||||
}
|
||||
|
||||
pub struct ExternalWatcher {
|
||||
path: PathBuf,
|
||||
handle: Option<Child>,
|
||||
}
|
||||
|
||||
impl ExternalWatcher {
|
||||
fn new(path: PathBuf) -> Option<Self> {
|
||||
if !path.is_file() {
|
||||
return None;
|
||||
}
|
||||
if path.metadata().ok()?.permissions().mode() & 0o111 == 0 {
|
||||
return None;
|
||||
}
|
||||
if !path.file_name()?.to_str()?.starts_with("aw-") {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Self { path, handle: None })
|
||||
}
|
||||
|
||||
fn start(&mut self) -> bool {
|
||||
if self.started() {
|
||||
debug!("Watcher {} is already started", self.name());
|
||||
return true;
|
||||
}
|
||||
debug!("Starting an external watcher {}", self.name());
|
||||
|
||||
let command = Command::new(&self.path).stdout(Stdio::null()).spawn();
|
||||
|
||||
match command {
|
||||
Ok(handle) => {
|
||||
self.handle = Some(handle);
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to start watcher {}: {e}", self.name());
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn stop(&mut self) {
|
||||
self.handle = if let Some(mut handle) = self.handle.take() {
|
||||
debug!("Stopping an external watcher {}", self.name());
|
||||
if let Err(e) = handle.kill() {
|
||||
error!("Failed to kill watcher {}: {}", self.name(), e);
|
||||
|
||||
Some(handle)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn started(&self) -> bool {
|
||||
self.handle.is_some()
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
self.path.file_name().unwrap().to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
self.path.as_path()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Manager {
|
||||
config_path: PathBuf,
|
||||
config: BundleConfig,
|
||||
pub path_watchers: Vec<ExternalWatcher>,
|
||||
}
|
||||
|
||||
impl Manager {
|
||||
pub fn new(path_env: &str, config_path: &Path) -> Self {
|
||||
let mut config_path = config_path.to_path_buf();
|
||||
config_path.push("bundle-config.toml");
|
||||
debug!("Processing bundle config at {}", config_path.display());
|
||||
|
||||
let config = Self::get_config(&config_path);
|
||||
|
||||
let mut path_watchers = Self::get_watchers_from_path_env(path_env);
|
||||
for watcher in &mut path_watchers {
|
||||
debug!("Found external watcher {}", watcher.name());
|
||||
let file_name = watcher.path.file_name().unwrap();
|
||||
if config
|
||||
.watchers
|
||||
.autostart
|
||||
.contains(&file_name.to_string_lossy().to_string())
|
||||
{
|
||||
watcher.start();
|
||||
} else {
|
||||
debug!(
|
||||
"External watcher {} is not configured to autostart",
|
||||
watcher.name()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
config_path,
|
||||
config,
|
||||
path_watchers,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_watcher(&mut self, watcher_path: &Path) -> bool {
|
||||
let watcher_name = if let Some(watcher) = self.get_watcher_by_path(watcher_path) {
|
||||
if watcher.start() {
|
||||
watcher.name().to_string()
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
};
|
||||
if !self.config.watchers.autostart.contains(&watcher_name) {
|
||||
self.config.watchers.autostart.push(watcher_name.clone());
|
||||
|
||||
self.update_config_watchers();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn stop_watcher(&mut self, watcher_path: &Path) {
|
||||
let watcher_name = if let Some(watcher) = self.get_watcher_by_path(watcher_path) {
|
||||
watcher.stop();
|
||||
Some(watcher.name().to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(watcher_name) = watcher_name {
|
||||
self.config
|
||||
.watchers
|
||||
.autostart
|
||||
.retain(|check| check != &watcher_name);
|
||||
|
||||
self.update_config_watchers();
|
||||
}
|
||||
}
|
||||
|
||||
fn update_config_watchers(&mut self) {
|
||||
let toml_content = toml::to_string_pretty(&self.config).unwrap();
|
||||
std::fs::write(&self.config_path, toml_content).unwrap();
|
||||
}
|
||||
|
||||
fn get_watcher_by_path(&mut self, watcher_path: &Path) -> Option<&mut ExternalWatcher> {
|
||||
self.path_watchers
|
||||
.iter_mut()
|
||||
.find(|watcher| watcher.path() == watcher_path)
|
||||
.or_else(|| {
|
||||
error!("Watcher is not found {}", watcher_path.display());
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
fn get_config(config_path: &Path) -> BundleConfig {
|
||||
let config_content = std::fs::read_to_string(config_path).ok();
|
||||
|
||||
if let Some(content) = config_content {
|
||||
toml::from_str(&content).unwrap_or_default()
|
||||
} else {
|
||||
debug!(
|
||||
"No bundle config found at {}, creating new file",
|
||||
config_path.display()
|
||||
);
|
||||
let config = BundleConfig::default();
|
||||
|
||||
let toml_content = toml::to_string_pretty(&config).unwrap();
|
||||
std::fs::write(config_path, toml_content).unwrap();
|
||||
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
fn get_watchers_from_path_env(path_env: &str) -> Vec<ExternalWatcher> {
|
||||
path_env
|
||||
.split(':')
|
||||
.map(Path::new)
|
||||
.filter(|&path| path.is_dir())
|
||||
.filter_map(|path| path.read_dir().ok())
|
||||
.flat_map(Iterator::flatten)
|
||||
.map(|entry| entry.path())
|
||||
.filter_map(ExternalWatcher::new)
|
||||
.fold(Vec::new(), |mut acc, watcher| {
|
||||
if acc.iter().any(|check| check.name() == watcher.name()) {
|
||||
warn!(
|
||||
"Duplicate watcher {} found in PATH, not running",
|
||||
watcher.path.display()
|
||||
);
|
||||
} else {
|
||||
acc.push(watcher);
|
||||
}
|
||||
acc
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rstest::{fixture, rstest};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use tempfile::{tempdir, TempDir};
|
||||
|
||||
#[test]
|
||||
fn test_get_watchers_from_path_env() {
|
||||
let dir = tempdir().unwrap();
|
||||
|
||||
let path = dir.path().join("test");
|
||||
let test_file = File::create(path).unwrap();
|
||||
|
||||
let path = dir.path().join("aw-test");
|
||||
let aw_test_file = File::create(path).unwrap();
|
||||
|
||||
let watchers = Manager::get_watchers_from_path_env(dir.path().to_str().unwrap());
|
||||
assert_eq!(watchers.len(), 0);
|
||||
|
||||
let mut permissions = test_file.metadata().unwrap().permissions();
|
||||
permissions.set_mode(0o111);
|
||||
test_file.set_permissions(permissions).unwrap();
|
||||
|
||||
let mut permissions = aw_test_file.metadata().unwrap().permissions();
|
||||
permissions.set_mode(0o111);
|
||||
aw_test_file.set_permissions(permissions).unwrap();
|
||||
|
||||
let watchers = Manager::get_watchers_from_path_env(dir.path().to_str().unwrap());
|
||||
assert_eq!(watchers.len(), 1);
|
||||
assert_eq!(watchers[0].name(), "aw-test");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_manager(temp_dir: TempDir) {
|
||||
std::fs::write(
|
||||
temp_dir.path().join("bundle-config.toml").as_path(),
|
||||
b"[watchers]\nautostart = [\"aw-test\", \"absent\"]\n",
|
||||
)
|
||||
.unwrap();
|
||||
let mut manager = Manager::new(temp_dir.path().to_str().unwrap(), temp_dir.path());
|
||||
assert_eq!(manager.path_watchers.len(), 1);
|
||||
assert_eq!(manager.path_watchers[0].name(), "aw-test");
|
||||
assert!(manager.path_watchers[0].handle.is_some());
|
||||
|
||||
assert!(!manager.start_watcher(&temp_dir.path().join("absent")));
|
||||
assert_autostart_content(&manager, &["aw-test", "absent"]);
|
||||
|
||||
assert!(manager.start_watcher(&temp_dir.path().join("aw-test"))); // already started
|
||||
assert!(manager.path_watchers[0].handle.is_some());
|
||||
assert_autostart_content(&manager, &["aw-test", "absent"]);
|
||||
|
||||
manager.stop_watcher(&temp_dir.path().join("absent"));
|
||||
assert_autostart_content(&manager, &["aw-test", "absent"]);
|
||||
|
||||
manager.stop_watcher(&temp_dir.path().join("aw-test"));
|
||||
assert!(manager.path_watchers[0].handle.is_none());
|
||||
assert_autostart_content(&manager, &["absent"]);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_broken_file(temp_dir: TempDir) {
|
||||
std::fs::write(
|
||||
temp_dir.path().join("bundle-config.toml").as_path(),
|
||||
b"[watchers]\n#autostart = [\"aw-test\"]\n",
|
||||
)
|
||||
.unwrap();
|
||||
let mut manager = Manager::new(temp_dir.path().to_str().unwrap(), temp_dir.path());
|
||||
assert_eq!(manager.path_watchers.len(), 1);
|
||||
assert_eq!(manager.path_watchers[0].name(), "aw-test");
|
||||
assert!(manager.path_watchers[0].handle.is_none()); // no starting in config
|
||||
|
||||
assert!(manager.start_watcher(&temp_dir.path().join("aw-test")));
|
||||
assert!(manager.path_watchers[0].handle.is_some());
|
||||
assert_autostart_content(&manager, &["aw-test"]);
|
||||
}
|
||||
|
||||
fn assert_autostart_content(manager: &Manager, watchers: &[&str]) {
|
||||
assert_eq!(manager.config.watchers.autostart, watchers);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(manager.config_path.as_path()).unwrap(),
|
||||
format!(
|
||||
"[watchers]\nautostart = [{}]\n",
|
||||
watchers
|
||||
.iter()
|
||||
.map(|w| format!("\"{w}\""))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", "),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn temp_dir() -> TempDir {
|
||||
let dir = tempdir().unwrap();
|
||||
|
||||
create_test_watcher(dir.path());
|
||||
|
||||
dir
|
||||
}
|
||||
|
||||
fn create_test_watcher(bin_dir: &Path) {
|
||||
let exec_path = bin_dir.join("aw-test");
|
||||
let mut aw_test_file = File::create(exec_path).unwrap();
|
||||
// write a bash script with infinite loop and sleep into the file:
|
||||
aw_test_file
|
||||
.write_all(b"#!/bin/bash\nwhile true; do sleep 1; done")
|
||||
.unwrap();
|
||||
// set execution permissions:
|
||||
let mut permissions = aw_test_file.metadata().unwrap().permissions();
|
||||
permissions.set_mode(0o755);
|
||||
aw_test_file.set_permissions(permissions).unwrap();
|
||||
aw_test_file.flush().unwrap();
|
||||
aw_test_file.sync_all().unwrap();
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@ zbus = {version = "3.14.1", optional = true}
|
||||
chrono = "0.4.31"
|
||||
toml = "0.8.1"
|
||||
dirs = "5.0.1"
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_default = "0.1.0"
|
||||
serde_json = "1.0.107"
|
||||
regex = "1.9.5"
|
||||
|
Loading…
x
Reference in New Issue
Block a user