mirror of
https://github.com/rust-lang/rustlings.git
synced 2026-08-14 22:36:57 +00:00
Add command timeout
This commit is contained in:
parent
b970e17bdf
commit
a5765fcb96
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -441,6 +441,7 @@ dependencies = [
|
|||||||
"shlex",
|
"shlex",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"toml",
|
"toml",
|
||||||
|
"wait-timeout",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -643,6 +644,15 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
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]]
|
[[package]]
|
||||||
name = "walkdir"
|
name = "walkdir"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
|||||||
@ -54,6 +54,7 @@ serde_json = "1"
|
|||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
shlex = "1"
|
shlex = "1"
|
||||||
toml.workspace = true
|
toml.workspace = true
|
||||||
|
wait-timeout = "0.2"
|
||||||
|
|
||||||
[target.'cfg(not(windows))'.dependencies]
|
[target.'cfg(not(windows))'.dependencies]
|
||||||
rustix = { version = "1.0", default-features = false, features = ["std", "stdio", "termios"] }
|
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 this sender to detect when the last thread is done.
|
||||||
drop(progress_sender);
|
drop(progress_sender);
|
||||||
|
|
||||||
// TODO: Timeout
|
|
||||||
while let Ok((exercise_ind, progress)) = progress_receiver.recv() {
|
while let Ok((exercise_ind, progress)) = progress_receiver.recv() {
|
||||||
let name = self.exercises[exercise_ind].name;
|
let name = self.exercises[exercise_ind].name;
|
||||||
match progress {
|
match progress {
|
||||||
|
|||||||
71
src/cmd.rs
71
src/cmd.rs
@ -3,47 +3,73 @@ use serde::Deserialize;
|
|||||||
use std::{
|
use std::{
|
||||||
io::{Read, pipe},
|
io::{Read, pipe},
|
||||||
path::PathBuf,
|
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.
|
/// 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.
|
/// 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> {
|
fn run_cmd(mut cmd: Command, description: &str, output: Option<&mut Vec<u8>>) -> Result<bool> {
|
||||||
let spawn = |mut cmd: Command| {
|
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())
|
cmd.stdin(Stdio::null())
|
||||||
.spawn()
|
.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 handle = if let Some(output) = output {
|
||||||
let (mut reader, writer) = pipe().with_context(|| {
|
let (mut reader, writer) =
|
||||||
format!("Failed to create a pipe to run the command `{description}``")
|
pipe().with_context(|| format!("Failed to create a pipe to run `{description}``"))?;
|
||||||
})?;
|
|
||||||
|
|
||||||
let writer_clone = writer.try_clone().with_context(|| {
|
let writer_clone = writer
|
||||||
format!("Failed to clone the pipe writer for the command `{description}`")
|
.try_clone()
|
||||||
})?;
|
.with_context(|| format!("Failed to clone the pipe writer for `{description}`"))?;
|
||||||
|
|
||||||
cmd.stdout(writer_clone).stderr(writer);
|
cmd.stdout(writer_clone).stderr(writer);
|
||||||
let handle = spawn(cmd)?;
|
let mut handle = spawn(cmd)?;
|
||||||
|
|
||||||
reader
|
let thread_handle = thread::Builder::new()
|
||||||
.read_to_end(output)
|
.spawn(move || {
|
||||||
.with_context(|| format!("Failed to read the output of the command `{description}`"))?;
|
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")?;
|
||||||
|
|
||||||
|
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');
|
output.push(b'\n');
|
||||||
|
return Ok(status.success());
|
||||||
|
}
|
||||||
|
|
||||||
handle
|
handle
|
||||||
} else {
|
} else {
|
||||||
cmd.stdout(Stdio::null()).stderr(Stdio::null());
|
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
|
handle
|
||||||
.wait()
|
.kill()
|
||||||
.with_context(|| format!("Failed to wait on the command `{description}` to exit"))
|
.with_context(|| format!("Failed to kill `{description}` after timeout"))?;
|
||||||
.map(|status| status.success())
|
bail!("`{description}` timed out after {TIMEOUT_SECS} seconds");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parses parts of the output of `cargo metadata`.
|
// Parses parts of the output of `cargo metadata`.
|
||||||
@ -71,12 +97,11 @@ impl CmdRunner {
|
|||||||
.context(CARGO_METADATA_ERR)?;
|
.context(CARGO_METADATA_ERR)?;
|
||||||
|
|
||||||
if !metadata_output.status.success() {
|
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)
|
let metadata: CargoMetadata = serde_json::de::from_slice(&metadata_output.stdout).context(
|
||||||
.context(
|
"Failed to read the field `target_directory` from the output of `cargo metadata …`",
|
||||||
"Failed to read the field `target_directory` from the output of the command `cargo metadata …`",
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
@ -116,7 +141,7 @@ impl CmdRunner {
|
|||||||
bin_path.push("debug");
|
bin_path.push("debug");
|
||||||
bin_path.push(bin_name);
|
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?
|
Did you already install Rust?
|
||||||
Try running `cargo --version` to diagnose the problem.";
|
Try running `cargo --version` to diagnose the problem.";
|
||||||
|
|
||||||
|
|||||||
@ -37,7 +37,7 @@ pub fn init() -> Result<()> {
|
|||||||
.stderr(Stdio::null())
|
.stderr(Stdio::null())
|
||||||
.output()
|
.output()
|
||||||
.context(
|
.context(
|
||||||
"Failed to run the command `cargo locate-project …`\n\
|
"Failed to run `cargo locate-project …`\n\
|
||||||
Did you already install Rust?\n\
|
Did you already install Rust?\n\
|
||||||
Try running `cargo --version` to diagnose the problem.",
|
Try running `cargo --version` to diagnose the problem.",
|
||||||
)?;
|
)?;
|
||||||
@ -49,7 +49,7 @@ pub fn init() -> Result<()> {
|
|||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::null())
|
.stderr(Stdio::null())
|
||||||
.status()
|
.status()
|
||||||
.context("Failed to run the command `cargo clippy --version`")?
|
.context("Failed to run `cargo clippy --version`")?
|
||||||
.success()
|
.success()
|
||||||
{
|
{
|
||||||
bail!(
|
bail!(
|
||||||
|
|||||||
@ -150,7 +150,7 @@ pub fn watch(
|
|||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
notify_exercise_names: Option<&'static [&'static [u8]]>,
|
notify_exercise_names: Option<&'static [&'static [u8]]>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// TODO: Use cfg_select! after bumping MSRV to at least 1.95
|
// TODO: Use cfg_select! after MSRV 1.95
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
{
|
{
|
||||||
let stdin_fd = rustix::stdio::stdin();
|
let stdin_fd = rustix::stdio::stdin();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user