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!(
138                    "Woke container from sleep for: {}, count: {:?}",
139                    event.name,
140                    event.counter.read()
141                );
142                resume_reason = Some(event.name.clone());
143            }
144        }
145    }
146
147    kernels.acquire_wake_lease(&container_job).await?;
148
149    log::info!("Notifying wake watchers of container wakeup.");
150    let watchers = suspend_context.wake_watchers.lock();
151    for event in watchers.iter() {
152        let (clear_mask, set_mask) = (ASLEEP_SIGNAL, AWAKE_SIGNAL);
153        event.signal_peer(clear_mask, set_mask)?;
154    }
155
156    log::info!("Returning successfully from suspend container");
157    Ok(Ok(fstarnixrunner::ManagerSuspendContainerResponse {
158        suspend_time: Some((zx::BootInstant::get() - suspend_start).into_nanos()),
159        resume_reason,
160        ..Default::default()
161    }))
162}
163
164/// Suspends the provided `zx::Job` by suspending each process in the job individually.
165///
166/// Returns the suspend handles for all the suspended processes.
167///
168/// Returns an error if any individual suspend failed. Any suspend handles will be dropped before
169/// the error is returned.
170async fn suspend_job(kernel_job: &zx::Job) -> Result<Vec<zx::Handle>, Error> {
171    let mut handles = std::collections::HashMap::<zx::Koid, zx::Handle>::new();
172    loop {
173        let process_koids = kernel_job.processes().expect("failed to get processes");
174        let mut found_new_process = false;
175        let mut processes = vec![];
176
177        for process_koid in process_koids {
178            if handles.get(&process_koid).is_some() {
179                continue;
180            }
181
182            found_new_process = true;
183
184            if let Ok(process_handle) = kernel_job.get_child(&process_koid, zx::Rights::SAME_RIGHTS)
185            {
186                let process = zx::Process::from_handle(process_handle);
187                match process.suspend() {
188                    Ok(suspend_handle) => {
189                        handles.insert(process_koid, suspend_handle);
190                    }
191                    Err(zx::Status::BAD_STATE) => {
192                        // The process was already dead or dying, and thus can't be suspended.
193                        continue;
194                    }
195                    Err(e) => {
196                        log::warn!("Failed process suspension: {:?}", e);
197                        return Err(e.into());
198                    }
199                };
200                processes.push(process);
201            }
202        }
203
204        for process in processes {
205            let threads = process.threads().expect("failed to get threads");
206            for thread_koid in &threads {
207                fuchsia_trace::duration!(c"power", c"starnix-runner:suspend_kernel", "thread_koid" => *thread_koid);
208                if let Ok(thread) = process.get_child(&thread_koid, zx::Rights::SAME_RIGHTS) {
209                    match thread.wait_handle(
210                        zx::Signals::THREAD_SUSPENDED,
211                        zx::MonotonicInstant::after(zx::MonotonicDuration::INFINITE),
212                    ) {
213                        Err(e) => {
214                            log::warn!("Error waiting for task suspension: {:?}", e);
215                            return Err(e.into());
216                        }
217                        _ => {}
218                    }
219                }
220            }
221        }
222
223        if !found_new_process {
224            break;
225        }
226    }
227
228    Ok(handles.into_values().collect())
229}