Skip to main content

fdf_power/
lib.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
5use core::future::Future;
6use fdf_component::{Driver, DriverContext};
7use fidl_fuchsia_hardware_power as fhw_power;
8use fidl_fuchsia_io as fio;
9use fidl_fuchsia_power_broker as fpower_broker;
10use fidl_fuchsia_power_system as fpower;
11use fuchsia_async as fasync;
12use fuchsia_component::client::{Connect, SVC_DIR};
13use fuchsia_component::directory::Directory;
14use futures::TryStreamExt;
15use log::{error, warn};
16use std::sync::{Arc, Weak};
17use zx::Status;
18
19use fidl_next as _;
20
21/// Implement this trait if you'd like to get notifications when the system is about to go into
22/// suspend and come out of resume.
23pub trait SuspendableDriver: Driver {
24    /// Called prior the system entering suspend. The system is not guaranteed to enter suspend,
25    /// but `resume` will be called regardless before a subsequent suspension attempt occurs.
26    fn suspend(&self) -> impl Future<Output = ()> + Send;
27
28    /// Called after `suspend` to indicate the system is no longer in suspend. The system may not
29    /// have actually entered suspension in between `suspend` and `resume` invocations. NOTE: there
30    /// is no initial call to `resume`; drivers are expected to start in a non-suspended state and
31    /// `resume` will only be called after `suspend`.
32    fn resume(&self) -> impl Future<Output = ()> + Send;
33
34    /// Returns whether or not suspend is enabled. If false is returned, suspend and resume methods
35    /// will never be called.
36    fn suspend_enabled(&self) -> bool;
37}
38
39/// Wrapper trait to indicate the driver supports power operations.
40pub struct Suspendable<T: Driver> {
41    #[expect(unused)]
42    scope: Option<fasync::Scope>,
43    driver: Arc<T>,
44}
45
46async fn run_suspend_blocker<T: SuspendableDriver>(
47    driver: Weak<T>,
48    mut service: fpower::SuspendBlockerRequestStream,
49) {
50    use fpower::SuspendBlockerRequest::*;
51    while let Some(req) = service.try_next().await.unwrap() {
52        match req {
53            BeforeSuspend { responder, .. } => {
54                if let Some(driver) = driver.upgrade() {
55                    driver.suspend().await;
56                } else {
57                    return;
58                }
59                let _ = responder.send();
60            }
61            AfterResume { responder, .. } => {
62                if let Some(driver) = driver.upgrade() {
63                    driver.resume().await;
64                } else {
65                    return;
66                }
67                let _ = responder.send();
68            }
69            // Ignore unknown requests.
70            _ => {
71                warn!("Received unknown sag listener request");
72            }
73        }
74    }
75}
76
77async fn run_element_runner<T: SuspendableDriver>(
78    driver: Weak<T>,
79    mut service: fpower_broker::ElementRunnerRequestStream,
80) {
81    let mut first_activation_occurred = false;
82    while let Some(req) = service.try_next().await.unwrap_or_default() {
83        if let fpower_broker::ElementRunnerRequest::SetLevel { level, responder } = req {
84            let Some(driver) = driver.upgrade() else { return };
85            if level != fhw_power::FrameworkElementLevels::Off.into_primitive() as u8 {
86                // Hide the initial resume because drivers should start in a resumed state and it is
87                // easier for users if we guarantee that a call to resume always follows a call to
88                // suspend.
89                if first_activation_occurred {
90                    driver.resume().await;
91                }
92            } else {
93                driver.suspend().await;
94            }
95            let _ = responder.send();
96            first_activation_occurred = true;
97        }
98    }
99}
100
101impl<T: SuspendableDriver + Send + Sync> Driver for Suspendable<T> {
102    const NAME: &str = T::NAME;
103
104    async fn start(mut context: DriverContext) -> Result<Self, Status> {
105        let mut runner = context
106            .start_args
107            .power_element_args
108            .as_mut()
109            .and_then(|args| args.runner_server.take());
110
111        let (svc, svc_server) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
112        context
113            .incoming
114            .open(SVC_DIR, fio::Flags::PROTOCOL_DIRECTORY, svc_server.into_channel())
115            .map_err(|error| {
116            error!(error:?; "Error opening svc directory");
117            Status::INTERNAL
118        })?;
119
120        let driver = Arc::new(T::start(context).await?);
121
122        let scope = if driver.suspend_enabled() {
123            let scope = fasync::Scope::new_with_name("suspend");
124            if let Some(runner) = runner.take() {
125                let weak_driver = Arc::downgrade(&driver);
126                scope.spawn(
127                    async move { run_element_runner(weak_driver, runner.into_stream()).await },
128                );
129            } else {
130                let sag =
131                    fpower::ActivityGovernorProxy::connect_at_dir_root(&svc).map_err(|error| {
132                        error!(error:?; "Error connecting to sag");
133                        Status::INTERNAL
134                    })?;
135
136                let (client, server) = fidl::endpoints::create_endpoints();
137
138                let _ = sag
139                    .register_suspend_blocker(
140                        fpower::ActivityGovernorRegisterSuspendBlockerRequest {
141                            suspend_blocker: Some(client),
142                            name: Some(Self::NAME.into()),
143                            ..Default::default()
144                        },
145                    )
146                    .await
147                    .map_err(|error| {
148                        error!(error:?; "Error connecting to sag");
149                        Status::INTERNAL
150                    })?
151                    .map_err(|error| {
152                        error!(error:?; "Error connecting to sag");
153                        Status::INTERNAL
154                    })?;
155
156                let weak_driver = Arc::downgrade(&driver);
157                scope.spawn(
158                    async move { run_suspend_blocker(weak_driver, server.into_stream()).await },
159                );
160            }
161            Some(scope)
162        } else {
163            None
164        };
165
166        Ok(Self { driver, scope })
167    }
168
169    async fn stop(&self) {
170        self.driver.stop().await;
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use fdf_component::testing::harness::TestHarness;
178    use fidl_fuchsia_driver_framework as fdf;
179    use std::sync::atomic::{AtomicBool, Ordering};
180
181    struct TestDriver {
182        suspend_called: Arc<AtomicBool>,
183        resume_called: Arc<AtomicBool>,
184        suspend_enabled: bool,
185        stop_called: Arc<AtomicBool>,
186    }
187
188    impl Driver for TestDriver {
189        const NAME: &str = "test_driver";
190
191        async fn start(_context: DriverContext) -> Result<Self, Status> {
192            Ok(Self {
193                suspend_called: Arc::new(AtomicBool::new(false)),
194                resume_called: Arc::new(AtomicBool::new(false)),
195                suspend_enabled: true,
196                stop_called: Arc::new(AtomicBool::new(false)),
197            })
198        }
199
200        async fn stop(&self) {
201            self.stop_called.store(true, Ordering::SeqCst);
202        }
203    }
204
205    impl SuspendableDriver for TestDriver {
206        async fn suspend(&self) {
207            self.suspend_called.store(true, Ordering::SeqCst);
208        }
209
210        async fn resume(&self) {
211            self.resume_called.store(true, Ordering::SeqCst);
212        }
213
214        fn suspend_enabled(&self) -> bool {
215            self.suspend_enabled
216        }
217    }
218
219    #[fuchsia::test]
220    async fn test_suspend_resume_with_runner() {
221        let (runner_client, runner_server) =
222            fidl::endpoints::create_endpoints::<fpower_broker::ElementRunnerMarker>();
223
224        let mut harness = TestHarness::<Suspendable<TestDriver>>::new().set_power_element_args(
225            fdf::PowerElementArgs { runner_server: Some(runner_server), ..Default::default() },
226        );
227
228        let driver_under_test = harness.start_driver().await.expect("Failed to start driver");
229        let (test_driver_stop_called, test_driver_resume_called, test_driver_suspend_called) = {
230            let suspendable = driver_under_test.get_driver().expect("Failed to get driver");
231            (
232                suspendable.driver.stop_called.clone(),
233                suspendable.driver.resume_called.clone(),
234                suspendable.driver.suspend_called.clone(),
235            )
236        };
237
238        let runner_proxy = runner_client.into_proxy();
239
240        // Level 1 should NOT trigger resume as it's the initial activation
241        runner_proxy.set_level(1).await.expect("Failed to set level");
242        assert!(!test_driver_resume_called.load(Ordering::SeqCst));
243
244        // Level 0 should trigger suspend
245        runner_proxy.set_level(0).await.expect("Failed to set level");
246        assert!(test_driver_suspend_called.load(Ordering::SeqCst));
247        test_driver_suspend_called.store(false, Ordering::SeqCst);
248
249        // Level 1 again should trigger resume
250        runner_proxy.set_level(1).await.expect("Failed to set level");
251        assert!(test_driver_resume_called.load(Ordering::SeqCst));
252
253        driver_under_test.stop_driver().await;
254        assert!(test_driver_stop_called.load(Ordering::SeqCst));
255    }
256}