package node import ( "context" _ "embed" "encoding/json" "io" "os" "os/exec" "time" ) //go:embed environment-profile.json var environmentProfile []byte type EnvironmentRun struct { Schema string `json:"schema"` ProfileRevision string `json:"profile_revision"` RunID string `json:"run_id"` State string `json:"state"` StartedAt float64 `json:"started_at"` UpdatedAt float64 `json:"updated_at"` Steps []EnvironmentStep `json:"steps"` } type EnvironmentStep struct { ID string `json:"id"` State string `json:"state"` Detail string `json:"detail"` } type EnvironmentStatus struct { Profile json.RawMessage `json:"profile"` Run *EnvironmentRun `json:"run"` Available bool `json:"available"` } func ReadEnvironment() EnvironmentStatus { result := readEnvironmentFile("/var/lib/mission-core-node-environment/last-run.json") if result.Run != nil && result.Run.State == "running" { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() state, err := exec.CommandContext(ctx, "/usr/bin/systemctl", "show", "--property=ActiveState", "--value", "mission-core-node-environment.service").Output() if err != nil || (string(state) != "activating\n" && string(state) != "active\n") { // A stopped job/reboot cannot leave yesterday's spinner running. result.Run.State = "interrupted" } } return result } func readEnvironmentFile(path string) EnvironmentStatus { result := EnvironmentStatus{Profile: json.RawMessage(environmentProfile), Available: true} f, err := os.Open(path) if os.IsNotExist(err) { return result } if err != nil { result.Available = false return result } defer f.Close() info, err := f.Stat() if err != nil || !info.Mode().IsRegular() || info.Size() > 32768 { result.Available = false return result } var run EnvironmentRun decoder := json.NewDecoder(io.LimitReader(f, 32769)) if decoder.Decode(&run) != nil || decoder.Decode(new(any)) != io.EOF || !validEnvironmentRun(run) { result.Available = false return result } result.Run = &run return result } // Invalid or inconsistent progress cannot turn a failed setup into green UI. func validEnvironmentRun(run EnvironmentRun) bool { if run.Schema != "missioncore.node.environment/v1" || run.RunID == "" || len(run.RunID) > 64 || len(run.Steps) == 0 || len(run.Steps) > 32 || run.ProfileRevision == "" { return false } if run.State != "running" && run.State != "complete" && run.State != "error" { return false } seen := map[string]bool{} for _, step := range run.Steps { if step.ID == "" || len(step.ID) > 64 || seen[step.ID] || len(step.Detail) > 4096 { return false } seen[step.ID] = true switch step.State { case "pending", "running", "complete", "error", "blocked": default: return false } if run.State == "complete" && step.State != "complete" { return false } } return true }