From b14d1ec97cdb3748281af15d6c2acbd23cd221a1 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Mon, 27 Aug 2018 16:03:52 -0700 Subject: [PATCH] Wait for child processes to finish (#345) Thanks to @bheisler for the feature request and initial implementation. Fixes #302 --- .gitattributes | 1 + Cargo.lock | 81 +++++++++++++ Cargo.toml | 6 + justfile | 2 +- rustfmt.toml | 2 + src/assignment_evaluator.rs | 5 +- src/common.rs | 2 + src/die.rs | 7 ++ src/function.rs | 9 +- src/interrupt_handler.rs | 93 +++++++++++++++ src/justfile.rs | 219 +++++++++++++++++++++++++--------- src/main.rs | 11 +- src/recipe.rs | 5 +- src/run.rs | 20 ++-- tests/interrupts.rs | 81 +++++++++++++ tests/invocation_directory.rs | 1 - 16 files changed, 465 insertions(+), 80 deletions(-) create mode 100644 .gitattributes create mode 100644 rustfmt.toml create mode 100644 src/die.rs create mode 100644 src/interrupt_handler.rs create mode 100644 tests/interrupts.rs diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..77c469c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +Cargo.lock linguist-generated diff=nodiff diff --git a/Cargo.lock b/Cargo.lock index 1de968a..7ad7ff3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,6 +88,15 @@ dependencies = [ "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "ctrlc" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "nix 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "dotenv" version = "0.13.0" @@ -108,6 +117,18 @@ name = "either" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "env_logger" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "atty 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "humantime 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "termcolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "executable-path" version = "1.0.0" @@ -151,6 +172,14 @@ name = "glob" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "humantime" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "itertools" version = "0.7.8" @@ -168,12 +197,15 @@ dependencies = [ "atty 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", "brev 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)", + "ctrlc 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "dotenv 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)", "edit-distance 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "env_logger 0.5.12 (registry+https://github.com/rust-lang/crates.io-index)", "executable-path 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "itertools 0.7.8 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "target 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", @@ -190,6 +222,14 @@ name = "libc" version = "0.2.42" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "log" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "memchr" version = "2.0.1" @@ -198,6 +238,23 @@ dependencies = [ "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "nix" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "cc 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", + "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quick-error" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "quote" version = "0.3.15" @@ -305,6 +362,14 @@ dependencies = [ "remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "termcolor" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "wincolor 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "termion" version = "1.5.1" @@ -389,6 +454,14 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "wincolor" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [metadata] "checksum aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d6531d44de723825aa81398a6415283229725a00fa30713812ab9323faa82fc4" "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" @@ -401,19 +474,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum cc 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)" = "49ec142f5768efb5b7622aebc3fdbdbb8950a4b9ba996393cb76ef7466e8747d" "checksum cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "405216fd8fe65f718daa7102ea808a946b6ce40c742998fbfd3463645552de18" "checksum clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0f16b89cbb9ee36d87483dc939fe9f1e13c05898d56d7b230a0d4dff033a536" +"checksum ctrlc 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "630391922b1b893692c6334369ff528dcc3a9d8061ccf4c803aa8f83cb13db5e" "checksum dotenv 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0d0a1279c96732bc6800ce6337b6a614697b0e74ae058dc03c62ebeb78b4d86" "checksum edit-distance 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3bd26878c3d921f89797a4e1a1711919f999a9f6946bb6f5a4ffda126d297b7e" "checksum either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3be565ca5c557d7f59e7cfcf1844f9e3033650c929c6566f511e8005f205c1d0" +"checksum env_logger 0.5.12 (registry+https://github.com/rust-lang/crates.io-index)" = "f4d7e69c283751083d53d01eac767407343b8b69c4bd70058e08adc2637cb257" "checksum executable-path 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ebc5a6d89e3c90b84e8f33c8737933dda8f1c106b5415900b38b9d433841478" "checksum failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "934799b6c1de475a012a02dab0ace1ace43789ee4b99bcfbf1a2e3e8ced5de82" "checksum failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c7cdda555bb90c9bb67a3b670a0f42de8e73f5981524123ad8578aafec8ddb8b" "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" "checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" +"checksum humantime 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0484fda3e7007f2a4a0d9c3a703ca38c71c54c55602ce4660c419fd32e188c9e" "checksum itertools 0.7.8 (registry+https://github.com/rust-lang/crates.io-index)" = "f58856976b776fedd95533137617a02fb25719f40e7d9b01c7043cd65474f450" "checksum lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e6412c5e2ad9584b0b8e979393122026cdd6d2a80b933f890dcd694ddbe73739" "checksum libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)" = "b685088df2b950fccadf07a7187c8ef846a959c142338a48f9dc0b94517eb5f1" +"checksum log 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "cba860f648db8e6f269df990180c2217f333472b4a6e901e97446858487971e2" "checksum memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "796fba70e76612589ed2ce7f45282f5af869e0fdd7cc6199fa1aa1f1d591ba9d" +"checksum nix 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d37e713a259ff641624b6cb20e3b12b2952313ba36b6823c0f16e6cfd9e5de17" +"checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0" "checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" "checksum rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eba5f8cb59cc50ed56be8880a5c7b496bfd9bd26394e176bc67884094145c2c5" "checksum redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "c214e91d3ecf43e9a4e41e578973adeb14b474f2bee858742d127af75a0112b1" @@ -428,6 +507,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum synstructure 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3a761d12e6d8dcb4dcf952a7a89b475e3a9d69e4a69307e01a470977642914bd" "checksum target 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "10000465bb0cc031c87a44668991b284fd84c0e6bd945f62d4af04e9e52a222a" "checksum tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +"checksum termcolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "722426c4a0539da2c4ffd9b419d90ad540b4cff4a053be9069c908d4d07e2836" "checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" "checksum textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0b59b6b4b44d867f1370ef1bd91bfb262bf07bf0ae65c202ea2fbc16153b693" "checksum thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "279ef31c19ededf577bfd12dfae728040a21f635b06a24cd670ff510edd38963" @@ -441,3 +521,4 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "773ef9dcc5f24b7d850d0ff101e542ff24c3b090a9768e03ff889fdef41f00fd" "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +"checksum wincolor 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b9dc3aa9dcda98b5a16150c54619c1ead22e3d3a5d458778ae914be760aa981a" diff --git a/Cargo.toml b/Cargo.toml index 7fe3bcf..12e5417 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,13 +15,19 @@ brev = "0.1.6" clap = "2.0.0" dotenv = "0.13.0" edit-distance = "2.0.0" +env_logger = "0.5.12" itertools = "0.7" lazy_static = "1.0.0" libc = "0.2.21" +log = "0.4.4" regex = "1.0.0" target = "1.0.0" tempdir = "0.3.5" unicode-width = "0.1.3" +[dependencies.ctrlc] +version = "3.1" +features = ['termination'] + [dev-dependencies] executable-path = "1.0.0" diff --git a/justfile b/justfile index ad26366..5e5b12f 100644 --- a/justfile +++ b/justfile @@ -27,7 +27,7 @@ check: cargo check watch COMMAND='test': - cargo watch {{COMMAND}} + cargo watch --clear --exec {{COMMAND}} version = `sed -En 's/version[[:space:]]*=[[:space:]]*"([^"]+)"/v\1/p' Cargo.toml` diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..346c9d2 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,2 @@ +tab_spaces = 2 +max_width = 100 diff --git a/src/assignment_evaluator.rs b/src/assignment_evaluator.rs index 9faab3f..f26436c 100644 --- a/src/assignment_evaluator.rs +++ b/src/assignment_evaluator.rs @@ -1,5 +1,3 @@ -use std::path::PathBuf; - use common::*; use brev; @@ -154,8 +152,9 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> { process::Stdio::inherit() }); - brev::output(cmd) + InterruptHandler::guard(|| brev::output(cmd) .map_err(|output_error| RuntimeError::Backtick{token: token.clone(), output_error}) + ) } } diff --git a/src/common.rs b/src/common.rs index aca73f8..537e97a 100644 --- a/src/common.rs +++ b/src/common.rs @@ -5,6 +5,7 @@ pub use std::io::prelude::*; pub use std::ops::Range; pub use std::path::{Path, PathBuf}; pub use std::process::Command; +pub use std::sync::{Mutex, MutexGuard}; pub use std::{cmp, env, fs, fmt, io, iter, process, vec, usize}; pub use color::Color; @@ -21,6 +22,7 @@ pub use cooked_string::CookedString; pub use expression::Expression; pub use fragment::Fragment; pub use function::{evaluate_function, resolve_function, FunctionContext}; +pub use interrupt_handler::InterruptHandler; pub use justfile::Justfile; pub use lexer::Lexer; pub use load_dotenv::load_dotenv; diff --git a/src/die.rs b/src/die.rs new file mode 100644 index 0000000..4c1e62d --- /dev/null +++ b/src/die.rs @@ -0,0 +1,7 @@ +macro_rules! die { + ($($arg:tt)*) => {{ + extern crate std; + eprintln!($($arg)*); + process::exit(EXIT_FAILURE) + }}; +} diff --git a/src/function.rs b/src/function.rs index 9a05988..49f3c37 100644 --- a/src/function.rs +++ b/src/function.rs @@ -1,6 +1,5 @@ -use std::path::PathBuf; - use common::*; + use target; use platform::{Platform, PlatformInterface}; @@ -106,14 +105,14 @@ pub fn invocation_directory(context: &FunctionContext) -> Result pub fn env_var(context: &FunctionContext, key: &str) -> Result { use std::env::VarError::*; - + if let Some(value) = context.dotenv.get(key) { return Ok(value.clone()); } match env::var(key) { Err(NotPresent) => Err(format!("environment variable `{}` not present", key)), - Err(NotUnicode(os_string)) => + Err(NotUnicode(os_string)) => Err(format!("environment variable `{}` not unicode: {:?}", key, os_string)), Ok(value) => Ok(value), } @@ -131,7 +130,7 @@ pub fn env_var_or_default( use std::env::VarError::*; match env::var(key) { Err(NotPresent) => Ok(default.to_string()), - Err(NotUnicode(os_string)) => + Err(NotUnicode(os_string)) => Err(format!("environment variable `{}` not unicode: {:?}", key, os_string)), Ok(value) => Ok(value), } diff --git a/src/interrupt_handler.rs b/src/interrupt_handler.rs new file mode 100644 index 0000000..b9bf533 --- /dev/null +++ b/src/interrupt_handler.rs @@ -0,0 +1,93 @@ +use common::*; + +use ctrlc; + +pub struct InterruptHandler { + blocks: u32, + interrupted: bool, +} + +impl InterruptHandler { + pub fn install() -> Result<(), ctrlc::Error> { + ctrlc::set_handler(|| InterruptHandler::instance().interrupt()) + } + + fn instance() -> MutexGuard<'static, InterruptHandler> { + lazy_static! { + static ref INSTANCE: Mutex = Mutex::new(InterruptHandler::new()); + } + + match INSTANCE.lock() { + Ok(guard) => guard, + Err(poison_error) => die!( + "{}", + RuntimeError::Internal { + message: format!("interrupt handler mutex poisoned: {}", poison_error), + } + ), + } + } + + fn new() -> InterruptHandler { + InterruptHandler { + blocks: 0, + interrupted: false, + } + } + + fn interrupt(&mut self) { + self.interrupted = true; + + if self.blocks > 0 { + return; + } + + Self::exit(); + } + + fn exit() { + process::exit(130); + } + + fn block(&mut self) { + self.blocks += 1; + } + + fn unblock(&mut self) { + if self.blocks == 0 { + die!( + "{}", + RuntimeError::Internal { + message: "attempted to unblock interrupt handler, but handler was not blocked" + .to_string(), + } + ); + } + + self.blocks -= 1; + + if self.interrupted { + Self::exit(); + } + } + + pub fn guard T>(function: F) -> T { + let _guard = InterruptGuard::new(); + function() + } +} + +pub struct InterruptGuard; + +impl InterruptGuard { + fn new() -> InterruptGuard { + InterruptHandler::instance().block(); + InterruptGuard + } +} + +impl Drop for InterruptGuard { + fn drop(&mut self) { + InterruptHandler::instance().unblock(); + } +} diff --git a/src/justfile.rs b/src/justfile.rs index 5c47d68..fba8e09 100644 --- a/src/justfile.rs +++ b/src/justfile.rs @@ -1,16 +1,17 @@ -use std::path::PathBuf; - use common::*; use edit_distance::edit_distance; pub struct Justfile<'a> { - pub recipes: Map<&'a str, Recipe<'a>>, + pub recipes: Map<&'a str, Recipe<'a>>, pub assignments: Map<&'a str, Expression<'a>>, - pub exports: Set<&'a str>, + pub exports: Set<&'a str>, } -impl<'a, 'b> Justfile<'a> where 'a: 'b { +impl<'a, 'b> Justfile<'a> +where + 'a: 'b, +{ pub fn first(&self) -> Option<&Recipe> { let mut first: Option<&Recipe> = None; for recipe in self.recipes.values() { @@ -30,13 +31,15 @@ impl<'a, 'b> Justfile<'a> where 'a: 'b { } pub fn suggest(&self, name: &str) -> Option<&'a str> { - let mut suggestions = self.recipes.keys() + let mut suggestions = self + .recipes + .keys() .map(|suggestion| (edit_distance(suggestion, name), suggestion)) .collect::>(); suggestions.sort(); if let Some(&(distance, suggestion)) = suggestions.first() { if distance < 3 { - return Some(suggestion) + return Some(suggestion); } } None @@ -45,15 +48,20 @@ impl<'a, 'b> Justfile<'a> where 'a: 'b { pub fn run( &'a self, invocation_directory: Result, - arguments: &[&'a str], + arguments: &[&'a str], configuration: &Configuration<'a>, ) -> RunResult<'a, ()> { - let unknown_overrides = configuration.overrides.keys().cloned() + let unknown_overrides = configuration + .overrides + .keys() + .cloned() .filter(|name| !self.assignments.contains_key(name)) .collect::>(); if !unknown_overrides.is_empty() { - return Err(RuntimeError::UnknownOverrides{overrides: unknown_overrides}); + return Err(RuntimeError::UnknownOverrides { + overrides: unknown_overrides, + }); } let dotenv = load_dotenv()?; @@ -82,7 +90,7 @@ impl<'a, 'b> Justfile<'a> where 'a: 'b { let mut missing = vec![]; let mut grouped = vec![]; - let mut rest = arguments; + let mut rest = arguments; while let Some((argument, mut tail)) = rest.split_first() { if let Some(recipe) = self.recipes.get(argument) { @@ -94,9 +102,9 @@ impl<'a, 'b> Justfile<'a> where 'a: 'b { if !argument_range.range_contains(argument_count) { return Err(RuntimeError::ArgumentCountMismatch { recipe: recipe.name, - found: tail.len(), - min: recipe.min_arguments(), - max: recipe.max_arguments(), + found: tail.len(), + min: recipe.min_arguments(), + max: recipe.max_arguments(), }); } grouped.push((recipe, &tail[0..argument_count])); @@ -114,12 +122,23 @@ impl<'a, 'b> Justfile<'a> where 'a: 'b { } else { None }; - return Err(RuntimeError::UnknownRecipes{recipes: missing, suggestion}); + return Err(RuntimeError::UnknownRecipes { + recipes: missing, + suggestion, + }); } let mut ran = empty(); for (recipe, arguments) in grouped { - self.run_recipe(&invocation_directory, recipe, arguments, &scope, &dotenv, configuration, &mut ran)? + self.run_recipe( + &invocation_directory, + recipe, + arguments, + &scope, + &dotenv, + configuration, + &mut ran, + )? } Ok(()) @@ -128,19 +147,34 @@ impl<'a, 'b> Justfile<'a> where 'a: 'b { fn run_recipe<'c>( &'c self, invocation_directory: &Result, - recipe: &Recipe<'a>, - arguments: &[&'a str], - scope: &Map<&'c str, String>, - dotenv: &Map, + recipe: &Recipe<'a>, + arguments: &[&'a str], + scope: &Map<&'c str, String>, + dotenv: &Map, configuration: &Configuration<'a>, - ran: &mut Set<&'a str>, + ran: &mut Set<&'a str>, ) -> RunResult<()> { for dependency_name in &recipe.dependencies { if !ran.contains(dependency_name) { - self.run_recipe(invocation_directory, &self.recipes[dependency_name], &[], scope, dotenv, configuration, ran)?; + self.run_recipe( + invocation_directory, + &self.recipes[dependency_name], + &[], + scope, + dotenv, + configuration, + ran, + )?; } } - recipe.run(invocation_directory, arguments, scope, dotenv, &self.exports, configuration)?; + recipe.run( + invocation_directory, + arguments, + scope, + dotenv, + &self.exports, + configuration, + )?; ran.insert(recipe.name); Ok(()) } @@ -182,8 +216,14 @@ mod test { #[test] fn unknown_recipes() { - match parse_success("a:\nb:\nc:").run(no_cwd_err(), &["a", "x", "y", "z"], &Default::default()).unwrap_err() { - UnknownRecipes{recipes, suggestion} => { + match parse_success("a:\nb:\nc:") + .run(no_cwd_err(), &["a", "x", "y", "z"], &Default::default()) + .unwrap_err() + { + UnknownRecipes { + recipes, + suggestion, + } => { assert_eq!(recipes, &["x", "y", "z"]); assert_eq!(suggestion, None); } @@ -191,7 +231,6 @@ mod test { } } - #[test] fn run_shebang() { // this test exists to make sure that shebang recipes @@ -210,12 +249,19 @@ a: x "; - match parse_success(text).run(no_cwd_err(), &["a"], &Default::default()).unwrap_err() { - Code{recipe, line_number, code} => { + match parse_success(text) + .run(no_cwd_err(), &["a"], &Default::default()) + .unwrap_err() + { + Code { + recipe, + line_number, + code, + } => { assert_eq!(recipe, "a"); assert_eq!(code, 200); assert_eq!(line_number, None); - }, + } other => panic!("expected a code run error, but got: {}", other), } } @@ -223,12 +269,18 @@ a: #[test] fn code_error() { match parse_success("fail:\n @exit 100") - .run(no_cwd_err(), &["fail"], &Default::default()).unwrap_err() { - Code{recipe, line_number, code} => { + .run(no_cwd_err(), &["fail"], &Default::default()) + .unwrap_err() + { + Code { + recipe, + line_number, + code, + } => { assert_eq!(recipe, "fail"); assert_eq!(code, 100); assert_eq!(line_number, Some(2)); - }, + } other => panic!("expected a code run error, but got: {}", other), } } @@ -239,38 +291,61 @@ a: a return code: @x() { {{return}} {{code + "0"}}; }; x"#; - match parse_success(text).run(no_cwd_err(), &["a", "return", "15"], &Default::default()).unwrap_err() { - Code{recipe, line_number, code} => { + match parse_success(text) + .run(no_cwd_err(), &["a", "return", "15"], &Default::default()) + .unwrap_err() + { + Code { + recipe, + line_number, + code, + } => { assert_eq!(recipe, "a"); assert_eq!(code, 150); assert_eq!(line_number, Some(3)); - }, + } other => panic!("expected a code run error, but got: {}", other), } } #[test] fn missing_some_arguments() { - match parse_success("a b c d:").run(no_cwd_err(), &["a", "b", "c"], &Default::default()).unwrap_err() { - ArgumentCountMismatch{recipe, found, min, max} => { + match parse_success("a b c d:") + .run(no_cwd_err(), &["a", "b", "c"], &Default::default()) + .unwrap_err() + { + ArgumentCountMismatch { + recipe, + found, + min, + max, + } => { assert_eq!(recipe, "a"); assert_eq!(found, 2); assert_eq!(min, 3); assert_eq!(max, 3); - }, + } other => panic!("expected a code run error, but got: {}", other), } } #[test] fn missing_some_arguments_variadic() { - match parse_success("a b c +d:").run(no_cwd_err(), &["a", "B", "C"], &Default::default()).unwrap_err() { - ArgumentCountMismatch{recipe, found, min, max} => { + match parse_success("a b c +d:") + .run(no_cwd_err(), &["a", "B", "C"], &Default::default()) + .unwrap_err() + { + ArgumentCountMismatch { + recipe, + found, + min, + max, + } => { assert_eq!(recipe, "a"); assert_eq!(found, 2); assert_eq!(min, 3); assert_eq!(max, usize::MAX - 1); - }, + } other => panic!("expected a code run error, but got: {}", other), } } @@ -278,39 +353,62 @@ a return code: #[test] fn missing_all_arguments() { match parse_success("a b c d:\n echo {{b}}{{c}}{{d}}") - .run(no_cwd_err(), &["a"], &Default::default()).unwrap_err() { - ArgumentCountMismatch{recipe, found, min, max} => { + .run(no_cwd_err(), &["a"], &Default::default()) + .unwrap_err() + { + ArgumentCountMismatch { + recipe, + found, + min, + max, + } => { assert_eq!(recipe, "a"); assert_eq!(found, 0); assert_eq!(min, 3); assert_eq!(max, 3); - }, + } other => panic!("expected a code run error, but got: {}", other), } } #[test] fn missing_some_defaults() { - match parse_success("a b c d='hello':").run(no_cwd_err(), &["a", "b"], &Default::default()).unwrap_err() { - ArgumentCountMismatch{recipe, found, min, max} => { + match parse_success("a b c d='hello':") + .run(no_cwd_err(), &["a", "b"], &Default::default()) + .unwrap_err() + { + ArgumentCountMismatch { + recipe, + found, + min, + max, + } => { assert_eq!(recipe, "a"); assert_eq!(found, 1); assert_eq!(min, 2); assert_eq!(max, 3); - }, + } other => panic!("expected a code run error, but got: {}", other), } } #[test] fn missing_all_defaults() { - match parse_success("a b c='r' d='h':").run(no_cwd_err(), &["a"], &Default::default()).unwrap_err() { - ArgumentCountMismatch{recipe, found, min, max} => { + match parse_success("a b c='r' d='h':") + .run(no_cwd_err(), &["a"], &Default::default()) + .unwrap_err() + { + ArgumentCountMismatch { + recipe, + found, + min, + max, + } => { assert_eq!(recipe, "a"); assert_eq!(found, 0); assert_eq!(min, 1); assert_eq!(max, 3); - }, + } other => panic!("expected a code run error, but got: {}", other), } } @@ -321,10 +419,12 @@ a return code: configuration.overrides.insert("foo", "bar"); configuration.overrides.insert("baz", "bob"); match parse_success("a:\n echo {{`f() { return 100; }; f`}}") - .run(no_cwd_err(), &["a"], &configuration).unwrap_err() { - UnknownOverrides{overrides} => { + .run(no_cwd_err(), &["a"], &configuration) + .unwrap_err() + { + UnknownOverrides { overrides } => { assert_eq!(overrides, &["baz", "foo"]); - }, + } other => panic!("expected a code run error, but got: {}", other), } } @@ -346,11 +446,18 @@ wut: ..Default::default() }; - match parse_success(text).run(no_cwd_err(), &["wut"], &configuration).unwrap_err() { - Code{code: _, line_number, recipe} => { + match parse_success(text) + .run(no_cwd_err(), &["wut"], &configuration) + .unwrap_err() + { + Code { + code: _, + line_number, + recipe, + } => { assert_eq!(recipe, "wut"); assert_eq!(line_number, Some(8)); - }, + } other => panic!("expected a recipe code errror, but got: {}", other), } } diff --git a/src/main.rs b/src/main.rs index b511615..43fe460 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,13 @@ -#[macro_use] -extern crate lazy_static; +#[macro_use] extern crate lazy_static; +#[macro_use] extern crate log; + extern crate ansi_term; extern crate brev; extern crate clap; +extern crate ctrlc; extern crate dotenv; extern crate edit_distance; +extern crate env_logger; extern crate itertools; extern crate libc; extern crate regex; @@ -16,6 +19,9 @@ extern crate unicode_width; #[macro_use] mod testing; +#[macro_use] +mod die; + mod assignment_evaluator; mod assignment_resolver; mod color; @@ -41,6 +47,7 @@ mod run; mod runtime_error; mod shebang; mod token; +mod interrupt_handler; use common::*; diff --git a/src/recipe.rs b/src/recipe.rs index 66e6440..83cbe19 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -1,6 +1,5 @@ use common::*; -use std::path::PathBuf; use std::process::{ExitStatus, Command, Stdio}; use platform::{Platform, PlatformInterface}; @@ -161,7 +160,7 @@ impl<'a> Recipe<'a> { command.export_environment_variables(scope, dotenv, exports)?; // run it! - match command.status() { + match InterruptHandler::guard(|| command.status()) { Ok(exit_status) => if let Some(code) = exit_status.code() { if code != 0 { return Err(RuntimeError::Code{recipe: self.name, line_number: None, code}) @@ -235,7 +234,7 @@ impl<'a> Recipe<'a> { cmd.export_environment_variables(scope, dotenv, exports)?; - match cmd.status() { + match InterruptHandler::guard(|| cmd.status()) { Ok(exit_status) => if let Some(code) = exit_status.code() { if code != 0 { return Err(RuntimeError::Code{ diff --git a/src/run.rs b/src/run.rs index be37f74..6090e7b 100644 --- a/src/run.rs +++ b/src/run.rs @@ -1,22 +1,16 @@ use common::*; -use std::{convert, ffi, cmp}; +use std::{convert, ffi}; use clap::{App, Arg, ArgGroup, AppSettings}; use configuration::DEFAULT_SHELL; use misc::maybe_s; use unicode_width::UnicodeWidthStr; +use env_logger; +use interrupt_handler::InterruptHandler; #[cfg(windows)] use ansi_term::enable_ansi_support; -macro_rules! die { - ($($arg:tt)*) => {{ - extern crate std; - eprintln!($($arg)*); - process::exit(EXIT_FAILURE) - }}; -} - fn edit>(path: P) -> ! { let editor = env::var_os("EDITOR") .unwrap_or_else(|| die!("Error getting EDITOR environment variable")); @@ -47,6 +41,10 @@ pub fn run() { #[cfg(windows)] enable_ansi_support().ok(); + env_logger::Builder::from_env( + env_logger::Env::new().filter("JUST_LOG").write_style("JUST_LOG_STYLE") + ).init(); + let invocation_directory = env::current_dir() .map_err(|e| format!("Error getting current directory: {}", e)); @@ -357,6 +355,10 @@ pub fn run() { overrides, }; + if let Err(error) = InterruptHandler::install() { + warn!("Failed to set CTRL-C handler: {}", error) + } + if let Err(run_error) = justfile.run( invocation_directory, &arguments, diff --git a/tests/interrupts.rs b/tests/interrupts.rs new file mode 100644 index 0000000..e3ce15a --- /dev/null +++ b/tests/interrupts.rs @@ -0,0 +1,81 @@ +extern crate brev; +extern crate executable_path; +extern crate libc; +extern crate tempdir; + +use executable_path::executable_path; +use tempdir::TempDir; +use std::{process::Command, time::{Duration, Instant}, thread}; + +#[cfg(unix)] +fn kill(process_id: u32) { + unsafe { + libc::kill(process_id as i32, libc::SIGINT); + } +} + +#[cfg(unix)] +fn interrupt_test(justfile: &str) { + let tmp = TempDir::new("just-interrupts") + .unwrap_or_else( + |err| panic!("integration test: failed to create temporary directory: {}", err)); + + let mut justfile_path = tmp.path().to_path_buf(); + justfile_path.push("justfile"); + brev::dump(justfile_path, justfile); + + let start = Instant::now(); + + let mut child = Command::new(&executable_path("just")) + .current_dir(&tmp) + .spawn() + .expect("just invocation failed"); + + thread::sleep(Duration::new(1, 0)); + + kill(child.id()); + + let status = child.wait().unwrap(); + + let elapsed = start.elapsed(); + + if elapsed > Duration::new(2, 500_000_000) { + panic!("process returned too late: {:?}", elapsed); + } + + if elapsed < Duration::new(1, 500_000_000) { + panic!("process returned too early : {:?}", elapsed); + } + + assert_eq!(status.code(), Some(130)); +} + +#[cfg(unix)] +#[test] +fn interrupt_shebang() { + interrupt_test(" +default: + #!/usr/bin/env sh + sleep 2 +"); +} + +#[cfg(unix)] +#[test] +fn interrupt_line() { + interrupt_test(" +default: + @sleep 2 +"); +} + +#[cfg(unix)] +#[test] +fn interrupt_backtick() { + interrupt_test(" +foo = `sleep 2` + +default: + @echo hello +"); +} diff --git a/tests/invocation_directory.rs b/tests/invocation_directory.rs index 2cf72a9..7a968c6 100644 --- a/tests/invocation_directory.rs +++ b/tests/invocation_directory.rs @@ -1,6 +1,5 @@ extern crate brev; extern crate executable_path; -extern crate libc; extern crate target; extern crate tempdir;