mirror of
https://github.com/zaphar/runwhen.git
synced 2025-07-26 21:19:50 -04:00
Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
27cf79ffe4 | |||
fb954c6f0d | |||
e980306761 | |||
2f0ea778b6 | |||
a7caf8a1c8 | |||
38b0b6aa59 | |||
4d33126c69 |
9
Cargo.lock
generated
9
Cargo.lock
generated
@ -109,6 +109,12 @@ version = "0.3.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
|
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glob"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
@ -295,9 +301,10 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "runwhen"
|
name = "runwhen"
|
||||||
version = "0.0.6"
|
version = "0.0.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
|
"glob",
|
||||||
"humantime",
|
"humantime",
|
||||||
"notify",
|
"notify",
|
||||||
]
|
]
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "runwhen"
|
name = "runwhen"
|
||||||
version = "0.0.7"
|
version = "0.0.8"
|
||||||
authors = ["Jeremy Wall <jeremy@marzhillstudios.com>"]
|
authors = ["Jeremy Wall <jeremy@marzhillstudios.com>"]
|
||||||
description = "Runs a command on user specified triggers."
|
description = "Runs a command on user specified triggers."
|
||||||
repository = "https://github.com/zaphar/runwhen"
|
repository = "https://github.com/zaphar/runwhen"
|
||||||
@ -11,6 +11,7 @@ license = "Apache-2.0"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
humantime = "2.1.0"
|
humantime = "2.1.0"
|
||||||
notify = "4.0.17"
|
notify = "4.0.17"
|
||||||
|
glob = "0.3.1"
|
||||||
|
|
||||||
[dependencies.clap]
|
[dependencies.clap]
|
||||||
version = "3.2.17"
|
version = "3.2.17"
|
||||||
|
55
flake.lock
generated
55
flake.lock
generated
@ -31,21 +31,6 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"flake-utils_2": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1637014545,
|
|
||||||
"narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "bba5dcc8e0b20ab664967ad83d24d64cb64ec4f4",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"naersk": {
|
"naersk": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
@ -78,49 +63,11 @@
|
|||||||
"type": "indirect"
|
"type": "indirect"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1650222748,
|
|
||||||
"narHash": "sha256-AHh/goEfG5hlhIMVgGQwACbuv5Wit2ND9vrcB4QthJs=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "ba88a5afa6fff7710c17b5423ff9d721386c4164",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-compat": "flake-compat",
|
"flake-compat": "flake-compat",
|
||||||
"flake-utils": "flake-utils",
|
"flake-utils": "flake-utils",
|
||||||
"naersk": "naersk",
|
"naersk": "naersk"
|
||||||
"nixpkgs": "nixpkgs_2",
|
|
||||||
"rust-overlay": "rust-overlay"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rust-overlay": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-utils": "flake-utils_2",
|
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1650162887,
|
|
||||||
"narHash": "sha256-e23LlN7NQGxrsSWNNAjyvrWlZ3kwFSav9kXbayibKWc=",
|
|
||||||
"owner": "oxalica",
|
|
||||||
"repo": "rust-overlay",
|
|
||||||
"rev": "26b570500cdd7a359526524e9abad341891122a6",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "oxalica",
|
|
||||||
"repo": "rust-overlay",
|
|
||||||
"type": "github"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
17
flake.nix
17
flake.nix
@ -2,31 +2,24 @@
|
|||||||
description = "runwhen";
|
description = "runwhen";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs";
|
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
rust-overlay = {
|
|
||||||
url = "github:oxalica/rust-overlay";
|
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
|
||||||
naersk.url = "github:nix-community/naersk";
|
naersk.url = "github:nix-community/naersk";
|
||||||
flake-compat = {
|
flake-compat = {
|
||||||
url = github:edolstra/flake-compat;
|
url = "github:edolstra/flake-compat";
|
||||||
flake = false;
|
flake = false;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = {self, nixpkgs, flake-utils, rust-overlay, naersk, flake-compat}:
|
outputs = {self, flake-utils, naersk, flake-compat}:
|
||||||
flake-utils.lib.eachDefaultSystem (system:
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
let
|
let
|
||||||
overlays = [ rust-overlay.overlay ];
|
|
||||||
pkgs = import nixpkgs { inherit system overlays; };
|
|
||||||
naersk-lib = naersk.lib."${system}";
|
naersk-lib = naersk.lib."${system}";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
defaultPackage = with pkgs;
|
inherit flake-compat;
|
||||||
naersk-lib.buildPackage rec {
|
defaultPackage = naersk-lib.buildPackage rec {
|
||||||
pname = "runwhen";
|
pname = "runwhen";
|
||||||
version = "0.0.6";
|
version = "0.0.8";
|
||||||
src = ./.;
|
src = ./.;
|
||||||
cargoBuildOptions = opts: opts ++ ["-p" "${pname}" ];
|
cargoBuildOptions = opts: opts ++ ["-p" "${pname}" ];
|
||||||
};
|
};
|
||||||
|
@ -21,6 +21,19 @@ pub enum WatchEventType {
|
|||||||
Ignore,
|
Ignore,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_file(evt: &DebouncedEvent) -> Option<&std::path::PathBuf> {
|
||||||
|
match evt {
|
||||||
|
DebouncedEvent::NoticeWrite(b)
|
||||||
|
| DebouncedEvent::NoticeRemove(b)
|
||||||
|
| DebouncedEvent::Create(b)
|
||||||
|
| DebouncedEvent::Write(b)
|
||||||
|
| DebouncedEvent::Chmod(b)
|
||||||
|
| DebouncedEvent::Remove(b)
|
||||||
|
| DebouncedEvent::Rename(b, _) => Some(b),
|
||||||
|
DebouncedEvent::Error(_, _) | DebouncedEvent::Rescan => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<DebouncedEvent> for WatchEventType {
|
impl From<DebouncedEvent> for WatchEventType {
|
||||||
fn from(e: DebouncedEvent) -> WatchEventType {
|
fn from(e: DebouncedEvent) -> WatchEventType {
|
||||||
match e {
|
match e {
|
||||||
|
@ -95,6 +95,8 @@ impl CancelableProcess {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE(jwall): We want to actually use this some time when we figure out if it can be made to not block or not.
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn check(&mut self) -> Result<Option<i32>, CommandError> {
|
pub fn check(&mut self) -> Result<Option<i32>, CommandError> {
|
||||||
Ok(match self.handle {
|
Ok(match self.handle {
|
||||||
// TODO(jwall): This appears to block the thread despite the documenation. Figure out if this is fixable or not.
|
// TODO(jwall): This appears to block the thread despite the documenation. Figure out if this is fixable or not.
|
||||||
|
60
src/file.rs
60
src/file.rs
@ -14,8 +14,9 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use glob;
|
||||||
use notify::{watcher, RecursiveMode, Watcher};
|
use notify::{watcher, RecursiveMode, Watcher};
|
||||||
|
|
||||||
use error::CommandError;
|
use error::CommandError;
|
||||||
@ -27,6 +28,7 @@ pub struct FileProcess<'a> {
|
|||||||
cmd: &'a str,
|
cmd: &'a str,
|
||||||
env: Option<Vec<String>>,
|
env: Option<Vec<String>>,
|
||||||
files: Vec<&'a str>,
|
files: Vec<&'a str>,
|
||||||
|
exclude: Option<Vec<&'a str>>,
|
||||||
method: WatchEventType,
|
method: WatchEventType,
|
||||||
poll: Option<Duration>,
|
poll: Option<Duration>,
|
||||||
}
|
}
|
||||||
@ -36,6 +38,7 @@ impl<'a> FileProcess<'a> {
|
|||||||
cmd: &'a str,
|
cmd: &'a str,
|
||||||
env: Option<Vec<String>>,
|
env: Option<Vec<String>>,
|
||||||
file: Vec<&'a str>,
|
file: Vec<&'a str>,
|
||||||
|
exclude: Option<Vec<&'a str>>,
|
||||||
method: WatchEventType,
|
method: WatchEventType,
|
||||||
poll: Option<Duration>,
|
poll: Option<Duration>,
|
||||||
) -> FileProcess<'a> {
|
) -> FileProcess<'a> {
|
||||||
@ -44,6 +47,7 @@ impl<'a> FileProcess<'a> {
|
|||||||
env,
|
env,
|
||||||
method,
|
method,
|
||||||
poll,
|
poll,
|
||||||
|
exclude,
|
||||||
files: file,
|
files: file,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -67,40 +71,44 @@ fn watch_for_change_events(
|
|||||||
println!("Spawning command");
|
println!("Spawning command");
|
||||||
exec.spawn().expect("Failed to start command");
|
exec.spawn().expect("Failed to start command");
|
||||||
println!("Starting watch loop");
|
println!("Starting watch loop");
|
||||||
loop {
|
run_loop_step(&mut exec);
|
||||||
// Wait our requisit number of seconds
|
println!("Waiting for first change event");
|
||||||
if let Some(poll) = poll {
|
if let Some(poll) = poll {
|
||||||
thread::sleep(dbg!(poll));
|
let mut poll_time = Instant::now();
|
||||||
|
loop {
|
||||||
|
let _ = ch.recv().expect("Channel was closed!!!");
|
||||||
|
let elapsed = Instant::now().duration_since(poll_time);
|
||||||
|
poll_time = Instant::now();
|
||||||
|
if elapsed >= poll {
|
||||||
|
run_loop_step(&mut exec);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//if let Err(err) = exec.check() {
|
} else {
|
||||||
// println!("Error running command! {}", err);
|
loop {
|
||||||
// println!("Continuing");
|
let _ = ch.recv().expect("Channel was closed!!!");
|
||||||
//};
|
run_loop_step(&mut exec);
|
||||||
// Default to not running the command.
|
|
||||||
if !run_loop_step(&ch, &mut exec) {
|
|
||||||
println!("Failed to start command");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_loop_step(ch: &Receiver<()>, exec: &mut CancelableProcess) -> bool {
|
fn run_loop_step(exec: &mut CancelableProcess) {
|
||||||
let _ = ch.recv().unwrap();
|
|
||||||
// We always want to check on our process each iteration of the loop.
|
// We always want to check on our process each iteration of the loop.
|
||||||
// set signal to false so we won't trigger on the
|
// set signal to false so we won't trigger on the
|
||||||
// next loop iteration unless we recieved more events.
|
// next loop iteration unless we recieved more events.
|
||||||
// On a true signal we want to start or restart our process.
|
// On a true signal we want to start or restart our process.
|
||||||
println!("Restarting process");
|
println!("Restarting process");
|
||||||
if let Err(err) = exec.reset() {
|
if let Err(err) = exec.reset() {
|
||||||
|
println!("Failed to start command");
|
||||||
println!("{:?}", err);
|
println!("{:?}", err);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wait_for_fs_events(
|
fn wait_for_fs_events(
|
||||||
ch: Sender<()>,
|
ch: Sender<()>,
|
||||||
method: WatchEventType,
|
method: WatchEventType,
|
||||||
files: &Vec<&str>,
|
files: &Vec<&str>,
|
||||||
|
excluded: &Option<Vec<&str>>,
|
||||||
) -> Result<(), CommandError> {
|
) -> Result<(), CommandError> {
|
||||||
// Notify requires a channel for communication.
|
// Notify requires a channel for communication.
|
||||||
let (tx, rx) = channel();
|
let (tx, rx) = channel();
|
||||||
@ -116,9 +124,25 @@ fn wait_for_fs_events(
|
|||||||
watcher.watch(*file, RecursiveMode::Recursive)?;
|
watcher.watch(*file, RecursiveMode::Recursive)?;
|
||||||
println!("Watching {:?}", *file);
|
println!("Watching {:?}", *file);
|
||||||
}
|
}
|
||||||
|
let mut patterns = Vec::new();
|
||||||
|
if let Some(exclude) = excluded {
|
||||||
|
for ef in exclude.iter() {
|
||||||
|
patterns.push(glob::Pattern::new(*ef).expect("Invalid path pattern"));
|
||||||
|
}
|
||||||
|
}
|
||||||
loop {
|
loop {
|
||||||
let evt: WatchEventType = match rx.recv() {
|
let evt: WatchEventType = match rx.recv() {
|
||||||
Ok(event) => WatchEventType::from(event),
|
Ok(event) => {
|
||||||
|
// TODO(jwall): Filter this based on the exclude pattern
|
||||||
|
if let Some(f) = crate::events::get_file(&event) {
|
||||||
|
for pat in patterns.iter() {
|
||||||
|
if pat.matches_path(&f) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WatchEventType::from(event)
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Watch Error: {}", e);
|
println!("Watch Error: {}", e);
|
||||||
WatchEventType::Error
|
WatchEventType::Error
|
||||||
@ -154,7 +178,7 @@ impl<'a> Process for FileProcess<'a> {
|
|||||||
watch_for_change_events(rx, cmd, env, poll);
|
watch_for_change_events(rx, cmd, env, poll);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
wait_for_fs_events(tx, self.method.clone(), &self.files)?;
|
wait_for_fs_events(tx, self.method.clone(), &self.files, &self.exclude)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
20
src/main.rs
20
src/main.rs
@ -14,6 +14,7 @@
|
|||||||
// runwhen - A utility that runs commands on user defined triggers.
|
// runwhen - A utility that runs commands on user defined triggers.
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate clap;
|
extern crate clap;
|
||||||
|
extern crate glob;
|
||||||
extern crate humantime;
|
extern crate humantime;
|
||||||
extern crate notify;
|
extern crate notify;
|
||||||
|
|
||||||
@ -44,16 +45,20 @@ fn do_flags() -> clap::ArgMatches {
|
|||||||
clap::Command::new("watch")
|
clap::Command::new("watch")
|
||||||
.about("Trigger that fires when a file or directory changes.")
|
.about("Trigger that fires when a file or directory changes.")
|
||||||
.arg(
|
.arg(
|
||||||
arg!(-f --file).name("file")
|
arg!(-f --file ...).name("file")
|
||||||
.takes_value(true).help("File or directory to watch for changes"),
|
.takes_value(true).help("File or directory to watch for changes"),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
arg!(-e --exclude ...).name("exclude")
|
||||||
|
.takes_value(true).help("path names to skip when watching. Specified in unix glob format."),
|
||||||
|
)
|
||||||
.arg(arg!(--touch).name("filetouch").help("Use file or directory timestamps to monitor for changes."))
|
.arg(arg!(--touch).name("filetouch").help("Use file or directory timestamps to monitor for changes."))
|
||||||
.arg(arg!(--poll).value_parser(value_parser!(humantime::Duration)).help("Duration of time between polls")))
|
.arg(arg!(--poll).name("poll").takes_value(true).value_parser(value_parser!(humantime::Duration)).help("Duration of time between polls")))
|
||||||
.subcommand(
|
.subcommand(
|
||||||
clap::Command::new("timer")
|
clap::Command::new("timer")
|
||||||
.about("Run command on a timer")
|
.about("Run command on a timer")
|
||||||
.arg(arg!(-t --duration).takes_value(true).value_parser(value_parser!(humantime::Duration)).help("Duration between runs"))
|
.arg(arg!(-t --duration).takes_value(true).value_parser(value_parser!(humantime::Duration)).help("Duration between runs"))
|
||||||
.arg(arg!(-n --repeat).value_parser(value_parser!(u32))).about("Number of times to run before finishing"))
|
.arg(arg!(-n --repeat).value_parser(value_parser!(u32)).help("Number of times to run before finishing")))
|
||||||
.subcommand(
|
.subcommand(
|
||||||
clap::Command::new("success")
|
clap::Command::new("success")
|
||||||
.about("Run a command when a test command succeeds")
|
.about("Run a command when a test command succeeds")
|
||||||
@ -90,7 +95,14 @@ fn main() {
|
|||||||
Some(d) => Some((*d).into()),
|
Some(d) => Some((*d).into()),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
Box::new(FileProcess::new(cmd, maybe_env, file, method, duration))
|
let exclude = match matches.values_of("exclude") {
|
||||||
|
Some(vr) => Some(vr.collect()),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
println!("Enforcing a poll time of {:?}", duration);
|
||||||
|
Box::new(FileProcess::new(
|
||||||
|
cmd, maybe_env, file, exclude, method, duration,
|
||||||
|
))
|
||||||
} else if let Some(matches) = app.subcommand_matches("timer") {
|
} else if let Some(matches) = app.subcommand_matches("timer") {
|
||||||
// TODO(jwall): This should use cancelable commands.
|
// TODO(jwall): This should use cancelable commands.
|
||||||
// Unwrap because this flag is required.
|
// Unwrap because this flag is required.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user