shutdown_shim/
lib.rs

1// Copyright 2024 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4mod reboot_reasons;
5mod shutdown_watcher;
6
7use crate::reboot_reasons::ShutdownOptionsWrapper;
8use crate::shutdown_watcher::ShutdownWatcher;
9use anyhow::{Context, format_err};
10use fidl::HandleBased;
11use fidl::endpoints::{DiscoverableProtocolMarker, ServerEnd};
12use fidl_fuchsia_hardware_power_statecontrol::{
13    AdminMexecRequest, AdminRequest, AdminRequestStream, AdminShutdownResponder,
14    RebootMethodsWatcherRegisterRequestStream, RebootReason, ShutdownAction, ShutdownOptions,
15    ShutdownReason, ShutdownWatcherRegisterRequestStream,
16};
17use fidl_fuchsia_power::CollaborativeRebootInitiatorRequestStream;
18use fidl_fuchsia_power_internal::{
19    CollaborativeRebootReason, CollaborativeRebootSchedulerRequestStream,
20};
21use fidl_fuchsia_sys2::SystemControllerMarker;
22use fidl_fuchsia_system_state::{
23    SystemPowerState, SystemStateTransitionRequest, SystemStateTransitionRequestStream,
24};
25use fuchsia_component::client;
26use fuchsia_component::directory::{AsRefDirectory, Directory};
27use fuchsia_component::server::ServiceFs;
28use fuchsia_sync::Mutex;
29use futures::channel::mpsc;
30use futures::lock::Mutex as AMutex;
31use futures::prelude::*;
32use futures::select;
33use shutdown_shim_config::Config;
34use std::pin::pin;
35use std::sync::{Arc, LazyLock};
36use std::time::Duration;
37use {fidl_fuchsia_io as fio, fidl_fuchsia_power_system as fsystem, fuchsia_async as fasync};
38
39mod collaborative_reboot;
40
41// The amount of time that the shim will spend waiting for a manually trigger
42// system shutdown to finish before forcefully restarting the system.
43const MANUAL_SYSTEM_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(60 * 60);
44
45enum IncomingRequest {
46    SystemStateTransition(SystemStateTransitionRequestStream),
47    Admin(AdminRequestStream),
48    CollaborativeRebootInitiator(CollaborativeRebootInitiatorRequestStream),
49    CollaborativeRebootScheduler(CollaborativeRebootSchedulerRequestStream),
50    RebootMethodsWatcherRegister(RebootMethodsWatcherRegisterRequestStream),
51    ShutdownWatcherRegister(ShutdownWatcherRegisterRequestStream),
52}
53
54pub async fn main(
55    svc: impl Directory + AsRefDirectory + 'static,
56    directory_request: ServerEnd<fio::DirectoryMarker>,
57    config_vmo: Option<zx::Vmo>,
58) -> Result<(), anyhow::Error> {
59    // Check the config
60    let config = Config::from_vmo(&config_vmo.expect("Config VMO handle must be present."))?;
61    println!("[shutdown-shim]: started with config: {:?}", config);
62
63    // Initialize the inspect framework.
64    //
65    // Note that shutdown-shim is a builtin component of ComponentManager, which
66    // means we must setup the inspector in a non-conventional way:
67    // * We must initialize the connection to the inspect sink relative to the
68    // `svc` directory.
69    // * We must not use the global `fuchsia_inspect::component::inspector()`;
70    //   this instance instance is a singleton and would be shared with
71    //   ComponentManager. Instead we declare our own local inspector.
72    let inspector = fuchsia_inspect::Inspector::new(fuchsia_inspect::InspectorConfig::default());
73    let (client, server) =
74        fidl::endpoints::create_endpoints::<fidl_fuchsia_inspect::InspectSinkMarker>();
75    // Note: The inspect server is detached, so we need not poll it.
76    let _inspect_server_task = inspect_runtime::publish(
77        &inspector,
78        inspect_runtime::PublishOptions::default().on_inspect_sink_client(client),
79    )
80    .ok_or_else(|| format_err!("failed to initialize inspect framework"))?;
81    svc.as_ref_directory()
82        .open(
83            fidl_fuchsia_inspect::InspectSinkMarker::PROTOCOL_NAME,
84            fio::Flags::PROTOCOL_SERVICE,
85            server.into_channel().into(),
86        )
87        .context("failed to connect to InspectSink")?;
88
89    let mut service_fs = ServiceFs::new();
90    service_fs.dir("svc").add_fidl_service(IncomingRequest::Admin);
91    service_fs.dir("svc").add_fidl_service(IncomingRequest::RebootMethodsWatcherRegister);
92    service_fs.dir("svc").add_fidl_service(IncomingRequest::ShutdownWatcherRegister);
93    service_fs.dir("svc").add_fidl_service(IncomingRequest::SystemStateTransition);
94    service_fs.dir("svc").add_fidl_service(IncomingRequest::CollaborativeRebootInitiator);
95    service_fs.dir("svc").add_fidl_service(IncomingRequest::CollaborativeRebootScheduler);
96    service_fs.serve_connection(directory_request).context("failed to serve outgoing namespace")?;
97
98    let (abort_tx, mut abort_rx) = mpsc::unbounded::<()>();
99    let (cr_state, cr_cancellations) = collaborative_reboot::new(&inspector);
100    let ctx = ProgramContext {
101        svc,
102        abort_tx,
103        collaborative_reboot: cr_state,
104        shutdown_pending: Arc::new(AMutex::new(false)),
105        shutdown_watcher: ShutdownWatcher::new_with_inspector(&inspector),
106        config,
107    };
108
109    let shutdown_watcher = ctx.shutdown_watcher.clone();
110    let mut service_fut = service_fs
111        .for_each_concurrent(None, |request: IncomingRequest| async {
112            match request {
113                IncomingRequest::Admin(stream) => ctx.handle_admin_request(stream).await,
114                IncomingRequest::RebootMethodsWatcherRegister(stream) => {
115                    shutdown_watcher.clone().handle_reboot_register_request(stream).await;
116                }
117                IncomingRequest::ShutdownWatcherRegister(stream) => {
118                    shutdown_watcher.clone().handle_shutdown_register_request(stream).await
119                }
120                IncomingRequest::SystemStateTransition(stream) => {
121                    ctx.handle_system_state_transition(stream).await
122                }
123                IncomingRequest::CollaborativeRebootInitiator(stream) => {
124                    ctx.collaborative_reboot.handle_initiator_requests(stream, &ctx).await
125                }
126                IncomingRequest::CollaborativeRebootScheduler(stream) => {
127                    ctx.collaborative_reboot.handle_scheduler_requests(stream).await
128                }
129            }
130        })
131        .fuse();
132    let collaborative_reboot_cancellation_fut = pin!(cr_cancellations.run());
133    let mut collaborative_reboot_cancellation_fut = collaborative_reboot_cancellation_fut.fuse();
134    let mut abort_fut = abort_rx.next().fuse();
135
136    select! {
137        () = service_fut => {},
138        () = collaborative_reboot_cancellation_fut => unreachable!(),
139        _ = abort_fut => {},
140    };
141
142    Err(format_err!("exited unexpectedly"))
143}
144
145struct ProgramContext<D: Directory + AsRefDirectory> {
146    svc: D,
147    abort_tx: mpsc::UnboundedSender<()>,
148    collaborative_reboot: collaborative_reboot::State,
149
150    /// Tracks the current shutdown request state. Used to ignore shutdown requests while a current
151    /// request is being processed.
152    shutdown_pending: Arc<AMutex<bool>>,
153
154    shutdown_watcher: Arc<ShutdownWatcher>,
155    config: Config,
156}
157
158impl<D: Directory + AsRefDirectory> ProgramContext<D> {
159    async fn handle_admin_request(&self, mut stream: AdminRequestStream) {
160        while let Ok(Some(request)) = stream.try_next().await {
161            match request {
162                AdminRequest::PowerFullyOn { responder, .. } => {
163                    let _ = responder.send(Err(zx::Status::NOT_SUPPORTED.into_raw()));
164                }
165                AdminRequest::Shutdown { options, responder } => {
166                    self.shutdown_request(options, responder).await;
167                }
168                // TODO(https://fxbug.dev/385742868): Delete this method once
169                // it's removed from the API.
170                AdminRequest::Reboot { reason, responder } => {
171                    let _reboot_control_lease = self.acquire_shutdown_control_lease().await;
172                    let target_state = if reason == RebootReason::OutOfMemory {
173                        SystemPowerState::RebootKernelInitiated
174                    } else {
175                        SystemPowerState::Reboot
176                    };
177                    set_system_power_state(target_state);
178                    let res = self
179                        .forward_command(
180                            target_state,
181                            Some(ShutdownOptionsWrapper::from_reboot_reason_deprecated(&reason)),
182                            None,
183                        )
184                        .await;
185                    let _ = responder.send(res.map_err(|s| s.into_raw()));
186                }
187                AdminRequest::PerformReboot { options, responder } => {
188                    let options = match options.reasons {
189                        Some(reasons) => {
190                            Some(ShutdownOptionsWrapper::from_reboot_reason2_deprecated(&reasons))
191                        }
192                        None => None,
193                    };
194
195                    let target_state = if options.as_ref().is_some_and(|options| {
196                        options.reasons.contains(&ShutdownReason::OutOfMemory)
197                    }) {
198                        SystemPowerState::RebootKernelInitiated
199                    } else {
200                        SystemPowerState::Reboot
201                    };
202
203                    let res = self.shutdown(target_state, options).await;
204                    let _ = responder.send(res.map_err(|s| s.into_raw()));
205                }
206                AdminRequest::RebootToBootloader { responder } => {
207                    let res = self.shutdown(SystemPowerState::RebootBootloader, None).await;
208                    let _ = responder.send(res.map_err(|s| s.into_raw()));
209                }
210                AdminRequest::RebootToRecovery { responder } => {
211                    let res = self.shutdown(SystemPowerState::RebootRecovery, None).await;
212                    let _ = responder.send(res.map_err(|s| s.into_raw()));
213                }
214                AdminRequest::Poweroff { responder } => {
215                    let res = self.shutdown(SystemPowerState::Poweroff, None).await;
216                    let _ = responder.send(res.map_err(|s| s.into_raw()));
217                }
218                AdminRequest::SuspendToRam { responder } => {
219                    let target_state = SystemPowerState::SuspendRam;
220                    set_system_power_state(target_state);
221                    let res = self.forward_command(target_state, None, None).await;
222                    let _ = responder.send(res.map_err(|s| s.into_raw()));
223                }
224                AdminRequest::Mexec { responder, kernel_zbi, data_zbi } => {
225                    let _reboot_control_lease = self.acquire_shutdown_control_lease().await;
226                    let res = async move {
227                        let target_state = SystemPowerState::Mexec;
228                        {
229                            // Duplicate the VMOs now, as forwarding the mexec request to power-manager
230                            // will consume them.
231                            let kernel_zbi =
232                                kernel_zbi.duplicate_handle(zx::Rights::SAME_RIGHTS)?;
233                            let data_zbi = data_zbi.duplicate_handle(zx::Rights::SAME_RIGHTS)?;
234
235                            set_system_power_state(SystemPowerState::Mexec);
236                            set_mexec_kernel_zbi(kernel_zbi);
237                            set_mexec_data_zbi(data_zbi);
238                        }
239
240                        self.forward_command(
241                            target_state,
242                            None,
243                            Some(AdminMexecRequest { kernel_zbi, data_zbi }),
244                        )
245                        .await
246                    }
247                    .await;
248                    let _ = responder.send(res.map_err(|s| s.into_raw()));
249                }
250            }
251        }
252    }
253
254    async fn shutdown_request(&self, options: ShutdownOptions, responder: AdminShutdownResponder) {
255        let Some(action) = options.action else {
256            println!("[shutdown-shim]: error, shutdown action must be specified");
257            let _ = responder.send(Err(zx::Status::INVALID_ARGS.into_raw()));
258            return;
259        };
260
261        let options = match options.reasons {
262            Some(reasons) => ShutdownOptionsWrapper { action, reasons },
263            None => ShutdownOptionsWrapper { action, reasons: vec![] },
264        };
265
266        match action {
267            ShutdownAction::Reboot => {
268                let target_state = if options.reasons.contains(&ShutdownReason::OutOfMemory) {
269                    SystemPowerState::RebootKernelInitiated
270                } else {
271                    SystemPowerState::Reboot
272                };
273
274                let res = self.shutdown(target_state, Some(options)).await;
275                let _ = responder.send(res.map_err(|s| s.into_raw()));
276            }
277            ShutdownAction::RebootToBootloader => {
278                let res = self.shutdown(SystemPowerState::RebootBootloader, Some(options)).await;
279                let _ = responder.send(res.map_err(|s| s.into_raw()));
280            }
281            ShutdownAction::RebootToRecovery => {
282                let res = self.shutdown(SystemPowerState::RebootRecovery, Some(options)).await;
283                let _ = responder.send(res.map_err(|s| s.into_raw()));
284            }
285            ShutdownAction::Poweroff => {
286                let res = self.shutdown(SystemPowerState::Poweroff, Some(options)).await;
287                let _ = responder.send(res.map_err(|s| s.into_raw()));
288            }
289            ShutdownAction::__SourceBreaking { unknown_ordinal } => {
290                println!(
291                    "[shutdown-shim]: error, unrecognized shutdown action ordinal '{unknown_ordinal}'"
292                );
293                let _ = responder.send(Err(zx::Status::INVALID_ARGS.into_raw()));
294            }
295        }
296    }
297
298    async fn handle_system_state_transition(&self, mut stream: SystemStateTransitionRequestStream) {
299        while let Ok(Some(request)) = stream.try_next().await {
300            match request {
301                SystemStateTransitionRequest::GetTerminationSystemState { responder } => {
302                    let state = (*SYSTEM_STATE).lock();
303                    let _ = responder.send(state.power_state);
304                }
305                SystemStateTransitionRequest::GetMexecZbis { responder } => {
306                    let mut state = (*SYSTEM_STATE).lock();
307                    if state.power_state != SystemPowerState::Mexec {
308                        let _ = responder.send(Err(zx::Status::BAD_STATE.into_raw()));
309                        continue;
310                    }
311                    let kernel_zbi = std::mem::replace(
312                        &mut state.mexec_kernel_zbi,
313                        zx::NullableHandle::invalid().into(),
314                    );
315                    let data_zbi = std::mem::replace(
316                        &mut state.mexec_data_zbi,
317                        zx::NullableHandle::invalid().into(),
318                    );
319                    let _ = responder.send(Ok((kernel_zbi, data_zbi)));
320                }
321            }
322        }
323    }
324
325    // TODO(https://fxbug.dev/414413282): make `options` non-optional once deprecated shutdown
326    // methods are removed.
327    async fn shutdown(
328        &self,
329        target_state: SystemPowerState,
330        options: Option<ShutdownOptionsWrapper>,
331    ) -> Result<(), zx::Status> {
332        println!("[shutdown-shim] shutdown with options [{:?}]", options);
333        let _reboot_control_lease = self.acquire_shutdown_control_lease().await;
334        set_system_power_state(target_state);
335        self.forward_command(target_state, options, None).await
336    }
337
338    async fn forward_command(
339        &self,
340        fallback_state: SystemPowerState,
341        options: Option<ShutdownOptionsWrapper>,
342        _mexec_request: Option<AdminMexecRequest>,
343    ) -> Result<(), zx::Status> {
344        println!("[shutdown-shim] entering {:?} state", fallback_state);
345        // Return if shutdown is already pending
346        {
347            let mut shutdown_pending = self.shutdown_pending.lock().await;
348            if *shutdown_pending {
349                return Err(zx::Status::ALREADY_EXISTS);
350            }
351            *shutdown_pending = true;
352        }
353
354        if let Some(options) = options
355            && !options.reasons.is_empty()
356        {
357            self.shutdown_watcher.handle_system_shutdown_message(options).await;
358        }
359
360        self.drive_shutdown_manually().await;
361
362        // We should block on fuchsia.sys.SystemController forever on this task, if
363        // it returns something has gone wrong.
364        eprintln!("[shutdown-shim]: we shouldn't still be running, crashing the system");
365        Self::abort(self.abort_tx.clone()).await
366    }
367
368    async fn drive_shutdown_manually(&self) {
369        let abort_tx = self.abort_tx.clone();
370        fasync::Task::spawn(async {
371            fasync::Timer::new(MANUAL_SYSTEM_SHUTDOWN_TIMEOUT).await;
372            // We shouldn't still be running at this point
373            Self::abort(abort_tx).await;
374        })
375        .detach();
376
377        if let Err(e) = self.initiate_component_shutdown().await {
378            eprintln!(
379                "[shutdown-shim]: error initiating component shutdown, system shutdown impossible: {e}"
380            );
381            // Recovery from this state is impossible. Exit with a non-zero exit code,
382            // so our critical marking causes the system to forcefully restart.
383            Self::abort(self.abort_tx.clone()).await;
384        }
385    }
386
387    async fn initiate_component_shutdown(&self) -> Result<(), anyhow::Error> {
388        println!("[shutdown-shim] shutting down components");
389        let system_controller_client = self
390            .connect_to_protocol::<SystemControllerMarker>()
391            .context("error connecting to component_manager")?;
392
393        system_controller_client.shutdown().await.context("failed to initiate shutdown")
394    }
395
396    async fn acquire_shutdown_control_lease(&self) -> Option<zx::EventPair> {
397        if !self.config.suspend_enabled {
398            return None;
399        }
400        let res = async {
401            let activity_governor = self
402                .connect_to_protocol::<fsystem::ActivityGovernorMarker>()
403                .context("error connecting to system_activity_governor")?;
404            activity_governor
405                .take_wake_lease("shutdown_control")
406                .await
407                .context("failed to take wake lease")
408        }
409        .await;
410        res.map_err(|e| {
411            eprintln!("[shutdown-shim]: {e}");
412            ()
413        })
414        .ok()
415    }
416
417    /// Cause the program to terminate.
418    async fn abort(mut abort_tx: mpsc::UnboundedSender<()>) -> ! {
419        let _ = abort_tx.send(()).await;
420        std::future::pending::<()>().await;
421        unreachable!();
422    }
423
424    fn connect_to_protocol<P: DiscoverableProtocolMarker>(
425        &self,
426    ) -> Result<P::Proxy, anyhow::Error> {
427        client::connect_to_protocol_at_dir_root::<P>(&self.svc)
428    }
429}
430
431impl<D: Directory + AsRefDirectory> collaborative_reboot::RebootActuator for ProgramContext<D> {
432    async fn perform_reboot(
433        &self,
434        reasons: Vec<CollaborativeRebootReason>,
435    ) -> Result<(), zx::Status> {
436        // Transform the reasons, and dispatch the request along the standard
437        // reboot pipeline.
438        let reasons = reasons
439            .into_iter()
440            .map(|reason| match reason {
441                CollaborativeRebootReason::NetstackMigration => ShutdownReason::NetstackMigration,
442                CollaborativeRebootReason::SystemUpdate => ShutdownReason::SystemUpdate,
443            })
444            .collect();
445        self.shutdown(
446            SystemPowerState::Reboot,
447            Some(ShutdownOptionsWrapper { action: ShutdownAction::Reboot, reasons }),
448        )
449        .await
450    }
451}
452
453struct SystemState {
454    power_state: SystemPowerState,
455    mexec_kernel_zbi: zx::Vmo,
456    mexec_data_zbi: zx::Vmo,
457}
458
459impl SystemState {
460    fn new() -> Self {
461        Self {
462            power_state: SystemPowerState::FullyOn,
463            mexec_kernel_zbi: zx::NullableHandle::invalid().into(),
464            mexec_data_zbi: zx::NullableHandle::invalid().into(),
465        }
466    }
467}
468
469static SYSTEM_STATE: LazyLock<Mutex<SystemState>> =
470    LazyLock::new(|| Mutex::new(SystemState::new()));
471
472fn set_system_power_state(new: SystemPowerState) {
473    let mut s = (*SYSTEM_STATE).lock();
474    s.power_state = new;
475}
476
477fn set_mexec_kernel_zbi(new: zx::Vmo) {
478    let mut s = (*SYSTEM_STATE).lock();
479    s.mexec_kernel_zbi = new;
480}
481
482fn set_mexec_data_zbi(new: zx::Vmo) {
483    let mut s = (*SYSTEM_STATE).lock();
484    s.mexec_data_zbi = new;
485}