PrintToVeraeTimeStamp/internal/netprint/dnsservice_darwin.go
Cursor Agent f96ade41bd
Ship the Mac menu bar as an app bundle and use one real stamp path.
The tray and AirDrop helper now live in Verae NetPrint.app so notifications can start, /api/stamp uses the same client as Print-Job and refuses mock receipts, and the default printer name includes the hostname with an mDNS rename when that name is taken.

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

216 lines
4.6 KiB
Go

//go:build darwin
package netprint
import (
"fmt"
"log"
"runtime"
"sync"
"unsafe"
"github.com/ebitengine/purego"
)
// darwinDNS registers with mDNSResponder through DNSServiceRegister.
// Subtypes are part of the regtype. The call does not use cgo; libSystem is
// loaded when the process runs on a Mac. A name conflict is reported on
// conflicts so the advertiser can register "Name (2)" instead.
type darwinDNS struct {
mu sync.Mutex
refs []uintptr
stop []func()
conflicts chan struct{}
}
func newDarwinDNS() dnsSDClient {
return &darwinDNS{conflicts: make(chan struct{}, 1)}
}
func (d *darwinDNS) Conflicts() <-chan struct{} { return d.conflicts }
// regState is the DNSServiceRegister context. It stays pinned until the
// registration is deallocated so the callback can read it.
type regState struct {
name string
conflicts chan struct{}
}
var (
dnsOnce sync.Once
dnsErr error
dnsRegister func(sdRef *uintptr, flags uint32, ifIndex uint32, name, regtype, domain, host *byte, port uint16, txtLen uint16, txtRecord unsafe.Pointer, callback uintptr, context unsafe.Pointer) int32
dnsProcess func(sdRef uintptr) int32
dnsDeallocate func(sdRef uintptr)
dnsReplyOnce sync.Once
dnsReplyPtr uintptr
regMu sync.Mutex
regStates = map[uintptr]*regState{}
)
func loadDNSService() error {
dnsOnce.Do(func() {
lib, err := purego.Dlopen("/usr/lib/libSystem.B.dylib", 1)
if err != nil {
dnsErr = err
return
}
purego.RegisterLibFunc(&dnsRegister, lib, "DNSServiceRegister")
purego.RegisterLibFunc(&dnsProcess, lib, "DNSServiceProcessResult")
purego.RegisterLibFunc(&dnsDeallocate, lib, "DNSServiceRefDeallocate")
})
return dnsErr
}
func (d *darwinDNS) Register(inst dnsSDInstance) error {
if err := loadDNSService(); err != nil {
return fmt.Errorf("DNSServiceRegister: %w", err)
}
name := cString(inst.Name)
regtype := cString(inst.RegType)
domain := cString("local")
var host *byte
hostBuf := []byte{}
if inst.Host != "" {
hostBuf = append([]byte(inst.Host), 0)
host = &hostBuf[0]
}
txt := encodeDNSTXT(inst.TXT)
var txtPtr unsafe.Pointer
if len(txt) > 0 {
txtPtr = unsafe.Pointer(&txt[0])
}
st := &regState{name: inst.Name, conflicts: d.conflicts}
var pin runtime.Pinner
pin.Pin(st)
key := uintptr(unsafe.Pointer(st))
regMu.Lock()
regStates[key] = st
regMu.Unlock()
flags := uint32(0)
var cb uintptr
var ctx unsafe.Pointer
keep := false
if reply := dnsReplyCallback(); reply != 0 {
flags = dnsFlagNoAutoRename
cb = reply
ctx = unsafe.Pointer(st)
keep = true
}
release := func() {
regMu.Lock()
delete(regStates, key)
regMu.Unlock()
pin.Unpin()
}
if !keep {
release()
}
var ref uintptr
rc := dnsRegister(&ref, flags, 0, name, regtype, domain, host, dnsPort(inst.Port), uint16(len(txt)), txtPtr, cb, ctx)
runtime.KeepAlive(name)
runtime.KeepAlive(regtype)
runtime.KeepAlive(domain)
runtime.KeepAlive(hostBuf)
runtime.KeepAlive(txt)
runtime.KeepAlive(st)
if rc != 0 {
if keep {
release()
}
if rc == dnsErrNameConflict {
return errNameConflict
}
return fmt.Errorf("DNSServiceRegister %s %s: code %d", inst.Name, inst.RegType, rc)
}
done := make(chan struct{})
go func() {
defer close(done)
for {
if err := dnsProcess(ref); err != 0 {
return
}
}
}()
d.mu.Lock()
d.refs = append(d.refs, ref)
d.stop = append(d.stop, func() {
dnsDeallocate(ref)
<-done
if keep {
release()
}
})
d.mu.Unlock()
log.Printf("netprint: DNSServiceRegister %s %s port %d", inst.Name, inst.RegType, inst.Port)
return nil
}
func dnsReplyCallback() uintptr {
dnsReplyOnce.Do(func() {
defer func() {
if recover() != nil {
dnsReplyPtr = 0
}
}()
dnsReplyPtr = purego.NewCallback(dnsRegisterReply)
})
return dnsReplyPtr
}
func dnsRegisterReply(sdRef, flags, errCode, namePtr, regtype, domain, context uintptr) uintptr {
if context == 0 {
return 0
}
regMu.Lock()
st := regStates[context]
regMu.Unlock()
if st == nil || st.conflicts == nil || int32(uint32(errCode)) != dnsErrNameConflict {
return 0
}
select {
case st.conflicts <- struct{}{}:
default:
}
runtime.KeepAlive(namePtr)
return 0
}
func (d *darwinDNS) Deregister(inst dnsSDInstance) error {
d.mu.Lock()
if len(d.stop) == 0 {
d.mu.Unlock()
return nil
}
stop := d.stop[0]
d.stop = d.stop[1:]
if len(d.refs) > 0 {
d.refs = d.refs[1:]
}
d.mu.Unlock()
stop()
return nil
}
func cString(s string) *byte {
b := append([]byte(s), 0)
return &b[0]
}
func encodeDNSTXT(txt []string) []byte {
var b []byte
for _, s := range txt {
if s == "" {
continue
}
if len(s) > 255 {
s = s[:255]
}
b = append(b, byte(len(s)))
b = append(b, s...)
}
return b
}