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>
190 lines
5.7 KiB
Go
190 lines
5.7 KiB
Go
//go:build windows
|
|
|
|
// Package prereboot ensures the reboot-resume infrastructure is in place
|
|
// before xetup triggers a system restart mid-deployment.
|
|
package prereboot
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"golang.org/x/sys/windows/registry"
|
|
)
|
|
|
|
// StablePath is where xetup.exe is copied so it survives across reboots.
|
|
const StablePath = `C:\Windows\Setup\Scripts\xetup.exe`
|
|
|
|
// TaskName is the scheduled task that re-launches xetup after each reboot.
|
|
const TaskName = "X9-Resume"
|
|
|
|
const adminUser = "adminx9"
|
|
const winlogonKey = `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon`
|
|
|
|
// IsAdminx9 reports whether the current process runs as the adminx9 account.
|
|
func IsAdminx9() bool {
|
|
return strings.EqualFold(os.Getenv("USERNAME"), adminUser)
|
|
}
|
|
|
|
// Prepare ensures the reboot-resume infrastructure is in place.
|
|
// When not running as adminx9 (first reboot trigger):
|
|
// - creates the adminx9 local account if it does not exist
|
|
// - copies the running binary to StablePath
|
|
// - enables autologon for adminx9 (AutoLogonCount=20 safety cap)
|
|
// - registers the X9-Resume scheduled task (AtLogOn adminx9, RunLevel Highest)
|
|
//
|
|
// When already running as adminx9 (subsequent rounds) all of the above is
|
|
// already in place; the function returns immediately without changes.
|
|
// Safe to call repeatedly (idempotent).
|
|
func Prepare() error {
|
|
if IsAdminx9() {
|
|
return nil
|
|
}
|
|
if err := ensureAdminx9User(); err != nil {
|
|
return fmt.Errorf("adminx9 account: %w", err)
|
|
}
|
|
if err := copySelfTo(StablePath); err != nil {
|
|
return fmt.Errorf("copy binary: %w", err)
|
|
}
|
|
if err := setAutologon(); err != nil {
|
|
return fmt.Errorf("autologon: %w", err)
|
|
}
|
|
if err := registerResumeTask(); err != nil {
|
|
return fmt.Errorf("resume task: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Cleanup disables autologon and removes the X9-Resume scheduled task.
|
|
// Called when all deployment steps have completed successfully.
|
|
func Cleanup() {
|
|
_ = disableAutologon()
|
|
_ = unregisterResumeTask()
|
|
}
|
|
|
|
// ensureAdminx9User creates the adminx9 local account if absent.
|
|
func ensureAdminx9User() error {
|
|
// "net user adminx9" exits 0 when user exists
|
|
if err := newHiddenCmd("net", "user", adminUser).Run(); err == nil {
|
|
return nil // already exists
|
|
}
|
|
// Create with empty password
|
|
if b, err := newHiddenCmd("net", "user", adminUser, "", "/add").CombinedOutput(); err != nil {
|
|
return fmt.Errorf("net user /add: %s: %w", strings.TrimSpace(string(b)), err)
|
|
}
|
|
// Add to local Administrators group
|
|
if b, err := newHiddenCmd("net", "localgroup", "Administrators", adminUser, "/add").CombinedOutput(); err != nil {
|
|
return fmt.Errorf("net localgroup: %s: %w", strings.TrimSpace(string(b)), err)
|
|
}
|
|
// Hide from the Windows login screen
|
|
k, _, err := registry.CreateKey(registry.LOCAL_MACHINE,
|
|
`SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList`,
|
|
registry.SET_VALUE)
|
|
if err != nil {
|
|
return fmt.Errorf("registry hide: %w", err)
|
|
}
|
|
defer k.Close()
|
|
return k.SetDWordValue(adminUser, 0)
|
|
}
|
|
|
|
// copySelfTo copies the running executable to dst.
|
|
// Skips the copy when src and dst resolve to the same path.
|
|
func copySelfTo(dst string) error {
|
|
src, err := os.Executable()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resolved, err := filepath.EvalSymlinks(src); err == nil {
|
|
src = resolved
|
|
}
|
|
if strings.EqualFold(src, dst) {
|
|
return nil
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
|
|
return err
|
|
}
|
|
return copyFile(src, dst)
|
|
}
|
|
|
|
func copyFile(src, dst string) error {
|
|
r, err := os.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer r.Close()
|
|
w, err := os.Create(dst)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer w.Close()
|
|
_, err = io.Copy(w, r)
|
|
return err
|
|
}
|
|
|
|
func setAutologon() error {
|
|
k, err := registry.OpenKey(registry.LOCAL_MACHINE, winlogonKey, registry.SET_VALUE)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer k.Close()
|
|
for _, kv := range []struct{ name, val string }{
|
|
{"AutoAdminLogon", "1"},
|
|
{"DefaultUserName", adminUser},
|
|
{"DefaultPassword", ""},
|
|
{"DefaultDomainName", "."},
|
|
} {
|
|
if err := k.SetStringValue(kv.name, kv.val); err != nil {
|
|
return fmt.Errorf("set %s: %w", kv.name, err)
|
|
}
|
|
}
|
|
// Safety cap: self-limits even if cleanup task fails to run
|
|
return k.SetDWordValue("AutoLogonCount", 20)
|
|
}
|
|
|
|
func disableAutologon() error {
|
|
k, err := registry.OpenKey(registry.LOCAL_MACHINE, winlogonKey, registry.SET_VALUE)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer k.Close()
|
|
_ = k.SetStringValue("AutoAdminLogon", "0")
|
|
_ = k.DeleteValue("DefaultPassword")
|
|
_ = k.DeleteValue("AutoLogonCount")
|
|
return nil
|
|
}
|
|
|
|
func registerResumeTask() error {
|
|
ps := fmt.Sprintf(`
|
|
$action = New-ScheduledTaskAction -Execute '%s' -Argument '--resume'
|
|
$trigger = New-ScheduledTaskTrigger -AtLogOn -User '%s'
|
|
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Hours 4) -MultipleInstances IgnoreNew
|
|
$principal = New-ScheduledTaskPrincipal -UserId '%s' -LogonType Interactive -RunLevel Highest
|
|
Unregister-ScheduledTask -TaskName 'X9-Resume' -Confirm:$false -ErrorAction SilentlyContinue
|
|
Register-ScheduledTask -TaskName 'X9-Resume' -Action $action -Trigger $trigger -Settings $settings -Principal $principal -Force | Out-Null
|
|
`, StablePath, adminUser, adminUser)
|
|
return runPS(ps)
|
|
}
|
|
|
|
func unregisterResumeTask() error {
|
|
return runPS(`Unregister-ScheduledTask -TaskName 'X9-Resume' -Confirm:$false -ErrorAction SilentlyContinue`)
|
|
}
|
|
|
|
func runPS(script string) error {
|
|
cmd := newHiddenCmd("powershell.exe",
|
|
"-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script)
|
|
b, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %w", strings.TrimSpace(string(b)), err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func newHiddenCmd(name string, args ...string) *exec.Cmd {
|
|
cmd := exec.Command(name, args...)
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
|
return cmd
|
|
}
|