Skip to main content

console_rust/
lib.rs

1// Copyright 2026 The Fuchsia Authors
2//
3// Use of this source code is governed by a MIT-style
4// license that can be found in the LICENSE file or at
5// https://opensource.org/licenses/MIT
6
7#![no_std]
8#![allow(unsafe_op_in_unsafe_fn)]
9#![allow(unused_crate_dependencies)]
10#![cfg(console_enabled)]
11
12use core::ffi::{c_char, c_int, c_void};
13use core::sync::atomic::{AtomicBool, Ordering};
14use zx_status::Status;
15
16// Converts a Status to its raw FFI representation for the C boundary.
17macro_rules! zx_status {
18    ($status:expr) => {
19        $status.into_raw()
20    };
21}
22
23pub const CMD_AVAIL_NORMAL: u8 = 1 << 0;
24pub const CMD_AVAIL_PANIC: u8 = 1 << 1;
25pub const CMD_AVAIL_ALWAYS: u8 = CMD_AVAIL_NORMAL | CMD_AVAIL_PANIC;
26
27// Command is happening at crash time.
28pub const CMD_FLAG_PANIC: u32 = 1 << 0;
29
30const BOOT_TEST_SUCCESS_STRING: &core::ffi::CStr = unsafe {
31    core::ffi::CStr::from_bytes_with_nul_unchecked(
32        concat!(env!("BOOT_TEST_SUCCESS_STRING"), "\0").as_bytes(),
33    )
34};
35
36pub static ECHO: AtomicBool = AtomicBool::new(true);
37pub static EXIT_CONSOLE: AtomicBool = AtomicBool::new(false);
38
39#[repr(C)]
40#[derive(Clone, Copy)]
41pub struct CmdArgs {
42    pub arg_str: *const c_char,
43    pub arg_uint: core::ffi::c_ulong,
44    pub arg_ptr: *mut c_void,
45    pub arg_int: core::ffi::c_long,
46    pub arg_bool: bool,
47}
48
49pub type CmdCallback = unsafe extern "C" fn(argc: c_int, argv: *const CmdArgs, flags: u32) -> c_int;
50
51#[repr(C)]
52pub struct Cmd {
53    pub cmd_str: *const c_char,
54    pub help_str: *const c_char,
55    pub cmd_callback: CmdCallback,
56    pub availability_mask: u8,
57}
58
59// Safety: Cmd structures placed in the commands section are read-only
60// after boot and safe to share between threads.
61unsafe impl Sync for Cmd {}
62
63// Verify size and alignment compatibility with C++ structs.
64zr::static_assert!(core::mem::size_of::<CmdArgs>() == 40);
65zr::static_assert!(core::mem::align_of::<CmdArgs>() == 8);
66zr::static_assert!(core::mem::size_of::<Cmd>() == 32);
67zr::static_assert!(core::mem::align_of::<Cmd>() == 8);
68
69#[cfg(console_enabled)]
70#[macro_export]
71macro_rules! commands_section {
72    () => {
73        ".data.rel.ro.commands"
74    };
75}
76
77#[macro_export]
78macro_rules! static_command {
79    ($var_name:ident, $cmd:expr, $help:expr, $func:expr, $mask:expr) => {
80        #[cfg(console_enabled)]
81        #[used]
82        #[unsafe(link_section = $crate::commands_section!())]
83        pub static $var_name: $crate::Cmd = $crate::Cmd {
84            cmd_str: $cmd,
85            help_str: $help,
86            cmd_callback: $func,
87            availability_mask: $mask,
88        };
89    };
90}
91
92// FFI imports.
93unsafe extern "C" {
94    // Used to print directly to the kernel console without stack-allocated formatting buffers.
95    fn printf(format: *const c_char, ...) -> c_int;
96    fn cpp_console_get_lastresult() -> c_int;
97}
98
99// Statically registered commands.
100static_command!(CMD_ECHO, c"echo".as_ptr(), core::ptr::null(), cmd_echo, CMD_AVAIL_ALWAYS);
101
102static_command!(
103    CMD_EXIT,
104    c"exit".as_ptr(),
105    c"exit the command processor".as_ptr(),
106    cmd_exit,
107    CMD_AVAIL_NORMAL
108);
109
110static_command!(
111    CMD_TEST,
112    c"test".as_ptr(),
113    c"test the command processor".as_ptr(),
114    cmd_test,
115    CMD_AVAIL_ALWAYS
116);
117
118static_command!(
119    CMD_GRACEFUL_SHUTDOWN,
120    c"graceful-shutdown".as_ptr(),
121    c"shut the system down gracefully".as_ptr(),
122    cmd_graceful_shutdown,
123    CMD_AVAIL_ALWAYS
124);
125
126static_command!(
127    CMD_BOOT_TEST_SUCCESS,
128    c"boot-test-success".as_ptr(),
129    c"report boot-test success".as_ptr(),
130    cmd_boot_test_success,
131    CMD_AVAIL_ALWAYS
132);
133
134static_command!(
135    CMD_AND,
136    c"and".as_ptr(),
137    c"execute command if last command succeeded".as_ptr(),
138    cmd_and,
139    CMD_AVAIL_ALWAYS
140);
141
142static_command!(
143    CMD_REPEAT,
144    c"repeat".as_ptr(),
145    c"execute command in a loop for N loops or until error".as_ptr(),
146    cmd_repeat,
147    CMD_AVAIL_ALWAYS
148);
149
150static_command!(CMD_HELP, c"help".as_ptr(), c"this list".as_ptr(), cmd_help, CMD_AVAIL_ALWAYS);
151
152// Callback implementations.
153unsafe extern "C" fn cmd_echo(argc: c_int, argv: *const CmdArgs, _flags: u32) -> c_int {
154    if argc > 1 {
155        let args = core::slice::from_raw_parts(argv, argc as usize);
156        ECHO.store(args[1].arg_bool, Ordering::Relaxed);
157    }
158    zx_status!(Status::OK)
159}
160
161unsafe extern "C" fn cmd_exit(_argc: c_int, _argv: *const CmdArgs, _flags: u32) -> c_int {
162    rust_console_set_exit(true);
163    zx_status!(Status::OK)
164}
165
166unsafe extern "C" fn cmd_test(argc: c_int, argv: *const CmdArgs, _flags: u32) -> c_int {
167    let args = core::slice::from_raw_parts(argv, argc as usize);
168    printf(c"argc %d, argv %p\n".as_ptr(), argc, argv);
169    for (i, arg) in args.iter().enumerate() {
170        printf(
171            c"\t%d: str '%s', int %ld, uint %#lx, ptr %p, bool %d\n".as_ptr(),
172            i as c_int,
173            arg.arg_str,
174            arg.arg_int,
175            arg.arg_uint,
176            arg.arg_ptr,
177            arg.arg_bool as c_int,
178        );
179    }
180    zx_status!(Status::OK)
181}
182
183unsafe extern "C" fn cmd_graceful_shutdown(
184    _argc: c_int,
185    _argv: *const CmdArgs,
186    _flags: u32,
187) -> c_int {
188    // We use %c and format arguments here to prevent the compiler from optimizing the printf call
189    // into puts or putchar, which are not defined in the kernel.
190    printf(c"%c** Performing graceful shutdown from kernel shell... ***\n".as_ptr(), b'*' as c_int);
191    const ZX_SEC_10: i64 = 10 * 1_000_000_000;
192    let dlog_deadline = platform_rs::current_mono_time() + ZX_SEC_10;
193    if let Err(status) = debuglog_rs::dlog_shutdown(dlog_deadline) {
194        printf(c"debuglog shutdown failed: %d\n".as_ptr(), status.into_raw());
195        // Proceed to platform_halt() even if debuglog_rs::dlog_shutdown() fails.
196    }
197    // Does not return.
198    platform_rs::power::platform_halt(
199        platform_rs::power::PlatformHaltAction::Shutdown,
200        platform_rs::power::ZirconCrashReason::NoCrash,
201    );
202}
203
204unsafe extern "C" fn cmd_boot_test_success(
205    _argc: c_int,
206    _argv: *const CmdArgs,
207    _flags: u32,
208) -> c_int {
209    let last = cpp_console_get_lastresult();
210    printf(c"*** Last script command result: %d ***\n".as_ptr(), last);
211    if last == 0 {
212        printf(c"%s%c".as_ptr(), BOOT_TEST_SUCCESS_STRING.as_ptr(), b'\n' as c_int);
213    }
214    last
215}
216
217unsafe fn get_commands() -> &'static [Cmd] {
218    unsafe extern "C" {
219        #[link_name = "__start_commands"]
220        static __start_commands: Cmd;
221        #[link_name = "__stop_commands"]
222        static __stop_commands: Cmd;
223    }
224    let start = &__start_commands as *const Cmd;
225    let stop = &__stop_commands as *const Cmd;
226    let count = (stop as usize - start as usize) / core::mem::size_of::<Cmd>();
227    unsafe { core::slice::from_raw_parts(start, count) }
228}
229
230unsafe fn match_command(name: *const c_char, availability_mask: u8) -> Option<&'static Cmd> {
231    let commands = unsafe { get_commands() };
232    let name_cstr = unsafe { core::ffi::CStr::from_ptr(name) };
233    for cmd in commands {
234        if (availability_mask & cmd.availability_mask) != 0 {
235            let cmd_str_cstr = unsafe { core::ffi::CStr::from_ptr(cmd.cmd_str) };
236            if cmd_str_cstr == name_cstr {
237                return Some(cmd);
238            }
239        }
240    }
241    None
242}
243
244unsafe extern "C" fn cmd_and(argc: c_int, argv: *const CmdArgs, flags: u32) -> c_int {
245    if argc < 2 {
246        printf(c"Usage: and COMMAND...%c".as_ptr(), b'\n' as c_int);
247        return zx_status!(Status::INVALID_ARGS);
248    }
249
250    let last = cpp_console_get_lastresult();
251    if last != 0 {
252        return last;
253    }
254
255    let args = core::slice::from_raw_parts(argv, argc as usize);
256    let cmd = match match_command(args[1].arg_str, CMD_AVAIL_NORMAL) {
257        Some(cmd) => cmd,
258        None => {
259            printf(c"command \"%s\" not found%c".as_ptr(), args[1].arg_str, b'\n' as c_int);
260            return zx_status!(Status::NOT_FOUND);
261        }
262    };
263
264    (cmd.cmd_callback)(argc - 1, argv.add(1), flags)
265}
266
267unsafe extern "C" fn cmd_repeat(argc: c_int, argv: *const CmdArgs, flags: u32) -> c_int {
268    const MIN_ARGS: c_int = 3;
269    if argc < MIN_ARGS {
270        printf(c"Usage: repeat <iterations | -1> COMMAND...%c".as_ptr(), b'\n' as c_int);
271        return zx_status!(Status::INVALID_ARGS);
272    }
273
274    let args = core::slice::from_raw_parts(argv, argc as usize);
275    let cmd = match match_command(args[2].arg_str, CMD_AVAIL_NORMAL) {
276        Some(cmd) => cmd,
277        None => {
278            printf(c"command \"%s\" not found%c".as_ptr(), args[2].arg_str, b'\n' as c_int);
279            return zx_status!(Status::NOT_FOUND);
280        }
281    };
282
283    // Negative arguments will cause it to effectively loop forever
284    let iterations = if args[1].arg_int >= 0 { args[1].arg_uint as usize } else { usize::MAX };
285    for i in 0..iterations {
286        if iterations == usize::MAX {
287            printf(c"repeat (%zu): %s".as_ptr(), i + 1, args[2].arg_str);
288        } else {
289            printf(c"repeat (%zu/%zu): %s".as_ptr(), i + 1, iterations, args[2].arg_str);
290        }
291        for arg in MIN_ARGS..argc {
292            printf(c" %s".as_ptr(), args[arg as usize].arg_str);
293        }
294        // We use %c and format arguments here to prevent the compiler from optimizing the printf
295        // call into puts or putchar, which are not defined in the kernel.
296        printf(c"%c%s".as_ptr(), b'\n' as c_int, c"".as_ptr());
297
298        let err = (cmd.cmd_callback)(argc - 2, argv.add(2), flags);
299        if err != zx_status!(Status::OK) {
300            printf(c"stopping repeat due to nonzero status %d%c".as_ptr(), err, b'\n' as c_int);
301            return err;
302        }
303    }
304
305    zx_status!(Status::OK)
306}
307
308unsafe extern "C" fn cmd_help(_argc: c_int, _argv: *const CmdArgs, flags: u32) -> c_int {
309    let commands = unsafe { get_commands() };
310    let count = commands.len();
311
312    // Filter out commands based on if we're called at normal or panic time.
313    let availability_mask =
314        if (flags & CMD_FLAG_PANIC) != 0 { CMD_AVAIL_PANIC } else { CMD_AVAIL_NORMAL };
315
316    unsafe {
317        printf(c"command list:%c".as_ptr(), b'\n' as c_int);
318    }
319
320    // If we're not panicking (and are free to allocate memory), sort the
321    // commands alphabetically before printing.
322    if (flags & CMD_FLAG_PANIC) != 0 {
323        for cmd in commands {
324            if (availability_mask & cmd.availability_mask) != 0 {
325                if !cmd.help_str.is_null() {
326                    unsafe {
327                        printf(
328                            c"\t%-16s: %s%c".as_ptr(),
329                            cmd.cmd_str,
330                            cmd.help_str,
331                            b'\n' as c_int,
332                        );
333                    }
334                }
335            }
336        }
337    } else {
338        let mut ptrs_slice = match kalloc::Box::<[*const Cmd]>::try_new_zeroed_slice(count) {
339            Ok(s) => s,
340            Err(_) => return zx_status!(Status::NO_MEMORY),
341        };
342        for (i, cmd) in commands.iter().enumerate() {
343            ptrs_slice[i] = cmd;
344        }
345
346        ptrs_slice.sort_unstable_by(|&a, &b| {
347            let a_str = unsafe { core::ffi::CStr::from_ptr((*a).cmd_str) };
348            let b_str = unsafe { core::ffi::CStr::from_ptr((*b).cmd_str) };
349            a_str.cmp(b_str)
350        });
351
352        for &cmd_ptr in ptrs_slice.iter() {
353            let cmd = unsafe { &*cmd_ptr };
354            if (availability_mask & cmd.availability_mask) != 0 {
355                if !cmd.help_str.is_null() {
356                    unsafe {
357                        printf(
358                            c"\t%-16s: %s%c".as_ptr(),
359                            cmd.cmd_str,
360                            cmd.help_str,
361                            b'\n' as c_int,
362                        );
363                    }
364                }
365            }
366        }
367    }
368
369    zx_status!(Status::OK)
370}
371
372// FFI exports.
373#[unsafe(no_mangle)]
374pub extern "C" fn rust_console_get_echo() -> bool {
375    ECHO.load(Ordering::Relaxed)
376}
377
378#[unsafe(no_mangle)]
379pub extern "C" fn rust_console_get_exit() -> bool {
380    EXIT_CONSOLE.load(Ordering::Relaxed)
381}
382
383#[unsafe(no_mangle)]
384pub extern "C" fn rust_console_set_exit(val: bool) {
385    EXIT_CONSOLE.store(val, Ordering::Relaxed);
386}
387
388/// # Safety
389///
390/// `name` must be a valid pointer to a null-terminated C string.
391#[unsafe(no_mangle)]
392pub unsafe extern "C" fn rust_console_match_command(
393    name: *const c_char,
394    availability_mask: u8,
395) -> *const Cmd {
396    match match_command(name, availability_mask) {
397        Some(cmd) => cmd,
398        None => core::ptr::null(),
399    }
400}