4a619f7adc
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).
87 lines
3.1 KiB
Go
87 lines
3.1 KiB
Go
package main
|
|
|
|
// IPC protocol between the root daemon (theta-agent) and the desktop tray
|
|
// (theta-agent-tray). Both processes communicate via a Unix domain socket at
|
|
// /run/theta/tray.sock (created by the daemon; the tray connects to it).
|
|
//
|
|
// Protocol: newline-delimited JSON. The daemon streams TrayStatus messages to
|
|
// any connected tray client. The tray sends TrayCommand messages to the daemon.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"runtime"
|
|
)
|
|
|
|
// TraySocketPaths are the paths the daemon tries to bind, in order.
|
|
//
|
|
// Windows has no /run or /tmp. The daemon runs as the SYSTEM service while the
|
|
// tray runs as the logged-in user, so the socket lives in the shared data dir
|
|
// (%ProgramData%\Theta42, created by the installer with a Users-writable ACL)
|
|
// rather than a per-user temp dir — the two processes must agree on one path.
|
|
// Linux keeps the original /run/theta path with a /tmp fallback.
|
|
var TraySocketPaths = func() []string {
|
|
if runtime.GOOS == "windows" {
|
|
return []string{windowsTraySocketPath()}
|
|
}
|
|
return []string{"/run/theta/tray.sock", "/tmp/theta-tray.sock"}
|
|
}()
|
|
|
|
// TraySocket is the canonical socket path the tray dials. It matches the last
|
|
// entry in TraySocketPaths on each platform.
|
|
var TraySocket = func() string {
|
|
if runtime.GOOS == "windows" {
|
|
return windowsTraySocketPath()
|
|
}
|
|
return "/tmp/theta-tray.sock"
|
|
}()
|
|
|
|
|
|
// TrayColor represents the icon color state.
|
|
type TrayColor string
|
|
|
|
const (
|
|
ColorRed TrayColor = "red" // not connected to directory
|
|
ColorYellow TrayColor = "yellow" // connected, but not home
|
|
ColorGreen TrayColor = "green" // connected, on home LAN (public IP matches)
|
|
ColorBlue TrayColor = "blue" // connected + WireGuard tunnel active to home site
|
|
)
|
|
|
|
// TrayStatus is sent from the daemon to the tray on every state change.
|
|
type TrayStatus struct {
|
|
Color TrayColor `json:"color"`
|
|
Connected bool `json:"connected"` // directory WebSocket is up
|
|
IsHome bool `json:"is_home"` // public IP matches home site
|
|
VPNActive bool `json:"vpn_active"` // WireGuard tunnel is up
|
|
AutoVPN bool `json:"auto_vpn"` // auto-connect preference
|
|
SiteName string `json:"site_name"` // configured site name
|
|
AgentPublicIP string `json:"agent_public_ip"` // this agent's detected public IP
|
|
HomePublicIP string `json:"home_public_ip"` // home site's public IP (from directory)
|
|
StatusText string `json:"status_text"` // one-line human description
|
|
}
|
|
|
|
// TrayCommand is sent from the tray to the daemon.
|
|
type TrayCommand struct {
|
|
Command string `json:"command"` // "set_auto_vpn", "vpn_connect", "vpn_disconnect"
|
|
Value bool `json:"value"` // used by set_auto_vpn
|
|
}
|
|
|
|
func encodeTrayStatus(s TrayStatus) ([]byte, error) {
|
|
b, err := json.Marshal(s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return append(b, '\n'), nil
|
|
}
|
|
|
|
func decodeTrayCommand(data []byte) (TrayCommand, error) {
|
|
var cmd TrayCommand
|
|
err := json.Unmarshal(data, &cmd)
|
|
return cmd, err
|
|
}
|
|
|
|
func decodeTrayStatus(data []byte) (TrayStatus, error) {
|
|
var s TrayStatus
|
|
err := json.Unmarshal(data, &s)
|
|
return s, err
|
|
}
|