From 2c89583135ce7d49dfc0b7f1e05051ed579223e6 Mon Sep 17 00:00:00 2001 From: David Estes <5317198+dav1do@users.noreply.github.com> Date: Wed, 10 Jan 2024 12:45:17 -0700 Subject: [PATCH] feat: ctrl+c/sigint handling and working directory override fix (#27) * feat: Add ctrl+c exit handling and optional force exit * fix: update quiet example in readme * fix: make sure custom directory is absolute --- README.md | 4 +- cli/Cargo.toml | 2 +- cli/src/install/ceramic_daemon.rs | 2 +- cli/src/main.rs | 152 +++++++++++++++++++++--------- 4 files changed, 111 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 4c66ef4..5e518da 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ by option. If you don't want to step through prompts at all, you can use wheel in "quiet" mode, which will default to clay - wheel --working-directory quiet --network --did --pk + wheel --working-directory quiet --network generate -This requires you to have already setup a DID and [CAS Auth](#cas-auth). Please run `wheel --help` for more options. +You can also pass an existing DID and PK via the `specify` option instead of `generate`. This requires you to have already setup a DID and [CAS Auth](#cas-auth). Please run `wheel --help` for more options. ### CAS Auth diff --git a/cli/Cargo.toml b/cli/Cargo.toml index d1f3ac6..220e6e4 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -23,7 +23,7 @@ serde_json.workspace = true spinners = "4.1" sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-rustls", "sqlite"] } ssi = "0.7" -tokio = { version = "1.25", default-features = false, features = ["fs", "macros", "process", "rt", "rt-multi-thread"] } +tokio = { version = "1.25", default-features = false, features = ["fs", "macros", "process", "rt", "rt-multi-thread", "signal"] } which = "4.4" zip = "0.6" diff --git a/cli/src/install/ceramic_daemon.rs b/cli/src/install/ceramic_daemon.rs index 6268902..0dd648c 100644 --- a/cli/src/install/ceramic_daemon.rs +++ b/cli/src/install/ceramic_daemon.rs @@ -83,7 +83,7 @@ pub async fn install_ceramic_daemon( if let Ok(exit) = process.wait().await { let _ = tx.send(exit.clone()).await; log::info!( - "Ceramic exited with code {}", + "\nCeramic exited with code {}", exit.code().unwrap_or_else(|| 0) ); if !exit.success() { diff --git a/cli/src/main.rs b/cli/src/main.rs index 3886ddd..839faeb 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,8 +1,10 @@ +use anyhow::{bail, Ok}; use clap::{Parser, Subcommand, ValueEnum}; use log::LevelFilter; use std::fmt::Formatter; use std::io::Write; use std::path::PathBuf; +use std::sync::atomic::{AtomicUsize, Ordering}; #[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)] enum Network { @@ -92,6 +94,8 @@ struct ProgramArgs { command: Option, } +static CANCEL_REQUEST_CNT: AtomicUsize = AtomicUsize::new(0); + #[tokio::main] async fn main() -> anyhow::Result<()> { let _ = env_logger::builder() @@ -104,63 +108,121 @@ async fn main() -> anyhow::Result<()> { let working_directory = args .working_directory .map(PathBuf::from) + .and_then(|p| { + if p.is_absolute() { + Some(p) + } else { + Some(current_directory.join(p)) + } + }) .unwrap_or_else(|| current_directory); let mut versions = wheel_3box::Versions::default(); - if let Some(v) = args.ceramic_version { + if let Some(ref v) = args.ceramic_version { versions.ceramic = Some(v.parse()?); } - if let Some(v) = args.composedb_version { + if let Some(ref v) = args.composedb_version { versions.composedb = Some(v.parse()?); } if let Some(v) = args.template_branch { versions.template_branch = Some(v); } - let opt_child = match args.command { - None => { - log::info!("Starting wheel interactive configuration"); - wheel_3box::interactive_default(working_directory, versions).await? - } - Some(Commands::Quiet(q)) => { - let network = match q.network { - Network::InMemory => wheel_3box::NetworkIdentifier::InMemory, - Network::Local => wheel_3box::NetworkIdentifier::Local, - Network::Dev => wheel_3box::NetworkIdentifier::Dev, - Network::Clay => wheel_3box::NetworkIdentifier::Clay, - Network::Mainnet => wheel_3box::NetworkIdentifier::Mainnet, - }; - let with_composedb = q.setup == Setup::ComposeDB; - let with_app_template = q.setup == Setup::DemoApplication; - let did = if let DidCommand::Specify(opts) = q.did { - Some(wheel_3box::DidOptions { - did: opts.did, - private_key: opts.private_key, - }) - } else { - None - }; - wheel_3box::quiet(wheel_3box::QuietOptions { - project_name: q.project_name, - working_directory: working_directory, - network_identifier: network, - versions, - did, - with_ceramic: with_app_template || with_composedb || q.setup == Setup::CeramicOnly, - with_composedb: with_app_template || with_composedb, - with_app_template: with_app_template, - }) - .await? + let (shutdown_tx, mut shutdown_rx) = tokio::sync::broadcast::channel::<()>(8); + + let main_task = tokio::spawn(async move { + let opt_child = match args.command { + None => { + log::info!("Starting wheel interactive configuration"); + + tokio::select! { + res = wheel_3box::interactive_default(working_directory, versions) => { + res? + }, + _shutdown = shutdown_rx.recv() => { + log::info!("\nReceived shutdown request, exiting interactive setup"); + return Ok(()) + } + } + } + Some(Commands::Quiet(q)) => { + let network = match q.network { + Network::InMemory => wheel_3box::NetworkIdentifier::InMemory, + Network::Local => wheel_3box::NetworkIdentifier::Local, + Network::Dev => wheel_3box::NetworkIdentifier::Dev, + Network::Clay => wheel_3box::NetworkIdentifier::Clay, + Network::Mainnet => wheel_3box::NetworkIdentifier::Mainnet, + }; + let with_composedb = q.setup == Setup::ComposeDB; + let with_app_template = q.setup == Setup::DemoApplication; + let did = if let DidCommand::Specify(opts) = q.did { + Some(wheel_3box::DidOptions { + did: opts.did, + private_key: opts.private_key, + }) + } else { + None + }; + + let opts = wheel_3box::QuietOptions { + project_name: q.project_name, + working_directory: working_directory, + network_identifier: network, + versions, + did, + with_ceramic: with_app_template + || with_composedb + || q.setup == Setup::CeramicOnly, + with_composedb: with_app_template || with_composedb, + with_app_template: with_app_template, + }; + + tokio::select! { + res = wheel_3box::quiet(opts) => { + res? + }, + _shutdown = shutdown_rx.recv() => { + log::info!("\nReceived shutdown request, exiting quiet setup"); + return Ok(()) + } + } + } + }; + + log::info!("Wheel setup is complete. If running a clay or mainnet node, please check out https://github.com/ceramicstudio/simpledeploy to deploy with k8s."); + + if let Some(child) = opt_child { + log::info!("Ceramic is now running in the background. Please use another terminal for additional commands. You can interrupt ceramic using ctrl-c."); + // we could select!, but the child daemon handles ctrl+c, so we ignore it so it gets a chance to exit gracefully. + child.await?; } - }; - log::info!("Wheel setup is complete. If running a clay or mainnet node, please check out https://github.com/ceramicstudio/simpledeploy to deploy with k8s."); + Ok(()) + }); - if let Some(child) = opt_child { - log::info!("Ceramic is now running in the background. Please use another terminal for additional commands. You can interrupt ceramic using ctrl-c."); - child.await?; - } else { - log::info!("Wheel setup is complete."); - } + let abort_handle = main_task.abort_handle(); + + let resp = tokio::select! { + res = main_task => { + res? + }, + ctrl_c = tokio::signal::ctrl_c() => { + if let Err(e) = ctrl_c { + log::error!("\nError attaching ctrl-c handler: {}", e); + bail!("Error listening for ctrl-c") + } + log::info!("\nReceived ctrl-c, shutting down gracefully. Press ctrl-c again to force shutdown."); + if let Err(_e) = shutdown_tx.send(()) { + log::warn!("\nError notifying child tasks of shutdown request"); + } + + let cnt = CANCEL_REQUEST_CNT.fetch_add(1, Ordering::SeqCst); + if cnt > 0 { + log::info!("\nReceived additional ctrl-c, forcing shutdown"); + abort_handle.abort(); + } + Ok(()) + } + }; - Ok(()) + resp }