Wait for child processes to finish (#345)

Thanks to @bheisler for the feature request and initial implementation.

Fixes #302
This commit is contained in:
Casey Rodarmor 2018-08-27 16:03:52 -07:00 committed by GitHub
parent c615a3fb0b
commit b14d1ec97c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 465 additions and 80 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
Cargo.lock linguist-generated diff=nodiff

81
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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`

2
rustfmt.toml Normal file
View File

@ -0,0 +1,2 @@
tab_spaces = 2
max_width = 100

View File

@ -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})
)
}
}

View File

@ -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;

7
src/die.rs Normal file
View File

@ -0,0 +1,7 @@
macro_rules! die {
($($arg:tt)*) => {{
extern crate std;
eprintln!($($arg)*);
process::exit(EXIT_FAILURE)
}};
}

View File

@ -1,6 +1,5 @@
use std::path::PathBuf;
use common::*;
use target;
use platform::{Platform, PlatformInterface};

93
src/interrupt_handler.rs Normal file
View File

@ -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<InterruptHandler> = 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, F: FnOnce() -> 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();
}
}

View File

@ -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::<Vec<_>>();
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<PathBuf, String>,
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::<Vec<_>>();
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<PathBuf, String>,
recipe: &Recipe<'a>,
arguments: &[&'a str],
scope: &Map<&'c str, String>,
dotenv: &Map<String, String>,
recipe: &Recipe<'a>,
arguments: &[&'a str],
scope: &Map<&'c str, String>,
dotenv: &Map<String, String>,
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),
}
}

View File

@ -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::*;

View File

@ -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{

View File

@ -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<P: convert::AsRef<ffi::OsStr>>(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,

81
tests/interrupts.rs Normal file
View File

@ -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
");
}

View File

@ -1,6 +1,5 @@
extern crate brev;
extern crate executable_path;
extern crate libc;
extern crate target;
extern crate tempdir;