package host import ( "context" "log" "net/http" "os" "path/filepath" "time" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/instancectl" "github.com/carlosframework/platform/internal/moves" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootparticipation" ) func RunInstanceControl(args []string) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() if err := registerProductionLaunchVerifier(); err != nil { log.Fatal(err) } if hostwrite.Enabled(os.Getenv) { if err := claimRecoveryReady(rootparticipation.OpenForUpdate); err != nil { log.Print(&ClaimDependencyError{Contracts: []string{"census", "registered verifier", "flag must stay off until Part 3 (G)"}}) return } } ctx, err := claimEntryContext(ctx, os.Getenv, openDeployStore) if err != nil { log.Fatal(err) } if !hostwrite.Enabled(claimEntryEnv(ctx)) { instancectl.RunWithRegistry(args, instanceRegistryHostApp(nil)) return } reg, err := openServiceRegistry(filepath.Join(dataDir(), "registry.db")) if err != nil { log.Fatal(err) } defer reg.Close() ctx = withClaimRuntimeResources(ctx, ClaimRuntimeResources{Mode: "instance-control", Registry: reg, DataDir: dataDir(), OptDir: release.OptDir(), UnitsDir: "/etc/systemd/system"}) ctx, err = ensureProductionClaimAssembly(ctx) if err != nil { log.Fatal(err) } entry, _ := ctx.Value(claimEntryKey{}).(claimEntry) defer func() { cleanup, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := entry.Assembly.Close(cleanup); err != nil { log.Printf("instance-control: host claims shutdown: %v", err) } }() startup, finishStartup := context.WithTimeout(ctx, 2*time.Minute) defer finishStartup() handler, err := NewProductionRootClaimStop(startup) if err != nil { log.Fatal(err) } execution := handler.(ClaimRootHTTP).executionRuntime defer func() { cleanup, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := execution.Close(cleanup); err != nil { log.Printf("instance-control: host claims shutdown: %v", err) } }() handler, err = WithProductionClaimLaunch(startup, handler) if err != nil { log.Fatal(err) } // moves Task 7b: root's held release, only with CARLOS_MOVES_PHASE1 on. handler, err = WithProductionMovesHeldRelease(startup, handler, reg, claimEntryEnv(ctx)) if err != nil { log.Fatal(err) } if err := activateClaimRoot(ctx, startup, handler); err != nil { log.Fatal(err) } finishStartup() fenceDone := make(chan struct{}) go func() { defer close(fenceDone); runHostFencePass(ctx) }() defer func() { cancel(); <-fenceDone }() instancectl.RunWithClaim(args, handler, heldReleaseRouteHandler(handler, claimEntryEnv(ctx)), rootInstallationHostApp(entry.Assembly.Guard, entry.Assembly.Claims)) } func activateClaimRoot(ctx, startup context.Context, handler http.Handler) error { recovery, ok := handler.(interface{ RecoverPending(context.Context) error }) if !ok { return ErrClaimStopUnavailable } if err := recovery.RecoverPending(startup); err != nil { log.Print(err) } if err := claimActivationReady(startup, rootparticipation.OpenForUpdate); err != nil { return err } lifecycle, ok := handler.(interface{ Start(context.Context) }) if !ok { return ErrClaimStopUnavailable } lifecycle.Start(ctx) return nil } // heldReleaseRouteHandler is the handler instancectl registers for the moves // held-release route: the claim root (which answers the path itself, // refusing while no held release is wired) only with both CARLOS_HOST_CLAIM // and CARLOS_MOVES_PHASE1 on; otherwise nil, so the route is not registered // and main's 404 stands (whole-branch fixes, item 6). func heldReleaseRouteHandler(handler http.Handler, getenv func(string) string) http.Handler { if handler == nil || getenv == nil || !hostwrite.Enabled(getenv) || !moves.Enabled(getenv) { return nil } return handler } func instanceRegistryHostApp(shared *registry.Registry) func(string) (string, string, error) { return func(host string) (string, string, error) { reg := shared if reg == nil { var err error reg, err = openServiceRegistryReadOnly(filepath.Join(dataDir(), "registry.db")) if err != nil { return "", "", err } defer reg.Close() } row, err := reg.Get(host) if err != nil { return "", "", err } if row == nil { return "", "", os.ErrNotExist } return row.Account, row.App, nil } } func claimActivationReady(ctx context.Context, open func() (rootparticipation.Ledger, bool, error)) error { l, required, err := open() if l.State != nil { defer l.State.Close() } if err != nil { return err } if !required { return rootparticipation.ErrUnavailable } ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() return l.WithLaunchExclusion(ctx, l.ActivationReady) } func claimRecoveryReady(open func() (rootparticipation.Ledger, bool, error)) error { l, required, err := open() if l.State != nil { defer l.State.Close() } if err != nil { return err } if !required { return rootparticipation.ErrUnavailable } _, err = l.RecoveryCensus() return err }