Windows DnsServiceRegister now registers the Bonjour service-type form and the RFC subtype name so a Mac browse of _ipp._tcp,_universal can see the printer. document-format-supported matches the TXT pdl list, including JPEG. Job ids persist in next-job and job-N directories. The service log is written and flushed even when stderr fails. The tray is started by Task Scheduler or Explorer so setup is not its parent. Co-authored-by: George Lambert <marchon@gmail.com>
138 lines
4.5 KiB
Go
138 lines
4.5 KiB
Go
package netprint
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// dnsSDInstance is one DNS-SD registration (primary service or a subtype).
|
|
type dnsSDInstance struct {
|
|
InstanceName string
|
|
Host string
|
|
Port int
|
|
IPv4 []net.IP
|
|
TXT []string
|
|
}
|
|
|
|
// dnsSDClient registers a service with the operating system's DNS-SD API.
|
|
// On Windows 10 and later this is DnsServiceRegister in dnsapi.dll.
|
|
type dnsSDClient interface {
|
|
Register(dnsSDInstance) error
|
|
Deregister(dnsSDInstance) error
|
|
}
|
|
|
|
func serviceToDNS(s Service) []dnsSDInstance {
|
|
host := sanitizeLabel(s.Host) + ".local"
|
|
base := sanitizeLabel(s.Instance)
|
|
// The first name is the primary service. A Mac `dns-sd -B _ipp._tcp` already
|
|
// resolves it. AirPrint and `dns-sd -B _ipp._tcp,_universal` query the PTR
|
|
// `_universal._sub._ipp._tcp.local`, which has to point at that same instance.
|
|
// DnsServiceRegister has no subtype field. Windows accepts the subtype on the
|
|
// service type, Bonjour-style ("<instance>._ipp._tcp,_universal.local"), and
|
|
// some builds only honor the RFC 6763 instance name
|
|
// ("<instance>._universal._sub._ipp._tcp.local"). Register both. A failure of
|
|
// either spelling is logged and does not remove the primary advertisement.
|
|
names := []string{
|
|
base + "._ipp._tcp.local",
|
|
base + "._ipp._tcp,_universal.local",
|
|
base + "._ipp._tcp,_print.local",
|
|
base + "._universal._sub._ipp._tcp.local",
|
|
base + "._print._sub._ipp._tcp.local",
|
|
}
|
|
out := make([]dnsSDInstance, 0, len(names))
|
|
for _, name := range names {
|
|
out = append(out, dnsSDInstance{
|
|
InstanceName: name,
|
|
Host: host,
|
|
Port: s.Port,
|
|
IPv4: append([]net.IP(nil), s.IPs...),
|
|
TXT: append([]string(nil), s.TXT...),
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// advertiseWithDNS publishes the printer through DnsServiceRegister (or a test double)
|
|
// and blocks until ctx is canceled. The primary _ipp._tcp name must succeed.
|
|
// Subtype failures are logged and do not hide the primary advertisement.
|
|
func advertiseWithDNS(ctx context.Context, current func() Service, api dnsSDClient, sink mdnsSink) error {
|
|
if api == nil {
|
|
return fmt.Errorf("DnsServiceRegister client is nil")
|
|
}
|
|
insts := serviceToDNS(current())
|
|
if len(insts) == 0 {
|
|
return fmt.Errorf("DnsServiceRegister: empty service")
|
|
}
|
|
var registered []dnsSDInstance
|
|
for i, inst := range insts {
|
|
if err := api.Register(inst); err != nil {
|
|
if i == 0 {
|
|
return fmt.Errorf("DnsServiceRegister %s: %w", inst.InstanceName, err)
|
|
}
|
|
log.Printf("netprint: mDNS subtype %s not registered: %v", inst.InstanceName, err)
|
|
continue
|
|
}
|
|
registered = append(registered, inst)
|
|
log.Printf("netprint: mDNS advertised via DnsServiceRegister %s port %d", inst.InstanceName, inst.Port)
|
|
}
|
|
if len(registered) == 0 {
|
|
return fmt.Errorf("DnsServiceRegister registered nothing")
|
|
}
|
|
if sink != nil {
|
|
var subtypes []string
|
|
for _, inst := range registered[1:] {
|
|
subtypes = append(subtypes, inst.InstanceName)
|
|
}
|
|
detail := fmt.Sprintf("DnsServiceRegister %s port %d", registered[0].InstanceName, registered[0].Port)
|
|
report := mdnsReport{
|
|
Advertising: true,
|
|
Mode: "dns-sd",
|
|
Detail: detail,
|
|
LastSent: time.Now(),
|
|
}
|
|
if len(subtypes) > 0 {
|
|
report.Detail = detail + "; subtypes " + strings.Join(subtypes, ", ")
|
|
}
|
|
if len(registered) < len(insts) {
|
|
report.Err = "AirPrint subtype registration incomplete"
|
|
}
|
|
sink(report)
|
|
}
|
|
<-ctx.Done()
|
|
for _, inst := range registered {
|
|
if err := api.Deregister(inst); err != nil {
|
|
log.Printf("netprint: mDNS DnsServiceDeRegister %s: %v", inst.InstanceName, err)
|
|
}
|
|
}
|
|
log.Printf("netprint: mDNS DnsServiceRegister removed")
|
|
return nil
|
|
}
|
|
|
|
// advertiseForOS is the discovery strategy for goos.
|
|
// Windows tries DnsServiceRegister first so the advertisement is sent by Dnscache,
|
|
// which already owns UDP 5353. A shared UDP socket is the fallback.
|
|
func advertiseForOS(goos string, ctx context.Context, current func() Service, api dnsSDClient, sink mdnsSink) error {
|
|
if goos == "windows" {
|
|
err := advertiseWithDNS(ctx, current, api, sink)
|
|
if err == nil || ctx.Err() != nil {
|
|
return nil
|
|
}
|
|
log.Printf("netprint: DnsServiceRegister failed: %v; trying a shared UDP 5353 socket", err)
|
|
if sink != nil {
|
|
sink(mdnsReport{
|
|
Mode: "dns-sd",
|
|
Err: err.Error(),
|
|
Detail: "DnsServiceRegister failed; trying shared UDP 5353",
|
|
})
|
|
}
|
|
if udpErr := advertiseUDP(ctx, current, sink); udpErr != nil {
|
|
return fmt.Errorf("DnsServiceRegister: %v; shared UDP 5353: %w", err, udpErr)
|
|
}
|
|
return nil
|
|
}
|
|
return advertiseUDP(ctx, current, sink)
|
|
}
|