First draft on a daemon instance.

pull/3/head
Joshua Potter 2021-12-30 13:18:52 -05:00
parent 8db3c4b4ab
commit ba69724c49
4 changed files with 110 additions and 13 deletions

View File

@ -13,6 +13,7 @@ edition = "2021"
ansi_term = "0.12.1" ansi_term = "0.12.1"
clap = { version = "3.0.0-rc.9", features = ["derive"] } clap = { version = "3.0.0-rc.9", features = ["derive"] }
notify = "4.0.16" notify = "4.0.16"
regex = "1.5.4"
serde = "1.0" serde = "1.0"
serde_derive = "1.0.132" serde_derive = "1.0.132"
serde_yaml = "0.8" serde_yaml = "0.8"

View File

@ -54,7 +54,7 @@ pub struct Remote {
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct Package { pub struct Package {
pub configs: Vec<String>, pub configs: Vec<PathBuf>,
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]

View File

@ -1,20 +1,114 @@
use super::config::PathConfig;
use notify::{RecommendedWatcher, RecursiveMode, Watcher}; 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::sync::mpsc::channel;
use std::time::Duration; use std::time::Duration;
fn watch(path: &Path) -> notify::Result<()> { // TODO(jrpotter): Add logging.
// Create a channel to receive the events. // 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<OsString> {
let re = Regex::new(r"\$(?P<env>[[: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<PathBuf> {
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(); let (tx, rx) = channel();
// Create a "debounced" watcher. Events will not trigger until after the
// Automatically select the best implementation for your platform. // specified duration has passed with no additional changes.
// You can also access each implementation directly e.g. INotifyWatcher.
let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_secs(2))?; let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_secs(2))?;
// Take in the `homesync` configuration and add watchers to all paths
// Add a path to be watched. All files and directories at that path and // specified in the configuration. `watch` appends the path onto a list so
// below will be monitored for changes. // avoid tracking the same path multiple times.
watcher.watch(path, RecursiveMode::NonRecursive)?; let mut tracked_paths: HashSet<PathBuf> = HashSet::new();
// TODO(jrpotter): Spawn thread responsible for polling for missing files.
let mut missing_paths: HashSet<PathBuf> = 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, // This is a simple loop, but you may want to use more complex logic here,
// for example to handle I/O. // for example to handle I/O.
loop { loop {

View File

@ -11,7 +11,9 @@ pub fn run_add(_candidates: Vec<PathBuf>) -> Result<(), config::Error> {
Ok(()) Ok(())
} }
pub fn run_daemon(_candidates: Vec<PathBuf>) -> Result<(), Box<dyn Error>> { pub fn run_daemon(candidates: Vec<PathBuf>) -> Result<(), Box<dyn Error>> {
let loaded = config::load(&candidates)?;
daemon::launch(loaded)?;
Ok(()) Ok(())
} }