PrintToVeraeTimeStamp/internal/netprint/service_darwin.go
Cursor Agent 5ed87da68f
Some checks failed
installers / netprint (push) Has been cancelled
installers / macos-pkg (push) Has been cancelled
installers / publish (push) Has been cancelled
Keep the macOS receipts SMB share across reboots.
Save the installer choice as shareReceiptsSMB, recreate the share when it is missing, and show the live smb:// address on the receipts page.

Co-authored-by: George Lambert <marchon@gmail.com>
2026-10-05 16:34:51 +00:00

281 lines
8.4 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"
trayAppExecutable = "/Applications/Verae NetPrint.app/Contents/MacOS/verae-print"
)
// trayProgram is the menu-bar executable inside Verae NetPrint.app.
// The LaunchDaemon keeps using the binary under Application Support.
func trayProgram() string {
if st, err := os.Stat(trayAppExecutable); err == nil && !st.IsDir() {
return trayAppExecutable
}
return mustExe()
}
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(trayProgram())), 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, opts InstallOptions) 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.MkdirAll(darwinSharedReceiptsDir, 0o755); err != nil {
return err
}
_ = os.Chmod(darwinSharedReceiptsDir, 0o755)
if _, err := os.Stat(cfgPath); os.IsNotExist(err) {
if err := os.WriteFile(cfgPath, ExampleJSON(darwinSharedReceiptsDir), 0o644); err != nil {
return err
}
}
noteInstallOptions(cfgPath, opts)
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(trayProgram())), 0o644); err != nil {
return err
}
linkCLI(exe)
copyToken(tokenFile)
allowAppFirewall(chosen)
if tray := trayProgram(); tray != chosen {
allowAppFirewall(tray)
}
_ = 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(trayProgram())), 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()
}
removeReceiptsShare()
_ = 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