mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-03-13 10:26:06 +00:00
135 lines
4.5 KiB
Go
135 lines
4.5 KiB
Go
//go:build darwin
|
|
|
|
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
)
|
|
|
|
const launchdPlistPath = "/Library/LaunchDaemons/ctrld.plist"
|
|
|
|
// serviceConfigFileExists returns true if the launchd plist for ctrld exists on disk.
|
|
// This is more reliable than checking launchctl status, which may report "not found"
|
|
// if the service was unloaded but the plist file still exists.
|
|
func serviceConfigFileExists() bool {
|
|
_, err := os.Stat(launchdPlistPath)
|
|
return err == nil
|
|
}
|
|
|
|
// appendServiceFlag appends a CLI flag (e.g., "--intercept-mode") to the installed
|
|
// service's launch arguments. This is used when upgrading an existing installation
|
|
// to intercept mode without losing the existing --cd flag and other arguments.
|
|
//
|
|
// On macOS, this modifies the launchd plist at /Library/LaunchDaemons/ctrld.plist
|
|
// using the "defaults" command, which is the standard way to edit plists.
|
|
//
|
|
// The function is idempotent: if the flag already exists, it's a no-op.
|
|
func appendServiceFlag(flag string) error {
|
|
// Read current ProgramArguments from plist.
|
|
out, err := exec.Command("defaults", "read", launchdPlistPath, "ProgramArguments").CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read plist ProgramArguments: %w (output: %s)", err, strings.TrimSpace(string(out)))
|
|
}
|
|
|
|
// Check if the flag is already present (idempotent).
|
|
args := string(out)
|
|
if strings.Contains(args, flag) {
|
|
mainLog.Load().Debug().Msgf("Service flag %q already present in plist, skipping", flag)
|
|
return nil
|
|
}
|
|
|
|
// Use PlistBuddy to append the flag to ProgramArguments array.
|
|
// PlistBuddy is more reliable than "defaults" for array manipulation.
|
|
addCmd := exec.Command(
|
|
"/usr/libexec/PlistBuddy",
|
|
"-c", fmt.Sprintf("Add :ProgramArguments: string %s", flag),
|
|
launchdPlistPath,
|
|
)
|
|
if out, err := addCmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to append %q to plist ProgramArguments: %w (output: %s)", flag, err, strings.TrimSpace(string(out)))
|
|
}
|
|
|
|
mainLog.Load().Info().Msgf("Appended %q to service launch arguments", flag)
|
|
return nil
|
|
}
|
|
|
|
// verifyServiceRegistration is a no-op on macOS (launchd plist verification not needed).
|
|
func verifyServiceRegistration() error {
|
|
return nil
|
|
}
|
|
|
|
// removeServiceFlag removes a CLI flag (and its value, if the next argument is not
|
|
// a flag) from the installed service's launch arguments. For example, removing
|
|
// "--intercept-mode" also removes the following "dns" or "hard" value argument.
|
|
//
|
|
// The function is idempotent: if the flag doesn't exist, it's a no-op.
|
|
func removeServiceFlag(flag string) error {
|
|
// Read current ProgramArguments to find the index.
|
|
out, err := exec.Command("/usr/libexec/PlistBuddy", "-c", "Print :ProgramArguments", launchdPlistPath).CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read plist ProgramArguments: %w (output: %s)", err, strings.TrimSpace(string(out)))
|
|
}
|
|
|
|
// Parse the PlistBuddy output to find the flag's index.
|
|
// PlistBuddy prints arrays as:
|
|
// Array {
|
|
// /path/to/ctrld
|
|
// run
|
|
// --cd=xxx
|
|
// --intercept-mode
|
|
// dns
|
|
// }
|
|
lines := strings.Split(string(out), "\n")
|
|
var entries []string
|
|
for _, line := range lines {
|
|
trimmed := strings.TrimSpace(line)
|
|
if trimmed == "Array {" || trimmed == "}" || trimmed == "" {
|
|
continue
|
|
}
|
|
entries = append(entries, trimmed)
|
|
}
|
|
|
|
index := -1
|
|
for i, entry := range entries {
|
|
if entry == flag {
|
|
index = i
|
|
break
|
|
}
|
|
}
|
|
|
|
if index < 0 {
|
|
mainLog.Load().Debug().Msgf("Service flag %q not present in plist, skipping removal", flag)
|
|
return nil
|
|
}
|
|
|
|
// Check if the next entry is a value (not a flag). If so, delete it first
|
|
// (deleting by index shifts subsequent entries down, so delete value before flag).
|
|
hasValue := index+1 < len(entries) && !strings.HasPrefix(entries[index+1], "-")
|
|
if hasValue {
|
|
delVal := exec.Command(
|
|
"/usr/libexec/PlistBuddy",
|
|
"-c", fmt.Sprintf("Delete :ProgramArguments:%d", index+1),
|
|
launchdPlistPath,
|
|
)
|
|
if out, err := delVal.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to remove value for %q from plist: %w (output: %s)", flag, err, strings.TrimSpace(string(out)))
|
|
}
|
|
}
|
|
|
|
// Delete the flag itself.
|
|
delCmd := exec.Command(
|
|
"/usr/libexec/PlistBuddy",
|
|
"-c", fmt.Sprintf("Delete :ProgramArguments:%d", index),
|
|
launchdPlistPath,
|
|
)
|
|
if out, err := delCmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to remove %q from plist ProgramArguments: %w (output: %s)", flag, err, strings.TrimSpace(string(out)))
|
|
}
|
|
|
|
mainLog.Load().Info().Msgf("Removed %q from service launch arguments", flag)
|
|
return nil
|
|
}
|