PrintToVeraeTimeStamp/internal/netprint/service_windows.go
Cursor Agent 17bb067010
Keep one IPP advertisement and accept link-local IPv6.
DnsServiceRegister stays the only owner of the _ipp._tcp instance. The shared UDP responder answers subtype PTRs only, so mDNS no longer renames a second copy to Name(1). The IPP port listens on IPv4 and on [::] with IPV6_V6ONLY, and the firewall rules allow fe80::/10, so a Mac connect to the host's link-local address completes.

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

255 lines
7.3 KiB
Go

//go:build windows
package netprint
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
)
const runKey = `HKLM\Software\Microsoft\Windows\CurrentVersion\Run`
const ownerKey = `HKLM\Software\Verae\NetPrint`
type osControl struct{}
func (osControl) Start() error { return sc("start", winService) }
func (osControl) Stop() error {
return delayed("cmd.exe", "/c", "ping -n 2 127.0.0.1 >NUL & sc stop "+winService)
}
func (osControl) Restart() error {
return delayed("cmd.exe", "/c", "sc stop "+winService+" & ping -n 2 127.0.0.1 >NUL & sc start "+winService)
}
func (osControl) LaunchAtLogin() (bool, error) {
out, err := exec.Command("sc.exe", "qc", winService).CombinedOutput()
if err != nil {
return false, nil
}
return strings.Contains(strings.ToUpper(string(out)), "AUTO_START"), nil
}
func (osControl) SetLaunchAtLogin(on bool) error {
mode := "demand"
if on {
mode = "auto"
}
return sc("config", winService, "start=", mode)
}
func (osControl) TrayAtLogin() (bool, error) {
out, err := exec.Command("reg.exe", "query", runKey, "/v", "VeraeNetPrintTray").CombinedOutput()
return err == nil && strings.Contains(string(out), "VeraeNetPrintTray"), nil
}
func (osControl) SetTrayAtLogin(on bool) error {
exe, _ := Executable()
return setTrayFor(exe, on)
}
func sc(args ...string) error {
out, err := exec.Command("sc.exe", args...).CombinedOutput()
if err != nil {
return fmt.Errorf("sc %s: %w (%s)", strings.Join(args, " "), err, strings.TrimSpace(string(out)))
}
return nil
}
func delayed(args ...string) error {
cmd := exec.Command(args[0], args[1:]...)
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: 0x00000008} // DETACHED_PROCESS
return cmd.Start()
}
func trayNextTo(serviceExe string) string {
serviceExe = strings.Trim(serviceExe, `"`)
if serviceExe == "" {
return `verae-print.exe tray`
}
sibling := filepath.Join(filepath.Dir(serviceExe), "verae-netprint-tray.exe")
if _, err := os.Stat(sibling); err == nil {
return `"` + sibling + `"`
}
return `"` + serviceExe + `" tray`
}
func setTrayFor(serviceExe string, on bool) error {
if !on {
return exec.Command("reg.exe", "delete", runKey, "/v", "VeraeNetPrintTray", "/f").Run()
}
return exec.Command("reg.exe", "add", runKey, "/v", "VeraeNetPrintTray", "/t", "REG_SZ", "/d", trayNextTo(serviceExe), "/f").Run()
}
func serviceBinPath(exe string) string {
return fmt.Sprintf(`"%s" netprint`, exe)
}
func currentServiceExe() string {
out, err := exec.Command("sc.exe", "qc", winService).CombinedOutput()
if err != nil {
return ""
}
return parseWindowsBinPath(string(out))
}
func peerBinary(owner string) string {
rel := filepath.Join("Verae", "NetPrint", "bin", "verae-print.exe")
if owner == "standalone" {
rel = filepath.Join("Verae", "PrintStamp", "bin", "verae-print.exe")
}
var roots []string
if p := os.Getenv("ProgramW6432"); p != "" {
roots = append(roots, p)
}
if p := os.Getenv("ProgramFiles"); p != "" {
roots = append(roots, p)
}
roots = append(roots, `C:\Program Files`)
seen := map[string]bool{}
for _, root := range roots {
if seen[root] {
continue
}
seen[root] = true
p := filepath.Join(root, rel)
if _, err := os.Stat(p); err == nil {
return p
}
}
return ""
}
func ensureFirewall(public bool) {
removeFirewall()
profile := firewallProfile(public)
rules := []struct{ name, proto, port string }{
{"Verae NetPrint IPP", "TCP", "631"},
{"Verae NetPrint IPP Fallback", "TCP", "8632"},
{"Verae NetPrint mDNS", "UDP", "5353"},
}
for _, r := range rules {
out, err := exec.Command("netsh", "advfirewall", "firewall", "add", "rule",
"name="+r.name, "dir=in", "action=allow", "protocol="+r.proto,
"localport="+r.port, "remoteip="+firewallRemoteIP, "profile="+profile).CombinedOutput()
if err != nil {
log.Printf("netprint: firewall %s profile %s: %v (%s)", r.name, profile, err, strings.TrimSpace(string(out)))
} else {
log.Printf("netprint: firewall %s profile %s", r.name, profile)
}
}
}
func applyFirewall(public bool) { ensureFirewall(public) }
func removeFirewall() {
for _, name := range []string{"Verae NetPrint IPP", "Verae NetPrint IPP Fallback", "Verae NetPrint mDNS"} {
_ = exec.Command("netsh", "advfirewall", "firewall", "delete", "rule", "name="+name).Run()
}
}
func setOwnerReg(name string, on bool) {
v := "0"
if on {
v = "1"
}
_ = exec.Command("reg.exe", "add", ownerKey, "/v", name, "/t", "REG_DWORD", "/d", v, "/f").Run()
}
func ownerReg(name string) bool {
out, err := exec.Command("reg.exe", "query", ownerKey, "/v", name).CombinedOutput()
if err != nil {
return false
}
return strings.Contains(string(out), "0x1")
}
// Install registers the VeraeNetPrint service and the tray Run key.
func Install(owner, tokenFile string, opts InstallOptions) error {
if _, err := addOwner(owner); err != nil {
return err
}
if owner == "driver" {
setOwnerReg("Driver", true)
} else {
setOwnerReg("Standalone", true)
}
exe, err := Executable()
if err != nil {
return err
}
cfgPath := SystemConfigPath()
_ = os.MkdirAll(filepath.Dir(cfgPath), 0o755)
if _, err := os.Stat(cfgPath); os.IsNotExist(err) {
if err := os.WriteFile(cfgPath, ExampleJSON(defaultDataDir()), 0o644); err != nil {
return err
}
}
current := currentServiceExe()
chosen := selectServiceBinary(exe, current)
bin := serviceBinPath(chosen)
if current == "" {
if err := sc("create", winService, "binPath=", bin, "start=", "auto", "DisplayName=", "Verae Network Printer"); err != nil {
return err
}
} else if chosen != current {
_ = exec.Command("sc.exe", "stop", winService).Run()
if err := sc("config", winService, "binPath=", bin); err != nil {
return err
}
}
_ = exec.Command("sc.exe", "description", winService, "Verae network timestamp printer").Run()
_ = exec.Command("sc.exe", "config", winService, "start=", "auto").Run()
copyToken(tokenFile)
noteInstallOptions(cfgPath, opts)
public := false
if opts.AllowPublic != nil {
public = *opts.AllowPublic
} else if c, err := Load(cfgPath, Config{}); err == nil {
public = c.PublicNetworks()
}
ensureFirewall(public)
startErr := sc("start", winService)
if opts.LocalQueue && startErr == nil {
port := waitBoundPort(DefaultControlPort)
if err := addLocalQueue(port); err != nil {
log.Printf("netprint: local printer queue: %v", err)
}
}
if err := setTrayFor(chosen, true); err != nil {
log.Printf("netprint: tray autostart: %v", err)
}
if err := LaunchTrayNow(); err != nil {
log.Printf("netprint: tray launch: %v", err)
}
return startErr
}
// Uninstall removes the service only when neither installer still owns it.
func Uninstall(owner string) error {
o, err := removeOwner(owner)
if err != nil {
return err
}
if owner == "driver" {
setOwnerReg("Driver", false)
} else {
setOwnerReg("Standalone", false)
}
if !o.empty() || ownerReg("Driver") || ownerReg("Standalone") {
if other := peerBinary(owner); other != "" {
_ = exec.Command("sc.exe", "stop", winService).Run()
_ = sc("config", winService, "binPath=", serviceBinPath(other))
_ = sc("start", winService)
_ = setTrayFor(other, true)
}
return nil
}
_ = exec.Command("sc.exe", "stop", winService).Run()
_ = exec.Command("sc.exe", "delete", winService).Run()
_ = setTrayFor("", false)
removeFirewall()
if err := removeLocalQueue(); err != nil {
log.Printf("netprint: remove local queue: %v", err)
}
return nil
}