├── .gitignore ├── .travis.yml ├── Cargo.toml ├── README.md ├── appveyor.yml ├── bors.toml ├── src ├── lib.rs ├── macros.rs ├── process.rs └── signal.rs └── tests └── all.rs /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /target/ 3 | **/*.rs.bk 4 | Cargo.lock 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | cache: cargo 3 | rust: 4 | - stable 5 | - beta 6 | 7 | branches: 8 | only: 9 | # Enable building pull requests. 10 | #- master 11 | # This is where pull requests from "bors r+" are built. 12 | - staging 13 | # This is where pull requests from "bors try" are built. 14 | - trying 15 | 16 | script: 17 | - cargo test -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Tim Ryan "] 3 | description = "Rust macro to build std::process::Command objects with shell syntax. Uses macro_rules! and works on stable." 4 | license = "MIT OR Apache-2.0" 5 | name = "commandspec" 6 | repository = "http://github.com/tcr/commandspec" 7 | version = "0.12.2" 8 | 9 | [dependencies] 10 | failure = "0.1.1" 11 | shlex = "0.1.1" 12 | lazy_static = "1.0.0" 13 | log = "0.4.4" 14 | 15 | [target.'cfg(unix)'.dependencies] 16 | nix = "0.11.0" 17 | 18 | [target.'cfg(windows)'.dependencies] 19 | winapi = "0.2.8" 20 | kernel32-sys = "0.2.2" 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # commandspec 2 | 3 | Simple Rust macro for building `std::process::Command` objects. Uses macro_rules! and works on stable. 4 | 5 | ```toml 6 | [dependencies] 7 | commandspec = "0.10" 8 | ``` 9 | 10 | Then: 11 | 12 | ```rust 13 | #[macro_use] 14 | extern crate commandspec; 15 | 16 | use commandspec::CommandSpec; // .execute() method on Command 17 | use std::process::Command; 18 | 19 | let result = execute!( 20 | r" 21 | cd path/location 22 | export RUST_LOG=full 23 | export RUST_BACKTRACE=1 24 | cargo run {release_flag} --bin {bin_name} -- {args} 25 | ", 26 | release_flag=Some("--release"), 27 | bin_name="binary", 28 | args=vec!["arg1", "arg2"], 29 | )?; 30 | // result = Ok(()) on success (error code 0), Err(CommandError) for all else 31 | ``` 32 | 33 | Format of the commandspec input, in order: 34 | 35 | * (optional) `cd ` to set the current working directory of the command, where path can be a literal, a quoted string, or format variable. 36 | * (optional) one or more `export =` lines to set environment variables, with the same formatting options. 37 | * Last, a command you want to invoke, optionally with format arguments. 38 | 39 | ### Features: 40 | 41 | * format-like invocation makes it easy to interpolate variables, with automatic quoting 42 | * Equivalent syntax to shell when prototyping 43 | * Works on stable Rust. 44 | 45 | ## License 46 | 47 | MIT or Apache-2.0, at your option. 48 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Appveyor configuration template for Rust using rustup for Rust installation 2 | # https://github.com/starkat99/appveyor-rust 3 | 4 | ## Operating System (VM environment) ## 5 | 6 | # Rust needs at least Visual Studio 2013 Appveyor OS for MSVC targets. 7 | os: Visual Studio 2015 8 | 9 | branches: 10 | only: 11 | # Enable building pull requests. 12 | #- master 13 | # This is where pull requests from "bors r+" are built. 14 | - staging 15 | # This is where pull requests from "bors try" are built. 16 | - trying 17 | 18 | ## Build Matrix ## 19 | 20 | # This configuration will setup a build for each channel & target combination (12 windows 21 | # combinations in all). 22 | # 23 | # There are 3 channels: stable, beta, and nightly. 24 | # 25 | # Alternatively, the full version may be specified for the channel to build using that specific 26 | # version (e.g. channel: 1.5.0) 27 | # 28 | # The values for target are the set of windows Rust build targets. Each value is of the form 29 | # 30 | # ARCH-pc-windows-TOOLCHAIN 31 | # 32 | # Where ARCH is the target architecture, either x86_64 or i686, and TOOLCHAIN is the linker 33 | # toolchain to use, either msvc or gnu. See https://www.rust-lang.org/downloads.html#win-foot for 34 | # a description of the toolchain differences. 35 | # See https://github.com/rust-lang-nursery/rustup.rs/#toolchain-specification for description of 36 | # toolchains and host triples. 37 | # 38 | # Comment out channel/target combos you do not wish to build in CI. 39 | # 40 | # You may use the `cargoflags` and `RUSTFLAGS` variables to set additional flags for cargo commands 41 | # and rustc, respectively. For instance, you can uncomment the cargoflags lines in the nightly 42 | # channels to enable unstable features when building for nightly. Or you could add additional 43 | # matrix entries to test different combinations of features. 44 | environment: 45 | matrix: 46 | 47 | ### MSVC Toolchains ### 48 | 49 | # Stable 64-bit MSVC 50 | - channel: stable 51 | target: x86_64-pc-windows-msvc 52 | # Stable 32-bit MSVC 53 | - channel: stable 54 | target: i686-pc-windows-msvc 55 | 56 | ## Install Script ## 57 | 58 | # This is the most important part of the Appveyor configuration. This installs the version of Rust 59 | # specified by the 'channel' and 'target' environment variables from the build matrix. This uses 60 | # rustup to install Rust. 61 | # 62 | # For simple configurations, instead of using the build matrix, you can simply set the 63 | # default-toolchain and default-host manually here. 64 | install: 65 | - appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe 66 | - rustup-init -yv --default-toolchain %channel% --default-host %target% 67 | - set PATH=%PATH%;%USERPROFILE%\.cargo\bin 68 | - rustc -vV 69 | - cargo -vV 70 | 71 | ## Build Script ## 72 | 73 | # 'cargo test' takes care of building for us, so disable Appveyor's build stage. This prevents 74 | # the "directory does not contain a project or solution file" error. 75 | build: false 76 | 77 | # Uses 'cargo test' to run tests and build. Alternatively, the project may call compiled programs 78 | #directly or perform other testing commands. Rust will automatically be placed in the PATH 79 | # environment variable. 80 | test_script: 81 | - cargo test --verbose %cargoflags% 82 | -------------------------------------------------------------------------------- /bors.toml: -------------------------------------------------------------------------------- 1 | status = [ 2 | "continuous-integration/travis-ci/push", 3 | "continuous-integration/appveyor/branch" 4 | ] 5 | # Uncomment this to use a two hour timeout. 6 | # The default is one hour. 7 | #timeout_sec = 7200 8 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate shlex; 2 | #[macro_use] 3 | extern crate failure; 4 | #[macro_use] 5 | extern crate lazy_static; 6 | #[macro_use] 7 | extern crate log; 8 | 9 | #[cfg(windows)] 10 | extern crate kernel32; 11 | #[cfg(unix)] 12 | extern crate nix; 13 | #[cfg(windows)] 14 | extern crate winapi; 15 | 16 | use std::process::Command; 17 | use std::fmt; 18 | use std::collections::HashMap; 19 | use std::sync::Arc; 20 | use std::sync::Mutex; 21 | use std::path::{Path, PathBuf}; 22 | use std::process::Stdio; 23 | 24 | // Re-export for macros. 25 | pub use failure::Error; 26 | 27 | pub mod macros; 28 | mod process; 29 | mod signal; 30 | 31 | use process::Process; 32 | use signal::Signal; 33 | 34 | lazy_static! { 35 | static ref PID_MAP: Arc>> = Arc::new(Mutex::new(HashMap::new())); 36 | } 37 | 38 | pub fn disable_cleanup_on_ctrlc() { 39 | signal::uninstall_handler(); 40 | } 41 | 42 | pub fn cleanup_on_ctrlc() { 43 | signal::install_handler(move |sig: Signal| { 44 | match sig { 45 | // SIGCHLD is special, initiate reap() 46 | Signal::SIGCHLD => { 47 | for (_pid, process) in PID_MAP.lock().unwrap().iter() { 48 | process.reap(); 49 | } 50 | } 51 | Signal::SIGINT => { 52 | for (_pid, process) in PID_MAP.lock().unwrap().iter() { 53 | process.signal(sig); 54 | } 55 | ::std::process::exit(130); 56 | } 57 | _ => { 58 | for (_pid, process) in PID_MAP.lock().unwrap().iter() { 59 | process.signal(sig); 60 | } 61 | } 62 | } 63 | }); 64 | } 65 | 66 | pub struct SpawnGuard(i32); 67 | 68 | impl ::std::ops::Drop for SpawnGuard { 69 | fn drop(&mut self) { 70 | PID_MAP.lock().unwrap().remove(&self.0).map(|process| process.reap()); 71 | } 72 | } 73 | 74 | //--------------- 75 | 76 | pub trait CommandSpecExt { 77 | fn execute(self) -> Result<(), CommandError>; 78 | 79 | fn scoped_spawn(self) -> Result; 80 | } 81 | 82 | #[derive(Debug, Fail)] 83 | pub enum CommandError { 84 | #[fail(display = "Encountered an IO error: {:?}", _0)] 85 | Io(#[cause] ::std::io::Error), 86 | 87 | #[fail(display = "Command was interrupted.")] 88 | Interrupt, 89 | 90 | #[fail(display = "Command failed with error code {}.", _0)] 91 | Code(i32), 92 | } 93 | 94 | impl CommandError { 95 | /// Returns the error code this command failed with. Can panic if not a `Code`. 96 | pub fn error_code(&self) -> i32 { 97 | if let CommandError::Code(value) = *self { 98 | value 99 | } else { 100 | panic!("Called error_code on a value that was not a CommandError::Code") 101 | } 102 | } 103 | } 104 | 105 | impl CommandSpecExt for Command { 106 | // Executes the command, and returns a comprehensive error type 107 | fn execute(mut self) -> Result<(), CommandError> { 108 | self.stdout(Stdio::inherit()); 109 | self.stderr(Stdio::inherit()); 110 | match self.output() { 111 | Ok(output) => { 112 | if output.status.success() { 113 | Ok(()) 114 | } else if let Some(code) = output.status.code() { 115 | Err(CommandError::Code(code)) 116 | } else { 117 | Err(CommandError::Interrupt) 118 | } 119 | }, 120 | Err(err) => Err(CommandError::Io(err)), 121 | } 122 | } 123 | 124 | fn scoped_spawn(self) -> Result { 125 | let process = Process::new(self)?; 126 | let id = process.id(); 127 | PID_MAP.lock().unwrap().insert(id, process); 128 | Ok(SpawnGuard(id)) 129 | } 130 | } 131 | 132 | //--------------- 133 | 134 | pub enum CommandArg { 135 | Empty, 136 | Literal(String), 137 | List(Vec), 138 | } 139 | 140 | fn shell_quote(value: &str) -> String { 141 | shlex::quote(&format!("{}", value)).to_string() 142 | } 143 | 144 | impl fmt::Display for CommandArg { 145 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 146 | use CommandArg::*; 147 | match *self { 148 | Empty => write!(f, ""), 149 | Literal(ref value) => { 150 | write!(f, "{}", shell_quote(&format!("{}", value))) 151 | }, 152 | List(ref list) => { 153 | write!(f, "{}", list 154 | .iter() 155 | .map(|x| shell_quote(&format!("{}", x)).to_string()) 156 | .collect::>() 157 | .join(" ")) 158 | } 159 | } 160 | } 161 | } 162 | 163 | impl<'a, 'b> From<&'a &'b str> for CommandArg { 164 | fn from(value: &&str) -> Self { 165 | CommandArg::Literal(value.to_string()) 166 | } 167 | } 168 | 169 | impl From for CommandArg { 170 | fn from(value: String) -> Self { 171 | CommandArg::Literal(value) 172 | } 173 | } 174 | 175 | impl<'a> From<&'a String> for CommandArg { 176 | fn from(value: &String) -> Self { 177 | CommandArg::Literal(value.to_string()) 178 | } 179 | } 180 | 181 | 182 | impl<'a> From<&'a str> for CommandArg { 183 | fn from(value: &str) -> Self { 184 | CommandArg::Literal(value.to_string()) 185 | } 186 | } 187 | 188 | impl<'a> From<&'a u64> for CommandArg { 189 | fn from(value: &u64) -> Self { 190 | CommandArg::Literal(value.to_string()) 191 | } 192 | } 193 | 194 | impl<'a> From<&'a f64> for CommandArg { 195 | fn from(value: &f64) -> Self { 196 | CommandArg::Literal(value.to_string()) 197 | } 198 | } 199 | 200 | impl<'a> From<&'a i32> for CommandArg { 201 | fn from(value: &i32) -> Self { 202 | CommandArg::Literal(value.to_string()) 203 | } 204 | } 205 | 206 | impl<'a> From<&'a i64> for CommandArg { 207 | fn from(value: &i64) -> Self { 208 | CommandArg::Literal(value.to_string()) 209 | } 210 | } 211 | 212 | impl<'a, T> From<&'a [T]> for CommandArg 213 | where T: fmt::Display { 214 | fn from(list: &[T]) -> Self { 215 | CommandArg::List( 216 | list 217 | .iter() 218 | .map(|x| format!("{}", x)) 219 | .collect() 220 | ) 221 | } 222 | } 223 | 224 | impl<'a, T> From<&'a Vec> for CommandArg 225 | where T: fmt::Display { 226 | fn from(list: &Vec) -> Self { 227 | CommandArg::from(list.as_slice()) 228 | } 229 | } 230 | 231 | impl<'a, T> From<&'a Option> for CommandArg 232 | where T: fmt::Display { 233 | fn from(opt: &Option) -> Self { 234 | if let Some(ref value) = *opt { 235 | CommandArg::Literal(format!("{}", value)) 236 | } else { 237 | CommandArg::Empty 238 | } 239 | } 240 | } 241 | 242 | pub fn command_arg<'a, T>(value: &'a T) -> CommandArg 243 | where CommandArg: std::convert::From<&'a T> { 244 | CommandArg::from(value) 245 | } 246 | 247 | //--------------- 248 | 249 | /// Represents the invocation specification used to generate a Command. 250 | #[derive(Debug)] 251 | struct CommandSpec { 252 | binary: String, 253 | args: Vec, 254 | env: HashMap, 255 | cd: Option, 256 | } 257 | 258 | impl CommandSpec { 259 | fn to_command(&self) -> Command { 260 | let cd = if let Some(ref cd) = self.cd { 261 | canonicalize_path(Path::new(cd)).unwrap() 262 | } else { 263 | ::std::env::current_dir().unwrap() 264 | }; 265 | let mut binary = Path::new(&self.binary).to_owned(); 266 | 267 | // On Windows, current_dir takes place after binary name resolution. 268 | // If current_dir is specified and the binary is referenced by a relative path, 269 | // add the dir change to its relative path. 270 | // https://github.com/rust-lang/rust/issues/37868 271 | if cfg!(windows) && binary.is_relative() && binary.components().count() != 1 { 272 | binary = cd.join(&binary); 273 | } 274 | 275 | // On windows, we run in cmd.exe by default. (This code is a naive way 276 | // of accomplishing this and may contain errors.) 277 | if cfg!(windows) { 278 | let mut cmd = Command::new("cmd"); 279 | cmd.current_dir(cd); 280 | let invoke_string = format!("{} {}", binary.as_path().to_string_lossy(), self.args.join(" ")); 281 | cmd.args(&["/C", &invoke_string]); 282 | for (key, value) in &self.env { 283 | cmd.env(key, value); 284 | } 285 | return cmd; 286 | } 287 | 288 | let mut cmd = Command::new(binary); 289 | cmd.current_dir(cd); 290 | cmd.args(&self.args); 291 | for (key, value) in &self.env { 292 | cmd.env(key, value); 293 | } 294 | cmd 295 | } 296 | } 297 | 298 | // Strips UNC from canonicalized paths. 299 | // See https://github.com/rust-lang/rust/issues/42869 for why this is needed. 300 | #[cfg(windows)] 301 | fn canonicalize_path<'p, P>(path: P) -> Result 302 | where P: Into<&'p Path> { 303 | use std::ffi::OsString; 304 | use std::os::windows::prelude::*; 305 | 306 | let canonical = path.into().canonicalize()?; 307 | let vec_chars = canonical.as_os_str().encode_wide().collect::>(); 308 | if vec_chars[0..4] == [92, 92, 63, 92] { 309 | return Ok(Path::new(&OsString::from_wide(&vec_chars[4..])).to_owned()); 310 | } 311 | 312 | Ok(canonical) 313 | } 314 | 315 | #[cfg(not(windows))] 316 | fn canonicalize_path<'p, P>(path: P) -> Result 317 | where P: Into<&'p Path> { 318 | Ok(path.into().canonicalize()?) 319 | } 320 | 321 | //--------------- 322 | 323 | pub fn commandify(value: String) -> Result { 324 | let lines = value.trim().split("\n").map(String::from).collect::>(); 325 | 326 | #[derive(Debug, PartialEq)] 327 | enum SpecState { 328 | Cd, 329 | Env, 330 | Cmd, 331 | } 332 | 333 | let mut env = HashMap::::new(); 334 | let mut cd = None; 335 | 336 | let mut state = SpecState::Cd; 337 | let mut command_lines = vec![]; 338 | for raw_line in lines { 339 | let mut line = shlex::split(&raw_line).unwrap_or(vec![]); 340 | if state == SpecState::Cmd { 341 | command_lines.push(raw_line); 342 | } else { 343 | if raw_line.trim().is_empty() { 344 | continue; 345 | } 346 | 347 | match line.get(0).map(|x| x.as_ref()) { 348 | Some("cd") => { 349 | if state != SpecState::Cd { 350 | bail!("cd should be the first line in your command! macro."); 351 | } 352 | ensure!(line.len() == 2, "Too many arguments in cd; expected 1, found {}", line.len() - 1); 353 | cd = Some(line.remove(1)); 354 | state = SpecState::Env; 355 | } 356 | Some("export") => { 357 | if state != SpecState::Cd && state != SpecState::Env { 358 | bail!("exports should follow cd but precede your command in the command! macro."); 359 | } 360 | ensure!(line.len() >= 2, "Not enough arguments in export; expected at least 1, found {}", line.len() - 1); 361 | for item in &line[1..] { 362 | let mut items = item.splitn(2, "=").collect::>(); 363 | ensure!(items.len() > 0, "Expected export of the format NAME=VALUE"); 364 | env.insert(items[0].to_string(), items[1].to_string()); 365 | } 366 | state = SpecState::Env; 367 | } 368 | None | Some(_) => { 369 | command_lines.push(raw_line); 370 | state = SpecState::Cmd; 371 | } 372 | } 373 | } 374 | } 375 | if state != SpecState::Cmd || command_lines.is_empty() { 376 | bail!("Didn't find a command in your command! macro."); 377 | } 378 | 379 | // Join the command string and split out binary / args. 380 | let command_string = command_lines.join("\n").replace("\\\n", "\n"); 381 | let mut command = shlex::split(&command_string).expect("Command string couldn't be parsed by shlex"); 382 | let binary = command.remove(0); 383 | let args = command; 384 | 385 | // Generate the CommandSpec struct. 386 | let spec = CommandSpec { 387 | binary, 388 | args, 389 | env, 390 | cd, 391 | }; 392 | 393 | // DEBUG 394 | // eprintln!("COMMAND: {:?}", spec); 395 | 396 | Ok(spec.to_command()) 397 | } 398 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | 2 | #[macro_export] 3 | macro_rules! command { 4 | ($fmt:expr) => ( command!($fmt,) ); 5 | ($fmt:expr, $( $id:ident = $value:expr ),* $(,)*) => ( 6 | { 7 | $crate::commandify( 8 | format!($fmt, $( $id = $crate::command_arg(&$value) ),*) 9 | ) 10 | } 11 | ); 12 | } 13 | 14 | 15 | #[macro_export] 16 | macro_rules! execute { 17 | ($fmt:expr) => ( execute!($fmt,) ); 18 | ($fmt:expr, $( $id:ident = $value:expr ),* $(,)*) => ( 19 | { 20 | use $crate::{CommandSpecExt}; 21 | command!($fmt, $( $id = $value ),*).unwrap().execute() 22 | } 23 | ); 24 | } 25 | 26 | #[macro_export] 27 | macro_rules! sh_command { 28 | ($fmt:expr) => ( sh_command!($fmt,) ); 29 | ($fmt:expr, $( $id:ident = $value:expr ),* $(,)*) => ( 30 | $crate::commandify( 31 | format!( 32 | "sh -c {}", 33 | $crate::command_arg( 34 | &format!("set -e\n\n{}", format!($fmt, $( $id = $crate::command_arg(&$value) ,)*)), 35 | ), 36 | ) 37 | ) 38 | ); 39 | } 40 | 41 | #[macro_export] 42 | macro_rules! sh_execute { 43 | ($fmt:expr) => ( sh_execute!($fmt,) ); 44 | ($fmt:expr, $( $id:ident = $value:expr ),* $(,)*) => ( 45 | { 46 | use $crate::{CommandSpecExt}; 47 | sh_command!($fmt, $( $id = $value ),*).unwrap().execute() 48 | } 49 | ); 50 | } -------------------------------------------------------------------------------- /src/process.rs: -------------------------------------------------------------------------------- 1 | // From https://raw.githubusercontent.com/watchexec/watchexec/master/src/process.rs 2 | #![allow(unused)] 3 | 4 | // use pathop::PathOp; 5 | use std::collections::{HashMap, HashSet}; 6 | use std::path::PathBuf; 7 | 8 | // pub fn spawn(cmd: &Vec, updated_paths: Vec, no_shell: bool) -> Process { 9 | // self::imp::Process::new(cmd, updated_paths, no_shell).expect("unable to spawn process") 10 | // } 11 | 12 | pub use self::imp::Process; 13 | 14 | /* 15 | fn needs_wrapping(s: &String) -> bool { 16 | s.contains(|ch| match ch { 17 | ' ' | '\t' | '\'' | '"' => true, 18 | _ => false, 19 | }) 20 | } 21 | 22 | #[cfg(target_family = "unix")] 23 | fn wrap_in_quotes(s: &String) -> String { 24 | format!( 25 | "'{}'", 26 | if s.contains('\'') { 27 | s.replace('\'', "'\"'\"'") 28 | } else { 29 | s.clone() 30 | } 31 | ) 32 | } 33 | 34 | #[cfg(target_family = "windows")] 35 | fn wrap_in_quotes(s: &String) -> String { 36 | format!( 37 | "\"{}\"", 38 | if s.contains('"') { 39 | s.replace('"', "\"\"") 40 | } else { 41 | s.clone() 42 | } 43 | ) 44 | } 45 | 46 | fn wrap_commands(cmd: &Vec) -> Vec { 47 | cmd.iter() 48 | .map(|fragment| { 49 | if needs_wrapping(fragment) { 50 | wrap_in_quotes(fragment) 51 | } else { 52 | fragment.clone() 53 | } 54 | }) 55 | .collect() 56 | } 57 | */ 58 | 59 | #[cfg(target_family = "unix")] 60 | mod imp { 61 | //use super::wrap_commands; 62 | use nix::libc::*; 63 | use nix::{self, Error}; 64 | // use pathop::PathOp; 65 | use signal::Signal; 66 | use std::io::{self, Result}; 67 | use std::process::Command; 68 | use std::sync::*; 69 | 70 | pub struct Process { 71 | pgid: pid_t, 72 | lock: Mutex, 73 | cvar: Condvar, 74 | } 75 | 76 | fn from_nix_error(err: nix::Error) -> io::Error { 77 | match err { 78 | Error::Sys(errno) => io::Error::from_raw_os_error(errno as i32), 79 | Error::InvalidPath => io::Error::new(io::ErrorKind::InvalidInput, err), 80 | _ => io::Error::new(io::ErrorKind::Other, err), 81 | } 82 | } 83 | 84 | #[allow(unknown_lints)] 85 | #[allow(mutex_atomic)] 86 | impl Process { 87 | pub fn new( 88 | mut command: Command, 89 | ) -> Result { 90 | use nix::unistd::*; 91 | use std::os::unix::process::CommandExt; 92 | 93 | command 94 | .before_exec(|| setpgid(Pid::from_raw(0), Pid::from_raw(0)).map_err(from_nix_error)) 95 | .spawn() 96 | .and_then(|p| { 97 | Ok(Process { 98 | pgid: p.id() as i32, 99 | lock: Mutex::new(false), 100 | cvar: Condvar::new(), 101 | }) 102 | }) 103 | } 104 | 105 | pub fn id(&self) -> i32 { 106 | self.pgid as i32 107 | } 108 | 109 | pub fn reap(&self) { 110 | use nix::sys::wait::*; 111 | use nix::unistd::Pid; 112 | 113 | let mut finished = true; 114 | loop { 115 | match waitpid(Pid::from_raw(-self.pgid), Some(WaitPidFlag::WNOHANG)) { 116 | Ok(WaitStatus::Exited(_, _)) | Ok(WaitStatus::Signaled(_, _, _)) => { 117 | finished = finished && true 118 | } 119 | Ok(_) => { 120 | finished = false; 121 | break; 122 | } 123 | Err(_) => break, 124 | } 125 | } 126 | 127 | if finished { 128 | let mut done = self.lock.lock().unwrap(); 129 | *done = true; 130 | self.cvar.notify_one(); 131 | } 132 | } 133 | 134 | pub fn signal(&self, signal: Signal) { 135 | use signal::ConvertToLibc; 136 | 137 | let signo = signal.convert_to_libc(); 138 | debug!("Sending {:?} (int: {}) to child process", signal, signo); 139 | self.c_signal(signo); 140 | } 141 | 142 | fn c_signal(&self, sig: c_int) { 143 | extern "C" { 144 | fn killpg(pgrp: pid_t, sig: c_int) -> c_int; 145 | } 146 | 147 | unsafe { 148 | killpg(self.pgid, sig); 149 | } 150 | } 151 | 152 | pub fn wait(&self) { 153 | let mut done = self.lock.lock().unwrap(); 154 | while !*done { 155 | done = self.cvar.wait(done).unwrap(); 156 | } 157 | } 158 | } 159 | } 160 | 161 | #[cfg(target_family = "windows")] 162 | mod imp { 163 | //use super::wrap_commands; 164 | use kernel32::*; 165 | // use pathop::PathOp; 166 | use signal::Signal; 167 | use std::io; 168 | use std::io::Result; 169 | use std::mem; 170 | use std::process::Command; 171 | use std::ptr; 172 | use winapi::*; 173 | 174 | pub struct Process { 175 | job: HANDLE, 176 | completion_port: HANDLE, 177 | } 178 | 179 | #[repr(C)] 180 | struct JOBOBJECT_ASSOCIATE_COMPLETION_PORT { 181 | completion_key: PVOID, 182 | completion_port: HANDLE, 183 | } 184 | 185 | impl Process { 186 | pub fn new( 187 | mut command: Command, 188 | ) -> Result { 189 | use std::os::windows::io::IntoRawHandle; 190 | use std::os::windows::process::CommandExt; 191 | 192 | fn last_err() -> io::Error { 193 | io::Error::last_os_error() 194 | } 195 | 196 | let job = unsafe { CreateJobObjectW(0 as *mut _, 0 as *const _) }; 197 | if job.is_null() { 198 | panic!("failed to create job object: {}", last_err()); 199 | } 200 | 201 | let completion_port = 202 | unsafe { CreateIoCompletionPort(INVALID_HANDLE_VALUE, ptr::null_mut(), 0, 1) }; 203 | if job.is_null() { 204 | panic!( 205 | "unable to create IO completion port for job: {}", 206 | last_err() 207 | ); 208 | } 209 | 210 | let mut associate_completion: JOBOBJECT_ASSOCIATE_COMPLETION_PORT = 211 | unsafe { mem::zeroed() }; 212 | associate_completion.completion_key = job; 213 | associate_completion.completion_port = completion_port; 214 | unsafe { 215 | let r = SetInformationJobObject( 216 | job, 217 | JobObjectAssociateCompletionPortInformation, 218 | &mut associate_completion as *mut _ as LPVOID, 219 | mem::size_of_val(&associate_completion) as DWORD, 220 | ); 221 | if r == 0 { 222 | panic!( 223 | "failed to associate completion port with job: {}", 224 | last_err() 225 | ); 226 | } 227 | } 228 | 229 | let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = unsafe { mem::zeroed() }; 230 | info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; 231 | let r = unsafe { 232 | SetInformationJobObject( 233 | job, 234 | JobObjectExtendedLimitInformation, 235 | &mut info as *mut _ as LPVOID, 236 | mem::size_of_val(&info) as DWORD, 237 | ) 238 | }; 239 | if r == 0 { 240 | panic!("failed to set job info: {}", last_err()); 241 | } 242 | 243 | command.creation_flags(CREATE_SUSPENDED); 244 | command.spawn().and_then(|p| { 245 | let handle = p.into_raw_handle(); 246 | let r = unsafe { AssignProcessToJobObject(job, handle) }; 247 | if r == 0 { 248 | panic!("failed to add to job object: {}", last_err()); 249 | } 250 | 251 | resume_threads(handle); 252 | 253 | Ok(Process { 254 | job: job, 255 | completion_port: completion_port, 256 | }) 257 | }) 258 | } 259 | 260 | pub fn id(&self) -> i32 { 261 | self.job as i32 262 | } 263 | 264 | pub fn reap(&self) {} 265 | 266 | pub fn signal(&self, _signal: Signal) { 267 | unsafe { 268 | let _ = TerminateJobObject(self.job, 1); 269 | } 270 | } 271 | 272 | pub fn wait(&self) { 273 | unsafe { 274 | loop { 275 | let mut code: DWORD = 0; 276 | let mut key: ULONG_PTR = 0; 277 | let mut overlapped: LPOVERLAPPED = mem::uninitialized(); 278 | GetQueuedCompletionStatus( 279 | self.completion_port, 280 | &mut code, 281 | &mut key, 282 | &mut overlapped, 283 | INFINITE, 284 | ); 285 | 286 | if code == JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO && (key as HANDLE) == self.job { 287 | break; 288 | } 289 | } 290 | } 291 | } 292 | } 293 | 294 | impl Drop for Process { 295 | fn drop(&mut self) { 296 | unsafe { 297 | let _ = CloseHandle(self.job); 298 | let _ = CloseHandle(self.completion_port); 299 | } 300 | } 301 | } 302 | 303 | unsafe impl Send for Process {} 304 | unsafe impl Sync for Process {} 305 | 306 | // This is pretty terrible, but it's either this or we re-implement all of Rust's std::process just to get at PROCESS_INFORMATION 307 | fn resume_threads(child_process: HANDLE) { 308 | unsafe { 309 | let child_id = GetProcessId(child_process); 310 | 311 | let h = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); 312 | let mut entry = THREADENTRY32 { 313 | dwSize: 28, 314 | cntUsage: 0, 315 | th32ThreadID: 0, 316 | th32OwnerProcessID: 0, 317 | tpBasePri: 0, 318 | tpDeltaPri: 0, 319 | dwFlags: 0, 320 | }; 321 | 322 | let mut result = Thread32First(h, &mut entry); 323 | while result != 0 { 324 | if entry.th32OwnerProcessID == child_id { 325 | let thread_handle = OpenThread(0x0002, 0, entry.th32ThreadID); 326 | ResumeThread(thread_handle); 327 | CloseHandle(thread_handle); 328 | } 329 | 330 | result = Thread32Next(h, &mut entry); 331 | } 332 | 333 | CloseHandle(h); 334 | } 335 | } 336 | } 337 | 338 | /// Collect `PathOp` details into op-categories to pass onto the exec'd command as env-vars 339 | /// 340 | /// WRITTEN -> `notify::ops::WRITE`, `notify::ops::CLOSE_WRITE` 341 | /// META_CHANGED -> `notify::ops::CHMOD` 342 | /// REMOVED -> `notify::ops::REMOVE` 343 | /// CREATED -> `notify::ops::CREATE` 344 | /// RENAMED -> `notify::ops::RENAME` 345 | // fn collect_path_env_vars(pathops: &[PathOp]) -> Vec<(String, String)> { 346 | // #[cfg(target_family = "unix")] 347 | // const ENV_SEP: &'static str = ":"; 348 | // #[cfg(not(target_family = "unix"))] 349 | // const ENV_SEP: &'static str = ";"; 350 | 351 | // let mut by_op = HashMap::new(); // Paths as `String`s collected by `notify::op` 352 | // let mut all_pathbufs = HashSet::new(); // All unique `PathBuf`s 353 | // for pathop in pathops { 354 | // if let Some(op) = pathop.op { 355 | // // ignore pathops that don't have a `notify::op` set 356 | // if let Some(s) = pathop.path.to_str() { 357 | // // ignore invalid utf8 paths 358 | // all_pathbufs.insert(pathop.path.clone()); 359 | // let e = by_op.entry(op).or_insert_with(Vec::new); 360 | // e.push(s.to_owned()); 361 | // } 362 | // } 363 | // } 364 | 365 | // let mut vars = vec![]; 366 | // // Only break off a common path if we have more than one unique path, 367 | // // otherwise we end up with a `COMMON_PATH` being set and other vars 368 | // // being present but empty. 369 | // let common_path = if all_pathbufs.len() > 1 { 370 | // let all_pathbufs: Vec = all_pathbufs.into_iter().collect(); 371 | // get_longest_common_path(&all_pathbufs) 372 | // } else { 373 | // None 374 | // }; 375 | // if let Some(ref common_path) = common_path { 376 | // vars.push(("WATCHEXEC_COMMON_PATH".to_string(), common_path.to_string())); 377 | // } 378 | // for (op, paths) in by_op { 379 | // let key = match op { 380 | // op if PathOp::is_create(op) => "WATCHEXEC_CREATED_PATH", 381 | // op if PathOp::is_remove(op) => "WATCHEXEC_REMOVED_PATH", 382 | // op if PathOp::is_rename(op) => "WATCHEXEC_RENAMED_PATH", 383 | // op if PathOp::is_write(op) => "WATCHEXEC_WRITTEN_PATH", 384 | // op if PathOp::is_meta(op) => "WATCHEXEC_META_CHANGED_PATH", 385 | // _ => continue, // ignore `notify::op::RESCAN`s 386 | // }; 387 | 388 | // let paths = if let Some(ref common_path) = common_path { 389 | // paths 390 | // .iter() 391 | // .map(|path_str| path_str.trim_left_matches(common_path).to_string()) 392 | // .collect::>() 393 | // } else { 394 | // paths 395 | // }; 396 | // vars.push((key.to_string(), paths.as_slice().join(ENV_SEP))); 397 | // } 398 | // vars 399 | // } 400 | 401 | fn get_longest_common_path(paths: &[PathBuf]) -> Option { 402 | match paths.len() { 403 | 0 => return None, 404 | 1 => return paths[0].to_str().map(|ref_val| ref_val.to_string()), 405 | _ => {} 406 | }; 407 | 408 | let mut longest_path: Vec<_> = paths[0].components().collect(); 409 | 410 | for path in &paths[1..] { 411 | let mut greatest_distance = 0; 412 | for component_pair in path.components().zip(longest_path.iter()) { 413 | if component_pair.0 != *component_pair.1 { 414 | break; 415 | } 416 | 417 | greatest_distance += 1; 418 | } 419 | 420 | if greatest_distance != longest_path.len() { 421 | longest_path.truncate(greatest_distance); 422 | } 423 | } 424 | 425 | let mut result = PathBuf::new(); 426 | for component in longest_path { 427 | result.push(component.as_os_str()); 428 | } 429 | 430 | result.to_str().map(|ref_val| ref_val.to_string()) 431 | } 432 | 433 | #[cfg(test)] 434 | #[cfg(target_family = "unix")] 435 | mod tests { 436 | // use notify; 437 | // use pathop::PathOp; 438 | use std::collections::HashSet; 439 | use std::path::PathBuf; 440 | 441 | // use super::collect_path_env_vars; 442 | use super::get_longest_common_path; 443 | // use super::spawn; 444 | // //use super::wrap_commands; 445 | 446 | // #[test] 447 | // fn test_start() { 448 | // let _ = spawn(&vec!["echo".into(), "hi".into()], vec![], true); 449 | // } 450 | 451 | /* 452 | #[test] 453 | fn wrap_commands_that_have_whitespace() { 454 | assert_eq!( 455 | wrap_commands(&vec!["echo".into(), "hello world".into()]), 456 | vec!["echo".into(), "'hello world'".into()] as Vec 457 | ); 458 | } 459 | 460 | #[test] 461 | fn wrap_commands_that_have_long_whitespace() { 462 | assert_eq!( 463 | wrap_commands(&vec!["echo".into(), "hello world".into()]), 464 | vec!["echo".into(), "'hello world'".into()] as Vec 465 | ); 466 | } 467 | 468 | #[test] 469 | fn wrap_commands_that_have_single_quotes() { 470 | assert_eq!( 471 | wrap_commands(&vec!["echo".into(), "hello ' world".into()]), 472 | vec!["echo".into(), "'hello '\"'\"' world'".into()] as Vec 473 | ); 474 | assert_eq!( 475 | wrap_commands(&vec!["echo".into(), "hello'world".into()]), 476 | vec!["echo".into(), "'hello'\"'\"'world'".into()] as Vec 477 | ); 478 | } 479 | 480 | #[test] 481 | fn wrap_commands_that_have_double_quotes() { 482 | assert_eq!( 483 | wrap_commands(&vec!["echo".into(), "hello \" world".into()]), 484 | vec!["echo".into(), "'hello \" world'".into()] as Vec 485 | ); 486 | assert_eq!( 487 | wrap_commands(&vec!["echo".into(), "hello\"world".into()]), 488 | vec!["echo".into(), "'hello\"world'".into()] as Vec 489 | ); 490 | } 491 | */ 492 | 493 | #[test] 494 | fn longest_common_path_should_return_correct_value() { 495 | let single_path = vec![PathBuf::from("/tmp/random/")]; 496 | let single_result = get_longest_common_path(&single_path).unwrap(); 497 | assert_eq!(single_result, "/tmp/random/"); 498 | 499 | let common_paths = vec![ 500 | PathBuf::from("/tmp/logs/hi"), 501 | PathBuf::from("/tmp/logs/bye"), 502 | PathBuf::from("/tmp/logs/bye"), 503 | PathBuf::from("/tmp/logs/fly"), 504 | ]; 505 | 506 | let common_result = get_longest_common_path(&common_paths).unwrap(); 507 | assert_eq!(common_result, "/tmp/logs"); 508 | 509 | let diverging_paths = vec![PathBuf::from("/tmp/logs/hi"), PathBuf::from("/var/logs/hi")]; 510 | 511 | let diverging_result = get_longest_common_path(&diverging_paths).unwrap(); 512 | assert_eq!(diverging_result, "/"); 513 | 514 | let uneven_paths = vec![ 515 | PathBuf::from("/tmp/logs/hi"), 516 | PathBuf::from("/tmp/logs/"), 517 | PathBuf::from("/tmp/logs/bye"), 518 | ]; 519 | 520 | let uneven_result = get_longest_common_path(&uneven_paths).unwrap(); 521 | assert_eq!(uneven_result, "/tmp/logs"); 522 | } 523 | 524 | // #[test] 525 | // fn pathops_collect_to_env_vars() { 526 | // let pathops = vec![ 527 | // PathOp::new( 528 | // &PathBuf::from("/tmp/logs/hi"), 529 | // Some(notify::op::CREATE), 530 | // None, 531 | // ), 532 | // PathOp::new( 533 | // &PathBuf::from("/tmp/logs/hey/there"), 534 | // Some(notify::op::CREATE), 535 | // None, 536 | // ), 537 | // PathOp::new( 538 | // &PathBuf::from("/tmp/logs/bye"), 539 | // Some(notify::op::REMOVE), 540 | // None, 541 | // ), 542 | // ]; 543 | // let expected_vars = vec![ 544 | // ("WATCHEXEC_COMMON_PATH".to_string(), "/tmp/logs".to_string()), 545 | // ("WATCHEXEC_REMOVED_PATH".to_string(), "/bye".to_string()), 546 | // ( 547 | // "WATCHEXEC_CREATED_PATH".to_string(), 548 | // "/hi:/hey/there".to_string(), 549 | // ), 550 | // ]; 551 | // let vars = collect_path_env_vars(&pathops); 552 | // assert_eq!( 553 | // vars.iter().collect::>(), 554 | // expected_vars.iter().collect::>() 555 | // ); 556 | // } 557 | } 558 | 559 | #[cfg(test)] 560 | #[cfg(target_family = "windows")] 561 | mod tests { 562 | // use super::spawn; 563 | //use super::wrap_commands; 564 | 565 | // #[test] 566 | // fn test_start() { 567 | // let _ = spawn(&vec!["echo".into(), "hi".into()], vec![], true); 568 | // } 569 | 570 | /* 571 | #[test] 572 | fn wrap_commands_that_have_whitespace() { 573 | assert_eq!( 574 | wrap_commands(&vec!["echo".into(), "hello world".into()]), 575 | vec!["echo".into(), "\"hello world\"".into()] as Vec 576 | ); 577 | } 578 | 579 | #[test] 580 | fn wrap_commands_that_have_long_whitespace() { 581 | assert_eq!( 582 | wrap_commands(&vec!["echo".into(), "hello world".into()]), 583 | vec!["echo".into(), "\"hello world\"".into()] as Vec 584 | ); 585 | } 586 | 587 | #[test] 588 | fn wrap_commands_that_have_single_quotes() { 589 | assert_eq!( 590 | wrap_commands(&vec!["echo".into(), "hello ' world".into()]), 591 | vec!["echo".into(), "\"hello ' world\"".into()] as Vec 592 | ); 593 | assert_eq!( 594 | wrap_commands(&vec!["echo".into(), "hello'world".into()]), 595 | vec!["echo".into(), "\"hello'world\"".into()] as Vec 596 | ); 597 | } 598 | 599 | #[test] 600 | fn wrap_commands_that_have_double_quotes() { 601 | assert_eq!( 602 | wrap_commands(&vec!["echo".into(), "hello \" world".into()]), 603 | vec!["echo".into(), "\"hello \"\" world\"".into()] as Vec 604 | ); 605 | assert_eq!( 606 | wrap_commands(&vec!["echo".into(), "hello\"world".into()]), 607 | vec!["echo".into(), "\"hello\"\"world\"".into()] as Vec 608 | ); 609 | } 610 | */ 611 | } 612 | -------------------------------------------------------------------------------- /src/signal.rs: -------------------------------------------------------------------------------- 1 | // From https://raw.githubusercontent.com/watchexec/watchexec/master/src/signal.rs 2 | #![allow(unused)] 3 | 4 | use std::sync::Mutex; 5 | use std::sync::atomic::{AtomicUsize, ATOMIC_USIZE_INIT, Ordering}; 6 | 7 | lazy_static! { 8 | static ref CLEANUP: Mutex>> = Mutex::new(None); 9 | } 10 | 11 | #[cfg(unix)] 12 | pub use nix::sys::signal::Signal; 13 | 14 | #[cfg(windows)] 15 | use winapi; 16 | 17 | // This is a dummy enum for Windows 18 | #[cfg(windows)] 19 | #[derive(Debug, Copy, Clone)] 20 | pub enum Signal { 21 | SIGKILL, 22 | SIGTERM, 23 | SIGINT, 24 | SIGHUP, 25 | SIGSTOP, 26 | SIGCONT, 27 | SIGCHLD, 28 | SIGUSR1, 29 | SIGUSR2, 30 | } 31 | 32 | #[cfg(unix)] 33 | use nix::libc::*; 34 | 35 | #[cfg(unix)] 36 | pub trait ConvertToLibc { 37 | fn convert_to_libc(self) -> c_int; 38 | } 39 | 40 | #[cfg(unix)] 41 | impl ConvertToLibc for Signal { 42 | fn convert_to_libc(self) -> c_int { 43 | // Convert from signal::Signal enum to libc::* c_int constants 44 | match self { 45 | Signal::SIGKILL => SIGKILL, 46 | Signal::SIGTERM => SIGTERM, 47 | Signal::SIGINT => SIGINT, 48 | Signal::SIGHUP => SIGHUP, 49 | Signal::SIGSTOP => SIGSTOP, 50 | Signal::SIGCONT => SIGCONT, 51 | Signal::SIGCHLD => SIGCHLD, 52 | Signal::SIGUSR1 => SIGUSR1, 53 | Signal::SIGUSR2 => SIGUSR2, 54 | _ => panic!("unsupported signal: {:?}", self), 55 | } 56 | } 57 | } 58 | 59 | pub fn new(signal_name: Option) -> Option { 60 | if let Some(signame) = signal_name { 61 | let signal = match signame.as_ref() { 62 | "SIGKILL" | "KILL" => Signal::SIGKILL, 63 | "SIGTERM" | "TERM" => Signal::SIGTERM, 64 | "SIGINT" | "INT" => Signal::SIGINT, 65 | "SIGHUP" | "HUP" => Signal::SIGHUP, 66 | "SIGSTOP" | "STOP" => Signal::SIGSTOP, 67 | "SIGCONT" | "CONT" => Signal::SIGCONT, 68 | "SIGCHLD" | "CHLD" => Signal::SIGCHLD, 69 | "SIGUSR1" | "USR1" => Signal::SIGUSR1, 70 | "SIGUSR2" | "USR2" => Signal::SIGUSR2, 71 | _ => panic!("unsupported signal: {}", signame), 72 | }; 73 | 74 | Some(signal) 75 | } else { 76 | None 77 | } 78 | } 79 | 80 | static GLOBAL_HANDLER_ID: AtomicUsize = ATOMIC_USIZE_INIT; 81 | 82 | #[cfg(unix)] 83 | pub fn uninstall_handler() { 84 | GLOBAL_HANDLER_ID.fetch_add(1, Ordering::Relaxed) + 1; 85 | 86 | use nix::libc::c_int; 87 | use nix::sys::signal::*; 88 | use nix::unistd::Pid; 89 | 90 | // Interrupt self to interrupt handler. 91 | kill(Pid::this(), Signal::SIGUSR2); 92 | } 93 | 94 | #[cfg(unix)] 95 | pub fn install_handler(handler: F) 96 | where 97 | F: Fn(self::Signal) + 'static + Send + Sync, 98 | { 99 | use nix::libc::c_int; 100 | use nix::sys::signal::*; 101 | use std::thread; 102 | 103 | // Mask all signals interesting to us. The mask propagates 104 | // to all threads started after this point. 105 | let mut mask = SigSet::empty(); 106 | mask.add(SIGKILL); 107 | mask.add(SIGTERM); 108 | mask.add(SIGINT); 109 | mask.add(SIGHUP); 110 | mask.add(SIGSTOP); 111 | mask.add(SIGCONT); 112 | mask.add(SIGCHLD); 113 | mask.add(SIGUSR1); 114 | mask.add(SIGUSR2); 115 | mask.thread_swap_mask(SigmaskHow::SIG_SETMASK).expect("unable to set signal mask"); 116 | 117 | set_handler(handler); 118 | 119 | // Indicate interest in SIGCHLD by setting a dummy handler 120 | pub extern "C" fn sigchld_handler(_: c_int) {} 121 | 122 | unsafe { 123 | let _ = sigaction( 124 | SIGCHLD, 125 | &SigAction::new( 126 | SigHandler::Handler(sigchld_handler), 127 | SaFlags::empty(), 128 | SigSet::empty(), 129 | ), 130 | ); 131 | } 132 | 133 | // Spawn a thread to catch these signals 134 | let id = GLOBAL_HANDLER_ID.fetch_add(1, Ordering::Relaxed) + 1; 135 | thread::spawn(move || { 136 | let mut is_current = true; 137 | while is_current { 138 | let signal = mask.wait().expect("Unable to sigwait"); 139 | debug!("Received {:?}", signal); 140 | 141 | if id != GLOBAL_HANDLER_ID.load(Ordering::Relaxed) { 142 | return; 143 | } 144 | // Invoke closure 145 | invoke(signal); 146 | 147 | // Restore default behavior for received signal and unmask it 148 | if signal != SIGCHLD { 149 | let default_action = 150 | SigAction::new(SigHandler::SigDfl, SaFlags::empty(), SigSet::empty()); 151 | 152 | unsafe { 153 | let _ = sigaction(signal, &default_action); 154 | } 155 | } 156 | 157 | let mut new_mask = SigSet::empty(); 158 | new_mask.add(signal); 159 | 160 | // Re-raise with signal unmasked 161 | let _ = new_mask.thread_unblock(); 162 | let _ = raise(signal); 163 | let _ = new_mask.thread_block(); 164 | } 165 | }); 166 | } 167 | 168 | #[cfg(windows)] 169 | pub fn uninstall_handler() { 170 | use kernel32::SetConsoleCtrlHandler; 171 | use winapi::{BOOL, DWORD, FALSE, TRUE}; 172 | 173 | unsafe { 174 | SetConsoleCtrlHandler(Some(ctrl_handler), FALSE); 175 | } 176 | debug!("Removed ConsoleCtrlHandler."); 177 | } 178 | 179 | #[cfg(windows)] 180 | pub unsafe extern "system" fn ctrl_handler(_: winapi::DWORD) -> winapi::BOOL { 181 | invoke(self::Signal::SIGTERM); 182 | 183 | winapi::FALSE 184 | } 185 | 186 | #[cfg(windows)] 187 | pub fn install_handler(handler: F) 188 | where 189 | F: Fn(self::Signal) + 'static + Send + Sync, 190 | { 191 | use kernel32::SetConsoleCtrlHandler; 192 | use winapi::{BOOL, DWORD, FALSE, TRUE}; 193 | 194 | set_handler(handler); 195 | 196 | unsafe { 197 | SetConsoleCtrlHandler(Some(ctrl_handler), TRUE); 198 | } 199 | } 200 | 201 | fn invoke(sig: self::Signal) { 202 | if let Some(ref handler) = *CLEANUP.lock().unwrap() { 203 | handler(sig) 204 | } 205 | } 206 | 207 | fn set_handler(handler: F) 208 | where 209 | F: Fn(self::Signal) + 'static + Send + Sync, 210 | { 211 | *CLEANUP.lock().unwrap() = Some(Box::new(handler)); 212 | } 213 | -------------------------------------------------------------------------------- /tests/all.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate commandspec; 3 | 4 | #[cfg(not(windows))] 5 | mod sh { 6 | #[test] 7 | fn sh_exit() { 8 | let res = sh_execute!(r"exit {a}", a = 42).unwrap_err(); 9 | assert_eq!(res.error_code(), 42); 10 | } 11 | 12 | #[test] 13 | fn sh_echo1() { 14 | let res = sh_command!( 15 | r"A={a}; echo $A", 16 | a = "SENTINEL" 17 | ).unwrap().output().unwrap(); 18 | assert_eq!(res.stdout, b"SENTINEL\n"); 19 | } 20 | 21 | #[test] 22 | fn sh_echo2() { 23 | let res = sh_command!( 24 | r"A={a}; echo $A", 25 | a = "SENTINEL", 26 | ).unwrap().output().unwrap(); 27 | assert_eq!(res.stdout, b"SENTINEL\n"); 28 | } 29 | 30 | #[test] 31 | fn sh_empty() { 32 | sh_execute!(r"true").unwrap(); 33 | } 34 | 35 | #[test] 36 | fn sh_empty_comma() { 37 | sh_execute!(r"true", ).unwrap(); 38 | } 39 | } 40 | 41 | #[test] 42 | fn cmd_rustc() { 43 | let args = vec!["-V"]; 44 | let res = command!( 45 | r" 46 | rustc {args} 47 | ", 48 | args = args, 49 | ).unwrap().output().unwrap(); 50 | assert!(res.stdout.starts_with(b"rustc ")); 51 | } 52 | --------------------------------------------------------------------------------