package host import ( "context" "errors" "github.com/carlosframework/platform/internal/hostfloor" "github.com/carlosframework/platform/internal/release" "log" "os" "path/filepath" "runtime" "syscall" "time" ) func floorDisk(optDir string) (hostfloor.Disk, error) { pub, err := release.ParsePublicKey(os.Getenv("CARLOS_RELEASE_PUBKEY"), "CARLOS_RELEASE_PUBKEY") if err != nil { return hostfloor.Disk{}, err } bucket := os.Getenv("CARLOS_DEPLOYMENT_BUCKET") if bucket == "" { return hostfloor.Disk{}, hostfloor.ErrFloor } return hostfloor.Disk{Dir: filepath.Join(optDir, "system", "guard"), Bucket: bucket, PublicKey: pub}, nil } func RunSystemGuardExec(args []string) { if len(args) < 1 { log.Fatal(hostfloor.ErrFloor) } d, err := floorDisk(release.OptDir()) if err != nil { log.Fatal(err) } policy, local, err := d.Load() if err != nil { log.Fatal(err) } if args[0] == "recover" { held, err := recoverLockHeld(release.OptDir()) if err != nil { log.Fatal(err) } if held { log.Print("system guard-exec recover: an apply holds the lock; it recovers at its own start") return } } networkFree := false if args[0] == "recover" { _, networkFree, err = rootMovesFloorIdentity() if err != nil { log.Fatal(err) } } if policy.Mode != "unpublished" && !networkFree { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) store, pub, e := openSystemStore(ctx) if e == nil { e = hostfloor.CheckRemotePolicy(ctx, store, pub, d.Bucket, policy, local) } cancel() if e != nil && !hostfloor.Offline(e) { log.Fatal(e) } } current := systemBinaryPath(release.OptDir()) candidate := current component := "router" switch args[0] { case "apply", "recover": _, err := os.Stat(filepath.Join(systemRootDir(release.OptDir()), "journal.json")) if err == nil { candidate = systemPrevPath(release.OptDir()) } else if !errors.Is(err, os.ErrNotExist) { log.Fatal(err) } mode := args[0] args = []string{"system", "apply"} if mode == "recover" { args = append(args, "--recover") } case "console": if len(args) < 2 { log.Fatal(hostfloor.ErrFloor) } component = "console" candidate = args[1] args = args[2:] case "edge": args = []string{"agent"} default: log.Fatal(hostfloor.ErrFloor) } if component == "router" && len(args) > 1 && args[0] == "system" { id, required, e := rootMovesFloorIdentity() if e != nil { log.Fatal(e) } if required { journal, e := rootFloorJournalPresent(release.OptDir()) if e != nil { log.Fatal(e) } e = withRootFloorGuardCandidate(context.Background(), release.OptDir(), id, journal && len(args) > 1 && args[0] == "system", func(selected string) error { return d.Exec(selected, runtime.GOOS+"-"+runtime.GOARCH, args, os.Environ(), component) }) if e != nil { log.Fatal(e) } return } } if err = d.Exec(candidate, runtime.GOOS+"-"+runtime.GOARCH, args, os.Environ(), component); err != nil { log.Fatal(err) } } // recoverLockHeld reports whether another process holds the apply lock. // Only root's apply, recover and guard-arm take it, and an apply recovers // any journal itself at its start, so guard-exec recover leaves the box to // the holder at once. The edge drop-in Requires= the recover unit, so every // edge restart inside an apply starts a recover; waiting out // systemRecoverLockWait there kept the restarted edge down for 30 s (Opus 8b // review I-2). A missing lock file is a free lock and is not created. func recoverLockHeld(optDir string) (bool, error) { lock, err := os.OpenFile(filepath.Join(systemRootDir(optDir), systemLockFile), os.O_RDWR, 0) if errors.Is(err, os.ErrNotExist) { return false, nil } if err != nil { return false, err } defer lock.Close() err = flockExclusiveNonblock(lock) if errors.Is(err, syscall.EWOULDBLOCK) { return true, nil } return false, err }