PrintToVeraeTimeStamp/internal/netprint/service_windows.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

230 lines
6.4 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() {
removeFirewall()
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, "profile=private,domain").CombinedOutput()
if err != nil {
log.Printf("netprint: firewall %s: %v (%s)", r.name, err, strings.TrimSpace(string(out)))
}
}
}
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) 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)
ensureFirewall()
if err := sc("start", winService); err != nil {
return err
}
return setTrayFor(chosen, true)
}
// 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()
return nil
}