1use 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
30const CANCELLATION_SIGNALS: zx::Signals =
32 zx::Signals::USER_ALL.union(zx::Signals::OBJECT_PEER_CLOSED);
33
34pub(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)]
53pub(super) struct State {
55 scheduled_requests: Arc<Mutex<ScheduledRequests>>,
59
60 cancellation_sender: futures::channel::mpsc::UnboundedSender<Cancel>,
64
65 _inspect_nodes: [fuchsia_inspect::types::Node; 2],
70}
71
72impl State {
73 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 return;
99 }
100 }
101 }
102 }
103 }
104 }
105
106 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 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 return;
149 }
150 }
151 }
152 }
153 }
154 }
155}
156
157pub(super) struct Cancellations {
159 scheduled_requests: Arc<Mutex<ScheduledRequests>>,
163
164 cancellation_receiver: futures::channel::mpsc::UnboundedReceiver<Cancel>,
168}
169
170impl Cancellations {
171 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 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
202struct Cancel {
204 reason: Reason,
205 signal: zx::EventPair,
206}
207
208#[derive(Debug)]
210struct ScheduledRequests {
211 system_update: usize,
212 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
257pub(super) trait RebootActuator {
259 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 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 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 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 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 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 let expected_reasons = if reasons.is_empty() {
342 None
343 } else {
344 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 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 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 let mut uncancelled_signals = vec![];
392 let mut uncancelled_reasons = vec![];
393
394 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 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 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 let expected_reasons = if uncancelled_reasons.is_empty() {
460 None
461 } else {
462 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 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 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 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 assert_data_tree!(inspector, root: {
546 "CollaborativeReboot": {
547 "ScheduledRequests": {
548 "SystemUpdate": 0u64,
549 }
550 }
551 });
552
553 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 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}