1use std::collections::HashMap;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::sync::mpsc::{self, Receiver, SyncSender};
11
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14
15#[non_exhaustive]
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub enum BootPhase {
19 Init,
21 Config,
23 Services,
25 ResourceTree,
27 Agents,
29 Network,
31 Ecc,
33 Ready,
35}
36
37impl BootPhase {
38 pub fn tag(&self) -> &'static str {
40 match self {
41 BootPhase::Init => "INIT",
42 BootPhase::Config => "CONFIG",
43 BootPhase::Services => "SERVICES",
44 BootPhase::ResourceTree => "TREE",
45 BootPhase::Agents => "AGENTS",
46 BootPhase::Network => "NETWORK",
47 BootPhase::Ecc => "ECC",
48 BootPhase::Ready => "READY",
49 }
50 }
51}
52
53impl std::fmt::Display for BootPhase {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 write!(f, "{}", self.tag())
56 }
57}
58
59#[non_exhaustive]
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62pub enum LogLevel {
63 Debug,
65 Info,
67 Warn,
69 Error,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct BootEvent {
76 pub timestamp: DateTime<Utc>,
78 pub phase: BootPhase,
80 pub message: String,
82 pub level: LogLevel,
84 #[serde(default)]
88 pub seq: u64,
89}
90
91impl BootEvent {
92 pub fn info(phase: BootPhase, message: impl Into<String>) -> Self {
94 Self {
95 timestamp: Utc::now(),
96 phase,
97 message: message.into(),
98 level: LogLevel::Info,
99 seq: 0,
100 }
101 }
102
103 pub fn warn(phase: BootPhase, message: impl Into<String>) -> Self {
105 Self {
106 timestamp: Utc::now(),
107 phase,
108 message: message.into(),
109 level: LogLevel::Warn,
110 seq: 0,
111 }
112 }
113
114 pub fn error(phase: BootPhase, message: impl Into<String>) -> Self {
116 Self {
117 timestamp: Utc::now(),
118 phase,
119 message: message.into(),
120 level: LogLevel::Error,
121 seq: 0,
122 }
123 }
124
125 pub fn format_line(&self) -> String {
130 let tag = self.phase.tag();
131 format!(" [{tag:<10}] {}", self.message)
132 }
133}
134
135#[derive(Debug, Clone, Default)]
140pub struct BootLog {
141 events: Vec<BootEvent>,
142}
143
144impl BootLog {
145 pub fn new() -> Self {
147 Self { events: Vec::new() }
148 }
149
150 pub fn push(&mut self, event: BootEvent) {
152 self.events.push(event);
153 }
154
155 pub fn events(&self) -> &[BootEvent] {
157 &self.events
158 }
159
160 pub fn format_all(&self) -> String {
162 let mut output = String::new();
163 for event in &self.events {
164 if event.level == LogLevel::Debug {
165 continue;
166 }
167 output.push_str(&event.format_line());
168 output.push('\n');
169 }
170 output
171 }
172
173 pub fn len(&self) -> usize {
175 self.events.len()
176 }
177
178 pub fn is_empty(&self) -> bool {
180 self.events.is_empty()
181 }
182}
183
184const DEFAULT_EVENT_LOG_CAPACITY: usize = 1024;
186
187const EVENT_LOG_SUBSCRIBER_CAPACITY: usize = 1024;
189
190pub struct KernelEventLog {
200 events: std::sync::Mutex<std::collections::VecDeque<BootEvent>>,
201 capacity: usize,
202 next_seq: AtomicU64,
204 subscribers: std::sync::Mutex<HashMap<u64, SyncSender<BootEvent>>>,
210 next_sub_id: AtomicU64,
211}
212
213impl KernelEventLog {
214 pub fn new() -> Self {
216 Self::with_capacity(DEFAULT_EVENT_LOG_CAPACITY)
217 }
218
219 pub fn with_capacity(capacity: usize) -> Self {
221 Self {
222 events: std::sync::Mutex::new(std::collections::VecDeque::with_capacity(capacity)),
223 capacity,
224 next_seq: AtomicU64::new(1),
225 subscribers: std::sync::Mutex::new(HashMap::new()),
226 next_sub_id: AtomicU64::new(1),
227 }
228 }
229
230 pub fn push(&self, mut event: BootEvent) {
236 let seq = self.next_seq.fetch_add(1, Ordering::Relaxed);
237 event.seq = seq;
238 {
239 let mut events = self.events.lock().unwrap();
240 if events.len() >= self.capacity {
241 events.pop_front();
242 }
243 events.push_back(event.clone());
244 }
245
246 let mut dead = Vec::new();
249 {
250 let subs = self.subscribers.lock().unwrap();
251 for (id, tx) in subs.iter() {
252 if tx.try_send(event.clone()).is_err() {
253 dead.push(*id);
254 }
255 }
256 }
257 if !dead.is_empty() {
258 let mut subs = self.subscribers.lock().unwrap();
259 for id in dead {
260 subs.remove(&id);
261 }
262 }
263 }
264
265 pub fn info(&self, source: &str, message: impl Into<String>) {
267 self.push(BootEvent {
268 timestamp: Utc::now(),
269 phase: BootPhase::Ready, message: format!("[{source}] {}", message.into()),
271 level: LogLevel::Info,
272 seq: 0,
273 });
274 }
275
276 pub fn warn(&self, source: &str, message: impl Into<String>) {
278 self.push(BootEvent {
279 timestamp: Utc::now(),
280 phase: BootPhase::Ready,
281 message: format!("[{source}] {}", message.into()),
282 level: LogLevel::Warn,
283 seq: 0,
284 });
285 }
286
287 pub fn error(&self, source: &str, message: impl Into<String>) {
289 self.push(BootEvent {
290 timestamp: Utc::now(),
291 phase: BootPhase::Ready,
292 message: format!("[{source}] {}", message.into()),
293 level: LogLevel::Error,
294 seq: 0,
295 });
296 }
297
298 pub fn subscribe(&self) -> (u64, Receiver<BootEvent>) {
305 let id = self.next_sub_id.fetch_add(1, Ordering::Relaxed);
306 let (tx, rx) = mpsc::sync_channel(EVENT_LOG_SUBSCRIBER_CAPACITY);
307 self.subscribers.lock().unwrap().insert(id, tx);
308 (id, rx)
309 }
310
311 pub fn unsubscribe(&self, id: u64) {
313 self.subscribers.lock().unwrap().remove(&id);
314 }
315
316 pub fn subscriber_count(&self) -> usize {
318 self.subscribers.lock().unwrap().len()
319 }
320
321 pub fn last_seq(&self) -> u64 {
323 self.next_seq.load(Ordering::Relaxed).saturating_sub(1)
324 }
325
326 #[cfg(feature = "exochain")]
328 pub fn info_with_chain(
329 &self,
330 source: &str,
331 message: impl Into<String>,
332 chain: Option<&crate::chain::ChainManager>,
333 ) {
334 let msg = message.into();
335 self.info(source, &msg);
336 if let Some(cm) = chain {
337 cm.append(
338 source,
339 "log.info",
340 Some(serde_json::json!({ "message": msg })),
341 );
342 }
343 }
344
345 pub fn ingest_boot_log(&self, boot_log: &BootLog) {
347 for event in boot_log.events() {
348 self.push(event.clone());
349 }
350 }
351
352 pub fn tail(&self, n: usize) -> Vec<BootEvent> {
354 let events = self.events.lock().unwrap();
355 if n == 0 || n >= events.len() {
356 events.iter().cloned().collect()
357 } else {
358 events.iter().skip(events.len() - n).cloned().collect()
359 }
360 }
361
362 pub fn filter_level(&self, min_level: &LogLevel, n: usize) -> Vec<BootEvent> {
364 let events = self.events.lock().unwrap();
365 let level_rank = |l: &LogLevel| -> u8 {
366 match l {
367 LogLevel::Debug => 0,
368 LogLevel::Info => 1,
369 LogLevel::Warn => 2,
370 LogLevel::Error => 3,
371 }
372 };
373 let min_rank = level_rank(min_level);
374 let filtered: Vec<BootEvent> = events
375 .iter()
376 .filter(|e| level_rank(&e.level) >= min_rank)
377 .cloned()
378 .collect();
379 if n == 0 || n >= filtered.len() {
380 filtered
381 } else {
382 filtered[filtered.len() - n..].to_vec()
383 }
384 }
385
386 pub fn len(&self) -> usize {
388 self.events.lock().unwrap().len()
389 }
390
391 pub fn is_empty(&self) -> bool {
393 self.events.lock().unwrap().is_empty()
394 }
395}
396
397impl Default for KernelEventLog {
398 fn default() -> Self {
399 Self::new()
400 }
401}
402
403impl std::fmt::Debug for KernelEventLog {
404 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
405 let len = self.len();
406 f.debug_struct("KernelEventLog")
407 .field("count", &len)
408 .field("capacity", &self.capacity)
409 .finish()
410 }
411}
412
413pub fn boot_banner() -> String {
421 let brand = clawft_types::config::brand();
422 let mut output = String::new();
423 output.push_str("\n ");
424 output.push_str(&brand);
425 output.push_str(" v");
426 output.push_str(env!("CARGO_PKG_VERSION"));
427 output.push('\n');
428 output.push_str(" ");
429 output.push_str(&"-".repeat(45));
430 output.push('\n');
431 if let Some(msg) = clawft_plugin::sandbox::sandbox_platform_banner_warning() {
432 output.push_str(" WARN ");
433 output.push_str(msg);
434 output.push('\n');
435 }
436 output
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442
443 #[test]
444 fn boot_phase_tags() {
445 assert_eq!(BootPhase::Init.tag(), "INIT");
446 assert_eq!(BootPhase::Config.tag(), "CONFIG");
447 assert_eq!(BootPhase::Services.tag(), "SERVICES");
448 assert_eq!(BootPhase::ResourceTree.tag(), "TREE");
449 assert_eq!(BootPhase::Agents.tag(), "AGENTS");
450 assert_eq!(BootPhase::Network.tag(), "NETWORK");
451 assert_eq!(BootPhase::Ready.tag(), "READY");
452 }
453
454 #[test]
455 fn boot_event_info() {
456 let msg = format!("WeftOS v{} booting...", env!("CARGO_PKG_VERSION"));
457 let event = BootEvent::info(BootPhase::Init, msg.clone());
458 assert_eq!(event.phase, BootPhase::Init);
459 assert_eq!(event.level, LogLevel::Info);
460 assert_eq!(event.message, msg);
461 }
462
463 #[test]
464 fn boot_event_format_line() {
465 let event = BootEvent::info(BootPhase::Init, "PID 0 (kernel)");
466 let line = event.format_line();
467 assert!(line.contains("[INIT"));
468 assert!(line.contains("PID 0 (kernel)"));
469 }
470
471 #[test]
472 fn boot_log_push_and_format() {
473 let mut log = BootLog::new();
474 log.push(BootEvent::info(BootPhase::Init, "booting..."));
475 log.push(BootEvent::info(BootPhase::Config, "config loaded"));
476 log.push(BootEvent::info(BootPhase::Ready, "ready"));
477
478 assert_eq!(log.len(), 3);
479 let formatted = log.format_all();
480 assert!(formatted.contains("booting..."));
481 assert!(formatted.contains("config loaded"));
482 assert!(formatted.contains("ready"));
483 }
484
485 #[test]
486 fn boot_log_skips_debug() {
487 let mut log = BootLog::new();
488 log.push(BootEvent {
489 timestamp: Utc::now(),
490 phase: BootPhase::Init,
491 message: "debug msg".into(),
492 level: LogLevel::Debug,
493 seq: 0,
494 });
495 log.push(BootEvent::info(BootPhase::Init, "info msg"));
496
497 let formatted = log.format_all();
498 assert!(!formatted.contains("debug msg"));
499 assert!(formatted.contains("info msg"));
500 }
501
502 #[test]
503 fn boot_log_empty() {
504 let log = BootLog::new();
505 assert!(log.is_empty());
506 assert_eq!(log.len(), 0);
507 assert!(log.format_all().is_empty());
508 }
509
510 #[test]
511 fn boot_banner_format() {
512 let banner = boot_banner();
513 assert!(banner.contains("WeftOS v"));
514 assert!(banner.contains(env!("CARGO_PKG_VERSION")));
515 assert!(banner.contains("---"));
516 }
517
518 #[test]
521 fn boot_banner_includes_macos_sandbox_downgrade_warning() {
522 let banner = boot_banner();
523 if cfg!(target_os = "linux") {
524 assert!(
525 !banner.contains("WARN"),
526 "Linux OS sandbox is available; banner must not warn"
527 );
528 } else {
529 assert!(
530 banner.contains("WARN"),
531 "non-Linux banner must include WARN: {banner}"
532 );
533 assert!(
534 banner.contains("OS sandbox unavailable"),
535 "banner must name the sandbox restriction: {banner}"
536 );
537 assert!(
538 banner.contains("WASM"),
539 "banner must name the WASM fallback: {banner}"
540 );
541 if cfg!(target_os = "macos") {
542 assert!(
543 banner.contains("macOS"),
544 "macOS banner should call out the platform: {banner}"
545 );
546 }
547 }
548 }
549
550 #[test]
551 fn boot_event_serde() {
552 let event = BootEvent::info(BootPhase::Services, "[OK] message-bus");
553 let json = serde_json::to_string(&event).unwrap();
554 let restored: BootEvent = serde_json::from_str(&json).unwrap();
555 assert_eq!(restored.phase, BootPhase::Services);
556 assert_eq!(restored.message, "[OK] message-bus");
557 }
558
559 #[test]
560 fn boot_event_warn_and_error() {
561 let warn_event = BootEvent::warn(BootPhase::Services, "slow start");
562 assert_eq!(warn_event.level, LogLevel::Warn);
563
564 let err_event = BootEvent::error(BootPhase::Services, "failed");
565 assert_eq!(err_event.level, LogLevel::Error);
566 }
567
568 #[test]
571 fn event_log_push_and_tail() {
572 let log = KernelEventLog::new();
573 log.info("test", "first");
574 log.info("test", "second");
575 log.warn("test", "third");
576
577 assert_eq!(log.len(), 3);
578
579 let all = log.tail(0);
580 assert_eq!(all.len(), 3);
581 assert!(all[0].message.contains("first"));
582
583 let last_two = log.tail(2);
584 assert_eq!(last_two.len(), 2);
585 assert!(last_two[0].message.contains("second"));
586 }
587
588 #[test]
589 fn event_log_ring_buffer_evicts() {
590 let log = KernelEventLog::with_capacity(3);
591 log.info("a", "1");
592 log.info("b", "2");
593 log.info("c", "3");
594 log.info("d", "4"); assert_eq!(log.len(), 3);
597 let all = log.tail(0);
598 assert!(all[0].message.contains("[b] 2"));
599 assert!(all[2].message.contains("[d] 4"));
600 }
601
602 #[test]
603 fn event_log_filter_level() {
604 let log = KernelEventLog::new();
605 log.info("test", "info msg");
606 log.warn("test", "warn msg");
607 log.error("test", "error msg");
608
609 let warns_and_above = log.filter_level(&LogLevel::Warn, 0);
610 assert_eq!(warns_and_above.len(), 2);
611
612 let errors_only = log.filter_level(&LogLevel::Error, 0);
613 assert_eq!(errors_only.len(), 1);
614 }
615
616 #[test]
617 fn event_log_ingest_boot_log() {
618 let mut boot_log = BootLog::new();
619 boot_log.push(BootEvent::info(BootPhase::Init, "booting"));
620 boot_log.push(BootEvent::info(BootPhase::Ready, "ready"));
621
622 let event_log = KernelEventLog::new();
623 event_log.ingest_boot_log(&boot_log);
624
625 assert_eq!(event_log.len(), 2);
626 let events = event_log.tail(0);
627 assert!(events[0].message.contains("booting"));
628 }
629
630 #[test]
631 fn event_log_empty() {
632 let log = KernelEventLog::new();
633 assert!(log.is_empty());
634 assert_eq!(log.len(), 0);
635 assert!(log.tail(10).is_empty());
636 }
637
638 #[test]
639 fn event_log_assigns_monotonic_seq() {
640 let log = KernelEventLog::new();
641 log.info("test", "a");
642 log.info("test", "b");
643 log.warn("test", "c");
644 let all = log.tail(0);
645 assert_eq!(all[0].seq, 1);
646 assert_eq!(all[1].seq, 2);
647 assert_eq!(all[2].seq, 3);
648 assert_eq!(log.last_seq(), 3);
649 }
650
651 #[test]
652 fn event_log_subscribe_receives_new_entries() {
653 let log = KernelEventLog::new();
654 log.info("test", "before-sub");
655 let (id, rx) = log.subscribe();
656 assert_eq!(log.subscriber_count(), 1);
657 log.info("test", "after-sub");
658 let got = rx.recv_timeout(std::time::Duration::from_secs(1)).expect("entry");
659 assert!(got.message.contains("after-sub"));
660 assert!(got.seq >= 2);
661 log.unsubscribe(id);
662 assert_eq!(log.subscriber_count(), 0);
663 }
664}