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