This commit is contained in:
2025-06-21 14:34:44 +02:00
parent 7b7ebb8c0e
commit 470089ae5b
15 changed files with 334 additions and 27 deletions

9
xtask/Cargo.toml Normal file
View File

@@ -0,0 +1,9 @@
[package]
name = "xtask"
version = "0.1.0"
edition = "2024"
[dependencies]
clap.workspace = true
tokio.workspace = true
tokio-util.workspace = true

16
xtask/src/args.rs Normal file
View File

@@ -0,0 +1,16 @@
use clap::*;
#[derive(Parser)]
#[command(name = "fcb-looper")]
#[command(version, about = "xtasks for the fcb-looper")]
pub struct Args {
#[command(subcommand)]
pub command: Option<Command>,
}
#[derive(Subcommand)]
pub enum Command {
Run,
Collect,
}

24
xtask/src/collect.rs Normal file
View File

@@ -0,0 +1,24 @@
use tokio::process::Command;
pub async fn collect() {
collect_dir_output("audio_engine", "../collect_audio_engine.txt").await;
collect_dir_output("gui", "../collect_gui.txt").await;
collect_dir_output("osc", "../collect_osc.txt").await;
collect_dir_output("simulator", "../collect_simulator.txt").await;
collect_dir_output("xtask", "../collect_xtask.txt").await;
}
pub async fn collect_dir_output(dir: &str, output: &str) {
#[cfg(target_os = "windows")]
let bash = "C:\\Program Files\\Git\\git-bash.exe";
#[cfg(target_os = "linux")]
let bash = "bash";
Command::new(bash)
.current_dir(dir)
.arg("-c")
.arg(format!("../collect.sh -s core -o {output}"))
.output()
.await
.expect("Failed to collect audio_engine sources");
}

22
xtask/src/main.rs Normal file
View File

@@ -0,0 +1,22 @@
mod args;
mod collect;
mod run;
mod workspace;
use clap::Parser;
#[tokio::main]
async fn main() {
let args = args::Args::parse();
workspace::change_to_workspace_dir().await.expect("Failed to change to workspace directory");
match args.command {
Some(args::Command::Run) | None => {
run::run().await;
}
Some(args::Command::Collect) => {
collect::collect().await;
}
}
}

176
xtask/src/run.rs Normal file
View File

