PrintToVeraeTimeStamp/internal/netprint/service_darwin.go
Cursor Agent 2f7f7aa873
Keep the network printer off the driver helper port and install folder.
Use 8632 when 631 is taken, install standalone Windows and macOS builds beside the driver instead of over it, open the LAN firewall ports, and give silent NSIS dialogs a default plus an install log.

Co-authored-by: George Lambert <marchon@gmail.com>
2026-10-04 19:51:03 +00:00

262 lines
7.8 KiB
Go

//go:build darwin
package netprint
import (
"fmt"
"log"
"os"
"os/exec"
"os/user"
"path/filepath"
"strconv"
"strings"
)
const (
daemonPlist = "/Library/LaunchDaemons/com.verae.netprint.plist"
agentPlist = "/Library/LaunchAgents/com.verae.netprint.tray.plist"
)
func plistDaemon(exe, config string) string {
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>
<string>%s</string>
<string>netprint</string>
<string>--config</string>
<string>%s</string>
</array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
</dict></plist>
`, launchdLabel, exe, config)
}
func plistAgent(exe string) string {
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>
<string>%s</string>
<string>tray</string>
</array>
<key>RunAtLoad</key><true/>
<key>LimitLoadToSessionType</key><string>Aqua</string>
</dict></plist>
`, trayLabel, exe)
}
type osControl struct{}
func (osControl) Start() error { return launchctl("bootstrap", "system", daemonPlist) }
func (osControl) Stop() error {
return delayed("launchctl", "bootout", "system/"+launchdLabel)
}
func (osControl) Restart() error {
return delayed("launchctl", "kickstart", "-k", "system/"+launchdLabel)
}
func (osControl) LaunchAtLogin() (bool, error) {
out, _ := exec.Command("launchctl", "print", "system/"+launchdLabel).CombinedOutput()
return strings.Contains(string(out), "state = running") || strings.Contains(string(out), "state = waiting"), nil
}
func (osControl) SetLaunchAtLogin(on bool) error {
if on {
_ = exec.Command("launchctl", "enable", "system/"+launchdLabel).Run()
return launchctl("bootstrap", "system", daemonPlist)
}
_ = exec.Command("launchctl", "disable", "system/"+launchdLabel).Run()
return exec.Command("launchctl", "bootout", "system/"+launchdLabel).Run()
}
func (osControl) TrayAtLogin() (bool, error) {
_, err := os.Stat(agentPlist)
if err != nil {
return false, nil
}
out, _ := exec.Command("launchctl", "print-disabled", "system").CombinedOutput()
return !strings.Contains(string(out), `"`+trayLabel+`" => disabled`), nil
}
func (osControl) SetTrayAtLogin(on bool) error {
uid := consoleUID()
target := "gui/" + uid
if on {
if err := os.WriteFile(agentPlist, []byte(plistAgent(mustExe())), 0o644); err != nil {
return err
}
_ = exec.Command("launchctl", "enable", target+"/"+trayLabel).Run()
return exec.Command("launchctl", "bootstrap", target, agentPlist).Run()
}
_ = exec.Command("launchctl", "bootout", target+"/"+trayLabel).Run()
return exec.Command("launchctl", "disable", target+"/"+trayLabel).Run()
}
func delayed(args ...string) error {
cmd := exec.Command("sh", append([]string{"-c", `sleep 0.4; exec "$@"`, "sh"}, args...)...)
return cmd.Start()
}
func launchctl(args ...string) error {
out, err := exec.Command("launchctl", args...).CombinedOutput()
if err != nil {
return fmt.Errorf("launchctl: %w (%s)", err, strings.TrimSpace(string(out)))
}
return nil
}
func mustExe() string {
p, err := Executable()
if err != nil {
if _, statErr := os.Stat("/Library/Application Support/Verae/PrintStamp/bin/verae-print"); statErr == nil {
return "/Library/Application Support/Verae/PrintStamp/bin/verae-print"
}
return "/Library/Application Support/Verae/NetPrint/bin/verae-print"
}
return p
}
func consoleUID() string {
if u := os.Getenv("SUDO_UID"); u != "" {
return u
}
out, err := exec.Command("stat", "-f", "%u", "/dev/console").Output()
if err == nil {
if s := strings.TrimSpace(string(out)); s != "" && s != "0" {
return s
}
}
if cu, err := user.Current(); err == nil && cu.Uid != "" {
return cu.Uid
}
return "501"
}
// Install registers the LaunchDaemon and the menu-bar agent.
func Install(owner, tokenFile string) error {
if _, err := addOwner(owner); err != nil {
return err
}
exe, err := Executable()
if err != nil {
return err
}
cfgPath := SystemConfigPath()
if err := os.MkdirAll(filepath.Dir(cfgPath), 0o755); err != nil {
return err
}
if _, err := os.Stat(cfgPath); os.IsNotExist(err) {
if err := os.WriteFile(cfgPath, ExampleJSON("/Library/Application Support/Verae/NetPrint"), 0o644); err != nil {
return err
}
}
chosen := selectServiceBinary(exe, currentServiceExe())
if err := os.MkdirAll("/Library/LaunchDaemons", 0o755); err != nil {
return err
}
if err := os.WriteFile(daemonPlist, []byte(plistDaemon(chosen, cfgPath)), 0o644); err != nil {
return err
}
if err := os.MkdirAll("/Library/LaunchAgents", 0o755); err != nil {
return err
}
if err := os.WriteFile(agentPlist, []byte(plistAgent(chosen)), 0o644); err != nil {
return err
}
linkCLI(exe)
copyToken(tokenFile)
allowAppFirewall(chosen)
_ = exec.Command("launchctl", "bootout", "system/"+launchdLabel).Run()
if err := launchctl("bootstrap", "system", daemonPlist); err != nil {
return err
}
uid := consoleUID()
_ = exec.Command("launchctl", "bootstrap", "gui/"+uid, agentPlist).Run()
return nil
}
// Uninstall removes the daemon and menu item only when no owner remains.
func Uninstall(owner string) error {
o, err := removeOwner(owner)
if err != nil {
return err
}
if !o.empty() {
if other := peerBinary(owner); other != "" {
cfgPath := SystemConfigPath()
_ = os.WriteFile(daemonPlist, []byte(plistDaemon(other, cfgPath)), 0o644)
_ = os.WriteFile(agentPlist, []byte(plistAgent(other)), 0o644)
_ = exec.Command("launchctl", "bootout", "system/"+launchdLabel).Run()
_ = launchctl("bootstrap", "system", daemonPlist)
}
return nil
}
_ = exec.Command("launchctl", "bootout", "system/"+launchdLabel).Run()
if uid := consoleUID(); uid != "" {
_ = exec.Command("launchctl", "bootout", "gui/"+uid+"/"+trayLabel).Run()
}
_ = os.Remove(daemonPlist)
_ = os.Remove(agentPlist)
return nil
}
func currentServiceExe() string {
b, err := os.ReadFile(daemonPlist)
if err != nil {
return ""
}
return parseDarwinProgram(string(b))
}
func peerBinary(owner string) string {
p := "/Library/Application Support/Verae/NetPrint/bin/verae-print"
if owner == "standalone" {
p = "/Library/Application Support/Verae/PrintStamp/bin/verae-print"
}
if _, err := os.Stat(p); err == nil {
return p
}
return ""
}
// linkCLI keeps /usr/local/bin/verae-print on the driver binary when that
// install is present. A real file at the link path is left alone.
func linkCLI(exe string) {
const link = "/usr/local/bin/verae-print"
const driver = "/Library/Application Support/Verae/PrintStamp/bin/verae-print"
target := exe
if _, err := os.Stat(driver); err == nil {
target = driver
}
_ = os.MkdirAll("/usr/local/bin", 0o755)
st, err := os.Lstat(link)
if err == nil && st.Mode()&os.ModeSymlink == 0 {
return
}
if err == nil {
_ = os.Remove(link)
}
_ = os.Symlink(target, link)
}
// allowAppFirewall asks the macOS application firewall to accept the service
// binary. System Settings → Network → Firewall still applies if this tool
// is unavailable or the user has blocked the app.
func allowAppFirewall(exe string) {
const fw = "/usr/libexec/ApplicationFirewall/socketfilterfw"
if _, err := os.Stat(fw); err != nil {
return
}
if out, err := exec.Command(fw, "--add", exe).CombinedOutput(); err != nil {
log.Printf("netprint: socketfilterfw --add: %v (%s)", err, strings.TrimSpace(string(out)))
}
if out, err := exec.Command(fw, "--unblockapp", exe).CombinedOutput(); err != nil {
log.Printf("netprint: socketfilterfw --unblockapp: %v (%s)", err, strings.TrimSpace(string(out)))
}
}
// silence unused in case strconv is needed by future uid parsing
var _ = strconv.Itoa