First draft on a daemon instance.
parent
8db3c4b4ab
commit
ba69724c49
|
@ -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"
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
116
src/daemon.rs
116
src/daemon.rs
|
@ -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 {
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue