iota/iota-daemon/src/main.rs
2026-07-24 01:36:14 +02:00

256 lines
9 KiB
Rust

use iota_daemon_lib::{
DaemonRuntime, DaemonServices, IpcServer, ShutdownReason, StartupPhase, log_broadcaster,
};
use iota_logger::{self as logger, log};
use iota_storage::users::user_manager;
use iota_storage::util::config_util::CONFIG;
use std::process::ExitCode;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{broadcast, watch};
#[tokio::main(flavor = "multi_thread")]
async fn main() -> ExitCode {
let scope = match std::env::var("IOTA_DEPLOYMENT_MODE").ok().as_deref() {
Some("system_socket_activated") | Some("system_always_on") => iota_paths::Scope::System,
_ => iota_paths::Scope::User,
};
let paths = match iota_paths::IotaPaths::resolve(scope) {
Ok(paths) => paths,
Err(error) => {
eprintln!("Cannot resolve Iota paths: {error}");
return ExitCode::FAILURE;
}
};
if let Err(error) = paths.migrate_legacy_layout() {
eprintln!("Cannot migrate legacy Iota layout: {error}");
return ExitCode::FAILURE;
}
if let Err(error) = paths.prepare_writable_directories() {
eprintln!("Cannot prepare Iota directories: {error}");
return ExitCode::FAILURE;
}
iota_util::file_util::configure_storage_directory(paths.storage_dir.clone());
iota_storage::util::config_util::configure_config_path(paths.config_file.clone());
iota_storage::util::config_util::load_config_from(&paths.config_file);
omikron_connector::omikron_connection::configure_identity_path(paths.keyring_file());
match paths.scope {
iota_paths::Scope::User => logger::startup_with_log_dir(Some(paths.log_dir.clone())),
iota_paths::Scope::System => logger::startup_with_log_dir(None),
}
let runtime = Arc::new(DaemonRuntime::new());
// --- IPC infrastructure ---
let (log_tx, _) = broadcast::channel(512);
log_broadcaster::spawn(log_tx.clone());
let (state_tx, state_rx) = watch::channel(runtime.snapshot());
runtime.set_startup_phase(StartupPhase::LoadingUsers);
if tokio::task::spawn_blocking(user_manager::load_users_sync)
.await
.ok()
.and_then(Result::ok)
.is_none()
{
runtime.set_component_failed(
iota_ipc::ComponentId::Storage,
"user storage failed to load".into(),
);
} else {
runtime.set_component_healthy(iota_ipc::ComponentId::Storage, None);
}
// Bind before migration and service startup: a successful bind is the
// readiness boundary visible to clients and socket activation.
let socket = match &paths.ipc_endpoint {
iota_paths::IpcEndpoint::UnixSocket(path) => path.clone(),
iota_paths::IpcEndpoint::WindowsPipe(_) => {
eprintln!("Windows named-pipe daemon transport is not implemented yet");
return ExitCode::FAILURE;
}
};
let omikron = match omikron_connector::omikron_connection::connect_initial(
runtime.cancellation.clone(),
runtime.state.active_tasks.clone(),
runtime.state.app.clone(),
)
.await
{
Ok(connection) => connection,
Err(omikron_connector::OmikronStartupError::InitialConnectionTimeout { connection }) => {
runtime.set_component_degraded(
iota_ipc::ComponentId::Omikron,
"Omikron connection unavailable; retrying".into(),
);
connection
}
Err(omikron_connector::OmikronStartupError::Authentication) => {
runtime.set_component_failed(
iota_ipc::ComponentId::Omikron,
"Omikron authentication failed".into(),
);
return ExitCode::FAILURE;
}
Err(omikron_connector::OmikronStartupError::Construction(error)) => {
eprintln!("Cannot construct Omikron connection: {error}");
return ExitCode::FAILURE;
}
};
let services = DaemonServices::new(omikron);
let ipc_server = match IpcServer::bind(
socket.clone(),
runtime.clone(),
services,
log_tx.clone(),
state_rx,
)
.await
{
Ok(server) => server,
Err(error) => {
eprintln!("Cannot bind daemon IPC socket: {error}");
return ExitCode::FAILURE;
}
};
eprintln!("iota-daemon IPC listener ready at {}", socket.display());
runtime.set_component_healthy(iota_ipc::ComponentId::Ipc, None);
let listener_runtime = runtime.clone();
runtime
.tasks
.spawn_tracked("ipc-server", async move {
if let Err(error) = ipc_server.serve().await {
eprintln!("iota-daemon IPC server failed: {error}");
listener_runtime.shutdown(ShutdownReason::Fatal(format!(
"IPC listener stopped: {error}"
)));
}
Ok(())
})
.await;
log!("iota-daemon IPC server ready");
runtime.set_startup_phase(StartupPhase::StartingServices);
// --- System monitor ---
runtime.spawn_system_monitor().await;
// --- State update publisher (watch-based, no full broadcast per tick) ---
let state_publisher = runtime.clone();
runtime
.tasks
.spawn_tracked("state-publisher", async move {
loop {
if state_publisher.is_shutting_down() {
break;
}
let snapshot = state_publisher.snapshot();
let _ = state_tx.send(snapshot);
tokio::time::sleep(Duration::from_millis(500)).await;
}
Ok(())
})
.await;
// --- Web server ---
let web = CONFIG.load().web.clone();
let web_config = web_server::WebConfig {
mode: match web.mode {
iota_storage::util::config_util::WebMode::Disabled => web_server::WebMode::Disabled,
iota_storage::util::config_util::WebMode::Loopback => web_server::WebMode::Loopback,
iota_storage::util::config_util::WebMode::Network => web_server::WebMode::Network,
},
bind: web
.bind
.parse()
.unwrap_or(std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)),
port: web.port,
asset_dir: resolve_config_path(&paths.config_file, &web.asset_dir, &paths.asset_dir),
tls: web
.certificate
.zip(web.key)
.map(|(certificate, key)| web_server::TlsConfig {
certificate: resolve_config_path(
&paths.config_file,
&certificate,
&paths.config_dir,
),
key: resolve_config_path(&paths.config_file, &key, &paths.config_dir),
}),
required: web.required,
};
match web_server::start(web_config, runtime.cancellation.clone()).await {
Ok(None) => {
runtime.set_component_healthy(iota_ipc::ComponentId::Web, Some("disabled".into()))
}
Ok(Some(handle)) => {
runtime.set_component_healthy(iota_ipc::ComponentId::Web, None);
runtime
.tasks
.spawn_tracked("web-server", async move {
handle.join().await;
Ok(())
})
.await;
}
Err(error) if web.required => {
runtime.set_component_failed(iota_ipc::ComponentId::Web, error.to_string());
}
Err(error) => {
runtime.set_component_degraded(iota_ipc::ComponentId::Web, error.to_string());
}
}
runtime.set_startup_phase(StartupPhase::Ready);
log!("iota-daemon started (phase: Ready)");
// --- Main lifecycle loop ---
let signal = async {
#[cfg(unix)]
{
let mut term =
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("SIGTERM handler");
tokio::select! { _ = tokio::signal::ctrl_c() => ShutdownReason::Stop, _ = term.recv() => ShutdownReason::Stop }
}
#[cfg(not(unix))]
{
let _ = tokio::signal::ctrl_c().await;
ShutdownReason::Stop
}
};
tokio::select! {
_ = runtime.cancellation.cancelled() => {},
reason = signal => runtime.shutdown(reason),
}
let reason = runtime.shutdown_reason().unwrap_or(ShutdownReason::Stop);
log!("iota-daemon shutting down (reason: {:?})", reason);
runtime.set_startup_phase(StartupPhase::Stopping);
let _ = runtime
.tasks
.join_with_timeout(Duration::from_secs(5))
.await;
let exit_code = reason.exit_code();
log!("iota-daemon exited (code: {})", exit_code);
ExitCode::from(exit_code as u8)
}
fn resolve_config_path(
config_file: &std::path::Path,
value: &str,
default: &std::path::Path,
) -> std::path::PathBuf {
if value.is_empty() {
return default.to_path_buf();
}
let path = std::path::PathBuf::from(value);
if path.is_absolute() {
path
} else {
config_file
.parent()
.expect("absolute configuration file has a parent")
.join(path)
}
}