//go:build windows package main // windowsPlatformOps implements PlatformOps for Windows. Remote ops map to // Windows equivalents: // // - reboot/shutdown → shutdown.exe // - service control → sc.exe // - fetch logs → Get-WinEvent (PowerShell) // - arbitrary_bash → powershell -EncodedCommand (survives arbitrary quoting) // - desktop control → theta-agent-helper (session-0 workaround, see // DESIGN-WINDOWS.md §4) // - self-update → staged `.new` + helper swap (the running exe is locked) // // configure_ldap is declined: Windows logon goes through the OpenCredential // credential provider, which the installer configures directly. import ( "encoding/base64" "fmt" "io" "log" "os" "os/exec" "strings" "syscall" "unicode/utf16" "golang.org/x/sys/windows" ) type windowsPlatformOps struct { exec Executor helperPath string // theta-agent-helper.exe (DESIGN-WINDOWS.md §8) serviceName string // Windows service name (defaults to theta-agent) } func (p *windowsPlatformOps) Reboot() ([]byte, error) { return p.exec.Execute("shutdown", "/r", "/t", "0") } func (p *windowsPlatformOps) Shutdown() ([]byte, error) { return p.exec.Execute("shutdown", "/s", "/t", "0") } func (p *windowsPlatformOps) FetchLogs(service string, lines int) ([]byte, error) { script := fmt.Sprintf("Get-WinEvent -LogName Application -MaxEvents %d -ErrorAction SilentlyContinue | Format-List TimeCreated, ProviderName, Id, LevelDisplayName, Message", lines) return p.runPowerShell(script) } // ServiceControl maps systemd-style actions onto sc.exe. `restart` has no // one-shot sc command, so it stops then starts; `status` is `sc query`. func (p *windowsPlatformOps) ServiceControl(service, action string) ([]byte, error) { switch action { case "status": return p.exec.Execute("sc.exe", "query", service) case "restart": // A service that is already stopped is not an error worth failing on. if _, err := p.exec.Execute("sc.exe", "stop", service); err != nil { log.Printf("[windows] sc stop %s: %v (continuing to start)", service, err) } out, err := p.exec.Execute("sc.exe", "start", service) if err != nil && strings.Contains(string(out), "1056") { // ERROR_SERVICE_ALREADY_RUNNING — the stop never landed; treat as up. return out, nil } return out, err default: return p.exec.Execute("sc.exe", action, service) } } func (p *windowsPlatformOps) RunScript(script string) ([]byte, error) { return p.runPowerShell(script) } // runPowerShell invokes powershell with a UTF-16LE base64 -EncodedCommand. An // operator script can contain arbitrary quotes, `&`, `%`, etc.; passing it as a // plain argument would be mangled by cmd.exe/arg quoting rules, while the // encoded form is byte-exact on both sides. func (p *windowsPlatformOps) runPowerShell(script string) ([]byte, error) { units := utf16.Encode([]rune(script)) b := make([]byte, len(units)*2) for i, r := range units { b[i*2] = byte(r) b[i*2+1] = byte(r >> 8) } enc := base64.StdEncoding.EncodeToString(b) return p.exec.Execute("powershell", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", enc) } // DesktopControl routes ops that need an interactive desktop to the helper, // which the service launches in the target session (DESIGN-WINDOWS.md §4). // Sleep can run from session 0 and is handled in-process. func (p *windowsPlatformOps) DesktopControl(subAction, targetUser string) ([]byte, error) { var action string switch subAction { case "sleep_host", "sleep": return nil, setSuspendState() case "lock_session", "lock": action = "lock" case "display_off": action = "display_off" case "logout_user", "logout": action = "logout" default: return nil, fmt.Errorf("unknown desktop action '%s'", subAction) } if p.helperPath == "" { return nil, fmt.Errorf("desktop control requires theta-agent-helper (desktop_helper not configured)") } args := []string{action} if targetUser != "" { args = append(args, targetUser) } return p.exec.Execute(p.helperPath, args...) } // setSuspendState puts the machine to sleep. Requires SeShutdownPrivilege, // which the SYSTEM service holds. func setSuspendState() error { powrprof := syscall.NewLazyDLL("powrprof.dll") proc := powrprof.NewProc("SetSuspendState") r, _, err := proc.Call(0, 0, 0) // Hibernate=false, ForceCritical=false, WakeIfDisarmed=false if r == 0 { return err } return nil } // ConfigureLDAP is not applicable on Windows: directory logon uses the // OpenCredential credential provider configured by the installer. func (p *windowsPlatformOps) ConfigureLDAP(configData string) error { return fmt.Errorf("configure_ldap is not applicable on Windows; logon is managed by the OpenCredential credential provider") } // ApplyUpdate downloads and verifies the new binary to `.new`, then hands // the swap to the helper: the running service holds the exe open, so the agent // must stop before the file can be replaced. The helper outlives the service // (detached process), swaps the files, and restarts the service. func (p *windowsPlatformOps) ApplyUpdate(downloadURL, checksum string) error { selfPath, err := os.Executable() if err != nil { return fmt.Errorf("failed to resolve current binary path: %w", err) } tmpPath, err := downloadBinary(downloadURL, checksum) if err != nil { return err } newPath := selfPath + ".new" if err := moveFile(tmpPath, newPath); err != nil { return fmt.Errorf("failed to stage new binary: %w", err) } helper := p.helperPath if helper == "" { return fmt.Errorf("desktop_helper not configured; cannot complete self-update") } service := p.serviceName if service == "" { service = "theta-agent" } log.Printf("[windows] staging self-update via helper (%s -> %s)", newPath, selfPath) if err := spawnDetached(helper, "update", newPath, selfPath, service); err != nil { return fmt.Errorf("failed to launch updater helper: %w", err) } return nil } func (p *windowsPlatformOps) SelfRestart() { stopAgent() } // spawnDetached launches exe as a background process that survives this one. func spawnDetached(exe string, args ...string) error { cmd := execCommand(exe, args...) cmd.SysProcAttr = &syscall.SysProcAttr{ CreationFlags: windows.DETACHED_PROCESS | windows.CREATE_NEW_PROCESS_GROUP, } cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Start() } // execCommand is a thin wrapper so tests that build the windows ops can stub it. func execCommand(name string, args ...string) *exec.Cmd { return exec.Command(name, args...) } // moveFile renames src onto dst, falling back to a copy when the source is on a // different volume than the destination (os.Rename fails across volumes). func moveFile(src, dst string) error { if err := os.Rename(src, dst); err == nil { return nil } in, err := os.Open(src) if err != nil { return err } defer in.Close() out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) if err != nil { return err } if _, err := io.Copy(out, in); err != nil { out.Close() os.Remove(dst) return err } if err := out.Close(); err != nil { os.Remove(dst) return err } return os.Remove(src) }