@@ -0,0 +1,176 @@
use std::sync::Arc;
use tokio::process::{Child, Command};
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
type ProcessHandle = Arc<Mutex<Option<Child>>>;
pub async fn run() {
let qjackctl_handle: ProcessHandle = Arc::new(Mutex::new(None));
let audio_engine_handle: ProcessHandle = Arc::new(Mutex::new(None));
let gui_handle: ProcessHandle = Arc::new(Mutex::new(None));
let simulator_handle: ProcessHandle = Arc::new(Mutex::new(None));
let cancel_token = CancellationToken::new();
// Set up signal handling for graceful shutdown
let cleanup_handles = vec![
qjackctl_handle.clone(),
audio_engine_handle.clone(),
gui_handle.clone(),
simulator_handle.clone(),
];
let cleanup_token = cancel_token.clone();
tokio::spawn(async move {
tokio::signal::ctrl_c().await.expect("Failed to listen for ctrl+c");
println!("Received Ctrl+C, shutting down...");
cleanup_token.cancel();
cleanup_processes(cleanup_handles).await;
stop_jack_daemon().await;
});
// Start processes
let qjackctl = spawn_qjackctl().await;
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
let audio_engine = spawn_audio_engine().await;
let gui = spawn_gui().await;
let simulator = spawn_simulator().await;
// Store handles for cleanup
*qjackctl_handle.lock().await = Some(qjackctl);
*audio_engine_handle.lock().await = Some(audio_engine);
*gui_handle.lock().await = Some(gui);
*simulator_handle.lock().await = Some(simulator);
// Get references back for waiting
let mut qjackctl = qjackctl_handle.lock().await.take().unwrap();
let mut audio_engine = audio_engine_handle.lock().await.take().unwrap();
let mut gui = gui_handle.lock().await.take().unwrap();
let mut simulator = simulator_handle.lock().await.take().unwrap();
// Wait for any process to exit or cancellation
tokio::select! {
result = qjackctl.wait() => {
println!("qjackctl exited: {:?}", result);
kill_process(&mut audio_engine, "audio_engine").await;
kill_process(&mut gui, "gui").await;
kill_process(&mut simulator, "simulator").await;
stop_jack_daemon().await;
}
result = audio_engine.wait() => {
println!("audio_engine exited: {:?}", result);
kill_process(&mut qjackctl, "qjackctl").await;
kill_process(&mut gui, "gui").await;
kill_process(&mut simulator, "simulator").await;
stop_jack_daemon().await;
}
result = gui.wait() => {
println!("gui exited: {:?}", result);
kill_process(&mut qjackctl, "qjackctl").await;
kill_process(&mut audio_engine, "audio_engine").await;
kill_process(&mut simulator, "simulator").await;
stop_jack_daemon().await;
}
result = simulator.wait() => {
println!("simulator exited: {:?}", result);
kill_process(&mut qjackctl, "qjackctl").await;
kill_process(&mut audio_engine, "audio_engine").await;
kill_process(&mut gui, "gui").await;
stop_jack_daemon().await;
}
_ = cancel_token.cancelled() => {
println!("Shutdown requested");
kill_process(&mut qjackctl, "qjackctl").await;
kill_process(&mut audio_engine, "audio_engine").await;
kill_process(&mut gui, "gui").await;
kill_process(&mut simulator, "simulator").await;
stop_jack_daemon().await;
}
}
println!("All processes stopped");
}
async fn cleanup_processes(handles: Vec<ProcessHandle>) {
for handle in handles {
if let Some(mut child) = handle.lock().await.take() {
let _ = child.kill().await;
}
}
}
async fn stop_jack_daemon() {
println!("Stopping JACK daemon...");
#[cfg(target_os = "windows")]
let (jack, args) = ("taskkill", vec!["/f", "/im", "jackd.exe"]);
#[cfg(target_os = "linux")]
let (jack, args) = ("pkill", vec!["-f", "jackd"]);
Command::new(jack).args(args).output().await.expect("Failed to stop JACK daemon");
}
async fn spawn_qjackctl() -> Child {
#[cfg(target_os = "windows")]
let qjackctl = "C:\\Program Files\\JACK2\\qjackctl\\qjackctl.exe";
#[cfg(target_os = "linux")]
let qjackctl = "qjackctl";
#[cfg(target_os = "windows")]
let qjackctl_arg = "/start-server";
#[cfg(target_os = "linux")]
let qjackctl_arg = "--start-server";
Command::new(qjackctl)
.arg(qjackctl_arg)
.spawn()
.expect("Could not start qjackctl")
}
async fn spawn_audio_engine() -> Child {
Command::new("cargo")
.args([
"run",
"--release",
"--bin",
"audio_engine",
"--",
"--config-path",
"config",
])
.spawn()
.expect("Could not start audio engine")
}
async fn spawn_gui() -> Child {
Command::new("cargo")
.args([
"run",
"--release",
"--bin",
"gui",
])
.spawn()
.expect("Could not start gui")
}
async fn spawn_simulator() -> Child {
Command::new("cargo")
.args([
"run",
"--release",
"--bin",
"simulator",
])
.spawn()
.expect("Could not start simulator")
}
async fn kill_process(child: &mut Child, name: &str) {
match child.kill().await {
Ok(()) => println!("Killed {}", name),
Err(e) => eprintln!("Failed to kill {}: {}", name, e),
}
}

31
xtask/src/workspace.rs Normal file
View File

@@ -0,0 +1,31 @@
use std::path::PathBuf;
use tokio::process::Command;
pub async fn change_to_workspace_dir() -> Result<(), Box<dyn std::error::Error>> {
// Use cargo to find the workspace root
let output = Command::new("cargo")
.args(["locate-project", "--workspace", "--message-format=plain"])
.output()
.await?;
if !output.status.success() {
return Err(format!(
"Failed to locate workspace: {}",
String::from_utf8_lossy(&output.stderr)
).into());
}
let workspace_cargo_toml = String::from_utf8(output.stdout)?;
let workspace_cargo_toml = workspace_cargo_toml.trim();
// Get the directory containing Cargo.toml
let workspace_dir = PathBuf::from(workspace_cargo_toml)
.parent()
.ok_or("Failed to get workspace directory")?
.to_path_buf();
println!("Changing to workspace directory: {}", workspace_dir.display());
std::env::set_current_dir(&workspace_dir)?;
Ok(())
}