diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..eb856df Binary files /dev/null and b/.DS_Store differ diff --git a/result b/result new file mode 120000 index 0000000..cbbdaa1 --- /dev/null +++ b/result @@ -0,0 +1 @@ +/nix/store/wllb7d3wx2wh2p4h8vj7y10p31f37jd8-runwhen-0.0.4 \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index ee70488..88af249 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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 for CommandError { CommandError::new(format!("{}", e)) } } + +impl From for CommandError { + fn from(e: io::Error) -> CommandError { + CommandError::new(format!("IO: {}", e)) + } +} diff --git a/src/exec.rs b/src/exec.rs index 6957c84..624f256 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -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,147 @@ fn env_var_to_tuple(var: &str) -> (String, String) { ("".to_string(), "".to_string()) } -pub fn run_cmd(cmd: &str, env: &Option>) -> Result { - let args = cmd - .split(' ') - .filter(|s| !s.is_empty()) - .collect::>(); - 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>, + exec: Option, + handle: Option, +} + +impl CancelableProcess { + pub fn new(cmd: &str, env: Option>) -> 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>) -> bool { - match run_cmd(cmd, &env) { - Ok(code) => code == 0, - _ => false, + fn create_command(cmd: &str, env: &Option>) -> Result { + let args = cmd + .split(' ') + .filter(|s| !s.is_empty()) + .collect::>(); + 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 { + 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, + } + } + + pub fn check(&mut self) -> Result, CommandError> { + Ok(match self.handle { + 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 { + 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>, + 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>, + env: Option>, 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); } } diff --git a/src/file.rs b/src/file.rs index 5adc38e..0f071e0 100644 --- a/src/file.rs +++ b/src/file.rs @@ -21,12 +21,12 @@ 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>, + env: Option>, files: Vec<&'a str>, method: WatchEventType, poll: Duration, @@ -35,17 +35,17 @@ pub struct FileProcess<'a> { impl<'a> FileProcess<'a> { pub fn new( cmd: &'a str, - env: Option>, + env: Option>, file: Vec<&'a str>, method: WatchEventType, poll: Duration, ) -> FileProcess<'a> { FileProcess { - cmd: cmd, - env: env, + cmd, + env, + method, + poll, files: file, - method: method, - poll: poll, } } } @@ -53,7 +53,7 @@ impl<'a> FileProcess<'a> { fn spawn_runner_thread( lock: Arc>, cmd: String, - env: Option>, + env: Option>, poll: Duration, ) { let copied_env = env.and_then(|v| { @@ -65,42 +65,46 @@ fn spawn_runner_thread( ) }); thread::spawn(move || { - let copied_env_refs: Option> = 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); + exec.spawn().expect("Failed to start command"); 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; - } + if !run_loop_step(lock.clone(), &mut exec) { + exec.reset().expect("Failed to start command"); } } }); } +fn run_loop_step(lock: Arc>, exec: &mut CancelableProcess) -> bool { + match lock.lock() { + Ok(mut signal) => { + // We always want to check on our process each iteration of the loop. + if let Err(err) = exec.check() { + println!("{:?}", err); + return false; + } + if *signal { + // set signal to false so we won't trigger on the + // next loop iteration unless we recieved more events. + *signal = false; + // On a true signal we want to start or restart our process. + if let Err(err) = exec.reset() { + println!("{:?}", err); + return false; + } + } + return true; + } + Err(err) => { + println!("Unexpected error; {}", err); + return false; + } + } +} + fn wait_for_fs_events( lock: Arc>, method: WatchEventType, @@ -153,7 +157,7 @@ fn wait_for_fs_events( } 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)); diff --git a/src/main.rs b/src/main.rs index 9f3b3f0..b96cf4c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,12 +72,11 @@ 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> = None; - if let Some(matches) = app.subcommand_matches("watch") { + let mut proc: Box = 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 @@ -91,19 +90,16 @@ fn main() { .get_one::("poll") .cloned() .unwrap_or(humantime::Duration::from_str("5s").unwrap()); - process = Some(Box::new(FileProcess::new( - cmd, maybe_env, file, method, duration, - ))); + Box::new(FileProcess::new(cmd, maybe_env, file, 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 duration = matches .get_one::("duration") .expect("duration flag is required") .clone(); let max_repeat = matches.get_one::("repeat").cloned(); - process = Some(Box::new(TimerProcess::new( - cmd, maybe_env, *duration, max_repeat, - ))); + 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"); @@ -112,20 +108,15 @@ fn main() { .get_one::("poll") .cloned() .unwrap_or(humantime::Duration::from_str("5s").unwrap()); - Some(Box::new(ExecProcess::new( - ifcmd, cmd, negate, maybe_env, duration, - ))); - } - match process { - Some(process) => match process.run() { - Ok(_) => return, - Err(err) => { - println!("{0}", err); - process::exit(1) - } - }, - None => { - println!("You must specify a subcommand."); + 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) } } diff --git a/src/timer.rs b/src/timer.rs index 8fcec69..0dd634e 100644 --- a/src/timer.rs +++ b/src/timer.rs @@ -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>, +pub struct TimerProcess { + cmd: CancelableProcess, poll_duration: Duration, max_repeat: Option, } -impl<'a> TimerProcess<'a> { +impl TimerProcess { pub fn new( - cmd: &'a str, - env: Option>, + cmd: &str, + env: Option>, poll_duration: Duration, max_repeat: Option, - ) -> 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); diff --git a/src/traits.rs b/src/traits.rs index 842203b..bec6be7 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -14,5 +14,5 @@ use error::CommandError; pub trait Process { - fn run(&self) -> Result<(), CommandError>; + fn run(&mut self) -> Result<(), CommandError>; }