[Wip] CLI & Daemon
This commit is contained in:
parent
3bc5cc959a
commit
6a535099bb
44 changed files with 4417 additions and 303 deletions
|
|
@ -9,12 +9,14 @@ iota-ipc = { path = "../iota-ipc" }
|
|||
iota-logger = { path = "../iota-logger" }
|
||||
iota-state = { path = "../iota-state" }
|
||||
iota-storage = { path = "../iota-storage" }
|
||||
iota-updater = { path = "../iota-updater" }
|
||||
iota-util = { path = "../iota-util" }
|
||||
omikron-connector = { path = "../omikron-connector" }
|
||||
mtp = { git = "https://git.methanium.net/Methanium/mtp.git" }
|
||||
dashmap = "6.1.0"
|
||||
libc = "0.2"
|
||||
sysinfo = "0.38.3"
|
||||
serde_yaml = "0.9"
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
tokio-util = { version = "0.7", features = ["rt"] }
|
||||
uuid = { version = "*", features = ["v4"] }
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use crate::log_buffer::LogBuffer;
|
||||
use crate::deployment::from_environment;
|
||||
use crate::{CommandRouter, DaemonRuntime, DaemonServices};
|
||||
use iota_ipc::{
|
||||
|
|
@ -8,7 +9,7 @@ use iota_logger::log;
|
|||
use std::io::Result;
|
||||
use std::os::unix::fs::{FileTypeExt, MetadataExt, OpenOptionsExt};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::{env, fs::File, os::fd::FromRawFd, os::unix::net::UnixListener as StdUnixListener};
|
||||
use tokio::net::{UnixListener, UnixStream};
|
||||
use tokio::sync::{broadcast, mpsc, watch};
|
||||
|
|
@ -22,11 +23,25 @@ const CLIENT_CHANNEL_SIZE: usize = 256;
|
|||
const MAX_HANDSHAKE_RETRIES: u32 = 1;
|
||||
const CLIENT_IO_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
|
||||
|
||||
/// Minimum metric subscription interval to prevent excessive update rates.
|
||||
const MIN_METRIC_INTERVAL_MS: u64 = 100;
|
||||
/// Maximum metric subscription interval.
|
||||
const MAX_METRIC_INTERVAL_MS: u64 = 60_000;
|
||||
/// Default metric interval if the client does not specify one.
|
||||
const DEFAULT_METRIC_INTERVAL_MS: u64 = 500;
|
||||
|
||||
/// Per-client subscription state.
|
||||
struct ClientSubscription {
|
||||
log_classes: Vec<String>,
|
||||
metric_interval_ms: u64,
|
||||
}
|
||||
|
||||
pub struct IpcServer {
|
||||
listener: UnixListener,
|
||||
runtime: Arc<DaemonRuntime>,
|
||||
services: Arc<DaemonServices>,
|
||||
log_tx: broadcast::Sender<DaemonMessage>,
|
||||
log_buffer: Arc<Mutex<LogBuffer>>,
|
||||
state_rx: watch::Receiver<iota_ipc::StateSnapshot>,
|
||||
instance_id: String,
|
||||
_instance_lock: File,
|
||||
|
|
@ -38,6 +53,7 @@ impl IpcServer {
|
|||
runtime: Arc<DaemonRuntime>,
|
||||
services: Arc<DaemonServices>,
|
||||
log_tx: broadcast::Sender<DaemonMessage>,
|
||||
log_buffer: Arc<Mutex<LogBuffer>>,
|
||||
state_rx: watch::Receiver<iota_ipc::StateSnapshot>,
|
||||
) -> Result<Self> {
|
||||
let path = path.into();
|
||||
|
|
@ -90,6 +106,7 @@ impl IpcServer {
|
|||
runtime,
|
||||
services,
|
||||
log_tx,
|
||||
log_buffer,
|
||||
state_rx,
|
||||
instance_id: Uuid::new_v4().to_string(),
|
||||
_instance_lock: lock,
|
||||
|
|
@ -101,6 +118,7 @@ impl IpcServer {
|
|||
runtime,
|
||||
services,
|
||||
log_tx,
|
||||
log_buffer,
|
||||
state_rx,
|
||||
instance_id: Uuid::new_v4().to_string(),
|
||||
_instance_lock: File::options().read(true).open("/dev/null")?,
|
||||
|
|
@ -114,11 +132,12 @@ impl IpcServer {
|
|||
let runtime = self.runtime.clone();
|
||||
let services = self.services.clone();
|
||||
let log_tx = self.log_tx.clone();
|
||||
let log_buffer = self.log_buffer.clone();
|
||||
let state_rx = self.state_rx.clone();
|
||||
let instance_id = self.instance_id.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(error) =
|
||||
handle_client(stream, runtime, services, log_tx, state_rx, instance_id).await
|
||||
handle_client(stream, runtime, services, log_tx, log_buffer, state_rx, instance_id).await
|
||||
{
|
||||
eprintln!("IPC client error: {error}");
|
||||
}
|
||||
|
|
@ -252,6 +271,7 @@ async fn handle_client(
|
|||
runtime: Arc<DaemonRuntime>,
|
||||
services: Arc<DaemonServices>,
|
||||
log_tx: broadcast::Sender<DaemonMessage>,
|
||||
log_buffer: Arc<Mutex<LogBuffer>>,
|
||||
mut state_rx: watch::Receiver<iota_ipc::StateSnapshot>,
|
||||
instance_id: String,
|
||||
) -> Result<()> {
|
||||
|
|
@ -337,12 +357,18 @@ async fn handle_client(
|
|||
|
||||
// --- Writer task: merge directed responses + shared log events ---
|
||||
let mut log_rx = log_tx.subscribe();
|
||||
let (sub_tx, mut sub_rx) = tokio::sync::watch::channel(ClientSubscription {
|
||||
log_classes: Vec::new(),
|
||||
metric_interval_ms: DEFAULT_METRIC_INTERVAL_MS,
|
||||
});
|
||||
let writer_task = {
|
||||
let runtime = runtime.clone();
|
||||
let session_cancellation = session_cancellation.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut directed_rx = directed_rx;
|
||||
let mut last_metric_sent = tokio::time::Instant::now();
|
||||
loop {
|
||||
let metric_interval = sub_rx.borrow().metric_interval_ms;
|
||||
tokio::select! {
|
||||
// Directed messages (responses to this client's requests)
|
||||
msg = directed_rx.recv() => {
|
||||
|
|
@ -360,9 +386,35 @@ async fn handle_client(
|
|||
// Shared log events
|
||||
result = log_rx.recv() => {
|
||||
match result {
|
||||
Ok(DaemonMessage::LogEntry(entry)) => {
|
||||
// Filter by subscribed log classes
|
||||
let log_classes = sub_rx.borrow().log_classes.clone();
|
||||
if log_classes.is_empty()
|
||||
|| log_classes.iter().any(|c| entry.sender == *c)
|
||||
{
|
||||
if let Err(error) = write_client_message(&mut writer, &DaemonMessage::LogEntry(entry)).await {
|
||||
eprintln!("IPC client writer stopped while sending log message: {error}");
|
||||
session_cancellation.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(DaemonMessage::MetricSample(sample)) => {
|
||||
// Rate-limit metric samples based on subscription interval
|
||||
let now = tokio::time::Instant::now();
|
||||
if now.duration_since(last_metric_sent) >= std::time::Duration::from_millis(metric_interval) {
|
||||
last_metric_sent = now;
|
||||
if let Err(error) = write_client_message(&mut writer, &DaemonMessage::MetricSample(sample)).await {
|
||||
eprintln!("IPC client writer stopped while sending metric sample: {error}");
|
||||
session_cancellation.cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(message) => {
|
||||
// Forward other broadcast messages as-is
|
||||
if let Err(error) = write_client_message(&mut writer, &message).await {
|
||||
eprintln!("IPC client writer stopped while sending log message: {error}");
|
||||
eprintln!("IPC client writer stopped while sending broadcast message: {error}");
|
||||
session_cancellation.cancel();
|
||||
break;
|
||||
}
|
||||
|
|
@ -389,13 +441,14 @@ async fn handle_client(
|
|||
break;
|
||||
}
|
||||
}
|
||||
_ = sub_rx.changed() => {}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
// --- Reader loop ---
|
||||
let router = CommandRouter::new(runtime.clone(), services);
|
||||
let router = CommandRouter::new(runtime.clone(), services, log_buffer);
|
||||
loop {
|
||||
let message = tokio::select! {
|
||||
_ = session_cancellation.cancelled() => break,
|
||||
|
|
@ -442,7 +495,17 @@ async fn handle_client(
|
|||
break;
|
||||
}
|
||||
}
|
||||
Ok(ClientMessage::Subscribe { .. }) => {
|
||||
Ok(ClientMessage::Subscribe {
|
||||
log_classes,
|
||||
metric_interval_ms,
|
||||
}) => {
|
||||
let interval = metric_interval_ms
|
||||
.unwrap_or(DEFAULT_METRIC_INTERVAL_MS)
|
||||
.clamp(MIN_METRIC_INTERVAL_MS, MAX_METRIC_INTERVAL_MS);
|
||||
let _ = sub_tx.send(ClientSubscription {
|
||||
log_classes,
|
||||
metric_interval_ms: interval,
|
||||
});
|
||||
let snapshot = DaemonMessage::StateUpdate(runtime.snapshot());
|
||||
let _ = directed_tx.send(snapshot).await;
|
||||
let _ = directed_tx.send(DaemonMessage::Subscribed).await;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ pub mod daemon_state;
|
|||
pub mod deployment;
|
||||
pub mod ipc_server;
|
||||
pub mod log_broadcaster;
|
||||
pub mod log_buffer;
|
||||
pub mod services;
|
||||
pub mod task_registry;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,30 @@
|
|||
use crate::log_buffer::LogBuffer;
|
||||
use iota_ipc::{DaemonMessage, LogEntry};
|
||||
use iota_logger::subscribe;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
/* The daemon adapts logger output to the wire protocol so the logger stays
|
||||
* independent from both the socket implementation and TUI state. */
|
||||
pub fn spawn(message_tx: broadcast::Sender<DaemonMessage>) {
|
||||
pub fn spawn(
|
||||
message_tx: broadcast::Sender<DaemonMessage>,
|
||||
buffer: Arc<Mutex<LogBuffer>>,
|
||||
) {
|
||||
let Some(mut logs) = subscribe() else {
|
||||
return;
|
||||
};
|
||||
tokio::spawn(async move {
|
||||
while let Ok(entry) = logs.recv().await {
|
||||
let _ = message_tx.send(DaemonMessage::LogEntry(LogEntry {
|
||||
let entry = LogEntry {
|
||||
timestamp_ms: entry.timestamp_ms,
|
||||
sender: entry.sender,
|
||||
message: entry.message,
|
||||
is_error: entry.is_error,
|
||||
}));
|
||||
};
|
||||
if let Ok(mut buf) = buffer.lock() {
|
||||
buf.push(entry.clone());
|
||||
}
|
||||
let _ = message_tx.send(DaemonMessage::LogEntry(entry));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
36
iota-daemon-lib/src/log_buffer.rs
Normal file
36
iota-daemon-lib/src/log_buffer.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
use iota_ipc::LogEntry;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
pub struct LogBuffer {
|
||||
entries: VecDeque<LogEntry>,
|
||||
capacity: usize,
|
||||
}
|
||||
|
||||
impl LogBuffer {
|
||||
pub fn new(capacity: usize) -> Self {
|
||||
Self {
|
||||
entries: VecDeque::with_capacity(capacity),
|
||||
capacity,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, entry: LogEntry) {
|
||||
if self.entries.len() == self.capacity {
|
||||
self.entries.pop_front();
|
||||
}
|
||||
self.entries.push_back(entry);
|
||||
}
|
||||
|
||||
pub fn recent(&self, limit: usize) -> Vec<LogEntry> {
|
||||
let _len = self.entries.len();
|
||||
self.entries
|
||||
.iter()
|
||||
.rev()
|
||||
.take(limit)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
use async_trait::async_trait;
|
||||
use iota_daemon_lib::{CommandRouter, DaemonRuntime, DaemonServices};
|
||||
use iota_daemon_lib::log_buffer::LogBuffer;
|
||||
use iota_ipc::{LocalRequest, ResponseResult};
|
||||
use mtp::codec::CommunicationValue;
|
||||
use omikron_connector::{OmikronClient, OmikronError};
|
||||
use std::sync::{
|
||||
Arc,
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
|
@ -43,7 +44,7 @@ async fn reconnect_uses_the_injected_client() {
|
|||
users: Default::default(),
|
||||
config: Default::default(),
|
||||
});
|
||||
let router = CommandRouter::new(Arc::new(DaemonRuntime::new()), services);
|
||||
let router = CommandRouter::new(Arc::new(DaemonRuntime::new()), services, Arc::new(Mutex::new(LogBuffer::new(100))));
|
||||
assert!(matches!(
|
||||
router.route(1, LocalRequest::ReconnectOmikron).await.result,
|
||||
ResponseResult::Ok(_)
|
||||
|
|
|
|||
Loading…
Reference in a new issue