Add the ability to specify environment variables.

This commit is contained in:
Jeremy Wall 2017-04-01 15:02:48 -05:00
parent 3991f03161
commit 07120d141f
7 changed files with 95 additions and 25 deletions

View File

@ -1,6 +1,6 @@
[package] [package]
name = "runwhen" name = "runwhen"
version = "0.0.1" version = "0.0.2"
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"
@ -12,4 +12,3 @@ license = "Apache-2.0"
clap = "~2.19.0" clap = "~2.19.0"
humantime = "~1.0.0" humantime = "~1.0.0"
notify = "~3.0.0" notify = "~3.0.0"
subprocess = "~0.1.7"

View File

@ -6,14 +6,15 @@
Runs a command on user defined triggers. Runs a command on user defined triggers.
USAGE: USAGE:
runwhen --cmd <cmd> [SUBCOMMAND] runwhen [OPTIONS] --cmd <cmd> [SUBCOMMAND]
FLAGS: FLAGS:
-h, --help Prints help information -h, --help Prints help information
-V, --version Prints version information -V, --version Prints version information
OPTIONS: OPTIONS:
-c, --cmd <cmd> Command to run on supplied triggers -c, --cmd <cmd> Command to run on supplied triggers
-e, --env <env>... Command to run on supplied triggers
SUBCOMMANDS: SUBCOMMANDS:
help Prints this message or the help of the given subcommand(s) help Prints this message or the help of the given subcommand(s)

View File

@ -22,8 +22,8 @@ pub struct CommandError {
} }
impl CommandError { impl CommandError {
pub fn new(msg: String) -> CommandError { pub fn new<S: Into<String>>(msg: S) -> CommandError {
CommandError { msg: msg } CommandError { msg: msg.into() }
} }
} }

View File

