1use super::*;
2
3impl RuntimeState {
4 pub(super) fn cancel_lifecycle(&self, session_id: &str) -> Result<()> {
5 let controller = self
7 .controller
8 .lock()
9 .unwrap_or_else(PoisonError::into_inner);
10 let lifecycle = self
11 .lifecycle
12 .lock()
13 .unwrap_or_else(PoisonError::into_inner);
14 let active = lifecycle.get(session_id).with_context(|| {
15 format!("no lifecycle operation is running for session {session_id}")
16 })?;
17 ensure!(
18 lifecycle_cancellable(active.kind, durable_session_state(&controller, session_id)),
19 "stop of {session_id} has passed its verified checkpoint and is removing the target; \
20 it cannot be cancelled"
21 );
22 ensure!(
23 active.request_cancel(),
24 "lifecycle operation is no longer cancellable"
25 );
26 drop(lifecycle);
27 drop(controller);
28 self.publish_revision();
29 Ok(())
30 }
31
32 pub(super) async fn cancel_and_wait_lifecycles(&self) -> Result<()> {
37 let mut pending = {
38 let lifecycle = self
39 .lifecycle
40 .lock()
41 .unwrap_or_else(PoisonError::into_inner);
42 lifecycle
43 .iter()
44 .filter(|(_, active)| active.result.borrow().is_none())
45 .map(|(session_id, active)| {
46 if active.kind != LifecycleKind::Cleanup {
47 active.request_cancel();
48 }
49 let stage = active
50 .active_stages
51 .keys()
52 .next_back()
53 .map(|stage| stage.label())
54 .unwrap_or_else(|| "container cleanup".to_owned());
55 (
56 session_id.clone(),
57 active.kind,
58 stage,
59 active.cancelled.clone(),
60 active.result.clone(),
61 )
62 })
63 .collect::<Vec<_>>()
64 };
65 let cleanup_deadline = tokio::time::Instant::now() + Duration::from_secs(8);
66 for (session_id, kind, stage, cancelled, result) in &mut pending {
67 if *kind != LifecycleKind::Cleanup || result.borrow().is_some() {
68 continue;
69 }
70 tracing::info!(%session_id, %stage, "daemon shutdown is waiting for deferred cleanup");
71 self.set_lifecycle_notice(
72 session_id,
73 &format!("Daemon shutdown is waiting for {stage}"),
74 );
75 let finished = tokio::time::timeout_at(cleanup_deadline, async {
76 while result.borrow_and_update().is_none() {
77 result.changed().await.with_context(|| {
78 format!("cleanup owner stopped without a result for session {session_id}")
79 })?;
80 }
81 Ok::<_, anyhow::Error>(())
82 })
83 .await;
84 match finished {
85 Ok(result) => result?,
86 Err(_) => {
87 tracing::warn!(%session_id, %stage, "deferred cleanup exceeded the daemon shutdown drain deadline");
88 cancelled.store(true, Ordering::Release);
89 }
90 }
91 }
92 let join_deadline = tokio::time::Instant::now() + Duration::from_secs(1);
93 for (session_id, _, stage, cancelled, mut result) in pending {
94 cancelled.store(true, Ordering::Release);
95 let joined = tokio::time::timeout_at(join_deadline, async {
96 while result.borrow_and_update().is_none() {
97 result.changed().await.with_context(|| {
98 format!("lifecycle owner stopped without a result for session {session_id}")
99 })?;
100 }
101 Ok::<_, anyhow::Error>(())
102 })
103 .await;
104 if joined.is_err() {
105 bail!(
106 "timed out cancelling lifecycle owner for session {session_id} while {stage}"
107 );
108 }
109 joined.expect("checked timeout")?;
110 }
111 Ok(())
112 }
113
114 pub fn active_lifecycles(&self) -> Vec<RuntimeLifecycleView> {
123 let controller = self
127 .controller
128 .lock()
129 .unwrap_or_else(PoisonError::into_inner);
130 self.active_lifecycles_with(&controller)
131 }
132
133 pub(super) fn active_lifecycles_with(
137 &self,
138 controller: &Controller,
139 ) -> Vec<RuntimeLifecycleView> {
140 self.lifecycle
141 .lock()
142 .unwrap_or_else(PoisonError::into_inner)
143 .iter()
144 .filter(|(_, active)| active.is_visible())
145 .map(|(session_id, active)| RuntimeLifecycleView {
146 operation_id: active.operation_id.clone(),
147 cancellable: active.is_cancellable()
148 && lifecycle_cancellable(
149 active.kind,
150 durable_session_state(controller, session_id),
151 ),
152 session_id: session_id.clone(),
153 kind: active.kind.into(),
154 started_at_epoch_seconds: active.started_at_epoch_seconds,
155 active_stages: active
156 .active_stages
157 .iter()
158 .map(|(stage, (_, started_at))| (*stage, *started_at))
159 .collect(),
160 resume_destination: active.resume_destination.clone(),
161 notice: active.notice.clone(),
162 })
163 .collect()
164 }
165
166 pub fn session_state(&self, session_id: &str) -> Option<mj_core::state::SessionState> {
170 if self.close_is_requested(session_id) {
171 return Some(SessionState::Closing);
172 }
173 self.controller
174 .lock()
175 .unwrap_or_else(PoisonError::into_inner)
176 .state
177 .sessions
178 .get(session_id)
179 .map(|record| record.state)
180 }
181
182 pub fn session_record(&self, session_id: &str) -> Option<SessionRecord> {
184 self.controller
185 .lock()
186 .unwrap_or_else(PoisonError::into_inner)
187 .state
188 .sessions
189 .get(session_id)
190 .cloned()
191 }
192
193 pub async fn workspace_session_handle(
194 &self,
195 session_id: &str,
196 ) -> Result<crate::session_manager::ManagedSessionHandle> {
197 let record = self.session_record(session_id).context("unknown session")?;
198 ensure!(
199 record.target.is_some()
200 && record.state == SessionState::Running
201 && !self.close_is_requested(session_id),
202 "session must have a live running target for file injection"
203 );
204 self.session_manager.session(session_id.to_owned()).await
205 }
206
207 pub async fn checkpoint_session_now(
217 &self,
218 session_id: &str,
219 ) -> Result<mj_core::state::CheckpointMetadata> {
220 let _upgrade_work = crate::upgrade::activity("requested checkpoint")?;
221 if let Some(busy) = self.session_lifecycle_busy(session_id) {
222 return Err(anyhow::Error::new(busy));
223 }
224 let mut controller = blocking(Controller::load).await?;
225 let checkpoint = controller.checkpoint_session(session_id).await?;
226 refresh_runtime_controller(self).await;
227 Ok(checkpoint)
228 }
229
230 pub(super) fn session_lifecycle_busy(&self, session_id: &str) -> Option<SessionLifecycleBusy> {
234 let lifecycle = self
235 .lifecycle
236 .lock()
237 .unwrap_or_else(PoisonError::into_inner);
238 let active = lifecycle.get(session_id)?;
239 active
240 .result
241 .borrow()
242 .is_none()
243 .then(|| describe_lifecycle_busy(session_id, active))
244 }
245
246 pub(super) fn any_lifecycle_busy(&self) -> Option<SessionLifecycleBusy> {
250 self.lifecycle
251 .lock()
252 .unwrap_or_else(PoisonError::into_inner)
253 .iter()
254 .find(|(_, active)| active.result.borrow().is_none())
255 .map(|(session_id, active)| describe_lifecycle_busy(session_id, active))
256 }
257
258 pub fn session_projection(
262 &self,
263 ) -> (BTreeMap<String, SessionRecord>, Vec<RuntimeLifecycleView>) {
264 let controller = self
265 .controller
266 .lock()
267 .unwrap_or_else(PoisonError::into_inner);
268 let operations = self.active_lifecycles_with(&controller);
269 let mut records = controller.state.sessions.clone();
270 for id in self
271 .close_requested
272 .lock()
273 .unwrap_or_else(PoisonError::into_inner)
274 .iter()
275 {
276 if let Some(record) = records.get_mut(id)
277 && record.state != SessionState::Stopped
278 {
279 record.state = SessionState::Closing;
280 }
281 }
282 (records, operations)
283 }
284
285 pub fn cancel_lifecycle_if_active(&self, session_id: &str) {
286 if let Some(active) = self
287 .lifecycle
288 .lock()
289 .unwrap_or_else(PoisonError::into_inner)
290 .get(session_id)
291 {
292 active.request_cancel();
293 self.publish_revision();
294 }
295 }
296
297 pub(super) fn set_lifecycle_resume_destination(
298 &self,
299 session_id: &str,
300 profile_id: String,
301 target_id: String,
302 ) {
303 if let Some(active) = self
304 .lifecycle
305 .lock()
306 .unwrap_or_else(PoisonError::into_inner)
307 .get_mut(session_id)
308 {
309 active.resume_destination = Some((profile_id, target_id));
310 self.publish_revision();
311 }
312 }
313
314 pub(super) fn change_lifecycle_stage(
315 &self,
316 session_id: &str,
317 stage: ProvisionStage,
318 active: bool,
319 ) {
320 let changed = {
321 let mut lifecycle = self
322 .lifecycle
323 .lock()
324 .unwrap_or_else(PoisonError::into_inner);
325 let Some(operation) = lifecycle.get_mut(session_id) else {
326 return;
327 };
328 if active {
329 let entry = operation
330 .active_stages
331 .entry(stage)
332 .or_insert_with(|| (0, epoch_seconds()));
333 entry.0 += 1;
334 entry.0 == 1
335 } else {
336 let Some((count, _)) = operation.active_stages.get_mut(&stage) else {
337 return;
338 };
339 *count -= 1;
340 if *count == 0 {
341 operation.active_stages.remove(&stage);
342 true
343 } else {
344 false
345 }
346 }
347 };
348 if changed {
349 self.publish_revision();
350 }
351 }
352
353 pub(super) fn push_notice(&self, session_id: &str, text: impl Into<String>) {
356 const RETAINED_NOTICES: usize = 32;
357
358 let notice = RuntimeNotice {
359 id: self.next_notice_id.fetch_add(1, Ordering::AcqRel),
360 session_id: session_id.to_owned(),
361 text: text.into(),
362 };
363 {
364 let mut notices = self.notices.lock().unwrap_or_else(PoisonError::into_inner);
365 notices.push_back(notice);
366 while notices.len() > RETAINED_NOTICES {
367 notices.pop_front();
368 }
369 }
370 self.publish_revision();
371 }
372
373 pub(super) fn set_lifecycle_notice(&self, session_id: &str, notice: &str) {
374 if let Some(active) = self
375 .lifecycle
376 .lock()
377 .unwrap_or_else(PoisonError::into_inner)
378 .get_mut(session_id)
379 {
380 if active.kind == LifecycleKind::Move && notice == "Preparing destination" {
381 active.move_source_closed = true;
382 }
383 active.notice = Some(notice.to_owned());
384 self.publish_revision();
385 }
386 }
387}