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>
262 lines
7.8 KiB
Go
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
|