//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 }