Compare commits

...

15 Commits

13 changed files with 563 additions and 441 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

350
Cargo.lock generated
View File

@ -2,15 +2,6 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "ansi_term"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "atty"
version = "0.2.14"
@ -23,16 +14,10 @@ dependencies = [
]
[[package]]
name = "bitflags"
version = "0.4.0"
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8dead7461c1127cf637931a1e50934eb6eee8bff2f74433ac7909e9afcee04a3"
[[package]]
name = "bitflags"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bitflags"
@ -40,12 +25,6 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bytes"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c129aff112dcc562970abb69e2508b40850dd24c274761bb50fb8a0067ba6c27"
[[package]]
name = "cfg-if"
version = "0.1.10"
@ -60,50 +39,88 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "2.34.0"
version = "3.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
checksum = "29e724a68d9319343bb3328c9cc2dfde263f4b3142ee1059a9980580171c954b"
dependencies = [
"ansi_term",
"atty",
"bitflags 1.3.2",
"bitflags",
"clap_lex",
"indexmap",
"once_cell",
"strsim",
"termcolor",
"textwrap",
"unicode-width",
"vec_map",
]
[[package]]
name = "clap_lex"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
dependencies = [
"os_str_bytes",
]
[[package]]
name = "filetime"
version = "0.1.15"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714653f3e34871534de23771ac7b26e999651a0a228f47beb324dfdf1dd4b10f"
checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c"
dependencies = [
"cfg-if 0.1.10",
"cfg-if 1.0.0",
"libc",
"redox_syscall",
"windows-sys",
]
[[package]]
name = "fsevent"
version = "0.2.17"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4bbbf71584aeed076100b5665ac14e3d85eeb31fdbb45fbd41ef9a682b5ec05"
checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6"
dependencies = [
"bitflags 0.7.0",
"bitflags",
"fsevent-sys",
"libc",
]
[[package]]
name = "fsevent-sys"
version = "0.1.6"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a772d36c338d07a032d5375a36f15f9a7043bf0cb8ce7cee658e037c6032874"
checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0"
dependencies = [
"libc",
]
[[package]]
name = "fuchsia-zircon"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
dependencies = [
"bitflags",
"fuchsia-zircon-sys",
]
[[package]]
name = "fuchsia-zircon-sys"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
[[package]]
name = "glob"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hermit-abi"
version = "0.1.19"
@ -115,18 +132,45 @@ dependencies = [
[[package]]
name = "humantime"
version = "1.0.0"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e9298fffb2a54569e1fcb818e9c2ff77caa2fad68d64b6e409b9f777bdb1960"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "indexmap"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
dependencies = [
"quick-error",
"autocfg",
"hashbrown",
]
[[package]]
name = "inotify"
version = "0.2.3"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8458c07bdbdaf309c80e2c3304d14c3db64e7465d4f07cf589ccb83fd0ff31a"
checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f"
dependencies = [
"bitflags",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
]
[[package]]
name = "iovec"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
dependencies = [
"libc",
]
@ -141,21 +185,18 @@ dependencies = [
"winapi-build",
]
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "libc"
version = "0.2.123"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb691a747a7ab48abc15c5b42066eaafde10dc427e3b6ee2a1cf43db04c763bd"
[[package]]
name = "log"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b"
dependencies = [
"log 0.4.16",
]
[[package]]
name = "log"
version = "0.4.16"
@ -167,26 +208,40 @@ dependencies = [
[[package]]
name = "mio"
version = "0.5.1"
version = "0.6.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a637d1ca14eacae06296a008fa7ad955347e34efcb5891cfd8ba05491a37907e"
checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4"
dependencies = [
"bytes",
"cfg-if 0.1.10",
"fuchsia-zircon",
"fuchsia-zircon-sys",
"iovec",
"kernel32-sys",
"libc",
"log 0.3.9",
"log",
"miow",
"net2",
"nix",
"slab",
"time",
"winapi 0.2.8",
]
[[package]]
name = "miow"
version = "0.1.5"
name = "mio-extras"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e690c5df6b2f60acd45d56378981e827ff8295562fc8d34f573deb267a59cd1"
checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19"
dependencies = [
"lazycell",
"log",
"mio",
"slab",
]
[[package]]
name = "miow"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d"
dependencies = [
"kernel32-sys",
"net2",
@ -205,116 +260,105 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "nix"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfb3ddedaa14746434a02041940495bf11325c22f6d36125d3bdd56090d50a79"
dependencies = [
"bitflags 0.4.0",
"libc",
]
[[package]]
name = "notify"
version = "3.0.1"
version = "4.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13fdd4a6894329b193f38f03a88823ce721275fdfdb29820c44a30515033524e"
checksum = "ae03c8c853dba7bfd23e571ff0cff7bc9dceb40a4cd684cd1681824183f45257"
dependencies = [
"bitflags 0.7.0",
"bitflags",
"filetime",
"fsevent",
"fsevent-sys",
"inotify",
"kernel32-sys",
"libc",
"mio",
"time",
"mio-extras",
"walkdir",
"winapi 0.2.8",
"winapi 0.3.9",
]
[[package]]
name = "quick-error"
version = "1.2.3"
name = "once_cell"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e"
[[package]]
name = "os_str_bytes"
version = "6.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff"
[[package]]
name = "redox_syscall"
version = "0.1.57"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags",
]
[[package]]
name = "runwhen"
version = "0.0.5"
version = "0.0.8"
dependencies = [
"clap",
"glob",
"humantime",
"notify",
]
[[package]]
name = "slab"
version = "0.1.3"
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d807fd58c4181bbabed77cb3b891ba9748241a552bcc5be698faaebefc54f46e"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "slab"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef"
dependencies = [
"autocfg",
]
[[package]]
name = "strsim"
version = "0.8.0"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "termcolor"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
dependencies = [
"winapi-util",
]
[[package]]
name = "textwrap"
version = "0.11.0"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [
"unicode-width",
]
[[package]]
name = "time"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
dependencies = [
"libc",
"wasi",
"winapi 0.3.9",
]
[[package]]
name = "unicode-width"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
[[package]]
name = "walkdir"
version = "0.1.8"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c66c0b9792f0a765345452775f3adbd28dde9d33f30d13e5dcc5ae17cf6f3780"
checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
dependencies = [
"kernel32-sys",
"winapi 0.2.8",
"same-file",
"winapi 0.3.9",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "winapi"
version = "0.2.8"
@ -343,12 +387,64 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
dependencies = [
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
[[package]]
name = "windows_i686_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
[[package]]
name = "windows_i686_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
[[package]]
name = "windows_x86_64_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
[[package]]
name = "ws2_32-sys"
version = "0.2.1"

View File

@ -1,6 +1,6 @@
[package]
name = "runwhen"
version = "0.0.5"
version = "0.0.8"
authors = ["Jeremy Wall <jeremy@marzhillstudios.com>"]
description = "Runs a command on user specified triggers."
repository = "https://github.com/zaphar/runwhen"
@ -9,6 +9,10 @@ keywords = ["file", "watcher", "command-line", "trigger"]
license = "Apache-2.0"
[dependencies]
clap = "~2.34"
humantime = "~1.0.0"
notify = "~3.0.0"
humantime = "2.1.0"
notify = "4.0.17"
glob = "0.3.1"
[dependencies.clap]
version = "3.2.17"
features = [ "cargo" ]

55
flake.lock generated
View File

@ -31,21 +31,6 @@
"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": {
"inputs": {
"nixpkgs": "nixpkgs"
@ -78,49 +63,11 @@
"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": {
"inputs": {
"flake-compat": "flake-compat",
"flake-utils": "flake-utils",
"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"
"naersk": "naersk"
}
}
},

View File

@ -2,33 +2,26 @@
description = "runwhen";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs";
flake-utils.url = "github:numtide/flake-utils";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
naersk.url = "github:nix-community/naersk";
flake-compat = {
url = github:edolstra/flake-compat;
url = "github:edolstra/flake-compat";
flake = false;
};
};
outputs = {self, nixpkgs, flake-utils, rust-overlay, naersk, flake-compat}:
outputs = {self, flake-utils, naersk, flake-compat}:
flake-utils.lib.eachDefaultSystem (system:
let
overlays = [ rust-overlay.overlay ];
pkgs = import nixpkgs { inherit system overlays; };
naersk-lib = naersk.lib."${system}";
in
{
defaultPackage = with pkgs;
naersk-lib.buildPackage rec {
inherit flake-compat;
defaultPackage = naersk-lib.buildPackage rec {
pname = "runwhen";
version = "0.0.4";
version = "0.0.8";
src = ./.;
cargoBuildOptions = opts: opts ++ ["-p" "${pname}" ];
};
});
}
}

1
result Symbolic link
View File

@ -0,0 +1 @@
/nix/store/wllb7d3wx2wh2p4h8vj7y10p31f37jd8-runwhen-0.0.4

View File

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::fmt;
use std::io;
use notify;
@ -37,3 +38,9 @@ impl From<notify::Error> for CommandError {
CommandError::new(format!("{}", e))
}
}
impl From<io::Error> for CommandError {
fn from(e: io::Error) -> CommandError {
CommandError::new(format!("IO: {}", e))
}
}

View File

@ -21,6 +21,19 @@ pub enum WatchEventType {
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 {
fn from(e: DebouncedEvent) -> WatchEventType {
match e {

View File

@ -11,11 +11,10 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::process::{Child, Command, Stdio};
use std::thread;
use std::time::Duration;
use std::process::{Command, Stdio};
use error::CommandError;
use traits::Process;
@ -30,79 +29,151 @@ fn env_var_to_tuple(var: &str) -> (String, String) {
("".to_string(), "".to_string())
}
pub fn run_cmd(cmd: &str, env: &Option<Vec<&str>>) -> Result<i32, CommandError> {
let args = cmd
.split(' ')
.filter(|s| !s.is_empty())
.collect::<Vec<&str>>();
if args.len() < 1 {
return Err(CommandError::new("Empty command string passed in"));
}
let mut exec = Command::new(args[0]);
if args.len() > 1 {
exec.args(&args[1..]);
}
exec.stdout(Stdio::inherit());
exec.stderr(Stdio::inherit());
if let &Some(ref env_vars) = env {
for var in env_vars {
let tpl = env_var_to_tuple(var);
exec.env(tpl.0, tpl.1);
pub struct CancelableProcess {
cmd: String,
env: Option<Vec<String>>,
exec: Option<Command>,
handle: Option<Child>,
}
impl CancelableProcess {
pub fn new(cmd: &str, env: Option<Vec<String>>) -> Self {
Self {
cmd: cmd.to_string(),
env,
exec: None,
handle: None,
}
}
return match exec.output() {
Ok(out) => match out.status.code() {
Some(val) => Ok(val),
None => Ok(0),
},
// TODO(jeremy): We should not swallow this error.
Err(_) => Err(CommandError::new("Error running command")),
};
}
fn is_cmd_success(cmd: &str, env: Option<Vec<&str>>) -> bool {
match run_cmd(cmd, &env) {
Ok(code) => code == 0,
_ => false,
fn create_command(cmd: &str, env: &Option<Vec<String>>) -> Result<Command, CommandError> {
let args = cmd
.split(' ')
.filter(|s| !s.is_empty())
.collect::<Vec<&str>>();
if args.len() < 1 {
return Err(CommandError::new("Empty command string passed in"));
}
let mut exec = Command::new(args[0]);
if args.len() > 1 {
exec.args(&args[1..]);
}
exec.stdout(Stdio::inherit());
exec.stderr(Stdio::inherit());
if let &Some(ref env_vars) = env {
for var in env_vars {
let tpl = env_var_to_tuple(var);
exec.env(tpl.0, tpl.1);
}
}
return Ok(exec);
}
pub fn block(&mut self) -> Result<i32, CommandError> {
if let Some(ref mut handle) = self.handle {
let code = handle.wait()?.code().unwrap_or(0);
self.exec = None;
self.handle = None;
Ok(code)
} else {
let mut exec = Self::create_command(&self.cmd, &self.env)?;
return match exec.output() {
Ok(out) => match out.status.code() {
Some(val) => Ok(val),
None => Ok(0),
},
// TODO(jeremy): We should not swallow this error.
Err(_) => Err(CommandError::new("Error running command")),
};
}
}
pub fn is_success(&mut self) -> bool {
match self.block() {
Ok(code) => code == 0,
_ => false,
}
}
// 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> {
Ok(match self.handle {
// TODO(jwall): This appears to block the thread despite the documenation. Figure out if this is fixable or not.
Some(ref mut h) => match h.try_wait()? {
Some(status) => Some(status.code().unwrap_or(0)),
None => Some(h.wait()?.code().unwrap_or(0)),
},
None => None,
})
}
pub fn spawn(&mut self) -> Result<(), CommandError> {
let mut exec = Self::create_command(&self.cmd, &self.env)?;
let handle = exec.spawn()?;
self.exec = Some(exec);
self.handle = Some(handle);
Ok(())
}
pub fn cancel(&mut self) -> Result<(), CommandError> {
if let Some(ref mut h) = self.handle {
let _ = h.kill();
}
self.exec = None;
self.handle = None;
Ok(())
}
pub fn reset(&mut self) -> Result<(), CommandError> {
self.cancel()?;
self.spawn()?;
Ok(())
}
}
pub struct ExecProcess<'a> {
test_cmd: &'a str,
// TODO(jwall): Make these CancelableProcess instead.
pub struct ExecProcess {
test_cmd: CancelableProcess,
negate: bool,
cmd: &'a str,
env: Option<Vec<&'a str>>,
cmd: CancelableProcess,
poll: Duration,
}
impl<'a> ExecProcess<'a> {
impl ExecProcess {
pub fn new(
test_cmd: &'a str,
cmd: &'a str,
test_cmd: &str,
cmd: &str,
negate: bool,
env: Option<Vec<&'a str>>,
env: Option<Vec<String>>,
poll: Duration,
) -> ExecProcess<'a> {
) -> ExecProcess {
let test_cmd = CancelableProcess::new(test_cmd, None);
let cmd = CancelableProcess::new(cmd, env);
ExecProcess {
test_cmd: test_cmd,
negate: negate,
cmd: cmd,
env: env,
poll: poll,
test_cmd,
negate,
cmd,
poll,
}
}
fn run_loop_step(&mut self) {
let test_result = self.test_cmd.is_success();
if (test_result && !self.negate) || (!test_result && self.negate) {
if let Err(err) = self.cmd.block() {
println!("{:?}", err)
}
}
}
}
impl<'a> Process for ExecProcess<'a> {
fn run(&self) -> Result<(), CommandError> {
impl Process for ExecProcess {
fn run(&mut self) -> Result<(), CommandError> {
loop {
// TODO(jwall): Should we set the environment the same as the other command?
let test_result = is_cmd_success(self.test_cmd, None);
if (test_result && !self.negate) || (!test_result && self.negate) {
if let Err(err) = run_cmd(self.cmd, &self.env) {
println!("{:?}", err)
}
}
self.run_loop_step();
thread::sleep(self.poll);
}
}

View File

@ -12,49 +12,52 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::path::Path;
use std::sync::mpsc::channel;
use std::sync::{Arc, Mutex};
use std::sync::mpsc::{channel, Receiver, Sender};
use std::thread;
use std::time::Duration;
use std::time::{Duration, Instant};
use glob;
use notify::{watcher, RecursiveMode, Watcher};
use error::CommandError;
use events::WatchEventType;
use exec::run_cmd;
use exec::CancelableProcess;
use traits::Process;
pub struct FileProcess<'a> {
cmd: &'a str,
env: Option<Vec<&'a str>>,
env: Option<Vec<String>>,
files: Vec<&'a str>,
exclude: Option<Vec<&'a str>>,
method: WatchEventType,
poll: Duration,
poll: Option<Duration>,
}
impl<'a> FileProcess<'a> {
pub fn new(
cmd: &'a str,
env: Option<Vec<&'a str>>,
env: Option<Vec<String>>,
file: Vec<&'a str>,
exclude: Option<Vec<&'a str>>,
method: WatchEventType,
poll: Duration,
poll: Option<Duration>,
) -> FileProcess<'a> {
FileProcess {
cmd: cmd,
env: env,
cmd,
env,
method,
poll,
exclude,
files: file,
method: method,
poll: poll,
}
}
}
fn spawn_runner_thread(
lock: Arc<Mutex<bool>>,
fn watch_for_change_events(
ch: Receiver<()>,
cmd: String,
env: Option<Vec<&str>>,
poll: Duration,
env: Option<Vec<String>>,
poll: Option<Duration>,
) {
let copied_env = env.and_then(|v| {
Some(
@ -64,52 +67,52 @@ fn spawn_runner_thread(
.collect::<Vec<String>>(),
)
});
thread::spawn(move || {
let copied_env_refs: Option<Vec<&str>> = match copied_env {
Some(ref vec) => {
let mut refs: Vec<&str> = Vec::new();
for s in vec.iter() {
refs.push(s);
}
Some(refs)
}
None => None,
};
let mut exec = CancelableProcess::new(&cmd, copied_env);
println!("Spawning command");
exec.spawn().expect("Failed to start command");
println!("Starting watch loop");
run_loop_step(&mut exec);
println!("Waiting for first change event");
if let Some(poll) = poll {
let mut poll_time = Instant::now();
loop {
// Wait our requisit number of seconds
thread::sleep(poll);
// Default to not running the command.
match lock.lock() {
Ok(mut signal) => {
if *signal {
// set signal to false so we won't trigger on the
// next loop iteration unless we recieved more events.
*signal = false;
// Run our command!
println!("exec: {}", cmd);
if let Err(err) = run_cmd(&cmd, &copied_env_refs) {
println!("{:?}", err)
}
}
}
Err(err) => {
println!("Unexpected error; {}", err);
return;
}
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;
}
}
});
} else {
loop {
let _ = ch.recv().expect("Channel was closed!!!");
run_loop_step(&mut exec);
}
}
}
fn run_loop_step(exec: &mut CancelableProcess) {
// We always want to check on our process each iteration of the loop.
// set signal to false so we won't trigger on the
// next loop iteration unless we recieved more events.
// On a true signal we want to start or restart our process.
println!("Restarting process");
if let Err(err) = exec.reset() {
println!("Failed to start command");
println!("{:?}", err);
}
}
fn wait_for_fs_events(
lock: Arc<Mutex<bool>>,
ch: Sender<()>,
method: WatchEventType,
files: &Vec<&str>,
excluded: &Option<Vec<&str>>,
) -> Result<(), CommandError> {
// Notify requires a channel for communication.
let (tx, rx) = channel();
let mut watcher = watcher(tx, Duration::from_secs(1))?;
// TODO(jwall): Better error handling.
for file in files {
// NOTE(jwall): this is necessary because notify::fsEventWatcher panics
// if the path doesn't exist. :-(
@ -121,48 +124,61 @@ fn wait_for_fs_events(
watcher.watch(*file, RecursiveMode::Recursive)?;
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 {
let evt: WatchEventType = match rx.recv() {
Ok(event) => WatchEventType::from(event),
Err(_) => WatchEventType::Error,
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) => {
println!("Watch Error: {}", e);
WatchEventType::Error
}
};
match evt {
WatchEventType::Ignore => {
// We ignore this one.
}
WatchEventType::Error => {
// We log this one.
WatchEventType::Ignore | WatchEventType::Error => {
// We ignore these.
//println!("Event: Ignore");
}
WatchEventType::Touched => {
if method == WatchEventType::Touched {
let mut signal = lock.lock().unwrap();
*signal = true;
} else {
println!("Ignoring touched event");
ch.send(()).unwrap();
}
}
WatchEventType::Changed => match lock.lock() {
Ok(mut signal) => *signal = true,
Err(err) => {
println!("Unexpected error; {}", err);
return Ok(());
}
},
WatchEventType::Changed => {
ch.send(()).unwrap();
}
}
}
}
impl<'a> Process for FileProcess<'a> {
fn run(&self) -> Result<(), CommandError> {
fn run(&mut self) -> Result<(), CommandError> {
// TODO(jeremy): Is this sufficent or do we want to ignore
// any events that come in while the command is running?
let lock = Arc::new(Mutex::new(false));
spawn_runner_thread(
lock.clone(),
self.cmd.to_string(),
self.env.clone(),
self.poll,
);
wait_for_fs_events(lock, self.method.clone(), &self.files)
let (tx, rx) = channel();
thread::spawn({
let cmd = self.cmd.to_string();
let env = self.env.clone();
let poll = self.poll.clone();
move || {
watch_for_change_events(rx, cmd, env, poll);
}
});
wait_for_fs_events(tx, self.method.clone(), &self.files, &self.exclude)?;
Ok(())
}
}

View File

@ -14,11 +14,11 @@
// runwhen - A utility that runs commands on user defined triggers.
#[macro_use]
extern crate clap;
extern crate glob;
extern crate humantime;
extern crate notify;
use std::process;
use std::str::FromStr;
use std::{process, str::FromStr};
mod error;
mod events;
@ -33,42 +33,39 @@ use file::FileProcess;
use timer::TimerProcess;
use traits::Process;
fn do_flags<'a>() -> clap::ArgMatches<'a> {
clap_app!(
runwhen =>
(version: crate_version!())
(author: crate_authors!())
(about: "Runs a command on user defined triggers.")
(@arg cmd: -c --cmd +required +takes_value "Command to run on supplied triggers")
(@arg env: -e --env +takes_value ... "Env variables to set for the command")
(@subcommand watch =>
(about: "Trigger that fires when a file or directory changes.")
// TODO(jeremy): We need to support filters
(@arg file: -f --file +takes_value ...
"File/Directory to watch. (default current working directory)")
(@arg filetouch: --touch
"Watches for attribute modifications as well as content changes.")
(@arg wait: --poll +takes_value
"How frequently to poll for events (default 5s)")
)
(@subcommand timer =>
(about: "Trigger that fires on a timer.")
(@arg duration: -t --duration +required +takes_value
"Defines timer frequency.")
(@arg repeat: -n --repeat +takes_value
"Defines an optional max number times to run on repeat.")
)
(@subcommand success =>
(about: "Trigger that fires if a command runs successful.")
(@arg ifcmd: --if +required +takes_value
"The command to test for successful exit from")
(@arg not: --not
"Negate the test command so we run on failure instead of success.")
(@arg wait: --poll +takes_value
"How frequently to test command (default 5s)")
)
)
.get_matches()
#[rustfmt::skip]
fn do_flags() -> clap::ArgMatches {
clap::command!()
.version(crate_version!())
.author(crate_authors!())
.about("Runs a command on user defined triggers.")
.arg(arg!(-c --cmd).takes_value(true).help("The command to run on the trigger"))
.arg(arg!(-e --env ...).takes_value(true).help("Set of environment variables to set for the command"))
.subcommand(
clap::Command::new("watch")
.about("Trigger that fires when a file or directory changes.")
.arg(
arg!(-f --file ...).name("file")
.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!(--poll).name("poll").takes_value(true).value_parser(value_parser!(humantime::Duration)).help("Duration of time between polls")))
.subcommand(
clap::Command::new("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!(-n --repeat).value_parser(value_parser!(u32)).help("Number of times to run before finishing")))
.subcommand(
clap::Command::new("success")
.about("Run a command when a test command succeeds")
.arg(arg!(--if).value_parser(value_parser!(String)).help("The command to run and check for success on"))
.arg(arg!(--not).help("Negate the success of the command"))
.arg(arg!(--poll).value_parser(value_parser!(humantime::Duration)).help("Duration of time between poll")))
.get_matches()
}
fn main() {
@ -79,12 +76,12 @@ fn main() {
if let Some(env_values) = app.values_of("env") {
let mut env_vec = Vec::new();
for v in env_values {
env_vec.push(v);
env_vec.push(v.to_string());
}
maybe_env = Some(env_vec);
}
let mut process: Option<Box<dyn Process>> = None;
if let Some(matches) = app.subcommand_matches("watch") {
let mut proc: Box<dyn Process> = if let Some(matches) = app.subcommand_matches("watch") {
let file = match matches.values_of("file") {
Some(v) => v.collect(),
// The default is our current directory
@ -94,66 +91,44 @@ fn main() {
if matches.is_present("filetouch") {
method = WatchEventType::Touched;
}
let poll = matches.value_of("poll").unwrap_or("5s");
let dur = humantime::parse_duration(poll).expect("Invalid poll value.");
process = Some(Box::new(FileProcess::new(
cmd, maybe_env, file, method, dur,
)));
let duration = match matches.get_one::<humantime::Duration>("poll") {
Some(d) => Some((*d).into()),
None => None,
};
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") {
// TODO(jwall): This should use cancelable commands.
// Unwrap because this flag is required.
let dur = humantime::parse_duration(
matches
.value_of("duration")
.expect("duration flag is required"),
);
match dur {
Ok(duration) => {
let max_repeat = if let Some(val) = matches.value_of("repeat") {
match u32::from_str(val) {
Ok(n) => Some(n),
Err(e) => {
println!("Invalid --repeat value {}", e);
println!("{}", matches.usage());
process::exit(1)
}
}
} else {
None
};
process = Some(Box::new(TimerProcess::new(
cmd, maybe_env, duration, max_repeat,
)));
}
Err(msg) => {
println!("Malformed duration {:?}", msg);
process::exit(1);
}
}
let duration = matches
.get_one::<humantime::Duration>("duration")
.expect("duration flag is required")
.clone();
let max_repeat = matches.get_one::<u32>("repeat").cloned();
Box::new(TimerProcess::new(cmd, maybe_env, *duration, max_repeat))
} else if let Some(matches) = app.subcommand_matches("success") {
// unwrap because this is required.
let ifcmd = matches.value_of("ifcmd").expect("ifcmd flag is required");
let negate = matches.is_present("not");
let dur = humantime::parse_duration(matches.value_of("poll").unwrap_or("5s"));
process = match dur {
Ok(duration) => Some(Box::new(ExecProcess::new(
ifcmd, cmd, negate, maybe_env, duration,
))),
Err(msg) => {
println!("Malformed poll {:?}", msg);
process::exit(1)
}
}
}
match process {
Some(process) => match process.run() {
Ok(_) => return,
Err(err) => {
println!("{0}", err);
process::exit(1)
}
},
None => {
println!("You must specify a subcommand.");
let duration = *matches
.get_one::<humantime::Duration>("poll")
.cloned()
.unwrap_or(humantime::Duration::from_str("5s").unwrap());
Box::new(ExecProcess::new(ifcmd, cmd, negate, maybe_env, duration))
} else {
println!("You must specify a subcommand.");
process::exit(1)
};
match proc.run() {
Ok(_) => return,
Err(err) => {
println!("{0}", err);
process::exit(1)
}
}

View File

@ -14,41 +14,40 @@
use std::thread;
use std::time::Duration;
use exec::CancelableProcess;
use error::CommandError;
use exec::run_cmd;
use traits::Process;
pub struct TimerProcess<'a> {
cmd: &'a str,
env: Option<Vec<&'a str>>,
pub struct TimerProcess {
cmd: CancelableProcess,
poll_duration: Duration,
max_repeat: Option<u32>,
}
impl<'a> TimerProcess<'a> {
impl TimerProcess {
pub fn new(
cmd: &'a str,
env: Option<Vec<&'a str>>,
cmd: &str,
env: Option<Vec<String>>,
poll_duration: Duration,
max_repeat: Option<u32>,
) -> TimerProcess<'a> {
) -> TimerProcess {
let cmd = CancelableProcess::new(cmd, env);
TimerProcess {
cmd: cmd,
env: env,
poll_duration: poll_duration,
max_repeat: max_repeat,
cmd,
poll_duration,
max_repeat,
}
}
}
impl<'a> Process for TimerProcess<'a> {
fn run(&self) -> Result<(), CommandError> {
impl Process for TimerProcess {
fn run(&mut self) -> Result<(), CommandError> {
let mut counter = 0;
loop {
if self.max_repeat.is_some() && counter >= self.max_repeat.unwrap() {
return Ok(());
}
if let Err(err) = run_cmd(self.cmd, &self.env) {
if let Err(err) = self.cmd.block() {
println!("{:?}", err)
}
thread::sleep(self.poll_duration);

View File

@ -14,5 +14,5 @@
use error::CommandError;
pub trait Process {
fn run(&self) -> Result<(), CommandError>;
fn run(&mut self) -> Result<(), CommandError>;
}