use std::sync::Arc;
use std::time::Duration;
use anyhow::Result;
use arcbox_core::Runtime;
use arcbox_docker::DockerContextManager;
use tokio::signal;
use tracing::{info, warn};
use crate::context::{DaemonContext, ServiceHandles, StartupHandles};
const DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
const STARTUP_ABORT_GRACE: Duration = Duration::from_secs(10);
pub async fn run(ctx: DaemonContext, mut handles: ServiceHandles) -> Result<()> {
tokio::select! {
() = wait_for_signal() => {}
() = ctx.shutdown.cancelled() => {
warn!("internal safety condition requested daemon shutdown");
}
}
println!("Shutting down guest VM... (press Ctrl+C again to force quit)");
info!("Shutdown signal received, draining connections...");
crate::nfs_mount::cleanup(&ctx).await;
ctx.shutdown.cancel();
drain(&mut handles).await;
let forced = match ctx.shared_runtime.get() {
Some(runtime) => stop_runtime(Arc::clone(runtime), None).await,
None => false,
};
cleanup(&ctx).await;
info!("ArcBox daemon stopped");
if forced {
std::process::exit(0);
}
Ok(())
}
async fn stop_runtime(runtime: Arc<Runtime>, grace: Option<Duration>) -> bool {
let graceful = tokio::spawn({
let runtime = Arc::clone(&runtime);
async move { runtime.shutdown().await }
});
let graceful = async move {
match grace {
Some(limit) => tokio::time::timeout(limit, graceful)
.await
.map_err(|_| limit),
None => Ok(graceful.await),
}
};
let forced = tokio::select! {
result = graceful => match result {
Ok(Ok(Ok(()))) => false,
Ok(Ok(Err(e))) => {
warn!("Runtime shutdown error: {e}");
false
}
Ok(Err(e)) => {
warn!("Runtime shutdown task panicked: {e}");
true
}
Err(limit) => {
warn!(
"Graceful stop did not finish within {}s, forcing",
limit.as_secs()
);
true
}
},
() = wait_for_signal() => {
println!("Force shutting down...");
warn!("Second signal received, forcing shutdown");
true
}
};
if forced {
let _ = runtime.shutdown_force().await;
}
forced
}
async fn cleanup(ctx: &DaemonContext) {
if ctx.docker_integration {
if let Ok(ctx_manager) = DockerContextManager::new(ctx.layout.docker_socket.clone()) {
let _ = ctx_manager.disable();
}
}
crate::nfs_mount::cleanup(ctx).await;
cleanup_container_route(ctx.container_network_lease_slot.get()).await;
remove_sockets(ctx);
}
async fn cleanup_container_route(lease: Option<&crate::startup::ContainerNetworkLease>) {
if let Some(lease) = lease
&& let Err(error) = lease.cleanup_route().await
{
warn!(%error, "failed to remove isolated container route");
}
}
fn remove_sockets(ctx: &DaemonContext) {
for path in [&ctx.layout.docker_socket, &ctx.layout.grpc_socket] {
if let Err(e) = std::fs::remove_file(path) {
if e.kind() != std::io::ErrorKind::NotFound {
warn!("Failed to remove socket {}: {}", path.display(), e);
}
}
}
}
pub async fn interrupt_startup(handles: &StartupHandles) -> Result<()> {
println!("Shutting down... (press Ctrl+C again to force quit)");
info!("Shutdown signal received during startup, aborting");
handles
.setup_state
.set_failed("startup interrupted by shutdown signal");
tokio::time::sleep(Duration::from_millis(200)).await;
handles.shutdown.cancel();
let Some(runtime) = handles.early_runtime.get() else {
info!("ArcBox daemon stopped");
return Ok(());
};
let forced = stop_runtime(Arc::clone(runtime), Some(STARTUP_ABORT_GRACE)).await;
cleanup_container_route(handles.container_network_lease.get()).await;
info!("ArcBox daemon stopped");
if forced {
std::process::exit(0);
}
Ok(())
}
pub async fn abort_failed_startup(handles: &StartupHandles) {
handles.shutdown.cancel();
if let Some(runtime) = handles.early_runtime.get() {
if let Err(error) = runtime.shutdown_force().await {
warn!(%error, "failed to force-clean runtime after startup error");
}
}
cleanup_container_route(handles.container_network_lease.get()).await;
}
pub async fn wait_for_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("Failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("Failed to install SIGTERM handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
() = ctrl_c => {},
() = terminate => {},
}
}
async fn drain(handles: &mut ServiceHandles) {
if tokio::time::timeout(DRAIN_TIMEOUT, async {
let _ = tokio::join!(&mut handles.dns, &mut handles.grpc);
if let Some(h) = handles.docker.as_mut() {
let _ = h.await;
}
if let Some(h) = handles.kubernetes_proxy.as_mut() {
let _ = h.await;
}
if let Some(h) = handles.route_guard.as_mut() {
let _ = h.await;
}
})
.await
.is_err()
{
warn!(
"Server drain timed out after {}s, aborting remaining tasks",
DRAIN_TIMEOUT.as_secs()
);
handles.dns.abort();
handles.grpc.abort();
if let Some(h) = handles.docker.as_mut() {
h.abort();
}
if let Some(h) = handles.kubernetes_proxy.as_mut() {
h.abort();
}
if let Some(h) = handles.route_guard.as_mut() {
h.abort();
}
}
}