1mod session_move;
4use crate::controller::move_session::{
5 MoveMutationGuard, MoveOutcome, MovePreparation, MoveSelection, MoveSessionRequest,
6};
7pub use mj_client::daemon::*;
8
9use std::collections::{BTreeMap, BTreeSet, VecDeque};
10use std::fs::{self, OpenOptions};
11use std::io::Write;
12use std::net::{IpAddr, Ipv4Addr, SocketAddr};
13use std::path::Path;
14use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering};
15use std::sync::{Arc, Mutex, PoisonError};
16use std::time::{Duration, SystemTime, UNIX_EPOCH};
17
18use crate::database::StoreSchemaMismatch;
19use crate::recovery_gate::RecoveryObserver;
20use crate::targets::{
21 CancellableProcessExecutor, CommandExecutor, CommandOutput, CommandSpec, ProcessExecutor,
22 ProvisionStage, ProvisionStageGuard,
23};
24use anyhow::{Context, Result, anyhow, bail, ensure};
25use mj_core::config::Config;
26use mj_core::relay::RelayCommand;
27use mj_core::state::{RecoveryObservation, SessionRecord, SessionState};
28use mj_core::subagent::SubagentRecord;
29
30use crate::controller::{
31 BranchDisposition, Controller, ControllerStoreGuard, SessionLaunchOptions, SessionResumeOptions,
32};
33use crate::review_host::TurnReviewHost;
34use crate::session_manager::{
35 ManagedSessionView, RemoteSessionPublisher, RemoteSessionRequest, SessionManagerChannels,
36 SessionManagerControl, ViewError, new_command_id, spawn_remote_session_manager,
37 spawn_session_manager,
38};
39#[cfg(test)]
40use crate::session_manager::{RelaySessionTarget, RemoteSessionRequests, SessionManagerShutdown};
41use crate::worker_upgrade::{WorkerUpgradeObservation, WorkerUpgradeObserver};
42use mj_core::workspace::WorkspaceRecord;
43use tokio::net::{TcpListener, TcpStream};
44use tokio_util::sync::CancellationToken;
45
46use crate::pollers::{
47 dashboard_worker_targets, dashboard_worker_targets_excluding, interrupted_close_session_ids,
48 reserve_recovery_or_cancel, spawn_image_refresher, spawn_interrupted_close_recovery,
49};
50
51const SHUTDOWN_FORCE_EXIT_TIMEOUT: Duration = Duration::from_secs(10);
61
62const FORCE_DESTROY_PREEMPT_TIMEOUT: Duration = Duration::from_secs(8);
68
69#[derive(Clone, Default)]
71pub struct CreateSessionControl {
72 state: Arc<AtomicU8>,
73 pub cancelled: Arc<AtomicBool>,
74}
75
76impl CreateSessionControl {
77 pub fn request_cancel(&self) -> bool {
78 let accepted = self
79 .state
80 .compare_exchange(0, 1, Ordering::AcqRel, Ordering::Acquire)
81 .is_ok();
82 if accepted {
83 self.cancelled.store(true, Ordering::Release);
84 }
85 accepted
86 }
87
88 pub fn grant_commit(&self) -> bool {
89 self.state
90 .compare_exchange(0, 2, Ordering::AcqRel, Ordering::Acquire)
91 .is_ok()
92 }
93
94 fn is_cancellable(&self) -> bool {
95 self.state.load(Ordering::Acquire) == 0
96 }
97}
98
99#[derive(Debug, Clone)]
100struct Attachment {
101 pid: u32,
102}
103
104pub struct RuntimeState {
105 attachments: Mutex<BTreeMap<String, Attachment>>,
106 phone_status: Mutex<WebViewerStatus>,
107 pub web_viewer: crate::web_viewer::ViewerControl,
108 ever_attached: AtomicBool,
109 sessions: Mutex<BTreeMap<String, RuntimeSessionView>>,
110 revisions: RuntimeRevisions,
111 workspaces_tx: tokio::sync::watch::Sender<Vec<WorkspaceRecord>>,
112 session_manager: SessionManagerControl,
113 lifecycle: Mutex<BTreeMap<String, ActiveLifecycle>>,
114 close_requested: Mutex<BTreeSet<String>>,
115 controller: Mutex<Controller>,
116 controller_loader: fn() -> Result<Controller>,
117 config_mutation: tokio::sync::Mutex<()>,
118 recovery_observer: RecoveryObserver,
119 worker_upgrade_observer: WorkerUpgradeObserver,
120 notices: Mutex<VecDeque<RuntimeNotice>>,
123 next_notice_id: AtomicU64,
124 review_config: Arc<Mutex<mj_core::config::ReviewConfig>>,
126 review_host: TurnReviewHost,
129 wiki: crate::sessionwiki::WikiIndexer,
131}
132
133#[derive(Clone)]
139struct RuntimeRevisions {
140 allocated: Arc<std::sync::atomic::AtomicU64>,
141 published: tokio::sync::watch::Sender<u64>,
142}
143
144impl RuntimeRevisions {
145 fn new(initial: u64) -> Self {
146 let (published, _) = tokio::sync::watch::channel(initial);
147 Self {
148 allocated: Arc::new(std::sync::atomic::AtomicU64::new(initial)),
149 published,
150 }
151 }
152
153 fn allocate(&self) -> u64 {
154 self.allocated.fetch_add(1, Ordering::AcqRel) + 1
155 }
156
157 fn publish(&self) -> u64 {
158 let revision = self.allocate();
159 self.publish_allocated(revision);
160 revision
161 }
162
163 fn publish_allocated(&self, revision: u64) {
164 self.published.send_if_modified(|visible| {
165 if revision > *visible {
166 *visible = revision;
167 true
168 } else {
169 false
170 }
171 });
172 }
173
174 fn notifier(&self) -> Arc<dyn Fn() + Send + Sync> {
175 let revisions = self.clone();
176 Arc::new(move || {
177 revisions.publish();
178 })
179 }
180
181 fn subscribe(&self) -> tokio::sync::watch::Receiver<u64> {
182 self.published.subscribe()
183 }
184
185 fn current(&self) -> u64 {
186 self.allocated.load(Ordering::Acquire)
187 }
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191enum LifecycleKind {
192 Create,
193 Close,
194 Resume,
195 Move,
196 ForceStop,
197 DestroyStopped,
198 ArchiveStopped,
201 ForceDestroy,
202 Cleanup,
203}
204
205fn lifecycle_owns_worker_target(kind: LifecycleKind, state: Option<SessionState>) -> bool {
210 match kind {
211 LifecycleKind::Close => state == Some(SessionState::Destroying),
212 LifecycleKind::Move => !matches!(
213 state,
214 Some(
215 SessionState::Running
216 | SessionState::Disconnected
217 | SessionState::Checkpointing
218 | SessionState::Closing
219 )
220 ),
221 _ => true,
222 }
223}
224
225fn lifecycle_cancellable(kind: LifecycleKind, state: Option<SessionState>) -> bool {
231 !(kind == LifecycleKind::Close && state == Some(SessionState::Destroying))
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236enum CloseRoute {
237 Graceful,
239 RecoverInterrupted,
241 DeferredCleanup,
243 Done,
245}
246
247fn close_route(session: Option<&SessionRecord>) -> CloseRoute {
251 let Some(session) = session else {
252 return CloseRoute::Graceful;
253 };
254 if crate::pollers::is_interrupted_close(session) {
255 CloseRoute::RecoverInterrupted
256 } else if session.state == SessionState::Stopped {
257 if session.target.is_some() {
258 CloseRoute::DeferredCleanup
259 } else {
260 CloseRoute::Done
261 }
262 } else {
263 CloseRoute::Graceful
264 }
265}
266
267fn durable_session_state(controller: &Controller, session_id: &str) -> Option<SessionState> {
269 controller
270 .state
271 .sessions
272 .get(session_id)
273 .map(|session| session.state)
274}
275
276struct ActiveLifecycle {
277 operation_id: String,
278 create_control: Option<CreateSessionControl>,
279 kind: LifecycleKind,
280 cancelled: Arc<AtomicBool>,
281 started_at_epoch_seconds: u64,
282 active_stages: BTreeMap<ProvisionStage, (usize, u64)>,
283 resume_workspace_id: Option<String>,
286 resume_destination: Option<(String, String)>,
287 notice: Option<String>,
288 request_key: Option<String>,
289 _move_guard: Option<MoveMutationGuard>,
290 move_source_closed: bool,
291 result:
292 tokio::sync::watch::Receiver<Option<std::result::Result<DaemonLifecycleResult, String>>>,
293}
294
295impl ActiveLifecycle {
296 fn is_visible(&self) -> bool {
297 let result = self.result.borrow();
298 result.is_none()
299 || matches!(
300 result.as_ref(),
301 Some(Ok(DaemonLifecycleResult::DeferredCleanup))
302 )
303 }
304
305 fn request_cancel(&self) -> bool {
306 if let Some(control) = &self.create_control {
307 control.request_cancel()
308 } else {
309 !self.cancelled.swap(true, Ordering::AcqRel)
310 }
311 }
312
313 fn is_cancellable(&self) -> bool {
314 self.result.borrow().is_none()
315 && self.create_control.as_ref().map_or_else(
316 || !self.cancelled.load(Ordering::Acquire),
317 CreateSessionControl::is_cancellable,
318 )
319 }
320}
321
322#[derive(Debug, Clone)]
323enum DaemonLifecycleResult {
324 Done,
325 DeferredCleanup,
326 Move(MoveOutcome),
327}
328
329impl From<LifecycleKind> for RuntimeLifecycleKind {
330 fn from(kind: LifecycleKind) -> Self {
331 match kind {
332 LifecycleKind::Create => Self::Create,
333 LifecycleKind::Close => Self::Close,
334 LifecycleKind::Resume => Self::Resume,
335 LifecycleKind::Move => Self::Move,
336 LifecycleKind::ForceStop => Self::ForceStop,
337 LifecycleKind::DestroyStopped | LifecycleKind::ArchiveStopped => Self::DestroyStopped,
338 LifecycleKind::ForceDestroy => Self::ForceDestroy,
339 LifecycleKind::Cleanup => Self::Cleanup,
340 }
341 }
342}
343
344mod close;
345mod create;
346mod lifecycle;
347mod resume;
348mod snapshot;
349mod state;
350mod support;
351mod views;
352use support::*;
353mod process;
354pub use process::*;
355mod serve;
356use serve::*;
357mod actions;
358use actions::*;
359mod guards;
360pub(crate) use guards::*;
361
362#[cfg(test)]
363mod tests;