Repository: nervosnetwork/ckb (language: rust)

Issue:
Easy daemon mode for ckb run
## Feature Request

#### Is your feature request related to a problem? Please describe.
There is no simple approach to run a ckb node in daemon or background mode. Although there is an introduction to [start a ckb service](https://github.com/nervosnetwork/ckb/tree/develop/devtools/init), it's still too complex for average users.

#### Describe the solution you'd like
Similar to bitcoind, if ckb run provide a simple switch like `ckb run --daemon`, I believe more nodes will appear. 


Discussion / hints:
I read [bitcoind daemon mode](https://bitcoin.stackexchange.com/questions/74116/bitcoind-daemon-mode), learned that: `bitcoind --daemon` just run `bitcoind` in background, it won't auto start after system reboot.

However, using `systemd` can make the process restarts automatically after a reboot


But assume the existence of `systemd` reduce daemon mode's compatibility? e.g. does it work on Mac?
MacOS doesn't have `systemd`, the equivalent of systmed on MacOS is called [launchd](https://apple.stackexchange.com/questions/455362/equivalent-to-systemd-timers-on-os-x)

Fix patch:
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<i32, ExitCode> {
+    let pid_str = fs::read_to_string(pid_file).map_err(|_| ExitCode::Failure)?;
+    let pid = pid_str
+        .trim()
+        .parse::<i32>()
+        .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::<i32>()
+        .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 <dev@nervos.org>")
         .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<DaemonArgs, ExitCode> {
+        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<InitArgs, ExitCode> {
         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<PathBuf, ExitCode> {
+        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<ChainSpec, ExitCode> {


Test patch: