Files
lmvpn_client/internal/ui/daemon.go
T
2026-07-06 16:21:06 +08:00

51 lines
1.4 KiB
Go

// Package ui contains the Fyne desktop GUI. It runs as the user,
// manages profiles and credentials (SQLite + Keychain), and controls
// the privileged daemon over IPC.
package ui
import (
"fmt"
"os"
"os/exec"
"time"
"lmvpn/internal/ipc"
"lmvpn/internal/log"
"lmvpn/internal/paths"
)
// ensureDaemon checks if the daemon is running and launches it (as
// root via osascript) if not. It blocks until the daemon is reachable
// or times out.
func ensureDaemon() (*ipc.Client, error) {
// Fast path: daemon already running.
if c, err := ipc.Dial(); err == nil {
return c, nil
}
exe, err := os.Executable()
if err != nil {
return nil, fmt.Errorf("resolve executable: %w", err)
}
// Launch daemon as root via osascript administrator prompt.
script := fmt.Sprintf(
`do shell script "nohup %s daemon > /dev/null 2>&1 &" with administrator privileges`,
exe)
cmd := exec.Command("osascript", "-e", script)
if out, err := cmd.CombinedOutput(); err != nil {
return nil, fmt.Errorf("launch daemon: %w (%s)", err, string(out))
}
log.L().Info("daemon launched via osascript")
// Wait for the daemon to become reachable.
for i := 0; i < 20; i++ {
time.Sleep(250 * time.Millisecond)
if c, err := ipc.Dial(); err == nil {
log.L().Info("daemon connected", "socket", paths.IPCSocketPath())
return c, nil
}
}
return nil, fmt.Errorf("daemon did not become reachable")
}