151 lines
3.6 KiB
Go
151 lines
3.6 KiB
Go
//go:build darwin
|
|
|
|
package platform
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// launchAgentLabel is a stable internal identifier. The product name shown to
|
|
// users comes from the application metadata, not this LaunchAgent label.
|
|
const launchAgentLabel = "net.commilitia.cdrop"
|
|
|
|
func launchAgentPath() (string, error) {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(home, "Library", "LaunchAgents", launchAgentLabel+".plist"), nil
|
|
}
|
|
|
|
// launchTarget returns the program the LaunchAgent should run. For a packaged
|
|
// .app it launches the bundle via `open` (proper LaunchServices activation);
|
|
// in a bare dev binary it runs the executable directly. Either way it appends
|
|
// --hidden so a login launch comes up to the menu bar with no window (see
|
|
// launchedHidden in main); `open --args` forwards the rest as the app's argv.
|
|
func launchTarget() ([]string, error) {
|
|
exe, err := os.Executable()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resolved, err := filepath.EvalSymlinks(exe); err == nil {
|
|
exe = resolved
|
|
}
|
|
if bundle := appBundlePath(exe); bundle != "" {
|
|
return []string{"/usr/bin/open", bundle, "--args", "--hidden"}, nil
|
|
}
|
|
return []string{exe, "--hidden"}, nil
|
|
}
|
|
|
|
// appBundlePath returns the enclosing .app path if exe lives inside one
|
|
// (…/Foo.app/Contents/MacOS/binary), else "".
|
|
func appBundlePath(exe string) string {
|
|
const marker = ".app/Contents/MacOS/"
|
|
if i := strings.LastIndex(exe, marker); i != -1 {
|
|
return exe[:i+len(".app")]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// SetLaunchAtLogin installs or removes the LaunchAgent plist. Removing a missing
|
|
// agent is a no-op success.
|
|
func SetLaunchAtLogin(enabled bool) error {
|
|
p, err := launchAgentPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !enabled {
|
|
if err := os.Remove(p); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
args, err := launchTarget()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
return err
|
|
}
|
|
if err := writeLaunchAgentAtomically(
|
|
p,
|
|
[]byte(buildLaunchAgentPlist(launchAgentLabel, args)),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeLaunchAgentAtomically(path string, contents []byte) error {
|
|
dir := filepath.Dir(path)
|
|
f, err := os.CreateTemp(dir, "."+filepath.Base(path)+".tmp-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tempPath := f.Name()
|
|
defer os.Remove(tempPath)
|
|
|
|
if err := f.Chmod(0o644); err != nil {
|
|
f.Close()
|
|
return err
|
|
}
|
|
if _, err := f.Write(contents); err != nil {
|
|
f.Close()
|
|
return err
|
|
}
|
|
if err := f.Sync(); err != nil {
|
|
f.Close()
|
|
return err
|
|
}
|
|
if err := f.Close(); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(tempPath, path)
|
|
}
|
|
|
|
// IsLaunchAtLoginEnabled reports whether the LaunchAgent plist is present.
|
|
func IsLaunchAtLoginEnabled() bool {
|
|
p, err := launchAgentPath()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
_, err = os.Stat(p)
|
|
return err == nil
|
|
}
|
|
|
|
func buildLaunchAgentPlist(label string, programArgs []string) string {
|
|
var args strings.Builder
|
|
for _, a := range programArgs {
|
|
fmt.Fprintf(&args, "\n <string>%s</string>", xmlEscape(a))
|
|
}
|
|
return fmt.Sprintf(`<?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>%s
|
|
</array>
|
|
<key>RunAtLoad</key>
|
|
<true/>
|
|
</dict>
|
|
</plist>
|
|
`, xmlEscape(label), args.String())
|
|
}
|
|
|
|
func xmlEscape(s string) string {
|
|
r := strings.NewReplacer(
|
|
"&", "&",
|
|
"<", "<",
|
|
">", ">",
|
|
`"`, """,
|
|
"'", "'",
|
|
)
|
|
return r.Replace(s)
|
|
}
|