@ -14,19 +14,53 @@
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use subprocess::{Exec, PopenError, ExitStatus}; use std::process::{Command, Stdio};
use traits::Process; use traits::Process;
use error::CommandError; use error::CommandError;
pub fn run_cmd(cmd: &str) -> Result<(), PopenError> {
Exec::shell(cmd).join()?; fn env_var_to_tuple(var: &str) -> (String, String) {
Ok(()) let mut vs = var.split('=');
if let Some(name) = vs.next() {
return match vs.next() {
Some(val) => (String::from(name), String::from(val)),
None => (String::from(name), "".to_string()),
}
}
("".to_string(), "".to_string())
} }
fn is_cmd_success(cmd: &str) -> bool { pub fn run_cmd(cmd: &str, env: &Option<Vec<&str>>) -> Result<i32, CommandError> {
match Exec::shell(cmd).join() { let args = cmd.split(' ').filter(|s| !s.is_empty()).collect::<Vec<&str>>();
Ok(ExitStatus::Exited(code)) => code == 0, 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 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, _ => false,
} }
} }
@ -34,14 +68,19 @@ fn is_cmd_success(cmd: &str) -> bool {
pub struct ExecProcess<'a> { pub struct ExecProcess<'a> {
test_cmd: &'a str, test_cmd: &'a str,
cmd: &'a str, cmd: &'a str,
env: Option<Vec<&'a str>>,
poll: Duration, poll: Duration,
} }
impl<'a> ExecProcess<'a> { impl<'a> ExecProcess<'a> {
pub fn new(test_cmd: &'a str, cmd: &'a str, poll: Duration) -> ExecProcess<'a> { pub fn new(test_cmd: &'a str,
cmd: &'a str,
env: Option<Vec<&'a str>>,
poll: Duration) -> ExecProcess<'a> {
ExecProcess { ExecProcess {
test_cmd: test_cmd, test_cmd: test_cmd,
cmd: cmd, cmd: cmd,
env: env,
poll: poll, poll: poll,
} }
} }
@ -50,8 +89,9 @@ impl<'a> ExecProcess<'a> {
impl<'a> Process for ExecProcess<'a> { impl<'a> Process for ExecProcess<'a> {
fn run(&self) -> Result<(), CommandError> { fn run(&self) -> Result<(), CommandError> {
loop { loop {
if is_cmd_success(self.test_cmd) { // TODO(jwall): Should we set the environment the same as the other command?
if let Err(err) = run_cmd(self.cmd) { if is_cmd_success(self.test_cmd, None) {
if let Err(err) = run_cmd(self.cmd, &self.env) {
println!("{:?}", err) println!("{:?}", err)
} }
} }

View File

@ -26,6 +26,7 @@ use exec::run_cmd;
pub struct FileProcess<'a> { pub struct FileProcess<'a> {
cmd: &'a str, cmd: &'a str,
env: Option<Vec<&'a str>>,
file: &'a str, file: &'a str,
method: WatchEventType, method: WatchEventType,
poll: Duration, poll: Duration,
@ -33,12 +34,14 @@ pub struct FileProcess<'a> {
impl<'a> FileProcess<'a> { impl<'a> FileProcess<'a> {
pub fn new(cmd: &'a str, pub fn new(cmd: &'a str,
env: Option<Vec<&'a str>>,
file: &'a str, file: &'a str,
method: WatchEventType, method: WatchEventType,
poll: Duration) poll: Duration)
-> FileProcess<'a> { -> FileProcess<'a> {
FileProcess { FileProcess {
cmd: cmd, cmd: cmd,
env: env,
file: file, file: file,
method: method, method: method,
poll: poll, poll: poll,
@ -46,8 +49,20 @@ impl<'a> FileProcess<'a> {
} }
} }
fn spawn_runner_thread(lock: Arc<Mutex<bool>>, cmd: String, poll: Duration) { fn spawn_runner_thread(lock: Arc<Mutex<bool>>, cmd: String,
env: Option<Vec<&str>>, poll: Duration) {
let copied_env = env.and_then(|v| Some(v.iter().cloned().map(|s| String::from(s)).collect::<Vec<String>>()));
thread::spawn(move || { 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,
};
loop { loop {
// Wait our requisit number of seconds // Wait our requisit number of seconds
thread::sleep(poll); thread::sleep(poll);
@ -59,7 +74,7 @@ fn spawn_runner_thread(lock: Arc<Mutex<bool>>, cmd: String, poll: Duration) {
*signal = false; *signal = false;
// Run our command! // Run our command!
println!("exec: {}", cmd); println!("exec: {}", cmd);
if let Err(err) = run_cmd(&cmd) { if let Err(err) = run_cmd(&cmd, &copied_env_refs) {
println!("{:?}", err) println!("{:?}", err)
} }
}, },
@ -123,7 +138,7 @@ impl<'a> Process for FileProcess<'a> {
// TODO(jeremy): Is this sufficent or do we want to ignore // TODO(jeremy): Is this sufficent or do we want to ignore
// any events that come in while the command is running? // any events that come in while the command is running?
let lock = Arc::new(Mutex::new(false)); let lock = Arc::new(Mutex::new(false));
spawn_runner_thread(lock.clone(), self.cmd.to_string(), self.poll); spawn_runner_thread(lock.clone(), self.cmd.to_string(), self.env.clone(), self.poll);
wait_for_fs_events(lock, self.method.clone(), self.file) wait_for_fs_events(lock, self.method.clone(), self.file)
} }
} }

View File

@ -16,7 +16,6 @@
extern crate clap; extern crate clap;
extern crate humantime; extern crate humantime;
extern crate notify; extern crate notify;
extern crate subprocess;
use std::process; use std::process;
use std::str::FromStr; use std::str::FromStr;
@ -41,6 +40,7 @@ fn do_flags<'a>() -> clap::ArgMatches<'a> {
(author: crate_authors!()) (author: crate_authors!())
(about: "Runs a command on user defined triggers.") (about: "Runs a command on user defined triggers.")
(@arg cmd: -c --cmd +required +takes_value "Command to run on supplied triggers") (@arg cmd: -c --cmd +required +takes_value "Command to run on supplied triggers")
(@arg env: -e --env +takes_value ... "Command to run on supplied triggers")
(@subcommand watch => (@subcommand watch =>
(about: "Trigger that fires when a file or directory changes.") (about: "Trigger that fires when a file or directory changes.")
// TODO(jeremy): We need to support filters // TODO(jeremy): We need to support filters
@ -73,6 +73,14 @@ fn main() {
let app = do_flags(); let app = do_flags();
// Unwrap because this flag is required. // Unwrap because this flag is required.
let cmd = app.value_of("cmd").expect("cmd flag is required"); let cmd = app.value_of("cmd").expect("cmd flag is required");
let mut maybe_env = None;
if let Some(env_values) = app.values_of("env") {
let mut env_vec = Vec::new();
for v in env_values {
env_vec.push(v);
}
maybe_env = Some(env_vec);
}
let mut process: Option<Box<Process>> = None; let mut process: Option<Box<Process>> = None;
if let Some(matches) = app.subcommand_matches("watch") { if let Some(matches) = app.subcommand_matches("watch") {
// Unwrap because this flag is required. // Unwrap because this flag is required.
@ -83,7 +91,8 @@ fn main() {
} }
let poll = matches.value_of("poll").unwrap_or("5s"); let poll = matches.value_of("poll").unwrap_or("5s");
let dur = humantime::parse_duration(poll).expect("Invalid poll value."); let dur = humantime::parse_duration(poll).expect("Invalid poll value.");
process = Some(Box::new(FileProcess::new(cmd, file, method, dur))); process = Some(Box::new(FileProcess::new(
cmd, maybe_env, file, method, dur)));
} else if let Some(matches) = app.subcommand_matches("timer") { } else if let Some(matches) = app.subcommand_matches("timer") {
// Unwrap because this flag is required. // Unwrap because this flag is required.
let dur = humantime::parse_duration(matches.value_of("duration") let dur = humantime::parse_duration(matches.value_of("duration")
@ -102,7 +111,8 @@ fn main() {
} else { } else {
None None
}; };
process = Some(Box::new(TimerProcess::new(cmd, duration, max_repeat))); process = Some(Box::new(TimerProcess::new(
cmd, maybe_env, duration, max_repeat)));
} }
Err(msg) => { Err(msg) => {
println!("Malformed duration {:?}", msg); println!("Malformed duration {:?}", msg);
@ -114,7 +124,7 @@ fn main() {
let ifcmd = matches.value_of("ifcmd").expect("ifcmd flag is required"); let ifcmd = matches.value_of("ifcmd").expect("ifcmd flag is required");
let dur = humantime::parse_duration(matches.value_of("poll").unwrap_or("5s")); let dur = humantime::parse_duration(matches.value_of("poll").unwrap_or("5s"));
process = match dur { process = match dur {
Ok(duration) => Some(Box::new(ExecProcess::new(ifcmd, cmd, duration))), Ok(duration) => Some(Box::new(ExecProcess::new(ifcmd, cmd, maybe_env, duration))),
Err(msg) => { Err(msg) => {
println!("Malformed poll {:?}", msg); println!("Malformed poll {:?}", msg);
process::exit(1) process::exit(1)

View File

@ -20,14 +20,19 @@ use exec::run_cmd;
pub struct TimerProcess<'a> { pub struct TimerProcess<'a> {
cmd: &'a str, cmd: &'a str,
env: Option<Vec<&'a str>>,
poll_duration: Duration, poll_duration: Duration,
max_repeat: Option<u32>, max_repeat: Option<u32>,
} }
impl<'a> TimerProcess<'a> { impl<'a> TimerProcess<'a> {
pub fn new(cmd: &'a str, poll_duration: Duration, max_repeat: Option<u32>) -> TimerProcess<'a> { pub fn new(cmd: &'a str,
env: Option<Vec<&'a str>>,
poll_duration: Duration,
max_repeat: Option<u32>) -> TimerProcess<'a> {
TimerProcess { TimerProcess {
cmd: cmd, cmd: cmd,
env: env,
poll_duration: poll_duration, poll_duration: poll_duration,
max_repeat: max_repeat, max_repeat: max_repeat,
} }
@ -41,7 +46,7 @@ impl<'a> Process for TimerProcess<'a> {
if self.max_repeat.is_some() && counter >= self.max_repeat.unwrap() { if self.max_repeat.is_some() && counter >= self.max_repeat.unwrap() {
return Ok(()); return Ok(());
} }
if let Err(err) = run_cmd(self.cmd) { if let Err(err) = run_cmd(self.cmd, &self.env) {
println!("{:?}", err) println!("{:?}", err)
} }
thread::sleep(self.poll_duration); thread::sleep(self.poll_duration);