1use alloc::{boxed::Box, sync::Arc};
8use core::fmt::{self, Write};
9
10use ax_lazyinit::OnceLock;
11use ax_sync::Mutex;
12use axpoll_set::PollSet;
13
14pub use crate::serial::RxItem;
15use crate::{
16 RuntimeError, RuntimeResult,
17 raw_console::RawConsoleInput,
18 serial,
19 structured_log::{RuntimeLogContext, write_record},
20 task::sync::RawSpinLock,
21};
22
23static ACTIVATION: OnceLock<ConsoleActivation> = OnceLock::new();
24static TTY_NUMBERS: OnceLock<Box<[Option<usize>]>> = OnceLock::new();
25static RAW_OUTPUT_LOCK: Mutex<()> = Mutex::new(());
28static RAW_HARDWARE_LOCK: RawSpinLock<()> = RawSpinLock::new(());
29static RAW_OUTPUT_SOURCE: OnceLock<Arc<PollSet>> = OnceLock::new();
30
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33pub(crate) enum ConsoleActivation {
34 Active {
36 runtime_index: usize,
37 tty_number: usize,
38 },
39 RawHal(ConsoleUnavailable),
42 FailedClosed(ConsoleUnavailable),
44}
45
46#[derive(Clone, Copy, Debug, Eq, PartialEq)]
48pub(crate) enum ConsoleUnavailable {
49 NoSerialDevice,
50 NoHardwareSelected,
51 SelectedDeviceNotFound,
52 NoTtyS0Fallback,
53 HandoffFailed,
54 RuntimeAdoptFailed,
55 LogRoutingBusy,
56}
57
58pub(crate) fn activate_before_smp() -> ConsoleActivation {
60 if let Some(activation) = ACTIVATION.get().copied() {
61 return activation;
62 }
63
64 let runtimes = serial::runtimes();
65 let tty_numbers = initialize_tty_numbers(runtimes);
66 let selection = select_runtime(runtimes, tty_numbers, ax_hal::console::device_id());
67 let (runtime_index, tty_number) = match selection {
68 Ok(selection) => selection,
69 Err(unavailable) => return use_raw_hal(unavailable),
70 };
71
72 let runtime = &runtimes[runtime_index];
73 if runtime.begin_console_handoff().is_err() {
74 return fail_closed(runtime, ConsoleUnavailable::HandoffFailed);
75 }
76 if runtime.adopt_prepared_console().is_err() {
77 return fail_closed(runtime, ConsoleUnavailable::RuntimeAdoptFailed);
78 }
79 if let Err(error) = runtime.commit_console_handoff() {
80 let reason = if error == RuntimeError::SerialConsoleBusy {
81 ConsoleUnavailable::LogRoutingBusy
82 } else {
83 ConsoleUnavailable::HandoffFailed
84 };
85 return fail_closed(runtime, reason);
86 }
87
88 let activation = ConsoleActivation::Active {
89 runtime_index,
90 tty_number,
91 };
92 ACTIVATION.call_once(|| activation);
93 activation
94}
95
96fn use_raw_hal(reason: ConsoleUnavailable) -> ConsoleActivation {
97 let activation = raw_hal_activation(reason);
98 ACTIVATION.call_once(|| activation);
99 activation
100}
101
102const fn raw_hal_activation(reason: ConsoleUnavailable) -> ConsoleActivation {
103 ConsoleActivation::RawHal(reason)
104}
105
106fn fail_closed(
107 runtime: &serial::SerialRuntimeHandle,
108 reason: ConsoleUnavailable,
109) -> ConsoleActivation {
110 runtime.fail_console_closed();
111 ax_hal::console::fail_runtime_handoff_closed();
112 let activation = ConsoleActivation::FailedClosed(reason);
113 ACTIVATION.call_once(|| activation);
114 activation
115}
116
117fn select_runtime(
118 runtimes: &[serial::SerialRuntimeHandle],
119 tty_numbers: &[Option<usize>],
120 selected: ax_hal::console::ConsoleDeviceIdResult,
121) -> Result<(usize, usize), ConsoleUnavailable> {
122 let candidates = runtimes
123 .iter()
124 .zip(tty_numbers.iter().copied())
125 .map(|(runtime, tty_number)| (runtime.info().device_id, tty_number))
126 .collect::<alloc::vec::Vec<_>>();
127 select_candidate(&candidates, selected)
128}
129
130fn select_candidate(
131 candidates: &[(ax_hal::console::ConsoleDeviceId, Option<usize>)],
132 selected: ax_hal::console::ConsoleDeviceIdResult,
133) -> Result<(usize, usize), ConsoleUnavailable> {
134 if candidates.is_empty() {
135 return Err(ConsoleUnavailable::NoSerialDevice);
136 }
137 match selected {
138 Ok(device_id) => candidates
139 .iter()
140 .position(|(candidate, _)| *candidate == device_id)
141 .and_then(|index| Some((index, candidates[index].1?)))
142 .ok_or(ConsoleUnavailable::SelectedDeviceNotFound),
143 Err(ax_hal::console::ConsoleDeviceIdError::NotSpecified) => candidates
144 .iter()
145 .position(|(_, number)| *number == Some(0))
146 .map(|index| (index, 0))
147 .ok_or(ConsoleUnavailable::NoTtyS0Fallback),
148 Err(ax_hal::console::ConsoleDeviceIdError::NoHardwareDevice) => {
149 Err(ConsoleUnavailable::NoHardwareSelected)
150 }
151 Err(ax_hal::console::ConsoleDeviceIdError::DeviceNotFound) => {
152 Err(ConsoleUnavailable::SelectedDeviceNotFound)
153 }
154 }
155}
156
157pub fn tty_number(runtime: &serial::SerialRuntimeHandle) -> Option<usize> {
159 let index = serial::runtimes()
160 .iter()
161 .position(|candidate| candidate.info().device_id == runtime.info().device_id)?;
162 TTY_NUMBERS.get()?.get(index).copied().flatten()
163}
164
165fn initialize_tty_numbers(runtimes: &[serial::SerialRuntimeHandle]) -> &'static [Option<usize>] {
166 TTY_NUMBERS.call_once(|| {
167 assign_tty_numbers(
168 &runtimes
169 .iter()
170 .map(|runtime| runtime.info().alias_index)
171 .collect::<alloc::vec::Vec<_>>(),
172 )
173 .into_boxed_slice()
174 })
175}
176
177fn activation() -> Option<ConsoleActivation> {
178 ACTIVATION.get().copied()
179}
180
181pub fn is_active(runtime: &serial::SerialRuntimeHandle) -> bool {
183 serial::active_console()
184 .is_some_and(|active| active.info().device_id == runtime.info().device_id)
185}
186
187fn inactive_console_error(activation: Option<ConsoleActivation>) -> RuntimeError {
188 match activation {
189 Some(ConsoleActivation::FailedClosed(_)) | Some(ConsoleActivation::Active { .. }) => {
190 RuntimeError::ConsoleFailedClosed
191 }
192 Some(ConsoleActivation::RawHal(_)) | None => RuntimeError::SerialNotStarted,
193 }
194}
195
196pub fn take_input() -> RuntimeResult<TaskConsoleInput> {
198 if let Some(runtime) = serial::active_console() {
199 return runtime
200 .take_rx_subscription()
201 .map(|inner| TaskConsoleInput {
202 inner: TaskConsoleInputInner::Runtime(inner),
203 })
204 .ok_or(RuntimeError::SerialConsoleBusy);
205 }
206 match activation() {
207 Some(ConsoleActivation::RawHal(_)) => Ok(TaskConsoleInput {
208 inner: TaskConsoleInputInner::RawHal(crate::raw_console::take_input()?),
209 }),
210 activation => Err(inactive_console_error(activation)),
211 }
212}
213
214pub fn output() -> RuntimeResult<TaskConsoleOutput> {
216 if let Some(runtime) = serial::active_console() {
217 return Ok(TaskConsoleOutput {
218 inner: TaskConsoleOutputInner::Runtime(runtime.task_output()),
219 });
220 }
221 match activation() {
222 Some(ConsoleActivation::RawHal(_)) => Ok(TaskConsoleOutput {
223 inner: TaskConsoleOutputInner::RawHal,
224 }),
225 activation => Err(inactive_console_error(activation)),
226 }
227}
228
229pub fn subscribe_logs() -> RuntimeResult<ConsoleLogSubscription> {
231 let runtime = serial::active_console().ok_or_else(|| match activation() {
232 Some(ConsoleActivation::RawHal(_)) => RuntimeError::OperationNotSupported,
233 activation => inactive_console_error(activation),
234 })?;
235 runtime
236 .take_log_subscription()
237 .map(|inner| ConsoleLogSubscription { inner })
238 .ok_or(RuntimeError::SerialConsoleBusy)
239}
240
241pub(crate) fn try_publish_without_runtime(
245 meta: ax_log::RecordMeta,
246 context: RuntimeLogContext,
247 args: fmt::Arguments<'_>,
248) -> Option<ax_log::PublishStatus> {
249 match activation()? {
250 ConsoleActivation::RawHal(_) => {
251 Some(publish_raw_record(meta, context, args, &mut RawHalWriter))
255 }
256 ConsoleActivation::Active { .. } | ConsoleActivation::FailedClosed(_) => {
257 Some(ax_log::PublishStatus::Dropped)
258 }
259 }
260}
261
262fn publish_raw_record(
263 meta: ax_log::RecordMeta,
264 context: RuntimeLogContext,
265 args: fmt::Arguments<'_>,
266 writer: &mut impl Write,
267) -> ax_log::PublishStatus {
268 let Some(_hardware) = RAW_HARDWARE_LOCK.try_lock_irqsave() else {
269 return ax_log::PublishStatus::Dropped;
270 };
271 if write_record(writer, meta, context, args).is_ok() {
272 ax_log::PublishStatus::Published
273 } else {
274 ax_log::PublishStatus::Dropped
275 }
276}
277
278pub struct TaskConsoleInput {
280 inner: TaskConsoleInputInner,
281}
282
283enum TaskConsoleInputInner {
284 Runtime(serial::SerialRxSubscription),
285 RawHal(RawConsoleInput),
286}
287
288impl TaskConsoleInput {
289 pub fn try_read(&self, out: &mut [RxItem]) -> usize {
290 match &self.inner {
291 TaskConsoleInputInner::Runtime(inner) => inner.drain(out),
292 TaskConsoleInputInner::RawHal(inner) => inner.try_read(out),
293 }
294 }
295
296 pub fn wait_readable(&self) -> RuntimeResult {
297 match &self.inner {
298 TaskConsoleInputInner::Runtime(inner) => inner.wait_readable(),
299 TaskConsoleInputInner::RawHal(inner) => {
300 inner.wait_readable();
301 Ok(())
302 }
303 }
304 }
305
306 pub fn read(&self, out: &mut [RxItem]) -> RuntimeResult<usize> {
307 if out.is_empty() {
308 return Ok(0);
309 }
310 loop {
311 let read = self.try_read(out);
312 if read != 0 {
313 return Ok(read);
314 }
315 self.wait_readable()?;
316 }
317 }
318
319 pub fn discard_pending(&self) -> RuntimeResult {
320 match &self.inner {
321 TaskConsoleInputInner::Runtime(inner) => inner.discard_pending(),
322 TaskConsoleInputInner::RawHal(inner) => {
323 inner.discard_pending();
324 Ok(())
325 }
326 }
327 }
328
329 pub fn poll_source(&self) -> Arc<PollSet> {
330 match &self.inner {
331 TaskConsoleInputInner::Runtime(inner) => inner.poll_source(),
332 TaskConsoleInputInner::RawHal(inner) => inner.poll_source(),
333 }
334 }
335
336 pub fn wait_event(&self, logs: &ConsoleLogSubscription) -> RuntimeResult {
338 match &self.inner {
339 TaskConsoleInputInner::Runtime(inner) => inner.wait_console_event(&logs.inner),
340 TaskConsoleInputInner::RawHal(inner) => {
341 inner.wait_readable();
342 Ok(())
343 }
344 }
345 }
346}
347
348fn raw_output_source() -> Arc<PollSet> {
349 RAW_OUTPUT_SOURCE
350 .call_once(|| Arc::new(PollSet::new()))
351 .clone()
352}
353
354#[derive(Clone)]
356pub struct TaskConsoleOutput {
357 inner: TaskConsoleOutputInner,
358}
359
360#[derive(Clone)]
361enum TaskConsoleOutputInner {
362 Runtime(serial::SerialTaskOutput),
363 RawHal,
364}
365
366impl TaskConsoleOutput {
367 pub fn try_write(&self, bytes: &[u8]) -> RuntimeResult<usize> {
368 match &self.inner {
369 TaskConsoleOutputInner::Runtime(inner) => inner.try_write(bytes),
370 TaskConsoleOutputInner::RawHal => {
371 let Some(_output) = RAW_OUTPUT_LOCK.try_lock() else {
372 return Err(RuntimeError::WouldBlock);
373 };
374 let Some(_hardware) = RAW_HARDWARE_LOCK.try_lock_irqsave() else {
375 return Err(RuntimeError::WouldBlock);
376 };
377 ax_hal::console::write_bytes(bytes);
378 Ok(bytes.len())
379 }
380 }
381 }
382
383 pub fn write_all(&self, bytes: &[u8]) -> RuntimeResult<usize> {
384 match &self.inner {
385 TaskConsoleOutputInner::Runtime(inner) => inner.write_all(bytes),
386 TaskConsoleOutputInner::RawHal => {
387 let _output = RAW_OUTPUT_LOCK.lock();
388 let _hardware = RAW_HARDWARE_LOCK.lock_irqsave();
389 ax_hal::console::write_bytes(bytes);
390 Ok(bytes.len())
391 }
392 }
393 }
394
395 pub fn write_text_all(&self, bytes: &[u8]) -> RuntimeResult<usize> {
396 match &self.inner {
397 TaskConsoleOutputInner::Runtime(inner) => inner.write_text_all(bytes),
398 TaskConsoleOutputInner::RawHal => {
399 let _output = RAW_OUTPUT_LOCK.lock();
400 let _hardware = RAW_HARDWARE_LOCK.lock_irqsave();
401 ax_hal::console::write_text_bytes(bytes);
402 Ok(bytes.len())
403 }
404 }
405 }
406
407 pub fn write_fmt(&self, args: fmt::Arguments<'_>) -> fmt::Result {
408 match &self.inner {
409 TaskConsoleOutputInner::Runtime(inner) => inner.write_fmt(args),
410 TaskConsoleOutputInner::RawHal => {
411 let _output = RAW_OUTPUT_LOCK.lock();
412 let _hardware = RAW_HARDWARE_LOCK.lock_irqsave();
413 RawHalWriter.write_fmt(args)
414 }
415 }
416 }
417
418 pub fn drain(&self) -> RuntimeResult {
419 match &self.inner {
420 TaskConsoleOutputInner::Runtime(inner) => inner.wait_idle(),
421 TaskConsoleOutputInner::RawHal => {
422 let _output = RAW_OUTPUT_LOCK.lock();
423 let _hardware = RAW_HARDWARE_LOCK.lock_irqsave();
424 Ok(())
425 }
426 }
427 }
428
429 pub fn discard_pending(&self) -> RuntimeResult {
430 match &self.inner {
431 TaskConsoleOutputInner::Runtime(inner) => inner.discard_pending(),
432 TaskConsoleOutputInner::RawHal => {
433 let _output = RAW_OUTPUT_LOCK.lock();
434 let _hardware = RAW_HARDWARE_LOCK.lock_irqsave();
435 Ok(())
436 }
437 }
438 }
439
440 pub fn reconfigure(
442 &self,
443 config: Option<serial::Config>,
444 drain: bool,
445 publish: impl FnOnce(),
446 ) -> RuntimeResult {
447 match &self.inner {
448 TaskConsoleOutputInner::Runtime(inner) => inner.reconfigure(config, drain, publish),
449 TaskConsoleOutputInner::RawHal => {
450 let _output = RAW_OUTPUT_LOCK.lock();
451 let _hardware = RAW_HARDWARE_LOCK.lock_irqsave();
452 if config.is_some() {
453 return Err(RuntimeError::OperationNotSupported);
454 }
455 let _ = drain;
456 publish();
457 Ok(())
458 }
459 }
460 }
461
462 pub fn poll_source(&self) -> Arc<PollSet> {
463 match &self.inner {
464 TaskConsoleOutputInner::Runtime(inner) => inner.poll_source(),
465 TaskConsoleOutputInner::RawHal => raw_output_source(),
466 }
467 }
468}
469
470struct RawHalWriter;
471
472impl Write for RawHalWriter {
473 fn write_str(&mut self, text: &str) -> fmt::Result {
474 ax_hal::console::write_text_bytes(text.as_bytes());
475 Ok(())
476 }
477}
478
479pub struct ConsoleLogRecord {
481 inner: serial::LogRecord,
482}
483
484impl ConsoleLogRecord {
485 pub fn output_tag(&self) -> Option<u128> {
487 match self.inner.kind() {
488 serial::LogRecordKind::Output(tag) => Some(tag),
489 _ => None,
490 }
491 }
492
493 pub fn bytes(&self) -> &[u8] {
494 self.inner.bytes()
495 }
496
497 pub fn cpu_id(&self) -> usize {
498 self.inner.cpu_id()
499 }
500
501 pub fn timestamp_nanos(&self) -> u64 {
502 self.inner.timestamp_nanos()
503 }
504
505 pub fn task_id(&self) -> Option<u64> {
506 self.inner.task_id()
507 }
508
509 pub fn is_truncated(&self) -> bool {
510 self.inner.is_truncated()
511 }
512
513 pub fn is_log(&self) -> bool {
514 self.inner.kind() == serial::LogRecordKind::Log
515 }
516}
517
518#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
520pub struct ConsoleLogDropReport {
521 pub records: usize,
522 pub source_bytes: usize,
523}
524
525pub struct ConsoleLogSubscription {
527 inner: serial::SerialLogSubscription,
528}
529
530impl ConsoleLogSubscription {
531 pub fn write_output(&self, tag: u128, bytes: &[u8]) -> RuntimeResult {
538 self.inner.write_output(tag, bytes)
539 }
540
541 pub fn try_read(&self) -> Option<ConsoleLogRecord> {
542 self.inner
543 .try_read()
544 .map(|inner| ConsoleLogRecord { inner })
545 }
546
547 pub fn dropped(&self) -> ConsoleLogDropReport {
548 let (records, source_bytes) = self.inner.dropped();
549 ConsoleLogDropReport {
550 records,
551 source_bytes,
552 }
553 }
554
555 pub fn wait_readable(&self) -> RuntimeResult {
556 self.inner.wait_readable()
557 }
558}
559
560fn assign_tty_numbers(alias_indices: &[Option<usize>]) -> alloc::vec::Vec<Option<usize>> {
561 let mut assigned = alloc::vec![None; alias_indices.len()];
562 let mut used = alloc::vec::Vec::new();
563
564 for (device_index, alias) in alias_indices.iter().copied().enumerate() {
565 let Some(number) = alias else {
566 continue;
567 };
568 if used.contains(&number) {
569 continue;
570 }
571 assigned[device_index] = Some(number);
572 used.push(number);
573 }
574
575 let mut next = 0usize;
576 for number in &mut assigned {
577 if number.is_some() {
578 continue;
579 }
580 while used.contains(&next) {
581 next += 1;
582 }
583 *number = Some(next);
584 used.push(next);
585 }
586 assigned
587}
588
589#[cfg(test)]
590mod tests {
591 use ax_hal::console::ConsoleDeviceIdError;
592
593 use super::{
594 ACTIVATION, ConsoleActivation, ConsoleUnavailable, RAW_OUTPUT_LOCK, assign_tty_numbers,
595 inactive_console_error, output, publish_raw_record, raw_hal_activation, select_candidate,
596 take_input,
597 };
598 use crate::{RuntimeError, structured_log::RuntimeLogContext};
599
600 #[test]
601 fn tty_numbering_preserves_aliases_and_fills_gaps() {
602 assert_eq!(
603 assign_tty_numbers(&[Some(0), None, Some(2), None]),
604 [Some(0), Some(1), Some(2), Some(3)]
605 );
606 assert_eq!(
607 assign_tty_numbers(&[Some(1), Some(1), None]),
608 [Some(1), Some(0), Some(2)]
609 );
610 }
611
612 #[test]
613 fn firmware_device_id_wins_over_ttys0() {
614 let tty_s0 = rdrive::DeviceId::from(10);
615 let tty_s1 = rdrive::DeviceId::from(11);
616 assert_eq!(
617 select_candidate(&[(tty_s0, Some(0)), (tty_s1, Some(1))], Ok(tty_s1)),
618 Ok((1, 1))
619 );
620 }
621
622 #[test]
623 fn only_not_specified_falls_back_to_ttys0() {
624 let tty_s0 = rdrive::DeviceId::from(10);
625 let candidates = [(tty_s0, Some(0))];
626 assert_eq!(
627 select_candidate(&candidates, Err(ConsoleDeviceIdError::NotSpecified)),
628 Ok((0, 0))
629 );
630 assert_eq!(
631 select_candidate(&candidates, Err(ConsoleDeviceIdError::NoHardwareDevice)),
632 Err(ConsoleUnavailable::NoHardwareSelected)
633 );
634 assert_eq!(
635 select_candidate(&candidates, Err(ConsoleDeviceIdError::DeviceNotFound)),
636 Err(ConsoleUnavailable::SelectedDeviceNotFound)
637 );
638 }
639
640 #[test]
641 fn missing_hardware_and_ttys0_are_unavailable_for_runtime_selection() {
642 let tty_s1 = rdrive::DeviceId::from(11);
643 assert_eq!(
644 select_candidate(
645 &[(tty_s1, Some(1))],
646 Err(ConsoleDeviceIdError::NotSpecified)
647 ),
648 Err(ConsoleUnavailable::NoTtyS0Fallback)
649 );
650 assert_eq!(
651 select_candidate(&[], Err(ConsoleDeviceIdError::NotSpecified)),
652 Err(ConsoleUnavailable::NoSerialDevice)
653 );
654 }
655
656 #[test]
657 fn unavailable_runtime_selection_keeps_the_raw_hal_owner() {
658 for reason in [
659 ConsoleUnavailable::NoSerialDevice,
660 ConsoleUnavailable::NoHardwareSelected,
661 ConsoleUnavailable::SelectedDeviceNotFound,
662 ConsoleUnavailable::NoTtyS0Fallback,
663 ] {
664 assert_eq!(
665 raw_hal_activation(reason),
666 ConsoleActivation::RawHal(reason)
667 );
668 }
669 }
670
671 #[test]
672 fn failed_closed_console_never_falls_back_to_the_raw_hal() {
673 assert_eq!(
674 inactive_console_error(Some(ConsoleActivation::FailedClosed(
675 ConsoleUnavailable::HandoffFailed,
676 ))),
677 RuntimeError::ConsoleFailedClosed
678 );
679 assert_eq!(
680 inactive_console_error(Some(ConsoleActivation::RawHal(
681 ConsoleUnavailable::NoSerialDevice,
682 ))),
683 RuntimeError::SerialNotStarted
684 );
685 }
686
687 #[test]
688 fn raw_hal_without_irq_does_not_fake_sleepable_input() {
689 ACTIVATION.call_once(|| ConsoleActivation::RawHal(ConsoleUnavailable::NoSerialDevice));
690 assert!(matches!(
691 take_input(),
692 Err(RuntimeError::OperationNotSupported)
693 ));
694 assert!(output().is_ok());
695 }
696
697 #[test]
698 fn raw_hal_logging_does_not_require_the_task_output_mutex() {
699 ACTIVATION.call_once(|| ConsoleActivation::RawHal(ConsoleUnavailable::NoSerialDevice));
700 let _task_output = RAW_OUTPUT_LOCK.lock();
701 let mut rendered = alloc::string::String::new();
702
703 assert_eq!(
704 publish_raw_record(
705 ax_log::RecordMeta::log(),
706 RuntimeLogContext::new(core::time::Duration::new(12, 345_678_000), Some(2), None),
707 format_args!("\u{1b}[37max_runtime:462] early secondary record\n"),
708 &mut rendered,
709 ),
710 ax_log::PublishStatus::Published
711 );
712 assert_eq!(
713 rendered,
714 "\u{1b}[37m[ 12.345678 2 \u{1b}[37max_runtime:462] early secondary record\n"
715 );
716 }
717}