Skip to content

Commit

Permalink
feat: add parallel mode between independent commands (#4)
Browse files Browse the repository at this point in the history
* feat: add parallel mode

* release: 0.1.4

* doc: update README
  • Loading branch information
guuzaa authored Jan 19, 2025
1 parent 8804d2f commit ef948a7
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 30 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cargo-q"
version = "0.1.3"
version = "0.1.4"
edition = "2021"
description = "A cargo subcommand for running multiple cargo commands in a time"
keywords = ["cargo", "subcommand", "plugin"]
Expand Down
76 changes: 54 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,48 +1,80 @@
# cargo-q

Cargo subcommand to run multiple Cargo commands in a time.
A Cargo subcommand that allows running multiple Cargo commands in a time.

<details>
<summary>TODO</summary>

- ✅ Add sequential execution
- ✅ Add ; as command separator
- ✅ Add & as command separator
- ❌ Add > as command separator
- ❌ Add parallel execution
- ✅ Add ; as command separator for independent commands
- ✅ Add & as command separator for dependent commands
- ✅ Add parallel execution between independent commands
- ❌ Add > as command separator for dependent commands
- ❌ Support mixed separators

</details>

## Installation

```bash
cargo install cargo-q
```

## Features

- Run multiple Cargo commands sequentially
- Use different separators for command execution:
- Space: Run commands sequentially (independent execution)
- `;`: Run independent commands sequentially
- `&`: Run commands with dependencies (each command depends on previous command's success)
- Support parallel execution for independent commands
- Verbose mode for detailed output

## Usage

### Run a command
### Run a Single Command

```bash
cargo q cmd
cargo q check
```

### Run multiple commands
### Run Multiple Commands

#### Sequential Execution (Space Separator)
```bash
# default quiet mode
cargo q "check test" # run `check` first then test whether `check` is successful
cargo q 'check test' # ' and " are the same
cargo q "test --features feature1 ; run" # if a command has dash or parameters, use ; as separator
# Run commands sequentially and independently
cargo q "check test" # Runs check, then test
cargo q 'check test' # Single and double quotes both work
```

cargo q "check & test & run" # run `check` first, then `test` if `check` is successful, and `run` if both are successful
cargo q "check&test&run" # same as above
#### Independent Commands (`;` Separator)
```bash
# Run commands sequentially and independently
cargo q "test --features feature1 ; run" # Commands with parameters need ; separator
```

#### Dependent Commands (`&` Separator)
```bash
# Run commands with explicit dependencies
cargo q "check & test & run" # Each command runs only if previous command succeeds
cargo q "check&test&run" # Spaces around & are optional
```

cargo q "test > analyze" # run `test` first, then `analyze` with `test`'s output
cargo q "test>analyze" # same as above
### Parallel Execution

# verbose mode
cargo q -v "check test" # run `check` first, then `test` if `check` is successful
cargo q --verbose "check test" # same as above
```bash
# Run independent commands in parallel
cargo q -p "build -r; build" # Run both commands in parallel
cargo q --parallel "check; test" # Same as above
```

### Run commands in parallel
### Verbose Output

```bash
cargo q -p "build -r; build" # run `build -r` and `build` in parallel
cargo q --parallel "build -r; build" # same as above
cargo q -v "check test" # Show detailed output
cargo q --verbose "check test" # Same as above
```

## License

Licensed under Apache-2.0 license ([LICENSE](LICENSE) or http://opensource.org/licenses/Apache-2.0)
59 changes: 54 additions & 5 deletions src/executor.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use crate::parser::Strategy;
use crate::process::{ColorExt, ExecutionSummary};
use crate::routine::Routine;
use crate::thread_pool::ThreadPool;
use std::io::{self, Error, ErrorKind};
use std::sync::{Arc, Mutex};

const MAX_THREADS: usize = 8;

pub(crate) struct Executor {
pub(super) parallel: bool,
Expand All @@ -21,13 +25,58 @@ impl Executor {
}

pub fn execute(&self) -> io::Result<()> {
if self.parallel {
return Err(Error::new(
ErrorKind::Unsupported,
"Parallel execution not yet implemented",
));
match (self.parallel, self.strategy) {
(true, Strategy::Independent) => self.execute_parallel(),
(true, _) => Err(Error::new(
ErrorKind::InvalidInput,
"Parallel execution only supports independent commands now",
)),
(false, _) => self.execute_sequential(),
}
}

fn execute_parallel(&self) -> io::Result<()> {
let summary = Arc::new(Mutex::new(ExecutionSummary::new(self.routines.len())));
let total_commands = self.routines.len();
let pool = ThreadPool::new(total_commands.min(MAX_THREADS));

for (idx, cmd) in self.routines.iter().enumerate() {
let summary = Arc::clone(&summary);
let cmd_str = if cmd.args.is_empty() {
cmd.name.clone()
} else {
format!("{} {}", cmd.name, cmd.args.join(" "))
};

println!(
"\n {} {}",
format!("[{}/{}]", idx + 1, total_commands).bold(),
cmd_str
);

let cmd = cmd.clone();
let verbose = self.verbose;

pool.execute(move || match cmd.run(verbose) {
Ok((success, output)) => {
if success {
summary.lock().unwrap().increment_success();
} else if !output.stderr.is_empty() {
eprintln!("error: Command failed");
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
}
}
Err(e) => {
eprintln!("error: Failed to execute command: {}", e);
}
});
}

// Pool will be dropped here, which waits for all jobs to complete
Ok(())
}

fn execute_sequential(&self) -> io::Result<()> {
let total_commands = self.routines.len();
let mut summary = ExecutionSummary::new(total_commands);

Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod executor;
mod parser;
mod process;
mod routine;
mod thread_pool;

use cli::Cli;
use parser::Parser;
Expand Down
2 changes: 1 addition & 1 deletion src/routine.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::io;
use std::process::{Command, Output, Stdio};

#[derive(Debug, Default)]
#[derive(Debug, Default, Clone)]
pub(crate) struct Routine {
pub(crate) name: String,
pub(crate) args: Vec<String>,
Expand Down
76 changes: 76 additions & 0 deletions src/thread_pool.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use std::sync::mpsc::{self, Receiver, Sender};
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};

type Job = Box<dyn FnOnce() + Send + 'static>;

pub struct ThreadPool {
workers: Vec<Worker>,
sender: Option<Sender<Job>>,
}

struct Worker {
_id: usize,
thread: Option<JoinHandle<()>>,
}

impl ThreadPool {
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);

let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);

for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}

ThreadPool {
workers,
sender: Some(sender),
}
}

pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.as_ref().unwrap().send(job).unwrap();
}
}

impl Drop for ThreadPool {
fn drop(&mut self) {
drop(self.sender.take());

for worker in &mut self.workers {
if let Some(thread) = worker.thread.take() {
thread.join().unwrap();
}
}
}
}

impl Worker {
fn new(id: usize, receiver: Arc<Mutex<Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || loop {
let message = receiver.lock().unwrap().recv();

match message {
Ok(job) => {
job();
}
Err(_) => {
break;
}
}
});

Worker {
_id: id,
thread: Some(thread),
}
}
}

0 comments on commit ef948a7

Please sign in to comment.