xetup/internal/state/state.go
X9 Dev 5e01826a49
All checks were successful
release / build-and-release (push) Successful in 24s
feat: state machine for reboot-resume across Windows Update cycles
xetup.exe now acts as an orchestrator across system reboots:
- PS scripts exit 9 to signal "reboot needed, re-run me" (WU) or
  "done but reboot needed to finalize" (Dell BIOS)
- On exit 9: xetup saves state.json, ensures adminx9 account,
  copies itself to stable path, enables autologon, registers
  X9-Resume scheduled task (AtLogOn adminx9, RunLevel Highest)
- On resume: loads pending steps from state, continues seamlessly
  with "Pokracuji po restartu..." label in the run window
- On completion: disables autologon, removes X9-Resume task,
  deletes state file, shows summary with accumulated results
  across all reboot rounds

New packages: internal/state, internal/prereboot
Script 12: simplified to exit 0 (done) or exit 9 (reboot needed)
Script 11: exit 9 when DCU exit code 1 (BIOS staged, reboot needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 17:09:53 +02:00

74 lines
2.1 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
}
// 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, creating parent directories as needed.
func Save(s *State) error {
p := statePath()
if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
return err
}
data, err := json.MarshalIndent(s, "", " ")
if err != nil {
return err
}
return os.WriteFile(p, data, 0644)
}
// 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
}