package activator import ( "context" "errors" "os" "os/exec" "path/filepath" "strings" "github.com/carlosframework/platform/internal/instancectl" ) // Control is what the exec backend needs from the root instance-control // channel (design §5.3). instancectl.Client is the production // implementation; tests use a fake. type Control interface { Start(ctx context.Context, host string, role instancectl.Role, memoryMax int64) (string, error) Seed(ctx context.Context, host string, memoryMax int64) (instancectl.SeedResponse, error) // Cmd starts host's cmd unit and returns its invocation; CmdStatus // long-polls it (run spec §5.4). Cmd(ctx context.Context, host string, memoryMax int64) (string, error) CmdStatus(ctx context.Context, host, invocation string) (instancectl.CmdStatus, error) Stop(ctx context.Context, host string, role instancectl.Role) error Reset(ctx context.Context, host string, role instancectl.Role) error Sweep(ctx context.Context) (instancectl.SweepResponse, error) Ready(ctx context.Context) error } var _ Control = (*instancectl.Client)(nil) // GenControl is the generation half of the instance-control channel // (zero-downtime spec §7.6): the carlos-instance-gen@: template's // start, stop and reset, and the host slice's parent limit (§7.3). It is a // separate interface so a Control without it (a fake, an older client) // still satisfies Control. The backend asks for it only while // GenContainment is on. type GenControl interface { StartGen(ctx context.Context, host string, n uint64, memoryMax int64) (string, error) StartGenExe(ctx context.Context, host string, n uint64, memoryMax int64, x instancectl.StagedExe) (string, error) StartSidecarExe(ctx context.Context, host string, memoryMax int64, x instancectl.StagedExe) (string, error) StopGen(ctx context.Context, host string, n uint64, kill bool) error ResetGen(ctx context.Context, host string, n uint64) error Parent(ctx context.Context, host string, memoryMax int64) error } var _ GenControl = (*instancectl.Client)(nil) // StagedExeControl is the original templates' half of naming an // executable (settings spec §4.2): the run unit at generation 0 and the // seed unit, started on a staged build root validates and binds in place // of the live link. A cold wake runs the build it recorded through it, // with generations on or off. It is separate from Control and GenControl // so a Control without it (a fake, an older client) still satisfies both; // the backend asks for it only when it has a file to name, and refuses // the start without it (stagedExeControl), never binding the link instead. type StagedExeControl interface { StartExe(ctx context.Context, host string, role instancectl.Role, memoryMax int64, x instancectl.StagedExe) (string, error) SeedExe(ctx context.Context, host string, memoryMax int64, x instancectl.StagedExe) (instancectl.SeedResponse, error) } var _ StagedExeControl = (*instancectl.Client)(nil) // UnitState answers `systemctl show -p ActiveState,InvocationID `, // which needs no privilege. An unloaded unit answers inactive with an // empty invocation. type UnitState func(ctx context.Context, unit string) (active, invocation string, err error) func systemctlUnitState(ctx context.Context, unit string) (string, string, error) { out, err := exec.CommandContext(ctx, "systemctl", "show", "-p", "ActiveState,InvocationID", unit).Output() if err != nil { return "", "", err } var active, inv string for _, line := range strings.Split(string(out), "\n") { if k, v, ok := strings.Cut(line, "="); ok { switch k { case "ActiveState": active = strings.TrimSpace(v) case "InvocationID": inv = strings.TrimSpace(v) } } } return active, inv, nil } // CgroupEvents reads the unit's cgroup.events under the host slice. cgroup // v2 files are world-readable, so the agent reads them as carlos, as the // sampler does. An absent directory is the unit's cgroup already removed. type CgroupEvents func(slice, unit string) (exists, populated bool, err error) func cgroupEventsUnder(root string) CgroupEvents { return func(slice, unit string) (bool, bool, error) { b, err := os.ReadFile(filepath.Join(root, "carlos.slice", "carlos-instances.slice", slice, unit, "cgroup.events")) if errors.Is(err, os.ErrNotExist) { return false, false, nil } if err != nil { return true, true, err } return true, !strings.Contains(string(b), "populated 0"), nil } }