mj_controller/daemon/
views.rs1use 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 if self.session_lifecycle_active(session_id) {
221 return Err(anyhow::Error::new(SessionLifecycleBusy {
222 session_id: session_id.to_owned(),
223 }));
224 }
225 let mut controller = blocking(Controller::load).await?;
226 let checkpoint = controller.checkpoint_session(session_id).await?;
227 refresh_runtime_controller(self).await;
228 Ok(checkpoint)
229 }
230
231 pub(super) fn session_lifecycle_active(&self, session_id: &str) -> bool {
235 self.lifecycle
236 .lock()
237 .unwrap_or_else(PoisonError::into_inner)
238 .get(session_id)
239 .is_some_and(|active| active.result.borrow().is_none())
240 }
241
242 pub fn session_projection(
246 &self,
247 ) -> (BTreeMap<String, SessionRecord>, Vec<RuntimeLifecycleView>) {
248 let controller = self
249 .controller
250 .lock()
251 .unwrap_or_else(PoisonError::into_inner);
252 let operations = self.active_lifecycles_with(&controller);
253 let mut records = controller.state.sessions.clone();
254 for id in self
255 .close_requested
256 .lock()
257 .unwrap_or_else(PoisonError::into_inner)
258 .iter()
259 {
260 if let Some(record) = records.get_mut(id)
261 && record.state != SessionState::Stopped
262 {
263 record.state = SessionState::Closing;
264 }
265 }
266 (records, operations)
267 }
268
269 pub fn cancel_lifecycle_if_active(&self, session_id: &str) {
270 if let Some(active) = self
271 .lifecycle
272 .lock()
273 .unwrap_or_else(PoisonError::into_inner)
274 .get(session_id)
275 {
276 active.request_cancel();
277 self.publish_revision();
278 }
279 }
280
281 pub(super) fn set_lifecycle_resume_destination(
282 &self,
283 session_id: &str,
284 profile_id: String,
285 target_id: String,
286 ) {
287 if let Some(active) = self
288 .lifecycle
289 .lock()
290 .unwrap_or_else(PoisonError::into_inner)
291 .get_mut(session_id)
292 {
293 active.resume_destination = Some((profile_id, target_id));
294 self.publish_revision();
295 }
296 }
297
298 pub(super) fn change_lifecycle_stage(
299 &self,
300 session_id: &str,
301 stage: ProvisionStage,
302 active: bool,
303 ) {
304 let changed = {
305 let mut lifecycle = self
306 .lifecycle
307 .lock()
308 .unwrap_or_else(PoisonError::into_inner);
309 let Some(operation) = lifecycle.get_mut(session_id) else {
310 return;
311 };
312 if active {
313 let entry = operation
314 .active_stages
315 .entry(stage)
316 .or_insert_with(|| (0, epoch_seconds()));
317 entry.0 += 1;
318 entry.0 == 1
319 } else {
320 let Some((count, _)) = operation.active_stages.get_mut(&stage) else {
321 return;
322 };
323 *count -= 1;
324 if *count == 0 {
325 operation.active_stages.remove(&stage);
326 true
327 } else {
328 false
329 }
330 }
331 };
332 if changed {
333 self.publish_revision();
334 }
335 }
336
337 pub(super) fn push_notice(&self, session_id: &str, text: impl Into<String>) {
340 const RETAINED_NOTICES: usize = 32;
341
342 let notice = RuntimeNotice {
343 id: self.next_notice_id.fetch_add(1, Ordering::AcqRel),
344 session_id: session_id.to_owned(),
345 text: text.into(),
346 };
347 {
348 let mut notices = self.notices.lock().unwrap_or_else(PoisonError::into_inner);
349 notices.push_back(notice);
350 while notices.len() > RETAINED_NOTICES {
351 notices.pop_front();
352 }
353 }
354 self.publish_revision();
355 }
356
357 pub(super) fn set_lifecycle_notice(&self, session_id: &str, notice: &str) {
358 if let Some(active) = self
359 .lifecycle
360 .lock()
361 .unwrap_or_else(PoisonError::into_inner)
362 .get_mut(session_id)
363 {
364 if active.kind == LifecycleKind::Move && notice == "Preparing destination" {
365 active.move_source_closed = true;
366 }
367 active.notice = Some(notice.to_owned());
368 self.publish_revision();
369 }
370 }
371}