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).
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
)
|
||||
@@ -14,15 +15,40 @@ import (
|
||||
// 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 := "/etc/theta42/agent.yml"
|
||||
configPath := defaultConfigPath()
|
||||
if len(os.Args) > 1 && !strings.HasPrefix(os.Args[1], "-") {
|
||||
configPath = os.Args[1]
|
||||
}
|
||||
@@ -41,8 +67,10 @@ func main() {
|
||||
cfg.Capabilities.ArbitraryBash,
|
||||
)
|
||||
|
||||
// Initialize system executor
|
||||
// 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()
|
||||
@@ -53,10 +81,16 @@ func main() {
|
||||
// Home detection + tray status push (polls public IP every 60s).
|
||||
go StartHomeMonitor(cfg, func() bool { return wsConnected.Load() })
|
||||
|
||||
// Block until signal is received
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigs
|
||||
// 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...")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user