1use std::{
2 collections::BTreeMap,
3 env,
4 error::Error,
5 ffi::OsString,
6 fmt, fs, io,
7 net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
8 path::{Path, PathBuf},
9 process,
10 time::Duration,
11};
12
13use fs4::{FileExt, TryLockError};
14use subc_protocol::PROTOCOL_VERSION;
15pub use subc_transport::user_connection_token;
16use subc_transport::{
17 authenticate_client, connection_file, generate_daemon_id, generate_key, write_atomic,
18 AuthError, ConnectionFileError, ConnectionInfo, Endpoint, SCHEMA_VERSION,
19};
20use tokio::{
21 net::{TcpListener, TcpStream},
22 task::{JoinError, JoinHandle},
23 time::{sleep, timeout},
24};
25use tracing::{error, info, warn};
26
27use crate::{
28 daemon_config::{self, ConfiguredModule, DaemonConfigError},
29 server::{serve_listeners, ServerAuth, ServerError},
30 supervise::HealthConfig,
31 ConnectedClients, ControlHandler, DaemonSelfWatchdog, DaemonSelfWatchdogConfig,
32 ForwardingTable, Registry, RestartPolicy, Router, Supervisor, SupervisorHandle,
33 SupervisorProcessLiveness,
34};
35use std::sync::Arc;
36
37pub const DEFAULT_SUBC_PORT: u16 = 8757;
38pub const SUBC_PORT_ENV: &str = "SUBC_PORT";
39use subc_transport::CONNECTION_FILE_NAME;
40const DAEMON_VERSION: &str = env!("CARGO_PKG_VERSION");
41const CONNECT_TIMEOUT: Duration = Duration::from_secs(2);
42const PROBE_AUTH_DEADLINE: Duration = Duration::from_secs(2);
43const START_LOCK_RETRIES: usize = 40;
44const START_LOCK_RETRY_DELAY: Duration = Duration::from_millis(25);
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum ConnectionFileSource {
48 XdgRuntimeDir,
49 TempDirFallback,
50 Explicit,
51}
52
53impl ConnectionFileSource {
54 fn reason(self) -> &'static str {
55 match self {
56 Self::XdgRuntimeDir => "XDG_RUNTIME_DIR set and non-empty",
57 Self::TempDirFallback => "XDG_RUNTIME_DIR unset or empty",
58 Self::Explicit => "configured path",
59 }
60 }
61}
62
63impl fmt::Display for ConnectionFileSource {
64 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
65 formatter.write_str(match self {
66 Self::XdgRuntimeDir => "xdg_runtime_dir",
67 Self::TempDirFallback => "temp_dir_fallback",
68 Self::Explicit => "explicit",
69 })
70 }
71}
72
73#[derive(Debug, Clone, Default)]
77struct AdmissionFactsConfig {
78 carrier_module_id: Option<String>,
79 targets: Option<Vec<String>>,
80}
81
82#[derive(Debug, Clone, Default, PartialEq, Eq)]
91pub enum CgroupPlacementConfig {
92 #[default]
93 Disabled,
94 Current,
95 Root(PathBuf),
96}
97
98#[derive(Debug, Clone)]
99pub struct BootstrapConfig {
100 pub operator_queue_wait: Duration,
101 pub operator_timeout: Duration,
102 pub operator_stuck_grace: Duration,
103 #[cfg(feature = "test-support")]
104 pub operator_script: Option<PathBuf>,
105 #[cfg(feature = "test-support")]
106 pub operator_events: Option<PathBuf>,
107 pub connection_file_path: PathBuf,
108 pub port: u16,
109 pub daemon_ver: String,
110 configured_modules: Vec<ConfiguredModule>,
111 storage_config: Option<daemon_config::StorageConfig>,
112 admission_facts: AdmissionFactsConfig,
113 scope_authority_owners: Vec<String>,
115 activity_config: daemon_config::ActivityConfig,
116 daemon_config_path: Option<PathBuf>,
117 configured_port: Option<u16>,
118 route_bind_relay_default_ms: Option<u64>,
122 reserved_capabilities: BTreeMap<String, String>,
123 watchdog_config: DaemonSelfWatchdogConfig,
124 connection_file_source: ConnectionFileSource,
125 cgroup_placement: CgroupPlacementConfig,
128 capture_logs_dir: Option<PathBuf>,
132 terminal_journal_path: Option<PathBuf>,
140 machine_id_path: Option<PathBuf>,
146 live_children_path: Option<PathBuf>,
154 privacy_trampoline: Option<PathBuf>,
155}
156
157impl BootstrapConfig {
158 pub fn new(connection_file_path: impl Into<PathBuf>, port: u16) -> Self {
159 Self {
160 connection_file_path: connection_file_path.into(),
161 operator_queue_wait: Duration::from_secs(150),
162 operator_timeout: Duration::from_secs(120),
163 operator_stuck_grace: Duration::from_secs(10),
164 #[cfg(feature = "test-support")]
165 operator_script: None,
166 #[cfg(feature = "test-support")]
167 operator_events: None,
168 port,
169 daemon_ver: DAEMON_VERSION.to_owned(),
170 configured_modules: Vec::new(),
171 storage_config: None,
172 admission_facts: AdmissionFactsConfig::default(),
173 scope_authority_owners: daemon_config::default_scope_authority_owners(),
174 activity_config: daemon_config::ActivityConfig::default(),
175 daemon_config_path: None,
176 configured_port: None,
177 route_bind_relay_default_ms: None,
178 reserved_capabilities: BTreeMap::new(),
179 watchdog_config: DaemonSelfWatchdogConfig::default(),
180 connection_file_source: ConnectionFileSource::Explicit,
181 cgroup_placement: CgroupPlacementConfig::default(),
182 capture_logs_dir: None,
183 terminal_journal_path: None,
184 machine_id_path: None,
185 live_children_path: None,
186 privacy_trampoline: None,
187 }
188 }
189
190 pub fn with_live_children_record(mut self, path: impl Into<PathBuf>) -> Self {
194 self.live_children_path = Some(path.into());
195 self
196 }
197
198 pub fn with_privacy_trampoline(mut self, path: impl Into<PathBuf>) -> Self {
201 self.privacy_trampoline = Some(path.into());
202 self
203 }
204
205 pub fn with_machine_id_path(mut self, path: impl Into<PathBuf>) -> Self {
209 self.machine_id_path = Some(path.into());
210 self
211 }
212
213 pub fn with_cgroup_placement(mut self, placement: CgroupPlacementConfig) -> Self {
215 self.cgroup_placement = placement;
216 self
217 }
218
219 pub fn with_capture_logs_dir(mut self, dir: impl Into<PathBuf>) -> Self {
223 self.capture_logs_dir = Some(dir.into());
224 self
225 }
226
227 pub fn with_terminal_journal_path(mut self, path: impl Into<PathBuf>) -> Self {
230 self.terminal_journal_path = Some(path.into());
231 self
232 }
233
234 pub fn from_env() -> Result<Self, BootstrapError> {
235 Self::from_env_with_daemon_config_path(daemon_config::default_config_path())
236 }
237
238 pub fn from_env_for_daemon_binary() -> Result<Self, BootstrapError> {
247 let run_dir = daemon_config::daemon_run_dir().map_err(BootstrapError::RunDir)?;
248 let machine_id_path =
249 crate::machine_id::default_machine_id_path().map_err(BootstrapError::MachineId)?;
250 let config = Self::from_env()?
251 .with_capture_logs_dir(run_dir.join("logs"))
252 .with_terminal_journal_path(run_dir.join("terminals.jsonl"))
253 .with_live_children_record(crate::live_children::record_path(&run_dir))
254 .with_machine_id_path(machine_id_path);
255 #[cfg(feature = "test-support")]
256 let config = {
257 let mut config = config;
258 config.operator_script =
259 std::env::var_os("SUBC_TEST_OPERATOR_SCRIPT").map(PathBuf::from);
260 config.operator_events =
261 std::env::var_os("SUBC_TEST_OPERATOR_EVENTS").map(PathBuf::from);
262 for (name, value) in [
263 (
264 "SUBC_TEST_OPERATOR_QUEUE_WAIT_MS",
265 &mut config.operator_queue_wait,
266 ),
267 (
268 "SUBC_TEST_OPERATOR_TIMEOUT_MS",
269 &mut config.operator_timeout,
270 ),
271 (
272 "SUBC_TEST_OPERATOR_STUCK_MS",
273 &mut config.operator_stuck_grace,
274 ),
275 ] {
276 if let Ok(ms) = std::env::var(name) {
277 if let Ok(ms) = ms.parse::<u64>() {
278 *value = Duration::from_millis(ms);
279 }
280 }
281 }
282 config
283 };
284 Ok(config)
285 }
286
287 pub fn from_env_with_daemon_config_path(
288 daemon_config_path: impl AsRef<Path>,
289 ) -> Result<Self, BootstrapError> {
290 let daemon_config_path = daemon_config_path.as_ref().to_path_buf();
291 let (daemon_config, activity_config) =
292 daemon_config::load_with_activity(&daemon_config_path)
293 .map_err(BootstrapError::DaemonConfig)?
294 .map(|(config, activity)| (Some(config), activity))
295 .unwrap_or_default();
296 let config_port = daemon_config.as_ref().and_then(|config| config.port);
297 let storage_config = daemon_config
298 .as_ref()
299 .and_then(|config| config.storage.clone());
300 let admission_facts_carrier_module_id = daemon_config
301 .as_ref()
302 .and_then(|config| config.admission_facts_carrier_module_id.clone());
303 let admission_facts_targets = daemon_config
304 .as_ref()
305 .and_then(|config| config.admission_facts_targets.clone());
306 let scope_authority_owners = daemon_config
307 .as_ref()
308 .map(|config| config.scope_authority_owners.clone())
309 .unwrap_or_else(daemon_config::default_scope_authority_owners);
310 let route_bind_relay_default_ms = daemon_config
311 .as_ref()
312 .and_then(|config| config.route_bind_relay_timeout_ms);
313 let reserved_capabilities = daemon_config
314 .as_ref()
315 .map(|config| config.reserved_capabilities.clone())
316 .unwrap_or_default();
317 let configured_modules = daemon_config
318 .map(|config| config.modules)
319 .unwrap_or_default();
320
321 let port = match env::var(SUBC_PORT_ENV) {
322 Ok(raw) if !raw.trim().is_empty() => {
323 let port = raw
324 .parse::<u16>()
325 .map_err(|source| BootstrapError::InvalidPort { raw, source })?;
326 if let Some(config_port) = config_port {
327 info!(
328 env = SUBC_PORT_ENV,
329 env_port = port,
330 config_port,
331 "SUBC_PORT overrides daemon config port"
332 );
333 }
334 port
335 }
336 Ok(_) | Err(_) => config_port.unwrap_or(DEFAULT_SUBC_PORT),
337 };
338
339 let (connection_file_path, connection_file_source) =
340 connection_file_path_with_source(non_empty_os_var("XDG_RUNTIME_DIR"));
341 Ok(Self::new(connection_file_path, port)
342 .with_configured_modules(configured_modules)
343 .with_storage_config(storage_config)
344 .with_admission_facts_config(admission_facts_carrier_module_id, admission_facts_targets)
345 .with_scope_authority_owners(scope_authority_owners)
346 .with_activity_config(activity_config)
347 .with_route_bind_relay_default_ms(route_bind_relay_default_ms)
348 .with_reserved_capabilities(reserved_capabilities)
349 .with_daemon_config_source(daemon_config_path, config_port)
350 .with_connection_file_source(connection_file_source))
351 }
352
353 pub fn with_daemon_config_path(
354 self,
355 daemon_config_path: impl AsRef<Path>,
356 ) -> Result<Self, BootstrapError> {
357 let daemon_config_path = daemon_config_path.as_ref().to_path_buf();
358 let (daemon_config, activity_config) =
359 daemon_config::load_with_activity(&daemon_config_path)
360 .map_err(BootstrapError::DaemonConfig)?
361 .map(|(config, activity)| (Some(config), activity))
362 .unwrap_or_default();
363 let configured_port = daemon_config.as_ref().and_then(|config| config.port);
364 let storage_config = daemon_config
365 .as_ref()
366 .and_then(|config| config.storage.clone());
367 let admission_facts_carrier_module_id = daemon_config
368 .as_ref()
369 .and_then(|config| config.admission_facts_carrier_module_id.clone());
370 let admission_facts_targets = daemon_config
371 .as_ref()
372 .and_then(|config| config.admission_facts_targets.clone());
373 let scope_authority_owners = daemon_config
374 .as_ref()
375 .map(|config| config.scope_authority_owners.clone())
376 .unwrap_or_else(daemon_config::default_scope_authority_owners);
377 let route_bind_relay_default_ms = daemon_config
378 .as_ref()
379 .and_then(|config| config.route_bind_relay_timeout_ms);
380 let reserved_capabilities = daemon_config
381 .as_ref()
382 .map(|config| config.reserved_capabilities.clone())
383 .unwrap_or_default();
384 let configured_modules = daemon_config
385 .map(|config| config.modules)
386 .unwrap_or_default();
387 Ok(self
388 .with_configured_modules(configured_modules)
389 .with_storage_config(storage_config)
390 .with_admission_facts_config(admission_facts_carrier_module_id, admission_facts_targets)
391 .with_scope_authority_owners(scope_authority_owners)
392 .with_activity_config(activity_config)
393 .with_route_bind_relay_default_ms(route_bind_relay_default_ms)
394 .with_reserved_capabilities(reserved_capabilities)
395 .with_daemon_config_source(daemon_config_path, configured_port))
396 }
397
398 pub fn with_configured_modules(
399 mut self,
400 modules: impl IntoIterator<Item = ConfiguredModule>,
401 ) -> Self {
402 self.configured_modules = modules.into_iter().collect();
403 self.configured_modules
404 .sort_by(|left, right| left.module_id.cmp(&right.module_id));
405 self
406 }
407
408 pub fn with_storage_config(
409 mut self,
410 storage_config: Option<daemon_config::StorageConfig>,
411 ) -> Self {
412 self.storage_config = storage_config;
413 self
414 }
415
416 pub fn with_admission_facts_config(
417 mut self,
418 carrier_module_id: Option<String>,
419 targets: Option<Vec<String>>,
420 ) -> Self {
421 self.admission_facts = AdmissionFactsConfig {
422 carrier_module_id,
423 targets,
424 };
425 self
426 }
427
428 pub fn with_scope_authority_owners(mut self, owners: Vec<String>) -> Self {
431 self.scope_authority_owners = owners;
432 self
433 }
434
435 pub fn with_activity_config(mut self, config: daemon_config::ActivityConfig) -> Self {
436 self.activity_config = config;
437 self
438 }
439
440 pub fn with_route_bind_relay_default_ms(mut self, ms: Option<u64>) -> Self {
445 self.route_bind_relay_default_ms = ms;
446 self
447 }
448
449 pub fn with_reserved_capabilities(
450 mut self,
451 reserved_capabilities: BTreeMap<String, String>,
452 ) -> Self {
453 self.reserved_capabilities = reserved_capabilities;
454 self
455 }
456
457 fn with_daemon_config_source(
458 mut self,
459 daemon_config_path: PathBuf,
460 configured_port: Option<u16>,
461 ) -> Self {
462 self.daemon_config_path = Some(daemon_config_path);
463 self.configured_port = configured_port;
464 self
465 }
466
467 fn with_connection_file_source(mut self, source: ConnectionFileSource) -> Self {
468 self.connection_file_source = source;
469 self
470 }
471
472 pub fn with_watchdog_config(mut self, watchdog_config: DaemonSelfWatchdogConfig) -> Self {
473 self.watchdog_config = watchdog_config;
474 self
475 }
476}
477
478#[allow(clippy::large_enum_variant)]
482#[derive(Debug)]
483pub enum Outcome {
484 AlreadyRunning,
486 Bound(BoundDaemon),
489}
490
491#[derive(Debug)]
492pub struct BoundDaemon {
493 pub listeners: Vec<TcpListener>,
494 pub connection_info: ConnectionInfo,
495 pub connection_file_path: PathBuf,
496 pub connection_file_source: ConnectionFileSource,
497 pub machine_id: Option<crate::machine_id::MachineId>,
500 run_dir_lock: Option<crate::run_dir_lock::RunDirLock>,
504 publication_lock: Option<StartLock>,
510}
511
512pub fn connection_file_path() -> PathBuf {
519 connection_file_path_with_source(non_empty_os_var("XDG_RUNTIME_DIR")).0
520}
521
522fn connection_file_path_with_source(
523 runtime_dir: Option<OsString>,
524) -> (PathBuf, ConnectionFileSource) {
525 if let Some(runtime_dir) = runtime_dir.filter(|value| !value.is_empty()) {
526 return (
527 PathBuf::from(runtime_dir).join(CONNECTION_FILE_NAME),
528 ConnectionFileSource::XdgRuntimeDir,
529 );
530 }
531
532 (
533 env::temp_dir().join(format!("subc-{}.connection.json", user_connection_token())),
534 ConnectionFileSource::TempDirFallback,
535 )
536}
537
538pub async fn run() -> Result<(), BootstrapError> {
544 run_with_config(
547 BootstrapConfig::from_env_for_daemon_binary()?
548 .with_cgroup_placement(CgroupPlacementConfig::Current),
549 )
550 .await
551}
552
553pub async fn run_with_config(config: BootstrapConfig) -> Result<(), BootstrapError> {
577 let forwarding = Arc::new(ForwardingTable::default());
578 forwarding.operator_confirms().configure_bootstrap(&config);
579 let configured_modules = config.configured_modules.clone();
580 let storage_config = config.storage_config.clone();
581 let admission_facts = config.admission_facts.clone();
582 let scope_authority_owners = config.scope_authority_owners.clone();
583 let activity_config = config.activity_config;
584 let daemon_config_path = config.daemon_config_path.clone();
585 let configured_port = config.configured_port;
586 let route_bind_relay_default_ms = config.route_bind_relay_default_ms;
587 let reserved_capabilities = config.reserved_capabilities.clone();
588 let watchdog_config = config.watchdog_config.clone();
589 let cgroup_placement_config = config.cgroup_placement.clone();
590 let capture_logs_dir = config.capture_logs_dir.clone();
591 let terminal_journal_path = config.terminal_journal_path.clone();
592 let live_children_path = config.live_children_path.clone();
593 let privacy_trampoline = config.privacy_trampoline.clone();
594 match ensure_singleton_inner(config, false).await? {
595 Outcome::AlreadyRunning => {
596 info!("subc daemon already running");
597 Ok(())
598 }
599 Outcome::Bound(bound) => {
600 #[cfg(target_os = "linux")]
601 let cgroup_placement = prepare_cgroup_placement(&cgroup_placement_config);
602 #[cfg(not(target_os = "linux"))]
603 let _ = cgroup_placement_config;
604 serve_bound_daemon(
605 bound,
606 forwarding,
607 configured_modules,
608 storage_config,
609 admission_facts,
610 scope_authority_owners,
611 activity_config,
612 daemon_config_path,
613 configured_port,
614 route_bind_relay_default_ms,
615 reserved_capabilities,
616 watchdog_config,
617 capture_logs_dir,
618 terminal_journal_path,
619 live_children_path,
620 privacy_trampoline,
621 #[cfg(target_os = "linux")]
622 cgroup_placement,
623 )
624 .await
625 }
626 }
627}
628
629#[cfg(target_os = "linux")]
630fn prepare_cgroup_placement(config: &CgroupPlacementConfig) -> Option<subc_cgroup::Placement> {
631 let result = match config {
632 CgroupPlacementConfig::Disabled => return None,
633 CgroupPlacementConfig::Current => subc_cgroup::prepare_current(),
634 CgroupPlacementConfig::Root(root) => subc_cgroup::prepare_at(root),
635 };
636
637 match result {
638 Ok(Some(placement)) => Some(placement),
639 Ok(None) => {
640 warn!(
641 placement = ?config,
642 "module cgroup placement is disabled: configured cgroup root is not delegated"
643 );
644 None
645 }
646 Err(error) => {
647 warn!(
648 placement = ?config,
649 error = %error,
650 "module cgroup placement is disabled by an unexpected cgroup probe error"
651 );
652 None
653 }
654 }
655}
656
657#[cfg(unix)]
664const NOFILE_TARGET: u64 = 65536;
665
666#[cfg(unix)]
670fn raise_nofile_limit() {
671 #[cfg(target_os = "macos")]
672 let ceiling = std::process::Command::new("/usr/sbin/sysctl")
673 .args(["-n", "kern.maxfilesperproc"])
674 .output()
675 .ok()
676 .filter(|output| output.status.success())
677 .and_then(|output| String::from_utf8(output.stdout).ok())
678 .and_then(|value| value.trim().parse::<u64>().ok())
679 .filter(|value| *value > 0);
680 #[cfg(not(target_os = "macos"))]
681 let ceiling = None;
682 raise_nofile_limit_with_ceiling(ceiling);
683}
684
685#[cfg(unix)]
686fn raise_nofile_limit_with_ceiling(kernel_ceiling: Option<u64>) {
687 match rlimit::Resource::NOFILE.get() {
688 Ok((soft, hard)) => {
689 let target = NOFILE_TARGET
693 .min(hard)
694 .min(kernel_ceiling.unwrap_or(u64::MAX));
695 if soft >= target {
696 return;
697 }
698 match rlimit::Resource::NOFILE.set(target, hard) {
699 Ok(()) => info!(
700 previous_soft = soft,
701 new_soft = target,
702 hard,
703 "raised open-file soft limit for daemon and module children"
704 ),
705 Err(err) => warn!(
706 soft,
707 hard,
708 error = %err,
709 "could not raise open-file soft limit; multi-root modules may exhaust descriptors"
710 ),
711 }
712 }
713 Err(err) => warn!(error = %err, "could not read open-file limit"),
714 }
715}
716
717#[cfg(windows)]
724fn raise_nofile_limit() {
725 const MAXSTDIO_TARGET: u32 = 8192;
726 let current = rlimit::getmaxstdio();
727 if current >= MAXSTDIO_TARGET {
728 return;
729 }
730 match rlimit::setmaxstdio(MAXSTDIO_TARGET) {
731 Ok(new_max) => info!(
732 previous = current,
733 new_max, "raised CRT stdio-stream limit for daemon"
734 ),
735 Err(err) => warn!(
736 current,
737 error = %err,
738 "could not raise CRT stdio-stream limit"
739 ),
740 }
741}
742
743#[cfg(not(any(unix, windows)))]
744fn raise_nofile_limit() {}
745
746pub async fn run_with_daemon_config_path(
747 config: BootstrapConfig,
748 daemon_config_path: impl AsRef<Path>,
749) -> Result<(), BootstrapError> {
750 run_with_config(config.with_daemon_config_path(daemon_config_path)?).await
751}
752
753#[allow(clippy::too_many_arguments)]
754async fn serve_bound_daemon(
755 bound: BoundDaemon,
756 forwarding: Arc<ForwardingTable>,
757 configured_modules: Vec<ConfiguredModule>,
758 storage_config: Option<daemon_config::StorageConfig>,
759 admission_facts: AdmissionFactsConfig,
760 scope_authority_owners: Vec<String>,
761 activity_config: daemon_config::ActivityConfig,
762 daemon_config_path: Option<PathBuf>,
763 configured_port: Option<u16>,
764 route_bind_relay_default_ms: Option<u64>,
765 reserved_capabilities: BTreeMap<String, String>,
766 watchdog_config: DaemonSelfWatchdogConfig,
767 capture_logs_dir: Option<PathBuf>,
768 terminal_journal_path: Option<PathBuf>,
769 live_children_path: Option<PathBuf>,
770 privacy_trampoline: Option<PathBuf>,
771 #[cfg(target_os = "linux")] cgroup_placement: Option<subc_cgroup::Placement>,
772) -> Result<(), BootstrapError> {
773 #[cfg(unix)]
774 let mut terminate = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
775 .map_err(BootstrapError::Signal)?;
776 raise_nofile_limit();
779
780 info!(
781 connection_file = %bound.connection_file_path.display(),
782 connection_file_source = %bound.connection_file_source,
783 connection_file_source_reason = bound.connection_file_source.reason(),
784 endpoints = ?bound.connection_info.endpoints,
785 configured_modules = configured_modules.len(),
786 machine_id = bound.machine_id.as_ref().map(|id| id.as_str()).unwrap_or("none"),
787 "subc daemon starting"
788 );
789
790 let run_dir_lock = bound.run_dir_lock;
801 let publication_lock = bound.publication_lock;
802 if let Some(owner) = &run_dir_lock {
803 crate::live_children::sweep_orphans(
804 owner,
805 &crate::live_children::AdoptedPids::none(),
806 crate::live_children::SweepBounds::default(),
807 )
808 .await;
809 }
810
811 let registry = Arc::new(Registry::default());
812 let process_liveness = Arc::new(SupervisorProcessLiveness::new());
813 let supervisor_handle = SupervisorHandle::new();
814 let connected_clients = ConnectedClients::new();
815 let daemon_incarnation = format!(
816 "{:032x}",
817 u128::from_be_bytes(bound.connection_info.daemon_id)
818 );
819 let supervisor = Supervisor::new(Arc::clone(®istry), RestartPolicy::default())
820 .with_process_liveness(process_liveness.clone())
821 .with_forwarding(Arc::clone(&forwarding))
822 .with_handle(supervisor_handle.clone())
823 .with_connection_file_path(bound.connection_file_path.clone())
824 .with_daemon_incarnation(daemon_incarnation.clone());
825 let supervisor = match privacy_trampoline {
826 Some(path) => supervisor.with_privacy_trampoline(path),
827 None => supervisor,
828 };
829 let activity_run_dir = terminal_journal_path
869 .as_ref()
870 .and_then(|path| path.parent())
871 .map(Path::to_path_buf);
872 let supervisor = match terminal_journal_path {
873 Some(path) => supervisor.with_terminal_journal(path, daemon_incarnation.clone()),
874 None => supervisor,
875 };
876 let supervisor = match capture_logs_dir {
877 Some(dir) => supervisor.with_capture_logs_dir(dir),
878 None => supervisor,
879 };
880 let supervisor = match live_children_path {
881 Some(path) => supervisor.with_live_children_record(path),
882 None => supervisor,
883 };
884 #[cfg(target_os = "linux")]
885 let supervisor = supervisor.with_cgroup_placement(cgroup_placement);
886 let route_bind_relay_timeouts = configured_modules
892 .iter()
893 .filter_map(|module| {
894 module
895 .route_bind_relay_timeout_ms
896 .map(|ms| (module.module_id.clone(), Duration::from_millis(ms)))
897 })
898 .collect::<std::collections::BTreeMap<_, _>>();
899 let control_start_clock = crate::clock::StartClock::capture();
900 let mut control = ControlHandler::with_forwarding(Arc::clone(®istry), forwarding)
901 .with_process_liveness(process_liveness)
902 .with_supervisor(supervisor_handle)
903 .with_connected_clients(connected_clients.clone())
904 .with_storage_config(storage_config)
905 .with_machine_id(bound.machine_id.clone())
906 .with_admission_facts_config(admission_facts.carrier_module_id, admission_facts.targets)
907 .with_scope_authority_owners(scope_authority_owners)
908 .with_route_bind_relay_timeouts(route_bind_relay_timeouts)
909 .with_daemon_provenance(
910 bound.connection_info.pid,
911 control_start_clock.started_at_ms(),
912 std::env::current_exe().ok(),
913 normalized_build_provenance(env!("SUBC_BUILD_GIT_SHA")),
914 normalized_build_provenance(env!("SUBC_BUILD_LOCK_DIGEST")),
915 )
916 .with_daemon_start_clock(control_start_clock)
917 .with_capability_config(
918 configured_modules
919 .iter()
920 .map(|module| (module.module_id.clone(), module.enabled)),
921 reserved_capabilities,
922 );
923 if let Some(run_dir) = activity_run_dir {
924 control =
925 control.with_activity_ledger_config(&run_dir, daemon_incarnation, activity_config);
926 }
927 if let Some(ms) = route_bind_relay_default_ms {
928 control = control.with_route_bind_relay_timeout(Duration::from_millis(ms));
931 }
932 if let Some(config_path) = daemon_config_path {
933 control = control.with_supervisor_rescan(supervisor.clone(), config_path, configured_port);
934 }
935 let control = Arc::new(control);
936 let router = Arc::new(Router::with_control_handler(Arc::clone(&control)));
937 let auth = ServerAuth::new(
938 bound.connection_info.key.clone(),
939 bound.connection_info.daemon_id,
940 bound.connection_info.daemon_ver.clone(),
941 )
942 .with_connected_clients(connected_clients);
943
944 let mut serve_task =
945 AbortOnDrop::new(tokio::spawn(serve_listeners(bound.listeners, router, auth)));
946 verify_serving(&bound.connection_info).await?;
947 write_atomic(&bound.connection_file_path, &bound.connection_info).map_err(|source| {
951 BootstrapError::ConnectionFileWrite {
952 path: bound.connection_file_path.clone(),
953 source,
954 }
955 })?;
956 drop(publication_lock);
957 let _clock_step_task = AbortOnDrop::new(crate::watchdog::spawn_clock_step_monitor());
958 #[cfg(target_os = "linux")]
960 let _kill_mode_check = AbortOnDrop::new(tokio::spawn(
961 crate::systemd_kill_mode::warn_if_kill_mode_defeats_ordered_shutdown(),
962 ));
963 let _watchdog_task = AbortOnDrop::new(
964 DaemonSelfWatchdog::new(
965 bound.connection_info.clone(),
966 bound.connection_file_path.clone(),
967 )
968 .with_config(watchdog_config)
969 .spawn(),
970 );
971
972 for configured in configured_modules {
973 let enabled = configured.enabled;
974 let health = configured.health.clone();
975 let module_id = configured.module_id.clone();
976 match supervisor.supervise_configured_with_health(
977 configured.module_spec(),
978 enabled,
979 health.clone(),
980 configured.drain_timeout_ms,
981 configured.restart,
982 ) {
983 Ok(_) => {
984 let default_threshold = HealthConfig::default().failure_threshold;
993 if enabled && health.failure_threshold > default_threshold {
994 warn!(
995 module_id = %module_id,
996 failure_threshold = health.failure_threshold,
997 default_threshold,
998 tolerance_secs = health.cadence.as_secs() * u64::from(health.failure_threshold),
999 "health failure threshold is relaxed above the default; a wedged module stays unflagged for longer"
1000 );
1001 }
1002 info!(module_id = %module_id, enabled, "configured module supervised");
1003 }
1004 Err(err) => {
1005 error!(module_id = %module_id, error = %err, "failed to supervise configured module; continuing daemon startup");
1006 }
1007 }
1008 }
1009
1010 control.refresh_capability_requirements();
1011 Arc::clone(&control).spawn_capability_deadline_loop();
1012 Arc::clone(&control).spawn_scope_expiry_loop();
1013
1014 #[cfg(unix)]
1015 {
1016 tokio::select! {
1017 result = serve_task.join() => {
1018 return result.map_err(BootstrapError::ServeJoin)?.map_err(BootstrapError::Serve);
1019 }
1020 _ = terminate.recv() => {}
1021 }
1022 supervisor.begin_daemon_shutdown();
1027 drop(_watchdog_task);
1031 drop(serve_task);
1034 let escalated = tokio::select! {
1035 biased;
1036 _ = terminate.recv() => {
1037 info!("second SIGTERM: abandoning daemon shutdown wait");
1038 true
1039 }
1040 result = supervisor.drain_for_daemon_shutdown() => {
1041 if let Err(error) = result {
1042 warn!(%error, "daemon shutdown drain failed; exiting anyway");
1043 }
1044 false
1045 }
1046 };
1047 supervisor
1054 .end_children_for_daemon_shutdown(escalated, async {
1055 terminate.recv().await;
1056 })
1057 .await;
1058 Ok(())
1059 }
1060 #[cfg(not(unix))]
1061 serve_task
1062 .join()
1063 .await
1064 .map_err(BootstrapError::ServeJoin)?
1065 .map_err(BootstrapError::Serve)
1066}
1067
1068fn normalized_build_provenance(value: &str) -> Option<String> {
1069 match value.trim() {
1070 "" | "unavailable" => None,
1071 value => Some(value.to_string()),
1072 }
1073}
1074
1075pub async fn ensure_singleton(
1083 connection_file_path: impl AsRef<Path>,
1084 port: u16,
1085) -> Result<Outcome, BootstrapError> {
1086 ensure_singleton_with_config(BootstrapConfig::new(connection_file_path.as_ref(), port)).await
1087}
1088
1089pub async fn ensure_singleton_with_config(
1090 config: BootstrapConfig,
1091) -> Result<Outcome, BootstrapError> {
1092 ensure_singleton_inner(config, true).await
1093}
1094
1095async fn ensure_singleton_inner(
1096 config: BootstrapConfig,
1097 publish: bool,
1098) -> Result<Outcome, BootstrapError> {
1099 let path = config.connection_file_path;
1100
1101 if matches!(probe_existing(&path).await?, Probe::Live) {
1102 return Ok(Outcome::AlreadyRunning);
1103 }
1104
1105 let lock = StartLock::acquire(&path).await?;
1106
1107 if matches!(probe_existing(&path).await?, Probe::Live) {
1111 return Ok(Outcome::AlreadyRunning);
1112 }
1113
1114 let run_dir_lock = config
1120 .live_children_path
1121 .as_deref()
1122 .map(crate::run_dir_lock::RunDirLock::acquire)
1123 .transpose()?;
1124
1125 remove_stale_connection_file_if_present(&path)?;
1126
1127 let machine_id = config
1131 .machine_id_path
1132 .as_deref()
1133 .map(crate::machine_id::load_or_mint)
1134 .transpose()
1135 .map_err(BootstrapError::MachineId)?;
1136
1137 let (listeners, endpoints) = bind_loopback(config.port).await?;
1138 let connection_info = ConnectionInfo {
1139 schema: SCHEMA_VERSION,
1140 wire_version: Some(PROTOCOL_VERSION),
1141 endpoints,
1142 key: generate_key().map_err(BootstrapError::GenerateConnectionFile)?,
1143 daemon_id: generate_daemon_id().map_err(BootstrapError::GenerateConnectionFile)?,
1144 pid: process::id(),
1145 daemon_ver: config.daemon_ver,
1146 };
1147
1148 if publish {
1152 if let Err(source) = write_atomic(&path, &connection_info) {
1153 drop(listeners);
1154 return Err(BootstrapError::ConnectionFileWrite { path, source });
1155 }
1156 }
1157
1158 Ok(Outcome::Bound(BoundDaemon {
1159 listeners,
1160 connection_info,
1161 connection_file_path: path,
1162 connection_file_source: config.connection_file_source,
1163 machine_id,
1164 run_dir_lock,
1165 publication_lock: (!publish).then_some(lock),
1166 }))
1167}
1168
1169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1170enum Probe {
1171 Live,
1172 StaleOrAbsent,
1173}
1174
1175async fn probe_existing(path: &Path) -> Result<Probe, BootstrapError> {
1176 let info = match connection_file::read(path) {
1177 Ok(info) => info,
1178 Err(source) if is_absent_or_stale_connection_file(&source) => {
1179 return Ok(Probe::StaleOrAbsent)
1180 }
1181 Err(source) => {
1182 return Err(BootstrapError::ConnectionFileRead {
1183 path: path.to_path_buf(),
1184 source,
1185 })
1186 }
1187 };
1188
1189 for endpoint in &info.endpoints {
1190 if matches!(probe_endpoint(&info, endpoint).await, Probe::Live) {
1191 return Ok(Probe::Live);
1192 }
1193 }
1194
1195 Ok(Probe::StaleOrAbsent)
1196}
1197
1198async fn probe_endpoint(info: &ConnectionInfo, endpoint: &Endpoint) -> Probe {
1199 let Ok(ip) = endpoint.host.parse::<IpAddr>() else {
1200 return Probe::StaleOrAbsent;
1201 };
1202 if !ip.is_loopback() {
1203 return Probe::StaleOrAbsent;
1204 }
1205 let addr = SocketAddr::new(ip, endpoint.port);
1206
1207 let mut stream = match timeout(CONNECT_TIMEOUT, TcpStream::connect(addr)).await {
1208 Ok(Ok(stream)) => stream,
1209 Ok(Err(_)) | Err(_) => return Probe::StaleOrAbsent,
1210 };
1211
1212 match authenticate_client(&mut stream, info, PROBE_AUTH_DEADLINE).await {
1213 Ok(()) => Probe::Live,
1214 Err(AuthError::DaemonIdMismatch)
1215 | Err(AuthError::InvalidServerProof)
1216 | Err(AuthError::UnexpectedEof { .. })
1217 | Err(AuthError::Timeout { .. })
1218 | Err(AuthError::JsonEncode { .. })
1219 | Err(AuthError::JsonDecode { .. })
1220 | Err(AuthError::Io { .. })
1221 | Err(AuthError::MessageTooLarge { .. })
1222 | Err(AuthError::KeyTooShort { .. })
1223 | Err(AuthError::Random(_))
1224 | Err(AuthError::InvalidClientAuth) => Probe::StaleOrAbsent,
1225 }
1226}
1227
1228async fn verify_serving(info: &ConnectionInfo) -> Result<(), BootstrapError> {
1229 if let Some(endpoint) = info.endpoints.first() {
1233 if matches!(probe_endpoint(info, endpoint).await, Probe::Live) {
1234 return Ok(());
1235 }
1236 }
1237 Err(BootstrapError::StartupNotServing)
1238}
1239
1240fn is_absent_or_stale_connection_file(err: &ConnectionFileError) -> bool {
1241 match err {
1242 ConnectionFileError::Io { source, .. } if source.kind() == io::ErrorKind::NotFound => true,
1243 ConnectionFileError::JsonRead { .. }
1244 | ConnectionFileError::UnsupportedSchema { .. }
1245 | ConnectionFileError::Invalid { .. }
1246 | ConnectionFileError::KeyTooShort { .. }
1247 | ConnectionFileError::InsecurePermissions { .. } => true,
1251 ConnectionFileError::MissingParent { .. }
1252 | ConnectionFileError::MissingFileName { .. }
1253 | ConnectionFileError::InsecureParentDirectory { .. }
1257 | ConnectionFileError::Io { .. }
1258 | ConnectionFileError::JsonWrite { .. }
1259 | ConnectionFileError::Random(_)
1260 | ConnectionFileError::WireVersionMismatch { .. } => false,
1263 }
1264}
1265
1266fn remove_stale_connection_file_if_present(path: &Path) -> Result<(), BootstrapError> {
1267 match fs::remove_file(path) {
1268 Ok(()) => Ok(()),
1269 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()),
1270 Err(source) => Err(BootstrapError::RemoveStale {
1271 path: path.to_path_buf(),
1272 source,
1273 }),
1274 }
1275}
1276
1277async fn bind_loopback(port: u16) -> Result<(Vec<TcpListener>, Vec<Endpoint>), BootstrapError> {
1278 bind_loopback_with(port, |port| TcpListener::bind((Ipv6Addr::LOCALHOST, port))).await
1279}
1280
1281async fn bind_loopback_with<F, Fut>(
1282 port: u16,
1283 mut bind_v6: F,
1284) -> Result<(Vec<TcpListener>, Vec<Endpoint>), BootstrapError>
1285where
1286 F: FnMut(u16) -> Fut,
1287 Fut: std::future::Future<Output = io::Result<TcpListener>>,
1288{
1289 let v4_host = Ipv4Addr::LOCALHOST;
1290 let v4 = TcpListener::bind((v4_host, port))
1291 .await
1292 .map_err(|source| BootstrapError::Bind {
1293 host: v4_host.to_string(),
1294 port,
1295 source,
1296 })?;
1297 let actual_port = v4
1298 .local_addr()
1299 .map_err(|source| BootstrapError::LocalAddr {
1300 host: v4_host.to_string(),
1301 source,
1302 })?
1303 .port();
1304
1305 let mut listeners = vec![v4];
1306 let mut endpoints = vec![Endpoint {
1307 host: v4_host.to_string(),
1308 port: actual_port,
1309 }];
1310
1311 let v6_host = Ipv6Addr::LOCALHOST;
1312 match bind_v6(actual_port).await {
1313 Ok(v6) => {
1314 listeners.push(v6);
1315 endpoints.push(Endpoint {
1316 host: v6_host.to_string(),
1317 port: actual_port,
1318 });
1319 }
1320 Err(err)
1321 if ipv6_loopback_unavailable(&err)
1322 || (port == 0 && err.kind() == io::ErrorKind::AddrInUse) =>
1323 {
1324 warn!(
1325 port = actual_port,
1326 error = %err,
1327 "IPv6 loopback unavailable; serving only IPv4 loopback"
1328 );
1329 }
1330 Err(source) => {
1331 drop(listeners);
1332 return Err(BootstrapError::Bind {
1333 host: v6_host.to_string(),
1334 port: actual_port,
1335 source,
1336 });
1337 }
1338 }
1339
1340 Ok((listeners, endpoints))
1341}
1342
1343fn ipv6_loopback_unavailable(err: &io::Error) -> bool {
1344 matches!(
1345 err.kind(),
1346 io::ErrorKind::AddrNotAvailable | io::ErrorKind::Unsupported
1347 ) || matches!(err.raw_os_error(), Some(47) | Some(49) | Some(97))
1348}
1349
1350struct AbortOnDrop<T> {
1351 handle: JoinHandle<T>,
1352}
1353
1354impl<T> AbortOnDrop<T> {
1355 fn new(handle: JoinHandle<T>) -> Self {
1356 Self { handle }
1357 }
1358
1359 async fn join(&mut self) -> Result<T, JoinError> {
1360 (&mut self.handle).await
1361 }
1362}
1363
1364impl<T> Drop for AbortOnDrop<T> {
1365 fn drop(&mut self) {
1366 if !self.handle.is_finished() {
1367 self.handle.abort();
1368 }
1369 }
1370}
1371
1372#[derive(Debug)]
1373struct StartLock {
1374 _file: fs::File,
1377}
1378
1379impl StartLock {
1380 async fn acquire(connection_file_path: &Path) -> Result<Self, BootstrapError> {
1381 let path = start_lock_path(connection_file_path);
1382 for _ in 0..START_LOCK_RETRIES {
1383 let file = match open_owner_only_lock(&path) {
1384 Ok(file) => file,
1385 Err(source) => return Err(BootstrapError::StartLockCreate { path, source }),
1386 };
1387 match FileExt::try_lock(&file) {
1388 Ok(()) => return Ok(Self { _file: file }),
1389 Err(TryLockError::WouldBlock) => sleep(START_LOCK_RETRY_DELAY).await,
1390 Err(TryLockError::Error(source)) => {
1391 return Err(BootstrapError::StartLockCreate { path, source });
1392 }
1393 }
1394 }
1395
1396 Err(BootstrapError::StartLockBusy {
1397 path,
1398 attempts: START_LOCK_RETRIES,
1399 })
1400 }
1401}
1402
1403pub(crate) fn open_owner_only_lock(path: &Path) -> io::Result<fs::File> {
1404 let mut options = fs::OpenOptions::new();
1405 options.read(true).write(true).create(true);
1406 #[cfg(unix)]
1407 {
1408 use std::os::unix::fs::OpenOptionsExt;
1409 options.mode(0o600);
1410 }
1411 options.open(path)
1412}
1413
1414fn start_lock_path(connection_file_path: &Path) -> PathBuf {
1415 let file_name = connection_file_path
1416 .file_name()
1417 .map(|name| name.to_string_lossy())
1418 .unwrap_or_else(|| CONNECTION_FILE_NAME.into());
1419 let lock_name = format!("{file_name}.start-lock");
1420 connection_file_path
1421 .parent()
1422 .filter(|parent| !parent.as_os_str().is_empty())
1423 .unwrap_or_else(|| Path::new("."))
1424 .join(lock_name)
1425}
1426
1427fn non_empty_os_var(key: &str) -> Option<OsString> {
1428 let value = env::var_os(key)?;
1429 if value.is_empty() {
1430 None
1431 } else {
1432 Some(value)
1433 }
1434}
1435
1436#[derive(Debug)]
1439pub enum BootstrapError {
1440 StartupNotServing,
1442 #[cfg(unix)]
1443 Signal(io::Error),
1444 InvalidPort {
1445 raw: String,
1446 source: std::num::ParseIntError,
1447 },
1448 ConnectionFileRead {
1449 path: PathBuf,
1450 source: ConnectionFileError,
1451 },
1452 ConnectionFileWrite {
1453 path: PathBuf,
1454 source: ConnectionFileError,
1455 },
1456 GenerateConnectionFile(ConnectionFileError),
1457 StartLockCreate {
1458 path: PathBuf,
1459 source: io::Error,
1460 },
1461 StartLockBusy {
1462 path: PathBuf,
1463 attempts: usize,
1464 },
1465 RunDirLockCreate {
1467 path: PathBuf,
1468 source: io::Error,
1469 },
1470 RunDirBusy {
1475 path: PathBuf,
1476 holder_pid: Option<u32>,
1477 },
1478 RemoveStale {
1479 path: PathBuf,
1480 source: io::Error,
1481 },
1482 Bind {
1483 host: String,
1484 port: u16,
1485 source: io::Error,
1486 },
1487 LocalAddr {
1488 host: String,
1489 source: io::Error,
1490 },
1491 DaemonConfig(DaemonConfigError),
1492 MachineId(crate::machine_id::MachineIdFileError),
1495 RunDir(daemon_config::DaemonRunDirError),
1498 Serve(ServerError),
1499 ServeJoin(tokio::task::JoinError),
1500}
1501
1502impl fmt::Display for BootstrapError {
1503 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1504 match self {
1505 Self::StartupNotServing => write!(f, "bound daemon did not authenticate its readiness probe; connection file was not published"),
1506 #[cfg(unix)]
1507 Self::Signal(error) => write!(f, "failed to register SIGTERM handler: {error}"),
1508 Self::InvalidPort { raw, source } => {
1509 write!(f, "invalid {SUBC_PORT_ENV} value '{raw}': {source}")
1510 }
1511 Self::ConnectionFileRead { path, source } => write!(
1512 f,
1513 "failed to read connection file {}: {source}",
1514 path.display()
1515 ),
1516 Self::ConnectionFileWrite { path, source } => write!(
1517 f,
1518 "failed to publish connection file {}: {source}",
1519 path.display()
1520 ),
1521 Self::GenerateConnectionFile(err) => {
1522 write!(f, "failed to generate connection-file auth material: {err}")
1523 }
1524 Self::StartLockCreate { path, source } => {
1525 write!(
1526 f,
1527 "failed to create start lock {}: {source}",
1528 path.display()
1529 )
1530 }
1531 Self::StartLockBusy { path, attempts } => write!(
1532 f,
1533 "start lock {} remained busy after {attempts} attempts",
1534 path.display()
1535 ),
1536 Self::RunDirLockCreate { path, source } => write!(
1537 f,
1538 "refusing to start: failed to lock run directory via {}: {source}",
1539 path.display()
1540 ),
1541 Self::RunDirBusy { path, holder_pid } => {
1542 write!(
1543 f,
1544 "refusing to start: run directory lock {} is held by another daemon",
1545 path.display()
1546 )?;
1547 if let Some(pid) = holder_pid {
1548 write!(f, " (pid {pid})")?;
1549 }
1550 write!(
1551 f,
1552 "; a live daemon owns this data home's run state (typically one started with a different XDG_RUNTIME_DIR)"
1553 )
1554 }
1555 Self::RemoveStale { path, source } => write!(
1556 f,
1557 "failed to remove stale connection file {}: {source}",
1558 path.display()
1559 ),
1560 Self::Bind { host, port, source } if source.kind() == io::ErrorKind::AddrInUse => {
1561 write!(
1562 f,
1563 "port {port} in use on loopback {host}: {source}; set the port in config"
1564 )
1565 }
1566 Self::Bind { host, port, source } => {
1567 write!(f, "failed to bind loopback TCP {host}:{port}: {source}")
1568 }
1569 Self::LocalAddr { host, source } => {
1570 write!(f, "failed to read local address for {host}: {source}")
1571 }
1572 Self::DaemonConfig(err) => write!(f, "failed to load daemon config: {err}"),
1573 Self::MachineId(err) => write!(f, "refusing to start: {err}"),
1574 Self::RunDir(err) => write!(f, "refusing to start: {err}"),
1575 Self::Serve(err) => write!(f, "daemon server failed: {err}"),
1576 Self::ServeJoin(err) => write!(f, "daemon server task failed: {err}"),
1577 }
1578 }
1579}
1580
1581impl Error for BootstrapError {
1582 fn source(&self) -> Option<&(dyn Error + 'static)> {
1583 match self {
1584 Self::StartupNotServing => None,
1585 #[cfg(unix)]
1586 Self::Signal(source) => Some(source),
1587 Self::InvalidPort { source, .. } => Some(source),
1588 Self::ConnectionFileRead { source, .. }
1589 | Self::ConnectionFileWrite { source, .. }
1590 | Self::GenerateConnectionFile(source) => Some(source),
1591 Self::StartLockCreate { source, .. }
1592 | Self::RunDirLockCreate { source, .. }
1593 | Self::RemoveStale { source, .. }
1594 | Self::Bind { source, .. }
1595 | Self::LocalAddr { source, .. } => Some(source),
1596 Self::DaemonConfig(err) => Some(err),
1597 Self::MachineId(err) => Some(err),
1598 Self::RunDir(err) => Some(err),
1599 Self::Serve(err) => Some(err),
1600 Self::ServeJoin(err) => Some(err),
1601 Self::StartLockBusy { .. } | Self::RunDirBusy { .. } => None,
1602 }
1603 }
1604}
1605
1606#[cfg(test)]
1607mod tests {
1608 mod activity {
1609 include!("activity_bootstrap_tests.rs");
1610 }
1611 use super::*;
1612 use crate::server::ServerAuth;
1613 use cortexkit_test_support::ScratchDir;
1614 #[cfg(target_os = "linux")]
1615 use std::collections::BTreeSet;
1616 use std::sync::Mutex;
1617 #[cfg(target_os = "linux")]
1618 use subc_control::ModuleProtocol;
1619 use subc_transport::MIN_KEY_LEN;
1620 use tokio::io::AsyncReadExt;
1621 use tokio::task::JoinHandle;
1622
1623 #[cfg(unix)]
1624 use std::os::unix::fs::PermissionsExt;
1625
1626 static ENV_LOCK: Mutex<()> = Mutex::new(());
1627
1628 #[test]
1629 fn normalized_build_provenance_preserves_real_values() {
1630 assert_eq!(normalized_build_provenance("abc"), Some("abc".to_string()));
1631 }
1632
1633 #[test]
1634 fn normalized_build_provenance_omits_unavailable_and_empty_values() {
1635 assert_eq!(normalized_build_provenance("unavailable"), None);
1636 assert_eq!(normalized_build_provenance(""), None);
1637 }
1638
1639 #[cfg(target_os = "linux")]
1640 fn current_cgroup_path_for_test() -> io::Result<PathBuf> {
1641 let cgroups = fs::read_to_string("/proc/self/cgroup")?;
1642 let relative = cgroups
1643 .lines()
1644 .find_map(|line| line.strip_prefix("0::"))
1645 .ok_or_else(|| {
1646 io::Error::new(io::ErrorKind::Unsupported, "cgroup v2 is unavailable")
1647 })?;
1648 Ok(Path::new("/sys/fs/cgroup").join(relative.trim_start_matches('/')))
1649 }
1650
1651 #[cfg(target_os = "linux")]
1652 fn module_cgroup_directories() -> io::Result<BTreeSet<OsString>> {
1653 let modules = current_cgroup_path_for_test()?.join("subc-modules");
1654 let entries = match fs::read_dir(modules) {
1655 Ok(entries) => entries,
1656 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(BTreeSet::new()),
1657 Err(error) => return Err(error),
1658 };
1659 let mut directories = BTreeSet::new();
1660 for entry in entries {
1661 let entry = entry?;
1662 if entry.file_type()?.is_dir() {
1663 directories.insert(entry.file_name());
1664 }
1665 }
1666 Ok(directories)
1667 }
1668
1669 #[cfg(target_os = "linux")]
1670 async fn wait_for_path(path: &Path, task: &JoinHandle<Result<(), BootstrapError>>) {
1671 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
1672 while !path.exists() && tokio::time::Instant::now() < deadline {
1673 assert!(
1674 !task.is_finished(),
1675 "daemon exited before creating {}",
1676 path.display()
1677 );
1678 sleep(Duration::from_millis(10)).await;
1679 }
1680 assert!(path.exists(), "daemon did not create {}", path.display());
1681 }
1682
1683 #[cfg(target_os = "linux")]
1688 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1689 async fn run_with_config_does_not_reconcile_the_ambient_cgroup_by_default() {
1690 let temp = unique_temp_dir("bootstrap-cgroup-default-disabled");
1691 let module_id = format!("cgroup-isolation-probe-{}", process::id());
1692 let before = module_cgroup_directories().expect("read ambient module cgroups before boot");
1693 assert!(
1694 !before.contains(&OsString::from(&module_id)),
1695 "isolation probe cgroup already exists before this daemon starts"
1696 );
1697 let capture = temp.join("logs").join(format!("{module_id}.stderr.log"));
1698 let module = ConfiguredModule {
1699 module_id,
1700 program: PathBuf::from("sh"),
1701 args: vec!["-c".to_string(), "sleep 30".to_string()],
1702 env: Vec::new(),
1703 log: None,
1704 enabled: true,
1705 reserved: false,
1706 reserved_prefixes: Vec::new(),
1707 protocol: ModuleProtocol::None,
1708 overlap: Default::default(),
1709 health: HealthConfig::default(),
1710 drain_timeout_ms: None,
1711 route_bind_relay_timeout_ms: None,
1712 restart: RestartPolicy::default(),
1713 };
1714 let config = BootstrapConfig::new(temp.join("connection.json"), 0)
1715 .with_configured_modules([module])
1716 .with_capture_logs_dir(temp.join("logs"))
1717 .with_terminal_journal_path(temp.join("terminals.jsonl"));
1718 let task = tokio::spawn(run_with_config(config));
1719
1720 wait_for_path(&capture, &task).await;
1721 let after = module_cgroup_directories().expect("read ambient module cgroups after boot");
1722
1723 task.abort();
1724 assert!(task
1725 .await
1726 .expect_err("aborted daemon task must cancel")
1727 .is_cancelled());
1728 assert_eq!(
1729 before, after,
1730 "an in-process daemon must not create or reconcile ambient module cgroups"
1731 );
1732 }
1733
1734 #[cfg(target_os = "linux")]
1735 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1736 async fn explicit_cgroup_root_is_prepared_inside_the_fixture_tree() {
1737 let temp = unique_temp_dir("bootstrap-cgroup-explicit-root");
1738 let cgroup_root = temp.join("cgroup");
1739 fs::create_dir(&cgroup_root).expect("create scratch cgroup root");
1740 fs::write(cgroup_root.join("cgroup.procs"), b"").expect("write scratch cgroup marker");
1741 let modules = cgroup_root.join("subc-modules");
1742 let config = BootstrapConfig::new(temp.join("connection.json"), 0)
1743 .with_cgroup_placement(CgroupPlacementConfig::Root(cgroup_root))
1744 .with_terminal_journal_path(temp.join("terminals.jsonl"));
1745 let task = tokio::spawn(run_with_config(config));
1746
1747 wait_for_path(&modules, &task).await;
1748
1749 task.abort();
1750 assert!(task
1751 .await
1752 .expect_err("aborted daemon task must cancel")
1753 .is_cancelled());
1754 assert!(
1755 fs::read_dir(&modules)
1756 .expect("read prepared modules directory")
1757 .next()
1758 .is_none(),
1759 "the delegation probe must clean up after itself"
1760 );
1761 }
1762
1763 struct EnvGuard {
1764 key: &'static str,
1765 previous: Option<OsString>,
1766 }
1767
1768 impl EnvGuard {
1769 fn set(key: &'static str, value: &Path) -> Self {
1770 let previous = env::var_os(key);
1771 env::set_var(key, value);
1772 Self { key, previous }
1773 }
1774
1775 fn set_str(key: &'static str, value: &str) -> Self {
1776 let previous = env::var_os(key);
1777 env::set_var(key, value);
1778 Self { key, previous }
1779 }
1780
1781 fn unset(key: &'static str) -> Self {
1782 let previous = env::var_os(key);
1783 env::remove_var(key);
1784 Self { key, previous }
1785 }
1786 }
1787
1788 impl Drop for EnvGuard {
1789 fn drop(&mut self) {
1790 match &self.previous {
1791 Some(value) => env::set_var(self.key, value),
1792 None => env::remove_var(self.key),
1793 }
1794 }
1795 }
1796
1797 fn unique_temp_dir(name: &str) -> ScratchDir {
1798 ScratchDir::new(name)
1799 }
1800
1801 fn temp_connection_file_path(name: &str) -> (ScratchDir, PathBuf) {
1802 let dir = unique_temp_dir(name);
1803 let path = dir.join("conn.json");
1804 (dir, path)
1805 }
1806
1807 fn auth_for(info: &ConnectionInfo) -> ServerAuth {
1808 ServerAuth::new(info.key.clone(), info.daemon_id, info.daemon_ver.clone())
1809 }
1810
1811 fn start_server(bound: BoundDaemon) -> JoinHandle<Result<(), ServerError>> {
1812 let auth = auth_for(&bound.connection_info);
1813 tokio::spawn(serve_listeners(
1814 bound.listeners,
1815 Arc::new(Router::with_default_self_handler()),
1816 auth,
1817 ))
1818 }
1819
1820 fn expect_bound(outcome: Outcome) -> BoundDaemon {
1821 match outcome {
1822 Outcome::Bound(bound) => bound,
1823 Outcome::AlreadyRunning => panic!("fresh connection file unexpectedly had a daemon"),
1824 }
1825 }
1826
1827 async fn connect_from_info(conn: &ConnectionInfo) -> io::Result<TcpStream> {
1828 let endpoint = conn
1829 .endpoints
1830 .first()
1831 .expect("test connection file should have an endpoint");
1832 let ip: IpAddr = endpoint.host.parse().unwrap();
1833 TcpStream::connect(SocketAddr::new(ip, endpoint.port)).await
1834 }
1835
1836 fn make_connection_info(port: u16) -> ConnectionInfo {
1837 ConnectionInfo {
1838 schema: SCHEMA_VERSION,
1839 wire_version: Some(PROTOCOL_VERSION),
1840 endpoints: vec![Endpoint {
1841 host: "127.0.0.1".to_owned(),
1842 port,
1843 }],
1844 key: generate_key().unwrap(),
1845 daemon_id: generate_daemon_id().unwrap(),
1846 pid: process::id(),
1847 daemon_ver: "test-subc".to_owned(),
1848 }
1849 }
1850
1851 fn write_raw_owner_only_connection_file(path: &Path, contents: &[u8]) {
1852 fs::write(path, contents).unwrap();
1853 #[cfg(unix)]
1854 fs::set_permissions(path, fs::Permissions::from_mode(0o600)).unwrap();
1855 }
1856
1857 fn assert_owner_only_connection_file(path: &Path) {
1858 #[cfg(unix)]
1861 {
1862 let mode = fs::metadata(path).unwrap().permissions().mode() & 0o777;
1863 assert_eq!(mode, 0o600);
1864 }
1865 #[cfg(not(unix))]
1866 let _ = path;
1867 }
1868
1869 #[test]
1870 fn connection_file_path_uses_xdg_runtime_dir_when_set() {
1871 let _env_lock = ENV_LOCK.lock().unwrap();
1872 let runtime_dir = unique_temp_dir("xdg-runtime");
1873 let _xdg = EnvGuard::set("XDG_RUNTIME_DIR", runtime_dir.path());
1874
1875 assert_eq!(
1876 connection_file_path(),
1877 runtime_dir.join(CONNECTION_FILE_NAME)
1878 );
1879 }
1880
1881 #[test]
1882 fn connection_file_path_source_is_xdg_runtime_dir_when_set() {
1883 let runtime_dir = OsString::from("/run/user/1000");
1884
1885 let (path, source) = connection_file_path_with_source(Some(runtime_dir));
1886
1887 assert_eq!(
1888 path,
1889 PathBuf::from("/run/user/1000").join(CONNECTION_FILE_NAME)
1890 );
1891 assert_eq!(source, ConnectionFileSource::XdgRuntimeDir);
1892 }
1893
1894 #[test]
1895 fn connection_file_path_falls_back_to_temp_dir_with_user_token_when_xdg_unset() {
1896 let _env_lock = ENV_LOCK.lock().unwrap();
1897 let _xdg = EnvGuard::unset("XDG_RUNTIME_DIR");
1898
1899 assert_eq!(
1900 connection_file_path(),
1901 env::temp_dir().join(format!("subc-{}.connection.json", user_connection_token()))
1902 );
1903 }
1904
1905 #[test]
1906 fn connection_file_path_source_is_temp_dir_when_xdg_unset() {
1907 let (path, source) = connection_file_path_with_source(None);
1908
1909 assert_eq!(
1910 path,
1911 env::temp_dir().join(format!("subc-{}.connection.json", user_connection_token()))
1912 );
1913 assert_eq!(source, ConnectionFileSource::TempDirFallback);
1914 }
1915
1916 #[test]
1921 fn user_connection_token_is_stable_under_concurrent_callers() {
1922 let expected = user_connection_token();
1923 let workers: Vec<_> = (0..32)
1924 .map(|_| {
1925 std::thread::spawn(|| (0..40).map(|_| user_connection_token()).collect::<Vec<_>>())
1926 })
1927 .collect();
1928 for worker in workers {
1929 for token in worker.join().expect("probe thread") {
1930 assert_eq!(token, expected, "token diverged under concurrent probes");
1931 }
1932 }
1933 }
1934
1935 #[test]
1936 fn connection_file_path_source_is_temp_dir_when_xdg_empty() {
1937 let (path, source) = connection_file_path_with_source(Some(OsString::new()));
1938
1939 assert_eq!(
1940 path,
1941 env::temp_dir().join(format!("subc-{}.connection.json", user_connection_token()))
1942 );
1943 assert_eq!(source, ConnectionFileSource::TempDirFallback);
1944 }
1945
1946 #[test]
1950 fn daemon_binary_config_journals_and_captures_into_the_run_dir() {
1951 let _env_lock = ENV_LOCK.lock().unwrap();
1952 let root = unique_temp_dir("daemon-binary-config");
1953 let data_home = root.join("data");
1954 let _data = EnvGuard::set("XDG_DATA_HOME", &data_home);
1955 let _config = EnvGuard::set("XDG_CONFIG_HOME", &root.join("config"));
1956 let _port = EnvGuard::unset(SUBC_PORT_ENV);
1957
1958 let config = BootstrapConfig::from_env_for_daemon_binary().unwrap();
1959
1960 let run_dir = data_home.join("cortexkit").join("run");
1961 assert_eq!(
1962 config.terminal_journal_path,
1963 Some(run_dir.join("terminals.jsonl"))
1964 );
1965 assert_eq!(config.capture_logs_dir, Some(run_dir.join("logs")));
1966 assert_eq!(
1967 BootstrapConfig::new(root.join("connection.json"), 0).terminal_journal_path,
1968 None,
1969 "an in-process config must not journal anywhere unless asked to"
1970 );
1971 }
1972
1973 #[test]
1974 fn daemon_binary_config_refuses_a_relative_data_home() {
1975 let _env_lock = ENV_LOCK.lock().unwrap();
1976 let root = unique_temp_dir("daemon-binary-relative-data");
1977 let _data = EnvGuard::set_str("XDG_DATA_HOME", "relative-data-home");
1978 let _config = EnvGuard::set("XDG_CONFIG_HOME", &root.join("config"));
1979 let _port = EnvGuard::unset(SUBC_PORT_ENV);
1980
1981 let error = BootstrapConfig::from_env_for_daemon_binary()
1982 .expect_err("a relative data home must refuse the daemon binary's config");
1983 assert!(
1984 matches!(error, BootstrapError::RunDir(_)),
1985 "expected a run-directory refusal, got {error}"
1986 );
1987 assert!(error.to_string().contains("XDG_DATA_HOME"), "{error}");
1988 }
1989
1990 #[test]
1991 fn configured_port_uses_default_config_and_env_override() {
1992 let _env_lock = ENV_LOCK.lock().unwrap();
1993 let (_dir, conn_path) = temp_connection_file_path("daemon-config-port");
1994 let config_path = conn_path.with_file_name("subc.jsonc");
1995
1996 let _port = EnvGuard::unset(SUBC_PORT_ENV);
1997 assert_eq!(
1998 BootstrapConfig::from_env_with_daemon_config_path(&config_path)
1999 .unwrap()
2000 .port,
2001 DEFAULT_SUBC_PORT
2002 );
2003
2004 fs::write(&config_path, r#"{ "version": 1, "port": 8123 }"#).unwrap();
2005 assert_eq!(
2006 BootstrapConfig::from_env_with_daemon_config_path(&config_path)
2007 .unwrap()
2008 .port,
2009 8123
2010 );
2011
2012 let _port = EnvGuard::set_str(SUBC_PORT_ENV, "9012");
2013 assert_eq!(
2014 BootstrapConfig::from_env_with_daemon_config_path(&config_path)
2015 .unwrap()
2016 .port,
2017 9012
2018 );
2019 }
2020
2021 #[tokio::test]
2022 async fn second_singleton_probe_against_served_tcp_daemon_reports_already_running() {
2023 let (_dir, path) = temp_connection_file_path("already-running");
2024
2025 let bound = expect_bound(ensure_singleton(&path, 0).await.unwrap());
2026 let server = start_server(bound);
2027
2028 let second = ensure_singleton(&path, 0).await.unwrap();
2029 assert!(matches!(second, Outcome::AlreadyRunning));
2030
2031 server.abort();
2032 let _ = server.await;
2033 }
2034
2035 #[tokio::test]
2036 async fn daemon_connection_file_publishes_protocol_wire_version() {
2037 let (_dir, path) = temp_connection_file_path("wire-version");
2038 let bound = expect_bound(ensure_singleton(&path, 0).await.unwrap());
2039 assert_eq!(bound.connection_info.wire_version, Some(PROTOCOL_VERSION));
2040 assert_eq!(
2041 connection_file::read(&path).unwrap().wire_version,
2042 Some(PROTOCOL_VERSION)
2043 );
2044
2045 drop(bound.listeners);
2046 }
2047
2048 #[tokio::test]
2049 async fn stale_unbound_connection_file_is_reclaimed() {
2050 let (_dir, path) = temp_connection_file_path("stale-reclaim");
2051 let stale = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
2052 let stale_port = stale.local_addr().unwrap().port();
2053 drop(stale);
2054 let stale_info = make_connection_info(stale_port);
2055 write_atomic(&path, &stale_info).unwrap();
2056
2057 let bound = expect_bound(ensure_singleton(&path, 0).await.unwrap());
2058 assert_ne!(bound.connection_info.key, stale_info.key);
2059 drop(bound.listeners);
2060 }
2061
2062 #[cfg(unix)]
2063 #[tokio::test]
2064 async fn ensure_singleton_reclaims_insecure_connection_file() {
2065 let (_dir, path) = temp_connection_file_path("insecure-reclaim");
2066 let stale = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
2067 let stale_port = stale.local_addr().unwrap().port();
2068 drop(stale);
2069 let stale_info = make_connection_info(stale_port);
2070 write_atomic(&path, &stale_info).unwrap();
2071 fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).unwrap();
2072
2073 let bound = expect_bound(ensure_singleton(&path, 0).await.unwrap());
2074 assert_ne!(bound.connection_info.key, stale_info.key);
2075 assert_ne!(bound.connection_info.daemon_id, stale_info.daemon_id);
2076 assert_owner_only_connection_file(&path);
2077
2078 drop(bound.listeners);
2079 }
2080
2081 #[tokio::test]
2082 async fn ensure_singleton_reclaims_non_loopback_connection_file() {
2083 let (_dir, path) = temp_connection_file_path("non-loopback-reclaim");
2084 let mut stale_info = make_connection_info(8757);
2085 stale_info.endpoints = vec![Endpoint {
2086 host: "192.0.2.10".to_owned(),
2087 port: 8757,
2088 }];
2089 write_atomic(&path, &stale_info).unwrap();
2090
2091 let bound = expect_bound(ensure_singleton(&path, 0).await.unwrap());
2092 assert_ne!(bound.connection_info.key, stale_info.key);
2093 assert_ne!(bound.connection_info.daemon_id, stale_info.daemon_id);
2094 assert!(bound
2095 .connection_info
2096 .endpoints
2097 .iter()
2098 .all(|endpoint| endpoint.host.parse::<IpAddr>().unwrap().is_loopback()));
2099 assert_owner_only_connection_file(&path);
2100
2101 drop(bound.listeners);
2102 }
2103
2104 #[tokio::test]
2105 async fn ensure_singleton_reclaims_invalid_connection_file_shapes() {
2106 let mut unsupported_schema = make_connection_info(8757);
2107 unsupported_schema.schema = SCHEMA_VERSION + 1;
2108
2109 let mut empty_endpoints = make_connection_info(8757);
2110 empty_endpoints.endpoints.clear();
2111
2112 let mut short_key = make_connection_info(8757);
2113 short_key.key = vec![0x5A; MIN_KEY_LEN - 1];
2114
2115 let cases = vec![
2116 (
2117 "unsupported-schema",
2118 serde_json::to_vec(&unsupported_schema).unwrap(),
2119 Some(unsupported_schema),
2120 ),
2121 (
2122 "empty-endpoints",
2123 serde_json::to_vec(&empty_endpoints).unwrap(),
2124 Some(empty_endpoints),
2125 ),
2126 (
2127 "short-key",
2128 serde_json::to_vec(&short_key).unwrap(),
2129 Some(short_key),
2130 ),
2131 ("invalid-json", b"{not valid connection json".to_vec(), None),
2132 ];
2133
2134 for (label, contents, old_info) in cases {
2135 let (_dir, path) = temp_connection_file_path(label);
2136 write_raw_owner_only_connection_file(&path, &contents);
2137
2138 let bound = expect_bound(ensure_singleton(&path, 0).await.unwrap());
2139 if let Some(old_info) = old_info {
2140 assert_ne!(bound.connection_info.key, old_info.key, "{label}");
2141 assert_ne!(
2142 bound.connection_info.daemon_id, old_info.daemon_id,
2143 "{label}"
2144 );
2145 }
2146 assert!(bound.connection_info.key.len() >= MIN_KEY_LEN, "{label}");
2147 assert_ne!(bound.connection_info.daemon_id, [0u8; 16], "{label}");
2148 assert_owner_only_connection_file(&path);
2149
2150 drop(bound.listeners);
2151 }
2152 }
2153
2154 #[tokio::test]
2155 async fn foreign_reused_port_connection_file_is_reclaimed_after_auth_probe_fails() {
2156 let (_dir, path) = temp_connection_file_path("foreign-reclaim");
2157 let foreign = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
2158 let foreign_port = foreign.local_addr().unwrap().port();
2159 write_atomic(&path, &make_connection_info(foreign_port)).unwrap();
2160 let foreign_task = tokio::spawn(async move {
2161 if let Ok((mut stream, _)) = foreign.accept().await {
2162 let mut buf = [0u8; 64];
2163 let _ = stream.read(&mut buf).await;
2164 }
2165 });
2166
2167 let bound = expect_bound(ensure_singleton(&path, 0).await.unwrap());
2168 assert!(bound
2169 .connection_info
2170 .endpoints
2171 .iter()
2172 .all(|endpoint| endpoint.port != foreign_port));
2173
2174 drop(bound.listeners);
2175 let _ = foreign_task.await;
2176 }
2177
2178 #[tokio::test]
2179 async fn stale_start_lock_file_is_reclaimable() {
2180 let (_dir, path) = temp_connection_file_path("start-lock-stale-file");
2181 let lock_path = start_lock_path(&path);
2182 drop(open_owner_only_lock(&lock_path).unwrap());
2183 assert!(lock_path.is_file());
2184
2185 let lock = StartLock::acquire(&path).await.unwrap();
2186 assert!(lock_path.is_file());
2187
2188 drop(lock);
2189 assert!(lock_path.is_file());
2190 }
2191
2192 #[tokio::test]
2193 async fn held_start_lock_blocks_second_acquire_until_release() {
2194 let (_dir, path) = temp_connection_file_path("start-lock-held");
2195 let lock_path = start_lock_path(&path);
2196 let first = StartLock::acquire(&path).await.unwrap();
2197
2198 let err = match StartLock::acquire(&path).await {
2199 Ok(_) => panic!("second acquire while held must stay busy"),
2200 Err(err) => err,
2201 };
2202 assert!(matches!(
2203 err,
2204 BootstrapError::StartLockBusy {
2205 ref path,
2206 attempts: START_LOCK_RETRIES,
2207 } if path == &lock_path
2208 ));
2209
2210 drop(first);
2211
2212 let second = StartLock::acquire(&path)
2213 .await
2214 .expect("released advisory lock should be reclaimable");
2215 drop(second);
2216 }
2217
2218 #[tokio::test]
2219 async fn bind_conflict_on_fixed_port_fails_loud_without_reselecting() {
2220 let (_dir, path) = temp_connection_file_path("bind-conflict");
2221 let occupied = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
2222 let occupied_port = occupied.local_addr().unwrap().port();
2223
2224 let err = ensure_singleton(&path, occupied_port).await.unwrap_err();
2225 assert!(matches!(
2226 err,
2227 BootstrapError::Bind { ref source, .. } if source.kind() == io::ErrorKind::AddrInUse
2228 ));
2229 assert!(err.to_string().contains("set the port in config"));
2230
2231 drop(occupied);
2232 }
2233
2234 #[tokio::test]
2235 async fn ephemeral_ipv6_collision_still_serves_ipv4() {
2236 let (listeners, endpoints) = bind_loopback_with(0, |port| async move {
2237 let occupied = TcpListener::bind((Ipv6Addr::LOCALHOST, port)).await?;
2238 let result = TcpListener::bind((Ipv6Addr::LOCALHOST, port)).await;
2239 drop(occupied);
2240 result
2241 })
2242 .await
2243 .expect("an incidental IPv6 collision must not prevent an ephemeral daemon from starting");
2244 assert_eq!(endpoints.len(), 1);
2245 assert_eq!(endpoints[0].host, "127.0.0.1");
2246 TcpStream::connect((Ipv4Addr::LOCALHOST, endpoints[0].port))
2247 .await
2248 .unwrap();
2249 drop(listeners);
2250 }
2251
2252 #[tokio::test]
2253 async fn unpublished_boot_holds_singleton_lock_until_publication() {
2254 let dir = ScratchDir::new("unpublished-singleton-lock");
2255 let path = dir.join("connection.json");
2256 let bound = expect_bound(
2257 ensure_singleton_inner(BootstrapConfig::new(&path, 0), false)
2258 .await
2259 .unwrap(),
2260 );
2261 assert!(!path.exists());
2262 let contender = open_owner_only_lock(&start_lock_path(&path)).unwrap();
2263 assert!(
2264 matches!(FileExt::try_lock(&contender), Err(TryLockError::WouldBlock)),
2265 "an unpublished boot must keep its singleton claim"
2266 );
2267 drop(bound);
2268 FileExt::try_lock(&contender).unwrap();
2269 }
2270
2271 #[tokio::test]
2272 async fn readiness_probe_requires_an_authenticating_server() {
2273 let dir = ScratchDir::new("unserved-readiness");
2274 let bound = expect_bound(
2275 ensure_singleton_inner(BootstrapConfig::new(dir.join("connection.json"), 0), false)
2276 .await
2277 .unwrap(),
2278 );
2279 assert!(matches!(
2280 verify_serving(&bound.connection_info).await,
2281 Err(BootstrapError::StartupNotServing)
2282 ));
2283 }
2284
2285 #[cfg(unix)]
2286 #[tokio::test]
2287 async fn orphan_sweep_finishes_before_connection_file_publication() {
2288 use crate::live_children::{ExecutableIdentity, LiveChild};
2289 let dir = ScratchDir::new("publish-after-sweep");
2290 let path = dir.join("connection.json");
2291 let record = dir.join("live-children.json");
2292 let ready = dir.join("ready");
2293 let mut child = std::process::Command::new("/bin/sh")
2294 .args([
2295 "-c",
2296 "trap '' TERM; : > \"$0\"; read -r _",
2297 ready.to_str().unwrap(),
2298 ])
2299 .stdin(std::process::Stdio::piped())
2300 .env("XDG_DATA_HOME", dir.path())
2301 .env("XDG_RUNTIME_DIR", dir.path())
2302 .env("XDG_CONFIG_HOME", dir.path())
2303 .spawn()
2304 .unwrap();
2305 timeout(Duration::from_secs(5), async {
2306 while !ready.exists() {
2307 sleep(Duration::from_millis(5)).await;
2308 }
2309 })
2310 .await
2311 .unwrap();
2312 let observed = subc_os::Process::open(child.id())
2313 .unwrap()
2314 .unwrap()
2315 .observe()
2316 .unwrap();
2317 crate::live_children::write_record(
2318 &record,
2319 &[LiveChild {
2320 module_id: "old-sleep".into(),
2321 pid: child.id(),
2322 protocol: subc_control::ModuleProtocol::None,
2323 start_time: Some(observed.start_time),
2324 executable: observed.executable.map(ExecutableIdentity::from),
2325 cgroup_name: None,
2326 }],
2327 )
2328 .unwrap();
2329 let config = BootstrapConfig::new(&path, 0).with_live_children_record(&record);
2330 let daemon = tokio::spawn(run_with_config(config));
2331 timeout(Duration::from_secs(5), async {
2332 while !path.exists() {
2333 sleep(Duration::from_millis(5)).await;
2334 }
2335 })
2336 .await
2337 .unwrap();
2338 assert!(path.exists());
2339 let ended = child.try_wait().unwrap().is_some();
2340 if !ended {
2341 child.kill().unwrap();
2342 }
2343 child.wait().unwrap();
2344 if ended {
2345 assert!(matches!(probe_existing(&path).await.unwrap(), Probe::Live));
2346 }
2347 daemon.abort();
2348 let _ = daemon.await;
2349 assert!(
2350 ended,
2351 "the published daemon must not still owe its orphan sweep"
2352 );
2353 }
2354
2355 #[cfg(target_os = "macos")]
2356 #[test]
2357 fn nofile_raise_worker() {
2358 let Some(path) = std::env::var_os("SUBC_NOFILE_TEST_FILE") else {
2359 return;
2360 };
2361 let (_, hard) = rlimit::Resource::NOFILE.get().unwrap();
2362 rlimit::Resource::NOFILE.set(256, hard).unwrap();
2363 raise_nofile_limit_with_ceiling(Some(10240));
2364 std::fs::write(path, rlimit::Resource::NOFILE.get().unwrap().0.to_string()).unwrap();
2365 }
2366
2367 #[cfg(target_os = "macos")]
2368 #[test]
2369 fn macos_nofile_raise_clamps_to_kernel_ceiling() {
2370 let dir = ScratchDir::new("nofile-ceiling");
2371 let result = dir.join("soft");
2372 let status = std::process::Command::new(std::env::current_exe().unwrap())
2373 .args(["--exact", "bootstrap::tests::nofile_raise_worker"])
2374 .env("SUBC_NOFILE_TEST_FILE", &result)
2375 .env("XDG_DATA_HOME", dir.path())
2376 .env("XDG_RUNTIME_DIR", dir.path())
2377 .env("XDG_CONFIG_HOME", dir.path())
2378 .status()
2379 .unwrap();
2380 assert!(status.success());
2381 let soft: u64 = std::fs::read_to_string(result).unwrap().parse().unwrap();
2382 assert!(
2383 soft > 256 && soft <= 10240,
2384 "the inherited limit must be raised within the kernel ceiling; got {soft}"
2385 );
2386 }
2387
2388 #[tokio::test]
2389 async fn key_rotation_republishes_new_material_and_old_file_fails_auth() {
2390 let (_dir, path) = temp_connection_file_path("key-rotation");
2391 let first = expect_bound(ensure_singleton(&path, 0).await.unwrap());
2392 let old_info = first.connection_info.clone();
2393 let fixed_port = old_info.endpoints[0].port;
2394 drop(first.listeners);
2395
2396 let second = expect_bound(ensure_singleton(&path, fixed_port).await.unwrap());
2397 let new_info = second.connection_info.clone();
2398 assert_ne!(old_info.key, new_info.key);
2399 assert_ne!(old_info.daemon_id, new_info.daemon_id);
2400 let server = start_server(second);
2401
2402 let mut old_stream = connect_from_info(&old_info).await.unwrap();
2403 let old_auth = authenticate_client(&mut old_stream, &old_info, PROBE_AUTH_DEADLINE).await;
2404 assert!(
2405 old_auth.is_err(),
2406 "old key must not authenticate after restart"
2407 );
2408
2409 let reread = connection_file::read(&path).unwrap();
2410 let mut new_stream = connect_from_info(&reread).await.unwrap();
2411 authenticate_client(&mut new_stream, &reread, PROBE_AUTH_DEADLINE)
2412 .await
2413 .unwrap();
2414
2415 server.abort();
2416 let _ = server.await;
2417 }
2418
2419 #[cfg(unix)]
2420 #[tokio::test]
2421 async fn published_connection_file_permissions_are_owner_only() {
2422 let (_dir, path) = temp_connection_file_path("permissions");
2423 let bound = expect_bound(ensure_singleton(&path, 0).await.unwrap());
2424
2425 let mode = fs::metadata(&path).unwrap().permissions().mode() & 0o777;
2426 assert_eq!(mode, 0o600);
2427
2428 drop(bound.listeners);
2429 }
2430}