Files
theta-agent/main.go
T
wmantly 4a619f7adc feat(windows): platform ops, service wrapper, helper, and air-gap paths
First Windows parity milestone (DESIGN-WINDOWS.md §13 build order item 1).

- Add a PlatformOps abstraction so command dispatch is OS-neutral:
  - linuxPlatformOps keeps today's systemctl/journalctl/bash behavior (deliberately
    untagged so shared dispatch tests run on Windows CI)
  - windowsPlatformOps maps reboot/shutdown to shutdown.exe, service control to
    sc.exe (stop+start for restart), fetch_logs to Get-WinEvent, arbitrary_bash to
    powershell -EncodedCommand (byte-exact under arbitrary quoting), and declines
    configure_ldap (Windows logon goes through OpenCredential)
- Run theta-agent as a Windows service (x/sys/windows/svc): SYSTEM auto-start,
    SCM stop/shutdown handling; CLI install-service/remove-service via svc/mgr
- Add theta-agent-helper (session-0 companion): lock/display_off/logout via
    user32/wtsapi32, and staged self-update (wait for service stop, swap the
    locked exe, sc start)
- Self-update becomes platform-aware: Linux renames over the running binary;
    Windows stages .new and hands the swap to the helper (running exe is locked)
- Platform paths: agent.yml and tray.sock under %ProgramData%\Theta42 (the
    service runs as SYSTEM while the tray runs as the user, so the per-user temp
    dir no longer works for tray IPC); LDAP byte-pump falls back to TCP loopback
- config: service_name, desktop_helper, public_ip_detect (air-gap: skips
    external public-IP lookups in telemetry + home monitor), wireguard block
- cli: platform-aware config path + self-update artifact name + service restart
- tests: dispatch tests pin linuxPlatformOps; 0600 mode assertions gated to
    POSIX so the suite is green on Windows

Rebuilds all tracked dist binaries (v2.1.0).
2026-08-09 17:10:07 -07:00

97 lines
2.5 KiB
Go

package main
import (
"fmt"
"log"
"os"
"os/signal"
"strings"
"sync"
"sync/atomic"
"syscall"
)
// wsConnected is flipped atomically by connectWebSocket as the connection
// comes up and drops, so StartHomeMonitor can read it without a mutex.
var wsConnected atomic.Bool
// agentStopCh closes once the agent should exit: a SIGINT/SIGTERM in the
// foreground, or the SCM stop request when running as a Windows service. Both
// runAgent (foreground) and the service handler wait on it.
var (
agentStopOnce sync.Once
agentStopCh = make(chan struct{})
)
// stopAgent signals the running agent to shut down. Idempotent.
func stopAgent() {
agentStopOnce.Do(func() { close(agentStopCh) })
}
func main() {
if len(os.Args) > 1 && handleCLI(os.Args[1:]) {
return
}
// Windows: when installed as a service the SCM starts us and svc.Run takes
// over the process lifecycle. Foreground (or any other OS) falls through to
// runAgent, which blocks until a signal arrives.
if maybeRunAsService() {
return
}
runAgent()
}
// runAgent runs the agent daemon until stopAgent is called.
func runAgent() {
log.Println("Starting Theta Agent...")
// Attempt to load configuration
configPath := defaultConfigPath()
if len(os.Args) > 1 && !strings.HasPrefix(os.Args[1], "-") {
configPath = os.Args[1]
}
cm, err := NewConfigManager(configPath)
if err != nil {
log.Fatalf("Error loading configuration from %s: %v", configPath, err)
}
cfg := cm.Get()
log.Printf("Connecting to SSO Manager at %s", cfg.ServerURL)
log.Printf("Loaded capabilities: Telemetry=%v, LDAP=%v, Reboot=%v, Bash=%v",
cfg.Capabilities.Telemetry,
cfg.Capabilities.ConfigureLDAP,
cfg.Capabilities.Reboot,
cfg.Capabilities.ArbitraryBash,
)
// Initialize system executor and pin the platform ops the command
// dispatcher runs behind.
exec := &SystemExecutor{}
defaultPlatformOps = NewPlatformOps(cfg, exec)
// Tray IPC server — desktop tray connects here for status updates.
go globalTrayServer.Start()
// WebSocket connection to SSO Manager
go connectWebSocket(cm, exec)
// Home detection + tray status push (polls public IP every 60s).
go StartHomeMonitor(cfg, func() bool { return wsConnected.Load() })
// Foreground: exit on SIGINT/SIGTERM. A Windows service ignores these and
// is driven by its own handler.
go func() {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
stopAgent()
}()
<-agentStopCh
fmt.Println("Shutting down Theta Agent...")
}