Skip to main content

shutdown_shim/
collaborative_reboot.rs

1// Copyright 2025 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.
4
5//! An implementation of collaborative reboot for Fuchsia.
6//!
7//! Collaborative reboot is a mechanism that allows multiple actors to work
8//! together to schedule a device reboot at a time that avoids user disruption.
9//!
10//! Scheduler actors express their desire to reboot the device by issuing
11//! `ScheduleReboot` requests. While the Initiator actor expresses when it is an
12//! appropriate time to reboot the device by issuing a `PerformPendingReboot`
13//! request.
14
15use fidl_fuchsia_power::{
16    CollaborativeRebootInitiatorPerformPendingRebootResponse as PerformPendingRebootResponse,
17    CollaborativeRebootInitiatorRequest as InitiatorRequest,
18    CollaborativeRebootInitiatorRequestStream as InitiatorRequestStream,
19};
20use fidl_fuchsia_power_internal::{
21    CollaborativeRebootReason as Reason, CollaborativeRebootSchedulerRequest as SchedulerRequest,
22    CollaborativeRebootSchedulerRequestStream as SchedulerRequestStream,
23};
24use fuchsia_async::OnSignals;
25use fuchsia_inspect::NumericProperty;
26use fuchsia_sync::Mutex;
27use futures::{StreamExt, TryStreamExt};
28use std::sync::Arc;
29
30/// The signals that can be used to cancel a scheduled collaborative reboot.
31const CANCELLATION_SIGNALS: zx::Signals =
32    zx::Signals::USER_ALL.union(zx::Signals::OBJECT_PEER_CLOSED);
33
34/// Construct a new [`State`] and it's paired [`Cancellations`].
35pub(super) fn new(inspector: &fuchsia_inspect::Inspector) -> (State, Cancellations) {
36    let (cancellation_sender, cancellation_receiver) = futures::channel::mpsc::unbounded();
37    let cr_node = inspector.root().create_child("CollaborativeReboot");
38    let scheduled_requests_node = cr_node.create_child("ScheduledRequests");
39
40    let scheduled_requests: Arc<Mutex<ScheduledRequests>> =
41        Arc::new(Mutex::new(ScheduledRequests::new(&scheduled_requests_node)));
42    (
43        State {
44            scheduled_requests: scheduled_requests.clone(),
45            cancellation_sender,
46            _inspect_nodes: [cr_node, scheduled_requests_node],
47        },
48        Cancellations { scheduled_requests, cancellation_receiver },
49    )
50}
51
52#[derive(Debug)]
53/// Collaborative Reboot State.
54pub(super) struct State {
55    /// The current outstanding requests for collaborative reboot.
56    ///
57    /// Also held by [`Cancellations`].
58    scheduled_requests: Arc<Mutex<ScheduledRequests>>,
59
60    /// The sender of cancellation signals.
61    ///
62    /// The receiver half is held by [`Cancellations`].
63    cancellation_sender: futures::channel::mpsc::UnboundedSender<Cancel>,
64
65    /// A container to hold the Inspect nodes for collaborative reboot.
66    ///
67    /// These nodes need to retained, as dropping them would delete the
68    /// underlying Inspect data.
69    _inspect_nodes: [fuchsia_inspect::types::Node; 2],
70}
71
72impl State {
73    /// Handle requests from a [`CollaborativeRebootScheduler`].
74    pub(super) async fn handle_scheduler_requests(&self, mut stream: SchedulerRequestStream) {
75        while let Ok(Some(request)) = stream.try_next().await {
76            match request {
77                SchedulerRequest::ScheduleReboot { reason, cancel, responder } => {
78                    {
79                        let mut scheduled_requests = self.scheduled_requests.lock();
80                        scheduled_requests.schedule(reason);
81                        println!(
82                            "[shutdown-shim] Collaborative reboot scheduled for reason: \
83                            {reason:?}. Current scheduled requests: {scheduled_requests}"
84                        );
85                    }
86                    if let Some(cancel) = cancel {
87                        self.cancellation_sender
88                            .unbounded_send(Cancel { signal: cancel, reason })
89                            .expect("receiver should not close");
90                    }
91                    match responder.send() {
92                        Ok(()) => {}
93                        Err(e) => {
94                            eprintln!(
95                                "[shutdown-shim] Failed to respond to 'ScheduleReboot': {e:?}"
96                            );
97                            // Returning closes the connection.
98                            return;
99                        }
100                    }
101                }
102            }
103        }
104    }
105
106    /// Handle requests from a [`CollaborativeRebootInitiator`].
107    pub(super) async fn handle_initiator_requests<R: RebootActuator>(
108        &self,
109        mut stream: InitiatorRequestStream,
110        actuator: &R,
111    ) {
112        while let Ok(Some(request)) = stream.try_next().await {
113            match request {
114                InitiatorRequest::PerformPendingReboot { responder } => {
115                    let reboot_reasons = {
116                        let scheduled_requests = self.scheduled_requests.lock();
117                        println!(
118                            "[shutdown-shim] Asked to perform collaborative reboot. \
119                            Current scheduled requests: {scheduled_requests}"
120                        );
121                        scheduled_requests.list_reasons()
122                    };
123
124                    let rebooting = !reboot_reasons.is_empty();
125
126                    if rebooting {
127                        println!("[shutdown-shim] Performing collaborative reboot ...");
128                        match actuator.perform_reboot(reboot_reasons).await {
129                            Ok(()) | Err(zx::Status::ALREADY_EXISTS) => {}
130                            Err(status) => {
131                                eprintln!("[shutdown-shim] Failed to perform reboot: {status:?}");
132                                // Returning closes the connection.
133                                return;
134                            }
135                        }
136                    }
137
138                    match responder.send(&PerformPendingRebootResponse {
139                        rebooting: Some(rebooting),
140                        __source_breaking: fidl::marker::SourceBreaking,
141                    }) {
142                        Ok(()) => {}
143                        Err(e) => {
144                            eprintln!(
145                                "[shutdown-shim] Failed to respond to 'PerformPendingReboot': {e:?}"
146                            );
147                            // Returning closes the connection.
148                            return;
149                        }
150                    }
151                }
152            }
153        }
154    }
155}
156
157/// Holds cancellation signals for collaborative reboot.
158pub(super) struct Cancellations {
159    /// The current outstanding requests for collaborative reboot.
160    ///
161    /// Also held by [`State`].
162    scheduled_requests: Arc<Mutex<ScheduledRequests>>,
163
164    /// The receiver of cancellation signals.
165    ///
166    /// The sender half is held by [`State`].
167    cancellation_receiver: futures::channel::mpsc::UnboundedReceiver<Cancel>,
168}
169
170impl Cancellations {
171    /// Drives the cancellation of scheduled requests.
172    ///
173    /// The returned future will only exit if the corresponding [`State`] is
174    /// dropped.
175    pub(super) async fn run(self) -> () {
176        let Self { scheduled_requests, cancellation_receiver } = self;
177        cancellation_receiver
178            .for_each_concurrent(None, |Cancel { reason, signal }| {
179                let scheduled_requests = scheduled_requests.clone();
180                async move {
181                    // Note: Panic if we observe an error, as all `Err` statuses
182                    // represent programming errors. See the documentation for
183                    // `zx_object_wait_one`:
184                    // https://fuchsia.dev/reference/syscalls/object_wait_one.
185                    let _signals = OnSignals::new(signal, CANCELLATION_SIGNALS)
186                        .await
187                        .expect("failed to wait for signals on eventpair");
188                    {
189                        let mut scheduled_requests = scheduled_requests.lock();
190                        scheduled_requests.cancel(reason);
191                        println!(
192                            "[shutdown-shim] Collaborative reboot canceled for reason: {reason:?}. \
193                            Current scheduled requests: {scheduled_requests}"
194                        );
195                    }
196                }
197            })
198            .await
199    }
200}
201
202// A tuple of the cancellation signal, and the reason being canceled.
203struct Cancel {
204    reason: Reason,
205    signal: zx::EventPair,
206}
207
208/// A reference count of the scheduled requests, broken down by reason.
209#[derive(Debug)]
210struct ScheduledRequests {
211    system_update: usize,
212    // Inspect properties to report the counters from above.
213    system_update_inspect_prop: fuchsia_inspect::types::UintProperty,
214}
215
216impl ScheduledRequests {
217    fn new(inspect_node: &fuchsia_inspect::types::Node) -> Self {
218        Self {
219            system_update: 0,
220            system_update_inspect_prop: inspect_node.create_uint("SystemUpdate", 0),
221        }
222    }
223
224    fn schedule(&mut self, reason: Reason) {
225        let (rc, inspect_prop) = match reason {
226            Reason::SystemUpdate => (&mut self.system_update, &self.system_update_inspect_prop),
227        };
228        *rc = rc.saturating_add(1);
229        inspect_prop.add(1);
230    }
231
232    fn cancel(&mut self, reason: Reason) {
233        let (rc, inspect_prop) = match reason {
234            Reason::SystemUpdate => (&mut self.system_update, &self.system_update_inspect_prop),
235        };
236        *rc = rc.saturating_sub(1);
237        inspect_prop.subtract(1);
238    }
239
240    fn list_reasons(&self) -> Vec<Reason> {
241        let Self { system_update, system_update_inspect_prop: _ } = self;
242        let mut reasons = Vec::new();
243        if *system_update != 0 {
244            reasons.push(Reason::SystemUpdate);
245        }
246        reasons
247    }
248}
249
250impl std::fmt::Display for ScheduledRequests {
251    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252        let Self { system_update, system_update_inspect_prop: _ } = self;
253        write!(f, "SystemUpdate:{system_update}")
254    }
255}
256
257/// A type capable of actuating reboots on behalf of collaborative reboot.
258pub(super) trait RebootActuator {
259    /// Perform the reboot with the given reasons.
260    async fn perform_reboot(&self, reasons: Vec<Reason>) -> Result<(), zx::Status>;
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    use std::cell::RefCell;
268
269    use assert_matches::assert_matches;
270    use diagnostics_assertions::assert_data_tree;
271    use fidl::Peered;
272    use fidl_fuchsia_power::CollaborativeRebootInitiatorMarker;
273    use fidl_fuchsia_power_internal::CollaborativeRebootSchedulerMarker;
274    use futures::FutureExt;
275    use futures::future::Either;
276    use futures::stream::FuturesUnordered;
277    use test_case::test_case;
278
279    #[derive(Default)]
280    struct MockRebooter {
281        /// Stores if `perform_reboot` has been called, and with what arguments.
282        ///
283        /// Wrapped in a `RefCell` to allow interior mutability. Because the
284        /// tests are single threaded, this isn't problematic.
285        reasons: RefCell<Option<Vec<Reason>>>,
286    }
287
288    impl RebootActuator for MockRebooter {
289        async fn perform_reboot(&self, reasons: Vec<Reason>) -> Result<(), zx::Status> {
290            let original_reasons = self.reasons.borrow_mut().replace(reasons);
291            // The mock only expects to have `perform_reboot` called once.
292            assert_eq!(None, original_reasons);
293            Ok(())
294        }
295    }
296
297    #[test_case(vec![] => false; "no_pending_reboot")]
298    #[test_case(vec![Reason::SystemUpdate] => true; "system_update")]
299    #[test_case(vec![Reason::SystemUpdate, Reason::SystemUpdate] => true; "same_reasons")]
300    #[fuchsia::test]
301    async fn collaborative_reboot(mut reasons: Vec<Reason>) -> bool {
302        // Note: this test doesn't exercise cancellations.
303        let inspector =
304            fuchsia_inspect::Inspector::new(fuchsia_inspect::InspectorConfig::default());
305        let (state, _cancellations) = new(&inspector);
306
307        let (scheduler_client, scheduler_request_stream) =
308            fidl::endpoints::create_request_stream::<CollaborativeRebootSchedulerMarker>();
309        let scheduler = scheduler_client.into_proxy();
310        let (initiator_client, initiator_request_stream) =
311            fidl::endpoints::create_request_stream::<CollaborativeRebootInitiatorMarker>();
312        let initiator = initiator_client.into_proxy();
313
314        // Schedule reboots for each reason (and drive the server
315        // implementation).
316        let schedule_server_fut = state.handle_scheduler_requests(scheduler_request_stream).fuse();
317        futures::pin_mut!(schedule_server_fut);
318        for reason in &reasons {
319            futures::select!(
320                result = scheduler.schedule_reboot(*reason, None).fuse() => {
321                    result.expect("failed to schedule reboot");
322                },
323                () = schedule_server_fut => {
324                    unreachable!("The `Scheduler` protocol worker shouldn't exit");
325                }
326            );
327        }
328
329        // Initiate the reboot (and drive the server implementation).
330        let mock = MockRebooter::default();
331        let PerformPendingRebootResponse { rebooting, __source_breaking } = futures::select!(
332            result = initiator.perform_pending_reboot().fuse() => {
333                result.expect("failed to initate reboot")
334            },
335            () = state.handle_initiator_requests(initiator_request_stream, &mock).fuse() => {
336                unreachable!("The `Initiator` protocol worker shouldn't exit.");
337            }
338        );
339
340        // Verify that the correct reasons were reported
341        let expected_reasons = if reasons.is_empty() {
342            None
343        } else {
344            // De-duplicate the list. If the same reason is scheduled multiple
345            // times, it's only expected to be reported once.
346            reasons.sort();
347            reasons.dedup();
348            Some(reasons)
349        };
350        assert_eq!(*mock.reasons.borrow(), expected_reasons);
351
352        rebooting.expect("rebooting should be present")
353    }
354
355    // Specifies how a request should be cancelled, if at all.
356    enum Cancel {
357        None,
358        UserSignal,
359        Drop,
360    }
361
362    #[test_case(vec![(Reason::SystemUpdate, Cancel::None)] => true; "not_canceled")]
363    #[test_case(vec![(Reason::SystemUpdate, Cancel::UserSignal)] => false; "canceled")]
364    #[test_case(vec![(Reason::SystemUpdate, Cancel::Drop)] => false; "canceled_with_drop")]
365    #[test_case(vec![
366        (Reason::SystemUpdate, Cancel::UserSignal), (Reason::SystemUpdate, Cancel::None)
367        ] => true; "same_reasons_only_one_canceled")]
368    #[test_case(vec![
369        (Reason::SystemUpdate, Cancel::UserSignal), (Reason::SystemUpdate, Cancel::UserSignal)
370        ] => false; "same_reasons_both_canceled")]
371    #[fuchsia::test]
372    fn cancellation(requests: Vec<(Reason, Cancel)>) -> bool {
373        // Note: This test requires partially driving the cancellation worker,
374        // so we need direct access to the executor.
375        let mut exec = fuchsia_async::TestExecutor::new();
376
377        let inspector =
378            fuchsia_inspect::Inspector::new(fuchsia_inspect::InspectorConfig::default());
379        let (state, cancellations_worker) = new(&inspector);
380
381        let (scheduler_client, scheduler_request_stream) =
382            fidl::endpoints::create_request_stream::<CollaborativeRebootSchedulerMarker>();
383        let scheduler = scheduler_client.into_proxy();
384        let (initiator_client, initiator_request_stream) =
385            fidl::endpoints::create_request_stream::<CollaborativeRebootInitiatorMarker>();
386        let initiator = initiator_client.into_proxy();
387
388        let mut cancellation_signals = vec![];
389        // Note: We need to hold onto the uncancelled_signals, as dropping them
390        // would result in the request being canceled.
391        let mut uncancelled_signals = vec![];
392        let mut uncancelled_reasons = vec![];
393
394        // Schedule reboots for each reason (and drive the server
395        // implementation).
396        let schedule_client_fut = FuturesUnordered::new();
397        for (reason, should_cancel) in requests {
398            let (ep1, ep2) = zx::EventPair::create();
399            schedule_client_fut.push(scheduler.schedule_reboot(reason, Some(ep2)));
400            match should_cancel {
401                Cancel::None => {
402                    uncancelled_signals.push(ep1);
403                    uncancelled_reasons.push(reason);
404                }
405                Cancel::UserSignal => cancellation_signals.push(ep1),
406                Cancel::Drop => std::mem::drop(ep1),
407            }
408        }
409        let schedule_client_fut = schedule_client_fut
410            .for_each(|result| futures::future::ready(result.expect("failed to schedule_reboot")));
411        futures::pin_mut!(schedule_client_fut);
412        let schedule_server_fut = state.handle_scheduler_requests(scheduler_request_stream);
413        futures::pin_mut!(schedule_server_fut);
414        let schedule_fut =
415            futures::future::select(schedule_client_fut, schedule_server_fut).map(|result| {
416                match result {
417                    Either::Left(((), _server_fut)) => {}
418                    Either::Right(((), _client_fut)) => {
419                        unreachable!("The `Scheduler` protocol worker shouldn't exit")
420                    }
421                }
422            });
423        futures::pin_mut!(schedule_fut);
424        exec.run_singlethreaded(&mut schedule_fut);
425
426        // Cancel some of the requests, and drive the cancellations worker
427        // until it stalls (indicating it's processed all cancellations).
428        for cancellation in cancellation_signals {
429            cancellation
430                .signal_peer(zx::Signals::NONE, zx::Signals::USER_0)
431                .expect("failed to cancel reboot");
432        }
433        let cancellation_fut = cancellations_worker.run();
434        futures::pin_mut!(cancellation_fut);
435        assert_eq!(futures::task::Poll::Pending, exec.run_until_stalled(&mut cancellation_fut));
436
437        // Initiate the reboot (and drive the server implementation).
438        let mock = MockRebooter::default();
439        let initiate_client_fut = initiator.perform_pending_reboot();
440        futures::pin_mut!(initiate_client_fut);
441        let initiate_server_fut = state.handle_initiator_requests(initiator_request_stream, &mock);
442        futures::pin_mut!(initiate_server_fut);
443        let initiate_fut =
444            futures::future::select(initiate_client_fut, initiate_server_fut).map(|result| {
445                match result {
446                    Either::Left((result, _server_fut)) => {
447                        result.expect("failed to initate reboot")
448                    }
449                    Either::Right(((), _client_fut)) => {
450                        unreachable!("The `Initiator` protocol worker shouldn't exit")
451                    }
452                }
453            });
454        futures::pin_mut!(initiate_fut);
455        let PerformPendingRebootResponse { rebooting, __source_breaking } =
456            exec.run_singlethreaded(&mut initiate_fut);
457
458        // Verify that the correct reasons were reported
459        let expected_reasons = if uncancelled_reasons.is_empty() {
460            None
461        } else {
462            // De-duplicate the list. If the same reason is scheduled multiple
463            // times, it's only expected to be reported once.
464            uncancelled_reasons.sort();
465            uncancelled_reasons.dedup();
466            Some(uncancelled_reasons)
467        };
468        assert_eq!(*mock.reasons.borrow(), expected_reasons);
469
470        rebooting.expect("rebooting should be present")
471    }
472
473    struct MockRebooterWithError {
474        /// The error status to return for each call to `perform_reboot`.
475        error: zx::Status,
476    }
477
478    impl RebootActuator for MockRebooterWithError {
479        async fn perform_reboot(&self, _reasons: Vec<Reason>) -> Result<(), zx::Status> {
480            Err(self.error)
481        }
482    }
483
484    #[test_case(zx::Status::ALREADY_EXISTS, true; "already_exists")]
485    #[test_case(zx::Status::INTERNAL, false; "internal")]
486    #[fuchsia::test]
487    async fn reboot_error(error: zx::Status, should_succeed: bool) {
488        let inspector =
489            fuchsia_inspect::Inspector::new(fuchsia_inspect::InspectorConfig::default());
490        let (state, _cancellations) = new(&inspector);
491
492        let (scheduler_client, scheduler_request_stream) =
493            fidl::endpoints::create_request_stream::<CollaborativeRebootSchedulerMarker>();
494        let scheduler = scheduler_client.into_proxy();
495        let (initiator_client, initiator_request_stream) =
496            fidl::endpoints::create_request_stream::<CollaborativeRebootInitiatorMarker>();
497        let initiator = initiator_client.into_proxy();
498
499        // Schedule a reboot with an arbitrary reason (and drive the server
500        // implementation).
501        let schedule_server_fut = state.handle_scheduler_requests(scheduler_request_stream).fuse();
502        futures::pin_mut!(schedule_server_fut);
503        futures::select!(
504            result = scheduler.schedule_reboot(Reason::SystemUpdate, None).fuse() => {
505                result.expect("failed to schedule reboot");
506            },
507            () = schedule_server_fut => {
508                unreachable!("The `Scheduler` protocol worker shouldn't exit");
509            }
510        );
511
512        // Initiate the reboot (and drive the server implementation).
513        let mock = MockRebooterWithError { error };
514        if should_succeed {
515            let result = futures::select!(
516                result = initiator.perform_pending_reboot().fuse() => result,
517                () = state.handle_initiator_requests(initiator_request_stream, &mock).fuse() => {
518                    unreachable!("The `Initiator` protocol worker shouldn't exit.");
519                }
520            );
521            assert_matches!(result, Ok(_))
522        } else {
523            let (result, ()) = futures::join!(
524                initiator.perform_pending_reboot().fuse(),
525                state.handle_initiator_requests(initiator_request_stream, &mock).fuse()
526            );
527            assert_matches!(result, Err(_))
528        }
529    }
530
531    #[fuchsia::test]
532    async fn inspect() {
533        let inspector =
534            fuchsia_inspect::Inspector::new(fuchsia_inspect::InspectorConfig::default());
535        let (state, cancellations_worker) = new(&inspector);
536
537        let mut cancellation_signals = vec![];
538        let (scheduler_client, scheduler_request_stream) =
539            fidl::endpoints::create_request_stream::<CollaborativeRebootSchedulerMarker>();
540        let scheduler = scheduler_client.into_proxy();
541        let schedule_server_fut = state.handle_scheduler_requests(scheduler_request_stream).fuse();
542        futures::pin_mut!(schedule_server_fut);
543
544        // At the start, expect the tree to be initialized with 0 values.
545        assert_data_tree!(inspector, root: {
546            "CollaborativeReboot": {
547                "ScheduledRequests": {
548                    "SystemUpdate": 0u64,
549                }
550            }
551        });
552
553        // Schedule a `SystemUpdate` reboot, twice, and verify the inspect data.
554        for count in [1u64, 2u64] {
555            let (ep1, ep2) = zx::EventPair::create();
556            cancellation_signals.push(ep1);
557            futures::select!(
558                result = scheduler.schedule_reboot(
559                    Reason::SystemUpdate, Some(ep2)).fuse() => {
560                        result.expect("failed to schedule reboot");
561                    },
562                () = schedule_server_fut => {
563                    unreachable!("The `Scheduler` protocol worker shouldn't exit");
564                }
565            );
566            assert_data_tree!(inspector, root: {
567                "CollaborativeReboot": {
568                    "ScheduledRequests": {
569                        "SystemUpdate": count,
570                    }
571                }
572            });
573        }
574
575        // Cancel the `SystemUpdate` requests, and verify the inspect data.
576        std::mem::drop(cancellation_signals);
577        state.cancellation_sender.close_channel();
578        cancellations_worker.run().await;
579        assert_data_tree!(inspector, root: {
580            "CollaborativeReboot": {
581                "ScheduledRequests": {
582                    "SystemUpdate": 0u64,
583                }
584            }
585        });
586    }
587}