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, DriverError};
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 _;
20use fidl_next_fuchsia_hardware_powerdomain as fpowerdomain;
21
22/// Implement this trait if you'd like to get notifications when the system is about to go into
23/// suspend and come out of resume.
24pub trait SuspendableDriver: Driver {
25    /// Called prior the system entering suspend. The system is not guaranteed to enter suspend,
26    /// but `resume` will be called regardless before a subsequent suspension attempt occurs.
27    fn suspend(&self) -> impl Future<Output = ()> + Send;
28
29    /// Called after `suspend` to indicate the system is no longer in suspend. The system may not
30    /// have actually entered suspension in between `suspend` and `resume` invocations. NOTE: there
31    /// is no initial call to `resume`; drivers are expected to start in a non-suspended state and
32    /// `resume` will only be called after `suspend`.
33    fn resume(&self) -> impl Future<Output = ()> + Send;
34
35    /// Returns whether or not suspend is enabled. If false is returned, suspend and resume methods
36    /// will never be called.
37    fn suspend_enabled(&self) -> bool;
38}
39
40/// Wrapper trait to indicate the driver supports power operations.
41pub struct Suspendable<T: Driver> {
42    #[expect(unused)]
43    scope: Option<fasync::Scope>,
44    driver: Arc<T>,
45}
46
47impl<T: Driver> core::ops::Deref for Suspendable<T> {
48    type Target = T;
49
50    fn deref(&self) -> &Self::Target {
51        &self.driver
52    }
53}
54
55async fn run_suspend_blocker<T: SuspendableDriver>(
56    driver: Weak<T>,
57    mut service: fpower::SuspendBlockerRequestStream,
58) {
59    use fpower::SuspendBlockerRequest::*;
60    while let Some(req) = service.try_next().await.unwrap() {
61        match req {
62            BeforeSuspend { responder, .. } => {
63                if let Some(driver) = driver.upgrade() {
64                    driver.suspend().await;
65                } else {
66                    return;
67                }
68                let _ = responder.send();
69            }
70            AfterResume { responder, .. } => {
71                if let Some(driver) = driver.upgrade() {
72                    driver.resume().await;
73                } else {
74                    return;
75                }
76                let _ = responder.send();
77            }
78            // Ignore unknown requests.
79            _ => {
80                warn!("Received unknown sag listener request");
81            }
82        }
83    }
84}
85
86async fn run_element_runner<T: SuspendableDriver>(
87    driver: Weak<T>,
88    mut service: fpower_broker::ElementRunnerRequestStream,
89) {
90    let mut first_activation_occurred = false;
91    while let Some(req) = service.try_next().await.unwrap_or_default() {
92        if let fpower_broker::ElementRunnerRequest::SetLevel { level, responder } = req {
93            let Some(driver) = driver.upgrade() else { return };
94            if level != fhw_power::FrameworkElementLevels::Off.into_primitive() as u8 {
95                // Hide the initial resume because drivers should start in a resumed state and it is
96                // easier for users if we guarantee that a call to resume always follows a call to
97                // suspend.
98                if first_activation_occurred {
99                    driver.resume().await;
100                }
101            } else {
102                driver.suspend().await;
103            }
104            let _ = responder.send();
105            first_activation_occurred = true;
106        }
107    }
108}
109
110impl<T: SuspendableDriver + Send + Sync> Driver for Suspendable<T> {
111    const NAME: &str = T::NAME;
112
113    async fn start(mut context: DriverContext) -> Result<Self, DriverError> {
114        let mut runner = context
115            .start_args
116            .power_element_args
117            .as_mut()
118            .and_then(|args| args.runner_server.take());
119
120        let (svc, svc_server) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
121        context
122            .incoming
123            .open(
124                SVC_DIR,
125                fio::Flags::PROTOCOL_DIRECTORY | fio::PERM_READABLE,
126                svc_server.into_channel(),
127            )
128            .map_err(|error| {
129                error!(error:?; "Error opening svc directory");
130                Status::INTERNAL
131            })?;
132
133        let driver = Arc::new(T::start(context).await?);
134
135        let scope = if driver.suspend_enabled() {
136            let scope = fasync::Scope::new_with_name("suspend");
137            if let Some(runner) = runner.take() {
138                let runner = fidl::endpoints::ServerEnd::<fpower_broker::ElementRunnerMarker>::new(
139                    runner.into_untyped(),
140                );
141                let weak_driver = Arc::downgrade(&driver);
142                scope.spawn(
143                    async move { run_element_runner(weak_driver, runner.into_stream()).await },
144                );
145            } else {
146                let sag =
147                    fpower::ActivityGovernorProxy::connect_at_dir_root(&svc).map_err(|error| {
148                        error!(error:?; "Error connecting to sag");
149                        Status::INTERNAL
150                    })?;
151
152                let (client, server) = fidl::endpoints::create_endpoints();
153
154                let _ = sag
155                    .register_suspend_blocker(
156                        fpower::ActivityGovernorRegisterSuspendBlockerRequest {
157                            suspend_blocker: Some(client),
158                            name: Some(Self::NAME.into()),
159                            ..Default::default()
160                        },
161                    )
162                    .await
163                    .map_err(|error| {
164                        error!(error:?; "Error connecting to sag");
165                        Status::INTERNAL
166                    })?
167                    .map_err(|error| {
168                        error!(error:?; "Error connecting to sag");
169                        Status::INTERNAL
170                    })?;
171
172                let weak_driver = Arc::downgrade(&driver);
173                scope.spawn(
174                    async move { run_suspend_blocker(weak_driver, server.into_stream()).await },
175                );
176            }
177            Some(scope)
178        } else {
179            None
180        };
181
182        Ok(Self { driver, scope })
183    }
184
185    async fn stop(&self) {
186        self.driver.stop().await;
187    }
188
189    async fn health_check(
190        &self,
191        request: fdf_component::HealthCheckRequest,
192    ) -> Result<fdf_component::HealthCheckResponse, fdf_component::HealthCheckError> {
193        self.driver.health_check(request).await
194    }
195}
196
197/// Extension trait for [`DriverContext`] to simplify connecting to power resources.
198pub trait PowerExt {
199    /// Connects to a power domain resource with the given instance name.
200    fn connect_to_powerdomain(
201        &self,
202        instance: &str,
203    ) -> Result<fidl_next::Client<fpowerdomain::Domain>, DriverError>;
204}
205
206impl PowerExt for DriverContext {
207    fn connect_to_powerdomain(
208        &self,
209        instance: &str,
210    ) -> Result<fidl_next::Client<fpowerdomain::Domain>, DriverError> {
211        let service = self
212            .incoming
213            .service::<fdf_component::ServiceInstance<fpowerdomain::Service>>()
214            .instance(instance)
215            .connect_next()?;
216        let (client, server) = fidl_next::fuchsia::create_channel();
217        service.domain(server)?;
218        Ok(client.spawn())
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use fdf_component::testing::harness::TestHarness;
226    use fdf_component::{
227        HealthCheckError, HealthCheckRequest, HealthCheckResponse, HealthCondition, HealthStatus,
228    };
229    use fidl_next_fuchsia_driver_framework as fdf;
230    use std::sync::atomic::{AtomicBool, Ordering};
231
232    struct TestDriver {
233        suspend_called: Arc<AtomicBool>,
234        resume_called: Arc<AtomicBool>,
235        suspend_enabled: bool,
236        stop_called: Arc<AtomicBool>,
237        health_check_called: AtomicBool,
238    }
239
240    impl Driver for TestDriver {
241        const NAME: &str = "test_driver";
242
243        async fn start(_context: DriverContext) -> Result<Self, DriverError> {
244            Ok(Self {
245                suspend_called: Arc::new(AtomicBool::new(false)),
246                resume_called: Arc::new(AtomicBool::new(false)),
247                suspend_enabled: true,
248                stop_called: Arc::new(AtomicBool::new(false)),
249                health_check_called: AtomicBool::new(false),
250            })
251        }
252
253        async fn stop(&self) {
254            self.stop_called.store(true, Ordering::SeqCst);
255        }
256
257        async fn health_check(
258            &self,
259            request: HealthCheckRequest,
260        ) -> Result<HealthCheckResponse, HealthCheckError> {
261            self.health_check_called.store(true, Ordering::SeqCst);
262            match request {
263                HealthCheckRequest::Fast => Ok(HealthCheckResponse {
264                    conditions: Some(vec![HealthCondition {
265                        status: Some(HealthStatus::Degraded),
266                        reason_code: Some("TEST_DEGRADED".to_string()),
267                        ..Default::default()
268                    }]),
269                    ..Default::default()
270                }),
271                _ => Err(HealthCheckError::NotSupported),
272            }
273        }
274    }
275
276    impl SuspendableDriver for TestDriver {
277        async fn suspend(&self) {
278            self.suspend_called.store(true, Ordering::SeqCst);
279        }
280
281        async fn resume(&self) {
282            self.resume_called.store(true, Ordering::SeqCst);
283        }
284
285        fn suspend_enabled(&self) -> bool {
286            self.suspend_enabled
287        }
288    }
289
290    #[fuchsia::test]
291    async fn test_suspend_resume_with_runner() {
292        let (runner_client, runner_server) =
293            fidl::endpoints::create_endpoints::<fpower_broker::ElementRunnerMarker>();
294
295        let mut harness = TestHarness::<Suspendable<TestDriver>>::new().set_power_element_args(
296            fdf::PowerElementArgs {
297                runner_server: Some(fidl_next::ServerEnd::from_untyped(
298                    runner_server.into_channel(),
299                )),
300                ..Default::default()
301            },
302        );
303
304        let driver_under_test = harness.start_driver().await.expect("Failed to start driver");
305        let (test_driver_stop_called, test_driver_resume_called, test_driver_suspend_called) = {
306            let suspendable = driver_under_test.get_driver().expect("Failed to get driver");
307            (
308                suspendable.driver.stop_called.clone(),
309                suspendable.driver.resume_called.clone(),
310                suspendable.driver.suspend_called.clone(),
311            )
312        };
313
314        let runner_proxy = runner_client.into_proxy();
315
316        // Level 1 should NOT trigger resume as it's the initial activation
317        runner_proxy.set_level(1).await.expect("Failed to set level");
318        assert!(!test_driver_resume_called.load(Ordering::SeqCst));
319
320        // Level 0 should trigger suspend
321        runner_proxy.set_level(0).await.expect("Failed to set level");
322        assert!(test_driver_suspend_called.load(Ordering::SeqCst));
323        test_driver_suspend_called.store(false, Ordering::SeqCst);
324
325        // Level 1 again should trigger resume
326        runner_proxy.set_level(1).await.expect("Failed to set level");
327        assert!(test_driver_resume_called.load(Ordering::SeqCst));
328
329        driver_under_test.stop_driver().await;
330        assert!(test_driver_stop_called.load(Ordering::SeqCst));
331    }
332
333    #[fuchsia::test]
334    async fn test_health_check_forwards_to_inner_driver() {
335        let (_runner_client, runner_server) =
336            fidl::endpoints::create_endpoints::<fpower_broker::ElementRunnerMarker>();
337        let mut harness = TestHarness::<Suspendable<TestDriver>>::new().set_power_element_args(
338            fdf::PowerElementArgs {
339                runner_server: Some(fidl_next::ServerEnd::from_untyped(
340                    runner_server.into_channel(),
341                )),
342                ..Default::default()
343            },
344        );
345        let driver_under_test = harness.start_driver().await.expect("Failed to start driver");
346        let suspendable = driver_under_test.get_driver().expect("Failed to get driver");
347
348        // `Suspendable<T>` derefs to the wrapped driver.
349        assert!(!suspendable.health_check_called.load(Ordering::SeqCst));
350
351        // `Suspendable<T>`'s own `Driver::health_check` forwards to the wrapped driver rather than
352        // answering with the trait default.
353        let result = <Suspendable<TestDriver> as Driver>::health_check(
354            suspendable,
355            HealthCheckRequest::Fast,
356        )
357        .await;
358        assert!(suspendable.health_check_called.load(Ordering::SeqCst));
359        let response = result.expect("FAST is answered by the wrapped driver");
360        let conditions = response.conditions.expect("conditions");
361        assert_eq!(conditions.len(), 1);
362        assert_eq!(conditions[0].status, Some(HealthStatus::Degraded));
363        assert_eq!(conditions[0].reason_code.as_deref(), Some("TEST_DEGRADED"));
364
365        let result = <Suspendable<TestDriver> as Driver>::health_check(
366            suspendable,
367            HealthCheckRequest::Deep,
368        )
369        .await;
370        assert_eq!(result, Err(HealthCheckError::NotSupported));
371
372        driver_under_test.stop_driver().await;
373    }
374}