diff --git a/Cargo.lock b/Cargo.lock index 9fca0ac1f2..909a2d2b0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -592,9 +592,12 @@ dependencies = [ "ckb-util", "ckb-verification-traits", "clap", + "colored", "ctrlc", + "daemonize", "fdlimit", "is-terminal", + "nix 0.24.3", "rayon", "sentry", "serde", @@ -1721,6 +1724,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "colored" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" +dependencies = [ + "is-terminal", + "lazy_static", + "windows-sys 0.48.0", +] + [[package]] name = "console" version = "0.15.7" @@ -1905,7 +1919,7 @@ version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82e95fbd621905b854affdc67943b043a0fbb6ed7385fd5a25650d19a8a6cfdf" dependencies = [ - "nix", + "nix 0.27.1", "windows-sys 0.48.0", ] @@ -1928,6 +1942,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "daemonize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8bfdaacb3c887a54d41bdf48d3af8873b3f5566469f8ba21b92057509f116e" +dependencies = [ + "libc", +] + [[package]] name = "darling" version = "0.20.3" @@ -3111,6 +3134,17 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + [[package]] name = "nix" version = "0.27.1" diff --git a/ckb-bin/Cargo.toml b/ckb-bin/Cargo.toml index 3a160c872d..1e75ae57a0 100644 --- a/ckb-bin/Cargo.toml +++ b/ckb-bin/Cargo.toml @@ -46,6 +46,11 @@ is-terminal = "0.4.7" fdlimit = "0.2.1" ckb-stop-handler = { path = "../util/stop-handler", version = "= 0.114.0-pre" } +[target.'cfg(not(target_os="windows"))'.dependencies] +daemonize = { version = "0.5.0" } +nix = { version = "0.24.0", default-features = false, features = ["signal"] } +colored = "2.0" + [features] deadlock_detection = ["ckb-util/deadlock_detection"] profiling = ["ckb-memory-tracker/profiling"] diff --git a/ckb-bin/src/helper.rs b/ckb-bin/src/helper.rs index 98f1bd5362..de27935cb5 100644 --- a/ckb-bin/src/helper.rs +++ b/ckb-bin/src/helper.rs @@ -1,4 +1,4 @@ -use ckb_logger::info; +use ckb_logger::debug; use std::io::{stdin, stdout, Write}; @@ -8,7 +8,7 @@ pub fn deadlock_detection() {} #[cfg(feature = "deadlock_detection")] pub fn deadlock_detection() { use ckb_channel::select; - use ckb_logger::warn; + use ckb_logger::{info, warn}; use ckb_stop_handler::{new_crossbeam_exit_rx, register_thread}; use ckb_util::parking_lot::deadlock; use std::{thread, time::Duration}; @@ -73,6 +73,6 @@ pub fn prompt(msg: &str) -> String { /// on the number of cores available. pub fn raise_fd_limit() { if let Some(limit) = fdlimit::raise_fd_limit() { - info!("raise_fd_limit newly-increased limit: {}", limit); + debug!("raise_fd_limit newly-increased limit: {}", limit); } } diff --git a/ckb-bin/src/lib.rs b/ckb-bin/src/lib.rs index 825a95b097..f959724667 100644 --- a/ckb-bin/src/lib.rs +++ b/ckb-bin/src/lib.rs @@ -4,15 +4,21 @@ mod helper; mod setup_guard; mod subcommand; - use ckb_app_config::{cli, ExitCode, Setup}; use ckb_async_runtime::new_global_runtime; use ckb_build_info::Version; -use ckb_logger::info; +use ckb_logger::{debug, info}; use ckb_network::tokio; +use clap::ArgMatches; use helper::raise_fd_limit; use setup_guard::SetupGuard; +#[cfg(not(target_os = "windows"))] +use colored::Colorize; +#[cfg(not(target_os = "windows"))] +use daemonize::Daemonize; +#[cfg(not(target_os = "windows"))] +use subcommand::check_process; #[cfg(feature = "with_sentry")] pub(crate) const LOG_TARGET_SENTRY: &str = "sentry"; @@ -56,8 +62,65 @@ pub fn run_app(version: Version) -> Result<(), ExitCode> { let (cmd, matches) = app_matches .subcommand() .expect("SubcommandRequiredElseHelp"); - let is_silent_logging = is_silent_logging(cmd); + #[cfg(not(target_os = "windows"))] + if run_deamon(cmd, matches) { + return run_app_in_daemon(version, bin_name, cmd, matches); + } + + debug!("ckb version: {}", version); + run_app_inner(version, bin_name, cmd, matches) +} + +#[cfg(not(target_os = "windows"))] +fn run_app_in_daemon( + version: Version, + bin_name: String, + cmd: &str, + matches: &ArgMatches, +) -> Result<(), ExitCode> { + eprintln!("starting CKB in daemon mode ..."); + eprintln!("check status : `{}`", "ckb daemon --check".green()); + eprintln!("stop daemon : `{}`", "ckb daemon --stop".yellow()); + + assert!(matches!(cmd, cli::CMD_RUN)); + let root_dir = Setup::root_dir_from_matches(matches)?; + let daemon_dir = root_dir.join("data/daemon"); + // make sure daemon dir exists + std::fs::create_dir_all(daemon_dir)?; + let pid_file = Setup::daemon_pid_file_path(matches)?; + + if check_process(&pid_file).is_ok() { + eprintln!("{}", "ckb is already running".red()); + return Ok(()); + } + eprintln!("no ckb process, starting ..."); + + let pwd = std::env::current_dir()?; + let daemon = Daemonize::new() + .pid_file(pid_file) + .chown_pid_file(true) + .working_directory(pwd); + + match daemon.start() { + Ok(_) => { + info!("Success, daemonized ..."); + run_app_inner(version, bin_name, cmd, matches) + } + Err(e) => { + info!("daemonize error: {}", e); + Err(ExitCode::Failure) + } + } +} + +fn run_app_inner( + version: Version, + bin_name: String, + cmd: &str, + matches: &ArgMatches, +) -> Result<(), ExitCode> { + let is_silent_logging = is_silent_logging(cmd); let (mut handle, mut handle_stop_rx, _runtime) = new_global_runtime(); let setup = Setup::from_matches(bin_name, cmd, matches)?; let _guard = SetupGuard::from_setup(&setup, &version, handle.clone(), is_silent_logging)?; @@ -73,6 +136,8 @@ pub fn run_app(version: Version) -> Result<(), ExitCode> { cli::CMD_STATS => subcommand::stats(setup.stats(matches)?, handle.clone()), cli::CMD_RESET_DATA => subcommand::reset_data(setup.reset_data(matches)?), cli::CMD_MIGRATE => subcommand::migrate(setup.migrate(matches)?), + #[cfg(not(target_os = "windows"))] + cli::CMD_DAEMON => subcommand::daemon(setup.daemon(matches)?), _ => unreachable!(), }; @@ -89,11 +154,24 @@ pub fn run_app(version: Version) -> Result<(), ExitCode> { ret } +#[cfg(not(target_os = "windows"))] +fn run_deamon(cmd: &str, matches: &ArgMatches) -> bool { + match cmd { + cli::CMD_RUN => matches.get_flag(cli::ARG_DAEMON), + _ => false, + } +} + type Silent = bool; fn is_silent_logging(cmd: &str) -> Silent { matches!( cmd, - cli::CMD_EXPORT | cli::CMD_IMPORT | cli::CMD_STATS | cli::CMD_MIGRATE | cli::CMD_RESET_DATA + cli::CMD_EXPORT + | cli::CMD_IMPORT + | cli::CMD_STATS + | cli::CMD_MIGRATE + | cli::CMD_RESET_DATA + | cli::CMD_DAEMON ) } diff --git a/ckb-bin/src/subcommand/daemon.rs b/ckb-bin/src/subcommand/daemon.rs new file mode 100644 index 0000000000..b1ceba9636 --- /dev/null +++ b/ckb-bin/src/subcommand/daemon.rs @@ -0,0 +1,89 @@ +use ckb_app_config::{DaemonArgs, ExitCode}; +use colored::*; +use nix::sys::signal::{kill, Signal}; +use nix::unistd::Pid; +use std::io::Write; +use std::path::PathBuf; +use std::{fs, io}; + +pub fn daemon(args: DaemonArgs) -> Result<(), ExitCode> { + let pid_file = &args.pid_file; + if args.check { + // find the pid file and check if the process is running + match check_process(pid_file) { + Ok(pid) => { + eprintln!("{}, pid - {}", "ckb daemon service is running".green(), pid); + } + _ => { + eprintln!("{}", "ckb daemon service is not running".red()); + } + } + } else if args.stop { + kill_process(pid_file, "ckb")?; + fs::remove_file(pid_file).map_err(|_| ExitCode::Failure)?; + } + Ok(()) +} + +pub fn check_process(pid_file: &PathBuf) -> Result { + let pid_str = fs::read_to_string(pid_file).map_err(|_| ExitCode::Failure)?; + let pid = pid_str + .trim() + .parse::() + .map_err(|_| ExitCode::Failure)?; + + // Check if the process is running + match kill(Pid::from_raw(pid), None) { + Ok(_) => Ok(pid), + Err(_) => Err(ExitCode::Failure), + } +} + +fn kill_process(pid_file: &PathBuf, name: &str) -> Result<(), ExitCode> { + if check_process(pid_file).is_err() { + eprintln!("{} is not running", name); + return Ok(()); + } + let pid_str = fs::read_to_string(pid_file).map_err(|_| ExitCode::Failure)?; + let pid = pid_str + .trim() + .parse::() + .map_err(|_| ExitCode::Failure)?; + eprintln!( + "stopping {} deamon service with pid {} ...", + name, + pid.to_string().red() + ); + // Send a SIGTERM signal to the process + let _ = kill(Pid::from_raw(pid), Some(Signal::SIGTERM)).map_err(|_| ExitCode::Failure); + let mut wait_time = 60; + eprintln!("{}", "waiting ckb service to stop ...".yellow()); + loop { + let res = check_process(pid_file); + match res { + Ok(_) => { + wait_time -= 1; + eprint!("{}", ".".yellow()); + let _ = io::stderr().flush(); + std::thread::sleep(std::time::Duration::from_secs(1)); + } + _ if wait_time <= 0 => { + eprintln!( + "{}", + format!( + "ckb daemon service is is still running with pid {}..., stop it now forcefully ...", + pid + ) + .red() + ); + kill(Pid::from_raw(pid), Some(Signal::SIGKILL)).map_err(|_| ExitCode::Failure)?; + break; + } + _ => { + break; + } + } + } + eprintln!("\n{}", "cbk daemon service stopped successfully".green()); + Ok(()) +} diff --git a/ckb-bin/src/subcommand/mod.rs b/ckb-bin/src/subcommand/mod.rs index 6940ac1704..cc75d07be5 100644 --- a/ckb-bin/src/subcommand/mod.rs +++ b/ckb-bin/src/subcommand/mod.rs @@ -1,3 +1,5 @@ +#[cfg(not(target_os = "windows"))] +mod daemon; mod export; mod import; mod init; @@ -10,6 +12,8 @@ mod reset_data; mod run; mod stats; +#[cfg(not(target_os = "windows"))] +pub use self::daemon::{check_process, daemon}; pub use self::export::export; pub use self::import::import; pub use self::init::init; diff --git a/ckb-bin/src/subcommand/run.rs b/ckb-bin/src/subcommand/run.rs index 6418540192..3b74a95cc7 100644 --- a/ckb-bin/src/subcommand/run.rs +++ b/ckb-bin/src/subcommand/run.rs @@ -54,6 +54,7 @@ pub fn run(args: RunArgs, version: Version, async_handle: Handle) -> Result<(), let tx_pool_builder = pack.take_tx_pool_builder(); tx_pool_builder.start(network_controller.clone()); + info!("CKB service started ..."); ctrlc::set_handler(|| { info!("Trapped exit signal, exiting..."); broadcast_exit_signals(); diff --git a/devtools/init/README.md b/devtools/init/README.md index 3d9cb45285..a1af2857c3 100644 --- a/devtools/init/README.md +++ b/devtools/init/README.md @@ -1,11 +1,35 @@ -# Init/Service Scripts +# CKB Init Scripts + +## Run CKB in deamon mode + +CKB has a builtin deamon mode, command to run CKB in deamon mode(only for Linux/MacOS): + +```bash +ckb run --deamon +``` + +Check deamon satus: + +```bash +ckb deamon --check +``` + +Stop deamon process: + +```bash +ckb deamon --stop +``` + +The deamon mode is only for Linux/MacOS, and the CKB service will not be started automatically after reboot. + +## Init/Service Scripts This folder provides the init/service scripts to start CKB node and miner as daemons on various Unix like distributions. See the README in each folder for the detailed instructions. -## Disclaimer +### Disclaimer Users are expected to know how to administer their system, and these files should be considered as only a guide or suggestion to setup CKB. diff --git a/util/app-config/src/args.rs b/util/app-config/src/args.rs index 15c3264034..1b8a478d72 100644 --- a/util/app-config/src/args.rs +++ b/util/app-config/src/args.rs @@ -17,6 +17,17 @@ pub struct ExportArgs { pub target: PathBuf, } +#[derive(Debug)] +/// Parsed command line arguments for `ckb daemon`. +pub struct DaemonArgs { + /// Check the daemon status + pub check: bool, + /// Stop daemon process + pub stop: bool, + /// The pid file path + pub pid_file: PathBuf, +} + /// Parsed command line arguments for `ckb import`. pub struct ImportArgs { /// Parsed `ckb.toml`. @@ -45,6 +56,8 @@ pub struct RunArgs { pub chain_spec_hash: Byte32, /// Whether start indexer, default false pub indexer: bool, + /// Whether start in daemon mode + pub daemon: bool, } /// Enable profile on blocks in the range `[from, to]`. diff --git a/util/app-config/src/cli.rs b/util/app-config/src/cli.rs index 06bc647076..436ee4b7fc 100644 --- a/util/app-config/src/cli.rs +++ b/util/app-config/src/cli.rs @@ -32,7 +32,8 @@ pub const CMD_GEN_SECRET: &str = "gen"; pub const CMD_FROM_SECRET: &str = "from-secret"; /// Subcommand `migrate`. pub const CMD_MIGRATE: &str = "migrate"; - +/// Subcommand `daemon` +pub const CMD_DAEMON: &str = "daemon"; /// Command line argument `--config-dir`. pub const ARG_CONFIG_DIR: &str = "config-dir"; /// Command line argument `--format`. @@ -73,6 +74,8 @@ pub const ARG_BA_HASH_TYPE: &str = "ba-hash-type"; pub const ARG_BA_MESSAGE: &str = "ba-message"; /// Command line argument `--ba-advanced`. pub const ARG_BA_ADVANCED: &str = "ba-advanced"; +/// Command line argument `--daemon` +pub const ARG_DAEMON: &str = "daemon"; /// Command line argument `--indexer`. pub const ARG_INDEXER: &str = "indexer"; /// Command line argument `--from`. @@ -111,13 +114,17 @@ pub const ARG_OVERWRITE_CHAIN_SPEC: &str = "overwrite-spec"; pub const ARG_ASSUME_VALID_TARGET: &str = "assume-valid-target"; /// Command line argument `--check`. pub const ARG_MIGRATE_CHECK: &str = "check"; +/// Command line argument `daemon --check` +pub const ARG_DAEMON_CHECK: &str = "check"; +/// Command line argument `daemon --stop` +pub const ARG_DAEMON_STOP: &str = "stop"; /// Command line arguments group `ba` for block assembler. const GROUP_BA: &str = "ba"; /// return root clap Command pub fn basic_app() -> Command { - Command::new(BIN_NAME) + let command = Command::new(BIN_NAME) .author("Nervos Core Dev ") .about("Nervos CKB - The Common Knowledge Base") .subcommand_required(true) @@ -143,7 +150,12 @@ pub fn basic_app() -> Command { .subcommand(stats()) .subcommand(reset_data()) .subcommand(peer_id()) - .subcommand(migrate()) + .subcommand(migrate()); + + #[cfg(not(target_os = "windows"))] + let command = command.subcommand(daemon()); + + command } /// Parse the command line arguments by supplying the version information. @@ -161,7 +173,7 @@ pub fn get_bin_name_and_matches(version: &Version) -> (String, ArgMatches) { } fn run() -> Command { - Command::new(CMD_RUN) + let command = Command::new(CMD_RUN) .about("Run CKB node") .arg( Arg::new(ARG_BA_ADVANCED) @@ -198,11 +210,23 @@ fn run() -> Command { .long(ARG_INDEXER) .action(clap::ArgAction::SetTrue) .help("Start the built-in indexer service"), - ) + ); + + #[cfg(not(target_os = "windows"))] + let command = command.arg( + Arg::new(ARG_DAEMON) + .long(ARG_DAEMON) + .action(clap::ArgAction::SetTrue) + .help( + "Starts ckb as a daemon, \ + which will run in the background and output logs to the specified log file", + ), + ); + command } fn miner() -> Command { - Command::new(CMD_MINER).about("Run CKB miner").arg( + Command::new(CMD_MINER).about("Runs ckb miner").arg( Arg::new(ARG_LIMIT) .short('l') .long(ARG_LIMIT) @@ -372,6 +396,25 @@ fn migrate() -> Command { ) } +#[cfg(not(target_os = "windows"))] +fn daemon() -> Command { + Command::new(CMD_DAEMON) + .about("Runs ckb daemon command") + .arg( + Arg::new(ARG_DAEMON_CHECK) + .long(ARG_DAEMON_CHECK) + .action(clap::ArgAction::SetTrue) + .help("Check the daemon status"), + ) + .arg( + Arg::new(ARG_DAEMON_STOP) + .long(ARG_DAEMON_STOP) + .action(clap::ArgAction::SetTrue) + .conflicts_with(ARG_DAEMON_CHECK) + .help("Stop the daemon process, both the miner and the node"), + ) +} + fn list_hashes() -> Command { Command::new(CMD_LIST_HASHES) .about("List well known hashes") diff --git a/util/app-config/src/lib.rs b/util/app-config/src/lib.rs index a8fbbab64d..7ef56f3d69 100644 --- a/util/app-config/src/lib.rs +++ b/util/app-config/src/lib.rs @@ -15,7 +15,7 @@ pub use app_config::{ AppConfig, CKBAppConfig, ChainConfig, LogConfig, MetricsConfig, MinerAppConfig, }; pub use args::{ - ExportArgs, ImportArgs, InitArgs, MigrateArgs, MinerArgs, PeerIDArgs, ReplayArgs, + DaemonArgs, ExportArgs, ImportArgs, InitArgs, MigrateArgs, MinerArgs, PeerIDArgs, ReplayArgs, ResetDataArgs, RunArgs, StatsArgs, }; pub use configs::*; @@ -98,6 +98,7 @@ impl Setup { overwrite_chain_spec: matches.get_flag(cli::ARG_OVERWRITE_CHAIN_SPEC), chain_spec_hash, indexer: matches.get_flag(cli::ARG_INDEXER), + daemon: matches.get_flag(cli::ARG_DAEMON), }) } @@ -218,6 +219,18 @@ impl Setup { }) } + /// Executes `ckb daemon`. + pub fn daemon(self, matches: &ArgMatches) -> Result { + let check = matches.get_flag(cli::ARG_DAEMON_CHECK); + let stop = matches.get_flag(cli::ARG_DAEMON_STOP); + let pid_file = Setup::daemon_pid_file_path(matches)?; + Ok(DaemonArgs { + check, + stop, + pid_file, + }) + } + /// Executes `ckb init`. pub fn init(matches: &ArgMatches) -> Result { if matches.contains_id("list-specs") { @@ -347,6 +360,12 @@ impl Setup { Ok(config_dir) } + /// Resolves the pid file path for ckb from the command line arguments. + pub fn daemon_pid_file_path(matches: &ArgMatches) -> Result { + let root_dir = Self::root_dir_from_matches(matches)?; + Ok(root_dir.join("data/daemon/ckb-run.pid")) + } + /// Loads the chain spec. #[cfg(feature = "with_sentry")] fn chain_spec(&self) -> Result {