Skip to main content

subc_daemon/
bootstrap.rs

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/// Runtime bootstrap configuration. Production uses the default fixed port and
74/// optional daemon-config override; tests pass port 0 to let the OS assign a free
75/// loopback port and discover it from the connection file.
76#[derive(Debug, Clone, Default)]
77struct AdmissionFactsConfig {
78    carrier_module_id: Option<String>,
79    targets: Option<Vec<String>>,
80}
81
82/// Controls where module cgroups are prepared.
83///
84/// In-process daemons default to [`Self::Disabled`] so they never derive a
85/// production location from the host process. The shipped daemon explicitly uses
86/// [`Self::Current`]. The `ck-subc` binary accepts
87/// `SUBC_CGROUP_PLACEMENT=disabled` for isolated test processes; an unset
88/// variable retains `Current`, and every other value is rejected at startup.
89/// Tests that exercise placement can own a [`Self::Root`].
90#[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    /// Module ids whose scopes may carry the attributes that grant authority.
114    scope_authority_owners: Vec<String>,
115    daemon_config_path: Option<PathBuf>,
116    configured_port: Option<u16>,
117    /// Daemon-wide route.bind relay budget in milliseconds (the fallback for
118    /// any module without a per-module override). `None` = built-in default
119    /// (12s — see `control::DEFAULT_ROUTE_BIND_RELAY_TIMEOUT`).
120    route_bind_relay_default_ms: Option<u64>,
121    reserved_capabilities: BTreeMap<String, String>,
122    watchdog_config: DaemonSelfWatchdogConfig,
123    connection_file_source: ConnectionFileSource,
124    /// Where module cgroups are prepared. Disabled unless a caller explicitly
125    /// opts in, because `Current` derives a host location from `/proc/self/cgroup`.
126    cgroup_placement: CgroupPlacementConfig,
127    /// Directory the supervisor writes per-module stdout/stderr capture files
128    /// into. `None` disables capture; the shipped binary supplies its real run
129    /// directory explicitly.
130    capture_logs_dir: Option<PathBuf>,
131    /// File the supervisor appends every module's terminal exits to, so exit
132    /// history survives a daemon restart. `None` keeps terminal history in
133    /// memory only (each module's ring). Absent by default for the same reason
134    /// as `capture_logs_dir`: an in-process daemon booted by a test must not
135    /// append its exits to the operator's real `terminals.jsonl`, where they
136    /// would show up in `ck module terminals`. The shipped binary supplies
137    /// `<run dir>/terminals.jsonl` explicitly.
138    terminal_journal_path: Option<PathBuf>,
139    /// Where the machine id is read from, or minted into when absent. `None`
140    /// serves no machine id. Absent by default for the same reason as
141    /// `capture_logs_dir`: an in-process daemon booted by a test must never
142    /// derive the operator's real data home and mint into it. The shipped binary
143    /// supplies `<data home>/cortexkit/machine-id` explicitly.
144    machine_id_path: Option<PathBuf>,
145    /// The live-children record: every supervised process this daemon has
146    /// running, kept so the next daemon can end the ones a crash left behind.
147    /// At startup, before any module is spawned, the previous daemon's record
148    /// here is swept. `None` keeps no record and sweeps nothing, for the same
149    /// reason as `capture_logs_dir`: an in-process daemon booted by a test
150    /// must never signal processes listed in the operator's real run
151    /// directory. The shipped binary supplies `<run dir>/live-children.json`.
152    live_children_path: Option<PathBuf>,
153    privacy_trampoline: Option<PathBuf>,
154}
155
156impl BootstrapConfig {
157    pub fn new(connection_file_path: impl Into<PathBuf>, port: u16) -> Self {
158        Self {
159            connection_file_path: connection_file_path.into(),
160            operator_queue_wait: Duration::from_secs(150),
161            operator_timeout: Duration::from_secs(120),
162            operator_stuck_grace: Duration::from_secs(10),
163            #[cfg(feature = "test-support")]
164            operator_script: None,
165            #[cfg(feature = "test-support")]
166            operator_events: None,
167            port,
168            daemon_ver: DAEMON_VERSION.to_owned(),
169            configured_modules: Vec::new(),
170            storage_config: None,
171            admission_facts: AdmissionFactsConfig::default(),
172            scope_authority_owners: daemon_config::default_scope_authority_owners(),
173            daemon_config_path: None,
174            configured_port: None,
175            route_bind_relay_default_ms: None,
176            reserved_capabilities: BTreeMap::new(),
177            watchdog_config: DaemonSelfWatchdogConfig::default(),
178            connection_file_source: ConnectionFileSource::Explicit,
179            cgroup_placement: CgroupPlacementConfig::default(),
180            capture_logs_dir: None,
181            terminal_journal_path: None,
182            machine_id_path: None,
183            live_children_path: None,
184            privacy_trampoline: None,
185        }
186    }
187
188    /// Keep the live-children record at `path`, and at startup end the
189    /// processes a previous daemon recorded there that are still running.
190    /// Embedding daemons and tests pass a path inside their own fixture tree.
191    pub fn with_live_children_record(mut self, path: impl Into<PathBuf>) -> Self {
192        self.live_children_path = Some(path.into());
193        self
194    }
195
196    /// macOS module launches require an explicit executable implementing the
197    /// subc-os trampoline entry point. Other systems ignore this setting.
198    pub fn with_privacy_trampoline(mut self, path: impl Into<PathBuf>) -> Self {
199        self.privacy_trampoline = Some(path.into());
200        self
201    }
202
203    /// Serve the machine id stored at `path`, minting it there at startup when
204    /// the file is absent. Embedding daemons and tests pass a path inside their
205    /// own fixture tree.
206    pub fn with_machine_id_path(mut self, path: impl Into<PathBuf>) -> Self {
207        self.machine_id_path = Some(path.into());
208        self
209    }
210
211    /// Selects module cgroup placement. The default is disabled.
212    pub fn with_cgroup_placement(mut self, placement: CgroupPlacementConfig) -> Self {
213        self.cgroup_placement = placement;
214        self
215    }
216
217    /// Redirects per-module stdout/stderr capture files out of the real run
218    /// directory. Tests that start an in-process daemon must call this with a
219    /// path inside their fixture tree.
220    pub fn with_capture_logs_dir(mut self, dir: impl Into<PathBuf>) -> Self {
221        self.capture_logs_dir = Some(dir.into());
222        self
223    }
224
225    /// Redirects the daemon-private journal, allowing embedded daemons and tests
226    /// to keep their observations out of the operator's live run directory.
227    pub fn with_terminal_journal_path(mut self, path: impl Into<PathBuf>) -> Self {
228        self.terminal_journal_path = Some(path.into());
229        self
230    }
231
232    pub fn from_env() -> Result<Self, BootstrapError> {
233        Self::from_env_with_daemon_config_path(daemon_config::default_config_path())
234    }
235
236    /// The SHIPPED BINARY's config, which is the only caller that should capture
237    /// child output into the operator's real run directory.
238    ///
239    /// Kept separate from `from_env` deliberately: see `capture_logs_dir` and
240    /// `terminal_journal_path` on this struct for why an absent value must mean
241    /// NO CAPTURE and NO JOURNAL rather than the operator's real run directory.
242    /// A run directory that cannot be resolved (a relative data home) refuses
243    /// startup instead of landing under the working directory.
244    pub fn from_env_for_daemon_binary() -> Result<Self, BootstrapError> {
245        let run_dir = daemon_config::daemon_run_dir().map_err(BootstrapError::RunDir)?;
246        let machine_id_path =
247            crate::machine_id::default_machine_id_path().map_err(BootstrapError::MachineId)?;
248        let config = Self::from_env()?
249            .with_capture_logs_dir(run_dir.join("logs"))
250            .with_terminal_journal_path(run_dir.join("terminals.jsonl"))
251            .with_live_children_record(crate::live_children::record_path(&run_dir))
252            .with_machine_id_path(machine_id_path);
253        #[cfg(feature = "test-support")]
254        let config = {
255            let mut config = config;
256            config.operator_script =
257                std::env::var_os("SUBC_TEST_OPERATOR_SCRIPT").map(PathBuf::from);
258            config.operator_events =
259                std::env::var_os("SUBC_TEST_OPERATOR_EVENTS").map(PathBuf::from);
260            for (name, value) in [
261                (
262                    "SUBC_TEST_OPERATOR_QUEUE_WAIT_MS",
263                    &mut config.operator_queue_wait,
264                ),
265                (
266                    "SUBC_TEST_OPERATOR_TIMEOUT_MS",
267                    &mut config.operator_timeout,
268                ),
269                (
270                    "SUBC_TEST_OPERATOR_STUCK_MS",
271                    &mut config.operator_stuck_grace,
272                ),
273            ] {
274                if let Ok(ms) = std::env::var(name) {
275                    if let Ok(ms) = ms.parse::<u64>() {
276                        *value = Duration::from_millis(ms);
277                    }
278                }
279            }
280            config
281        };
282        Ok(config)
283    }
284
285    pub fn from_env_with_daemon_config_path(
286        daemon_config_path: impl AsRef<Path>,
287    ) -> Result<Self, BootstrapError> {
288        let daemon_config_path = daemon_config_path.as_ref().to_path_buf();
289        let daemon_config =
290            daemon_config::load(&daemon_config_path).map_err(BootstrapError::DaemonConfig)?;
291        let config_port = daemon_config.as_ref().and_then(|config| config.port);
292        let storage_config = daemon_config
293            .as_ref()
294            .and_then(|config| config.storage.clone());
295        let admission_facts_carrier_module_id = daemon_config
296            .as_ref()
297            .and_then(|config| config.admission_facts_carrier_module_id.clone());
298        let admission_facts_targets = daemon_config
299            .as_ref()
300            .and_then(|config| config.admission_facts_targets.clone());
301        let scope_authority_owners = daemon_config
302            .as_ref()
303            .map(|config| config.scope_authority_owners.clone())
304            .unwrap_or_else(daemon_config::default_scope_authority_owners);
305        let route_bind_relay_default_ms = daemon_config
306            .as_ref()
307            .and_then(|config| config.route_bind_relay_timeout_ms);
308        let reserved_capabilities = daemon_config
309            .as_ref()
310            .map(|config| config.reserved_capabilities.clone())
311            .unwrap_or_default();
312        let configured_modules = daemon_config
313            .map(|config| config.modules)
314            .unwrap_or_default();
315
316        let port = match env::var(SUBC_PORT_ENV) {
317            Ok(raw) if !raw.trim().is_empty() => {
318                let port = raw
319                    .parse::<u16>()
320                    .map_err(|source| BootstrapError::InvalidPort { raw, source })?;
321                if let Some(config_port) = config_port {
322                    info!(
323                        env = SUBC_PORT_ENV,
324                        env_port = port,
325                        config_port,
326                        "SUBC_PORT overrides daemon config port"
327                    );
328                }
329                port
330            }
331            Ok(_) | Err(_) => config_port.unwrap_or(DEFAULT_SUBC_PORT),
332        };
333
334        let (connection_file_path, connection_file_source) =
335            connection_file_path_with_source(non_empty_os_var("XDG_RUNTIME_DIR"));
336        Ok(Self::new(connection_file_path, port)
337            .with_configured_modules(configured_modules)
338            .with_storage_config(storage_config)
339            .with_admission_facts_config(admission_facts_carrier_module_id, admission_facts_targets)
340            .with_scope_authority_owners(scope_authority_owners)
341            .with_route_bind_relay_default_ms(route_bind_relay_default_ms)
342            .with_reserved_capabilities(reserved_capabilities)
343            .with_daemon_config_source(daemon_config_path, config_port)
344            .with_connection_file_source(connection_file_source))
345    }
346
347    pub fn with_daemon_config_path(
348        self,
349        daemon_config_path: impl AsRef<Path>,
350    ) -> Result<Self, BootstrapError> {
351        let daemon_config_path = daemon_config_path.as_ref().to_path_buf();
352        let daemon_config =
353            daemon_config::load(&daemon_config_path).map_err(BootstrapError::DaemonConfig)?;
354        let configured_port = daemon_config.as_ref().and_then(|config| config.port);
355        let storage_config = daemon_config
356            .as_ref()
357            .and_then(|config| config.storage.clone());
358        let admission_facts_carrier_module_id = daemon_config
359            .as_ref()
360            .and_then(|config| config.admission_facts_carrier_module_id.clone());
361        let admission_facts_targets = daemon_config
362            .as_ref()
363            .and_then(|config| config.admission_facts_targets.clone());
364        let scope_authority_owners = daemon_config
365            .as_ref()
366            .map(|config| config.scope_authority_owners.clone())
367            .unwrap_or_else(daemon_config::default_scope_authority_owners);
368        let route_bind_relay_default_ms = daemon_config
369            .as_ref()
370            .and_then(|config| config.route_bind_relay_timeout_ms);
371        let reserved_capabilities = daemon_config
372            .as_ref()
373            .map(|config| config.reserved_capabilities.clone())
374            .unwrap_or_default();
375        let configured_modules = daemon_config
376            .map(|config| config.modules)
377            .unwrap_or_default();
378        Ok(self
379            .with_configured_modules(configured_modules)
380            .with_storage_config(storage_config)
381            .with_admission_facts_config(admission_facts_carrier_module_id, admission_facts_targets)
382            .with_scope_authority_owners(scope_authority_owners)
383            .with_route_bind_relay_default_ms(route_bind_relay_default_ms)
384            .with_reserved_capabilities(reserved_capabilities)
385            .with_daemon_config_source(daemon_config_path, configured_port))
386    }
387
388    pub fn with_configured_modules(
389        mut self,
390        modules: impl IntoIterator<Item = ConfiguredModule>,
391    ) -> Self {
392        self.configured_modules = modules.into_iter().collect();
393        self.configured_modules
394            .sort_by(|left, right| left.module_id.cmp(&right.module_id));
395        self
396    }
397
398    pub fn with_storage_config(
399        mut self,
400        storage_config: Option<daemon_config::StorageConfig>,
401    ) -> Self {
402        self.storage_config = storage_config;
403        self
404    }
405
406    pub fn with_admission_facts_config(
407        mut self,
408        carrier_module_id: Option<String>,
409        targets: Option<Vec<String>>,
410    ) -> Self {
411        self.admission_facts = AdmissionFactsConfig {
412            carrier_module_id,
413            targets,
414        };
415        self
416    }
417
418    /// Set the module ids whose scopes may carry `agent_id` and `delegates`.
419    /// Absent from a config, this is `daemon_config::default_scope_authority_owners`.
420    pub fn with_scope_authority_owners(mut self, owners: Vec<String>) -> Self {
421        self.scope_authority_owners = owners;
422        self
423    }
424
425    /// Set the daemon-wide route.bind relay default (the fallback for any
426    /// module without a per-module override). `None` preserves the built-in
427    /// default (12s). `serve_bound_daemon` reads this at startup and threads
428    /// it into the control handler's daemon-wide field.
429    pub fn with_route_bind_relay_default_ms(mut self, ms: Option<u64>) -> Self {
430        self.route_bind_relay_default_ms = ms;
431        self
432    }
433
434    pub fn with_reserved_capabilities(
435        mut self,
436        reserved_capabilities: BTreeMap<String, String>,
437    ) -> Self {
438        self.reserved_capabilities = reserved_capabilities;
439        self
440    }
441
442    fn with_daemon_config_source(
443        mut self,
444        daemon_config_path: PathBuf,
445        configured_port: Option<u16>,
446    ) -> Self {
447        self.daemon_config_path = Some(daemon_config_path);
448        self.configured_port = configured_port;
449        self
450    }
451
452    fn with_connection_file_source(mut self, source: ConnectionFileSource) -> Self {
453        self.connection_file_source = source;
454        self
455    }
456
457    pub fn with_watchdog_config(mut self, watchdog_config: DaemonSelfWatchdogConfig) -> Self {
458        self.watchdog_config = watchdog_config;
459        self
460    }
461}
462
463/// Result of singleton discovery.
464// Built once per daemon start and immediately matched, so the size gap between
465// the variants costs nothing worth a box on the public shape.
466#[allow(clippy::large_enum_variant)]
467#[derive(Debug)]
468pub enum Outcome {
469    /// A live daemon authenticated from the connection file; this invocation should exit 0.
470    AlreadyRunning,
471    /// This process won the singleton race, owns bound loopback listener(s), and
472    /// has published a fresh connection file.
473    Bound(BoundDaemon),
474}
475
476#[derive(Debug)]
477pub struct BoundDaemon {
478    pub listeners: Vec<TcpListener>,
479    pub connection_info: ConnectionInfo,
480    pub connection_file_path: PathBuf,
481    pub connection_file_source: ConnectionFileSource,
482    /// The machine id this daemon serves, established before any connection is
483    /// accepted. `None` when the config named no machine id path.
484    pub machine_id: Option<crate::machine_id::MachineId>,
485    /// Ownership of the run directory, held until the daemon exits so no other
486    /// daemon sweeps or writes this one's run state. `None` when the config
487    /// keeps no live-children record, and so has no run directory to own.
488    run_dir_lock: Option<crate::run_dir_lock::RunDirLock>,
489    /// The singleton start lock, kept when binding deferred publishing the
490    /// connection file. It stays held until the daemon has verified it is serving
491    /// and written the connection file, so a second daemon starting meanwhile
492    /// cannot claim the singleton too. `None` when the connection
493    /// file was already written at bind time.
494    publication_lock: Option<StartLock>,
495}
496
497/// Resolve subc's per-user TCP connection-file path.
498///
499/// `$XDG_RUNTIME_DIR/subc-connection.json` is preferred because the runtime
500/// directory is already per-user on Unix desktops. Without it, subc falls back
501/// to the system temp dir with a per-user token in the filename so different OS
502/// users do not collide on shared temp directories.
503pub fn connection_file_path() -> PathBuf {
504    connection_file_path_with_source(non_empty_os_var("XDG_RUNTIME_DIR")).0
505}
506
507fn connection_file_path_with_source(
508    runtime_dir: Option<OsString>,
509) -> (PathBuf, ConnectionFileSource) {
510    if let Some(runtime_dir) = runtime_dir.filter(|value| !value.is_empty()) {
511        return (
512            PathBuf::from(runtime_dir).join(CONNECTION_FILE_NAME),
513            ConnectionFileSource::XdgRuntimeDir,
514        );
515    }
516
517    (
518        env::temp_dir().join(format!("subc-{}.connection.json", user_connection_token())),
519        ConnectionFileSource::TempDirFallback,
520    )
521}
522
523/// Resolve, claim, and serve the per-user daemon singleton.
524///
525/// A second invocation is successful: if a live daemon authenticates from the
526/// existing connection file, this returns `Ok(())` after logging and the caller
527/// exits with status 0.
528pub async fn run() -> Result<(), BootstrapError> {
529    // `run` is a binary entry point, so it opts into the production cgroup and
530    // child-log locations. In-process callers keep both features disabled by default.
531    run_with_config(
532        BootstrapConfig::from_env_for_daemon_binary()?
533            .with_cgroup_placement(CgroupPlacementConfig::Current),
534    )
535    .await
536}
537
538/// Serve a daemon from an explicit config. This is the entry point the twelve
539/// sibling repos use to boot an in-process daemon in their integration tests.
540///
541/// # THIS INSTALLS NO TRACING SUBSCRIBER, SO THE DAEMON IS SILENT BY DEFAULT
542///
543/// The daemon's own diagnostics go through `tracing`, and `tracing` DISCARDS
544/// every event when no subscriber is installed. The shipped binary installs one
545/// in `main` (`init_tracing`); this function deliberately does not, because a
546/// library that installs a global subscriber fights with whatever the host
547/// process already set up.
548///
549/// The consequence for a test harness is not "less verbose": it is that the
550/// daemon has NOTHING TO SAY about any failure, and a missing instrument reads
551/// exactly like a clean one. PLEX found this on 2026-09-18 while trying to
552/// capture daemon logs beside an intermittent bind failure, and discovered the
553/// daemon had been silent in every conformance run that repo had ever done --
554/// so the one client-side error string was all the evidence that could exist,
555/// and they had spent a real investigation on a failure whose second source was
556/// never being recorded.
557///
558/// Install one in the harness before calling this, and assert it did something
559/// (they measured 236 daemon lines with the subscriber installed, 0 with the
560/// call commented out) -- otherwise the fix is itself unverified.
561pub async fn run_with_config(config: BootstrapConfig) -> Result<(), BootstrapError> {
562    let forwarding = Arc::new(ForwardingTable::default());
563    forwarding.operator_confirms().configure_bootstrap(&config);
564    let configured_modules = config.configured_modules.clone();
565    let storage_config = config.storage_config.clone();
566    let admission_facts = config.admission_facts.clone();
567    let scope_authority_owners = config.scope_authority_owners.clone();
568    let daemon_config_path = config.daemon_config_path.clone();
569    let configured_port = config.configured_port;
570    let route_bind_relay_default_ms = config.route_bind_relay_default_ms;
571    let reserved_capabilities = config.reserved_capabilities.clone();
572    let watchdog_config = config.watchdog_config.clone();
573    let cgroup_placement_config = config.cgroup_placement.clone();
574    let capture_logs_dir = config.capture_logs_dir.clone();
575    let terminal_journal_path = config.terminal_journal_path.clone();
576    let live_children_path = config.live_children_path.clone();
577    let privacy_trampoline = config.privacy_trampoline.clone();
578    match ensure_singleton_inner(config, false).await? {
579        Outcome::AlreadyRunning => {
580            info!("subc daemon already running");
581            Ok(())
582        }
583        Outcome::Bound(bound) => {
584            #[cfg(target_os = "linux")]
585            let cgroup_placement = prepare_cgroup_placement(&cgroup_placement_config);
586            #[cfg(not(target_os = "linux"))]
587            let _ = cgroup_placement_config;
588            serve_bound_daemon(
589                bound,
590                forwarding,
591                configured_modules,
592                storage_config,
593                admission_facts,
594                scope_authority_owners,
595                daemon_config_path,
596                configured_port,
597                route_bind_relay_default_ms,
598                reserved_capabilities,
599                watchdog_config,
600                capture_logs_dir,
601                terminal_journal_path,
602                live_children_path,
603                privacy_trampoline,
604                #[cfg(target_os = "linux")]
605                cgroup_placement,
606            )
607            .await
608        }
609    }
610}
611
612#[cfg(target_os = "linux")]
613fn prepare_cgroup_placement(config: &CgroupPlacementConfig) -> Option<subc_cgroup::Placement> {
614    let result = match config {
615        CgroupPlacementConfig::Disabled => return None,
616        CgroupPlacementConfig::Current => subc_cgroup::prepare_current(),
617        CgroupPlacementConfig::Root(root) => subc_cgroup::prepare_at(root),
618    };
619
620    match result {
621        Ok(Some(placement)) => Some(placement),
622        Ok(None) => {
623            warn!(
624                placement = ?config,
625                "module cgroup placement is disabled: configured cgroup root is not delegated"
626            );
627            None
628        }
629        Err(error) => {
630            warn!(
631                placement = ?config,
632                error = %error,
633                "module cgroup placement is disabled by an unexpected cgroup probe error"
634            );
635            None
636        }
637    }
638}
639
640/// Target soft limit for open file descriptors, applied to the daemon before any
641/// module is spawned so children inherit it. Multi-root modules (one process
642/// aggregating every project root's sqlite stores, index caches, watchers, and
643/// LSP pipes) trivially exceed the macOS default soft limit of 256; a launchd
644/// user agent does not pass login-shell ulimits through, so the raise must
645/// happen in-process.
646#[cfg(unix)]
647const NOFILE_TARGET: u64 = 65536;
648
649/// Raise RLIMIT_NOFILE to `NOFILE_TARGET` (clamped to the hard limit).
650/// Best-effort: failure is logged and never fatal, since the daemon can run
651/// under the inherited limit — modules with few roots just have less headroom.
652#[cfg(unix)]
653fn raise_nofile_limit() {
654    #[cfg(target_os = "macos")]
655    let ceiling = std::process::Command::new("/usr/sbin/sysctl")
656        .args(["-n", "kern.maxfilesperproc"])
657        .output()
658        .ok()
659        .filter(|output| output.status.success())
660        .and_then(|output| String::from_utf8(output.stdout).ok())
661        .and_then(|value| value.trim().parse::<u64>().ok())
662        .filter(|value| *value > 0);
663    #[cfg(not(target_os = "macos"))]
664    let ceiling = None;
665    raise_nofile_limit_with_ceiling(ceiling);
666}
667
668#[cfg(unix)]
669fn raise_nofile_limit_with_ceiling(kernel_ceiling: Option<u64>) {
670    match rlimit::Resource::NOFILE.get() {
671        Ok((soft, hard)) => {
672            // On XNU an infinite hard limit does not remove the separate
673            // per-process kernel ceiling. Exceeding it makes setrlimit fail
674            // without raising the inherited (often 256) soft limit at all.
675            let target = NOFILE_TARGET
676                .min(hard)
677                .min(kernel_ceiling.unwrap_or(u64::MAX));
678            if soft >= target {
679                return;
680            }
681            match rlimit::Resource::NOFILE.set(target, hard) {
682                Ok(()) => info!(
683                    previous_soft = soft,
684                    new_soft = target,
685                    hard,
686                    "raised open-file soft limit for daemon and module children"
687                ),
688                Err(err) => warn!(
689                    soft,
690                    hard,
691                    error = %err,
692                    "could not raise open-file soft limit; multi-root modules may exhaust descriptors"
693                ),
694            }
695        }
696        Err(err) => warn!(error = %err, "could not read open-file limit"),
697    }
698}
699
700/// CRT stdio-stream target on Windows (the `_setmaxstdio` maximum). Win32
701/// HANDLEs — what Rust `File`, tokio sockets, and SQLite's Win32 VFS actually
702/// consume — have a per-process quota in the millions and need no raise; the
703/// C-runtime stream table (default 512) is the only low ceiling, and it is
704/// per-process rather than inherited, so supervised modules linking the CRT
705/// must raise their own. Raising it here covers the daemon itself.
706#[cfg(windows)]
707fn raise_nofile_limit() {
708    const MAXSTDIO_TARGET: u32 = 8192;
709    let current = rlimit::getmaxstdio();
710    if current >= MAXSTDIO_TARGET {
711        return;
712    }
713    match rlimit::setmaxstdio(MAXSTDIO_TARGET) {
714        Ok(new_max) => info!(
715            previous = current,
716            new_max, "raised CRT stdio-stream limit for daemon"
717        ),
718        Err(err) => warn!(
719            current,
720            error = %err,
721            "could not raise CRT stdio-stream limit"
722        ),
723    }
724}
725
726#[cfg(not(any(unix, windows)))]
727fn raise_nofile_limit() {}
728
729pub async fn run_with_daemon_config_path(
730    config: BootstrapConfig,
731    daemon_config_path: impl AsRef<Path>,
732) -> Result<(), BootstrapError> {
733    run_with_config(config.with_daemon_config_path(daemon_config_path)?).await
734}
735
736#[allow(clippy::too_many_arguments)]
737async fn serve_bound_daemon(
738    bound: BoundDaemon,
739    forwarding: Arc<ForwardingTable>,
740    configured_modules: Vec<ConfiguredModule>,
741    storage_config: Option<daemon_config::StorageConfig>,
742    admission_facts: AdmissionFactsConfig,
743    scope_authority_owners: Vec<String>,
744    daemon_config_path: Option<PathBuf>,
745    configured_port: Option<u16>,
746    route_bind_relay_default_ms: Option<u64>,
747    reserved_capabilities: BTreeMap<String, String>,
748    watchdog_config: DaemonSelfWatchdogConfig,
749    capture_logs_dir: Option<PathBuf>,
750    terminal_journal_path: Option<PathBuf>,
751    live_children_path: Option<PathBuf>,
752    privacy_trampoline: Option<PathBuf>,
753    #[cfg(target_os = "linux")] cgroup_placement: Option<subc_cgroup::Placement>,
754) -> Result<(), BootstrapError> {
755    #[cfg(unix)]
756    let mut terminate = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
757        .map_err(BootstrapError::Signal)?;
758    // Windows has no SIGTERM. Ctrl-C is a different event, not an equivalent
759    // service-stop contract, so this shutdown handler is intentionally Unix-only.
760    raise_nofile_limit();
761
762    info!(
763        connection_file = %bound.connection_file_path.display(),
764        connection_file_source = %bound.connection_file_source,
765        connection_file_source_reason = bound.connection_file_source.reason(),
766        endpoints = ?bound.connection_info.endpoints,
767        configured_modules = configured_modules.len(),
768        machine_id = bound.machine_id.as_ref().map(|id| id.as_str()).unwrap_or("none"),
769        "subc daemon starting"
770    );
771
772    // Before anything can spawn a module (the configured modules below, or a
773    // client's start request once the listeners are served): a previous daemon
774    // that died without its shutdown stop may have left children running, and
775    // a fresh copy beside one would fight it for its port and stores. The
776    // record is not a running daemon's because this daemon holds the lock on
777    // the run directory the record lives in, and a running daemon holds that
778    // lock for its whole life. (Claiming the singleton is not enough: it is
779    // keyed on the connection file, which can live in a different runtime
780    // directory from the run directory.) The lock stays held until this
781    // function returns, which is when the daemon exits.
782    let run_dir_lock = bound.run_dir_lock;
783    let publication_lock = bound.publication_lock;
784    if let Some(owner) = &run_dir_lock {
785        crate::live_children::sweep_orphans(
786            owner,
787            &crate::live_children::AdoptedPids::none(),
788            crate::live_children::SweepBounds::default(),
789        )
790        .await;
791    }
792
793    let registry = Arc::new(Registry::default());
794    let process_liveness = Arc::new(SupervisorProcessLiveness::new());
795    let supervisor_handle = SupervisorHandle::new();
796    let connected_clients = ConnectedClients::new();
797    let daemon_incarnation = format!(
798        "{:032x}",
799        u128::from_be_bytes(bound.connection_info.daemon_id)
800    );
801    let supervisor = Supervisor::new(Arc::clone(&registry), RestartPolicy::default())
802        .with_process_liveness(process_liveness.clone())
803        .with_forwarding(Arc::clone(&forwarding))
804        .with_handle(supervisor_handle.clone())
805        .with_connection_file_path(bound.connection_file_path.clone())
806        .with_daemon_incarnation(daemon_incarnation.clone());
807    let supervisor = match privacy_trampoline {
808        Some(path) => supervisor.with_privacy_trampoline(path),
809        None => supervisor,
810    };
811    // ABSENT MEANS NO CAPTURE AND NO JOURNAL, NOT "THE REAL RUN DIRECTORY", and
812    // the difference is a production-corruption hazard rather than a preference.
813    // Both fields below follow the same rule: `None` for `capture_logs_dir`
814    // means supervised output is not captured, and `None` for
815    // `terminal_journal_path` means terminal history lives only in each
816    // module's in-memory ring (`supervisor.terminals` still answers, with the
817    // journal counters at zero).
818    //
819    // The terminal journal used to fall back to
820    // `daemon_run_dir().join("terminals.jsonl")`, so an in-process test daemon
821    // appended its fixture exits to the operator's journal, where they then
822    // appeared in `ck module terminals`.
823    //
824    // The capture line used to be `unwrap_or_else(|| daemon_run_dir().join("logs"))`,
825    // so ANY caller that did not set the field captured supervised children into
826    // the operator's live `~/.local/share/cortexkit/run/logs/`. That is twelve
827    // sibling repos whose integration tests boot an in-process daemon through
828    // `run_with_config` -- none of which asked for it, and none of which can see
829    // it from their side.
830    //
831    // Harmless while fixture module ids are fixture-shaped: this host carries 20
832    // zero-byte files from subc's own tests (good-aft, missing-aft,
833    // preview-consumer...). THE HAZARD IS A COLLISION. A fixture named "broca"
834    // or "aft" appends to a PRODUCTION capture file that operators read
835    // forensically and that placement gates count lines in -- with no residue to
836    // notice, because the file legitimately exists and legitimately grows.
837    //
838    // Found by BROCA (2026-09-19) from the other side: their rigs spawn the
839    // SHIPPED ck-subc and set XDG_CONFIG_HOME + XDG_RUNTIME_DIR but not
840    // XDG_DATA_HOME, so every local rig run supervised a module named "broca"
841    // and captured it into production's broca.stderr.log -- the same file I
842    // count seal lines in before and after placing their binaries.
843    //
844    // The supervisor already treats `None` as no-capture and no-journal, so
845    // this only removes invented defaults. The binary keeps capturing and
846    // journaling via `BootstrapConfig::from_env_for_daemon_binary`.
847
848    // Keep both ledgers in the explicitly selected run directory. An embedded
849    // daemon without a journal path must not write to the operator's live ledger.
850    let activity_run_dir = terminal_journal_path
851        .as_ref()
852        .and_then(|path| path.parent())
853        .map(Path::to_path_buf);
854    let supervisor = match terminal_journal_path {
855        Some(path) => supervisor.with_terminal_journal(path, daemon_incarnation.clone()),
856        None => supervisor,
857    };
858    let supervisor = match capture_logs_dir {
859        Some(dir) => supervisor.with_capture_logs_dir(dir),
860        None => supervisor,
861    };
862    let supervisor = match live_children_path {
863        Some(path) => supervisor.with_live_children_record(path),
864        None => supervisor,
865    };
866    #[cfg(target_os = "linux")]
867    let supervisor = supervisor.with_cgroup_placement(cgroup_placement);
868    // Collect per-module route.bind relay overrides BEFORE handing the
869    // `configured_modules` vector to the supervisor (which only needs each
870    // module's `drain_timeout_ms`). Each entry was filled in by parse-time
871    // resolution (per-module > daemon-wide > absent), so modules with no
872    // override are absent from this map and the daemon-wide default applies.
873    let route_bind_relay_timeouts = configured_modules
874        .iter()
875        .filter_map(|module| {
876            module
877                .route_bind_relay_timeout_ms
878                .map(|ms| (module.module_id.clone(), Duration::from_millis(ms)))
879        })
880        .collect::<std::collections::BTreeMap<_, _>>();
881    let control_start_clock = crate::clock::StartClock::capture();
882    let mut control = ControlHandler::with_forwarding(Arc::clone(&registry), forwarding)
883        .with_process_liveness(process_liveness)
884        .with_supervisor(supervisor_handle)
885        .with_connected_clients(connected_clients.clone())
886        .with_storage_config(storage_config)
887        .with_machine_id(bound.machine_id.clone())
888        .with_admission_facts_config(admission_facts.carrier_module_id, admission_facts.targets)
889        .with_scope_authority_owners(scope_authority_owners)
890        .with_route_bind_relay_timeouts(route_bind_relay_timeouts)
891        .with_daemon_provenance(
892            bound.connection_info.pid,
893            control_start_clock.started_at_ms(),
894            std::env::current_exe().ok(),
895            normalized_build_provenance(env!("SUBC_BUILD_GIT_SHA")),
896            normalized_build_provenance(env!("SUBC_BUILD_LOCK_DIGEST")),
897        )
898        .with_daemon_start_clock(control_start_clock)
899        .with_capability_config(
900            configured_modules
901                .iter()
902                .map(|module| (module.module_id.clone(), module.enabled)),
903            reserved_capabilities,
904        );
905    if let Some(run_dir) = activity_run_dir {
906        control = control.with_activity_ledger(&run_dir, daemon_incarnation);
907    }
908    if let Some(ms) = route_bind_relay_default_ms {
909        // A daemon-wide config value overrides the built-in default; a
910        // `None` here leaves the ControlHandler's 12s default in place.
911        control = control.with_route_bind_relay_timeout(Duration::from_millis(ms));
912    }
913    if let Some(config_path) = daemon_config_path {
914        control = control.with_supervisor_rescan(supervisor.clone(), config_path, configured_port);
915    }
916    let control = Arc::new(control);
917    let router = Arc::new(Router::with_control_handler(Arc::clone(&control)));
918    let auth = ServerAuth::new(
919        bound.connection_info.key.clone(),
920        bound.connection_info.daemon_id,
921        bound.connection_info.daemon_ver.clone(),
922    )
923    .with_connected_clients(connected_clients);
924
925    let mut serve_task =
926        AbortOnDrop::new(tokio::spawn(serve_listeners(bound.listeners, router, auth)));
927    verify_serving(&bound.connection_info).await?;
928    // A published endpoint promises that authentication can complete now.
929    // Orphan cleanup and control-plane construction can take longer than an
930    // existing-daemon probe's deadline, so finish both before discovery.
931    write_atomic(&bound.connection_file_path, &bound.connection_info).map_err(|source| {
932        BootstrapError::ConnectionFileWrite {
933            path: bound.connection_file_path.clone(),
934            source,
935        }
936    })?;
937    drop(publication_lock);
938    let _clock_step_task = AbortOnDrop::new(crate::watchdog::spawn_clock_step_monitor());
939    // Off the startup path: it runs `systemctl`, and only ever logs.
940    #[cfg(target_os = "linux")]
941    let _kill_mode_check = AbortOnDrop::new(tokio::spawn(
942        crate::systemd_kill_mode::warn_if_kill_mode_defeats_ordered_shutdown(),
943    ));
944    let _watchdog_task = AbortOnDrop::new(
945        DaemonSelfWatchdog::new(
946            bound.connection_info.clone(),
947            bound.connection_file_path.clone(),
948        )
949        .with_config(watchdog_config)
950        .spawn(),
951    );
952
953    for configured in configured_modules {
954        let enabled = configured.enabled;
955        let health = configured.health.clone();
956        let module_id = configured.module_id.clone();
957        match supervisor.supervise_configured_with_health(
958            configured.module_spec(),
959            enabled,
960            health.clone(),
961            configured.drain_timeout_ms,
962            configured.restart,
963        ) {
964            Ok(_) => {
965                // A raised failure threshold is normally a temporary allowance for a
966                // drive that deliberately stops a module, and it widens the window in
967                // which a genuinely wedged module looks fine. It is only ever noticed
968                // when someone thinks to re-read the config, so a relaxation outlives
969                // its reason silently: a rig ran five days at 240s of tolerance against
970                // a 90s default because a comment promising a revert was mistaken for
971                // the revert. Saying so on every boot costs one line and removes the
972                // need for anyone to remember.
973                let default_threshold = HealthConfig::default().failure_threshold;
974                if enabled && health.failure_threshold > default_threshold {
975                    warn!(
976                        module_id = %module_id,
977                        failure_threshold = health.failure_threshold,
978                        default_threshold,
979                        tolerance_secs = health.cadence.as_secs() * u64::from(health.failure_threshold),
980                        "health failure threshold is relaxed above the default; a wedged module stays unflagged for longer"
981                    );
982                }
983                info!(module_id = %module_id, enabled, "configured module supervised");
984            }
985            Err(err) => {
986                error!(module_id = %module_id, error = %err, "failed to supervise configured module; continuing daemon startup");
987            }
988        }
989    }
990
991    control.refresh_capability_requirements();
992    Arc::clone(&control).spawn_capability_deadline_loop();
993    Arc::clone(&control).spawn_scope_expiry_loop();
994
995    #[cfg(unix)]
996    {
997        tokio::select! {
998            result = serve_task.join() => {
999                return result.map_err(BootstrapError::ServeJoin)?.map_err(BootstrapError::Serve);
1000            }
1001            _ = terminate.recv() => {}
1002        }
1003        // First, before the notice, the drain, or any connection close: from
1004        // here on a module exit is recorded as `daemon_shutdown` and never
1005        // respawned. Also before allowing a second signal to cut the bounded
1006        // wait short, so the journal marker is always written.
1007        supervisor.begin_daemon_shutdown();
1008        // Stop the self-watchdog before closing the listener. Its next tick would
1009        // connect to that listener, fail, and log an ERROR indistinguishable from
1010        // a wedged daemon, once for every interval a planned stop lasts.
1011        drop(_watchdog_task);
1012        // Dropping the listener stops new accepts, not established connections:
1013        // their detached tasks must remain live throughout notice and drain.
1014        drop(serve_task);
1015        let escalated = tokio::select! {
1016            biased;
1017            _ = terminate.recv() => {
1018                info!("second SIGTERM: abandoning daemon shutdown wait");
1019                true
1020            }
1021            result = supervisor.drain_for_daemon_shutdown() => {
1022                if let Err(error) = result {
1023                    warn!(%error, "daemon shutdown drain failed; exiting anyway");
1024                }
1025                false
1026            }
1027        };
1028        // Supervised modules lead their own process groups, so a service
1029        // manager's kill of this process's group does not reach them. (A
1030        // systemd unit with KillMode=control-group kills by cgroup instead and
1031        // does reach them; see `systemd_kill_mode`.) The daemon ends them
1032        // itself: EOF (or SIGTERM for a protocol none child) first, then
1033        // signals at each child's own drain deadline.
1034        supervisor
1035            .end_children_for_daemon_shutdown(escalated, async {
1036                terminate.recv().await;
1037            })
1038            .await;
1039        Ok(())
1040    }
1041    #[cfg(not(unix))]
1042    serve_task
1043        .join()
1044        .await
1045        .map_err(BootstrapError::ServeJoin)?
1046        .map_err(BootstrapError::Serve)
1047}
1048
1049fn normalized_build_provenance(value: &str) -> Option<String> {
1050    match value.trim() {
1051        "" | "unavailable" => None,
1052        value => Some(value.to_string()),
1053    }
1054}
1055
1056/// Find an existing daemon or atomically bind loopback TCP for this daemon.
1057///
1058/// The algorithm is intentionally connect-first: an endpoint from the connection
1059/// file is treated as live only after the TCP+key server-proof authenticates for
1060/// that file's key and daemon_id. Stale or foreign connection files are reclaimed
1061/// only while holding the per-user start lock; the TCP port is never the
1062/// singleton primitive.
1063pub async fn ensure_singleton(
1064    connection_file_path: impl AsRef<Path>,
1065    port: u16,
1066) -> Result<Outcome, BootstrapError> {
1067    ensure_singleton_with_config(BootstrapConfig::new(connection_file_path.as_ref(), port)).await
1068}
1069
1070pub async fn ensure_singleton_with_config(
1071    config: BootstrapConfig,
1072) -> Result<Outcome, BootstrapError> {
1073    ensure_singleton_inner(config, true).await
1074}
1075
1076async fn ensure_singleton_inner(
1077    config: BootstrapConfig,
1078    publish: bool,
1079) -> Result<Outcome, BootstrapError> {
1080    let path = config.connection_file_path;
1081
1082    if matches!(probe_existing(&path).await?, Probe::Live) {
1083        return Ok(Outcome::AlreadyRunning);
1084    }
1085
1086    let lock = StartLock::acquire(&path).await?;
1087
1088    // Re-probe after acquiring the start lock so a peer that won the race between
1089    // our first failed probe and the lock acquisition is observed instead of
1090    // overwritten.
1091    if matches!(probe_existing(&path).await?, Probe::Live) {
1092        return Ok(Outcome::AlreadyRunning);
1093    }
1094
1095    // The start lock and the probe above only rule out a daemon using this
1096    // connection file. A daemon started with another runtime directory but
1097    // the same data home shares the run directory while passing both, so the
1098    // run directory needs its own owner: refuse at once if a live daemon holds
1099    // it, before anything below reads or writes run or data-home state.
1100    let run_dir_lock = config
1101        .live_children_path
1102        .as_deref()
1103        .map(crate::run_dir_lock::RunDirLock::acquire)
1104        .transpose()?;
1105
1106    remove_stale_connection_file_if_present(&path)?;
1107
1108    // Established under the start lock, and under the run-directory lock when
1109    // there is one, and before binding: a corrupt file stops boot before
1110    // anything is published, and two daemons racing to start cannot both mint.
1111    let machine_id = config
1112        .machine_id_path
1113        .as_deref()
1114        .map(crate::machine_id::load_or_mint)
1115        .transpose()
1116        .map_err(BootstrapError::MachineId)?;
1117
1118    let (listeners, endpoints) = bind_loopback(config.port).await?;
1119    let connection_info = ConnectionInfo {
1120        schema: SCHEMA_VERSION,
1121        wire_version: Some(PROTOCOL_VERSION),
1122        endpoints,
1123        key: generate_key().map_err(BootstrapError::GenerateConnectionFile)?,
1124        daemon_id: generate_daemon_id().map_err(BootstrapError::GenerateConnectionFile)?,
1125        pid: process::id(),
1126        daemon_ver: config.daemon_ver,
1127    };
1128
1129    // The low-level binding API historically publishes for callers that serve
1130    // these listeners themselves. The daemon entry point defers publication
1131    // until its own authentication server is running.
1132    if publish {
1133        if let Err(source) = write_atomic(&path, &connection_info) {
1134            drop(listeners);
1135            return Err(BootstrapError::ConnectionFileWrite { path, source });
1136        }
1137    }
1138
1139    Ok(Outcome::Bound(BoundDaemon {
1140        listeners,
1141        connection_info,
1142        connection_file_path: path,
1143        connection_file_source: config.connection_file_source,
1144        machine_id,
1145        run_dir_lock,
1146        publication_lock: (!publish).then_some(lock),
1147    }))
1148}
1149
1150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1151enum Probe {
1152    Live,
1153    StaleOrAbsent,
1154}
1155
1156async fn probe_existing(path: &Path) -> Result<Probe, BootstrapError> {
1157    let info = match connection_file::read(path) {
1158        Ok(info) => info,
1159        Err(source) if is_absent_or_stale_connection_file(&source) => {
1160            return Ok(Probe::StaleOrAbsent)
1161        }
1162        Err(source) => {
1163            return Err(BootstrapError::ConnectionFileRead {
1164                path: path.to_path_buf(),
1165                source,
1166            })
1167        }
1168    };
1169
1170    for endpoint in &info.endpoints {
1171        if matches!(probe_endpoint(&info, endpoint).await, Probe::Live) {
1172            return Ok(Probe::Live);
1173        }
1174    }
1175
1176    Ok(Probe::StaleOrAbsent)
1177}
1178
1179async fn probe_endpoint(info: &ConnectionInfo, endpoint: &Endpoint) -> Probe {
1180    let Ok(ip) = endpoint.host.parse::<IpAddr>() else {
1181        return Probe::StaleOrAbsent;
1182    };
1183    if !ip.is_loopback() {
1184        return Probe::StaleOrAbsent;
1185    }
1186    let addr = SocketAddr::new(ip, endpoint.port);
1187
1188    let mut stream = match timeout(CONNECT_TIMEOUT, TcpStream::connect(addr)).await {
1189        Ok(Ok(stream)) => stream,
1190        Ok(Err(_)) | Err(_) => return Probe::StaleOrAbsent,
1191    };
1192
1193    match authenticate_client(&mut stream, info, PROBE_AUTH_DEADLINE).await {
1194        Ok(()) => Probe::Live,
1195        Err(AuthError::DaemonIdMismatch)
1196        | Err(AuthError::InvalidServerProof)
1197        | Err(AuthError::UnexpectedEof { .. })
1198        | Err(AuthError::Timeout { .. })
1199        | Err(AuthError::JsonEncode { .. })
1200        | Err(AuthError::JsonDecode { .. })
1201        | Err(AuthError::Io { .. })
1202        | Err(AuthError::MessageTooLarge { .. })
1203        | Err(AuthError::KeyTooShort { .. })
1204        | Err(AuthError::Random(_))
1205        | Err(AuthError::InvalidClientAuth) => Probe::StaleOrAbsent,
1206    }
1207}
1208
1209async fn verify_serving(info: &ConnectionInfo) -> Result<(), BootstrapError> {
1210    // Scheduling a server task is not a readiness barrier. Authenticate once
1211    // against the mandatory IPv4 listener before exposing discovery to peers.
1212    // This uses the normal probe deadline, with no retries or widened budget.
1213    if let Some(endpoint) = info.endpoints.first() {
1214        if matches!(probe_endpoint(info, endpoint).await, Probe::Live) {
1215            return Ok(());
1216        }
1217    }
1218    Err(BootstrapError::StartupNotServing)
1219}
1220
1221fn is_absent_or_stale_connection_file(err: &ConnectionFileError) -> bool {
1222    match err {
1223        ConnectionFileError::Io { source, .. } if source.kind() == io::ErrorKind::NotFound => true,
1224        ConnectionFileError::JsonRead { .. }
1225        | ConnectionFileError::UnsupportedSchema { .. }
1226        | ConnectionFileError::Invalid { .. }
1227        | ConnectionFileError::KeyTooShort { .. }
1228        // A live daemon always publishes the file owner-only (0600), so a file
1229        // with insecure permissions is never a daemon we should defer to: treat it
1230        // as stale and take over (which republishes a correct 0600 file).
1231        | ConnectionFileError::InsecurePermissions { .. } => true,
1232        ConnectionFileError::MissingParent { .. }
1233        | ConnectionFileError::MissingFileName { .. }
1234        // A writable ancestor is an operator misconfiguration, never evidence
1235        // about whether a daemon is live. Reclaiming the file would republish key
1236        // material into the same directory the refusal is about.
1237        | ConnectionFileError::InsecureParentDirectory { .. }
1238        | ConnectionFileError::Io { .. }
1239        | ConnectionFileError::JsonWrite { .. }
1240        | ConnectionFileError::Random(_)
1241        // A wire mismatch may identify a newer live daemon, so never reclaim its
1242        // connection file merely because this binary cannot speak its envelope.
1243        | ConnectionFileError::WireVersionMismatch { .. } => false,
1244    }
1245}
1246
1247fn remove_stale_connection_file_if_present(path: &Path) -> Result<(), BootstrapError> {
1248    match fs::remove_file(path) {
1249        Ok(()) => Ok(()),
1250        Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()),
1251        Err(source) => Err(BootstrapError::RemoveStale {
1252            path: path.to_path_buf(),
1253            source,
1254        }),
1255    }
1256}
1257
1258async fn bind_loopback(port: u16) -> Result<(Vec<TcpListener>, Vec<Endpoint>), BootstrapError> {
1259    bind_loopback_with(port, |port| TcpListener::bind((Ipv6Addr::LOCALHOST, port))).await
1260}
1261
1262async fn bind_loopback_with<F, Fut>(
1263    port: u16,
1264    mut bind_v6: F,
1265) -> Result<(Vec<TcpListener>, Vec<Endpoint>), BootstrapError>
1266where
1267    F: FnMut(u16) -> Fut,
1268    Fut: std::future::Future<Output = io::Result<TcpListener>>,
1269{
1270    let v4_host = Ipv4Addr::LOCALHOST;
1271    let v4 = TcpListener::bind((v4_host, port))
1272        .await
1273        .map_err(|source| BootstrapError::Bind {
1274            host: v4_host.to_string(),
1275            port,
1276            source,
1277        })?;
1278    let actual_port = v4
1279        .local_addr()
1280        .map_err(|source| BootstrapError::LocalAddr {
1281            host: v4_host.to_string(),
1282            source,
1283        })?
1284        .port();
1285
1286    let mut listeners = vec![v4];
1287    let mut endpoints = vec![Endpoint {
1288        host: v4_host.to_string(),
1289        port: actual_port,
1290    }];
1291
1292    let v6_host = Ipv6Addr::LOCALHOST;
1293    match bind_v6(actual_port).await {
1294        Ok(v6) => {
1295            listeners.push(v6);
1296            endpoints.push(Endpoint {
1297                host: v6_host.to_string(),
1298                port: actual_port,
1299            });
1300        }
1301        Err(err)
1302            if ipv6_loopback_unavailable(&err)
1303                || (port == 0 && err.kind() == io::ErrorKind::AddrInUse) =>
1304        {
1305            warn!(
1306                port = actual_port,
1307                error = %err,
1308                "IPv6 loopback unavailable; serving only IPv4 loopback"
1309            );
1310        }
1311        Err(source) => {
1312            drop(listeners);
1313            return Err(BootstrapError::Bind {
1314                host: v6_host.to_string(),
1315                port: actual_port,
1316                source,
1317            });
1318        }
1319    }
1320
1321    Ok((listeners, endpoints))
1322}
1323
1324fn ipv6_loopback_unavailable(err: &io::Error) -> bool {
1325    matches!(
1326        err.kind(),
1327        io::ErrorKind::AddrNotAvailable | io::ErrorKind::Unsupported
1328    ) || matches!(err.raw_os_error(), Some(47) | Some(49) | Some(97))
1329}
1330
1331struct AbortOnDrop<T> {
1332    handle: JoinHandle<T>,
1333}
1334
1335impl<T> AbortOnDrop<T> {
1336    fn new(handle: JoinHandle<T>) -> Self {
1337        Self { handle }
1338    }
1339
1340    async fn join(&mut self) -> Result<T, JoinError> {
1341        (&mut self.handle).await
1342    }
1343}
1344
1345impl<T> Drop for AbortOnDrop<T> {
1346    fn drop(&mut self) {
1347        if !self.handle.is_finished() {
1348            self.handle.abort();
1349        }
1350    }
1351}
1352
1353#[derive(Debug)]
1354struct StartLock {
1355    // Keep the locked file handle alive for the duration of bootstrap; closing
1356    // it releases the advisory lock while leaving the stable path in place.
1357    _file: fs::File,
1358}
1359
1360impl StartLock {
1361    async fn acquire(connection_file_path: &Path) -> Result<Self, BootstrapError> {
1362        let path = start_lock_path(connection_file_path);
1363        for _ in 0..START_LOCK_RETRIES {
1364            let file = match open_owner_only_lock(&path) {
1365                Ok(file) => file,
1366                Err(source) => return Err(BootstrapError::StartLockCreate { path, source }),
1367            };
1368            match FileExt::try_lock(&file) {
1369                Ok(()) => return Ok(Self { _file: file }),
1370                Err(TryLockError::WouldBlock) => sleep(START_LOCK_RETRY_DELAY).await,
1371                Err(TryLockError::Error(source)) => {
1372                    return Err(BootstrapError::StartLockCreate { path, source });
1373                }
1374            }
1375        }
1376
1377        Err(BootstrapError::StartLockBusy {
1378            path,
1379            attempts: START_LOCK_RETRIES,
1380        })
1381    }
1382}
1383
1384pub(crate) fn open_owner_only_lock(path: &Path) -> io::Result<fs::File> {
1385    let mut options = fs::OpenOptions::new();
1386    options.read(true).write(true).create(true);
1387    #[cfg(unix)]
1388    {
1389        use std::os::unix::fs::OpenOptionsExt;
1390        options.mode(0o600);
1391    }
1392    options.open(path)
1393}
1394
1395fn start_lock_path(connection_file_path: &Path) -> PathBuf {
1396    let file_name = connection_file_path
1397        .file_name()
1398        .map(|name| name.to_string_lossy())
1399        .unwrap_or_else(|| CONNECTION_FILE_NAME.into());
1400    let lock_name = format!("{file_name}.start-lock");
1401    connection_file_path
1402        .parent()
1403        .filter(|parent| !parent.as_os_str().is_empty())
1404        .unwrap_or_else(|| Path::new("."))
1405        .join(lock_name)
1406}
1407
1408fn non_empty_os_var(key: &str) -> Option<OsString> {
1409    let value = env::var_os(key)?;
1410    if value.is_empty() {
1411        None
1412    } else {
1413        Some(value)
1414    }
1415}
1416
1417/// Bootstrap-layer errors are deliberately typed so startup never panics for
1418/// ordinary daemon-discovery races or stale filesystem state.
1419#[derive(Debug)]
1420pub enum BootstrapError {
1421    /// The bound server could not authenticate its own readiness probe.
1422    StartupNotServing,
1423    #[cfg(unix)]
1424    Signal(io::Error),
1425    InvalidPort {
1426        raw: String,
1427        source: std::num::ParseIntError,
1428    },
1429    ConnectionFileRead {
1430        path: PathBuf,
1431        source: ConnectionFileError,
1432    },
1433    ConnectionFileWrite {
1434        path: PathBuf,
1435        source: ConnectionFileError,
1436    },
1437    GenerateConnectionFile(ConnectionFileError),
1438    StartLockCreate {
1439        path: PathBuf,
1440        source: io::Error,
1441    },
1442    StartLockBusy {
1443        path: PathBuf,
1444        attempts: usize,
1445    },
1446    /// The run-directory lock file could not be created, opened or locked.
1447    RunDirLockCreate {
1448        path: PathBuf,
1449        source: io::Error,
1450    },
1451    /// Another live process, almost certainly a daemon started with a
1452    /// different runtime directory over the same data home, owns the run
1453    /// directory. The daemon does not start. `holder_pid` is read from the
1454    /// lock file when possible and is informational only.
1455    RunDirBusy {
1456        path: PathBuf,
1457        holder_pid: Option<u32>,
1458    },
1459    RemoveStale {
1460        path: PathBuf,
1461        source: io::Error,
1462    },
1463    Bind {
1464        host: String,
1465        port: u16,
1466        source: io::Error,
1467    },
1468    LocalAddr {
1469        host: String,
1470        source: io::Error,
1471    },
1472    DaemonConfig(DaemonConfigError),
1473    /// The machine id could not be established: its file is corrupt, unreadable
1474    /// or unwritable, or the data home is relative. The daemon does not start.
1475    MachineId(crate::machine_id::MachineIdFileError),
1476    /// The daemon run directory could not be resolved because the data home is
1477    /// relative. The daemon does not start.
1478    RunDir(daemon_config::DaemonRunDirError),
1479    Serve(ServerError),
1480    ServeJoin(tokio::task::JoinError),
1481}
1482
1483impl fmt::Display for BootstrapError {
1484    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1485        match self {
1486            Self::StartupNotServing => write!(f, "bound daemon did not authenticate its readiness probe; connection file was not published"),
1487            #[cfg(unix)]
1488            Self::Signal(error) => write!(f, "failed to register SIGTERM handler: {error}"),
1489            Self::InvalidPort { raw, source } => {
1490                write!(f, "invalid {SUBC_PORT_ENV} value '{raw}': {source}")
1491            }
1492            Self::ConnectionFileRead { path, source } => write!(
1493                f,
1494                "failed to read connection file {}: {source}",
1495                path.display()
1496            ),
1497            Self::ConnectionFileWrite { path, source } => write!(
1498                f,
1499                "failed to publish connection file {}: {source}",
1500                path.display()
1501            ),
1502            Self::GenerateConnectionFile(err) => {
1503                write!(f, "failed to generate connection-file auth material: {err}")
1504            }
1505            Self::StartLockCreate { path, source } => {
1506                write!(
1507                    f,
1508                    "failed to create start lock {}: {source}",
1509                    path.display()
1510                )
1511            }
1512            Self::StartLockBusy { path, attempts } => write!(
1513                f,
1514                "start lock {} remained busy after {attempts} attempts",
1515                path.display()
1516            ),
1517            Self::RunDirLockCreate { path, source } => write!(
1518                f,
1519                "refusing to start: failed to lock run directory via {}: {source}",
1520                path.display()
1521            ),
1522            Self::RunDirBusy { path, holder_pid } => {
1523                write!(
1524                    f,
1525                    "refusing to start: run directory lock {} is held by another daemon",
1526                    path.display()
1527                )?;
1528                if let Some(pid) = holder_pid {
1529                    write!(f, " (pid {pid})")?;
1530                }
1531                write!(
1532                    f,
1533                    "; a live daemon owns this data home's run state (typically one started with a different XDG_RUNTIME_DIR)"
1534                )
1535            }
1536            Self::RemoveStale { path, source } => write!(
1537                f,
1538                "failed to remove stale connection file {}: {source}",
1539                path.display()
1540            ),
1541            Self::Bind { host, port, source } if source.kind() == io::ErrorKind::AddrInUse => {
1542                write!(
1543                    f,
1544                    "port {port} in use on loopback {host}: {source}; set the port in config"
1545                )
1546            }
1547            Self::Bind { host, port, source } => {
1548                write!(f, "failed to bind loopback TCP {host}:{port}: {source}")
1549            }
1550            Self::LocalAddr { host, source } => {
1551                write!(f, "failed to read local address for {host}: {source}")
1552            }
1553            Self::DaemonConfig(err) => write!(f, "failed to load daemon config: {err}"),
1554            Self::MachineId(err) => write!(f, "refusing to start: {err}"),
1555            Self::RunDir(err) => write!(f, "refusing to start: {err}"),
1556            Self::Serve(err) => write!(f, "daemon server failed: {err}"),
1557            Self::ServeJoin(err) => write!(f, "daemon server task failed: {err}"),
1558        }
1559    }
1560}
1561
1562impl Error for BootstrapError {
1563    fn source(&self) -> Option<&(dyn Error + 'static)> {
1564        match self {
1565            Self::StartupNotServing => None,
1566            #[cfg(unix)]
1567            Self::Signal(source) => Some(source),
1568            Self::InvalidPort { source, .. } => Some(source),
1569            Self::ConnectionFileRead { source, .. }
1570            | Self::ConnectionFileWrite { source, .. }
1571            | Self::GenerateConnectionFile(source) => Some(source),
1572            Self::StartLockCreate { source, .. }
1573            | Self::RunDirLockCreate { source, .. }
1574            | Self::RemoveStale { source, .. }
1575            | Self::Bind { source, .. }
1576            | Self::LocalAddr { source, .. } => Some(source),
1577            Self::DaemonConfig(err) => Some(err),
1578            Self::MachineId(err) => Some(err),
1579            Self::RunDir(err) => Some(err),
1580            Self::Serve(err) => Some(err),
1581            Self::ServeJoin(err) => Some(err),
1582            Self::StartLockBusy { .. } | Self::RunDirBusy { .. } => None,
1583        }
1584    }
1585}
1586
1587#[cfg(test)]
1588mod tests {
1589    mod activity {
1590        include!("activity_bootstrap_tests.rs");
1591    }
1592    use super::*;
1593    use crate::server::ServerAuth;
1594    use cortexkit_test_support::ScratchDir;
1595    #[cfg(target_os = "linux")]
1596    use std::collections::BTreeSet;
1597    use std::sync::Mutex;
1598    #[cfg(target_os = "linux")]
1599    use subc_control::ModuleProtocol;
1600    use subc_transport::MIN_KEY_LEN;
1601    use tokio::io::AsyncReadExt;
1602    use tokio::task::JoinHandle;
1603
1604    #[cfg(unix)]
1605    use std::os::unix::fs::PermissionsExt;
1606
1607    static ENV_LOCK: Mutex<()> = Mutex::new(());
1608
1609    #[test]
1610    fn normalized_build_provenance_preserves_real_values() {
1611        assert_eq!(normalized_build_provenance("abc"), Some("abc".to_string()));
1612    }
1613
1614    #[test]
1615    fn normalized_build_provenance_omits_unavailable_and_empty_values() {
1616        assert_eq!(normalized_build_provenance("unavailable"), None);
1617        assert_eq!(normalized_build_provenance(""), None);
1618    }
1619
1620    #[cfg(target_os = "linux")]
1621    fn current_cgroup_path_for_test() -> io::Result<PathBuf> {
1622        let cgroups = fs::read_to_string("/proc/self/cgroup")?;
1623        let relative = cgroups
1624            .lines()
1625            .find_map(|line| line.strip_prefix("0::"))
1626            .ok_or_else(|| {
1627                io::Error::new(io::ErrorKind::Unsupported, "cgroup v2 is unavailable")
1628            })?;
1629        Ok(Path::new("/sys/fs/cgroup").join(relative.trim_start_matches('/')))
1630    }
1631
1632    #[cfg(target_os = "linux")]
1633    fn module_cgroup_directories() -> io::Result<BTreeSet<OsString>> {
1634        let modules = current_cgroup_path_for_test()?.join("subc-modules");
1635        let entries = match fs::read_dir(modules) {
1636            Ok(entries) => entries,
1637            Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(BTreeSet::new()),
1638            Err(error) => return Err(error),
1639        };
1640        let mut directories = BTreeSet::new();
1641        for entry in entries {
1642            let entry = entry?;
1643            if entry.file_type()?.is_dir() {
1644                directories.insert(entry.file_name());
1645            }
1646        }
1647        Ok(directories)
1648    }
1649
1650    #[cfg(target_os = "linux")]
1651    async fn wait_for_path(path: &Path, task: &JoinHandle<Result<(), BootstrapError>>) {
1652        let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
1653        while !path.exists() && tokio::time::Instant::now() < deadline {
1654            assert!(
1655                !task.is_finished(),
1656                "daemon exited before creating {}",
1657                path.display()
1658            );
1659            sleep(Duration::from_millis(10)).await;
1660        }
1661        assert!(path.exists(), "daemon did not create {}", path.display());
1662    }
1663
1664    /// An in-process daemon with the default (disabled) cgroup placement must
1665    /// leave the host's live module cgroup tree exactly as it found it.
1666    /// Red-checking this test (making it fail) creates a
1667    /// `cgroup-isolation-probe-*` directory in the live cgroup tree, which must be removed by hand.
1668    #[cfg(target_os = "linux")]
1669    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1670    async fn run_with_config_does_not_reconcile_the_ambient_cgroup_by_default() {
1671        let temp = unique_temp_dir("bootstrap-cgroup-default-disabled");
1672        let module_id = format!("cgroup-isolation-probe-{}", process::id());
1673        let before = module_cgroup_directories().expect("read ambient module cgroups before boot");
1674        assert!(
1675            !before.contains(&OsString::from(&module_id)),
1676            "isolation probe cgroup already exists before this daemon starts"
1677        );
1678        let capture = temp.join("logs").join(format!("{module_id}.stderr.log"));
1679        let module = ConfiguredModule {
1680            module_id,
1681            program: PathBuf::from("sh"),
1682            args: vec!["-c".to_string(), "sleep 30".to_string()],
1683            env: Vec::new(),
1684            log: None,
1685            enabled: true,
1686            reserved: false,
1687            reserved_prefixes: Vec::new(),
1688            protocol: ModuleProtocol::None,
1689            overlap: Default::default(),
1690            health: HealthConfig::default(),
1691            drain_timeout_ms: None,
1692            route_bind_relay_timeout_ms: None,
1693            restart: RestartPolicy::default(),
1694        };
1695        let config = BootstrapConfig::new(temp.join("connection.json"), 0)
1696            .with_configured_modules([module])
1697            .with_capture_logs_dir(temp.join("logs"))
1698            .with_terminal_journal_path(temp.join("terminals.jsonl"));
1699        let task = tokio::spawn(run_with_config(config));
1700
1701        wait_for_path(&capture, &task).await;
1702        let after = module_cgroup_directories().expect("read ambient module cgroups after boot");
1703
1704        task.abort();
1705        assert!(task
1706            .await
1707            .expect_err("aborted daemon task must cancel")
1708            .is_cancelled());
1709        assert_eq!(
1710            before, after,
1711            "an in-process daemon must not create or reconcile ambient module cgroups"
1712        );
1713    }
1714
1715    #[cfg(target_os = "linux")]
1716    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1717    async fn explicit_cgroup_root_is_prepared_inside_the_fixture_tree() {
1718        let temp = unique_temp_dir("bootstrap-cgroup-explicit-root");
1719        let cgroup_root = temp.join("cgroup");
1720        fs::create_dir(&cgroup_root).expect("create scratch cgroup root");
1721        fs::write(cgroup_root.join("cgroup.procs"), b"").expect("write scratch cgroup marker");
1722        let modules = cgroup_root.join("subc-modules");
1723        let config = BootstrapConfig::new(temp.join("connection.json"), 0)
1724            .with_cgroup_placement(CgroupPlacementConfig::Root(cgroup_root))
1725            .with_terminal_journal_path(temp.join("terminals.jsonl"));
1726        let task = tokio::spawn(run_with_config(config));
1727
1728        wait_for_path(&modules, &task).await;
1729
1730        task.abort();
1731        assert!(task
1732            .await
1733            .expect_err("aborted daemon task must cancel")
1734            .is_cancelled());
1735        assert!(
1736            fs::read_dir(&modules)
1737                .expect("read prepared modules directory")
1738                .next()
1739                .is_none(),
1740            "the delegation probe must clean up after itself"
1741        );
1742    }
1743
1744    struct EnvGuard {
1745        key: &'static str,
1746        previous: Option<OsString>,
1747    }
1748
1749    impl EnvGuard {
1750        fn set(key: &'static str, value: &Path) -> Self {
1751            let previous = env::var_os(key);
1752            env::set_var(key, value);
1753            Self { key, previous }
1754        }
1755
1756        fn set_str(key: &'static str, value: &str) -> Self {
1757            let previous = env::var_os(key);
1758            env::set_var(key, value);
1759            Self { key, previous }
1760        }
1761
1762        fn unset(key: &'static str) -> Self {
1763            let previous = env::var_os(key);
1764            env::remove_var(key);
1765            Self { key, previous }
1766        }
1767    }
1768
1769    impl Drop for EnvGuard {
1770        fn drop(&mut self) {
1771            match &self.previous {
1772                Some(value) => env::set_var(self.key, value),
1773                None => env::remove_var(self.key),
1774            }
1775        }
1776    }
1777
1778    fn unique_temp_dir(name: &str) -> ScratchDir {
1779        ScratchDir::new(name)
1780    }
1781
1782    fn temp_connection_file_path(name: &str) -> (ScratchDir, PathBuf) {
1783        let dir = unique_temp_dir(name);
1784        let path = dir.join("conn.json");
1785        (dir, path)
1786    }
1787
1788    fn auth_for(info: &ConnectionInfo) -> ServerAuth {
1789        ServerAuth::new(info.key.clone(), info.daemon_id, info.daemon_ver.clone())
1790    }
1791
1792    fn start_server(bound: BoundDaemon) -> JoinHandle<Result<(), ServerError>> {
1793        let auth = auth_for(&bound.connection_info);
1794        tokio::spawn(serve_listeners(
1795            bound.listeners,
1796            Arc::new(Router::with_default_self_handler()),
1797            auth,
1798        ))
1799    }
1800
1801    fn expect_bound(outcome: Outcome) -> BoundDaemon {
1802        match outcome {
1803            Outcome::Bound(bound) => bound,
1804            Outcome::AlreadyRunning => panic!("fresh connection file unexpectedly had a daemon"),
1805        }
1806    }
1807
1808    async fn connect_from_info(conn: &ConnectionInfo) -> io::Result<TcpStream> {
1809        let endpoint = conn
1810            .endpoints
1811            .first()
1812            .expect("test connection file should have an endpoint");
1813        let ip: IpAddr = endpoint.host.parse().unwrap();
1814        TcpStream::connect(SocketAddr::new(ip, endpoint.port)).await
1815    }
1816
1817    fn make_connection_info(port: u16) -> ConnectionInfo {
1818        ConnectionInfo {
1819            schema: SCHEMA_VERSION,
1820            wire_version: Some(PROTOCOL_VERSION),
1821            endpoints: vec![Endpoint {
1822                host: "127.0.0.1".to_owned(),
1823                port,
1824            }],
1825            key: generate_key().unwrap(),
1826            daemon_id: generate_daemon_id().unwrap(),
1827            pid: process::id(),
1828            daemon_ver: "test-subc".to_owned(),
1829        }
1830    }
1831
1832    fn write_raw_owner_only_connection_file(path: &Path, contents: &[u8]) {
1833        fs::write(path, contents).unwrap();
1834        #[cfg(unix)]
1835        fs::set_permissions(path, fs::Permissions::from_mode(0o600)).unwrap();
1836    }
1837
1838    fn assert_owner_only_connection_file(path: &Path) {
1839        // `path` is only inspected on Unix (mode bits); on Windows the owner-only
1840        // guarantee comes from the inherited %TEMP% ACL, nothing to assert here.
1841        #[cfg(unix)]
1842        {
1843            let mode = fs::metadata(path).unwrap().permissions().mode() & 0o777;
1844            assert_eq!(mode, 0o600);
1845        }
1846        #[cfg(not(unix))]
1847        let _ = path;
1848    }
1849
1850    #[test]
1851    fn connection_file_path_uses_xdg_runtime_dir_when_set() {
1852        let _env_lock = ENV_LOCK.lock().unwrap();
1853        let runtime_dir = unique_temp_dir("xdg-runtime");
1854        let _xdg = EnvGuard::set("XDG_RUNTIME_DIR", runtime_dir.path());
1855
1856        assert_eq!(
1857            connection_file_path(),
1858            runtime_dir.join(CONNECTION_FILE_NAME)
1859        );
1860    }
1861
1862    #[test]
1863    fn connection_file_path_source_is_xdg_runtime_dir_when_set() {
1864        let runtime_dir = OsString::from("/run/user/1000");
1865
1866        let (path, source) = connection_file_path_with_source(Some(runtime_dir));
1867
1868        assert_eq!(
1869            path,
1870            PathBuf::from("/run/user/1000").join(CONNECTION_FILE_NAME)
1871        );
1872        assert_eq!(source, ConnectionFileSource::XdgRuntimeDir);
1873    }
1874
1875    #[test]
1876    fn connection_file_path_falls_back_to_temp_dir_with_user_token_when_xdg_unset() {
1877        let _env_lock = ENV_LOCK.lock().unwrap();
1878        let _xdg = EnvGuard::unset("XDG_RUNTIME_DIR");
1879
1880        assert_eq!(
1881            connection_file_path(),
1882            env::temp_dir().join(format!("subc-{}.connection.json", user_connection_token()))
1883        );
1884    }
1885
1886    #[test]
1887    fn connection_file_path_source_is_temp_dir_when_xdg_unset() {
1888        let (path, source) = connection_file_path_with_source(None);
1889
1890        assert_eq!(
1891            path,
1892            env::temp_dir().join(format!("subc-{}.connection.json", user_connection_token()))
1893        );
1894        assert_eq!(source, ConnectionFileSource::TempDirFallback);
1895    }
1896
1897    /// Concurrent callers must derive one token. The former temp-file uid probe
1898    /// could fail transiently (same-tick name collision, fd exhaustion) and send
1899    /// the loser down the env-derived fallback with a different identity for the
1900    /// same user; this fence keeps identity independent of filesystem luck.
1901    #[test]
1902    fn user_connection_token_is_stable_under_concurrent_callers() {
1903        let expected = user_connection_token();
1904        let workers: Vec<_> = (0..32)
1905            .map(|_| {
1906                std::thread::spawn(|| (0..40).map(|_| user_connection_token()).collect::<Vec<_>>())
1907            })
1908            .collect();
1909        for worker in workers {
1910            for token in worker.join().expect("probe thread") {
1911                assert_eq!(token, expected, "token diverged under concurrent probes");
1912            }
1913        }
1914    }
1915
1916    #[test]
1917    fn connection_file_path_source_is_temp_dir_when_xdg_empty() {
1918        let (path, source) = connection_file_path_with_source(Some(OsString::new()));
1919
1920        assert_eq!(
1921            path,
1922            env::temp_dir().join(format!("subc-{}.connection.json", user_connection_token()))
1923        );
1924        assert_eq!(source, ConnectionFileSource::TempDirFallback);
1925    }
1926
1927    /// The shipped binary is the only caller that journals terminal exits into
1928    /// the real run directory, so its constructor must supply that path itself;
1929    /// the in-process default leaves it unset.
1930    #[test]
1931    fn daemon_binary_config_journals_and_captures_into_the_run_dir() {
1932        let _env_lock = ENV_LOCK.lock().unwrap();
1933        let root = unique_temp_dir("daemon-binary-config");
1934        let data_home = root.join("data");
1935        let _data = EnvGuard::set("XDG_DATA_HOME", &data_home);
1936        let _config = EnvGuard::set("XDG_CONFIG_HOME", &root.join("config"));
1937        let _port = EnvGuard::unset(SUBC_PORT_ENV);
1938
1939        let config = BootstrapConfig::from_env_for_daemon_binary().unwrap();
1940
1941        let run_dir = data_home.join("cortexkit").join("run");
1942        assert_eq!(
1943            config.terminal_journal_path,
1944            Some(run_dir.join("terminals.jsonl"))
1945        );
1946        assert_eq!(config.capture_logs_dir, Some(run_dir.join("logs")));
1947        assert_eq!(
1948            BootstrapConfig::new(root.join("connection.json"), 0).terminal_journal_path,
1949            None,
1950            "an in-process config must not journal anywhere unless asked to"
1951        );
1952    }
1953
1954    #[test]
1955    fn daemon_binary_config_refuses_a_relative_data_home() {
1956        let _env_lock = ENV_LOCK.lock().unwrap();
1957        let root = unique_temp_dir("daemon-binary-relative-data");
1958        let _data = EnvGuard::set_str("XDG_DATA_HOME", "relative-data-home");
1959        let _config = EnvGuard::set("XDG_CONFIG_HOME", &root.join("config"));
1960        let _port = EnvGuard::unset(SUBC_PORT_ENV);
1961
1962        let error = BootstrapConfig::from_env_for_daemon_binary()
1963            .expect_err("a relative data home must refuse the daemon binary's config");
1964        assert!(
1965            matches!(error, BootstrapError::RunDir(_)),
1966            "expected a run-directory refusal, got {error}"
1967        );
1968        assert!(error.to_string().contains("XDG_DATA_HOME"), "{error}");
1969    }
1970
1971    #[test]
1972    fn configured_port_uses_default_config_and_env_override() {
1973        let _env_lock = ENV_LOCK.lock().unwrap();
1974        let (_dir, conn_path) = temp_connection_file_path("daemon-config-port");
1975        let config_path = conn_path.with_file_name("subc.jsonc");
1976
1977        let _port = EnvGuard::unset(SUBC_PORT_ENV);
1978        assert_eq!(
1979            BootstrapConfig::from_env_with_daemon_config_path(&config_path)
1980                .unwrap()
1981                .port,
1982            DEFAULT_SUBC_PORT
1983        );
1984
1985        fs::write(&config_path, r#"{ "version": 1, "port": 8123 }"#).unwrap();
1986        assert_eq!(
1987            BootstrapConfig::from_env_with_daemon_config_path(&config_path)
1988                .unwrap()
1989                .port,
1990            8123
1991        );
1992
1993        let _port = EnvGuard::set_str(SUBC_PORT_ENV, "9012");
1994        assert_eq!(
1995            BootstrapConfig::from_env_with_daemon_config_path(&config_path)
1996                .unwrap()
1997                .port,
1998            9012
1999        );
2000    }
2001
2002    #[tokio::test]
2003    async fn second_singleton_probe_against_served_tcp_daemon_reports_already_running() {
2004        let (_dir, path) = temp_connection_file_path("already-running");
2005
2006        let bound = expect_bound(ensure_singleton(&path, 0).await.unwrap());
2007        let server = start_server(bound);
2008
2009        let second = ensure_singleton(&path, 0).await.unwrap();
2010        assert!(matches!(second, Outcome::AlreadyRunning));
2011
2012        server.abort();
2013        let _ = server.await;
2014    }
2015
2016    #[tokio::test]
2017    async fn daemon_connection_file_publishes_protocol_wire_version() {
2018        let (_dir, path) = temp_connection_file_path("wire-version");
2019        let bound = expect_bound(ensure_singleton(&path, 0).await.unwrap());
2020        assert_eq!(bound.connection_info.wire_version, Some(PROTOCOL_VERSION));
2021        assert_eq!(
2022            connection_file::read(&path).unwrap().wire_version,
2023            Some(PROTOCOL_VERSION)
2024        );
2025
2026        drop(bound.listeners);
2027    }
2028
2029    #[tokio::test]
2030    async fn stale_unbound_connection_file_is_reclaimed() {
2031        let (_dir, path) = temp_connection_file_path("stale-reclaim");
2032        let stale = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
2033        let stale_port = stale.local_addr().unwrap().port();
2034        drop(stale);
2035        let stale_info = make_connection_info(stale_port);
2036        write_atomic(&path, &stale_info).unwrap();
2037
2038        let bound = expect_bound(ensure_singleton(&path, 0).await.unwrap());
2039        assert_ne!(bound.connection_info.key, stale_info.key);
2040        drop(bound.listeners);
2041    }
2042
2043    #[cfg(unix)]
2044    #[tokio::test]
2045    async fn ensure_singleton_reclaims_insecure_connection_file() {
2046        let (_dir, path) = temp_connection_file_path("insecure-reclaim");
2047        let stale = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
2048        let stale_port = stale.local_addr().unwrap().port();
2049        drop(stale);
2050        let stale_info = make_connection_info(stale_port);
2051        write_atomic(&path, &stale_info).unwrap();
2052        fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).unwrap();
2053
2054        let bound = expect_bound(ensure_singleton(&path, 0).await.unwrap());
2055        assert_ne!(bound.connection_info.key, stale_info.key);
2056        assert_ne!(bound.connection_info.daemon_id, stale_info.daemon_id);
2057        assert_owner_only_connection_file(&path);
2058
2059        drop(bound.listeners);
2060    }
2061
2062    #[tokio::test]
2063    async fn ensure_singleton_reclaims_non_loopback_connection_file() {
2064        let (_dir, path) = temp_connection_file_path("non-loopback-reclaim");
2065        let mut stale_info = make_connection_info(8757);
2066        stale_info.endpoints = vec![Endpoint {
2067            host: "192.0.2.10".to_owned(),
2068            port: 8757,
2069        }];
2070        write_atomic(&path, &stale_info).unwrap();
2071
2072        let bound = expect_bound(ensure_singleton(&path, 0).await.unwrap());
2073        assert_ne!(bound.connection_info.key, stale_info.key);
2074        assert_ne!(bound.connection_info.daemon_id, stale_info.daemon_id);
2075        assert!(bound
2076            .connection_info
2077            .endpoints
2078            .iter()
2079            .all(|endpoint| endpoint.host.parse::<IpAddr>().unwrap().is_loopback()));
2080        assert_owner_only_connection_file(&path);
2081
2082        drop(bound.listeners);
2083    }
2084
2085    #[tokio::test]
2086    async fn ensure_singleton_reclaims_invalid_connection_file_shapes() {
2087        let mut unsupported_schema = make_connection_info(8757);
2088        unsupported_schema.schema = SCHEMA_VERSION + 1;
2089
2090        let mut empty_endpoints = make_connection_info(8757);
2091        empty_endpoints.endpoints.clear();
2092
2093        let mut short_key = make_connection_info(8757);
2094        short_key.key = vec![0x5A; MIN_KEY_LEN - 1];
2095
2096        let cases = vec![
2097            (
2098                "unsupported-schema",
2099                serde_json::to_vec(&unsupported_schema).unwrap(),
2100                Some(unsupported_schema),
2101            ),
2102            (
2103                "empty-endpoints",
2104                serde_json::to_vec(&empty_endpoints).unwrap(),
2105                Some(empty_endpoints),
2106            ),
2107            (
2108                "short-key",
2109                serde_json::to_vec(&short_key).unwrap(),
2110                Some(short_key),
2111            ),
2112            ("invalid-json", b"{not valid connection json".to_vec(), None),
2113        ];
2114
2115        for (label, contents, old_info) in cases {
2116            let (_dir, path) = temp_connection_file_path(label);
2117            write_raw_owner_only_connection_file(&path, &contents);
2118
2119            let bound = expect_bound(ensure_singleton(&path, 0).await.unwrap());
2120            if let Some(old_info) = old_info {
2121                assert_ne!(bound.connection_info.key, old_info.key, "{label}");
2122                assert_ne!(
2123                    bound.connection_info.daemon_id, old_info.daemon_id,
2124                    "{label}"
2125                );
2126            }
2127            assert!(bound.connection_info.key.len() >= MIN_KEY_LEN, "{label}");
2128            assert_ne!(bound.connection_info.daemon_id, [0u8; 16], "{label}");
2129            assert_owner_only_connection_file(&path);
2130
2131            drop(bound.listeners);
2132        }
2133    }
2134
2135    #[tokio::test]
2136    async fn foreign_reused_port_connection_file_is_reclaimed_after_auth_probe_fails() {
2137        let (_dir, path) = temp_connection_file_path("foreign-reclaim");
2138        let foreign = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
2139        let foreign_port = foreign.local_addr().unwrap().port();
2140        write_atomic(&path, &make_connection_info(foreign_port)).unwrap();
2141        let foreign_task = tokio::spawn(async move {
2142            if let Ok((mut stream, _)) = foreign.accept().await {
2143                let mut buf = [0u8; 64];
2144                let _ = stream.read(&mut buf).await;
2145            }
2146        });
2147
2148        let bound = expect_bound(ensure_singleton(&path, 0).await.unwrap());
2149        assert!(bound
2150            .connection_info
2151            .endpoints
2152            .iter()
2153            .all(|endpoint| endpoint.port != foreign_port));
2154
2155        drop(bound.listeners);
2156        let _ = foreign_task.await;
2157    }
2158
2159    #[tokio::test]
2160    async fn stale_start_lock_file_is_reclaimable() {
2161        let (_dir, path) = temp_connection_file_path("start-lock-stale-file");
2162        let lock_path = start_lock_path(&path);
2163        drop(open_owner_only_lock(&lock_path).unwrap());
2164        assert!(lock_path.is_file());
2165
2166        let lock = StartLock::acquire(&path).await.unwrap();
2167        assert!(lock_path.is_file());
2168
2169        drop(lock);
2170        assert!(lock_path.is_file());
2171    }
2172
2173    #[tokio::test]
2174    async fn held_start_lock_blocks_second_acquire_until_release() {
2175        let (_dir, path) = temp_connection_file_path("start-lock-held");
2176        let lock_path = start_lock_path(&path);
2177        let first = StartLock::acquire(&path).await.unwrap();
2178
2179        let err = match StartLock::acquire(&path).await {
2180            Ok(_) => panic!("second acquire while held must stay busy"),
2181            Err(err) => err,
2182        };
2183        assert!(matches!(
2184            err,
2185            BootstrapError::StartLockBusy {
2186                ref path,
2187                attempts: START_LOCK_RETRIES,
2188            } if path == &lock_path
2189        ));
2190
2191        drop(first);
2192
2193        let second = StartLock::acquire(&path)
2194            .await
2195            .expect("released advisory lock should be reclaimable");
2196        drop(second);
2197    }
2198
2199    #[tokio::test]
2200    async fn bind_conflict_on_fixed_port_fails_loud_without_reselecting() {
2201        let (_dir, path) = temp_connection_file_path("bind-conflict");
2202        let occupied = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
2203        let occupied_port = occupied.local_addr().unwrap().port();
2204
2205        let err = ensure_singleton(&path, occupied_port).await.unwrap_err();
2206        assert!(matches!(
2207            err,
2208            BootstrapError::Bind { ref source, .. } if source.kind() == io::ErrorKind::AddrInUse
2209        ));
2210        assert!(err.to_string().contains("set the port in config"));
2211
2212        drop(occupied);
2213    }
2214
2215    #[tokio::test]
2216    async fn ephemeral_ipv6_collision_still_serves_ipv4() {
2217        let (listeners, endpoints) = bind_loopback_with(0, |port| async move {
2218            let occupied = TcpListener::bind((Ipv6Addr::LOCALHOST, port)).await?;
2219            let result = TcpListener::bind((Ipv6Addr::LOCALHOST, port)).await;
2220            drop(occupied);
2221            result
2222        })
2223        .await
2224        .expect("an incidental IPv6 collision must not prevent an ephemeral daemon from starting");
2225        assert_eq!(endpoints.len(), 1);
2226        assert_eq!(endpoints[0].host, "127.0.0.1");
2227        TcpStream::connect((Ipv4Addr::LOCALHOST, endpoints[0].port))
2228            .await
2229            .unwrap();
2230        drop(listeners);
2231    }
2232
2233    #[tokio::test]
2234    async fn unpublished_boot_holds_singleton_lock_until_publication() {
2235        let dir = ScratchDir::new("unpublished-singleton-lock");
2236        let path = dir.join("connection.json");
2237        let bound = expect_bound(
2238            ensure_singleton_inner(BootstrapConfig::new(&path, 0), false)
2239                .await
2240                .unwrap(),
2241        );
2242        assert!(!path.exists());
2243        let contender = open_owner_only_lock(&start_lock_path(&path)).unwrap();
2244        assert!(
2245            matches!(FileExt::try_lock(&contender), Err(TryLockError::WouldBlock)),
2246            "an unpublished boot must keep its singleton claim"
2247        );
2248        drop(bound);
2249        FileExt::try_lock(&contender).unwrap();
2250    }
2251
2252    #[tokio::test]
2253    async fn readiness_probe_requires_an_authenticating_server() {
2254        let dir = ScratchDir::new("unserved-readiness");
2255        let bound = expect_bound(
2256            ensure_singleton_inner(BootstrapConfig::new(dir.join("connection.json"), 0), false)
2257                .await
2258                .unwrap(),
2259        );
2260        assert!(matches!(
2261            verify_serving(&bound.connection_info).await,
2262            Err(BootstrapError::StartupNotServing)
2263        ));
2264    }
2265
2266    #[cfg(unix)]
2267    #[tokio::test]
2268    async fn orphan_sweep_finishes_before_connection_file_publication() {
2269        use crate::live_children::{ExecutableIdentity, LiveChild};
2270        let dir = ScratchDir::new("publish-after-sweep");
2271        let path = dir.join("connection.json");
2272        let record = dir.join("live-children.json");
2273        let ready = dir.join("ready");
2274        let mut child = std::process::Command::new("/bin/sh")
2275            .args([
2276                "-c",
2277                "trap '' TERM; : > \"$0\"; read -r _",
2278                ready.to_str().unwrap(),
2279            ])
2280            .stdin(std::process::Stdio::piped())
2281            .env("XDG_DATA_HOME", dir.path())
2282            .env("XDG_RUNTIME_DIR", dir.path())
2283            .env("XDG_CONFIG_HOME", dir.path())
2284            .spawn()
2285            .unwrap();
2286        timeout(Duration::from_secs(5), async {
2287            while !ready.exists() {
2288                sleep(Duration::from_millis(5)).await;
2289            }
2290        })
2291        .await
2292        .unwrap();
2293        let observed = subc_os::Process::open(child.id())
2294            .unwrap()
2295            .unwrap()
2296            .observe()
2297            .unwrap();
2298        crate::live_children::write_record(
2299            &record,
2300            &[LiveChild {
2301                module_id: "old-sleep".into(),
2302                pid: child.id(),
2303                protocol: subc_control::ModuleProtocol::None,
2304                start_time: Some(observed.start_time),
2305                executable: observed.executable.map(ExecutableIdentity::from),
2306                cgroup_name: None,
2307            }],
2308        )
2309        .unwrap();
2310        let config = BootstrapConfig::new(&path, 0).with_live_children_record(&record);
2311        let daemon = tokio::spawn(run_with_config(config));
2312        timeout(Duration::from_secs(5), async {
2313            while !path.exists() {
2314                sleep(Duration::from_millis(5)).await;
2315            }
2316        })
2317        .await
2318        .unwrap();
2319        assert!(path.exists());
2320        let ended = child.try_wait().unwrap().is_some();
2321        if !ended {
2322            child.kill().unwrap();
2323        }
2324        child.wait().unwrap();
2325        if ended {
2326            assert!(matches!(probe_existing(&path).await.unwrap(), Probe::Live));
2327        }
2328        daemon.abort();
2329        let _ = daemon.await;
2330        assert!(
2331            ended,
2332            "the published daemon must not still owe its orphan sweep"
2333        );
2334    }
2335
2336    #[cfg(target_os = "macos")]
2337    #[test]
2338    fn nofile_raise_worker() {
2339        let Some(path) = std::env::var_os("SUBC_NOFILE_TEST_FILE") else {
2340            return;
2341        };
2342        let (_, hard) = rlimit::Resource::NOFILE.get().unwrap();
2343        rlimit::Resource::NOFILE.set(256, hard).unwrap();
2344        raise_nofile_limit_with_ceiling(Some(10240));
2345        std::fs::write(path, rlimit::Resource::NOFILE.get().unwrap().0.to_string()).unwrap();
2346    }
2347
2348    #[cfg(target_os = "macos")]
2349    #[test]
2350    fn macos_nofile_raise_clamps_to_kernel_ceiling() {
2351        let dir = ScratchDir::new("nofile-ceiling");
2352        let result = dir.join("soft");
2353        let status = std::process::Command::new(std::env::current_exe().unwrap())
2354            .args(["--exact", "bootstrap::tests::nofile_raise_worker"])
2355            .env("SUBC_NOFILE_TEST_FILE", &result)
2356            .env("XDG_DATA_HOME", dir.path())
2357            .env("XDG_RUNTIME_DIR", dir.path())
2358            .env("XDG_CONFIG_HOME", dir.path())
2359            .status()
2360            .unwrap();
2361        assert!(status.success());
2362        let soft: u64 = std::fs::read_to_string(result).unwrap().parse().unwrap();
2363        assert!(
2364            soft > 256 && soft <= 10240,
2365            "the inherited limit must be raised within the kernel ceiling; got {soft}"
2366        );
2367    }
2368
2369    #[tokio::test]
2370    async fn key_rotation_republishes_new_material_and_old_file_fails_auth() {
2371        let (_dir, path) = temp_connection_file_path("key-rotation");
2372        let first = expect_bound(ensure_singleton(&path, 0).await.unwrap());
2373        let old_info = first.connection_info.clone();
2374        let fixed_port = old_info.endpoints[0].port;
2375        drop(first.listeners);
2376
2377        let second = expect_bound(ensure_singleton(&path, fixed_port).await.unwrap());
2378        let new_info = second.connection_info.clone();
2379        assert_ne!(old_info.key, new_info.key);
2380        assert_ne!(old_info.daemon_id, new_info.daemon_id);
2381        let server = start_server(second);
2382
2383        let mut old_stream = connect_from_info(&old_info).await.unwrap();
2384        let old_auth = authenticate_client(&mut old_stream, &old_info, PROBE_AUTH_DEADLINE).await;
2385        assert!(
2386            old_auth.is_err(),
2387            "old key must not authenticate after restart"
2388        );
2389
2390        let reread = connection_file::read(&path).unwrap();
2391        let mut new_stream = connect_from_info(&reread).await.unwrap();
2392        authenticate_client(&mut new_stream, &reread, PROBE_AUTH_DEADLINE)
2393            .await
2394            .unwrap();
2395
2396        server.abort();
2397        let _ = server.await;
2398    }
2399
2400    #[cfg(unix)]
2401    #[tokio::test]
2402    async fn published_connection_file_permissions_are_owner_only() {
2403        let (_dir, path) = temp_connection_file_path("permissions");
2404        let bound = expect_bound(ensure_singleton(&path, 0).await.unwrap());
2405
2406        let mode = fs::metadata(&path).unwrap().permissions().mode() & 0o777;
2407        assert_eq!(mode, 0o600);
2408
2409        drop(bound.listeners);
2410    }
2411}