1use anyhow::{Context, Error};
6use async_lock::OnceCell;
7use fidl_fuchsia_boot as fboot;
8use fidl_fuchsia_feedback::{LastRebootInfoProviderMarker, RebootReason};
9use fidl_fuchsia_io as fio;
10use fuchsia_component::client::{connect_to_protocol, connect_to_protocol_sync};
11use fuchsia_fs::node::OpenError;
12use log::{debug, info};
13use zx_status::Status;
14
15const STARTED_ONCE: &str = "component-started-once";
17static HAS_STARNIX_SESSION_RESTARTED: OnceCell<bool> = OnceCell::new();
20static ANDROID_BOOTREASON: OnceCell<Result<String, Error>> = OnceCell::new();
22
23const LRIP_FIDL_TIMEOUT: zx::MonotonicDuration = zx::MonotonicDuration::INFINITE;
25
26async fn has_session_restarted(dir: Option<fio::DirectoryProxy>) -> bool {
29 match dir {
30 Some(dir) => {
31 match fuchsia_fs::directory::open_file(&dir, STARTED_ONCE, fio::Flags::FLAG_MUST_CREATE)
32 .await
33 {
34 Ok(_file) => false,
35 Err(OpenError::OpenError(Status::ALREADY_EXISTS)) => true,
36 Err(err) => {
37 info!("Failed to generate the file with err {err:#?}.");
38 false
39 }
40 }
41 }
42 None => false,
43 }
44}
45
46pub async fn get_or_init_android_bootreason(
48 dir: Option<fio::DirectoryProxy>,
49 android_provided_bootreason: Option<String>,
50) -> &'static Result<String, Error> {
51 ANDROID_BOOTREASON
52 .get_or_init(async || update_android_bootreason(dir, android_provided_bootreason).await)
53 .await
54}
55
56const ANDROID_BOOTREASON_BOOTLOADER_FILE: &str = "androidboot.bootreason";
64
65pub async fn get_bootloader_file_bootreason() -> Result<Option<String>, Error> {
69 let items =
70 connect_to_protocol::<fboot::ItemsMarker>().context("Failed to connect to boot items")?;
71
72 let Some(vmo) = items
73 .get_bootloader_file(ANDROID_BOOTREASON_BOOTLOADER_FILE)
74 .await
75 .context("FIDL: Failed to get bootloader file")?
76 else {
77 return Ok(None);
78 };
79
80 let size = vmo.get_content_size().context("Failed to get bootloader file content size")?;
82 let bytes = vmo.read_to_vec(0, size).context("Failed to read bootloader file vmo")?;
83
84 Ok(Some(String::from_utf8(bytes).context("Bootloader file is not valid UTF-8")?))
85}
86
87fn is_specific_bootloader_bootreason(reason: &str) -> bool {
88 const SPECIFIC_PREFIXES: &[&str] = &[
89 "reboot,uvlo",
90 "reboot,ocp",
91 "reboot,master_dc,reset",
92 "reboot,sys_ldo_ok",
93 "reboot,smpl_timeout",
94 "reboot,longkey",
95 ];
96 SPECIFIC_PREFIXES.iter().any(|prefix| reason.starts_with(prefix))
97}
98
99pub async fn update_android_bootreason(
102 dir: Option<fio::DirectoryProxy>,
103 android_provided_bootreason: Option<String>,
104) -> Result<String, Error> {
105 if *HAS_STARNIX_SESSION_RESTARTED.get_or_init(async || has_session_restarted(dir).await).await {
107 info!("Session restart observed, set android bootreason to kernel_panic.");
108 return Ok("kernel_panic".to_string());
109 }
110
111 if let Some(reason) = &android_provided_bootreason {
114 info!("Android bootloader provided bootreason: {reason}");
115 if is_specific_bootloader_bootreason(reason) {
116 return Ok(reason.clone());
117 }
118 }
119
120 info!("Converting LastRebootInfo to an android-friendly bootreason.");
121 let reboot_info_proxy = connect_to_protocol_sync::<LastRebootInfoProviderMarker>()?;
122 let deadline = zx::MonotonicInstant::after(LRIP_FIDL_TIMEOUT);
123 let reboot_info = reboot_info_proxy.get(deadline)?;
124
125 let bootreason = match reboot_info.reason {
126 Some(RebootReason::Unknown) => "reboot,unknown",
127 Some(RebootReason::Cold) => "reboot,cold",
128 Some(RebootReason::BriefPowerLoss) => "reboot,hard_reset",
129 Some(RebootReason::Brownout) => "reboot,undervoltage",
130 Some(RebootReason::KernelPanic) => "kernel_panic",
131 Some(RebootReason::SystemOutOfMemory) => "kernel_panic,oom",
132 Some(RebootReason::HardwareWatchdogTimeout) => "watchdog",
133 Some(RebootReason::SoftwareWatchdogTimeout) => "watchdog,sw",
134 Some(RebootReason::SuspensionFailure) => "kernel_panic",
135 Some(RebootReason::RootJobTermination) => "kernel_panic",
136 Some(RebootReason::UserRequest) => "reboot,userrequested",
137 Some(RebootReason::UserRequestDeviceStuck) => "reboot,userrequested",
138 Some(RebootReason::UserHardReset) => "reboot,longkey,s2",
139 Some(RebootReason::DeveloperRequest) => "reboot,shell",
140 Some(RebootReason::RetrySystemUpdate) => "reboot,ota",
141 Some(RebootReason::HighTemperature) => "shutdown,thermal",
142 Some(RebootReason::SessionFailure) => "kernel_panic",
143 Some(RebootReason::SysmgrFailure) => "kernel_panic",
144 Some(RebootReason::FactoryDataReset) => "reboot,factory_reset",
145 Some(RebootReason::CriticalComponentFailure) => "kernel_panic",
146 Some(RebootReason::CriticalDriverFailure) => "kernel_panic",
147 Some(RebootReason::ZbiSwap) => "reboot,normal",
148 Some(RebootReason::SystemUpdate) => "reboot,ota",
149 Some(RebootReason::NetstackMigration) => "reboot,normal",
150 Some(RebootReason::AndroidUnexpectedReason) => "reboot,normal",
151 Some(RebootReason::AndroidNoReason) => "reboot",
152 Some(RebootReason::AndroidRescueParty) => "reboot,rescueparty",
153 Some(RebootReason::AndroidCriticalProcessFailure) => "reboot,userspace_failed",
154 Some(RebootReason::BatteryDrained) => "shutdown,battery",
155 Some(RebootReason::__SourceBreaking { .. }) => "reboot,normal",
156 None => "reboot,unknown",
157 };
158 Ok(bootreason.to_string())
159}
160
161fn get_reboot_reason() -> Option<RebootReason> {
163 let reboot_info_proxy = connect_to_protocol_sync::<LastRebootInfoProviderMarker>().ok();
164 let deadline = zx::MonotonicInstant::after(LRIP_FIDL_TIMEOUT);
165 let reboot_info = reboot_info_proxy?.get(deadline);
166 match reboot_info {
167 Ok(info) => match info.reason {
168 Some(r) => Some(r),
169 None => {
170 info!("Failed to get the reboot reason.");
171 Some(RebootReason::unknown())
172 }
173 },
174 Err(e) => {
175 info!("Failed to get the reboot info: {:?}", e);
176 Some(RebootReason::unknown())
177 }
178 }
179}
180
181pub fn get_console_ramoops() -> Option<Vec<u8>> {
186 debug!("Getting console-ramoops contents");
187 if HAS_STARNIX_SESSION_RESTARTED.get().copied().unwrap_or(false) {
188 return Some(format!("Last Reboot Reason: Starnix Crash\n").as_bytes().to_vec());
189 }
190 match ANDROID_BOOTREASON.get() {
191 Some(Ok(reason)) => match reason.as_str() {
192 "kernel_panic" | "watchdog" | "watchdog,sw" => Some(
193 format!("Last Reboot Reason: {:?}\n", get_reboot_reason()?).as_bytes().to_vec(),
194 ),
195 _ => None,
196 },
197 Some(Err(e)) => {
198 info!("Failed to get android bootreason for console_ramoops: {:?}", e);
199 None
200 }
201 None => {
202 info!("Android bootreason not initialized.");
203 None
204 }
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn test_is_specific_bootloader_bootreason() {
214 for specific in [
215 "reboot,uvlo",
216 "reboot,uvlo,pmic,main",
217 "reboot,uvlo,pmic,sub",
218 "reboot,ocp,pmic,if",
219 "reboot,ocp2,pmic,sub",
220 "reboot,ocp3,pmic,if,usb",
221 "reboot,master_dc,reset",
222 "reboot,sys_ldo_ok,pmic,main",
223 "reboot,smpl_timeout,pmic,main",
224 "reboot,longkey,s2",
225 ] {
226 assert!(
227 is_specific_bootloader_bootreason(specific),
228 "Expected {specific} to pass through"
229 );
230 }
231
232 for non_specific in ["warm", "cold", "reboot,cold", "reboot,warm", "reboot"] {
233 assert!(
234 !is_specific_bootloader_bootreason(non_specific),
235 "Did not expect {non_specific} to pass through"
236 );
237 }
238 }
239}