add recording, fix solpush, persistence tests and tiny improvements
This commit is contained in:
@@ -12,6 +12,17 @@ import (
|
||||
"text/template"
|
||||
)
|
||||
|
||||
var currentUserHomeDir = func() (string, error) {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if usr.HomeDir == "" {
|
||||
return "", fmt.Errorf("current user home directory is empty")
|
||||
}
|
||||
return usr.HomeDir, nil
|
||||
}
|
||||
|
||||
const launchAgentPlist = `<?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">
|
||||
@@ -48,26 +59,37 @@ func plistLabel() string {
|
||||
return "com.overlord.agent"
|
||||
}
|
||||
|
||||
func validateStartupName() error {
|
||||
if DefaultStartupName != "" && !strings.HasPrefix(DefaultStartupName, "com.") {
|
||||
return fmt.Errorf("startup name %q is invalid for macOS: LaunchAgent labels must start with \"com.\" (e.g. com.apple.updater)", DefaultStartupName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getPlistPath() (string, error) {
|
||||
usr, err := user.Current()
|
||||
homeDir, err := currentUserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(usr.HomeDir, "Library", "LaunchAgents", plistLabel()+".plist"), nil
|
||||
return filepath.Join(homeDir, "Library", "LaunchAgents", plistLabel()+".plist"), nil
|
||||
}
|
||||
|
||||
func getTargetPath() (string, error) {
|
||||
usr, err := user.Current()
|
||||
if err := validateStartupName(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
homeDir, err := currentUserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(usr.HomeDir, "Library", "Application Support", "Overlord", binaryName()), nil
|
||||
return filepath.Join(homeDir, "Library", "Application Support", "Overlord", binaryName()), nil
|
||||
}
|
||||
|
||||
func install(exePath string) error {
|
||||
|
||||
if DefaultStartupName != "" && !strings.HasPrefix(DefaultStartupName, "com.") {
|
||||
return fmt.Errorf("startup name %q is invalid for macOS: LaunchAgent labels must start with \"com.\" (e.g. com.apple.updater)", DefaultStartupName)
|
||||
if err := validateStartupName(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetPath, err := getTargetPath()
|
||||
@@ -150,11 +172,11 @@ func replaceExecutable(exePath, targetPath string) error {
|
||||
|
||||
if err := os.Rename(tmpPath, targetPath); err != nil {
|
||||
if removeErr := os.Remove(targetPath); removeErr == nil {
|
||||
if err = os.Rename(tmpPath, targetPath); err == nil {
|
||||
return nil
|
||||
}
|
||||
err = os.Rename(tmpPath, targetPath)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to replace executable at %s: %w", targetPath, err)
|
||||
}
|
||||
return fmt.Errorf("failed to replace executable at %s: %w", targetPath, err)
|
||||
}
|
||||
|
||||
if err := os.Chmod(targetPath, 0755); err != nil {
|
||||
@@ -165,6 +187,10 @@ func replaceExecutable(exePath, targetPath string) error {
|
||||
}
|
||||
|
||||
func configure(exePath string) error {
|
||||
if err := validateStartupName(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
plistPath, err := getPlistPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get plist path: %w", err)
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
//go:build darwin && !ios && !ios_target
|
||||
|
||||
package persistence
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func withDarwinTempHome(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
home := t.TempDir()
|
||||
origHome := currentUserHomeDir
|
||||
origStartupName := DefaultStartupName
|
||||
currentUserHomeDir = func() (string, error) { return home, nil }
|
||||
DefaultStartupName = ""
|
||||
|
||||
t.Cleanup(func() {
|
||||
currentUserHomeDir = origHome
|
||||
DefaultStartupName = origStartupName
|
||||
})
|
||||
|
||||
return home
|
||||
}
|
||||
|
||||
func writeDarwinSourceExecutable(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
src := filepath.Join(t.TempDir(), "source-agent")
|
||||
if err := os.WriteFile(src, []byte("agent-bytes"), 0644); err != nil {
|
||||
t.Fatalf("write source executable: %v", err)
|
||||
}
|
||||
return src
|
||||
}
|
||||
|
||||
func TestDarwinInstallCreatesLaunchAgentAndExecutable(t *testing.T) {
|
||||
home := withDarwinTempHome(t)
|
||||
src := writeDarwinSourceExecutable(t)
|
||||
|
||||
if err := InstallFrom(src); err != nil {
|
||||
t.Fatalf("InstallFrom() error: %v", err)
|
||||
}
|
||||
|
||||
target := filepath.Join(home, "Library", "Application Support", "Overlord", "agent")
|
||||
gotBytes, err := os.ReadFile(target)
|
||||
if err != nil {
|
||||
t.Fatalf("read installed executable: %v", err)
|
||||
}
|
||||
if string(gotBytes) != "agent-bytes" {
|
||||
t.Fatalf("installed executable content mismatch: %q", string(gotBytes))
|
||||
}
|
||||
if info, err := os.Stat(target); err != nil {
|
||||
t.Fatalf("stat installed executable: %v", err)
|
||||
} else if info.Mode().Perm() != 0755 {
|
||||
t.Fatalf("installed executable mode = %o, want 0755", info.Mode().Perm())
|
||||
}
|
||||
|
||||
plistPath := filepath.Join(home, "Library", "LaunchAgents", "com.overlord.agent.plist")
|
||||
plistBytes, err := os.ReadFile(plistPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read launch agent plist: %v", err)
|
||||
}
|
||||
plist := string(plistBytes)
|
||||
for _, want := range []string{
|
||||
"<string>com.overlord.agent</string>",
|
||||
"<key>ProgramArguments</key>",
|
||||
"<string>" + target + "</string>",
|
||||
"<key>RunAtLoad</key>",
|
||||
"<key>KeepAlive</key>",
|
||||
} {
|
||||
if !strings.Contains(plist, want) {
|
||||
t.Fatalf("launch agent plist missing %q:\n%s", want, plist)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDarwinConfigureCreatesLaunchAgentForExistingPath(t *testing.T) {
|
||||
home := withDarwinTempHome(t)
|
||||
exePath := filepath.Join(home, "bin", "existing-agent")
|
||||
|
||||
if err := Configure(exePath); err != nil {
|
||||
t.Fatalf("Configure() error: %v", err)
|
||||
}
|
||||
|
||||
plistPath := filepath.Join(home, "Library", "LaunchAgents", "com.overlord.agent.plist")
|
||||
plistBytes, err := os.ReadFile(plistPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read launch agent plist: %v", err)
|
||||
}
|
||||
if plist := string(plistBytes); !strings.Contains(plist, "<string>"+exePath+"</string>") {
|
||||
t.Fatalf("launch agent plist missing executable path %q:\n%s", exePath, plist)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDarwinCustomStartupNameRequiresLaunchAgentLabel(t *testing.T) {
|
||||
home := withDarwinTempHome(t)
|
||||
DefaultStartupName = "updater"
|
||||
|
||||
if err := InstallFrom(writeDarwinSourceExecutable(t)); err == nil {
|
||||
t.Fatal("InstallFrom() error = nil, want invalid startup name error")
|
||||
}
|
||||
if err := Configure(filepath.Join(home, "bin", "updater")); err == nil {
|
||||
t.Fatal("Configure() error = nil, want invalid startup name error")
|
||||
}
|
||||
if _, err := TargetPath(); err == nil {
|
||||
t.Fatal("TargetPath() error = nil, want invalid startup name error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDarwinCustomStartupNameCreatesCustomLabelAndBinary(t *testing.T) {
|
||||
home := withDarwinTempHome(t)
|
||||
DefaultStartupName = "com.example.updater"
|
||||
|
||||
if err := InstallFrom(writeDarwinSourceExecutable(t)); err != nil {
|
||||
t.Fatalf("InstallFrom() error: %v", err)
|
||||
}
|
||||
|
||||
target := filepath.Join(home, "Library", "Application Support", "Overlord", "com.example.updater")
|
||||
if _, err := os.Stat(target); err != nil {
|
||||
t.Fatalf("stat custom executable: %v", err)
|
||||
}
|
||||
|
||||
plistPath := filepath.Join(home, "Library", "LaunchAgents", "com.example.updater.plist")
|
||||
plistBytes, err := os.ReadFile(plistPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read custom launch agent plist: %v", err)
|
||||
}
|
||||
plist := string(plistBytes)
|
||||
if !strings.Contains(plist, "<string>com.example.updater</string>") {
|
||||
t.Fatalf("custom launch agent plist missing label:\n%s", plist)
|
||||
}
|
||||
if !strings.Contains(plist, "<string>"+target+"</string>") {
|
||||
t.Fatalf("custom launch agent plist missing target %q:\n%s", target, plist)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDarwinReplaceExecutablePreservesExecutableModeWhenTargetExists(t *testing.T) {
|
||||
_ = withDarwinTempHome(t)
|
||||
src := writeDarwinSourceExecutable(t)
|
||||
target := filepath.Join(t.TempDir(), "agent")
|
||||
|
||||
if err := os.WriteFile(target, []byte("old"), 0644); err != nil {
|
||||
t.Fatalf("write existing target: %v", err)
|
||||
}
|
||||
if err := replaceExecutable(src, target); err != nil {
|
||||
t.Fatalf("replaceExecutable() error: %v", err)
|
||||
}
|
||||
if info, err := os.Stat(target); err != nil {
|
||||
t.Fatalf("stat replaced executable: %v", err)
|
||||
} else if info.Mode().Perm() != 0755 {
|
||||
t.Fatalf("replaced executable mode = %o, want 0755", info.Mode().Perm())
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,17 @@ import (
|
||||
"text/template"
|
||||
)
|
||||
|
||||
var currentUserHomeDir = func() (string, error) {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if usr.HomeDir == "" {
|
||||
return "", fmt.Errorf("current user home directory is empty")
|
||||
}
|
||||
return usr.HomeDir, nil
|
||||
}
|
||||
|
||||
const systemdService = `[Unit]
|
||||
Description=Overlord Agent
|
||||
After=network.target
|
||||
@@ -43,27 +54,27 @@ func binaryName() string {
|
||||
}
|
||||
|
||||
func getSystemdPath() (string, error) {
|
||||
usr, err := user.Current()
|
||||
homeDir, err := currentUserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(usr.HomeDir, ".config", "systemd", "user", binaryName()+".service"), nil
|
||||
return filepath.Join(homeDir, ".config", "systemd", "user", binaryName()+".service"), nil
|
||||
}
|
||||
|
||||
func getAutostartPath() (string, error) {
|
||||
usr, err := user.Current()
|
||||
homeDir, err := currentUserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(usr.HomeDir, ".config", "autostart", binaryName()+".desktop"), nil
|
||||
return filepath.Join(homeDir, ".config", "autostart", binaryName()+".desktop"), nil
|
||||
}
|
||||
|
||||
func getTargetPath() (string, error) {
|
||||
usr, err := user.Current()
|
||||
homeDir, err := currentUserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(usr.HomeDir, ".local", "share", "overlord", binaryName()), nil
|
||||
return filepath.Join(homeDir, ".local", "share", "overlord", binaryName()), nil
|
||||
}
|
||||
|
||||
func install(exePath string) error {
|
||||
@@ -119,11 +130,11 @@ func replaceExecutable(exePath, targetPath string) error {
|
||||
|
||||
if err := os.Rename(tmpPath, targetPath); err != nil {
|
||||
if removeErr := os.Remove(targetPath); removeErr == nil {
|
||||
if err = os.Rename(tmpPath, targetPath); err == nil {
|
||||
return nil
|
||||
}
|
||||
err = os.Rename(tmpPath, targetPath)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to replace executable at %s: %w", targetPath, err)
|
||||
}
|
||||
return fmt.Errorf("failed to replace executable at %s: %w", targetPath, err)
|
||||
}
|
||||
|
||||
if err := os.Chmod(targetPath, 0755); err != nil {
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package persistence
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func withLinuxTempHome(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
home := t.TempDir()
|
||||
origHome := currentUserHomeDir
|
||||
origStartupName := DefaultStartupName
|
||||
currentUserHomeDir = func() (string, error) { return home, nil }
|
||||
DefaultStartupName = ""
|
||||
|
||||
t.Cleanup(func() {
|
||||
currentUserHomeDir = origHome
|
||||
DefaultStartupName = origStartupName
|
||||
})
|
||||
|
||||
return home
|
||||
}
|
||||
|
||||
func writeLinuxSourceExecutable(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
src := filepath.Join(t.TempDir(), "source-agent")
|
||||
if err := os.WriteFile(src, []byte("agent-bytes"), 0644); err != nil {
|
||||
t.Fatalf("write source executable: %v", err)
|
||||
}
|
||||
return src
|
||||
}
|
||||
|
||||
func TestLinuxInstallCreatesSystemdServiceAndExecutable(t *testing.T) {
|
||||
home := withLinuxTempHome(t)
|
||||
src := writeLinuxSourceExecutable(t)
|
||||
|
||||
if err := InstallFrom(src); err != nil {
|
||||
t.Fatalf("InstallFrom() error: %v", err)
|
||||
}
|
||||
|
||||
target := filepath.Join(home, ".local", "share", "overlord", "agent")
|
||||
gotBytes, err := os.ReadFile(target)
|
||||
if err != nil {
|
||||
t.Fatalf("read installed executable: %v", err)
|
||||
}
|
||||
if string(gotBytes) != "agent-bytes" {
|
||||
t.Fatalf("installed executable content mismatch: %q", string(gotBytes))
|
||||
}
|
||||
if info, err := os.Stat(target); err != nil {
|
||||
t.Fatalf("stat installed executable: %v", err)
|
||||
} else if info.Mode().Perm() != 0755 {
|
||||
t.Fatalf("installed executable mode = %o, want 0755", info.Mode().Perm())
|
||||
}
|
||||
|
||||
servicePath := filepath.Join(home, ".config", "systemd", "user", "agent.service")
|
||||
serviceBytes, err := os.ReadFile(servicePath)
|
||||
if err != nil {
|
||||
t.Fatalf("read systemd service: %v", err)
|
||||
}
|
||||
service := string(serviceBytes)
|
||||
for _, want := range []string{
|
||||
"Description=Overlord Agent",
|
||||
"ExecStart=" + target,
|
||||
"Restart=always",
|
||||
"WantedBy=default.target",
|
||||
} {
|
||||
if !strings.Contains(service, want) {
|
||||
t.Fatalf("systemd service missing %q:\n%s", want, service)
|
||||
}
|
||||
}
|
||||
|
||||
autostartPath := filepath.Join(home, ".config", "autostart", "agent.desktop")
|
||||
if _, err := os.Stat(autostartPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("default install should prefer systemd and not write desktop entry, stat err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinuxConfigureCreatesSystemdServiceForExistingPath(t *testing.T) {
|
||||
home := withLinuxTempHome(t)
|
||||
exePath := filepath.Join(home, "bin", "existing-agent")
|
||||
|
||||
if err := Configure(exePath); err != nil {
|
||||
t.Fatalf("Configure() error: %v", err)
|
||||
}
|
||||
|
||||
servicePath := filepath.Join(home, ".config", "systemd", "user", "agent.service")
|
||||
serviceBytes, err := os.ReadFile(servicePath)
|
||||
if err != nil {
|
||||
t.Fatalf("read systemd service: %v", err)
|
||||
}
|
||||
if service := string(serviceBytes); !strings.Contains(service, "ExecStart="+exePath) {
|
||||
t.Fatalf("systemd service missing executable path %q:\n%s", exePath, service)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinuxInstallAutostartCreatesDesktopEntry(t *testing.T) {
|
||||
home := withLinuxTempHome(t)
|
||||
exePath := filepath.Join(home, "bin", "existing-agent")
|
||||
|
||||
if err := installAutostart(exePath); err != nil {
|
||||
t.Fatalf("installAutostart() error: %v", err)
|
||||
}
|
||||
|
||||
desktopPath := filepath.Join(home, ".config", "autostart", "agent.desktop")
|
||||
desktopBytes, err := os.ReadFile(desktopPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read desktop entry: %v", err)
|
||||
}
|
||||
desktop := string(desktopBytes)
|
||||
for _, want := range []string{
|
||||
"[Desktop Entry]",
|
||||
"Type=Application",
|
||||
"Exec=" + exePath,
|
||||
"X-GNOME-Autostart-enabled=true",
|
||||
} {
|
||||
if !strings.Contains(desktop, want) {
|
||||
t.Fatalf("desktop entry missing %q:\n%s", want, desktop)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinuxCustomStartupNameChangesServiceAndBinaryNames(t *testing.T) {
|
||||
home := withLinuxTempHome(t)
|
||||
DefaultStartupName = "updater"
|
||||
|
||||
if err := InstallFrom(writeLinuxSourceExecutable(t)); err != nil {
|
||||
t.Fatalf("InstallFrom() error: %v", err)
|
||||
}
|
||||
|
||||
target := filepath.Join(home, ".local", "share", "overlord", "updater")
|
||||
if _, err := os.Stat(target); err != nil {
|
||||
t.Fatalf("stat custom executable: %v", err)
|
||||
}
|
||||
|
||||
servicePath := filepath.Join(home, ".config", "systemd", "user", "updater.service")
|
||||
serviceBytes, err := os.ReadFile(servicePath)
|
||||
if err != nil {
|
||||
t.Fatalf("read custom systemd service: %v", err)
|
||||
}
|
||||
if service := string(serviceBytes); !strings.Contains(service, "ExecStart="+target) {
|
||||
t.Fatalf("custom systemd service missing target %q:\n%s", target, service)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinuxUninstallRemovesStartupFilesAndExecutable(t *testing.T) {
|
||||
home := withLinuxTempHome(t)
|
||||
|
||||
target := filepath.Join(home, ".local", "share", "overlord", "agent")
|
||||
servicePath := filepath.Join(home, ".config", "systemd", "user", "agent.service")
|
||||
desktopPath := filepath.Join(home, ".config", "autostart", "agent.desktop")
|
||||
for _, path := range []string{target, servicePath, desktopPath} {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
t.Fatalf("mkdir %q: %v", filepath.Dir(path), err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte("x"), 0644); err != nil {
|
||||
t.Fatalf("write %q: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := Remove(); err != nil {
|
||||
t.Fatalf("Remove() error: %v", err)
|
||||
}
|
||||
for _, path := range []string{target, servicePath, desktopPath} {
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected %q to be removed, stat err=%v", path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinuxReplaceExecutablePreservesExecutableModeWhenTargetExists(t *testing.T) {
|
||||
_ = withLinuxTempHome(t)
|
||||
src := writeLinuxSourceExecutable(t)
|
||||
target := filepath.Join(t.TempDir(), "agent")
|
||||
|
||||
if err := os.WriteFile(target, []byte("old"), 0644); err != nil {
|
||||
t.Fatalf("write existing target: %v", err)
|
||||
}
|
||||
if err := replaceExecutable(src, target); err != nil {
|
||||
t.Fatalf("replaceExecutable() error: %v", err)
|
||||
}
|
||||
if info, err := os.Stat(target); err != nil {
|
||||
t.Fatalf("stat replaced executable: %v", err)
|
||||
} else if info.Mode().Perm() != 0755 {
|
||||
t.Fatalf("replaced executable mode = %o, want 0755", info.Mode().Perm())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user