Skip to main content

somehal/
boot_console.rs

1use rdrive::{DeviceId, Fdt};
2
3#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4enum ConsoleSpec {
5    HardwareSerial(usize),
6    VirtualTty,
7}
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum ConsoleDeviceIdError {
11    NotSpecified,
12    NoHardwareDevice,
13    DeviceNotFound,
14}
15
16pub fn device_id() -> Result<DeviceId, ConsoleDeviceIdError> {
17    match device_id_from_bootargs(someboot::cmdline()) {
18        Ok(device_id) => Ok(device_id),
19        Err(ConsoleDeviceIdError::NotSpecified) => device_id_from_acpi_spcr()
20            .or_else(device_id_from_fdt_stdout)
21            .ok_or(ConsoleDeviceIdError::NotSpecified),
22        Err(
23            err @ (ConsoleDeviceIdError::NoHardwareDevice | ConsoleDeviceIdError::DeviceNotFound),
24        ) => Err(err),
25    }
26}
27
28fn device_id_from_bootargs(cmdline: Option<&str>) -> Result<DeviceId, ConsoleDeviceIdError> {
29    device_id_from_bootargs_with(cmdline, device_id_from_serial_index)
30}
31
32fn device_id_from_bootargs_with(
33    cmdline: Option<&str>,
34    serial_device_id: impl Fn(usize) -> Option<DeviceId>,
35) -> Result<DeviceId, ConsoleDeviceIdError> {
36    let cmdline = cmdline.ok_or(ConsoleDeviceIdError::NotSpecified)?;
37    let mut has_console_spec = false;
38    let mut last_hardware_serial = None;
39
40    for spec in console_specs(cmdline) {
41        has_console_spec = true;
42        if let ConsoleSpec::HardwareSerial(index) = spec {
43            last_hardware_serial = Some(index);
44        }
45    }
46
47    match last_hardware_serial {
48        Some(index) => serial_device_id(index).ok_or(ConsoleDeviceIdError::DeviceNotFound),
49        None if has_console_spec => Err(ConsoleDeviceIdError::NoHardwareDevice),
50        None => Err(ConsoleDeviceIdError::NotSpecified),
51    }
52}
53
54fn console_specs(cmdline: &str) -> impl Iterator<Item = ConsoleSpec> + '_ {
55    cmdline
56        .split_ascii_whitespace()
57        .filter_map(|arg| arg.strip_prefix("console="))
58        .filter_map(parse_console_spec)
59}
60
61fn parse_console_spec(spec: &str) -> Option<ConsoleSpec> {
62    let name = spec.split(',').next().unwrap_or(spec);
63    if name == "tty"
64        || name == "ttynull"
65        || name
66            .strip_prefix("tty")
67            .is_some_and(|suffix| !suffix.is_empty() && suffix.bytes().all(|c| c.is_ascii_digit()))
68    {
69        return Some(ConsoleSpec::VirtualTty);
70    }
71
72    parse_number_suffix(name, "ttyS")
73        .or_else(|| parse_number_suffix(name, "ttyAMA"))
74        .map(ConsoleSpec::HardwareSerial)
75}
76
77fn parse_number_suffix(name: &str, prefix: &str) -> Option<usize> {
78    name.strip_prefix(prefix)?.parse().ok()
79}
80
81fn device_id_from_serial_index(index: usize) -> Option<DeviceId> {
82    device_id_from_serial_index_with(index, fdt_serial_alias_device_id, device_id_from_acpi_spcr)
83}
84
85fn device_id_from_serial_index_with(
86    index: usize,
87    fdt_device_id: impl FnOnce(usize) -> Option<DeviceId>,
88    spcr_device_id: impl FnOnce() -> Option<DeviceId>,
89) -> Option<DeviceId> {
90    fdt_device_id(index).or_else(|| if index == 0 { spcr_device_id() } else { None })
91}
92
93fn fdt_serial_alias_device_id(index: usize) -> Option<DeviceId> {
94    rdrive::with_fdt(|fdt| {
95        let alias = alloc::format!("serial{index}");
96        let path = alias_path(fdt, &alias)?;
97        rdrive::fdt_path_to_device_id(path)
98    })
99    .flatten()
100}
101
102fn device_id_from_acpi_spcr() -> Option<DeviceId> {
103    rdrive::acpi_spcr_console_device_id()
104}
105
106fn device_id_from_fdt_stdout() -> Option<DeviceId> {
107    rdrive::with_fdt(stdout_device_id).flatten()
108}
109
110fn stdout_device_id(fdt: &Fdt) -> Option<DeviceId> {
111    let chosen = fdt.get_by_path("/chosen")?;
112    ["stdout-path", "linux,stdout-path"]
113        .into_iter()
114        .find_map(|key| {
115            let raw = chosen.as_node().get_property(key)?.as_str()?;
116            let path = split_stdout_options(raw);
117            if path.is_empty() {
118                return None;
119            }
120            if path.starts_with('/') {
121                return rdrive::fdt_path_to_device_id(path);
122            }
123            alias_path(fdt, path).and_then(rdrive::fdt_path_to_device_id)
124        })
125}
126
127fn split_stdout_options(stdout: &str) -> &str {
128    stdout.split(':').next().unwrap_or(stdout)
129}
130
131fn alias_path<'a>(fdt: &'a Fdt, alias: &str) -> Option<&'a str> {
132    fdt.get_by_path("/aliases")?
133        .as_node()
134        .get_property(alias)?
135        .as_str()
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn console_specs_keep_command_line_order() {
144        let specs: alloc::vec::Vec<_> =
145            console_specs("console=ttyS2,1500000 console=tty1 console=ttyAMA3,115200").collect();
146
147        assert_eq!(
148            specs,
149            alloc::vec![
150                ConsoleSpec::HardwareSerial(2),
151                ConsoleSpec::VirtualTty,
152                ConsoleSpec::HardwareSerial(3),
153            ]
154        );
155    }
156
157    #[test]
158    fn parses_supported_serial_console_names() {
159        assert_eq!(
160            parse_console_spec("ttyS0,115200n8"),
161            Some(ConsoleSpec::HardwareSerial(0))
162        );
163        assert_eq!(
164            parse_console_spec("ttyAMA1"),
165            Some(ConsoleSpec::HardwareSerial(1))
166        );
167        assert_eq!(parse_console_spec("ttynull"), Some(ConsoleSpec::VirtualTty));
168        assert_eq!(parse_console_spec("tty7"), Some(ConsoleSpec::VirtualTty));
169        assert_eq!(parse_console_spec("ttySx"), None);
170    }
171
172    #[test]
173    fn bootargs_console_spec_suppresses_firmware_fallback() {
174        assert_eq!(
175            device_id_from_bootargs(Some("root=/dev/vda")),
176            Err(ConsoleDeviceIdError::NotSpecified)
177        );
178        assert_eq!(
179            device_id_from_bootargs(Some("console=tty1")),
180            Err(ConsoleDeviceIdError::NoHardwareDevice)
181        );
182        assert_eq!(
183            device_id_from_bootargs(Some("console=ttyS2 console=tty1")),
184            Err(ConsoleDeviceIdError::DeviceNotFound)
185        );
186    }
187
188    #[test]
189    fn later_virtual_console_does_not_hide_hardware_device_id() {
190        let serial2_device = DeviceId::from(42);
191
192        assert_eq!(
193            device_id_from_bootargs_with(Some("console=ttyS2,1500000 console=tty1"), |index| {
194                (index == 2).then_some(serial2_device)
195            }),
196            Ok(serial2_device)
197        );
198    }
199
200    #[test]
201    fn last_available_hardware_console_wins_over_earlier_serial_console() {
202        let serial3_device = DeviceId::from(43);
203
204        assert_eq!(
205            device_id_from_bootargs_with(
206                Some("console=ttyS2,1500000 console=tty1 console=ttyAMA3,115200"),
207                |index| (index == 3).then_some(serial3_device),
208            ),
209            Ok(serial3_device)
210        );
211    }
212
213    #[test]
214    fn later_missing_hardware_console_overrides_earlier_available_serial_console() {
215        let serial2_device = DeviceId::from(42);
216
217        assert_eq!(
218            device_id_from_bootargs_with(
219                Some("console=ttyS2,1500000 console=ttyS3,115200 console=tty1"),
220                |index| (index == 2).then_some(serial2_device),
221            ),
222            Err(ConsoleDeviceIdError::DeviceNotFound)
223        );
224        assert_eq!(
225            device_id_from_bootargs_with(
226                Some("console=ttyS2,1500000 console=ttyS3,115200"),
227                |index| (index == 2).then_some(serial2_device),
228            ),
229            Err(ConsoleDeviceIdError::DeviceNotFound)
230        );
231    }
232
233    #[test]
234    fn serial_index_zero_can_use_acpi_spcr_when_fdt_alias_is_absent() {
235        let spcr_device = DeviceId::from(42);
236
237        assert_eq!(
238            device_id_from_serial_index_with(0, |_| None, || Some(spcr_device)),
239            Some(spcr_device)
240        );
241    }
242
243    #[test]
244    fn non_zero_serial_index_does_not_fallback_to_acpi_spcr() {
245        let spcr_device = DeviceId::from(42);
246
247        assert_eq!(
248            device_id_from_serial_index_with(2, |_| None, || Some(spcr_device)),
249            None
250        );
251    }
252
253    #[test]
254    fn splits_stdout_options() {
255        assert_eq!(
256            split_stdout_options("/soc/serial@1000:115200n8"),
257            "/soc/serial@1000"
258        );
259        assert_eq!(split_stdout_options("serial0"), "serial0");
260    }
261}