Files
theta-agent/tray_ipc.go
T
wmantly 565a171797 fix(tray): support Windows socket paths and always run the tray companion on Windows
The tray IPC socket was hardcoded to /run/theta/tray.sock and /tmp/theta-tray.sock,
which cannot be bound on Windows (they resolve to C:\run\... and C:\tmp\... and need
admin rights). The daemon now binds a Unix socket under the per-user temp dir on
Windows, and the tray companion dials the same path.

The tray also exited immediately on Windows because it checked DISPLAY/WAYLAND_DISPLAY,
which are never set there. That graphical-session guard is now Windows-only; the tray
always runs on Windows.
2026-08-09 16:11:42 -07:00

87 lines
3.0 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"
"os"
"path/filepath"
"runtime"
)
// TraySocketPaths are the paths the daemon tries to bind, in order.
//
// Windows has no /run or /tmp; a Unix socket in the per-user temp dir works
// there (AF_UNIX is supported since Windows 10 1803) and needs no admin
// rights. Linux keeps the original /run/theta path with a /tmp fallback.
var TraySocketPaths = func() []string {
if runtime.GOOS == "windows" {
return []string{filepath.Join(os.TempDir(), "theta-tray.sock")}
}
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 filepath.Join(os.TempDir(), "theta-tray.sock")
}
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
}