[Wip] CLI & Daemon

This commit is contained in:
Alex 2026-07-25 18:23:36 +02:00
commit 6a535099bb
Signed by: alex
SSH key fingerprint: SHA256:D1+Ub8o0v4K5y1JNivW8IxEOelqLSvPmUzBbDIoZkRQ
44 changed files with 4417 additions and 303 deletions

View file

@ -1,10 +1,16 @@
use crate::{DaemonRuntime, DaemonServices};
use iota_ipc::{ExitIntent, IpcErrorCode, LocalRequest, ResponseEnvelope, ResponseResult};
use crate::log_buffer::LogBuffer;
use iota_ipc::{
ComponentStatusResponse, CommunitySummary, ConfigResponse, ExitIntent, IpcErrorCode,
LogEntriesResponse, LocalRequest, OmikronStatusResponse, ResponseEnvelope, ResponsePayload,
ResponseResult, StatusResponse, TaskSummary, UpdateStatusResponse, UserDetailResponse,
UserSummary,
};
use iota_logger::{log, log_command};
use iota_storage::users::user_manager;
use iota_storage::util::config_util::modify_config;
use iota_storage::util::config_util::{self, modify_config};
use mtp::codec::{CommunicationType, CommunicationValue};
use std::sync::Arc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use crate::daemon_state::{ShutdownReason, StartupPhase};
@ -13,11 +19,12 @@ use crate::daemon_state::{ShutdownReason, StartupPhase};
pub struct CommandRouter {
runtime: Arc<DaemonRuntime>,
services: Arc<DaemonServices>,
log_buffer: Arc<Mutex<LogBuffer>>,
}
impl CommandRouter {
pub fn new(runtime: Arc<DaemonRuntime>, services: Arc<DaemonServices>) -> Self {
Self { runtime, services }
pub fn new(runtime: Arc<DaemonRuntime>, services: Arc<DaemonServices>, log_buffer: Arc<Mutex<LogBuffer>>) -> Self {
Self { runtime, services, log_buffer }
}
pub async fn route(&self, request_id: u64, request: LocalRequest) -> ResponseEnvelope {
@ -26,35 +33,6 @@ impl CommandRouter {
ResponseEnvelope { request_id, result }
}
/// Parse a legacy console command string into a typed request.
pub fn parse_console_command(line: &str) -> Option<LocalRequest> {
let parts: Vec<&str> = line.trim_start_matches('/').split_whitespace().collect();
match parts.as_slice() {
["help"] => None,
["tasks"] => Some(LocalRequest::ListTasks),
["ping", _] | ["ping"] => None,
["user", "add", username] => Some(LocalRequest::CreateUser {
username: username.to_string(),
}),
["user", "remove", username] => {
let user = user_manager::get_user_by_username(username)?;
Some(LocalRequest::RemoveUser {
user_id: user.user_id,
})
}
["user", "list"] => Some(LocalRequest::ListUsers),
["reconnect"] => Some(LocalRequest::ReconnectOmikron),
["regenerate", "keys"] => Some(LocalRequest::RotateIotaIdentity),
["reload"] | ["restart"] => Some(LocalRequest::RequestProcessExit {
intent: ExitIntent::Restart,
}),
["shutdown"] | ["stop"] => Some(LocalRequest::RequestProcessExit {
intent: ExitIntent::Stop,
}),
_ => None,
}
}
async fn execute(&self, request: LocalRequest) -> ResponseResult {
let needs_omikron = matches!(
request,
@ -83,28 +61,33 @@ impl CommandRouter {
.iter()
.map(|task| task.to_string())
.collect();
let mut info = format!("Phase: {:?}, Tasks: {}", phase, tasks.join(", "));
if let Some(reason) = degraded {
info.push_str(&format!(", Degraded: {}", reason));
}
ResponseResult::Ok(info)
ResponseResult::Ok(ResponsePayload::Status(StatusResponse {
phase: format!("{:?}", phase),
tasks: tasks.clone(),
degraded_reason: degraded,
}))
}
LocalRequest::ListTasks => {
let tasks: Vec<String> = self
let tasks: Vec<TaskSummary> = self
.runtime
.state
.active_tasks
.iter()
.map(|task| task.to_string())
.map(|task| TaskSummary {
name: task.to_string(),
})
.collect();
ResponseResult::Ok(tasks.join(", "))
ResponseResult::Ok(ResponsePayload::Tasks(tasks))
}
LocalRequest::ListUsers => {
let users: Vec<String> = user_manager::get_users()
let users: Vec<UserSummary> = user_manager::get_users()
.into_iter()
.map(|user| format!("{} ({})", user.username, user.user_id))
.map(|user| UserSummary {
user_id: user.user_id,
username: user.username,
})
.collect();
ResponseResult::Ok(users.join("\n"))
ResponseResult::Ok(ResponsePayload::Users(users))
}
LocalRequest::CreateUser { username } => {
match omikron_connector::user_ops::create_user(
@ -113,7 +96,12 @@ impl CommandRouter {
)
.await
{
(Some(user), _) => ResponseResult::Ok(format!("Created user {}", user.user_id)),
(Some(user), _) => {
ResponseResult::Ok(ResponsePayload::UserCreated {
user_id: user.user_id,
username: user.username,
})
}
_ => ResponseResult::Error(IpcErrorCode::StorageFailure),
}
}
@ -128,10 +116,12 @@ impl CommandRouter {
return ResponseResult::Error(IpcErrorCode::OmikronUnavailable);
}
user_manager::remove_user(user.user_id);
ResponseResult::Ok(format!("Removed user {}", user.user_id))
ResponseResult::Ok(ResponsePayload::UserRemoved { user_id })
}
LocalRequest::ReconnectOmikron => match self.services.omikron.reconnect().await {
Ok(()) => ResponseResult::Ok("Reconnected to Omikron server".into()),
Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged {
message: "Reconnected to Omikron server".into(),
}),
Err(_) => ResponseResult::Error(IpcErrorCode::OmikronUnavailable),
},
LocalRequest::RotateIotaIdentity => {
@ -141,9 +131,9 @@ impl CommandRouter {
config.iota_id = None;
});
match self.services.omikron.reconnect().await {
Ok(()) => ResponseResult::Ok(
"Key pair regenerated and Omikron reconnection requested".into(),
),
Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged {
message: "Key pair regenerated and Omikron reconnection requested".into(),
}),
Err(_) => ResponseResult::Error(IpcErrorCode::OmikronUnavailable),
}
}
@ -160,18 +150,120 @@ impl CommandRouter {
ExitIntent::Stop => ShutdownReason::Stop,
ExitIntent::Restart => ShutdownReason::Restart,
});
ResponseResult::Ok("process exit accepted".into())
ResponseResult::Ok(ResponsePayload::Acknowledged {
message: "process exit accepted".into(),
})
}
LocalRequest::GetDaemonStatus => {
ResponseResult::Ok(format!("{:?}", self.runtime.snapshot()))
ResponseResult::Ok(ResponsePayload::DaemonStatus(
iota_ipc::DaemonStatusResponse {
formatted: format!("{:?}", self.runtime.snapshot()),
},
))
}
LocalRequest::RestartDaemon => {
self.runtime.shutdown(ShutdownReason::Restart);
ResponseResult::Ok("Daemon restart requested".into())
ResponseResult::Ok(ResponsePayload::Acknowledged {
message: "Daemon restart requested".into(),
})
}
LocalRequest::StopDaemon => {
self.runtime.shutdown(ShutdownReason::Stop);
ResponseResult::Ok("Daemon shutdown requested".into())
ResponseResult::Ok(ResponsePayload::Acknowledged {
message: "Daemon shutdown requested".into(),
})
}
LocalRequest::GetConfig => {
let cfg = config_util::CONFIG.load();
let yaml = serde_yaml::to_string(&**cfg).unwrap_or_default();
ResponseResult::Ok(ResponsePayload::Config(ConfigResponse { yaml }))
}
LocalRequest::SetConfig { key, value } => {
match config_util::modify_config_value(&key, &value) {
Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged {
message: format!("Set {key} = {value}"),
}),
Err(_e) => ResponseResult::Error(IpcErrorCode::InvalidRequest),
}
}
LocalRequest::ReloadConfig => {
config_util::load_config();
ResponseResult::Ok(ResponsePayload::Acknowledged {
message: "Configuration reloaded".into(),
})
}
LocalRequest::GetOmikronStatus => {
let connected = self.services.omikron.is_connected().await;
let iota_id = config_util::CONFIG.load().iota_id;
ResponseResult::Ok(ResponsePayload::OmikronStatus(
OmikronStatusResponse {
connected,
iota_id,
},
))
}
LocalRequest::ListComponents => {
let snapshot = self.runtime.snapshot();
let components: Vec<ComponentStatusResponse> = snapshot
.components
.into_iter()
.map(|(id, health)| ComponentStatusResponse {
id,
status: health.status,
message: health.message,
})
.collect();
ResponseResult::Ok(ResponsePayload::Components(components))
}
LocalRequest::GetUser { user_id } => {
match user_manager::get_user(user_id) {
Some(user) => ResponseResult::Ok(ResponsePayload::UserDetail(
UserDetailResponse {
user_id: user.user_id,
username: user.username,
display_name: user.display_name,
created_at: user.created_at,
trusted_apps: user.trusted_apps.keys().cloned().collect(),
},
)),
None => ResponseResult::Error(IpcErrorCode::NotFound),
}
}
LocalRequest::ImportUser { username } => {
match user_manager::load_from_tu(&username).await {
Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged {
message: format!("Imported user {username}"),
}),
Err(()) => ResponseResult::Error(IpcErrorCode::StorageFailure),
}
}
LocalRequest::GetLogs { limit } => {
let entries = if let Ok(buf) = self.log_buffer.lock() {
buf.recent(limit)
} else {
Vec::new()
};
ResponseResult::Ok(ResponsePayload::LogEntries(LogEntriesResponse { entries }))
}
LocalRequest::CheckUpdate => {
match iota_updater::check_update().await {
Ok(available) => ResponseResult::Ok(ResponsePayload::UpdateStatus(
UpdateStatusResponse { available },
)),
Err(_e) => ResponseResult::Error(IpcErrorCode::InternalFailure),
}
}
LocalRequest::ListCommunities => {
let iota_id = config_util::CONFIG.load().iota_id.map(|id| id as i64).unwrap_or(0);
let stored = iota_storage::util::communities_util::CommunitiesUtil::get_communities(iota_id);
let summaries: Vec<CommunitySummary> = stored
.into_iter()
.map(|c| CommunitySummary {
name: c.address,
title: c.title,
})
.collect();
ResponseResult::Ok(ResponsePayload::Communities(summaries))
}
}
}