Some checks failed
release / build-and-release (push) Failing after 32s
Critical fixes: - Fix resume mode: StepsByIDs returned Enabled=false, all resume steps would be SKIPPED (deployment could never resume after reboot) - Add reboot loop protection: per-step retry counter (max 5) prevents infinite reboot cycles when a step always exits with code 9 - Block reboot when state.Save() fails in resumePhase (prevents state loss leading to full restart from scratch) - Atomic state file write (write-to-tmp + rename) prevents JSON corruption on BSOD/power loss mid-write - Script watchdog: kills scripts after 30 min of no output (resets on each line, so active long-running scripts are never killed) - Fix copyFile: check Close() error explicitly instead of deferred close that silently drops flush errors (e.g. disk full) High severity: - Cleanup() now logs errors instead of silently ignoring them - Email report: 3 retries with backoff + always saves C:\X9\report.html - Winget parallel jobs: 10 min timeout, kill hung jobs - UCPD stop verification: 2s wait + state check before PDF association - Atera installer: /qn -> /qb so MFA window can appear - GVLK activation: match by EditionID (registry, not localized) instead of fragile OS caption string matching Medium severity: - Default profile hive unload: retry loop (5 attempts, increasing delay) - LayoutModification.xml: UTF-8 without BOM (PS 5.1 Set-Content adds BOM) - Set-Reg SYSTEM task: try/finally ensures temp file + task cleanup - Windows Update: @($available).Count for PS 5.1 single-result edge case - config.json: add missing kmsServer field in activation section Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
82 lines
2.4 KiB
Go
82 lines
2.4 KiB
Go
// Package state manages the persistent xetup deployment state used to resume
|
|
// after a system reboot. The state file is written before each reboot and
|
|
// deleted once all deployment steps have completed.
|
|
package state
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"git.xetup.x9.cz/x9/xetup/internal/config"
|
|
)
|
|
|
|
// StepResult records the outcome of a single deployment step.
|
|
// Results are accumulated across reboot rounds so the final summary
|
|
// can show every step regardless of which round it ran in.
|
|
type StepResult struct {
|
|
StepID string `json:"stepID"`
|
|
Num string `json:"num"`
|
|
Name string `json:"name"`
|
|
Status string `json:"status"` // "OK", "ERROR", "SKIPPED"
|
|
Elapsed time.Duration `json:"elapsed"`
|
|
}
|
|
|
|
// State holds everything xetup needs to resume after a reboot.
|
|
type State struct {
|
|
Config config.Config `json:"config"`
|
|
ConfigPath string `json:"configPath"` // path to original config.json
|
|
LogFile string `json:"logFile"`
|
|
PendingSteps []string `json:"pendingSteps"` // step IDs to run next, in canonical order
|
|
Results []StepResult `json:"results"` // accumulated across all rounds
|
|
RetryCounts map[string]int `json:"retryCounts,omitempty"` // per-step reboot retry counter
|
|
}
|
|
|
|
// Load reads the state file. Returns a non-nil error when the file is absent.
|
|
func Load() (*State, error) {
|
|
data, err := os.ReadFile(statePath())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var s State
|
|
if err := json.Unmarshal(data, &s); err != nil {
|
|
return nil, err
|
|
}
|
|
return &s, nil
|
|
}
|
|
|
|
// Save writes the state file atomically (write-to-temp + rename), creating
|
|
// parent directories as needed. This prevents corruption if the system
|
|
// crashes mid-write (e.g. BSOD, power loss).
|
|
func Save(s *State) error {
|
|
p := statePath()
|
|
dir := filepath.Dir(p)
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return err
|
|
}
|
|
data, err := json.MarshalIndent(s, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tmp := p + ".tmp"
|
|
if err := os.WriteFile(tmp, data, 0644); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(tmp, p)
|
|
}
|
|
|
|
// Delete removes the state file. Silently ignores not-found.
|
|
func Delete() error {
|
|
err := os.Remove(statePath())
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Exists reports whether a state file is present on disk.
|
|
func Exists() bool {
|
|
_, err := os.Stat(statePath())
|
|
return err == nil
|
|
}
|