package activator import ( "errors" "fmt" "io/fs" "os" "path/filepath" "strings" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/instancectl" "github.com/carlosframework/platform/internal/registry" ) // ClaimLaunchRoles are the processes the real start of rt spawns: main // always, the route's first-boot seed when it has one, and the legacy // co-spawned sidecar when its env file is staged. The isolated claim launch // carries main alone. func (b *ExecBackend) ClaimLaunchRoles(rt registry.Route) ([]string, error) { return b.claimLaunchRoles(rt, os.Stat) } func (b *ExecBackend) claimLaunchRoles(rt registry.Route, stat func(string) (os.FileInfo, error)) ([]string, error) { roles := []string{"main"} if rt.Seed != "" { roles = append(roles, "seed") } _, err := stat(b.pathsFor(rt).sidecarEnv) switch { case err == nil: roles = append(roles, "sidecar") case errors.Is(err, fs.ErrNotExist): // A proven absence is the only safe negative answer. Other inspection // failures cannot admit a launch whose complete role set is unknown. default: return nil, fmt.Errorf("inspect staged sidecar for %s: %w", rt.Host, err) } return roles, nil } // claimLaunchShape refuses, read-only, a route whose real start is more than // the isolated main launch. func (b *ExecBackend) claimLaunchShape(rt registry.Route) error { roles, err := b.ClaimLaunchRoles(rt) if err != nil { return err } if len(roles) != 1 { return hostbox.ErrUnsupported } return nil } func isolatedCleanAbs(p string) bool { return filepath.IsAbs(p) && filepath.Clean(p) == p && !strings.ContainsRune(p, 0) } // IsolatedArgv validates the complete isolated launch shape and returns the // trampoline's argv: the unit's view of the host's live build, then exactly // --socket in the run directory and --db in the instance directory, each // once. args excludes argv[0]. env must be well-formed (KEY=value, no NUL, // no duplicate key) and carry the instance's admin token. It is the one // validator for startIsolated and the claim preflight. func IsolatedArgv(host, launchDir, runDir string, args, env []string) (argv []string, socket, db string, err error) { if len(args)%2 != 0 || !isolatedCleanAbs(launchDir) || !isolatedCleanAbs(runDir) { return nil, "", "", hostbox.ErrRoleLaunchShape } for i := 0; i < len(args); i += 2 { value := args[i+1] switch { case args[i] == "--socket" && socket == "" && isolatedCleanAbs(value) && filepath.Dir(value) == runDir: socket = value case args[i] == "--db" && db == "" && isolatedCleanAbs(value) && filepath.Dir(value) == launchDir: db = value default: return nil, "", "", hostbox.ErrRoleLaunchShape } } if socket == "" || db == "" { return nil, "", "", hostbox.ErrRoleLaunchShape } seen := make(map[string]bool, len(env)) for _, kv := range env { k, _, ok := strings.Cut(kv, "=") if !ok || k == "" || strings.ContainsRune(kv, 0) || seen[k] { return nil, "", "", hostbox.ErrRoleLaunchShape } seen[k] = true } if !seen["CARLOS_ADMIN_TOKEN"] || !envHas(env, "CARLOS_ADMIN_TOKEN=") { return nil, "", "", hostbox.ErrRoleLaunchShape } return append([]string{filepath.Join(instancectl.LiveDir, host)}, args...), socket, db, nil } // envHas reports a non-empty value for the given "KEY=" prefix. func envHas(env []string, prefix string) bool { for _, kv := range env { if v, ok := strings.CutPrefix(kv, prefix); ok && v != "" { return true } } return false }