First draft on a daemon instance.
parent
8db3c4b4ab
commit
ba69724c49
|
@ -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"
|
||||
|
|
|
@ -54,7 +54,7 @@ pub struct Remote {
|
|||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Package {
|
||||
pub configs: Vec<String>,
|
||||
pub configs: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
|
|
116
src/daemon.rs
116
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<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();
|
||||
|
||||
// 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<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,
|
||||
// for example to handle I/O.
|
||||
loop {
|
||||
|
|
|
@ -11,7 +11,9 @@ pub fn run_add(_candidates: Vec<PathBuf>) -> Result<(), config::Error> {
|
|||
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(())
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue