[Fix] IPC, cli, daemon
This commit is contained in:
parent
36a70e82a0
commit
56aad3a023
32 changed files with 1356 additions and 399 deletions
|
|
@ -1,43 +1,472 @@
|
|||
use iota_ipc::{ClientMessage, DaemonMessage, read_msg, write_msg};
|
||||
use iota_ipc::{
|
||||
ClientMessage, DaemonMessage, LocalRequest, MIN_PROTOCOL_VERSION, PROTOCOL_VERSION,
|
||||
RequestEnvelope, ResponseResult, read_msg, write_msg,
|
||||
};
|
||||
use iota_state::{ClientState, UiLogEntry};
|
||||
use std::collections::HashMap;
|
||||
use std::io::Result;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::Duration;
|
||||
use tokio::net::UnixStream;
|
||||
use tokio::net::unix::OwnedWriteHalf;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::net::unix::{OwnedReadHalf, OwnedWriteHalf};
|
||||
use tokio::sync::{Mutex, oneshot, watch};
|
||||
|
||||
const INITIAL_BACKOFF: Duration = Duration::from_millis(200);
|
||||
const MAX_BACKOFF: Duration = Duration::from_secs(10);
|
||||
const MAX_RECONNECT_ATTEMPTS: u32 = 50;
|
||||
|
||||
/// Connection state exposed to the UI.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum IpcConnectionState {
|
||||
Connecting,
|
||||
Connected,
|
||||
Reconnecting { attempt: u32 },
|
||||
Incompatible { message: String },
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
/// Pending request awaiting a response.
|
||||
struct PendingRequest {
|
||||
response_tx: oneshot::Sender<ResponseResult>,
|
||||
}
|
||||
|
||||
/* The TUI owns this cache. IPC updates replace daemon snapshots and append
|
||||
* logs, so rendering never reaches into daemon-owned storage or connections. */
|
||||
pub struct IpcClient {
|
||||
state: ClientState,
|
||||
writer: Mutex<OwnedWriteHalf>,
|
||||
next_request_id: AtomicU64,
|
||||
pending: Mutex<HashMap<u64, PendingRequest>>,
|
||||
connection_state: watch::Sender<IpcConnectionState>,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl IpcClient {
|
||||
pub async fn connect(path: impl AsRef<Path>) -> Result<Arc<Self>> {
|
||||
let stream = UnixStream::connect(path).await?;
|
||||
let path = path.as_ref().to_path_buf();
|
||||
let stream = Self::try_connect(&path).await?;
|
||||
let (mut reader, writer) = stream.into_split();
|
||||
|
||||
let (conn_state_tx, _) = watch::channel(IpcConnectionState::Connecting);
|
||||
let client = Arc::new(Self {
|
||||
state: ClientState::new(),
|
||||
writer: Mutex::new(writer),
|
||||
next_request_id: AtomicU64::new(1),
|
||||
pending: Mutex::new(HashMap::new()),
|
||||
connection_state: conn_state_tx,
|
||||
path: path.clone(),
|
||||
});
|
||||
|
||||
// --- Handshake: send Hello, read HelloAck ---
|
||||
{
|
||||
let mut w = client.writer.lock().await;
|
||||
write_msg(
|
||||
&mut *w,
|
||||
&ClientMessage::Hello {
|
||||
supported_versions: vec![MIN_PROTOCOL_VERSION, PROTOCOL_VERSION],
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
match read_msg::<_, DaemonMessage>(&mut reader).await {
|
||||
Ok(DaemonMessage::HelloAck(ack)) => {
|
||||
if ack.protocol_version < MIN_PROTOCOL_VERSION {
|
||||
let _ = client
|
||||
.connection_state
|
||||
.send(IpcConnectionState::Incompatible {
|
||||
message: format!(
|
||||
"Daemon protocol {} < required {}",
|
||||
ack.protocol_version, MIN_PROTOCOL_VERSION
|
||||
),
|
||||
});
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Unsupported,
|
||||
format!(
|
||||
"Protocol version mismatch: daemon={}, minimum={}",
|
||||
ack.protocol_version, MIN_PROTOCOL_VERSION
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"Expected HelloAck from daemon",
|
||||
));
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
|
||||
let _ = client.connection_state.send(IpcConnectionState::Connected);
|
||||
|
||||
// Start reader task (continues reading after handshake)
|
||||
let reader_client = client.clone();
|
||||
tokio::spawn(async move {
|
||||
while let Ok(message) = read_msg::<_, DaemonMessage>(&mut reader).await {
|
||||
reader_client.apply(message).await;
|
||||
}
|
||||
reader_client.read_loop(reader).await;
|
||||
});
|
||||
client.send(ClientMessage::Subscribe).await?;
|
||||
|
||||
// Subscribe to events
|
||||
client
|
||||
.send(ClientMessage::Subscribe {
|
||||
log_classes: vec![],
|
||||
metric_interval_ms: Some(500),
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
/// Try to connect with retries for socket activation.
|
||||
pub async fn connect_or_activate(path: impl AsRef<Path>) -> Result<Arc<Self>> {
|
||||
let path = path.as_ref().to_path_buf();
|
||||
let max_attempts = 30;
|
||||
for attempt in 0..max_attempts {
|
||||
match Self::connect(&path).await {
|
||||
Ok(client) => return Ok(client),
|
||||
Err(error) => {
|
||||
if attempt < max_attempts - 1 {
|
||||
let delay = Duration::from_millis(100 + attempt as u64 * 100);
|
||||
tokio::time::sleep(delay).await;
|
||||
continue;
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn try_connect(path: &Path) -> Result<UnixStream> {
|
||||
let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
|
||||
loop {
|
||||
match UnixStream::connect(path).await {
|
||||
Ok(stream) => return Ok(stream),
|
||||
Err(error) => {
|
||||
if tokio::time::Instant::now() >= deadline {
|
||||
return Err(error);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the reconnection actor.
|
||||
pub fn spawn_reconnector(self: &Arc<Self>) {
|
||||
let client = self.clone();
|
||||
tokio::spawn(async move {
|
||||
client.reconnection_loop().await;
|
||||
});
|
||||
}
|
||||
|
||||
async fn reconnection_loop(self: Arc<Self>) {
|
||||
let mut rx = self.connection_status();
|
||||
|
||||
loop {
|
||||
// Wait until the connection enters the Disconnected state.
|
||||
loop {
|
||||
let disconnected = matches!(*rx.borrow(), IpcConnectionState::Disconnected);
|
||||
if disconnected {
|
||||
break;
|
||||
}
|
||||
if rx.changed().await.is_err() {
|
||||
return; // sender dropped
|
||||
}
|
||||
}
|
||||
|
||||
let mut backoff = INITIAL_BACKOFF;
|
||||
let mut attempt: u32 = 0;
|
||||
|
||||
// Attempt reconnection until success or max attempts.
|
||||
loop {
|
||||
tokio::time::sleep(backoff).await;
|
||||
attempt += 1;
|
||||
|
||||
if attempt > MAX_RECONNECT_ATTEMPTS {
|
||||
let _ = self
|
||||
.connection_state
|
||||
.send(IpcConnectionState::Incompatible {
|
||||
message: "Max reconnection attempts exceeded".into(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let _ = self
|
||||
.connection_state
|
||||
.send(IpcConnectionState::Reconnecting { attempt });
|
||||
|
||||
match Self::try_connect(&self.path).await {
|
||||
Ok(stream) => {
|
||||
let (mut reader, writer) = stream.into_split();
|
||||
*self.writer.lock().await = writer;
|
||||
|
||||
// Re-handshake
|
||||
{
|
||||
let mut w = self.writer.lock().await;
|
||||
if write_msg(
|
||||
&mut *w,
|
||||
&ClientMessage::Hello {
|
||||
supported_versions: vec![
|
||||
MIN_PROTOCOL_VERSION,
|
||||
PROTOCOL_VERSION,
|
||||
],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
let _ = self
|
||||
.connection_state
|
||||
.send(IpcConnectionState::Disconnected);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Read HelloAck
|
||||
match read_msg::<_, DaemonMessage>(&mut reader).await {
|
||||
Ok(DaemonMessage::HelloAck(ack)) => {
|
||||
if ack.protocol_version < MIN_PROTOCOL_VERSION {
|
||||
let _ = self.connection_state.send(
|
||||
IpcConnectionState::Incompatible {
|
||||
message: format!(
|
||||
"Daemon protocol {} < required {}",
|
||||
ack.protocol_version, MIN_PROTOCOL_VERSION
|
||||
),
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let _ = self
|
||||
.connection_state
|
||||
.send(IpcConnectionState::Disconnected);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear pending requests with connection-lost errors
|
||||
{
|
||||
let mut pending = self.pending.lock().await;
|
||||
for (_, request) in pending.drain() {
|
||||
let _ = request.response_tx.send(
|
||||
ResponseResult::Error(
|
||||
iota_ipc::IpcErrorCode::Disconnected,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let _ = self.connection_state.send(IpcConnectionState::Connected);
|
||||
|
||||
// Start new reader loop
|
||||
let reader_client = self.clone();
|
||||
tokio::spawn(async move {
|
||||
reader_client.read_loop(reader).await;
|
||||
});
|
||||
|
||||
// Resubscribe
|
||||
let _ = self
|
||||
.send(ClientMessage::Subscribe {
|
||||
log_classes: vec![],
|
||||
metric_interval_ms: Some(500),
|
||||
})
|
||||
.await;
|
||||
|
||||
// Successfully reconnected; go back to waiting for
|
||||
// the next disconnect.
|
||||
break;
|
||||
}
|
||||
Err(_) => {
|
||||
backoff = std::cmp::min(backoff * 2, MAX_BACKOFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_loop(self: Arc<Self>, mut reader: OwnedReadHalf) {
|
||||
loop {
|
||||
match read_msg::<_, DaemonMessage>(&mut reader).await {
|
||||
Ok(message) => self.apply(message).await,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => {
|
||||
let _ = self.connection_state.send(IpcConnectionState::Disconnected);
|
||||
break;
|
||||
}
|
||||
Err(_) => {
|
||||
let _ = self.connection_state.send(IpcConnectionState::Disconnected);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn state(&self) -> ClientState {
|
||||
self.state.clone()
|
||||
}
|
||||
|
||||
pub async fn send_command(&self, seq: u64, line: String) -> Result<()> {
|
||||
self.send(ClientMessage::Command { seq, line }).await
|
||||
pub fn connection_status(&self) -> watch::Receiver<IpcConnectionState> {
|
||||
self.connection_state.subscribe()
|
||||
}
|
||||
|
||||
pub fn connection_status_snapshot(&self) -> IpcConnectionState {
|
||||
self.connection_state.borrow().clone()
|
||||
}
|
||||
|
||||
pub async fn send_request(&self, request: LocalRequest) -> Result<ResponseResult> {
|
||||
let request_id = self.next_request_id.fetch_add(1, Ordering::Relaxed);
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
|
||||
{
|
||||
let mut pending = self.pending.lock().await;
|
||||
pending.insert(request_id, PendingRequest { response_tx });
|
||||
}
|
||||
|
||||
let envelope = RequestEnvelope {
|
||||
request_id,
|
||||
protocol_version: PROTOCOL_VERSION,
|
||||
request,
|
||||
};
|
||||
self.send(ClientMessage::Request(envelope)).await?;
|
||||
|
||||
match tokio::time::timeout(Duration::from_secs(30), response_rx).await {
|
||||
Ok(Ok(result)) => Ok(result),
|
||||
Ok(Err(_)) => Ok(ResponseResult::Error(iota_ipc::IpcErrorCode::Disconnected)),
|
||||
Err(_) => {
|
||||
self.pending.lock().await.remove(&request_id);
|
||||
Ok(ResponseResult::Error(iota_ipc::IpcErrorCode::Disconnected))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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),
|
||||
["user", "add", username] => Some(LocalRequest::CreateUser {
|
||||
username: username.to_string(),
|
||||
}),
|
||||
["user", "remove", user_id_str] => {
|
||||
let user_id = user_id_str.parse::<i64>().ok()?;
|
||||
Some(LocalRequest::RemoveUser { user_id })
|
||||
}
|
||||
["user", "list"] => Some(LocalRequest::ListUsers),
|
||||
["reconnect"] => Some(LocalRequest::ReconnectOmikron),
|
||||
["regenerate", "keys"] => Some(LocalRequest::RotateIotaIdentity),
|
||||
["reload"] | ["restart"] => Some(LocalRequest::RestartDaemon),
|
||||
["shutdown"] | ["stop"] => Some(LocalRequest::StopDaemon),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Legacy command interface: parse text command, send as typed request.
|
||||
pub async fn send_command(&self, _seq: u64, line: String) -> Result<()> {
|
||||
let trimmed = line.trim_start_matches('/').trim();
|
||||
|
||||
// Handle ping as a direct Ping message (not a LocalRequest).
|
||||
if trimmed == "ping" || trimmed.starts_with("ping ") {
|
||||
let seq = self.next_request_id.fetch_add(1, Ordering::Relaxed);
|
||||
if let Err(e) = self.send(ClientMessage::Ping { seq }).await {
|
||||
let mut state = self
|
||||
.state
|
||||
.app
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
state.push_log(UiLogEntry {
|
||||
timestamp_ms: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis(),
|
||||
sender: "Console".into(),
|
||||
message: format!("Failed to send ping: {}", e),
|
||||
is_error: true,
|
||||
});
|
||||
return Err(e);
|
||||
}
|
||||
let mut state = self
|
||||
.state
|
||||
.app
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
state.push_log(UiLogEntry {
|
||||
timestamp_ms: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis(),
|
||||
sender: "Console".into(),
|
||||
message: "Ping sent".into(),
|
||||
is_error: false,
|
||||
});
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(request) = Self::parse_console_command(&line) {
|
||||
match self.send_request(request).await {
|
||||
Ok(result) => {
|
||||
let mut state = self
|
||||
.state
|
||||
.app
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
let message = match &result {
|
||||
ResponseResult::Ok(msg) => msg.clone(),
|
||||
ResponseResult::Error(code) => format!("Error: {:?}", code),
|
||||
};
|
||||
state.push_log(UiLogEntry {
|
||||
timestamp_ms: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis(),
|
||||
sender: "Command".into(),
|
||||
message,
|
||||
is_error: matches!(&result, ResponseResult::Error(_)),
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
let mut state = self
|
||||
.state
|
||||
.app
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
state.push_log(UiLogEntry {
|
||||
timestamp_ms: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis(),
|
||||
sender: "Console".into(),
|
||||
message: format!("Failed to send: {}", e),
|
||||
is_error: true,
|
||||
});
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut state = self
|
||||
.state
|
||||
.app
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
state.push_log(UiLogEntry {
|
||||
timestamp_ms: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis(),
|
||||
sender: "Console".into(),
|
||||
message: if line.trim() == "help" {
|
||||
"Available commands: tasks, ping, user, reconnect, regenerate, restart, stop"
|
||||
.into()
|
||||
} else {
|
||||
format!("Unknown command: {}", line)
|
||||
},
|
||||
is_error: false,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn send(&self, message: ClientMessage) -> Result<()> {
|
||||
|
|
@ -46,19 +475,26 @@ impl IpcClient {
|
|||
}
|
||||
|
||||
async fn apply(&self, message: DaemonMessage) {
|
||||
let mut state = self
|
||||
.state
|
||||
.app
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
match message {
|
||||
DaemonMessage::LogEntry(entry) => state.push_log(UiLogEntry {
|
||||
timestamp_ms: entry.timestamp_ms,
|
||||
sender: entry.sender,
|
||||
message: entry.message,
|
||||
is_error: entry.is_error,
|
||||
}),
|
||||
DaemonMessage::LogEntry(entry) => {
|
||||
let mut state = self
|
||||
.state
|
||||
.app
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
state.push_log(UiLogEntry {
|
||||
timestamp_ms: entry.timestamp_ms,
|
||||
sender: entry.sender,
|
||||
message: entry.message,
|
||||
is_error: entry.is_error,
|
||||
});
|
||||
}
|
||||
DaemonMessage::StateUpdate(snapshot) => {
|
||||
let mut state = self
|
||||
.state
|
||||
.app
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
state.cpu = snapshot.cpu;
|
||||
state.ram = snapshot.ram;
|
||||
state.ping = snapshot.ping;
|
||||
|
|
@ -66,18 +502,106 @@ impl IpcClient {
|
|||
state.net_down = snapshot.net_down;
|
||||
state.sys_info = snapshot.sys_info;
|
||||
}
|
||||
DaemonMessage::CommandResult {
|
||||
success, message, ..
|
||||
} => state.push_log(UiLogEntry {
|
||||
timestamp_ms: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis(),
|
||||
sender: "Command".into(),
|
||||
message,
|
||||
is_error: !success,
|
||||
}),
|
||||
DaemonMessage::MetricSample(sample) => {
|
||||
let mut state = self
|
||||
.state
|
||||
.app
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
if let Some(cpu) = sample.cpu {
|
||||
let idx = state.cpu.len() as f64;
|
||||
state.cpu.push((idx, cpu));
|
||||
if state.cpu.len() > iota_state::MAX_POINTS {
|
||||
state.cpu.remove(0);
|
||||
}
|
||||
}
|
||||
if let Some(ram) = sample.ram {
|
||||
let idx = state.ram.len() as f64;
|
||||
state.ram.push((idx, ram));
|
||||
if state.ram.len() > iota_state::MAX_POINTS {
|
||||
state.ram.remove(0);
|
||||
}
|
||||
}
|
||||
if let Some(ping) = sample.ping {
|
||||
state.push_ping_val(ping);
|
||||
}
|
||||
if let Some(net_up) = sample.net_up {
|
||||
let idx = state.net_up.len() as f64;
|
||||
state.net_up.push((idx, net_up));
|
||||
if state.net_up.len() > iota_state::MAX_POINTS {
|
||||
state.net_up.remove(0);
|
||||
}
|
||||
}
|
||||
if let Some(net_down) = sample.net_down {
|
||||
let idx = state.net_down.len() as f64;
|
||||
state.net_down.push((idx, net_down));
|
||||
if state.net_down.len() > iota_state::MAX_POINTS {
|
||||
state.net_down.remove(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
DaemonMessage::Response(response) => {
|
||||
let mut pending = self.pending.lock().await;
|
||||
if let Some(request) = pending.remove(&response.request_id) {
|
||||
let _ = request.response_tx.send(response.result);
|
||||
} else {
|
||||
let mut state = self
|
||||
.state
|
||||
.app
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
let message = match &response.result {
|
||||
ResponseResult::Ok(msg) => msg.clone(),
|
||||
ResponseResult::Error(code) => format!("Error: {:?}", code),
|
||||
};
|
||||
state.push_log(UiLogEntry {
|
||||
timestamp_ms: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis(),
|
||||
sender: "Command".into(),
|
||||
message,
|
||||
is_error: matches!(&response.result, ResponseResult::Error(_)),
|
||||
});
|
||||
}
|
||||
}
|
||||
DaemonMessage::HelloAck(_) => {}
|
||||
DaemonMessage::Pong { .. } => {}
|
||||
DaemonMessage::LifecycleEvent(event) => match event {
|
||||
iota_ipc::LifecycleEvent::Shutdown { reason } => {
|
||||
let mut state = self
|
||||
.state
|
||||
.app
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
state.push_log(UiLogEntry {
|
||||
timestamp_ms: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis(),
|
||||
sender: "Daemon".into(),
|
||||
message: format!("Daemon shutting down: {}", reason),
|
||||
is_error: true,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
DaemonMessage::Gap { skipped } => {
|
||||
let mut state = self
|
||||
.state
|
||||
.app
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
state.push_log(UiLogEntry {
|
||||
timestamp_ms: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis(),
|
||||
sender: "System".into(),
|
||||
message: format!("Skipped {} messages, resynchronizing", skipped),
|
||||
is_error: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue