kernel_manager/
suspend.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 crate::Kernels;
6use anyhow::Error;
7use fidl::{HandleBased, Peered};
8use fidl_fuchsia_starnix_runner as fstarnixrunner;
9use fuchsia_sync::Mutex;
10use log::warn;
11use std::sync::Arc;
12use zx::{AsHandleRef, Task};
13
14/// The signal that the kernel raises to indicate that it's awake.
15pub const AWAKE_SIGNAL: zx::Signals = zx::Signals::USER_0;
16
17/// The signal that the kernel raises to indicate that it's suspended.
18pub const ASLEEP_SIGNAL: zx::Signals = zx::Signals::USER_1;
19
20pub struct WakeSource {
21    counter: zx::Counter,
22    name: String,
23}
24
25impl WakeSource {
26    pub fn new(counter: zx::Counter, name: String) -> Self {
27        Self { counter, name }
28    }
29
30    fn as_wait_item(&self) -> zx::WaitItem<'_> {
31        zx::WaitItem {
32            handle: self.counter.as_handle_ref(),
33            waitfor: zx::Signals::COUNTER_POSITIVE,
34            pending: zx::Signals::empty(),
35        }
36    }
37}
38
39pub type WakeSources = std::collections::HashMap<zx::Koid, WakeSource>;
40
41#[derive(Default)]
42pub struct SuspendContext {
43    pub wake_sources: Arc<Mutex<WakeSources>>,
44    pub wake_watchers: Arc<Mutex<Vec<zx::EventPair>>>,
45}
46
47/// Suspends the container specified by the `payload`.
48pub async fn suspend_container(
49    payload: fstarnixrunner::ManagerSuspendContainerRequest,
50    suspend_context: &Arc<SuspendContext>,
51    kernels: &Kernels,
52) -> Result<
53    Result<fstarnixrunner::ManagerSuspendContainerResponse, fstarnixrunner::SuspendError>,
54    Error,
55> {
56    fuchsia_trace::duration!(c"power", c"starnix-runner:suspending-container");
57    let Some(container_job) = payload.container_job else {
58        warn!(
59            "error suspending container: could not find container job {:?}",
60            payload.container_job
61        );
62        return Ok(Err(fstarnixrunner::SuspendError::SuspendFailure));
63    };
64
65    // These handles need to kept alive until the end of the block, as they will
66    // resume the kernel when dropped.
67    log::info!("Suspending all container processes.");
68    let _suspend_handles = match suspend_job(&container_job).await {
69        Ok(handles) => handles,
70        Err(e) => {
71            warn!("error suspending container {:?}", e);
72            fuchsia_trace::instant!(
73                c"power",
74                c"starnix-runner:suspend-failed-actual",
75                fuchsia_trace::Scope::Process
76            );
77            return Ok(Err(fstarnixrunner::SuspendError::SuspendFailure));
78        }
79    };
80    log::info!("Finished suspending all container processes.");
81
82    let suspend_start = zx::BootInstant::get();
83
84    if let Some(wake_locks) = payload.wake_locks {
85        match wake_locks.wait_handle(zx::Signals::EVENT_SIGNALED, zx::MonotonicInstant::ZERO) {
86            Ok(_) => {
87                // There were wake locks active after suspending all processes, resume
88                // and fail the suspend call.
89                warn!("error suspending container: Linux wake locks exist");
90                fuchsia_trace::instant!(
91                    c"power",
92                    c"starnix-runner:suspend-failed-with-wake-locks",
93                    fuchsia_trace::Scope::Process
94                );
95                return Ok(Err(fstarnixrunner::SuspendError::WakeLocksExist));
96            }
97            Err(_) => {}
98        };
99    }
100
101    {
102        log::info!("Notifying wake watchers of container suspend.");
103        let watchers = suspend_context.wake_watchers.lock();
104        for event in watchers.iter() {
105            let (clear_mask, set_mask) = (AWAKE_SIGNAL, ASLEEP_SIGNAL);
106            event.signal_peer(clear_mask, set_mask)?;
107        }
108    }
109    kernels.drop_wake_lease(&container_job)?;
110
111    let wake_sources = suspend_context.wake_sources.lock();
112    let mut wait_items: Vec<zx::WaitItem<'_>> =
113        wake_sources.iter().map(|(_, w)| w.as_wait_item()).collect();
114
115    // TODO: We will likely have to handle a larger number of wake sources in the
116    // future, at which point we may want to consider a Port-based approach. This
117    // would also allow us to unblock this thread.
118    {
119        fuchsia_trace::duration!(c"power", c"starnix-runner:waiting-on-container-wake");
120        if wait_items.len() > 0 {
121            log::info!("Waiting on container to receive incoming message on wake proxies");
122            match zx::object_wait_many(&mut wait_items, zx::MonotonicInstant::INFINITE) {
123                Ok(_) => (),
124                Err(e) => {
125                    warn!("error waiting for wake event {:?}", e);
126                }
127            };
128        }
129    }
130    log::info!("Finished waiting on container wake proxies.");
131
132    let mut resume_reason: Option<String> = None;
133    for wait_item in &wait_items {
134        if wait_item.pending.contains(zx::Signals::COUNTER_POSITIVE) {
135            let koid = wait_item.handle.get_koid().unwrap();
136            if let Some(event) = wake_sources.get(&koid) {
137                log::info!("Woke container from sleep for: {}", event.name);
138                resume_reason = Some(event.name.clone());
139            }
140        }
141    }
142
143    kernels.acquire_wake_lease(&container_job).await?;
144
145    log::info!("Notifying wake watchers of container wakeup.");
146    let watchers = suspend_context.wake_watchers.lock();
147    for event in watchers.iter() {
148        let (clear_mask, set_mask) = (ASLEEP_SIGNAL, AWAKE_SIGNAL);
149        event.signal_peer(clear_mask, set_mask)?;
150    }
151
152    log::info!("Returning successfully from suspend container");
153    Ok(Ok(fstarnixrunner::ManagerSuspendContainerResponse {
154        suspend_time: Some((zx::BootInstant::get() - suspend_start).into_nanos()),
155        resume_reason,
156        ..Default::default()
157    }))
158}
159
160/// Suspends the provided `zx::Job` by suspending each process in the job individually.
161///
162/// Returns the suspend handles for all the suspended processes.
163///
164/// Returns an error if any individual suspend failed. Any suspend handles will be dropped before
165/// the error is returned.
166async fn suspend_job(kernel_job: &zx::Job) -> Result<Vec<zx::Handle>, Error> {
167    let mut handles = std::collections::HashMap::<zx::Koid, zx::Handle>::new();
168    loop {
169        let process_koids = kernel_job.processes().expect("failed to get processes");
170        let mut found_new_process = false;
171        let mut processes = vec![];
172
173        for process_koid in process_koids {
174            if handles.get(&process_koid).is_some() {
175                continue;
176            }
177
178            found_new_process = true;
179
180            if let Ok(process_handle) = kernel_job.get_child(&process_koid, zx::Rights::SAME_RIGHTS)
181            {
182                let process = zx::Process::from_handle(process_handle);
183                match process.suspend() {
184                    Ok(suspend_handle) => {
185                        handles.insert(process_koid, suspend_handle);
186                    }
187                    Err(zx::Status::BAD_STATE) => {
188                        // The process was already dead or dying, and thus can't be suspended.
189                        continue;
190                    }
191                    Err(e) => {
192                        log::warn!("Failed process suspension: {:?}", e);
193                        return Err(e.into());
194                    }
195                };
196                processes.push(process);
197            }
198        }
199
200        for process in processes {
201            let threads = process.threads().expect("failed to get threads");
202            for thread_koid in &threads {
203                fuchsia_trace::duration!(c"power", c"starnix-runner:suspend_kernel", "thread_koid" => *thread_koid);
204                if let Ok(thread) = process.get_child(&thread_koid, zx::Rights::SAME_RIGHTS) {
205                    match thread.wait_handle(
206                        zx::Signals::THREAD_SUSPENDED,
207                        zx::MonotonicInstant::after(zx::MonotonicDuration::INFINITE),
208                    ) {
209                        Err(e) => {
210                            log::warn!("Error waiting for task suspension: {:?}", e);
211                            return Err(e.into());
212                        }
213                        _ => {}
214                    }
215                }
216            }
217        }
218
219        if !found_new_process {
220            break;
221        }
222    }
223
224    Ok(handles.into_values().collect())
225}