Skip to main content

arcbox_virtio_console/
device.rs

1//! `VirtioConsole` device — config, ports, queue handling, `VirtioDevice` impl.
2
3use std::collections::VecDeque;
4use std::sync::{Arc, Mutex};
5
6use tokio::sync::mpsc;
7
8use arcbox_virtio_core::error::{Result, VirtioError};
9use arcbox_virtio_core::queue::VirtQueue;
10use arcbox_virtio_core::{QueueConfig, VirtioDevice, VirtioDeviceId, virtio_bindings};
11
12use crate::{ConsoleIo, StdioConsole};
13
14/// Console device configuration.
15#[derive(Debug, Clone)]
16pub struct ConsoleConfig {
17    /// Number of columns.
18    pub cols: u16,
19    /// Number of rows.
20    pub rows: u16,
21    /// Maximum number of ports.
22    pub max_ports: u32,
23    /// Enable multiport.
24    pub multiport: bool,
25}
26
27impl Default for ConsoleConfig {
28    fn default() -> Self {
29        Self {
30            cols: 80,
31            rows: 25,
32            max_ports: 1,
33            multiport: false,
34        }
35    }
36}
37
38/// Console port state.
39#[derive(Debug)]
40#[allow(dead_code)]
41struct ConsolePort {
42    /// Port number.
43    id: u32,
44    /// Whether the port is open.
45    open: bool,
46    /// Input buffer.
47    input_buffer: VecDeque<u8>,
48    /// Output buffer.
49    output_buffer: VecDeque<u8>,
50}
51
52impl ConsolePort {
53    fn new(id: u32) -> Self {
54        Self {
55            id,
56            open: false,
57            input_buffer: VecDeque::with_capacity(4096),
58            output_buffer: VecDeque::with_capacity(4096),
59        }
60    }
61
62    /// Drains all complete (newline-terminated) lines from `output_buffer`,
63    /// returning them trimmed. Bytes after the last newline stay buffered.
64    fn take_complete_lines(&mut self) -> Vec<String> {
65        let mut lines = Vec::new();
66        while let Some(pos) = self.output_buffer.iter().position(|&b| b == b'\n') {
67            let line: Vec<u8> = self.output_buffer.drain(..=pos).collect();
68            if let Ok(s) = std::str::from_utf8(&line) {
69                lines.push(s.trim_end().to_string());
70            }
71        }
72        lines
73    }
74
75    /// Drains everything, including a final line with no trailing newline — e.g.
76    /// a kernel panic or a crash's dying line that would otherwise be lost when
77    /// the device is reset or dropped. Empty trailing fragments are skipped.
78    fn take_all_lines(&mut self) -> Vec<String> {
79        let mut lines = self.take_complete_lines();
80        if !self.output_buffer.is_empty() {
81            let rest: Vec<u8> = self.output_buffer.drain(..).collect();
82            if let Ok(s) = std::str::from_utf8(&rest) {
83                let trimmed = s.trim_end();
84                if !trimmed.is_empty() {
85                    lines.push(trimmed.to_string());
86                }
87            }
88        }
89        lines
90    }
91}
92
93/// Emits captured guest console lines to the host `guest_console` log target.
94fn emit_console_lines(lines: Vec<String>) {
95    for line in lines {
96        tracing::info!(target: "guest_console", "{line}");
97    }
98}
99
100/// `VirtIO` console device.
101#[allow(dead_code)]
102pub struct VirtioConsole {
103    config: ConsoleConfig,
104    features: u64,
105    acked_features: u64,
106    /// Console ports.
107    ports: Vec<ConsolePort>,
108    /// Receive queue (host -> guest).
109    rx_queue: Option<VirtQueue>,
110    /// Transmit queue (guest -> host).
111    tx_queue: Option<VirtQueue>,
112    /// Console I/O handler.
113    io: Option<Arc<Mutex<dyn ConsoleIo>>>,
114    /// Event sender for console input.
115    input_tx: Option<mpsc::UnboundedSender<Vec<u8>>>,
116    /// Last consumed available index for the RX queue (host -> guest).
117    /// Tracked directly because the HV path reads the ring from guest memory
118    /// rather than via the internal `VirtQueue`.
119    rx_last_avail: u16,
120}
121
122impl VirtioConsole {
123    /// Feature: Console size.
124    pub const FEATURE_SIZE: u64 = 1 << 0;
125    /// Feature: Multiport.
126    pub const FEATURE_MULTIPORT: u64 = 1 << 1;
127    /// Feature: Emergency write.
128    pub const FEATURE_EMERG_WRITE: u64 = 1 << 2;
129    /// `VirtIO` 1.0 feature.
130    pub const FEATURE_VERSION_1: u64 = 1 << virtio_bindings::virtio_config::VIRTIO_F_VERSION_1;
131
132    /// Creates a new console device.
133    #[must_use]
134    pub fn new(config: ConsoleConfig) -> Self {
135        // EVENT_IDX is not advertised for console because activate() does not
136        // propagate it to queues. Add it when console queue setup is updated to
137        // call set_event_idx().
138        let mut features = Self::FEATURE_SIZE | Self::FEATURE_EMERG_WRITE | Self::FEATURE_VERSION_1;
139
140        if config.multiport {
141            features |= Self::FEATURE_MULTIPORT;
142        }
143
144        let mut ports = Vec::with_capacity(config.max_ports as usize);
145        ports.push(ConsolePort::new(0)); // Port 0 is always present
146
147        Self {
148            config,
149            features,
150            acked_features: 0,
151            ports,
152            rx_queue: None,
153            tx_queue: None,
154            io: None,
155            input_tx: None,
156            rx_last_avail: 0,
157        }
158    }
159
160    /// Creates a console with standard I/O.
161    #[must_use]
162    pub fn with_stdio() -> Self {
163        let mut console = Self::new(ConsoleConfig::default());
164        console.io = Some(Arc::new(Mutex::new(StdioConsole)));
165        console
166    }
167
168    /// Sets the console I/O handler.
169    pub fn set_io(&mut self, io: Arc<Mutex<dyn ConsoleIo>>) {
170        self.io = Some(io);
171    }
172
173    /// Queues input data to be read by the guest.
174    ///
175    /// # Errors
176    ///
177    /// Returns an error if the console is not active.
178    pub fn queue_input(&mut self, data: &[u8]) -> Result<()> {
179        if let Some(port) = self.ports.first_mut() {
180            port.input_buffer.extend(data);
181            Ok(())
182        } else {
183            Err(VirtioError::NotReady("No console port".into()))
184        }
185    }
186
187    /// Reads output data written by the guest.
188    #[must_use]
189    pub fn read_output(&mut self) -> Vec<u8> {
190        if let Some(port) = self.ports.first_mut() {
191            port.output_buffer.drain(..).collect()
192        } else {
193            Vec::new()
194        }
195    }
196
197    /// Handles data from the guest (TX).
198    fn handle_tx(&mut self, data: &[u8]) -> Result<()> {
199        if let Some(port) = self.ports.first_mut() {
200            port.output_buffer.extend(data);
201        }
202
203        if let Some(io) = &self.io {
204            let mut io = io
205                .lock()
206                .map_err(|e| VirtioError::Io(format!("Failed to lock I/O: {e}")))?;
207            io.write(data)
208                .map_err(|e| VirtioError::Io(format!("Write failed: {e}")))?;
209            io.flush()
210                .map_err(|e| VirtioError::Io(format!("Flush failed: {e}")))?;
211        }
212
213        tracing::trace!("Console TX: {} bytes", data.len());
214        Ok(())
215    }
216
217    /// Handles data to the guest (RX).
218    #[allow(dead_code)]
219    fn handle_rx(&mut self, buf: &mut [u8]) -> Result<usize> {
220        if let Some(port) = self.ports.first_mut() {
221            if !port.input_buffer.is_empty() {
222                let len = buf.len().min(port.input_buffer.len());
223                for (i, byte) in port.input_buffer.drain(..len).enumerate() {
224                    buf[i] = byte;
225                }
226                return Ok(len);
227            }
228        }
229
230        if let Some(io) = &self.io {
231            let mut io = io
232                .lock()
233                .map_err(|e| VirtioError::Io(format!("Failed to lock I/O: {e}")))?;
234            let n = io
235                .read(buf)
236                .map_err(|e| VirtioError::Io(format!("Read failed: {e}")))?;
237            tracing::trace!("Console RX: {} bytes", n);
238            return Ok(n);
239        }
240
241        Ok(0)
242    }
243
244    /// Processes the transmit queue.
245    ///
246    /// # Errors
247    ///
248    /// Returns an error if processing fails.
249    pub fn process_tx_queue(&mut self, memory: &[u8]) -> Result<Vec<(u16, u32)>> {
250        let mut tx_data: Vec<(u16, Vec<u8>)> = Vec::new();
251
252        {
253            let queue = self
254                .tx_queue
255                .as_mut()
256                .ok_or_else(|| VirtioError::NotReady("TX queue not ready".into()))?;
257
258            while let Some((head_idx, chain)) = queue.pop_avail() {
259                let mut data = Vec::new();
260
261                for desc in chain {
262                    if !desc.is_write_only() {
263                        let start = desc.addr as usize;
264                        let end = start + desc.len as usize;
265                        if end <= memory.len() {
266                            data.extend_from_slice(&memory[start..end]);
267                        }
268                    }
269                }
270
271                tx_data.push((head_idx, data));
272            }
273        }
274
275        let mut completed = Vec::new();
276        for (head_idx, data) in tx_data {
277            let len = data.len() as u32;
278            self.handle_tx(&data)?;
279            completed.push((head_idx, len));
280        }
281
282        Ok(completed)
283    }
284
285    /// Gets the number of bytes available for RX.
286    #[must_use]
287    pub fn rx_available(&self) -> usize {
288        self.ports
289            .first()
290            .map(|p| p.input_buffer.len())
291            .unwrap_or(0)
292    }
293
294    /// Injects pending host input (queued via [`queue_input`](Self::queue_input))
295    /// into the guest RX queue (queue 0), advancing the used ring.
296    ///
297    /// Descriptors are read directly from guest memory — the HV path drives the
298    /// queue from `QueueConfig` rather than the internal [`VirtQueue`]. Returns
299    /// `true` if at least one descriptor was filled, in which case the caller
300    /// must raise an `INT_VRING` interrupt so the guest services the input.
301    ///
302    /// # Errors
303    ///
304    /// Returns an error if a queue address lies below the guest RAM base.
305    pub fn process_rx_queue(
306        &mut self,
307        memory: &mut [u8],
308        queue_config: &QueueConfig,
309    ) -> Result<bool> {
310        if !queue_config.ready || queue_config.size == 0 {
311            return Ok(false);
312        }
313        // Nothing buffered to deliver.
314        if self.rx_available() == 0 {
315            return Ok(false);
316        }
317
318        // SAFETY: `memory` is the guest RAM slice; the queue accesses it only
319        // through the GuestMemWriter built here, and `memory` is not touched
320        // directly while the queue is alive.
321        let mem = std::sync::Arc::new(unsafe {
322            arcbox_virtio_core::GuestMemWriter::new(
323                memory.as_mut_ptr(),
324                memory.len(),
325                queue_config.gpa_base as usize,
326            )
327        });
328        let mut queue = arcbox_virtio_core::SplitQueue::new(mem, 0, queue_config, false);
329        queue.set_last_avail_idx(self.rx_last_avail);
330
331        let mut completions: Vec<(u16, u32)> = Vec::new();
332        // Fill RX descriptor chains with buffered input. Stop as soon as the
333        // input buffer drains, leaving the remaining descriptors for the next
334        // batch of keystrokes.
335        while self.rx_available() > 0 {
336            let Some(chain) = queue.pop_avail() else {
337                break;
338            };
339            let mut written = 0u32;
340            let mut input_done = false;
341            for desc in &chain.descriptors {
342                // RX descriptors are write-only (the device writes guest input).
343                if desc.is_write() && desc.len > 0 {
344                    if let Some(port) = self.ports.first_mut() {
345                        let n = (desc.len as usize).min(port.input_buffer.len());
346                        // SAFETY: write-only descriptor buffers are device-owned.
347                        if let Some(buf) = unsafe { queue.mem().slice_mut(desc.addr as usize, n) } {
348                            for slot in buf.iter_mut() {
349                                if let Some(b) = port.input_buffer.pop_front() {
350                                    *slot = b;
351                                }
352                            }
353                            written += n as u32;
354                            input_done = port.input_buffer.is_empty();
355                        }
356                    }
357                }
358                if input_done {
359                    break; // input exhausted
360                }
361            }
362            completions.push((chain.head_idx, written));
363        }
364
365        let injected = !completions.is_empty();
366        queue.push_used_batch(&completions);
367        self.rx_last_avail = queue.last_avail_idx();
368        Ok(injected)
369    }
370}
371
372impl VirtioDevice for VirtioConsole {
373    fn device_id(&self) -> VirtioDeviceId {
374        VirtioDeviceId::Console
375    }
376
377    fn features(&self) -> u64 {
378        self.features
379    }
380
381    fn ack_features(&mut self, features: u64) {
382        self.acked_features = self.features & features;
383    }
384
385    fn read_config(&self, offset: u64, data: &mut [u8]) {
386        // Configuration space layout (VirtIO 1.1):
387        // offset 0: cols (u16)
388        // offset 2: rows (u16)
389        // offset 4: max_nr_ports (u32)
390        // offset 8: emerg_wr (u32)
391        let config_data = [
392            self.config.cols.to_le_bytes().as_slice(),
393            &self.config.rows.to_le_bytes(),
394            &self.config.max_ports.to_le_bytes(),
395            &0u32.to_le_bytes(), // emerg_wr
396        ]
397        .concat();
398
399        let offset = offset as usize;
400        let len = data.len().min(config_data.len().saturating_sub(offset));
401        if len > 0 {
402            data[..len].copy_from_slice(&config_data[offset..offset + len]);
403        }
404    }
405
406    fn write_config(&mut self, offset: u64, data: &[u8]) {
407        // Handle emergency write at offset 8
408        if offset == 8 && data.len() >= 4 {
409            let ch = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
410            if ch != 0 {
411                if let Some(c) = char::from_u32(ch) {
412                    eprint!("{c}");
413                }
414            }
415        }
416    }
417
418    fn activate(&mut self) -> Result<()> {
419        self.rx_queue = Some(VirtQueue::new(256)?);
420        self.tx_queue = Some(VirtQueue::new(256)?);
421
422        if let Some(port) = self.ports.first_mut() {
423            port.open = true;
424        }
425
426        tracing::info!(
427            "VirtIO console activated: {}x{}, {} ports",
428            self.config.cols,
429            self.config.rows,
430            self.config.max_ports
431        );
432
433        Ok(())
434    }
435
436    fn reset(&mut self) {
437        self.acked_features = 0;
438        self.rx_queue = None;
439        self.tx_queue = None;
440        self.rx_last_avail = 0;
441
442        for port in &mut self.ports {
443            port.open = false;
444            port.input_buffer.clear();
445            // Flush any final partial line (e.g. a panic message with no trailing
446            // newline) before discarding the buffer, so the dying line isn't lost.
447            emit_console_lines(port.take_all_lines());
448        }
449    }
450
451    fn process_queue(
452        &mut self,
453        queue_idx: u16,
454        memory: &mut [u8],
455        queue_config: &QueueConfig,
456    ) -> Result<Vec<(u16, u32)>> {
457        // Queue 0 = RX (host→guest), Queue 1 = TX (guest→host).
458        // We only handle TX here — extract guest output from descriptors.
459        if queue_idx != 1 {
460            return Ok(Vec::new());
461        }
462
463        if !queue_config.ready || queue_config.size == 0 {
464            return Ok(Vec::new());
465        }
466
467        // SAFETY: `memory` is the guest RAM slice; the queue accesses it only
468        // through the GuestMemWriter built here, and `memory` is not touched
469        // directly while the queue is alive.
470        let mem = std::sync::Arc::new(unsafe {
471            arcbox_virtio_core::GuestMemWriter::new(
472                memory.as_mut_ptr(),
473                memory.len(),
474                queue_config.gpa_base as usize,
475            )
476        });
477        let mut queue = arcbox_virtio_core::SplitQueue::new(mem, queue_idx, queue_config, false);
478        // TX posts exactly one used entry per consumed avail entry, so the avail
479        // cursor tracks the guest's current used.idx (which SplitQueue seeded).
480        let start = queue.mem().read_u16(queue_config.used_addr as usize + 2);
481        queue.set_last_avail_idx(start);
482
483        // Mirror guest output to the attached backend (e.g. the debug-console
484        // Unix socket) in addition to the `guest_console` tracing target, so an
485        // interactive operator sees raw output immediately — including partial
486        // lines and prompts that never carry a trailing newline.
487        let io = self.io.clone();
488
489        let mut completions = Vec::new();
490        while let Some(chain) = queue.pop_avail() {
491            let mut total_len = 0u32;
492            for desc in &chain.descriptors {
493                // Read-only descriptor = data FROM guest (TX output).
494                if desc.is_write() {
495                    continue;
496                }
497                let Some(data) = queue.mem().slice(desc.addr as usize, desc.len as usize) else {
498                    continue;
499                };
500                if let Some(io) = &io {
501                    if let Ok(mut g) = io.lock() {
502                        let _ = g.write(data);
503                        let _ = g.flush();
504                    }
505                }
506                if let Some(port) = self.ports.first_mut() {
507                    port.output_buffer.extend(data.iter().copied());
508                    // Emit complete (newline-terminated) lines; any trailing
509                    // partial stays buffered for the next batch (or is flushed
510                    // on reset/drop).
511                    emit_console_lines(port.take_complete_lines());
512                }
513                total_len += desc.len;
514            }
515            completions.push((chain.head_idx, total_len));
516        }
517
518        queue.push_used_batch(&completions);
519        Ok(completions)
520    }
521}
522
523impl Drop for VirtioConsole {
524    fn drop(&mut self) {
525        // Capture any final partial line buffered on each port (a panic/crash
526        // message that never got a trailing newline) before teardown loses it.
527        for port in &mut self.ports {
528            emit_console_lines(port.take_all_lines());
529        }
530    }
531}
532
533#[cfg(test)]
534mod tests {
535    use super::*;
536    use crate::BufferConsole;
537
538    #[test]
539    fn test_console_creation() {
540        let console = VirtioConsole::new(ConsoleConfig::default());
541        assert_eq!(console.device_id(), VirtioDeviceId::Console);
542        assert!(console.features() & VirtioConsole::FEATURE_SIZE != 0);
543    }
544
545    #[test]
546    fn test_console_config_read() {
547        let config = ConsoleConfig {
548            cols: 120,
549            rows: 40,
550            max_ports: 4,
551            multiport: false,
552        };
553        let console = VirtioConsole::new(config);
554
555        let mut data = [0u8; 8];
556        console.read_config(0, &mut data);
557
558        assert_eq!(u16::from_le_bytes([data[0], data[1]]), 120); // cols
559        assert_eq!(u16::from_le_bytes([data[2], data[3]]), 40); // rows
560        assert_eq!(u32::from_le_bytes([data[4], data[5], data[6], data[7]]), 4);
561        // max_ports
562    }
563
564    #[test]
565    fn test_console_input_queue() {
566        let mut console = VirtioConsole::new(ConsoleConfig::default());
567        console.activate().unwrap();
568
569        console.queue_input(b"test input").unwrap();
570        assert_eq!(console.rx_available(), 10);
571    }
572
573    #[test]
574    fn test_console_output() {
575        let buffer = Arc::new(Mutex::new(BufferConsole::new()));
576        let mut console = VirtioConsole::new(ConsoleConfig::default());
577        console.set_io(buffer.clone());
578        console.activate().unwrap();
579
580        console.handle_tx(b"Hello, World!").unwrap();
581
582        let output = buffer.lock().unwrap().take_output();
583        assert_eq!(&output, b"Hello, World!");
584    }
585
586    #[test]
587    fn test_console_multiport_feature() {
588        let config = ConsoleConfig {
589            multiport: true,
590            ..Default::default()
591        };
592        let console = VirtioConsole::new(config);
593        assert!(console.features() & VirtioConsole::FEATURE_MULTIPORT != 0);
594    }
595
596    #[test]
597    fn test_console_activate_and_reset() {
598        let mut console = VirtioConsole::new(ConsoleConfig::default());
599
600        console.activate().unwrap();
601        assert!(console.rx_queue.is_some());
602        assert!(console.tx_queue.is_some());
603
604        console.reset();
605        assert!(console.rx_queue.is_none());
606        assert!(console.tx_queue.is_none());
607        assert_eq!(console.acked_features, 0);
608    }
609
610    #[test]
611    fn test_console_read_output() {
612        let mut console = VirtioConsole::new(ConsoleConfig::default());
613        console.activate().unwrap();
614
615        let output = console.read_output();
616        assert!(output.is_empty());
617
618        console.handle_tx(b"test output").unwrap();
619        let output = console.read_output();
620        assert_eq!(&output, b"test output");
621
622        let output2 = console.read_output();
623        assert!(output2.is_empty());
624    }
625
626    #[test]
627    fn test_console_queue_input_not_ready() {
628        let mut console = VirtioConsole::new(ConsoleConfig::default());
629
630        console.ports.clear();
631
632        let result = console.queue_input(b"test");
633        assert!(result.is_err());
634    }
635
636    #[test]
637    fn test_console_config_write() {
638        let mut console = VirtioConsole::new(ConsoleConfig::default());
639
640        let emergency_char = 'X' as u32;
641        console.write_config(8, &emergency_char.to_le_bytes());
642
643        // Should not crash — emergency write goes to stderr.
644    }
645
646    #[test]
647    fn test_console_feature_negotiation() {
648        let mut console = VirtioConsole::new(ConsoleConfig::default());
649
650        let offered = console.features();
651        assert!(offered & VirtioConsole::FEATURE_VERSION_1 != 0);
652
653        console.ack_features(VirtioConsole::FEATURE_SIZE | VirtioConsole::FEATURE_VERSION_1);
654        assert!(console.acked_features & VirtioConsole::FEATURE_SIZE != 0);
655    }
656
657    #[test]
658    fn test_console_with_stdio() {
659        let console = VirtioConsole::with_stdio();
660        assert!(console.io.is_some());
661    }
662
663    #[test]
664    fn test_console_config_partial_read() {
665        let console = VirtioConsole::new(ConsoleConfig {
666            cols: 80,
667            rows: 25,
668            max_ports: 1,
669            multiport: false,
670        });
671
672        let mut data = [0u8; 2];
673        console.read_config(0, &mut data);
674        assert_eq!(u16::from_le_bytes(data), 80);
675
676        let mut data2 = [0u8; 2];
677        console.read_config(2, &mut data2);
678        assert_eq!(u16::from_le_bytes(data2), 25);
679    }
680
681    #[test]
682    fn test_console_rx_available_empty() {
683        let console = VirtioConsole::new(ConsoleConfig::default());
684        assert_eq!(console.rx_available(), 0);
685    }
686
687    #[test]
688    fn test_console_process_rx_queue_injects_input() {
689        // Lay out a 4-entry RX virtqueue inside a flat guest-memory buffer.
690        const SIZE: u16 = 4;
691        const DESC: usize = 0x100;
692        const AVAIL: usize = 0x200;
693        const USED: usize = 0x300;
694        const BUF: usize = 0x400;
695        let mut mem = vec![0u8; 0x600];
696
697        // Descriptor 0: write-only buffer at BUF, len 16.
698        mem[DESC..DESC + 8].copy_from_slice(&(BUF as u64).to_le_bytes());
699        mem[DESC + 8..DESC + 12].copy_from_slice(&16u32.to_le_bytes());
700        mem[DESC + 12..DESC + 14].copy_from_slice(&2u16.to_le_bytes()); // VIRTQ_DESC_F_WRITE
701        mem[DESC + 14..DESC + 16].copy_from_slice(&0u16.to_le_bytes());
702
703        // Avail ring: ring[0] = descriptor 0, idx = 1.
704        mem[AVAIL + 4..AVAIL + 6].copy_from_slice(&0u16.to_le_bytes());
705        mem[AVAIL + 2..AVAIL + 4].copy_from_slice(&1u16.to_le_bytes());
706
707        let qc = QueueConfig {
708            desc_addr: DESC as u64,
709            avail_addr: AVAIL as u64,
710            used_addr: USED as u64,
711            size: SIZE,
712            ready: true,
713            gpa_base: 0,
714        };
715
716        let mut console = VirtioConsole::new(ConsoleConfig::default());
717        // No input yet → nothing to inject.
718        assert!(!console.process_rx_queue(&mut mem, &qc).unwrap());
719
720        console.queue_input(b"hello").unwrap();
721        let injected = console.process_rx_queue(&mut mem, &qc).unwrap();
722        assert!(injected);
723        assert_eq!(&mem[BUF..BUF + 5], b"hello");
724
725        // Used ring advanced to 1, entry id = descriptor 0, len = 5.
726        assert_eq!(u16::from_le_bytes([mem[USED + 2], mem[USED + 3]]), 1);
727        assert_eq!(
728            u32::from_le_bytes([mem[USED + 4], mem[USED + 5], mem[USED + 6], mem[USED + 7]]),
729            0
730        );
731        assert_eq!(
732            u32::from_le_bytes([mem[USED + 8], mem[USED + 9], mem[USED + 10], mem[USED + 11]]),
733            5
734        );
735
736        // Input fully consumed; a second call is a no-op (no fresh avail entries).
737        assert_eq!(console.rx_available(), 0);
738        assert!(!console.process_rx_queue(&mut mem, &qc).unwrap());
739    }
740
741    #[test]
742    fn take_complete_lines_leaves_trailing_partial_buffered() {
743        let mut port = ConsolePort::new(0);
744        port.output_buffer
745            .extend(b"first\nsecond\npartial no newline");
746
747        // Only the newline-terminated lines come out; the partial stays buffered.
748        assert_eq!(port.take_complete_lines(), vec!["first", "second"]);
749        assert!(!port.output_buffer.is_empty());
750    }
751
752    #[test]
753    fn take_all_lines_flushes_the_dying_partial_line() {
754        let mut port = ConsolePort::new(0);
755        // A panic message that never got a trailing newline before teardown.
756        port.output_buffer
757            .extend(b"Kernel panic - not syncing: Attempted to kill init");
758
759        // take_complete_lines would drop it (no newline); take_all_lines keeps it.
760        let lines = port.take_all_lines();
761        assert_eq!(
762            lines,
763            vec!["Kernel panic - not syncing: Attempted to kill init"]
764        );
765        assert!(port.output_buffer.is_empty());
766    }
767}