All checks were successful
release / build-and-release (push) Successful in 24s
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>
739 lines
19 KiB
Go
739 lines
19 KiB
Go
//go:build windows
|
||
|
||
// Package gui implements the Walk-based graphical interface for xetup.
|
||
//
|
||
// Three sequential phases, each with its own window:
|
||
// 1. Config form – PC name, product key, profile, step checkboxes,
|
||
// load/save config buttons for per-client presets
|
||
// 2. Live run – real-time log streamed from PowerShell scripts
|
||
// 3. Summary – per-step OK / ERROR / SKIPPED with elapsed time
|
||
//
|
||
// Resume mode (state file present): skips the config form and runs only
|
||
// the pending steps recorded in the state file. On completion, disables
|
||
// autologon and removes the X9-Resume scheduled task.
|
||
package gui
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
"os/exec"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/lxn/walk"
|
||
. "github.com/lxn/walk/declarative"
|
||
|
||
"git.xetup.x9.cz/x9/xetup/internal/config"
|
||
"git.xetup.x9.cz/x9/xetup/internal/prereboot"
|
||
"git.xetup.x9.cz/x9/xetup/internal/runner"
|
||
"git.xetup.x9.cz/x9/xetup/internal/state"
|
||
)
|
||
|
||
// Run opens the xetup window and blocks until the user closes it.
|
||
func Run(cfg config.Config, runCfg runner.RunConfig, cfgPath string) {
|
||
// Resume mode: state file present from a previous interrupted run
|
||
if st, err := state.Load(); err == nil {
|
||
resumePhase(st, runCfg)
|
||
return
|
||
}
|
||
|
||
// Normal flow: config form → run → summary
|
||
for {
|
||
res := formPhase(cfg, runCfg, cfgPath)
|
||
switch res.action {
|
||
case "quit":
|
||
return
|
||
case "reload":
|
||
cfg = res.cfg
|
||
cfgPath = res.cfgPath
|
||
case "run":
|
||
runCfg.ProfileType = res.cfg.Deployment.ProfileType
|
||
results, needsReboot := runPhase(runCfg, res.steps, false)
|
||
if needsReboot {
|
||
prepareRebootAndRestart(res.cfg, res.steps, results, cfgPath, runCfg)
|
||
return
|
||
}
|
||
donePhase(results, nil)
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
// --------------------------------------------------------------------------
|
||
// Resume mode
|
||
// --------------------------------------------------------------------------
|
||
|
||
func resumePhase(st *state.State, runCfg runner.RunConfig) {
|
||
runCfg.LogFile = st.LogFile
|
||
|
||
steps := runner.StepsByIDs(st.PendingSteps)
|
||
results, needsReboot := runPhase(runCfg, steps, true)
|
||
|
||
// Accumulate completed results (NeedsReboot step excluded – runs again)
|
||
newResults := append(st.Results, toStateResults(results)...) //nolint:gocritic
|
||
|
||
if needsReboot {
|
||
// Update state and reboot – infrastructure already in place (running as adminx9)
|
||
pending := pendingStepIDs(steps, results)
|
||
newSt := &state.State{
|
||
Config: st.Config,
|
||
ConfigPath: st.ConfigPath,
|
||
LogFile: st.LogFile,
|
||
PendingSteps: pending,
|
||
Results: newResults,
|
||
}
|
||
if err := state.Save(newSt); err != nil {
|
||
walk.MsgBox(nil, "Chyba", "Nelze ulozit stav: "+err.Error(), walk.MsgBoxIconError)
|
||
}
|
||
reboot()
|
||
return
|
||
}
|
||
|
||
// All steps complete
|
||
prereboot.Cleanup()
|
||
_ = state.Delete()
|
||
donePhase(results, st.Results)
|
||
}
|
||
|
||
// --------------------------------------------------------------------------
|
||
// Phase 1 – Config form
|
||
// --------------------------------------------------------------------------
|
||
|
||
type formResult struct {
|
||
action string // "run", "reload", "quit"
|
||
cfg config.Config
|
||
cfgPath string
|
||
steps []runner.Step
|
||
}
|
||
|
||
func formPhase(cfg config.Config, runCfg runner.RunConfig, cfgPath string) formResult {
|
||
var mw *walk.MainWindow
|
||
result := formResult{action: "quit", cfgPath: cfgPath}
|
||
|
||
var (
|
||
pcNameLE *walk.LineEdit
|
||
pcDescLE *walk.LineEdit
|
||
productKeyLE *walk.LineEdit
|
||
profileCB *walk.ComboBox
|
||
)
|
||
|
||
items := runner.AllSelectableItems()
|
||
checkPtrs := make([]*walk.CheckBox, len(items))
|
||
checkWidgets := make([]Widget, len(items))
|
||
for i, item := range items {
|
||
i := i // capture for closure
|
||
checkWidgets[i] = CheckBox{
|
||
AssignTo: &checkPtrs[i],
|
||
Text: item.Label,
|
||
Checked: itemEnabled(cfg, item),
|
||
}
|
||
}
|
||
|
||
collectCfg := func() config.Config {
|
||
out := cfg
|
||
out.Deployment.PCName = pcNameLE.Text()
|
||
out.Deployment.PCDescription = pcDescLE.Text()
|
||
out.Activation.ProductKey = productKeyLE.Text()
|
||
profiles := []string{"default", "admin", "user"}
|
||
if idx := profileCB.CurrentIndex(); idx >= 0 && idx < len(profiles) {
|
||
out.Deployment.ProfileType = profiles[idx]
|
||
}
|
||
selected := make(map[string]bool, len(items))
|
||
for i, item := range items {
|
||
selected[item.Key] = checkPtrs[i].Checked()
|
||
}
|
||
_, features := buildStepsAndFeatures(selected)
|
||
out.Features = features
|
||
return out
|
||
}
|
||
|
||
if err := (MainWindow{
|
||
AssignTo: &mw,
|
||
Title: "xetup \u2014 Windows deployment",
|
||
Size: Size{Width: 760, Height: 740},
|
||
Layout: VBox{},
|
||
Children: []Widget{
|
||
// ── Form fields ──────────────────────────────────────────────────
|
||
Composite{
|
||
Layout: Grid{Columns: 2},
|
||
Children: []Widget{
|
||
Label{Text: "PC jmeno:"},
|
||
LineEdit{
|
||
AssignTo: &pcNameLE,
|
||
Text: cfg.Deployment.PCName,
|
||
CueBanner: "napr. NB-KLIENT-01 (prazdne = neprejmenovat)",
|
||
},
|
||
Label{Text: "Popis PC:"},
|
||
LineEdit{
|
||
AssignTo: &pcDescLE,
|
||
Text: cfg.Deployment.PCDescription,
|
||
CueBanner: "napr. PC recepce",
|
||
},
|
||
Label{Text: "Product Key:"},
|
||
LineEdit{
|
||
AssignTo: &productKeyLE,
|
||
Text: cfg.Activation.ProductKey,
|
||
CueBanner: "prazdne = OA3 / GVLK fallback",
|
||
},
|
||
Label{Text: "Profil:"},
|
||
ComboBox{
|
||
AssignTo: &profileCB,
|
||
Model: []string{"default", "admin", "user"},
|
||
CurrentIndex: profileIndex(cfg.Deployment.ProfileType),
|
||
},
|
||
},
|
||
},
|
||
HSeparator{},
|
||
// ── Step checkboxes ──────────────────────────────────────────────
|
||
Label{Text: "Kroky a nastaveni (odskrtnete co nechcete spustit):"},
|
||
ScrollView{
|
||
MinSize: Size{Height: 400},
|
||
Layout: VBox{MarginsZero: true},
|
||
Children: checkWidgets,
|
||
},
|
||
HSeparator{},
|
||
// ── Buttons ──────────────────────────────────────────────────────
|
||
Composite{
|
||
Layout: HBox{MarginsZero: true},
|
||
Children: []Widget{
|
||
PushButton{
|
||
Text: "Nacist config...",
|
||
OnClicked: func() {
|
||
dlg := new(walk.FileDialog)
|
||
dlg.Filter = "JSON soubory (*.json)|*.json|Vsechny soubory (*.*)|*.*"
|
||
dlg.Title = "Nacist konfiguraci"
|
||
ok, err := dlg.ShowOpen(mw)
|
||
if err != nil || !ok {
|
||
return
|
||
}
|
||
data, err := os.ReadFile(dlg.FilePath)
|
||
if err != nil {
|
||
walk.MsgBox(mw, "Chyba", err.Error(), walk.MsgBoxIconError)
|
||
return
|
||
}
|
||
newCfg := config.DefaultConfig()
|
||
if err := json.Unmarshal(data, &newCfg); err != nil {
|
||
walk.MsgBox(mw, "Chyba", err.Error(), walk.MsgBoxIconError)
|
||
return
|
||
}
|
||
result = formResult{
|
||
action: "reload",
|
||
cfg: newCfg,
|
||
cfgPath: dlg.FilePath,
|
||
}
|
||
mw.Close()
|
||
},
|
||
},
|
||
PushButton{
|
||
Text: "Ulozit config...",
|
||
OnClicked: func() {
|
||
dlg := new(walk.FileDialog)
|
||
dlg.Filter = "JSON soubory (*.json)|*.json|Vsechny soubory (*.*)|*.*"
|
||
dlg.Title = "Ulozit konfiguraci"
|
||
dlg.FilePath = "config.json"
|
||
ok, err := dlg.ShowSave(mw)
|
||
if err != nil || !ok {
|
||
return
|
||
}
|
||
data, err := json.MarshalIndent(collectCfg(), "", " ")
|
||
if err != nil {
|
||
walk.MsgBox(mw, "Chyba", err.Error(), walk.MsgBoxIconError)
|
||
return
|
||
}
|
||
if err := os.WriteFile(dlg.FilePath, data, 0644); err != nil {
|
||
walk.MsgBox(mw, "Chyba", err.Error(), walk.MsgBoxIconError)
|
||
}
|
||
},
|
||
},
|
||
HSpacer{},
|
||
PushButton{
|
||
Text: " SPUSTIT ",
|
||
OnClicked: func() {
|
||
finalCfg := collectCfg()
|
||
selected := make(map[string]bool, len(items))
|
||
for i, item := range items {
|
||
selected[item.Key] = checkPtrs[i].Checked()
|
||
}
|
||
steps, features := buildStepsAndFeatures(selected)
|
||
finalCfg.Features = features
|
||
_ = config.Save(finalCfg, cfgPath)
|
||
result = formResult{
|
||
action: "run",
|
||
cfg: finalCfg,
|
||
cfgPath: cfgPath,
|
||
steps: steps,
|
||
}
|
||
mw.Close()
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}.Create()); err != nil {
|
||
walk.MsgBox(nil, "Chyba", fmt.Sprintf("Nelze vytvorit okno: %v", err), walk.MsgBoxIconError)
|
||
return result
|
||
}
|
||
|
||
mw.Run()
|
||
return result
|
||
}
|
||
|
||
// --------------------------------------------------------------------------
|
||
// Phase 2 – Live run view
|
||
// --------------------------------------------------------------------------
|
||
|
||
// stepIndicator tracks a step's label and current status.
|
||
type stepIndicator struct {
|
||
label *walk.Label
|
||
stepID string
|
||
status string // "pending", "running", "ok", "error", "skipped", "cancelled"
|
||
}
|
||
|
||
// runPhase runs steps and streams live log output.
|
||
// resuming=true changes the title and initial status message.
|
||
// Returns (results, needsReboot).
|
||
func runPhase(runCfg runner.RunConfig, steps []runner.Step, resuming bool) ([]runner.Result, bool) {
|
||
var (
|
||
mw *walk.MainWindow
|
||
statusLbl *walk.Label
|
||
logTE *walk.TextEdit
|
||
cancelFn context.CancelFunc
|
||
)
|
||
|
||
// Build step indicator labels (one per step, in order)
|
||
indicators := make([]stepIndicator, len(steps))
|
||
stepIndex := make(map[string]int, len(steps))
|
||
indWidgets := make([]Widget, len(steps))
|
||
for i, s := range steps {
|
||
indicators[i] = stepIndicator{stepID: s.ID, status: "pending"}
|
||
stepIndex[s.ID] = i
|
||
indWidgets[i] = Label{
|
||
AssignTo: &indicators[i].label,
|
||
Text: s.Num + " \u00b7",
|
||
Font: Font{Family: "Consolas", PointSize: 9},
|
||
MinSize: Size{Width: 38},
|
||
}
|
||
}
|
||
|
||
title := "xetup \u2014 probiha instalace"
|
||
initialStatus := "Spoustim..."
|
||
if resuming {
|
||
title = "xetup \u2014 pokracuji po restartu"
|
||
initialStatus = "Pokracuji po restartu..."
|
||
}
|
||
|
||
if err := (MainWindow{
|
||
AssignTo: &mw,
|
||
Title: title,
|
||
Size: Size{Width: 760, Height: 740},
|
||
Layout: VBox{},
|
||
Children: []Widget{
|
||
Label{AssignTo: &statusLbl, Text: initialStatus},
|
||
// Step progress strip
|
||
Composite{
|
||
Layout: HBox{MarginsZero: true},
|
||
Children: indWidgets,
|
||
},
|
||
HSeparator{},
|
||
TextEdit{
|
||
AssignTo: &logTE,
|
||
ReadOnly: true,
|
||
VScroll: true,
|
||
Font: Font{Family: "Consolas", PointSize: 9},
|
||
MinSize: Size{Height: 530},
|
||
},
|
||
HSeparator{},
|
||
Composite{
|
||
Layout: HBox{MarginsZero: true},
|
||
Children: []Widget{
|
||
HSpacer{},
|
||
PushButton{
|
||
Text: " ZASTAVIT ",
|
||
OnClicked: func() {
|
||
if cancelFn != nil {
|
||
cancelFn()
|
||
}
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}.Create()); err != nil {
|
||
return nil, false
|
||
}
|
||
|
||
// updateIndicator refreshes a step label's text and color — call inside Synchronize.
|
||
updateIndicator := func(idx int) {
|
||
ind := &indicators[idx]
|
||
s := steps[idx]
|
||
switch ind.status {
|
||
case "running":
|
||
ind.label.SetText(s.Num + " \u25ba") // ►
|
||
ind.label.SetTextColor(walk.RGB(30, 144, 255))
|
||
case "ok":
|
||
ind.label.SetText(s.Num + " \u2713") // ✓
|
||
ind.label.SetTextColor(walk.RGB(0, 180, 0))
|
||
case "error":
|
||
ind.label.SetText(s.Num + " \u2717") // ✗
|
||
ind.label.SetTextColor(walk.RGB(220, 50, 50))
|
||
case "skipped", "cancelled":
|
||
ind.label.SetText(s.Num + " \u2013") // –
|
||
ind.label.SetTextColor(walk.RGB(140, 140, 140))
|
||
default:
|
||
ind.label.SetText(s.Num + " \u00b7") // ·
|
||
ind.label.SetTextColor(walk.RGB(180, 180, 180))
|
||
}
|
||
}
|
||
|
||
var (
|
||
mu sync.Mutex
|
||
results []runner.Result
|
||
done = make(chan struct{})
|
||
)
|
||
|
||
ctx, cancel := context.WithCancel(context.Background())
|
||
cancelFn = cancel
|
||
|
||
mw.Closing().Attach(func(_ *bool, _ walk.CloseReason) {
|
||
cancelFn()
|
||
})
|
||
|
||
r := runner.New(
|
||
runCfg,
|
||
func(l runner.LogLine) {
|
||
mw.Synchronize(func() {
|
||
logTE.AppendText(l.Text + "\r\n")
|
||
})
|
||
},
|
||
func(step runner.Step) {
|
||
mw.Synchronize(func() {
|
||
statusLbl.SetText(fmt.Sprintf(
|
||
"Krok %s \u2013 %s...", step.Num, step.Name,
|
||
))
|
||
if idx, ok := stepIndex[step.ID]; ok {
|
||
indicators[idx].status = "running"
|
||
updateIndicator(idx)
|
||
}
|
||
})
|
||
},
|
||
func(res runner.Result) {
|
||
mw.Synchronize(func() {
|
||
statusLbl.SetText(fmt.Sprintf(
|
||
"Krok %s \u2013 %s: %s", res.Step.Num, res.Step.Name, res.Status,
|
||
))
|
||
if idx, ok := stepIndex[res.Step.ID]; ok {
|
||
indicators[idx].status = strings.ToLower(res.Status)
|
||
updateIndicator(idx)
|
||
}
|
||
})
|
||
},
|
||
)
|
||
|
||
go func() {
|
||
defer close(done)
|
||
res := r.Run(ctx, steps)
|
||
mu.Lock()
|
||
results = res
|
||
mu.Unlock()
|
||
mw.Synchronize(func() {
|
||
mw.Close()
|
||
})
|
||
}()
|
||
|
||
mw.Run()
|
||
cancel()
|
||
<-done
|
||
|
||
mu.Lock()
|
||
defer mu.Unlock()
|
||
|
||
needsReboot := len(results) > 0 && results[len(results)-1].NeedsReboot
|
||
return results, needsReboot
|
||
}
|
||
|
||
// --------------------------------------------------------------------------
|
||
// Pre-reboot helper
|
||
// --------------------------------------------------------------------------
|
||
|
||
// prepareRebootAndRestart saves state, sets up resume infrastructure, and reboots.
|
||
func prepareRebootAndRestart(
|
||
cfg config.Config,
|
||
allSteps []runner.Step,
|
||
completedResults []runner.Result,
|
||
cfgPath string,
|
||
runCfg runner.RunConfig,
|
||
) {
|
||
pending := pendingStepIDs(allSteps, completedResults)
|
||
st := &state.State{
|
||
Config: cfg,
|
||
ConfigPath: cfgPath,
|
||
LogFile: runCfg.LogFile,
|
||
PendingSteps: pending,
|
||
Results: toStateResults(completedResults),
|
||
}
|
||
if err := state.Save(st); err != nil {
|
||
walk.MsgBox(nil, "Chyba", "Nelze ulozit stav: "+err.Error(), walk.MsgBoxIconError)
|
||
return
|
||
}
|
||
if err := prereboot.Prepare(); err != nil {
|
||
walk.MsgBox(nil, "Chyba", "Prereboot setup selhal: "+err.Error(), walk.MsgBoxIconError)
|
||
return
|
||
}
|
||
reboot()
|
||
}
|
||
|
||
// pendingStepIDs returns IDs of steps that still need to run.
|
||
// Steps with NeedsReboot=true (re-run after reboot) and steps not yet
|
||
// reached (absent from completedResults) are both included.
|
||
func pendingStepIDs(allSteps []runner.Step, completedResults []runner.Result) []string {
|
||
done := make(map[string]bool, len(completedResults))
|
||
for _, r := range completedResults {
|
||
if !r.NeedsReboot {
|
||
done[r.Step.ID] = true
|
||
}
|
||
}
|
||
var pending []string
|
||
for _, s := range allSteps {
|
||
if !done[s.ID] {
|
||
pending = append(pending, s.ID)
|
||
}
|
||
}
|
||
return pending
|
||
}
|
||
|
||
// toStateResults converts runner results to state.StepResult for persistence.
|
||
// Steps with NeedsReboot=true are excluded (they will run again next round).
|
||
func toStateResults(results []runner.Result) []state.StepResult {
|
||
var sr []state.StepResult
|
||
for _, r := range results {
|
||
if r.NeedsReboot {
|
||
continue
|
||
}
|
||
sr = append(sr, state.StepResult{
|
||
StepID: r.Step.ID,
|
||
Num: r.Step.Num,
|
||
Name: r.Step.Name,
|
||
Status: r.Status,
|
||
Elapsed: r.Elapsed,
|
||
})
|
||
}
|
||
return sr
|
||
}
|
||
|
||
// --------------------------------------------------------------------------
|
||
// Phase 3 – Summary with auto-reboot countdown
|
||
// --------------------------------------------------------------------------
|
||
|
||
const rebootCountdown = 60 // seconds before automatic reboot
|
||
|
||
// donePhase shows the final summary screen.
|
||
// currentResults are from the last run; prevResults accumulate earlier rounds.
|
||
func donePhase(currentResults []runner.Result, prevResults []state.StepResult) {
|
||
var (
|
||
mw *walk.MainWindow
|
||
countdownLbl *walk.Label
|
||
)
|
||
|
||
// Combined ordered list: previous rounds first, then current round
|
||
type displayRow struct {
|
||
Num string
|
||
Name string
|
||
Status string
|
||
Elapsed time.Duration
|
||
}
|
||
|
||
var rows []displayRow
|
||
for _, r := range prevResults {
|
||
rows = append(rows, displayRow{r.Num, r.Name, r.Status, r.Elapsed})
|
||
}
|
||
for _, r := range currentResults {
|
||
if r.NeedsReboot {
|
||
continue
|
||
}
|
||
rows = append(rows, displayRow{r.Step.Num, r.Step.Name, r.Status, r.Elapsed})
|
||
}
|
||
|
||
ok, errs, skipped := 0, 0, 0
|
||
widgets := make([]Widget, 0, len(rows))
|
||
for _, row := range rows {
|
||
var icon string
|
||
switch row.Status {
|
||
case "OK":
|
||
ok++
|
||
icon = "OK "
|
||
case "ERROR":
|
||
errs++
|
||
icon = "ERR "
|
||
default:
|
||
skipped++
|
||
icon = "\u2013 "
|
||
}
|
||
text := icon + row.Num + " \u2013 " + row.Name
|
||
if row.Elapsed > 0 {
|
||
text += fmt.Sprintf(" (%s)", row.Elapsed.Round(time.Second))
|
||
}
|
||
widgets = append(widgets, Label{
|
||
Text: text,
|
||
Font: Font{Family: "Consolas", PointSize: 9},
|
||
})
|
||
}
|
||
|
||
summaryText := fmt.Sprintf("OK: %d CHYBY: %d PRESKOCENO: %d", ok, errs, skipped)
|
||
|
||
cancelReboot := make(chan struct{})
|
||
|
||
if err := (MainWindow{
|
||
AssignTo: &mw,
|
||
Title: "xetup \u2014 hotovo",
|
||
Size: Size{Width: 760, Height: 740},
|
||
Layout: VBox{},
|
||
Children: []Widget{
|
||
Label{
|
||
Text: "Hotovo",
|
||
Alignment: AlignHCenterVNear,
|
||
Font: Font{Bold: true, PointSize: 12},
|
||
},
|
||
HSeparator{},
|
||
ScrollView{
|
||
MinSize: Size{Height: 510},
|
||
Layout: VBox{MarginsZero: true},
|
||
Children: widgets,
|
||
},
|
||
HSeparator{},
|
||
Label{
|
||
Text: summaryText,
|
||
Alignment: AlignHCenterVNear,
|
||
Font: Font{Bold: true},
|
||
},
|
||
Label{
|
||
AssignTo: &countdownLbl,
|
||
Text: fmt.Sprintf("Restart za %ds...", rebootCountdown),
|
||
Alignment: AlignHCenterVNear,
|
||
},
|
||
Composite{
|
||
Layout: HBox{MarginsZero: true},
|
||
Children: []Widget{
|
||
HSpacer{},
|
||
PushButton{
|
||
Text: " Restartovat ted ",
|
||
OnClicked: func() {
|
||
close(cancelReboot)
|
||
reboot()
|
||
mw.Close()
|
||
},
|
||
},
|
||
PushButton{
|
||
Text: " Zrusit restart ",
|
||
OnClicked: func() {
|
||
select {
|
||
case <-cancelReboot:
|
||
default:
|
||
close(cancelReboot)
|
||
}
|
||
countdownLbl.SetText("Restart zrusen.")
|
||
},
|
||
},
|
||
HSpacer{},
|
||
},
|
||
},
|
||
},
|
||
}.Create()); err != nil {
|
||
return
|
||
}
|
||
|
||
// Countdown goroutine
|
||
go func() {
|
||
ticker := time.NewTicker(time.Second)
|
||
defer ticker.Stop()
|
||
remaining := rebootCountdown
|
||
for {
|
||
select {
|
||
case <-cancelReboot:
|
||
return
|
||
case <-ticker.C:
|
||
remaining--
|
||
r := remaining
|
||
mw.Synchronize(func() {
|
||
if r > 0 {
|
||
countdownLbl.SetText(fmt.Sprintf("Restart za %ds...", r))
|
||
} else {
|
||
countdownLbl.SetText("Restartuji...")
|
||
}
|
||
})
|
||
if remaining <= 0 {
|
||
reboot()
|
||
mw.Synchronize(func() { mw.Close() })
|
||
return
|
||
}
|
||
}
|
||
}
|
||
}()
|
||
|
||
mw.Run()
|
||
}
|
||
|
||
// reboot issues an immediate Windows restart.
|
||
func reboot() {
|
||
exec.Command("shutdown", "/r", "/t", "0").Run() //nolint:errcheck
|
||
}
|
||
|
||
// --------------------------------------------------------------------------
|
||
// Helpers
|
||
// --------------------------------------------------------------------------
|
||
|
||
func itemEnabled(cfg config.Config, item runner.SelectableItem) bool {
|
||
if item.FeatureID == "" {
|
||
if v, ok := cfg.Steps[item.StepID]; ok {
|
||
return v
|
||
}
|
||
return true
|
||
}
|
||
if feats, ok := cfg.Features[item.StepID]; ok {
|
||
if v, ok2 := feats[item.FeatureID]; ok2 {
|
||
return v
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
func buildStepsAndFeatures(selected map[string]bool) ([]runner.Step, config.Features) {
|
||
items := runner.AllSelectableItems()
|
||
features := make(config.Features)
|
||
stepOn := make(map[string]bool)
|
||
|
||
for _, item := range items {
|
||
if item.FeatureID == "" {
|
||
stepOn[item.StepID] = selected[item.Key]
|
||
} else {
|
||
if features[item.StepID] == nil {
|
||
features[item.StepID] = make(map[string]bool)
|
||
}
|
||
features[item.StepID][item.FeatureID] = selected[item.Key]
|
||
if selected[item.Key] {
|
||
stepOn[item.StepID] = true
|
||
}
|
||
}
|
||
}
|
||
|
||
allSteps := runner.AllSteps()
|
||
steps := make([]runner.Step, len(allSteps))
|
||
for i, s := range allSteps {
|
||
s.Enabled = stepOn[s.ID]
|
||
steps[i] = s
|
||
}
|
||
return steps, features
|
||
}
|
||
|
||
func profileIndex(p string) int {
|
||
switch p {
|
||
case "admin":
|
||
return 1
|
||
case "user":
|
||
return 2
|
||
default:
|
||
return 0
|
||
}
|
||
}
|