package host import ( "os" "path/filepath" "github.com/carlosframework/platform/internal/safefile" ) // adoptPassLockFile is the lock every root adoption pass holds for its whole // run. It sits in the data dir beside the restart marks, so the single // filesystem the sandboxed agent and the root passes both write is the single // filesystem their coordination lives on. const adoptPassLockFile = "adopt-pass.lock" func adoptPassLockPath(dd string) string { return filepath.Join(dd, adoptPassLockFile) } // lockAdoptPass takes an exclusive flock for the whole of one RunAdopt, in // EVERY mode. // // Four units exec `carlos adopt`: carlos-adopt.service (hourly), // carlos-stage.service (the agent relaying a stage it is sandboxed out of), // carlos-restart.service and carlos-config-sync.service — and systemd will // start them in the same second without a thought. Until the staged-version // sweep existed that was tolerable: every write a pass performs is a // temp-file-then-rename or a symlink swap, so two passes doing the same work // converge on the same answer. // // os.RemoveAll is the first operation in a pass that is not idempotent // against a concurrent one. The window is small and real: the sweep reads the // live links, a stage pass repoints one at a version the sweep has just // decided is unpinned, and the sweep deletes the tree that link now names. // Rename-to-a-trash-dir does not close it either, because PointLive stats the // target and THEN symlinks — a rename between those two leaves a dangling // link and a unit that will not start. Serialising the passes does close it, // so that is what this is. // // The lock is a file of its own rather than a flock on something the pass // also writes: a flock lives on an inode, and every state file a pass owns is // replaced by rename, which would leave the winner holding a lock on a file // that is no longer the state (lockRestartMarks makes the same choice for the // same reason). // // Blocking, like lockRestartMarks: a relayed stage that waits out an hourly // pass is doing the right thing, and a stage is already a request the agent // re-raises on its next loop. func lockAdoptPass(dd string) (unlock func(), err error) { return lockExclusive(adoptPassLockPath(dd)) } // lockExclusive opens (creating) path and takes an exclusive advisory lock on // it, returning the release. Shared by lockAdoptPass and lockRestartMarks so // the two locks cannot drift in how they are taken; the ORDER between them is // fixed by their callers, adopt-pass first, and there is no third holder. func lockExclusive(path string) (unlock func(), err error) { f, err := safefile.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o644) if err != nil { return nil, err } if err := lockFileEx(f); err != nil { f.Close() return nil, err } return func() { unlockFile(f) f.Close() }, nil }