commit 4513694f255002651ceb2ef752820c6bc6c2de67 Author: Jeremy Wall Date: Sun Jan 29 16:39:48 2017 -0600 Initial Commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a78405 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +target +Cargo.lock +target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c7d3645 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "runwhen" +version = "0.1.0" +authors = ["Jeremy Wall "] +description = "Runs a command on user specified triggers." + +[dependencies] +clap = "~2.19.0" +humantime = "~1.0.0" +notify = "~3.0.0" +md5 = "~0.3.2" +subprocess = "~0.1.7" \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f7e8937 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# runwhen - A utility that executes commands on user defined triggers. + +## Usage + +``` +Runs a command on user defined triggers. + +USAGE: + runwhen --cmd [SUBCOMMAND] + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + +OPTIONS: + -c, --cmd Command to run on supplied triggers + +SUBCOMMANDS: + help Prints this message or the help of the given subcommand(s) + success Trigger that fires if a command runs successful. + timer Trigger that fires on a timer. + watch Trigger that fires when a file or directory changes. +``` + +## Description + +I wanted a project to learn Rust on and this one scratches an itch I've had for +a while. runwhen executes a command on a user specified trigger. There are other +utilities out there that will execute on a timer or when a file changes but I +haven't seen any that bundled all the types of triggers into one utility. diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..0f3f743 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,42 @@ +// Copyright 2017 Jeremy Wall +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// 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::error::Error; +use std::fmt; + +use notify; + +#[derive(Debug)] +pub struct CommandError { + msg: String, +} + +impl CommandError { + pub fn new(msg: String) -> CommandError { + CommandError{ + msg: msg + } + } +} + +impl fmt::Display for CommandError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self.msg) + } +} + +impl From for CommandError { + fn from(e: notify::Error) -> CommandError { + CommandError::new(e.description().to_string()) + } +} diff --git a/src/events.rs b/src/events.rs new file mode 100644 index 0000000..3988344 --- /dev/null +++ b/src/events.rs @@ -0,0 +1,39 @@ +// Copyright 2017 Jeremy Wall +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// 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 notify::DebouncedEvent; + +#[derive(PartialEq,Clone)] +pub enum WatchEventType { + Touched, + Changed, + Error, + Ignore +} + +impl From for WatchEventType { + fn from(e: DebouncedEvent) -> WatchEventType { + println!("Found event: {:?}", e); + match e { + DebouncedEvent::Chmod(_) => WatchEventType::Touched, + DebouncedEvent::Create(_) => WatchEventType::Touched, + DebouncedEvent::Remove(_) => WatchEventType::Changed, + DebouncedEvent::Rename(_, _) => WatchEventType::Changed, + DebouncedEvent::Write(_) => WatchEventType::Changed, + DebouncedEvent::NoticeRemove(_) => WatchEventType::Ignore, + DebouncedEvent::NoticeWrite(_) => WatchEventType::Ignore, + DebouncedEvent::Rescan => WatchEventType::Ignore, + DebouncedEvent::Error(_, _) => WatchEventType::Ignore + } + } +} diff --git a/src/exec.rs b/src/exec.rs new file mode 100644 index 0000000..442f7f1 --- /dev/null +++ b/src/exec.rs @@ -0,0 +1,57 @@ +// Copyright 2017 Jeremy Wall +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// 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::thread; +use std::time::Duration; + +use subprocess::{Exec,PopenError,ExitStatus}; + +use traits::Process; +use error::CommandError; + +pub fn run_cmd(cmd: &str) -> Result<(), PopenError> { + Exec::shell(cmd).join()?; + Ok(()) +} + +fn is_cmd_success(cmd: &str) -> bool { + match Exec::shell(cmd).join() { + Ok(ExitStatus::Exited(code)) => code == 0, + _ => false, + } +} + +pub struct ExecProcess<'a> { + test_cmd: &'a str, + cmd: &'a str, + poll: Duration, +} + +impl<'a> ExecProcess<'a> { + pub fn new(test_cmd: &'a str, cmd: &'a str, poll: Duration) -> ExecProcess<'a> { + ExecProcess{test_cmd: test_cmd, cmd: cmd, poll: poll} + } +} + +impl<'a> Process for ExecProcess<'a> { + fn run(&self) -> Result<(), CommandError> { + loop { + if is_cmd_success(self.test_cmd) { + if let Err(err) = run_cmd(self.cmd) { + println!("{:?}", err) + } + } + thread::sleep(self.poll); + } + } +} diff --git a/src/file.rs b/src/file.rs new file mode 100644 index 0000000..860fb8f --- /dev/null +++ b/src/file.rs @@ -0,0 +1,116 @@ +// Copyright 2017 Jeremy Wall +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// 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::thread; +use std::sync::{Arc,RwLock}; +use std::time::Duration; +use std::path::Path; +use std::sync::mpsc::channel; + +use notify::{Watcher,RecursiveMode,watcher}; + +use traits::Process; +use error::CommandError; +use events::WatchEventType; +use exec::run_cmd; + +pub struct FileProcess<'a> { + cmd: &'a str, + file: &'a str, + method: WatchEventType, + poll: Duration, +} + +impl<'a> FileProcess<'a> { + pub fn new(cmd: &'a str, file: &'a str, method: WatchEventType, poll: Duration) -> FileProcess<'a> { + FileProcess{ cmd: cmd, file: file, method: method, poll: poll} + } +} + +fn spawn_runner_thread(lock: Arc>, cmd: String, poll: Duration) { + thread::spawn(move || { + loop { + // Wait our requisit number of seconds + thread::sleep(poll); + // Default to not running the command. + let should_run = { // get a new scope for our read lock. + // Check our evt drop off. + *(lock.read().unwrap()) + }; // drop our read lock + if should_run { + { // Set a new scope for our write lock. + let mut signal = lock.write().unwrap(); + // set signal to false so we won't trigger on the + // next loop iteration unless we recieved more events. + *signal = false; + // Run our command! + if let Err(err) = run_cmd(&cmd) { + println!("{:?}", err) + } + } // drop our write lock. + } + } + }); +} + +fn wait_for_fs_events(lock: Arc>, method: WatchEventType, file: &str) -> Result<(), CommandError> { + // Notify requires a channel for communication. + let (tx, rx) = channel(); + let mut watcher = try!(watcher(tx, Duration::from_secs(1))); + // TODO(jwall): Better error handling. + try!(watcher.watch(file, RecursiveMode::Recursive)); + println!("Watching {:?}", file); + loop { + let evt: WatchEventType = match rx.recv() { + Ok(event) => { + WatchEventType::from(event) + }, + Err(_) => { + WatchEventType::Error + } + }; + match evt { + WatchEventType::Ignore => { + // We ignore this one. + }, + WatchEventType::Error => { + // We log this one. + }, + WatchEventType::Touched => { + if method == WatchEventType::Touched { + let mut signal = lock.write().unwrap(); + *signal = true; + } + }, + WatchEventType::Changed => { + let mut signal = lock.write().unwrap(); + *signal = true; + } + } + } +} + +impl<'a> Process for FileProcess<'a> { + fn run(&self) -> Result<(), CommandError> { + // NOTE(jwall): this is necessary because notify::fsEventWatcher panics + // if the path doesn't exist. :-( + if !Path::new(self.file).exists() { + return Err(CommandError::new(format!("No such path! {0}", self.file).to_string())) + } + // 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(RwLock::new(false)); + spawn_runner_thread(lock.clone(), self.cmd.to_string(), self.poll); + wait_for_fs_events(lock, self.method.clone(), self.file) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..1a8435c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,129 @@ +// Copyright 2017 Jeremy Wall +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// 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. +#[macro_use] +extern crate clap; +extern crate humantime; +extern crate md5; +extern crate notify; +extern crate subprocess; + +use std::process; +use std::str::FromStr; + +mod traits; +mod file; +mod timer; +mod error; +mod events; +mod exec; + +use traits::Process; +use file::FileProcess; +use timer::TimerProcess; +use exec::ExecProcess; +use events::WatchEventType; + +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") + (@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 wait: --poll +takes_value "How frequently to test command (default 5s)") + ) + ).get_matches() +} + +fn main() { + let app = do_flags(); + // Unwrap because this flag is required. + let cmd = app.value_of("cmd").unwrap(); + let mut process: Option> = None; + if let Some(matches) = app.subcommand_matches("watch") { + // Unwrap because this flag is required. + let file = matches.value_of("file").unwrap_or("."); + let mut method = WatchEventType::Changed; + if matches.is_present("filetouch") { + method = WatchEventType::Touched; + } + let poll = matches.value_of("poll").unwrap_or("5s"); + let dur = humantime::parse_duration(poll).unwrap(); + process = Some(Box::new(FileProcess::new(cmd, file, method, dur))); + } else if let Some(matches) = app.subcommand_matches("timer") { + // Unwrap because this flag is required. + let dur = humantime::parse_duration(matches.value_of("duration").unwrap()); + 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, duration, max_repeat))); + }, + Err(msg) => { + println!("Malformed duration {:?}", msg); + process::exit(1); + } + } + } else if let Some(matches) = app.subcommand_matches("success") { + // unwrap because this is required. + let ifcmd = matches.value_of("ifcmd").unwrap(); + let dur = humantime::parse_duration( + matches.value_of("poll").unwrap_or("5s")); + process = match dur { + Ok(duration) => Some(Box::new(ExecProcess::new(ifcmd, cmd, 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."); + process::exit(1) + }, + } +} diff --git a/src/timer.rs b/src/timer.rs new file mode 100644 index 0000000..c1c4599 --- /dev/null +++ b/src/timer.rs @@ -0,0 +1,47 @@ +// Copyright 2017 Jeremy Wall +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// 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::thread; +use std::time::Duration; + +use traits::Process; +use error::CommandError; +use exec::run_cmd; + +pub struct TimerProcess<'a> { + cmd: &'a str, + poll_duration: Duration, + max_repeat: Option +} + +impl<'a> TimerProcess<'a> { + pub fn new(cmd: &'a str, poll_duration: Duration, max_repeat: Option) -> TimerProcess<'a> { + TimerProcess{cmd: cmd, poll_duration: poll_duration, max_repeat: max_repeat} + } +} + +impl<'a> Process for TimerProcess<'a> { + fn run(&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) { + println!("{:?}", err) + } + thread::sleep(self.poll_duration); + if self.max_repeat.is_some() { counter += 1 } + } + } +} diff --git a/src/traits.rs b/src/traits.rs new file mode 100644 index 0000000..842203b --- /dev/null +++ b/src/traits.rs @@ -0,0 +1,18 @@ +// Copyright 2017 Jeremy Wall +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// 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 error::CommandError; + +pub trait Process { + fn run(&self) -> Result<(), CommandError>; +}