Skip to main content

mj_controller/daemon/
process.rs

1use super::diagnostics::ServingPhase;
2use super::*;
3use std::time::Instant;
4
5/// PID of the process this daemon must not outlive, if one was requested.
6///
7/// Tests start daemons that no client ever attaches to, so idle exit cannot
8/// retire them, and a test process that dies without unwinding never runs its
9/// teardown. Naming an owner makes the daemon responsible for its own lifetime.
10pub(super) fn owner_pid_to_watch() -> Result<Option<u32>> {
11    let Some(value) = mj_core::config::env_override("DAEMON_OWNER_PID") else {
12        return Ok(None);
13    };
14    let pid: u32 = value
15        .trim()
16        .parse()
17        .map_err(|_| anyhow!("MJ_DAEMON_OWNER_PID must be a process id, but it is {value:?}"))?;
18    ensure!(
19        process_is_alive(pid),
20        "MJ_DAEMON_OWNER_PID names process {pid}, which is not running"
21    );
22    Ok(Some(pid))
23}
24
25pub async fn run_daemon_process() -> Result<()> {
26    // Checked before the store is locked so a bad value fails fast and leaves
27    // no daemon state behind.
28    let owner_pid = owner_pid_to_watch()?;
29    let opening = Instant::now();
30    let guard = ControllerStoreGuard::acquire()?;
31    let database_writer = guard.start_database_writer()?;
32    log_startup_phase("open the store", opening);
33    let epilogue_started = AtomicBool::new(false);
34    let mut outcome = run_daemon_runtime(&epilogue_started, owner_pid).await;
35    if !epilogue_started.load(Ordering::Acquire) {
36        // Initialization failed before the runtime-owned epilogue existed.
37        // The same process-level bound still applies to closing the writer.
38        spawn_shutdown_watchdog();
39    }
40    // The writer commits every write it accepted before it stops, so this is
41    // the one wait after the epilogue that durability requires. Timed, so a
42    // slow exit says whether the writer or the runtime held it.
43    let writer_started = Instant::now();
44    let writer_shutdown = tokio::task::spawn_blocking(move || database_writer.shutdown())
45        .await
46        .context("database writer shutdown task panicked")
47        .and_then(std::convert::identity);
48    let writer_took = writer_started.elapsed();
49    if writer_took >= Duration::from_millis(250) {
50        tracing::info!(
51            duration_ms = writer_took.as_millis(),
52            "database writer shut down"
53        );
54    }
55    record_daemon_cleanup(&mut outcome, "shut down database writer", writer_shutdown);
56    outcome
57}
58
59pub(super) async fn run_daemon_runtime(
60    epilogue_started: &AtomicBool,
61    owner_pid: Option<u32>,
62) -> Result<()> {
63    let progress = super::diagnostics::DaemonProgressMonitor::start()?;
64    // Before this daemon can start a checkpoint, so every checkpoint file older
65    // than this belongs to a checkpoint that no longer runs.
66    let started = SystemTime::now();
67    let startup_work = crate::upgrade::activity("daemon startup recovery")?;
68    // Freeze worker sources before any session can be created or upgraded.
69    // Copying binaries belongs on a blocking task, never the runtime event loop.
70    tokio::task::spawn_blocking(crate::controller::pin_worker_binary_sources)
71        .await
72        .context("worker source snapshot task failed")??;
73    // Lock files outlive the SSH masters they guarded (launch finding R3-11).
74    tokio::task::spawn_blocking(crate::targets::SshSessions::remove_stale_master_locks)
75        .await
76        .context("SSH master lock cleanup task failed")?;
77    Controller::recover_config_id_rename()?;
78    let config = Config::load()?;
79    crate::database::recover_interrupted_checkpointing_sessions(
80        &chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
81    )?;
82    crate::controller::reconcile_managed_checkpoint_archives()?;
83
84    let loading = Instant::now();
85    let controller = tokio::task::spawn_blocking(|| {
86        let mut controller = Controller::load()?;
87        controller.prepare_persisted_sessions()?;
88        Ok::<_, anyhow::Error>(controller)
89    })
90    .await
91    .context("prepare persisted daemon sessions")??;
92    log_startup_phase("load persisted sessions", loading);
93    // A local session an earlier release started from a profile home keeps
94    // running from it until it is next staged. The link has to be in place
95    // before any launch configuration is refreshed or any credential sync runs.
96    {
97        let state = controller.state.clone();
98        tokio::task::spawn_blocking(move || {
99            crate::controller::local_profile_homes::link_profile_homes_of_earlier_sessions(&state)
100        })
101        .await
102        .context("earlier sessions' profile home link task failed")?;
103    }
104    let listener = TcpListener::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0))
105        .await
106        .context("bind Mjolnir daemon loopback endpoint")?;
107    let metadata = DaemonMetadata {
108        protocol_version: PROTOCOL_VERSION,
109        pid: std::process::id(),
110        address: listener.local_addr()?,
111        token: random_hex::<32>()?,
112        started_at: chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
113        // The version, with the revision and build times as semver build
114        // metadata that older clients ignore; see `mj_client::build_identity`.
115        build_version: mj_client::build_identity::this_build().published(),
116    };
117    let workspaces = tokio::task::spawn_blocking(crate::database::list_workspaces)
118        .await
119        .context("daemon workspace load task panicked")??;
120    let mut remote = if config.phone.enabled {
121        Some(spawn_remote_session_manager()?)
122    } else {
123        None
124    };
125
126    // Start the primary manager after store preparation. Bootstrap failures
127    // below have an awaited epilogue just like failures in the serving loop.
128    let (delegation_tx, delegation_updates) = crate::session_manager::delegation_channel();
129    let manager = crate::session_manager::spawn_session_manager_observed(Some(delegation_tx))?;
130    let manager_targets = manager.targets;
131    manager_targets.send_replace(dashboard_worker_targets(&controller));
132    let manager_updates = manager.updates;
133    let manager_control = manager.control.clone();
134    let manager_shutdown = manager.shutdown;
135    let mut recovery = crate::recovery::RecoveryCoordinator::spawn(manager_control.clone());
136    let recovery_observer = recovery.observer();
137    // Shares the recovery gate, so a recovery copy and a worker upgrade never
138    // act on one session at the same time.
139    let mut worker_upgrades = crate::worker_upgrade::WorkerUpgradeCoordinator::spawn(
140        manager_control.clone(),
141        &recovery_observer,
142    );
143    let state = Arc::new(RuntimeState::new(
144        manager_control.clone(),
145        Controller {
146            config: controller.config.clone(),
147            state: controller.state.clone(),
148        },
149        recovery_observer.clone(),
150        worker_upgrades.observer(),
151        workspaces,
152    ));
153    // Views are published only from the serving loop below, so the first
154    // targets are installed before any can arrive.
155    state.owner().install_relay_sessions(
156        manager_targets
157            .borrow()
158            .iter()
159            .map(|target| target.session_id.clone())
160            .collect(),
161    );
162    let cancellation = crate::termination::Coordinator::install().token();
163    // Bootstrap already owns live managers. Capture its error so those owners
164    // are shut down before the process-level writer can be closed.
165    let bootstrapping = Instant::now();
166    let bootstrap = async {
167        let move_operations = blocking(crate::database::load_move_operations).await?;
168        let move_sessions = move_operations
169            .iter()
170            .map(|operation| operation.selection.session_id.clone())
171            .collect::<BTreeSet<_>>();
172        let mut lifecycle_owned = state.recover_moves(move_operations)?;
173        let restart_intents = blocking(crate::database::load_session_restarts).await?;
174        lifecycle_owned.extend(
175            restart_intents
176                .iter()
177                .map(|intent| intent.session_id.clone()),
178        );
179        lifecycle_owned.extend(state.recover_restarts(restart_intents)?);
180        lifecycle_owned.extend(move_sessions.iter().cloned());
181        state.resume_retained_cleanups();
182        state.resume_startup_cleanups(true);
183        state.restore_startup_deliveries(&cancellation).await?;
184        Ok::<_, anyhow::Error>((move_sessions, lifecycle_owned))
185    }
186    .await;
187    log_startup_phase("recover moves and startup deliveries", bootstrapping);
188    let (move_sessions, move_owned) = match bootstrap {
189        Ok(ownership) => ownership,
190        Err(error) => {
191            epilogue_started.store(true, Ordering::Release);
192            spawn_shutdown_watchdog();
193            cancellation.cancel();
194            let mut outcome = Err(error);
195            record_daemon_cleanup(
196                &mut outcome,
197                "shut down bootstrap worker upgrade coordinator",
198                worker_upgrades.shutdown().await,
199            );
200            record_daemon_cleanup(
201                &mut outcome,
202                "shut down bootstrap recovery coordinator",
203                recovery.shutdown().await,
204            );
205            record_daemon_cleanup(
206                &mut outcome,
207                "shut down turn review host after bootstrap failure",
208                state
209                    .review_host()
210                    .shutdown()
211                    .await
212                    .map_err(anyhow::Error::msg),
213            );
214            record_daemon_cleanup(
215                &mut outcome,
216                "cancel bootstrap lifecycle operations",
217                state.cancel_and_wait_lifecycles().await,
218            );
219            record_daemon_cleanup(
220                &mut outcome,
221                "drain bootstrap startup deliveries",
222                state.cancel_and_join_startup_prompts().await,
223            );
224            if let Some(remote) = remote.take() {
225                record_daemon_cleanup(
226                    &mut outcome,
227                    "shut down bootstrap remote session manager",
228                    remote.shutdown.shutdown().await,
229                );
230            }
231            record_daemon_cleanup(
232                &mut outcome,
233                "shut down bootstrap session manager",
234                manager_shutdown.shutdown().await,
235            );
236            return outcome;
237        }
238    };
239    // Checkpoint files that a cancelled or interrupted checkpoint left in a
240    // local worker root. Deleting them can take minutes after a long leak, so
241    // this runs beside the daemon rather than before it serves. It stops at
242    // shutdown, and the next start finishes it.
243    let checkpoint_sweep = {
244        let cancellation = cancellation.clone();
245        tokio::task::spawn_blocking(move || {
246            crate::controller::sweep_local_checkpoint_leftovers(
247                &crate::targets::BoundedProcessExecutor::new(Duration::from_secs(15)),
248                started,
249                &|| cancellation.is_cancelled(),
250            );
251        })
252    };
253    let mut update_feed = continuation::spawn(state.clone(), manager_updates, cancellation.clone());
254
255    let (delegation_services, mut delegation_task) = super::delegation::spawn(
256        state.clone(),
257        manager_control.clone(),
258        delegation_updates,
259        cancellation.clone(),
260    );
261
262    // CPU publication is bounded and reconstructed from workers. It holds no
263    // admission during handoff and exits with the daemon's cancellation token.
264    let cpu_publication = {
265        let mut receiver = manager.session_cpu;
266        let state = state.clone();
267        let cancellation = cancellation.clone();
268        tokio::spawn(async move {
269            let mut next_publication = tokio::time::Instant::now();
270            loop {
271                tokio::select! {
272                    _ = cancellation.cancelled() => return,
273                    changed = receiver.changed() => {
274                        if changed.is_err() {
275                            tracing::error!("session CPU publication channel closed");
276                            return;
277                        }
278                    }
279                }
280                // Publish the first update promptly, then coalesce other actors'
281                // updates. A separate polling phase adds a needless ten seconds.
282                tokio::select! {
283                    _ = cancellation.cancelled() => return,
284                    _ = tokio::time::sleep_until(next_publication) => {}
285                }
286                receiver.borrow_and_update();
287                state.publish_revision();
288                next_publication = tokio::time::Instant::now() + Duration::from_secs(10);
289            }
290        })
291    };
292    let project_catalog = tokio::spawn(state.projects().run(cancellation.child_token()));
293    // Before the web viewer starts: it shows this service's readings.
294    let capacity_service =
295        super::capacity::spawn_capacity_service(state.clone(), cancellation.clone());
296
297    let target_refresh = spawn_manager_target_refresher(
298        manager_targets.clone(),
299        cancellation.clone(),
300        state.clone(),
301    );
302    let image_refresh = spawn_image_refresher(
303        {
304            let state = state.clone();
305            move || state.with_config(crate::controller::image_refresh_plan)
306        },
307        {
308            // A background download is the daemon's own work, not a session's,
309            // so these notices carry an empty session id and reach every
310            // workspace.
311            let state = state.clone();
312            move |report| {
313                let text = match report {
314                    crate::pollers::ImageRefreshReport::Started { host, image } => {
315                        format!("Downloading image {image} for {host}\u{2026}")
316                    }
317                    crate::pollers::ImageRefreshReport::Pulled { host, image } => {
318                        format!("Image {image} is ready on {host}.")
319                    }
320                    crate::pollers::ImageRefreshReport::Failed { host, image, error } => {
321                        format!("Could not pull image {image} on {host}: {error}")
322                    }
323                };
324                state.push_notice("", text);
325            }
326        },
327        cancellation.clone(),
328    );
329    let exit_when_idle = mj_core::config::env_override_os("DAEMON_EXIT_WHEN_IDLE").is_some();
330    let mut idle_tick = tokio::time::interval(Duration::from_millis(100));
331    idle_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
332    let mut owner_tick = tokio::time::interval(Duration::from_millis(500));
333    owner_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
334    let mut recovery_tick = tokio::time::interval(Duration::from_millis(250));
335    recovery_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
336    let mut background_policy_tick = tokio::time::interval(Duration::from_secs(1));
337    background_policy_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
338    // How often the harness readiness wait is looked at. The wait itself is
339    // minutes long, so this only bounds how late a failure is noticed, and it
340    // reads in-memory state rather than the store.
341    let mut readiness_tick = tokio::time::interval(Duration::from_secs(5));
342    readiness_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
343    let mut progress_tick = tokio::time::interval(Duration::from_secs(1));
344    progress_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
345    let (interrupted_close_tx, mut interrupted_close_rx) = tokio::sync::mpsc::unbounded_channel();
346    let mut interrupted_close_tasks = Vec::new();
347    for session_id in interrupted_suspend_session_ids(&controller) {
348        if move_owned.contains(&session_id) {
349            continue;
350        }
351        let recovery_state = state.clone();
352        let recovery_shutdown = cancellation.clone();
353        let updates = interrupted_close_tx.clone();
354        let upgrade_work = startup_work.clone();
355        let interrupted_close_task = tokio::spawn(async move {
356            let _upgrade_work = upgrade_work;
357            let result = tokio::select! {
358                result = recovery_state.suspend_session(session_id.clone()) => result,
359                () = recovery_shutdown.cancelled() => return,
360            }
361            .map(|()| crate::pollers::LifecycleSuccess::Closed)
362            .map_err(|error| format!("{error:#}"));
363            if updates
364                .send(crate::pollers::LifecycleUpdate {
365                    session_id,
366                    result,
367                    deferred_cleanup: false,
368                })
369                .is_err()
370            {
371                tracing::debug!("suspension recovery receiver stopped");
372            }
373        });
374        interrupted_close_tasks.push(interrupted_close_task);
375    }
376    // A destroy that failed left its record in `Error` with the target still
377    // present; it is finished the way `mj destroy` finishes it. The branch is
378    // kept, because whether to delete it was that command's choice and is not
379    // recorded.
380    for session_id in interrupted_destroy_session_ids(&controller) {
381        let recovery_state = state.clone();
382        let recovery_shutdown = cancellation.clone();
383        let updates = interrupted_close_tx.clone();
384        let upgrade_work = startup_work.clone();
385        interrupted_close_tasks.push(tokio::spawn(async move {
386            let _upgrade_work = upgrade_work;
387            let result = tokio::select! {
388                result = recovery_state.force_destroy_session(
389                    session_id.clone(),
390                    crate::daemon::BranchDisposition::Keep,
391                ) => result,
392                () = recovery_shutdown.cancelled() => return,
393            }
394            .map(|()| crate::pollers::LifecycleSuccess::ForceDestroyed)
395            .map_err(|error| format!("{error:#}"));
396            let _ = updates.send(crate::pollers::LifecycleUpdate {
397                session_id,
398                result,
399                deferred_cleanup: false,
400            });
401        }));
402    }
403    // Whatever is still in an in-flight lifecycle state now has no owner: the
404    // moves, the interrupted closes, and the checkpointing rows above are
405    // every operation that legitimately resumes. The list comes from the
406    // startup snapshot, so a session created after this cannot be caught by
407    // it, and the writes run off this path because they touch the database.
408    let reconciliation = {
409        let unowned = unowned_interrupted_lifecycles(
410            &controller,
411            &move_owned.union(&move_sessions).cloned().collect(),
412        );
413        (!unowned.is_empty()).then(|| {
414            let state = state.clone();
415            let upgrade_work = startup_work.clone();
416            tokio::spawn(async move {
417                let _upgrade_work = upgrade_work;
418                let reconciled = tokio::task::spawn_blocking(move || {
419                    let mut controller = Controller::load()?;
420                    let mut reconciled = 0usize;
421                    for (session_id, cause) in unowned {
422                        match controller.fail_interrupted_lifecycle(&session_id, &cause) {
423                            Ok(true) => {
424                                tracing::warn!(%session_id, %cause, "session left in flight by a daemon restart marked failed");
425                                reconciled += 1;
426                            }
427                            Ok(false) => {}
428                            Err(error) => tracing::warn!(
429                                %session_id,
430                                error = format!("{error:#}"),
431                                "could not reconcile an interrupted lifecycle state"
432                            ),
433                        }
434                    }
435                    anyhow::Ok(reconciled)
436                })
437                .await;
438                match reconciled {
439                    Ok(Ok(0)) => {}
440                    Ok(Ok(_)) => refresh_runtime_controller(&state).await,
441                    Ok(Err(error)) => tracing::warn!(
442                        error = format!("{error:#}"),
443                        "could not load the controller to reconcile interrupted lifecycles"
444                    ),
445                    Err(error) => {
446                        tracing::warn!(%error, "interrupted lifecycle reconciliation task failed");
447                    }
448                }
449            })
450        })
451    };
452    // Rows left as tombstones by an older build, or by a discard that could
453    // not finish before the daemon stopped. The list comes from the startup
454    // snapshot, so a session that becomes lost after this is handled by the
455    // view watcher instead.
456    let tombstone_sweep = {
457        let tombstones = tombstone_session_ids(&controller);
458        (!tombstones.is_empty()).then(|| {
459            let state = state.clone();
460            let upgrade_work = startup_work.clone();
461            tokio::spawn(async move {
462                let _upgrade_work = upgrade_work;
463                for session_id in tombstones {
464                    state.discard_lost_session(session_id).await;
465                }
466            })
467        })
468    };
469    let mut phone_publisher: Option<RemoteSessionPublisher> = None;
470    let mut phone_targets = None;
471    let mut prepared_targets = manager_targets.subscribe();
472    let mut phone_task = None;
473    let mut remote_request_bridge = None;
474    if let Some(remote) = remote.take() {
475        remote
476            .targets
477            .send_replace(prepared_targets.borrow_and_update().clone());
478        phone_targets = Some(remote.targets.clone());
479        phone_publisher = Some(remote.publisher.clone());
480        remote_request_bridge = Some(spawn_remote_request_bridge(
481            remote.requests,
482            manager_control.clone(),
483        ));
484        phone_task = Some(spawn_phone_server(
485            config.phone,
486            cancellation.clone(),
487            state.clone(),
488            SessionManagerChannels {
489                session_cpu: remote.control.session_cpu.clone(),
490                targets: remote.targets,
491                control: remote.control,
492                updates: remote.updates,
493                shutdown: remote.shutdown,
494            },
495            delegation_services,
496        ));
497    } else {
498        state.set_phone_status(WebViewerStatus::Disabled);
499        state.web_viewer.publish(crate::server::WebViewerAccess::Unavailable("Web access is disabled. Enable [phone].enabled in your configuration, then restart the daemon.".into()));
500    }
501    let daemon_metadata_path = metadata_path();
502    let mut client_tasks = tokio::task::JoinSet::new();
503    let mut delegation_outcome = None;
504    let mut cache_task = tokio::spawn(crate::controller::mbx::service::run(
505        state.clone(),
506        cancellation.clone(),
507    ));
508    let mut mailbox_task = tokio::spawn(crate::mailbox_outbox::run(
509        state.clone(),
510        manager_control.clone(),
511        cancellation.clone(),
512    ));
513    let mut github_watch_task = tokio::spawn(crate::github_watch::service::run(
514        state.clone(),
515        cancellation.clone(),
516    ));
517
518    let mut cache_outcome = None;
519    let mut mailbox_outcome = None;
520    let mut github_watch_outcome = None;
521
522    // Everything a client can use is initialized before this atomic
523    // publication. From here on every exit, including an error from the test
524    // hook or the event loop, flows through the same bounded epilogue.
525    let mut outcome = async {
526        write_metadata(&daemon_metadata_path, &metadata)?;
527        reach_test_hook("daemon_metadata_before_listening").await?;
528        tracing::info!("the daemon is serving");
529        drop(startup_work);
530        loop {
531            progress.serving_tick();
532            tokio::select! {
533                _ = cancellation.cancelled() => break,
534                _ = progress_tick.tick() => {},
535                changed = prepared_targets.changed(), if phone_targets.is_some() => {
536                    progress.phase(ServingPhase::Targets);
537                    changed.context("daemon worker target publication stopped")?;
538                    if let Some(targets) = &phone_targets {
539                        targets.send_replace(prepared_targets.borrow_and_update().clone());
540                    }
541                }
542                result = &mut cache_task => {
543                    cache_outcome = Some(result.context("machine cache service failed").and_then(|result| result));
544                    anyhow::bail!("machine build cache service stopped unexpectedly");
545                }
546                result = &mut mailbox_task => {
547                    mailbox_outcome = Some(result.context("mailbox outbox service failed").and_then(|result| result));
548                    anyhow::bail!("mailbox outbox service stopped unexpectedly");
549                }
550                result = &mut github_watch_task => {
551                    github_watch_outcome = Some(result.context("GitHub watch service failed").and_then(|result| result));
552                    anyhow::bail!("GitHub watch service stopped unexpectedly");
553                }
554                result = &mut delegation_task => {
555                    delegation_outcome = Some(result.map_err(anyhow::Error::from).and_then(|result| result));
556                    anyhow::bail!("delegation coordinator stopped unexpectedly");
557                }
558                _ = idle_tick.tick(), if exit_when_idle && state.ever_attached.load(Ordering::Acquire) => {
559                    progress.phase(ServingPhase::IdleCheck);
560                    state.prune_dead_clients();
561                    if state.attachments().is_empty() {
562                        break;
563                    }
564                }
565                _ = owner_tick.tick(), if owner_pid.is_some() => {
566                    progress.phase(ServingPhase::OwnerCheck);
567                    if let Some(owner) = owner_pid
568                        && !process_is_alive(owner)
569                    {
570                        tracing::info!(owner_pid = owner, "daemon owner process exited; shutting down");
571                        break;
572                    }
573                }
574                _ = recovery_tick.tick() => {
575                    progress.phase(ServingPhase::Recovery);
576                    while let Some(result) = recovery.try_result() {
577                        if let Err(error) = &result.outcome {
578                            // A deferred copy found the agent working. That is
579                            // the normal state of a session in use, so it is
580                            // news, not a fault.
581                            if result.deferred {
582                                tracing::info!(session_id = %result.session_id, %error, "recovery copy deferred: agent is working");
583                            } else if result.cancelled {
584                                // The daemon asked for this cancel (a suspend
585                                // or close superseded the copy), so it is not
586                                // a failure of the checkpoint.
587                                tracing::info!(session_id = %result.session_id, %error, reason = "superseded by suspend", "daemon recovery checkpoint cancelled");
588                            } else {
589                                tracing::warn!(session_id = %result.session_id, %error, "daemon recovery checkpoint failed");
590                            }
591                        }
592                        refresh_runtime_controller(&state).await;
593                    }
594                    while let Some(result) = worker_upgrades.try_result() {
595                        report_worker_upgrade(&state, &result).await;
596                    }
597                }
598                _ = background_policy_tick.tick() => {
599                    progress.phase(ServingPhase::BackgroundPolicy);
600                    state.refresh_background_policies();
601                    state.resume_startup_cleanups(false);
602                }
603                _ = readiness_tick.tick() => {
604                    progress.phase(ServingPhase::Readiness);
605                    // A live session whose harness never advertised itself
606                    // takes no prompt and reports no failure, so nothing else
607                    // ever ends its wait (#1090). The store write runs off
608                    // this loop.
609                    for unready in state.sessions_without_a_usable_harness() {
610                        let state = state.clone();
611                        client_tasks.spawn(async move {
612                            state.fail_unready_session(unready).await;
613                        });
614                    }
615                }
616                completed = interrupted_close_rx.recv() => {
617                    progress.phase(ServingPhase::SuspensionRecovery);
618                    if let Some(completed) = completed {
619                        let recovered = completed.result.is_ok();
620                        if let Err(error) = completed.result {
621                            tracing::warn!(session_id = %completed.session_id, %error, "daemon could not resume interrupted close");
622                        }
623                        refresh_runtime_controller(&state).await;
624                        if recovered && completed.deferred_cleanup
625                            && let Err(error) = state.start_deferred_cleanup(completed.session_id.clone())
626                        {
627                            tracing::warn!(session_id = %completed.session_id, error = format!("{error:#}"), "could not continue cleanup after interrupted close");
628                            state.push_notice(
629                                &completed.session_id,
630                                format!("Could not continue container storage cleanup: {error:#}"),
631                            );
632                        }
633                    }
634                }
635                accepted = listener.accept() => {
636                    progress.phase(ServingPhase::Client);
637                    let (stream, peer) = accepted.context("accept Mjolnir daemon client")?;
638                    if !peer.ip().is_loopback() {
639                        tracing::warn!(%peer, "rejected non-loopback daemon client");
640                        continue;
641                    }
642                    let metadata = metadata.clone();
643                    let state = state.clone();
644                    let cancellation = cancellation.clone();
645                    client_tasks.spawn(async move {
646                        if let Err(error) = serve_client(stream, metadata, state, cancellation).await {
647                            tracing::debug!(error = format!("{error:#}"), "daemon client disconnected");
648                        }
649                    });
650                }
651                completed = client_tasks.join_next(), if !client_tasks.is_empty() => {
652                    if let Some(Err(error)) = completed {
653                        tracing::warn!(%error, "daemon client task failed");
654                    }
655                }
656                update = update_feed.next() => {
657                    progress.phase(ServingPhase::SessionUpdate);
658                    // The feed says why it ended. A shutdown also stops the
659                    // continuation service and closes this channel, so a
660                    // closed channel is not evidence of a failure.
661                    let Some(update) = update? else { break };
662                    if let Some((detail, observed_updated_at)) =
663                        state.missing_target_record(&update.session_id, &update.view)
664                    {
665                        let state = state.clone();
666                        let session_id = update.session_id.clone();
667                        client_tasks.spawn(async move {
668                            if let Err(error) = state.persist_missing_target(
669                                &session_id, detail, observed_updated_at,
670                            ).await {
671                                tracing::warn!(%session_id, %error, "could not persist missing worker target");
672                                state.push_notice(&session_id, format!("Could not record missing session target: {error:#}"));
673                            }
674                        });
675                    }
676                    if let Some(publisher) = phone_publisher.as_ref()
677                        && let Err(error) = publisher.try_publish(
678                            update.session_id.clone(),
679                            update.view.clone(),
680                        )
681                    {
682                        tracing::warn!(%error, "phone session view bridge stopped");
683                        phone_publisher = None;
684                    }
685                    // Every session's view passes here whether or not anything is
686                    // attached, which is exactly what an automatic review needs to
687                    // see: the turn that just finished.
688                    // The continuation completion gate has already notified review.
689                    state.publish_session(update.session_id, update.view).await?;
690                    // State is published before waking the delivery owner, so
691                    // its readiness check observes the same session update.
692                    crate::mailbox_outbox::notify_mailbox_outbox_changed();
693                }
694            }
695        }
696        Ok(())
697    }
698    .await;
699
700    progress.phase(ServingPhase::Shutdown);
701    let mut epilogue = EpilogueClock::start();
702    epilogue_started.store(true, Ordering::Release);
703    spawn_shutdown_watchdog();
704    // Idle exit and fallible loop exits do not arrive through the termination
705    // coordinator. Stop every daemon-owned task before closing the sole writer.
706    cancellation.cancel();
707    epilogue.record(
708        &mut outcome,
709        "join machine build cache applications",
710        match cache_outcome {
711            Some(result) => result,
712            None => cache_task
713                .await
714                .context("machine cache service failed")
715                .and_then(|result| result),
716        },
717    );
718    epilogue.record(
719        &mut outcome,
720        "join mailbox outbox service",
721        match mailbox_outcome {
722            Some(result) => result,
723            None => mailbox_task
724                .await
725                .context("mailbox outbox service failed")
726                .and_then(|result| result),
727        },
728    );
729    epilogue.record(
730        &mut outcome,
731        "join GitHub watch service",
732        match github_watch_outcome {
733            Some(result) => result,
734            None => github_watch_task
735                .await
736                .context("GitHub watch service failed")
737                .and_then(|result| result),
738        },
739    );
740    // Recovery copies and worker upgrades do not hold up a handoff, so some
741    // may still be running. Stop them first: the next daemon starts them again.
742    // The coordinators share one gate, and dropping either cancels both.
743    epilogue.record(
744        &mut outcome,
745        "shut down worker upgrade coordinator",
746        worker_upgrades.shutdown().await,
747    );
748    epilogue.record(
749        &mut outcome,
750        "shut down recovery coordinator",
751        recovery.shutdown().await,
752    );
753    epilogue.record(
754        &mut outcome,
755        "join continuation service",
756        update_feed.join().await,
757    );
758    epilogue.record(
759        &mut outcome,
760        "join delegation coordinator",
761        match delegation_outcome {
762            Some(result) => result,
763            None => delegation_task
764                .await
765                .map_err(anyhow::Error::from)
766                .and_then(|result| result),
767        },
768    );
769    drop(interrupted_close_tx);
770    epilogue.record(
771        &mut outcome,
772        "remove daemon metadata",
773        remove_daemon_metadata(&daemon_metadata_path),
774    );
775    epilogue.record(
776        &mut outcome,
777        "shut down turn review host",
778        state
779            .review_host()
780            .shutdown()
781            .await
782            .map_err(anyhow::Error::msg),
783    );
784    epilogue.record(
785        &mut outcome,
786        "join CPU publication",
787        cpu_publication.await.map_err(anyhow::Error::from),
788    );
789    record_daemon_cleanup(
790        &mut outcome,
791        "join project discovery",
792        project_catalog.await.map_err(anyhow::Error::from),
793    );
794    epilogue.record(
795        &mut outcome,
796        "join controller target refresher",
797        target_refresh.await.map_err(anyhow::Error::new),
798    );
799    epilogue.record(
800        &mut outcome,
801        "join container image refresher",
802        image_refresh.await.map_err(anyhow::Error::new),
803    );
804    epilogue.record(
805        &mut outcome,
806        "join capacity service",
807        capacity_service.await.map_err(anyhow::Error::new),
808    );
809    if let Some(phone_task) = phone_task {
810        epilogue.record(
811            &mut outcome,
812            "join phone server",
813            phone_task.await.map_err(anyhow::Error::new),
814        );
815    }
816    if let Some(remote_request_bridge) = remote_request_bridge {
817        epilogue.record(
818            &mut outcome,
819            "join phone session request bridge",
820            remote_request_bridge.await.map_err(anyhow::Error::new),
821        );
822    }
823    client_tasks.abort_all();
824    while let Some(result) = client_tasks.join_next().await {
825        if let Err(error) = result
826            && !error.is_cancelled()
827        {
828            record_daemon_cleanup(
829                &mut outcome,
830                "join daemon client task",
831                Err(anyhow::Error::new(error)),
832            );
833        }
834    }
835    epilogue.record(&mut outcome, "join daemon client tasks", Ok(()));
836    epilogue.record(
837        &mut outcome,
838        "cancel daemon lifecycle operations",
839        state.cancel_and_wait_lifecycles().await,
840    );
841    epilogue.record(
842        &mut outcome,
843        "drain startup prompts",
844        state.cancel_and_join_startup_prompts().await,
845    );
846    if let Some(reconciliation) = reconciliation {
847        epilogue.record(
848            &mut outcome,
849            "join interrupted lifecycle reconciliation",
850            reconciliation.await.map_err(anyhow::Error::new),
851        );
852    }
853    if let Some(tombstone_sweep) = tombstone_sweep {
854        epilogue.record(
855            &mut outcome,
856            "join lost-session discard sweep",
857            tombstone_sweep.await.map_err(anyhow::Error::new),
858        );
859    }
860    epilogue.record(
861        &mut outcome,
862        "join checkpoint leftover sweep",
863        checkpoint_sweep.await.map_err(anyhow::Error::new),
864    );
865    for interrupted_close_task in interrupted_close_tasks {
866        epilogue.record(
867            &mut outcome,
868            "join interrupted close recovery",
869            interrupted_close_task.await.map_err(anyhow::Error::new),
870        );
871    }
872    epilogue.record(
873        &mut outcome,
874        "shut down controller daemon session manager",
875        manager_shutdown.shutdown().await,
876    );
877    epilogue.finish();
878    outcome
879}
880
881/// Log a startup phase that took long enough to matter, so a slow start after
882/// an upgrade records where its time went.
883fn log_startup_phase(phase: &'static str, started: Instant) {
884    let took = started.elapsed();
885    if took >= EpilogueClock::LOGGED_STEP {
886        tracing::info!(
887            phase,
888            duration_ms = took.as_millis(),
889            "daemon startup phase finished"
890        );
891    }
892}
893
894/// Sessions whose record is nothing but a tombstone: they ended without a
895/// target and without a checkpoint, so the record offers no action but its own
896/// removal. `DestroyedWithDataLoss` only ever comes from an older build.
897pub(super) fn tombstone_session_ids(controller: &Controller) -> Vec<String> {
898    controller
899        .state
900        .sessions
901        .values()
902        .filter(|session| {
903            matches!(
904                session.state,
905                SessionState::Lost | SessionState::DestroyedWithDataLoss
906            )
907        })
908        .map(|session| session.id.clone())
909        .collect()
910}
911
912pub(super) fn remove_daemon_metadata(path: &Path) -> Result<()> {
913    match fs::remove_file(path) {
914        Ok(()) => Ok(()),
915        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
916        Err(error) => Err(error).with_context(|| format!("remove {}", path.display())),
917    }
918}
919
920/// Keep the event-loop failure as the primary result while still running and
921/// reporting every cleanup step. If the loop ended normally, the first
922/// cleanup failure becomes the daemon's result.
923/// Times the shutdown epilogue, so a slow exit, such as the end of an upgrade
924/// handoff, records which step took the time.
925struct EpilogueClock {
926    began: Instant,
927    step: Instant,
928}
929
930impl EpilogueClock {
931    /// Steps shorter than this are not logged.
932    const LOGGED_STEP: Duration = Duration::from_millis(250);
933
934    fn start() -> Self {
935        let now = Instant::now();
936        tracing::info!("daemon shutdown began");
937        Self {
938            began: now,
939            step: now,
940        }
941    }
942
943    /// Record `cleanup`, the result of the step that just finished. Steps run
944    /// one after another, so the time since the previous one is this step's.
945    fn record(&mut self, outcome: &mut Result<()>, operation: &'static str, cleanup: Result<()>) {
946        let took = self.step.elapsed();
947        if took >= Self::LOGGED_STEP {
948            tracing::info!(
949                step = operation,
950                duration_ms = took.as_millis(),
951                "daemon shutdown step finished"
952            );
953        }
954        record_daemon_cleanup(outcome, operation, cleanup);
955        self.step = Instant::now();
956    }
957
958    fn finish(self) {
959        tracing::info!(
960            duration_ms = self.began.elapsed().as_millis(),
961            "daemon shutdown finished"
962        );
963    }
964}
965
966pub(super) fn record_daemon_cleanup(
967    outcome: &mut Result<()>,
968    operation: &'static str,
969    cleanup: Result<()>,
970) {
971    let Err(error) = cleanup else {
972        return;
973    };
974    let error = error.context(operation);
975    if outcome.is_ok() {
976        *outcome = Err(error);
977    } else {
978        tracing::warn!(error = format!("{error:#}"), "daemon cleanup step failed");
979    }
980}
981
982/// Bounds the epilogue below.
983///
984/// The daemon leaves on its own long before this fires: a graceful exit
985/// returns from `run_daemon_process`, the process exits 0, and this task dies
986/// with the runtime. It exists so no unwinding step can hold the process open
987/// past the deadline its clients wait on, whatever the cause of the shutdown.
988pub(super) fn spawn_shutdown_watchdog() {
989    tokio::spawn(async move {
990        tokio::time::sleep(SHUTDOWN_FORCE_EXIT_TIMEOUT).await;
991        tracing::error!(
992            seconds = SHUTDOWN_FORCE_EXIT_TIMEOUT.as_secs(),
993            "daemon shutdown did not finish in time; exiting"
994        );
995        // The metadata file points clients at a process that is about to stop
996        // answering. Removing it is what the epilogue would have done.
997        if let Err(error) = fs::remove_file(metadata_path())
998            && error.kind() != std::io::ErrorKind::NotFound
999        {
1000            tracing::warn!(%error, "could not remove daemon metadata before the forced exit");
1001        }
1002        // 128 + signal is reserved for exits that really were signalled.
1003        std::process::exit(1);
1004    });
1005}
1006
1007pub(super) fn spawn_manager_target_refresher(
1008    targets: tokio::sync::watch::Sender<Vec<crate::session_manager::RelaySessionTarget>>,
1009    cancellation: CancellationToken,
1010    state: Arc<RuntimeState>,
1011) -> tokio::task::JoinHandle<()> {
1012    tokio::spawn(async move {
1013        let mut committed = state
1014            .committed
1015            .as_ref()
1016            .expect("daemon owns the writer")
1017            .clone();
1018        let mut revisions = state.revisions();
1019        let mut compatibility = tokio::time::interval(Duration::from_millis(500));
1020        compatibility.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
1021        let mut installed = None;
1022        let mut credential_installed = None;
1023        let mut auth = crate::pollers::ProviderAuthCache::default();
1024        let mut auth_profiles = None;
1025        let mut staged_sources = None;
1026        loop {
1027            let inputs = {
1028                let owner = state.owner();
1029                if let Err(error) = owner.ensure_available() {
1030                    tracing::error!(%error, "daemon durable state is unavailable; shutting down");
1031                    cancellation.cancel();
1032                    return;
1033                }
1034                owner.pollable_worker_inputs()
1035            };
1036            let config = inputs.controller().config;
1037            if auth_profiles.as_ref() != Some(&config.profiles) {
1038                let mut cache = auth.clone();
1039                let profiles = config.profiles.clone();
1040                match tokio::task::spawn_blocking(move || {
1041                    cache.refresh(&config);
1042                    cache
1043                })
1044                .await
1045                {
1046                    Ok(cache) => {
1047                        auth = cache;
1048                        auth_profiles = Some(profiles);
1049                    }
1050                    Err(error) => {
1051                        tracing::error!(%error, "provider refresh task stopped");
1052                        cancellation.cancel();
1053                        return;
1054                    }
1055                }
1056            }
1057            if !installed
1058                .as_ref()
1059                .is_some_and(|current| Arc::ptr_eq(current, &inputs))
1060            {
1061                let preparation = inputs.clone();
1062                let refreshed =
1063                    match tokio::task::spawn_blocking(move || preparation.prepare()).await {
1064                        Ok(targets) => targets,
1065                        Err(error) => {
1066                            tracing::error!(%error, "worker target preparation failed");
1067                            cancellation.cancel();
1068                            return;
1069                        }
1070                    };
1071                let retained = refreshed
1072                    .iter()
1073                    .map(|target| target.session_id.clone())
1074                    .collect::<BTreeSet<_>>();
1075                let views_dropped = {
1076                    let mut owner = state.owner();
1077                    // A lifecycle may have claimed a target while its commands
1078                    // were being prepared. Only the owner can authorize install.
1079                    if !Arc::ptr_eq(&owner.pollable_worker_inputs(), &inputs) {
1080                        continue;
1081                    }
1082                    targets.send_if_modified(|current| {
1083                        if *current == refreshed {
1084                            false
1085                        } else {
1086                            *current = refreshed;
1087                            true
1088                        }
1089                    });
1090                    // Same lock as the install, so a view is published only
1091                    // while its actor is still wanted.
1092                    owner.install_relay_sessions(retained.clone())
1093                };
1094                if views_dropped {
1095                    state.publish_revision();
1096                }
1097                state.review_host().retain_sessions(retained);
1098                installed = Some(inputs.clone());
1099            }
1100            let closing = state.owner().close_requested.clone();
1101            if !staged_sources
1102                .as_ref()
1103                .is_some_and(|(current, _)| Arc::ptr_eq(current, &inputs))
1104            {
1105                let staging = inputs.clone();
1106                match tokio::task::spawn_blocking(move || staging.staged_credentials()).await {
1107                    Ok(staged) => staged_sources = Some((inputs.clone(), staged)),
1108                    Err(error) => {
1109                        tracing::error!(%error, "credential staging observation stopped");
1110                        cancellation.cancel();
1111                        return;
1112                    }
1113                }
1114            }
1115            let staged = &staged_sources
1116                .as_ref()
1117                .expect("credential sources observed")
1118                .1;
1119            let credential_key = (
1120                inputs.clone(),
1121                auth.schemes.clone(),
1122                closing.clone(),
1123                staged.clone(),
1124            );
1125            if !credential_installed.as_ref().is_some_and(
1126                |(current, schemes, closed, previous_staged)| {
1127                    Arc::ptr_eq(current, &inputs)
1128                        && *schemes == auth.schemes
1129                        && *closed == closing
1130                        && previous_staged == staged
1131                },
1132            ) {
1133                let preparation = inputs.clone();
1134                let schemes = auth.schemes.clone();
1135                let staged = staged.clone();
1136                let prepared = match tokio::task::spawn_blocking(move || {
1137                    preparation.prepare_credentials(&schemes, &staged)
1138                })
1139                .await
1140                {
1141                    Ok(targets) => targets,
1142                    Err(error) => {
1143                        tracing::error!(%error, "credential target preparation stopped");
1144                        cancellation.cancel();
1145                        return;
1146                    }
1147                };
1148                if !state.owner().install_credentials(
1149                    &inputs,
1150                    &closing,
1151                    prepared,
1152                    &state.credential_targets,
1153                ) {
1154                    continue;
1155                }
1156                credential_installed = Some(credential_key);
1157            }
1158            tokio::select! {
1159                _ = cancellation.cancelled() => return,
1160                changed = committed.changed() => {
1161                    if changed.is_err() {
1162                        tracing::error!("database publication feed stopped");
1163                        cancellation.cancel();
1164                        return;
1165                    }
1166                    state.publish_revision();
1167                }
1168                changed = revisions.changed() => {
1169                    if changed.is_err() { return; }
1170                }
1171                _ = compatibility.tick() => {
1172                    let _config_mutation = state.config_mutation.lock().await;
1173                    let mut cache = auth.clone();
1174                    let staging = inputs.clone();
1175                    let refreshed = tokio::task::spawn_blocking(move || {
1176                        crate::database::check_read_compatibility()?;
1177                        let config = Config::load()?;
1178                        cache.refresh(&config);
1179                        let staged = staging.staged_credentials();
1180                        Ok::<_, anyhow::Error>((config, cache, staged))
1181                    }).await;
1182                    match refreshed {
1183                        Ok(Ok((config, cache, staged))) => {
1184                            staged_sources = Some((inputs.clone(), staged));
1185                            auth = cache;
1186                            auth_profiles = Some(config.profiles.clone());
1187                            state.review_config.lock().unwrap_or_else(PoisonError::into_inner)
1188                                .clone_from(&config.review);
1189                            let changed = {
1190                                let mut owner = state.owner();
1191                                let changed = owner.controller().config != config;
1192                                owner.install_config(config);
1193                                changed
1194                            };
1195                            if changed { state.publish_revision(); }
1196                        }
1197                        Ok(Err(error)) => {
1198                            if error.chain().any(|cause| cause.downcast_ref::<StoreSchemaMismatch>().is_some()) {
1199                                tracing::error!(%error, "daemon store schema diverged underneath the daemon; shutting down");
1200                                cancellation.cancel();
1201                                return;
1202                            }
1203                            tracing::warn!(error = format!("{error:#}"), "could not refresh daemon configuration");
1204                        }
1205                        Err(error) => {
1206                            tracing::error!(%error, "daemon configuration reader failed");
1207                            cancellation.cancel();
1208                            return;
1209                        }
1210                    }
1211                }
1212            }
1213        }
1214    })
1215}
1216
1217pub(super) async fn refresh_runtime_controller(state: &RuntimeState) {
1218    if let Err(error) = state.reload_controller().await {
1219        tracing::warn!(
1220            error = format!("{error:#}"),
1221            "could not refresh daemon controller state"
1222        );
1223    }
1224}