Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
75 changes: 30 additions & 45 deletions src/app_state.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use anyhow::{Context, Error, Result, bail};
use crossterm::{QueueableCommand, cursor, terminal};
use crossterm::{QueueableCommand, cursor};
use std::{
collections::HashSet,
fs::{File, OpenOptions},
Expand Down Expand Up @@ -43,8 +43,6 @@ pub enum StateFileStatus {

#[derive(Clone, Copy)]
pub enum CheckProgress {
None,
Checking,
Done,
Pending,
}
Expand Down Expand Up @@ -398,22 +396,18 @@ impl AppState {
}

fn check_all_exercises_impl(&mut self, stdout: &mut StdoutLock) -> Result<Option<usize>> {
let term_width = terminal::size()
.context("Failed to get the terminal size")?
.0;
let mut progress_visualizer = CheckProgressVisualizer::build(stdout, term_width)?;
let mut progress_visualizer = CheckProgressVisualizer::build(stdout, self.exercises.len())?;

let next_exercise_ind = AtomicUsize::new(0);
let mut progresses = vec![CheckProgress::None; self.exercises.len()];
let next_exercise_ind = &AtomicUsize::new(0);
let mut progresses = vec![None; self.exercises.len()];

thread::scope(|s| {
let (exercise_progress_sender, exercise_progress_receiver) = mpsc::channel();
let (progress_sender, progress_receiver) = mpsc::channel();
let n_threads = thread::available_parallelism()
.map_or(DEFAULT_CHECK_PARALLELISM, |count| count.get());

for _ in 0..n_threads {
let exercise_progress_sender = exercise_progress_sender.clone();
let next_exercise_ind = &next_exercise_ind;
let progress_sender = progress_sender.clone();
let slf = &self;
thread::Builder::new()
.spawn_scoped(s, move || {
Expand All @@ -424,73 +418,64 @@ impl AppState {
break;
};

if exercise_progress_sender
.send((exercise_ind, CheckProgress::Checking))
.is_err()
{
break;
}

let success = exercise.run_exercise(None, &slf.cmd_runner);
let progress = match success {
Ok(true) => CheckProgress::Done,
Ok(false) => CheckProgress::Pending,
Err(_) => CheckProgress::None,
};
if let Ok(success) = exercise.run_exercise(None, &slf.cmd_runner) {
let progress = if success {
CheckProgress::Done
} else {
CheckProgress::Pending
};

if exercise_progress_sender
.send((exercise_ind, progress))
.is_err()
{
break;
if progress_sender.send((exercise_ind, progress)).is_err() {
break;
}
}
}
})
.context("Failed to spawn a thread to check all exercises")?;
}

// Drop this sender to detect when the last thread is done.
drop(exercise_progress_sender);
drop(progress_sender);

while let Ok((exercise_ind, progress)) = exercise_progress_receiver.recv() {
progresses[exercise_ind] = progress;
progress_visualizer.update(&progresses)?;
while let Ok((exercise_ind, progress)) = progress_receiver.recv() {
let name = self.exercises[exercise_ind].name;
match progress {
CheckProgress::Done => progress_visualizer.done(name)?,
CheckProgress::Pending => progress_visualizer.pending(name)?,
}
progresses[exercise_ind] = Some(progress);
}

Ok::<_, Error>(())
})?;

let mut first_pending_exercise_ind = None;
for exercise_ind in 0..progresses.len() {
match progresses[exercise_ind] {
CheckProgress::Done => {
for (exercise_ind, progress) in progresses.into_iter().enumerate() {
match progress {
Some(CheckProgress::Done) => {
self.set_status(exercise_ind, true)?;
}
CheckProgress::Pending => {
Some(CheckProgress::Pending) => {
self.set_status(exercise_ind, false)?;
if first_pending_exercise_ind.is_none() {
first_pending_exercise_ind = Some(exercise_ind);
}
}
CheckProgress::None | CheckProgress::Checking => {
None => {
// If we got an error while checking all exercises in parallel,
// it could be because we exceeded the limit of open file descriptors.
// Therefore, try running exercises with errors sequentially.
progresses[exercise_ind] = CheckProgress::Checking;
progress_visualizer.update(&progresses)?;

let exercise = &self.exercises[exercise_ind];
let success = exercise.run_exercise(None, &self.cmd_runner)?;
if success {
progresses[exercise_ind] = CheckProgress::Done;
progress_visualizer.done(exercise.name)?;
} else {
progresses[exercise_ind] = CheckProgress::Pending;
progress_visualizer.pending(exercise.name)?;
if first_pending_exercise_ind.is_none() {
first_pending_exercise_ind = Some(exercise_ind);
}
}
self.set_status(exercise_ind, success)?;
progress_visualizer.update(&progresses)?;
}
}
}
Expand Down
79 changes: 52 additions & 27 deletions src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;

reader
.read_to_end(output)
.with_context(|| format!("Failed to read the output of the command `{description}`"))?;

output.push(b'\n');
let mut handle = spawn(cmd)?;

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")?;

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`.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
}

Expand All @@ -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.";

Expand Down
14 changes: 12 additions & 2 deletions src/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
)?;
Expand All @@ -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!(
Expand Down Expand Up @@ -165,6 +165,8 @@ pub fn init() -> Result<()> {
fs::write(".gitignore", GITIGNORE)
.context("Failed to create the file `rustlings/.gitignore`")?;

fs::write("README.md", README).context("Failed to create the file `rustlings/README.md`")?;

create_dir(".vscode").context("Failed to create the directory `rustlings/.vscode`")?;
fs::write(".vscode/extensions.json", VS_CODE_EXTENSIONS_JSON)
.context("Failed to create the file `rustlings/.vscode/extensions.json`")?;
Expand Down Expand Up @@ -220,6 +222,14 @@ target/
.vscode/
";

const README: &[u8] = b"# Rustlings

This is your space to solve Rustlings exercises.
Simply run `rustlings` in this directory to get started!
Learn more about using Rustlings here:
<https://rustlings.rust-lang.org/usage/>
";

pub const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#;

const IN_INITIALIZED_DIR_ERR: &str = "It looks like Rustlings is already initialized in this directory.
Expand Down
Loading