1use super::diagnostics::ServingPhase;
2use super::*;
3use std::time::Instant;
4
5pub(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 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 spawn_shutdown_watchdog();
39 }
40 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 let started = SystemTime::now();
67 let startup_work = crate::upgrade::activity("daemon startup recovery")?;
68 tokio::task::spawn_blocking(crate::controller::pin_worker_binary_sources)
71 .await
72 .context("worker source snapshot task failed")??;
73 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 {
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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 if result.deferred {
582 tracing::info!(session_id = %result.session_id, %error, "recovery copy deferred: agent is working");
583 } else if result.cancelled {
584 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 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 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 state.publish_session(update.session_id, update.view).await?;
690 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 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 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
881fn 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
894pub(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
920struct EpilogueClock {
926 began: Instant,
927 step: Instant,
928}
929
930impl EpilogueClock {
931 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 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
982pub(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 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 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 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 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}