From ba69724c496366db4a109897c9cc516c6f8f13ab Mon Sep 17 00:00:00 2001 From: Joshua Potter Date: Thu, 30 Dec 2021 13:18:52 -0500 Subject: [PATCH] First draft on a daemon instance. --- Cargo.toml | 1 + src/config.rs | 2 +- src/daemon.rs | 116 +++++++++++++++++++++++++++++++++++++++++++++----- src/lib.rs | 4 +- 4 files changed, 110 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bb4251d..22c6b51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ edition = "2021" ansi_term = "0.12.1" clap = { version = "3.0.0-rc.9", features = ["derive"] } notify = "4.0.16" +regex = "1.5.4" serde = "1.0" serde_derive = "1.0.132" serde_yaml = "0.8" diff --git a/src/config.rs b/src/config.rs index c985f1f..04f627e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -54,7 +54,7 @@ pub struct Remote { #[derive(Debug, Deserialize, Serialize)] pub struct Package { - pub configs: Vec, + pub configs: Vec, } #[derive(Debug, Deserialize, Serialize)] diff --git a/src/daemon.rs b/src/daemon.rs index 274f1d6..f4ce347 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,20 +1,114 @@ +use super::config::PathConfig; use notify::{RecommendedWatcher, RecursiveMode, Watcher}; -use std::path::Path; +use regex::Regex; +use std::collections::HashSet; +use std::env; +use std::ffi::{OsStr, OsString}; +use std::io; +use std::path::{Component, Path, PathBuf}; use std::sync::mpsc::channel; use std::time::Duration; -fn watch(path: &Path) -> notify::Result<()> { - // Create a channel to receive the events. +// TODO(jrpotter): Add logging. +// TODO(jrpotter): Add pid file to only allow one daemon at a time. + +// Find environment variables found within the argument and expand them if +// possible. +// +// Returns `None` in the case an environment variable present within the +// argument is not defined. +fn expand_str(s: &OsStr) -> Option { + let re = Regex::new(r"\$(?P[[:alnum:]]+)").unwrap(); + let lossy = s.to_string_lossy(); + let mut path = lossy.clone().to_string(); + for caps in re.captures_iter(&lossy) { + let evar = env::var(&caps["env"]).ok()?; + path = path.replace(&format!("${}", &caps["env"]), &evar); + } + Some(path.into()) +} + +// Normalizes the provided path, returning a new instance. +// +// There current doesn't exist a method that yields some canonical path for +// files that do not exist (at least in the parts of the standard library I've +// looked in). We create a consistent view of every path so as to avoid +// watching the same path multiple times, which would duplicate messages on +// changes. +fn normalize_path(path: &Path) -> io::Result { + let mut pb = env::current_dir()?; + for comp in path.components() { + match comp { + Component::Prefix(_) => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "We do not currently support Windows.", + )) + } + Component::RootDir => { + pb.clear(); + pb.push(Component::RootDir) + } + Component::CurDir => (), // Make no changes. + Component::ParentDir => { + if !pb.pop() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Cannot take parent of root.", + )); + } + } + Component::Normal(c) => match expand_str(c) { + Some(c) => pb.push(Component::Normal(&c)), + None => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("Cannot find path {}", path.display().to_string()), + )) + } + }, + } + } + Ok(pb) +} + +/// Launches the daemon instance. +/// +/// This method also spawns an additional thread responsible for handling +/// polling of files that are specified in the config but do not exist. +pub fn launch(config: PathConfig) -> notify::Result<()> { let (tx, rx) = channel(); - - // Automatically select the best implementation for your platform. - // You can also access each implementation directly e.g. INotifyWatcher. + // Create a "debounced" watcher. Events will not trigger until after the + // specified duration has passed with no additional changes. let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_secs(2))?; - - // Add a path to be watched. All files and directories at that path and - // below will be monitored for changes. - watcher.watch(path, RecursiveMode::NonRecursive)?; - + // Take in the `homesync` configuration and add watchers to all paths + // specified in the configuration. `watch` appends the path onto a list so + // avoid tracking the same path multiple times. + let mut tracked_paths: HashSet = HashSet::new(); + // TODO(jrpotter): Spawn thread responsible for polling for missing files. + let mut missing_paths: HashSet = HashSet::new(); + for (_, package) in &config.1.packages { + for path in &package.configs { + match normalize_path(&path) { + // `notify-rs` is not able to handle files that do not exist and + // are then created. This is handled internally by the library + // via the `fs::canonicalize` which fails on missing paths. So + // track which paths end up missing and apply polling on them. + Ok(normalized) => match watcher.watch(&normalized, RecursiveMode::NonRecursive) { + Ok(_) => { + tracked_paths.insert(normalized); + } + Err(notify::Error::PathNotFound) => { + missing_paths.insert(normalized); + } + Err(e) => return Err(e), + }, + // TODO(jrpotter): Retry even in cases where environment + // variables are not defined. + Err(e) => eprintln!("{}", e), + }; + } + } // This is a simple loop, but you may want to use more complex logic here, // for example to handle I/O. loop { diff --git a/src/lib.rs b/src/lib.rs index f5631ef..8857427 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,9 @@ pub fn run_add(_candidates: Vec) -> Result<(), config::Error> { Ok(()) } -pub fn run_daemon(_candidates: Vec) -> Result<(), Box> { +pub fn run_daemon(candidates: Vec) -> Result<(), Box> { + let loaded = config::load(&candidates)?; + daemon::launch(loaded)?; Ok(()) }