refactor(cli): defer process termination (#11440)

This commit is contained in:
Taku Kodama authored and GitHub committed 2026-07-28 18:50:13 -05:00
1 parent 8bd923ec85
commit 259b3584ff
28 files changed
+272 -86

No files matched your search

+1
View File
@@ -7,4 +7,5 @@
disallowed-methods = [
{ path = "std::env::vars", reason = "panics on non-UTF-8 environment variables (jdx/mise#5370); use crate::env::vars_safe(), or std::env::vars_os() when values must not be dropped" },
{ path = "std::env::args", reason = "panics on non-UTF-8 arguments (jdx/mise#10056); use crate::env::args_safe(), or std::env::args_os()" },
{ path = "std::process::exit", reason = "skips destructors; propagate the status to the binary's outermost boundary and allow only that boundary" },
]
+1 -1
View File
@@ -5,7 +5,7 @@ extern crate log;
#[cfg(feature = "cli")]
mod cli;
#[allow(clippy::needless_return)]
#[allow(clippy::disallowed_methods, clippy::needless_return)]
#[cfg(feature = "cli")]
#[tokio::main]
async fn main() {
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
assert_exit_code() {
local expected="$1"
shift
local output status=0
output="$("$@" 2>&1)" || status=$?
if [[ $status -ne $expected ]]; then
fail "[$*] expected exit code $expected, got $status: $output"
fi
assert_not_contains_text "$output" "exit with status"
}
# Clap help and errors should keep their conventional statuses without exposing
# the internal exit request used to unwind the command.
assert_exit_code 0 mise --help
assert_exit_code 2 mise --definitely-not-a-real-option
# Wrapper commands must preserve the exact child status.
assert_exit_code 42 mise exec -- bash -c "exit 42"
# Task failures also propagate the task's status after result reporting.
cat <<'EOF' >mise.toml
[tasks.fail]
run = "exit 23"
[tasks.wait]
run = """
touch "$READY_FILE"
trap '' INT TERM
while true; do sleep 1; done
"""
EOF
assert_exit_code 23 mise run fail
# Task execution handles the first Ctrl-C gracefully and force-exits on the
# second. The forced path should still unwind command-scoped destructors.
ready_file="$TMPDIR/exit-status-child-ready"
output_file="$TMPDIR/exit-status-child-output"
# Isolate the process tree so the failure fallback can reap a child that
# deliberately ignores INT and TERM.
READY_FILE="$ready_file" setsid mise run wait >"$output_file" 2>&1 &
mise_pid=$!
wait_for_file "$ready_file" "task child" 30 "$mise_pid"
kill -INT "$mise_pid"
sleep 0.2
kill -INT "$mise_pid"
for _ in {1..50}; do
if ! kill -0 "$mise_pid" 2>/dev/null; then
break
fi
sleep 0.1
done
if kill -0 "$mise_pid" 2>/dev/null; then
kill -KILL -- "-$mise_pid" 2>/dev/null || true
wait "$mise_pid" 2>/dev/null || true
fail "mise run did not exit after repeated Ctrl-C"
fi
status=0
wait "$mise_pid" || status=$?
output="$(cat "$output_file")"
if [[ $status -ne 1 ]]; then
fail "mise run expected exit code 1 after repeated Ctrl-C, got $status: $output"
fi
assert_not_contains_text "$output" "exit with status"
+8 -8
View File
@@ -1214,7 +1214,7 @@ impl BootstrapStatus {
table.print()?;
}
if self.missing && report.any_missing {
crate::exit(1);
return Err(crate::request_exit(1));
}
Ok(())
}
@@ -1948,7 +1948,7 @@ impl BootstrapPluginsStatus {
}
table.print()?;
if self.missing && any_missing {
crate::exit(1);
return Err(crate::request_exit(1));
}
Ok(())
}
@@ -2087,7 +2087,7 @@ impl BootstrapReposStatus {
table.print()?;
}
if self.missing && any_missing {
crate::exit(1);
return Err(crate::request_exit(1));
}
Ok(())
}
@@ -2216,7 +2216,7 @@ impl BootstrapLaunchdStatus {
table.print()?;
}
if self.missing && any_missing {
crate::exit(1);
return Err(crate::request_exit(1));
}
Ok(())
}
@@ -2322,7 +2322,7 @@ impl BootstrapSystemdStatus {
table.print()?;
}
if self.missing && any_missing {
crate::exit(1);
return Err(crate::request_exit(1));
}
Ok(())
}
@@ -2419,7 +2419,7 @@ impl BootstrapMacosDefaultsStatus {
table.print()?;
}
if self.missing && any_missing {
crate::exit(1);
return Err(crate::request_exit(1));
}
Ok(())
}
@@ -2498,7 +2498,7 @@ impl BootstrapShellStatus {
table.print()?;
}
if self.missing && any_missing {
crate::exit(1);
return Err(crate::request_exit(1));
}
Ok(())
}
@@ -2596,7 +2596,7 @@ impl BootstrapUserStatus {
table.print()?;
}
if self.missing && any_missing {
crate::exit(1);
return Err(crate::request_exit(1));
}
Ok(())
}
+3 -3
View File
@@ -1,6 +1,6 @@
mod path;
use crate::{exit, plugins::PluginEnum};
use crate::plugins::PluginEnum;
use std::collections::HashSet;
use std::{collections::BTreeMap, sync::Arc};
@@ -252,7 +252,7 @@ impl Doctor {
println!("{out}");
if !self.errors.is_empty() {
exit(1);
return Err(crate::request_exit(1));
}
Ok(())
}
@@ -343,7 +343,7 @@ impl Doctor {
let num = style::nred(format!("{}.", i + 1));
miseprintln!("{num} {}\n", info::indent_by(check, " ").trim_start());
}
exit(1);
return Err(crate::request_exit(1));
}
Ok(())
+1 -1
View File
@@ -153,7 +153,7 @@ impl DotfilesStatus {
}
}
if self.missing && any_missing {
crate::exit(1);
return Err(crate::request_exit(1));
}
Ok(())
}
+1 -1
View File
@@ -263,7 +263,7 @@ impl Edit {
info!("cancelled");
}
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {
std::process::exit(130);
return Err(crate::request_exit(130));
}
Err(e) => return Err(eyre!(e)),
}
+2 -4
View File
@@ -482,7 +482,7 @@ where
let body = last.to_string_lossy();
if let Some(mut c) = crate::path::cmd_verbatim_command(prog, &flags, &body) {
match c.status()?.code() {
Some(code) => std::process::exit(code),
Some(code) => return Err(crate::request_exit(code)),
None => return Err(eyre!("command failed: terminated by signal")),
}
}
@@ -492,9 +492,7 @@ where
let cmd = cmd::cmd(program, args);
let res = cmd.unchecked().run()?;
match res.status.code() {
Some(code) => {
std::process::exit(code);
}
Some(code) => Err(crate::request_exit(code)),
None => Err(eyre!("command failed: terminated by signal")),
}
}
+2 -2
View File
@@ -8,7 +8,7 @@ use crate::hook_env::{PREV_SESSION, WatchFilePattern};
use crate::shell::{ShellType, get_shell};
use crate::toolset::{ResolveOptions, Toolset, ToolsetBuilder};
use crate::ui::style;
use crate::{env, exit, hook_env, hooks, watch_files};
use crate::{env, hook_env, hooks, watch_files};
use console::truncate_str;
use eyre::Result;
use indexmap::IndexSet;
@@ -78,7 +78,7 @@ impl HookEnv {
display_path(&config_path)
);
}
exit(1);
return Err(crate::request_exit(1));
}
};
// Shell activation must stay fast and non-networked; missing tools are
+2 -2
View File
@@ -1,4 +1,4 @@
use crate::exit;
use crate::request_exit;
use eyre::Result;
@@ -33,6 +33,6 @@ impl HookNotFound {
return Ok(());
}
}
exit(127);
Err(request_exit(127))
}
}
+2 -2
View File
@@ -318,7 +318,7 @@ impl Install {
if self.dry_run_code {
let has_work = versions.iter().any(|tv| tv.install_satisfied != Some(true));
if has_work {
exit::exit(1);
return Err(exit::request(1));
}
}
return install_error;
@@ -540,7 +540,7 @@ impl Install {
};
if self.is_dry_run() {
if self.dry_run_code && has_missing {
exit::exit(1);
return Err(exit::request(1));
}
return install_error;
}
+59 -14
View File
@@ -1,12 +1,11 @@
use crate::config::{Config, Settings};
use crate::exit::exit;
use crate::task::TaskOutput;
use crate::ui::{self, ctrlc};
use crate::{Result, backend};
use crate::{Result, backend, request_exit};
use crate::{cli::args::ToolArg, path::PathExt};
use crate::{hook_env as hook_env_module, logger, migrate, shims};
use clap::{ArgAction, CommandFactory, Subcommand};
use eyre::bail;
use eyre::{Report, bail};
use std::path::PathBuf;
mod activate;
@@ -676,6 +675,10 @@ fn preprocess_args_for_naked_run(cmd: &clap::Command, args: &[String]) -> Vec<St
impl Cli {
pub async fn run(args: &Vec<String>) -> Result<()> {
run_with_exit_signal(Self::run_inner(args), ctrlc::exit_signal()).await
}
async fn run_inner(args: &Vec<String>) -> Result<()> {
crate::env::ARGS.write().unwrap().clone_from(args);
// Load .miserc.toml early, before MISE_ENV and other early settings are accessed.
// This allows setting MISE_ENV in a config file instead of only via env vars.
@@ -693,7 +696,6 @@ impl Cli {
measure!("logger", { logger::init() });
check_working_directory();
measure!("handle_shim", { shims::handle_shim().await })?;
ctrlc::init();
let print_version = version::print_version_if_requested(args)?;
// Clap's tool argument parsers consult installed plugin/tool metadata while
// resolving registry options. Initialize that filesystem-only state before
@@ -713,12 +715,12 @@ impl Cli {
// Reuse the already-built clap command rather than letting
// `Cli::parse_from` construct the whole tree a second time.
let cli = measure!("get_matches_from", {
let matches = cmd.get_matches_from(processed_args.iter());
match <Cli as clap::FromArgMatches>::from_arg_matches(&matches) {
Ok(cli) => cli,
Err(err) => err.format(&mut Cli::command()).exit(),
}
});
let matches = cmd
.try_get_matches_from(processed_args.iter())
.map_err(clap_error)?;
<Cli as clap::FromArgMatches>::from_arg_matches(&matches)
.map_err(|err| clap_error(err.format(&mut Cli::command())))
})?;
// Validate --cd path BEFORE Settings processes it and changes the directory
validate_cd_path(&cli.cd)?;
measure!("add_cli_matches", { Settings::add_cli_matches(&cli) });
@@ -738,7 +740,7 @@ impl Cli {
trace!("MISE_BIN: {}", crate::env::MISE_BIN.display_user());
if print_version {
version::show_latest().await;
exit(0);
return Err(request_exit(0));
}
let cmd = cli.get_command().await?;
measure!("run {cmd}", { cmd.run().await })
@@ -752,7 +754,7 @@ impl Cli {
// Handle special case: "help", "-h", or "--help" as task should print help
if task == "help" || task == "-h" || task == "--help" {
Cli::command().print_help()?;
exit(0);
return Err(request_exit(0));
}
let config = Config::get().await?;
@@ -818,15 +820,33 @@ impl Cli {
.chain(self.task_args_last)
.collect(),
)?;
exit(0);
return Err(request_exit(0));
}
}
Cli::command().print_help()?;
exit(1)
Err(request_exit(1))
}
}
}
async fn run_with_exit_signal<T>(
command: impl std::future::Future<Output = Result<T>>,
exit_signal: impl std::future::Future<Output = i32>,
) -> Result<T> {
tokio::select! {
result = command => result,
code = exit_signal => Err(request_exit(code)),
}
}
fn clap_error(err: clap::Error) -> Report {
let code = err.exit_code();
match err.print() {
Ok(()) => request_exit(code),
Err(err) => err.into(),
}
}
const LONG_ABOUT: &str = "mise prepares your development environment before each command runs. https://github.com/jdx/mise";
const LONG_TASK_ABOUT: &str = r#"Task to run.
@@ -921,6 +941,31 @@ mod tests {
}
use super::*;
#[tokio::test]
async fn exit_signal_drops_command_future_before_returning() {
struct DropGuard(std::sync::Arc<std::sync::atomic::AtomicBool>);
impl Drop for DropGuard {
fn drop(&mut self) {
self.0.store(true, std::sync::atomic::Ordering::SeqCst);
}
}
let dropped = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let guard = DropGuard(dropped.clone());
let command = async move {
let _guard = guard;
std::future::pending::<Result<()>>().await
};
let err = run_with_exit_signal(command, std::future::ready(42))
.await
.unwrap_err();
assert_eq!(crate::exit::requested_exit_code(&err), Some(42));
assert!(dropped.load(std::sync::atomic::Ordering::SeqCst));
}
#[test]
fn test_subcommands_are_sorted() {
let cmd = Cli::command();
+1 -1
View File
@@ -210,7 +210,7 @@ impl Run {
let status = run_result?;
if let Some(code) = status.code() {
if code != 0 {
std::process::exit(code);
return Err(crate::request_exit(code));
}
} else if !status.success() {
bail!("{engine_bin} exited abnormally: {status:?}");
+1 -1
View File
@@ -81,7 +81,7 @@ impl Prune {
let has_work = !to_delete.is_empty();
delete(&config, self.is_dry_run(), to_delete).await?;
if self.dry_run_code && has_work {
exit::exit(1);
return Err(exit::request(1));
}
if self.is_dry_run() {
return Ok(());
+1 -1
View File
@@ -579,7 +579,7 @@ impl Run {
this.continue_on_error,
this.timings(),
);
results_display.display_results(num_tasks, timer);
results_display.display_results(num_tasks, timer)?;
time!("parallelize_tasks done");
Ok(())
+1 -1
View File
@@ -108,7 +108,7 @@ impl SystemStatus {
}
}
if self.missing && any_missing {
crate::exit(1);
return Err(crate::request_exit(1));
}
Ok(())
}
+1 -1
View File
@@ -82,7 +82,7 @@ impl Uninstall {
if self.is_dry_run() {
if self.dry_run_code && has_work {
exit::exit(1);
return Err(exit::request(1));
}
return Ok(());
}
+1 -1
View File
@@ -280,7 +280,7 @@ impl Upgrade {
}
}
if self.dry_run_code {
exit::exit(1);
return Err(exit::request(1));
}
return Ok(());
}
+1 -1
View File
@@ -326,7 +326,7 @@ impl Use {
);
}
if self.dry_run_code {
exit::exit(1);
return Err(exit::request(1));
}
}
} else if !quiet {
+2 -2
View File
@@ -5,7 +5,7 @@ use crate::cmd;
use crate::config::Config;
use crate::dirs;
use crate::env;
use crate::exit::exit;
use crate::request_exit;
use crate::task::Deps;
use crate::task::task_source_checker::task_cwd;
use crate::toolset::ToolsetBuilder;
@@ -74,7 +74,7 @@ impl Watch {
eprintln!("{}: {}", style("Error").red().bold(), err);
eprintln!("{}: Install watchexec with:", style("Hint").bold());
eprintln!(" mise use -g watchexec@latest");
exit(1);
return Err(request_exit(1));
}
}
let args = once(self.task)
+41 -1
View File
@@ -1,14 +1,54 @@
use crate::cmd::CmdLineRunner;
use eyre::Report;
#[cfg(unix)]
use nix::sys::signal::SIGTERM;
use std::error::Error;
use std::fmt::{self, Display, Formatter};
pub fn exit(code: i32) -> ! {
#[derive(Debug)]
struct ExitRequest {
code: i32,
}
impl Display for ExitRequest {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "exit with status {}", self.code)
}
}
impl Error for ExitRequest {}
/// Request a process status without terminating before callers can unwind.
pub fn request(code: i32) -> Report {
Report::new(ExitRequest { code })
}
pub fn requested_exit_code(err: &Report) -> Option<i32> {
err.downcast_ref::<ExitRequest>().map(|exit| exit.code)
}
pub fn kill_all() {
#[cfg(unix)]
CmdLineRunner::kill_all(SIGTERM);
#[cfg(windows)]
CmdLineRunner::kill_all();
}
/// Terminate only after command scopes and their destructors have unwound.
#[allow(clippy::disallowed_methods)]
pub fn terminate(code: i32) -> ! {
debug!("exiting with code: {code}");
std::process::exit(code)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn finds_requested_exit_code_through_context() {
let err = request(42).wrap_err("context");
assert_eq!(requested_exit_code(&err), Some(42));
}
}
+42 -8
View File
@@ -6,6 +6,7 @@
use std::{
panic,
sync::atomic::{AtomicBool, Ordering},
time::Duration,
};
use crate::cli::Cli;
@@ -100,11 +101,11 @@ mod versions_host;
mod watch_files;
mod wildcard;
pub(crate) use crate::exit::exit;
pub(crate) use crate::exit::request as request_exit;
pub(crate) use crate::result::Result;
use crate::ui::multi_progress_report::MultiProgressReport;
fn main() -> eyre::Result<()> {
fn main() {
let nprocs = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or_default();
@@ -117,11 +118,41 @@ fn main() -> eyre::Result<()> {
let threads = crate::env::MISE_JOBS
.unwrap_or_else(|| nprocs.min(16))
.max(8);
tokio::runtime::Builder::new_multi_thread()
let runtime = match tokio::runtime::Builder::new_multi_thread()
.enable_all()
.worker_threads(threads)
.build()?
.block_on(main_())
.build()
{
Ok(runtime) => runtime,
Err(err) => {
eprintln!("Error: {err:?}");
exit::terminate(1);
}
};
let result = runtime.block_on(main_());
let (code, requested_exit) = match result {
Ok(()) => (0, false),
Err(err) => match exit::requested_exit_code(&err) {
Some(code) => {
exit::kill_all();
(code, true)
}
None => {
eprintln!("Error: {err:?}");
(1, false)
}
},
};
if requested_exit {
// Blocking tasks cannot be cancelled and may ignore the signals sent
// above. Bound the wait so an intentional exit cannot hang forever.
runtime.shutdown_timeout(Duration::from_secs(1));
} else {
drop(runtime);
}
if code != 0 {
exit::terminate(code);
}
}
async fn main_() -> eyre::Result<()> {
@@ -152,6 +183,9 @@ async fn main_() -> eyre::Result<()> {
}
fn handle_err(err: Report) -> eyre::Result<()> {
if exit::requested_exit_code(&err).is_some() {
return Err(err);
}
if let Some(err) = err.downcast_ref::<std::io::Error>()
&& err.kind() == std::io::ErrorKind::BrokenPipe
{
@@ -159,20 +193,20 @@ fn handle_err(err: Report) -> eyre::Result<()> {
}
if is_interrupted_io_error(&err) {
stop_multi_progress();
exit(130);
return Err(request_exit(130));
}
// Check for miette diagnostic errors and render them specially
if let Some(diagnostic) = err.downcast_ref::<config::config_file::diagnostic::MiseDiagnostic>()
{
safe_eprintln!("{}", diagnostic.render());
exit(1);
return Err(request_exit(1));
}
show_github_rate_limit_err(&err);
if *env::MISE_FRIENDLY_ERROR {
display_friendly_err(&err);
exit(1);
return Err(request_exit(1));
}
let async_backtrace = async_backtrace::taskdump_tree(true);
Err(err.section(async_backtrace.header("Async Tasks")))
+2 -2
View File
@@ -13,7 +13,7 @@ use crate::toolset::install_state;
use crate::ui::multi_progress_report::MultiProgressReport;
use crate::ui::progress_report::SingleReport;
use crate::ui::prompt;
use crate::{backend, dirs, env, exit, file, lock_file, registry};
use crate::{backend, dirs, env, file, lock_file, registry};
use async_trait::async_trait;
use clap::Command;
use console::style;
@@ -459,7 +459,7 @@ Plugins could support local directories in the future but for now a symlink is r
);
Settings::ensure_not_safe("executing asdf plugin scripts")?;
let result = self.script_man.cmd(&script).unchecked().run()?;
exit(result.status.code().unwrap_or(-1));
Err(crate::request_exit(result.status.code().unwrap_or(-1)))
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
use crate::exit;
use crate::request_exit;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
@@ -77,7 +77,7 @@ pub async fn handle_shim() -> Result<()> {
};
time!("shim exec");
exec.run().await?;
exit(0);
Err(request_exit(0))
}
#[cfg(windows)]
+7 -6
View File
@@ -1,7 +1,7 @@
use crate::exit;
use crate::task::task_output_handler::OutputHandler;
use crate::task::{FailedTasks, Task};
use crate::ui::{style, time};
use crate::{Result, request_exit};
/// Handles display of task execution results and failure summaries
pub struct TaskResultsDisplay {
@@ -27,11 +27,11 @@ impl TaskResultsDisplay {
}
/// Display final results and handle failures
pub fn display_results(&self, num_tasks: usize, timer: std::time::Instant) {
pub fn display_results(&self, num_tasks: usize, timer: std::time::Instant) -> Result<()> {
self.display_keep_order_output();
self.display_timing_summary(num_tasks, timer);
self.maybe_print_failure_summary();
self.exit_if_failed();
self.exit_if_failed()
}
/// Flush any remaining keep-order output (safety net).
@@ -78,8 +78,8 @@ impl TaskResultsDisplay {
}
}
/// Exit if any tasks failed
fn exit_if_failed(&self) {
/// Request a failing exit status if any tasks failed
fn exit_if_failed(&self) -> Result<()> {
if let Some((task, status)) = self.failed_tasks.lock().unwrap().first() {
let prefix = task.estyled_prefix();
self.eprint(
@@ -87,8 +87,9 @@ impl TaskResultsDisplay {
&prefix,
&format!("{} task failed", style::ered("ERROR")),
);
exit(status.unwrap_or(1));
return Err(request_exit(status.unwrap_or(1)));
}
Ok(())
}
/// Print error message for a task
+2 -2
View File
@@ -1,6 +1,6 @@
use crate::config::{Config, Settings};
use crate::env_diff::EnvMap;
use crate::exit::exit;
use crate::request_exit;
use crate::shell::ShellType;
use crate::task::Task;
use crate::tera::{TeraEngine, contains_template_syntax, get_tera, render_str};
@@ -704,7 +704,7 @@ impl TaskScriptParser {
// just print exactly what usage returns so the error output isn't double-wrapped
// this could be displaying help or a parse error
eprintln!("{}", format!("{e}").trim_end());
exit(1);
return Err(request_exit(1));
}
};
+14 -17
View File
@@ -1,4 +1,3 @@
use crate::exit;
use std::sync::atomic::{AtomicBool, Ordering};
use crate::cmd::CmdLineRunner;
@@ -9,23 +8,21 @@ static SHOW_CURSOR: AtomicBool = AtomicBool::new(false);
static CANCELLED: AtomicBool = AtomicBool::new(false);
// static HANDLERS: OnceCell<Vec<Box<dyn Fn() + Send + Sync + 'static>>> = OnceCell::new();
pub fn init() {
tokio::spawn(async move {
loop {
tokio::signal::ctrl_c().await.unwrap();
if SHOW_CURSOR.load(Ordering::Relaxed) {
let _ = Term::stderr().show_cursor();
}
CmdLineRunner::kill_all(nix::sys::signal::SIGINT);
if EXIT.load(Ordering::Relaxed) || CANCELLED.load(Ordering::Relaxed) {
debug!("Ctrl-C pressed, exiting...");
exit(1);
}
// First ctrl-c when EXIT is false: mark as cancelled so a second
// ctrl-c will force-exit and in-process operations can check the flag.
CANCELLED.store(true, Ordering::Relaxed);
pub async fn exit_signal() -> i32 {
loop {
tokio::signal::ctrl_c().await.unwrap();
if SHOW_CURSOR.load(Ordering::Relaxed) {
let _ = Term::stderr().show_cursor();
}
});
CmdLineRunner::kill_all(nix::sys::signal::SIGINT);
if EXIT.load(Ordering::Relaxed) || CANCELLED.load(Ordering::Relaxed) {
debug!("Ctrl-C pressed, exiting...");
return 1;
}
// First ctrl-c when EXIT is false: mark as cancelled so a second
// ctrl-c will end the command and in-process operations can check the flag.
CANCELLED.store(true, Ordering::Relaxed);
}
}
pub fn exit_on_ctrl_c(do_exit: bool) {
+3 -1
View File
@@ -1,4 +1,6 @@
pub fn init() {}
pub async fn exit_signal() -> i32 {
std::future::pending().await
}
// pub fn add_handler(_func: impl Fn() + Send + Sync + 'static) {}