package host import ( "bytes" "os" "os/exec" "path/filepath" "slices" "strings" ) // systemRecoverUnit is carlos-system-recover.service, spelled out once // rather than repeated as a string literal at each call site below. const systemRecoverUnit = "carlos-system-recover.service" // instanceControlSocket is the root instance-control channel's socket // (design §5.2). ensureSystemUnits arms it the same way it arms the // self-update units: this pass is the second belt for any unit a roll // script forgot, and a box with the socket unarmed has no isolated-exec // control channel at all. const instanceControlSocket = "carlos-instance-control.socket" // ensureSystemArmed is what ensureSystemUnits arms (systemctlPlan: enable, // and start for the path unit and the sockets); carlos-dbread.socket only // where host.env arms db reads (dbReadsSocketWanted). The claim census reads // the same list (claimCensusEnabledUnits), so a unit armed here always has // its install links classified there. var ensureSystemArmed = []string{systemUpdatePathUnit, systemRecoverUnit, instanceControlSocket, dbReadSocketUnit} // carlos-dbread.socket and its service template are the db-read worker's // (db region forward spec §8.3): installed and the socket armed here, the // template never started. var systemUnitFiles = []string{"carlos-system-update.path", "carlos-system-update.service", systemRecoverUnit, instanceControlSocket, "carlos-instance-control.service", dbReadSocketUnit, "carlos-dbread@.service"} // ensureSystemUnits is the second belt of spec §7: a box the roll script // forgot to arm is armed within the hour by the root adopt pass. It // installs the embedded units when missing and arms them through // systemctlPlan (never starting a service). Non-fatal: it runs before // RunAdopt's registry and pubkey gates, and logs instead of exiting. // // It writes into the same /systemd/system unit files that `carlos // system apply`'s installUnits copies, using the same durable-write // staging name — so it takes the apply's own lock, non-blocking, before // touching anything. An apply in flight wins; this pass leaves it alone // rather than risk tearing a unit file mid-copy. lockPath is the apply's // lock file (systemRootDir(release.OptDir())/lock in production; a temp // path in tests). // // Two systemctl seams, because the two uses want opposite things from an // exit status: systemctl runs the arming COMMANDS, where non-zero is a // failure to report, and probe asks the two QUESTIONS, where non-zero is // half the answer (see the block below). func ensureSystemUnits(etcDir, lockPath string, systemctl, probe func(args ...string) (string, error), logf func(string, ...any)) { if err := os.MkdirAll(filepath.Dir(lockPath), 0o755); err != nil { logf("adopt: system units: %v", err) return } lock, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0o600) if err != nil { logf("adopt: system units: %v", err) return } defer lock.Close() // Any error here — real contention, or errSystemUnsupported on the // windows build that has no flock — means don't touch the units: an // apply may be mid-copy, or this platform can't tell, and either way // leaving it alone is the safe default. The lock's cadence takes care // of trying again next tick. if err := flockExclusiveNonblock(lock); err != nil { logf("adopt: system units: an apply is running; leaving the self-update units to it") return } claimSettings, settingsErr := readClaimUnitSettings(filepath.Join(etcDir, "carlos", "host.env")) if settingsErr != nil { logf("adopt: instance-control unit settings: %v", settingsErr) } unitDir := filepath.Join(etcDir, "systemd", "system") // systemctl answers these two in stdout and spends its EXIT STATUS on // the answer's polarity: is-active exits 3 for "inactive", is-enabled // exits 1 for "disabled" and again for a unit file that is not there // yet. Reading a non-zero exit as a broken systemctl is what made every // unarmed box — the exact box this pass exists to arm — look like a box // with no systemctl at all, so the printed word is the answer and the // exit status is ignored, exactly as probes.go has always done it. active, activeErr := probe("is-active", systemUpdatePathUnit) // The control socket's own is-active check, alongside the self-update // path unit's: the same silent-systemctl-vs-unarmed-unit question, one // more unit wide (design §5.2). ctlActiveOut, ctlActiveErr := probe("is-active", instanceControlSocket) // And the db-read worker's socket (db region forward spec §8.3), which // is armed only where host.env arms db reads: elsewhere it must not // listen at all, so its enablement is asked too. dbrActiveOut, dbrActiveErr := probe("is-active", dbReadSocketUnit) dbrEnabledOut, _ := probe("is-enabled", dbReadSocketUnit) dbrWant, dbrWhy := dbReadsSocketWanted(filepath.Join(etcDir, "carlos", "host.env")) // The is-enabled error is dropped on purpose: its cases are the words // parseIsEnabled reads, and "no such unit file" is a perfectly good // "not enabled" on the box we are here to fix. enabled, _ := probe("is-enabled", systemRecoverUnit) // is-active is the probe that tells a silent systemctl from an unarmed // unit: it prints a state word even for a unit systemd has never heard // of, so no word at all means the question could not be asked — no // binary, or an exec that failed outright. isActive := parseIsActive(active) ctlActive := parseIsActive(ctlActiveOut) dbrActive := parseIsActive(dbrActiveOut) if isActive == nil || ctlActive == nil || dbrActive == nil { logf("adopt: system units: systemctl said nothing; not touching the self-update units: %v / %v / %v", activeErr, ctlActiveErr, dbrActiveErr) return } isEnabled := parseIsEnabled(enabled) missing := false for _, u := range systemUnitFiles { if _, err := os.Stat(filepath.Join(unitDir, u)); err != nil { missing = true } } control, err := unitsFS.ReadFile("units/" + claimControlUnit) if err != nil { logf("adopt: system units: %v", err) return } installed, err := os.ReadFile(filepath.Join(unitDir, claimControlUnit)) if err != nil && !os.IsNotExist(err) { logf("adopt: instance-control unit: %v", err) return } controlChanged := settingsErr == nil && (claimSettings.enabled || bytes.Contains(installed, []byte(claimControlEnvironment))) && (err != nil || !bytes.Equal(installed, claimUnitContent(claimControlUnit, control, claimSettings))) dbrEnabled := parseIsEnabled(dbrEnabledOut) // Dark: neither listening nor enabled for the next boot. dbrDark := !*dbrActive && (dbrEnabled == nil || !*dbrEnabled) dbrSettled := (dbrWant && *dbrActive) || (!dbrWant && dbrDark) if !missing && !controlChanged && *isActive && isEnabled != nil && *isEnabled && *ctlActive && dbrSettled { return } if err := os.MkdirAll(unitDir, 0o755); err != nil { logf("adopt: system units: %v", err) return } for _, u := range systemUnitFiles { // A host.env this pass cannot parse says nothing about the claim // (6b-N2): a control unit rendered for an enabled box stays as it is. if u == claimControlUnit && settingsErr != nil && bytes.Contains(installed, []byte(claimControlEnvironment)) { continue } b, err := unitsFS.ReadFile("units/" + u) if err != nil { logf("adopt: system units: %v", err) return } if err := writeFileDurable(filepath.Join(unitDir, u), claimUnitContent(u, b, claimSettings), 0o644); err != nil { logf("adopt: system units: %v", err) return } } arm := ensureSystemArmed if !dbrWant { arm = slices.DeleteFunc(slices.Clone(arm), func(u string) bool { return u == dbReadSocketUnit }) if !dbrDark { logf("adopt: system units: db reads are not armed here (%s); stopping %s", dbrWhy, dbReadSocketUnit) if _, err := systemctl("disable", "--now", dbReadSocketUnit); err != nil { logf("adopt: system units: systemctl disable --now %s: %v", dbReadSocketUnit, err) return } } } for _, args := range systemctlPlan(arm) { if _, err := systemctl(args...); err != nil { logf("adopt: system units: systemctl %s: %v", strings.Join(args, " "), err) return } } if settingsErr == nil { if err := restartClaimControl(filepath.Join(unitDir, claimControlUnit), installed, claimUnitContent(claimControlUnit, control, claimSettings), systemctl); err != nil { logf("adopt: system units: %v", err) return } } logf("adopt: self-update units installed and armed") } // parseIsEnabled maps `systemctl is-enabled`'s vocabulary onto "armed for // the next boot, or not", as probes.go's parseIsActive maps is-active's onto // "running now". enabled and enabled-runtime are the wants-symlink states; // static, indirect, alias and generated are units that cannot be enabled at // all, so an enable would be a no-op and there is nothing there to fix. // Everything else — disabled, masked, linked, the empty answer a unit file // that is not installed gets, a word a future systemd invents — is nil, and // the caller arms a nil, because the answer this must never invent is the // reassuring one. func parseIsEnabled(out string) *bool { yes, no := true, false switch strings.TrimSpace(out) { case "enabled", "enabled-runtime", "static", "indirect", "alias", "generated": return &yes case "disabled", "masked", "masked-runtime", "linked", "linked-runtime", "transient", "bad": return &no } return nil } // systemctlProbe is `systemctl ` asked as a QUESTION: stdout is the // answer and the exit status is the caller's to ignore, so it uses Output() // rather than productionSysOps().systemctl's fail-on-non-zero. The error // comes back only so a caller left with no word to read can say why. func systemctlProbe(args ...string) (string, error) { out, err := exec.Command("systemctl", args...).Output() return string(out), err }