mirror of
https://github.com/rust-lang/rustlings.git
synced 2026-08-14 14:26:56 +00:00
Add command timeout
This commit is contained in:
parent
62c90241ac
commit
283a508ad9
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -441,6 +441,7 @@ dependencies = [
|
||||
"shlex",
|
||||
"tempfile",
|
||||
"toml",
|
||||
"wait-timeout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -643,6 +644,15 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "wait-timeout"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
|
||||
@ -54,6 +54,7 @@ serde_json = "1"
|
||||
serde.workspace = true
|
||||
shlex = "1"
|
||||
toml.workspace = true
|
||||
wait-timeout = "0.2"
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
rustix = { version = "1.0", default-features = false, features = ["std", "stdio", "termios"] }
|
||||
|
||||
@ -437,7 +437,6 @@ impl AppState {
|
||||
// Drop this sender to detect when the last thread is done.
|
||||
drop(progress_sender);
|
||||
|
||||
// TODO: Timeout
|
||||
while let Ok((exercise_ind, progress)) = progress_receiver.recv() {
|
||||
let name = self.exercises[exercise_ind].name;
|
||||
match progress {
|
||||
|
||||
75
src/cmd.rs
75
src/cmd.rs
@ -3,47 +3,73 @@ use serde::Deserialize;
|
||||
use std::{
|
||||
io::{Read, pipe},
|
||||
path::PathBuf,
|
||||
process::{Command, Stdio},
|
||||
process::{Child, Command, Stdio},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
use wait_timeout::ChildExt;
|
||||
|
||||
const TIMEOUT_SECS: u64 = 30;
|
||||
|
||||
/// Run a command with a description for a possible error and append the merged stdout and stderr.
|
||||
/// The boolean in the returned `Result` is true if the command's exit status is success.
|
||||
fn run_cmd(mut cmd: Command, description: &str, output: Option<&mut Vec<u8>>) -> Result<bool> {
|
||||
let spawn = |mut cmd: Command| {
|
||||
// NOTE: The closure drops `cmd` which prevents a pipe deadlock.
|
||||
// The closure drops `cmd` which prevents a pipe deadlock.
|
||||
cmd.stdin(Stdio::null())
|
||||
.spawn()
|
||||
.with_context(|| format!("Failed to run the command `{description}`"))
|
||||
.with_context(|| format!("Failed to run `{description}`"))
|
||||
};
|
||||
let wait = |handle: &mut Child| {
|
||||
handle
|
||||
.wait_timeout(Duration::from_secs(TIMEOUT_SECS))
|
||||
.with_context(|| format!("Failed to wait on `{description}` to exit"))
|
||||
};
|
||||
|
||||
let mut handle = if let Some(output) = output {
|
||||
let (mut reader, writer) = pipe().with_context(|| {
|
||||
format!("Failed to create a pipe to run the command `{description}``")
|
||||
})?;
|
||||
let (mut reader, writer) =
|
||||
pipe().with_context(|| format!("Failed to create a pipe to run `{description}``"))?;
|
||||
|
||||
let writer_clone = writer.try_clone().with_context(|| {
|
||||
format!("Failed to clone the pipe writer for the command `{description}`")
|
||||
})?;
|
||||
let writer_clone = writer
|
||||
.try_clone()
|
||||
.with_context(|| format!("Failed to clone the pipe writer for `{description}`"))?;
|
||||
|
||||
cmd.stdout(writer_clone).stderr(writer);
|
||||
let handle = spawn(cmd)?;
|
||||
let mut handle = spawn(cmd)?;
|
||||
|
||||
reader
|
||||
.read_to_end(output)
|
||||
.with_context(|| format!("Failed to read the output of the command `{description}`"))?;
|
||||
let thread_handle = thread::Builder::new()
|
||||
.spawn(move || {
|
||||
let mut out = Vec::with_capacity(128);
|
||||
reader.read_to_end(&mut out).map(|_| out)
|
||||
})
|
||||
.context("Failed to spawn a thread to collect a command's output")?;
|
||||
|
||||
output.push(b'\n');
|
||||
if let Some(status) = wait(&mut handle)? {
|
||||
let out = thread_handle
|
||||
.join()
|
||||
.unwrap()
|
||||
.with_context(|| format!("Failed to read the output of `{description}`"))?;
|
||||
output.extend_from_slice(&out);
|
||||
output.push(b'\n');
|
||||
return Ok(status.success());
|
||||
}
|
||||
|
||||
handle
|
||||
} else {
|
||||
cmd.stdout(Stdio::null()).stderr(Stdio::null());
|
||||
spawn(cmd)?
|
||||
let mut handle = spawn(cmd)?;
|
||||
|
||||
if let Some(status) = wait(&mut handle)? {
|
||||
return Ok(status.success());
|
||||
}
|
||||
|
||||
handle
|
||||
};
|
||||
|
||||
handle
|
||||
.wait()
|
||||
.with_context(|| format!("Failed to wait on the command `{description}` to exit"))
|
||||
.map(|status| status.success())
|
||||
.kill()
|
||||
.with_context(|| format!("Failed to kill `{description}` after timeout"))?;
|
||||
bail!("`{description}` timed out after {TIMEOUT_SECS} seconds");
|
||||
}
|
||||
|
||||
// Parses parts of the output of `cargo metadata`.
|
||||
@ -71,13 +97,12 @@ impl CmdRunner {
|
||||
.context(CARGO_METADATA_ERR)?;
|
||||
|
||||
if !metadata_output.status.success() {
|
||||
bail!("The command `cargo metadata …` failed. Are you in the `rustlings/` directory?");
|
||||
bail!("`cargo metadata …` failed. Are you in the `rustlings/` directory?");
|
||||
}
|
||||
|
||||
let metadata: CargoMetadata = serde_json::de::from_slice(&metadata_output.stdout)
|
||||
.context(
|
||||
"Failed to read the field `target_directory` from the output of the command `cargo metadata …`",
|
||||
)?;
|
||||
let metadata: CargoMetadata = serde_json::de::from_slice(&metadata_output.stdout).context(
|
||||
"Failed to read the field `target_directory` from the output of `cargo metadata …`",
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
target_dir: metadata.target_directory,
|
||||
@ -116,7 +141,7 @@ impl CmdRunner {
|
||||
bin_path.push("debug");
|
||||
bin_path.push(bin_name);
|
||||
|
||||
run_cmd(Command::new(&bin_path), &bin_path.to_string_lossy(), output)
|
||||
run_cmd(Command::new(&bin_path), bin_name, output)
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,7 +165,7 @@ impl CargoSubcommand<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
const CARGO_METADATA_ERR: &str = "Failed to run the command `cargo metadata …`
|
||||
const CARGO_METADATA_ERR: &str = "Failed to run `cargo metadata …`
|
||||
Did you already install Rust?
|
||||
Try running `cargo --version` to diagnose the problem.";
|
||||
|
||||
|
||||
@ -37,7 +37,7 @@ pub fn init() -> Result<()> {
|
||||
.stderr(Stdio::null())
|
||||
.output()
|
||||
.context(
|
||||
"Failed to run the command `cargo locate-project …`\n\
|
||||
"Failed to run `cargo locate-project …`\n\
|
||||
Did you already install Rust?\n\
|
||||
Try running `cargo --version` to diagnose the problem.",
|
||||
)?;
|
||||
@ -49,7 +49,7 @@ pub fn init() -> Result<()> {
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.context("Failed to run the command `cargo clippy --version`")?
|
||||
.context("Failed to run `cargo clippy --version`")?
|
||||
.success()
|
||||
{
|
||||
bail!(
|
||||
|
||||
@ -150,7 +150,7 @@ pub fn watch(
|
||||
app_state: &mut AppState,
|
||||
notify_exercise_names: Option<&'static [&'static [u8]]>,
|
||||
) -> Result<()> {
|
||||
// TODO: Use cfg_select! after bumping MSRV to at least 1.95
|
||||
// TODO: Use cfg_select! after MSRV 1.95
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let stdin_fd = rustix::stdio::stdin();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user