Files
byre/cmd/byre/install.go
T
2026-10-18 22:17:54 -07:00

112 lines
2.8 KiB
Go

package main
import (
"context"
"encoding/xml"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"g.fc.run/ai/byre/internal/tailscale"
)
const serviceLabel = "run.fc.byre"
const plistTemplate = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>%s</string>
<key>ProgramArguments</key>
<array>
<string>%s</string>
<string>serve</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>%s</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>%s</string>
<key>StandardErrorPath</key>
<string>%s</string>
</dict>
</plist>
`
func installCmd() {
requireChecks()
exe, err := os.Executable()
if err != nil {
fatal("find executable", "err", err)
}
exe, err = filepath.EvalSymlinks(exe)
if err != nil {
fatal("resolve executable", "err", err)
}
home, err := os.UserHomeDir()
if err != nil {
fatal("home dir", "err", err)
}
plistPath := filepath.Join(home, "Library", "LaunchAgents", serviceLabel+".plist")
logPath := filepath.Join(home, "Library", "Logs", "byre.log")
plist := fmt.Sprintf(plistTemplate, xmlText(serviceLabel), xmlText(exe), xmlText(filepath.Dir(exe)+":"+os.Getenv("PATH")), xmlText(logPath), xmlText(logPath))
err = os.MkdirAll(filepath.Dir(plistPath), 0o755)
if err != nil {
fatal("create launch agents dir", "err", err)
}
err = os.WriteFile(plistPath, []byte(plist), 0o644)
if err != nil {
fatal("write launch agent", "err", err)
}
fmt.Println("wrote", plistPath)
url, err := tailscale.ServeRoot(context.Background(), "http://"+defaultListen)
if err != nil {
fatal("tailscale serve", "err", err)
}
fmt.Println("byre is at", url)
domain := fmt.Sprintf("gui/%d", os.Getuid())
service := domain + "/" + serviceLabel
if exec.Command("launchctl", "print", service).Run() != nil {
run("launchctl", "bootstrap", domain, plistPath)
fmt.Println("started", service, "running", exe)
return
}
// bootout kills every process the service started, which includes this one when an agent runs it.
reload := exec.Command("/bin/sh", "-c", `launchctl bootout "$1"; launchctl bootstrap "$2" "$3"`, "sh", service, domain, plistPath)
reload.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
err = reload.Start()
if err != nil {
fatal("restart service", "err", err)
}
fmt.Println("restarting", service, "on", exe)
}
func run(name string, args ...string) {
cmd := exec.Command(name, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
fatal("run "+name, "args", args, "err", err)
}
}
func xmlText(s string) string {
var b strings.Builder
xml.EscapeText(&b, []byte(s))
return b.String()
}