iota/iota-ipc/src/protocol.rs
2026-07-25 22:59:25 +02:00

404 lines
10 KiB
Rust

use serde::{Deserialize, Serialize};
// ---------------------------------------------------------------------------
// Client → Daemon
// ---------------------------------------------------------------------------
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
pub enum ClientMessage {
Hello {
supported_versions: Vec<u16>,
},
Subscribe {
log_classes: Vec<String>,
metric_interval_ms: Option<u64>,
},
Request(RequestEnvelope),
Ping {
seq: u64,
},
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct RequestEnvelope {
pub request_id: u64,
pub protocol_version: u16,
pub request: LocalRequest,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
pub enum LocalRequest {
GetStatus,
ListTasks,
ListUsers,
CreateUser {
username: String,
},
RemoveUser {
user_id: i64,
},
ReconnectOmikron,
RotateIotaIdentity,
RequestProcessExit {
intent: ExitIntent,
},
GetDaemonStatus,
#[serde(skip)]
RestartDaemon,
#[serde(skip)]
StopDaemon,
GetConfig,
SetConfig {
key: String,
value: String,
},
ReloadConfig,
GetOmikronStatus,
ListComponents,
GetUser {
user_id: i64,
},
ImportUser {
username: String,
},
GetLogs {
limit: usize,
},
CheckUpdate,
ListCommunities,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ExitIntent {
Stop,
Restart,
}
// ---------------------------------------------------------------------------
// Daemon → Client
// ---------------------------------------------------------------------------
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
pub enum DaemonMessage {
HelloAck(HelloAck),
/// Confirms that the server has installed this connection's subscription.
Subscribed,
LogEntry(LogEntry),
StateUpdate(StateSnapshot),
MetricSample(MetricSample),
Response(ResponseEnvelope),
Pong {
seq: u64,
},
LifecycleEvent(LifecycleEvent),
Gap {
skipped: u64,
},
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct HelloAck {
pub protocol_version: u16,
pub daemon_version: String,
pub instance_id: String,
pub startup_phase: StartupPhase,
pub capabilities: Vec<String>,
#[serde(default)]
pub lifecycle: LifecyclePhase,
#[serde(default)]
pub health: HealthStatus,
#[serde(default)]
pub deployment_mode: DeploymentMode,
#[serde(default)]
pub supervisor: SupervisorKind,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ResponseEnvelope {
pub request_id: u64,
pub result: ResponseResult,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
pub enum ResponseResult {
Ok(ResponsePayload),
Error(IpcErrorCode),
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
pub enum ResponsePayload {
Status(StatusResponse),
Tasks(Vec<TaskSummary>),
Users(Vec<UserSummary>),
UserCreated { user_id: i64, username: String },
UserRemoved { user_id: i64 },
Acknowledged { message: String },
DaemonStatus(DaemonStatusResponse),
Config(ConfigResponse),
OmikronStatus(OmikronStatusResponse),
Components(Vec<ComponentStatusResponse>),
UserDetail(UserDetailResponse),
LogEntries(LogEntriesResponse),
UpdateStatus(UpdateStatusResponse),
Communities(Vec<CommunitySummary>),
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ConfigResponse {
pub yaml: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct OmikronStatusResponse {
pub connected: bool,
pub iota_id: Option<u64>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ComponentStatusResponse {
pub id: ComponentId,
pub status: HealthStatus,
pub message: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct UserDetailResponse {
pub user_id: i64,
pub username: String,
pub display_name: Option<String>,
pub created_at: i64,
pub trusted_apps: Vec<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct LogEntriesResponse {
pub entries: Vec<LogEntry>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct UpdateStatusResponse {
pub available: bool,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct CommunitySummary {
pub name: String,
pub title: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct StatusResponse {
pub phase: String,
pub tasks: Vec<String>,
pub degraded_reason: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct TaskSummary {
pub name: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct UserSummary {
pub user_id: i64,
pub username: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct DaemonStatusResponse {
pub formatted: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum IpcErrorCode {
InvalidRequest,
NotFound,
Conflict,
StorageFailure,
OmikronUnavailable,
UnsupportedVersion,
NotReady,
Disconnected,
Timeout,
Cancelled,
Unauthorized,
InternalFailure,
}
impl std::fmt::Display for IpcErrorCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::InvalidRequest => "the daemon rejected the request",
Self::NotFound => "the requested resource was not found",
Self::Conflict => "the request conflicts with current daemon state",
Self::StorageFailure => "the daemon could not access local storage",
Self::OmikronUnavailable => "Omikron is unavailable",
Self::UnsupportedVersion => "the client and daemon protocol versions are incompatible",
Self::NotReady => "the daemon is not ready yet",
Self::Disconnected => "the daemon connection was lost",
Self::Timeout => "the daemon did not respond in time",
Self::Cancelled => "the daemon cancelled the request",
Self::Unauthorized => "the daemon denied this operation",
Self::InternalFailure => "the daemon encountered an internal failure",
})
}
}
#[cfg(test)]
mod error_tests {
use super::IpcErrorCode;
#[test]
fn error_codes_have_operator_facing_messages() {
assert_eq!(
IpcErrorCode::NotReady.to_string(),
"the daemon is not ready yet"
);
assert!(
!IpcErrorCode::InternalFailure
.to_string()
.contains("InternalFailure")
);
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
pub enum LifecycleEvent {
StateChanged(ConnectionStatus),
Shutdown { reason: String },
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConnectionStatus {
Connected,
Reconnecting,
Degraded,
Disconnected,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StartupPhase {
Starting,
MigratingStorage,
LoadingUsers,
StartingServices,
Ready,
Degraded,
Stopping,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LifecyclePhase {
#[default]
Starting,
Ready,
Stopping,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HealthStatus {
#[default]
Healthy,
Degraded,
Failed,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ComponentId {
Storage,
Ipc,
Omikron,
Web,
Updater,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DeploymentMode {
SessionChild,
UiAutoStart,
UserService,
SystemSocketActivated,
SystemAlwaysOn,
#[default]
External,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SupervisorKind {
#[default]
None,
IotaUi,
Systemd,
External,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ComponentHealth {
pub status: HealthStatus,
pub message: Option<String>,
pub changed_at_ms: u128,
}
impl Default for StartupPhase {
fn default() -> Self {
Self::Starting
}
}
// ---------------------------------------------------------------------------
// Shared types
// ---------------------------------------------------------------------------
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct LogEntry {
pub timestamp_ms: u128,
pub sender: String,
pub message: String,
pub is_error: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct StateSnapshot {
pub cpu: Vec<(f64, f64)>,
pub ram: Vec<(f64, f64)>,
pub ping: Vec<(f64, f64)>,
pub net_up: Vec<(f64, f64)>,
pub net_down: Vec<(f64, f64)>,
pub sys_info: String,
#[serde(default)]
pub startup_phase: StartupPhase,
#[serde(default)]
pub degraded_reason: Option<String>,
#[serde(default)]
pub lifecycle: LifecyclePhase,
#[serde(default)]
pub startup_step: Option<String>,
#[serde(default)]
pub overall_health: HealthStatus,
#[serde(default)]
pub components: std::collections::BTreeMap<ComponentId, ComponentHealth>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct MetricSample {
pub cpu: Option<f64>,
pub ram: Option<f64>,
pub ping: Option<f64>,
pub net_up: Option<f64>,
pub net_down: Option<f64>,
}