Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions cupshelpers/cupshelpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import cups, pprint, os, tempfile, re, string
import locale
import urllib.parse
from . import _debugprint
from . import config
from functools import reduce
Expand Down Expand Up @@ -535,9 +536,12 @@ def __init__(self, uri, **kw):

self.id_dict = parseDeviceID (self.id)

s = uri.find("serial=")
if s != -1 and not self.id_dict.get ('SN',''):
self.id_dict['SN'] = uri[s + 7:]
if not self.id_dict.get ('SN', ''):
serial = urllib.parse.parse_qs (
urllib.parse.urlsplit (uri).query,
keep_blank_values=True).get ('serial', [])
if len (serial) > 0:
self.id_dict['SN'] = serial[0]

def __repr__ (self):
return "<cupshelpers.Device \"%s\">" % self.uri
Expand Down
149 changes: 133 additions & 16 deletions dnssdresolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,128 @@
import urllib.parse
from debug import *


def _txt_value (txt, key):
prefix = key + "="
for value in txt:
value = _txt_text (value)
if value.startswith (prefix):
return value[len (prefix):]
return ''


def _txt_text (value):
if isinstance (value, str):
return value
if isinstance (value, bytes):
return value.decode ("utf-8", "replace")
if isinstance (value, dbus.ByteArray):
return bytes (value).decode ("utf-8", "replace")
if isinstance (value, dbus.Array):
try:
return bytes (value).decode ("utf-8", "replace")
except (TypeError, ValueError):
return ''.join (_txt_text (item) for item in value)
return str (value)


def _service_tuple_from_uri (uri):
parsed = urllib.parse.urlparse (uri)
if parsed.scheme == 'dnssd':
hostname = parsed.netloc
elif parsed.scheme in ('ipp', 'ipps') and \
parsed.netloc.find ("._ipp._tcp.local") != -1:
hostname = parsed.netloc
else:
return None

elements = hostname.rsplit (".", 3)
if len (elements) != 4:
return None

name, stype, protocol, domain = elements
name = urllib.parse.unquote (name)
stype += "." + protocol # e.g. _printer._tcp
return (name, stype, domain)


def needs_service_resolution (uri):
return _service_tuple_from_uri (uri) is not None


def _device_serial (device):
if hasattr (device, 'id_dict'):
return device.id_dict.get ('SN', '')
if hasattr (device, 'sn'):
return device.sn
return ''


def is_ipp_over_usb_device (device):
parsed = urllib.parse.urlparse (device.uri)
return (parsed.scheme in ('ipp', 'ipps') and
_service_tuple_from_uri (device.uri) is not None)


def _is_legacy_usb_device (device):
return (getattr (device, 'device_class', '') == 'direct' and
getattr (device, 'type', '') == 'usb')


def ipp_usb_serials (devices):
devices = list (devices)
serials = set ()
for device in devices:
if is_ipp_over_usb_device (device):
serial = _device_serial (device)
if serial != '':
serials.add (serial)
return serials


class LegacyUSBDeviceCache:
"""Track legacy USB device serials across sequential discovery batches."""

def __init__ (self):
self._usb_serials = set ()

def note_devices (self, devices):
for device in devices:
if _is_legacy_usb_device (device):
serial = _device_serial (device)
if serial != '':
self._usb_serials.add (serial)

def suppress(self, devices):
devices = list(devices)

ipp_serials = ipp_usb_serials(devices)

if not ipp_serials:
return devices

filtered = []
for device in devices:
if (_is_legacy_usb_device(device) and
_device_serial(device) in ipp_serials):
continue

filtered.append(device)

return filtered

def superseded_usb_serials (self, devices):
"""Return cached USB serials superseded by IPP-over-USB in devices."""
return self._usb_serials & ipp_usb_serials (devices)


def suppress_legacy_usb_devices (devices, cache=None):
devices = list (devices)
if cache is None:
cache = LegacyUSBDeviceCache ()
cache.note_devices (devices)
return cache.suppress (devices)

class DNSSDHostNamesResolver:
def __init__ (self, devices):
self._devices = devices
Expand All @@ -44,29 +166,19 @@ def resolve (self, reply_handler):
return

for uri, device in self._devices.items ():
if not uri.startswith ("dnssd://"):
service = _service_tuple_from_uri (uri)
if service is None:
self._unresolved -= 1
continue

# We need to resolve the DNS-SD hostname in order to
# compare with other network devices.
result = urllib.parse.urlparse (uri)
hostname = result.netloc
elements = hostname.rsplit (".", 3)
if len (elements) != 4:
self._resolved ()
continue

name, stype, protocol, domain = elements
name = urllib.parse.unquote (name)
stype += "." + protocol # e.g. _printer._tcp
name, stype, domain = service

try:
obj = bus.get_object ("org.freedesktop.Avahi", "/")
server = dbus.Interface (obj,
"org.freedesktop.Avahi.Server")
self._device_uri_by_name[(name, stype, domain)] = uri
debugprint ("Resolving address for %s" % hostname)
debugprint ("Resolving address for %s" % uri)
server.ResolveService (-1, -1,
name, stype, domain,
-1, 0,
Expand All @@ -88,13 +200,18 @@ def _resolved (self):
def _reply (self, interface, protocol, name, stype, domain,
host, aprotocol, address, port, txt, flags):
uri = self._device_uri_by_name[(name, stype, domain)]
self._devices[uri].address = address
device = self._devices[uri]
device.address = address
hostname = host
p = hostname.find(".")
if p != -1:
hostname = hostname[:p]
debugprint ("%s is at %s (%s)" % (uri, address, hostname))
self._devices[uri].hostname = hostname
device.hostname = hostname
if hasattr (device, 'id_dict') and not device.id_dict.get ('SN', ''):
serial = _txt_value (txt, 'usb_SER')
if serial != '':
device.id_dict['SN'] = serial
self._resolved ()

def _error (self, uri, error):
Expand Down
102 changes: 83 additions & 19 deletions newprinter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1901,6 +1901,9 @@ def setNPButtons(self):
try:
uri = self.getDeviceURI ()
valid = validDeviceURI (uri)
except AttributeError:
# No device selected yet.
valid = False
except:
nonfatalException ()
self.btnNPForward.set_sensitive(valid)
Expand Down Expand Up @@ -2023,47 +2026,71 @@ def error_getting_devices (self, conn, exc):
self.fetchDevices_conn._end_operation ()
self.fetchDevices_conn.destroy ()
self.fetchDevices_conn = None
# Display any devices buffered before the error, then clear.
if self._pending_devices is not None:
pending = self._pending_devices
self._pending_devices = None
self.add_devices (pending, None, no_more=True)

def local_devices_reply (self, conn, result, current_uri):
self.dec_spinner_task ()

# Now we've got the local devices, start a request for the
# network devices.
self.fetchDevices (network=True, current_uri=current_uri)
# Buffer local devices rather than displaying them immediately.
# This prevents the UI from flickering when a legacy USB device
# is briefly shown and then later suppressed/replaced by its
# IPP-over-USB counterpart discovered during the network/DNS-SD phase.
self._pending_devices = result.copy()

# Add the local devices to the list.
self.add_devices (result, current_uri)
# Now start a request for the network devices.
self.fetchDevices (network=True, current_uri=current_uri)

def network_devices_reply (self, conn, result, current_uri):
self.fetchDevices_conn._end_operation ()
self.fetchDevices_conn.destroy ()
self.fetchDevices_conn = None

# Add the network devices to the list.
no_more = True
# Separate devices that need DNS-SD resolution.
need_resolving = {}
for uri, device in result.items ():
if uri.startswith ("dnssd://"):
if dnssdresolve.needs_service_resolution (uri):
need_resolving[uri] = device
no_more = False

for uri in need_resolving.keys ():
del result[uri]

self.add_devices (result, current_uri, no_more=no_more)
# Merge non-DNS-SD network devices into the pending buffer.
if self._pending_devices is None:
self._pending_devices = result.copy()
else:
self._pending_devices.update (result)

if len (need_resolving) > 0:
# DNS-SD resolution required — keep buffering.
resolver = dnssdresolve.DNSSDHostNamesResolver (need_resolving)
self.inc_spinner_task ()
resolver.resolve (reply_handler=lambda devices:
self.dnssd_resolve_reply (current_uri,
devices))
else:
# No DNS-SD resolution needed — display the final device list.
pending = self._pending_devices
self._pending_devices = None
self.add_devices (pending, current_uri, no_more=True)

self.dec_spinner_task ()
self.check_firewall ()

def dnssd_resolve_reply (self, current_uri, devices):
self.add_devices (devices, current_uri, no_more=True)
# Merge resolved DNS-SD devices into the pending buffer and
# display the complete device list exactly once.
if self._pending_devices is None:
self._pending_devices = {}
self._pending_devices.update (devices)

pending = self._pending_devices
self._pending_devices = None
self.add_devices (pending, current_uri, no_more=True)

self.dec_spinner_task ()
self.check_firewall ()

Expand Down Expand Up @@ -2265,6 +2292,8 @@ def fillDeviceTab(self, current_uri=None):
self.devices_find_nw_iter = find_nw_iter
self.devices_network_iter = network_iter
self.devices_network_fetched = False
self._legacy_usb_cache = dnssdresolve.LegacyUSBDeviceCache ()
self._pending_devices = None
self.tvNPDevices.set_model (model)
self.entNPTDevice.set_text ('')
self.expNPDeviceURIs.hide ()
Expand Down Expand Up @@ -2323,21 +2352,28 @@ def start_fetching_devices (self):
self.fetchDevices_conn._begin_operation (_("fetching device list"))
self.fetchDevices (network=False, current_uri=self.current_uri)
del self.current_uri

def add_devices (self, devices, current_uri, no_more=False):
current_from_batch = False
if current_uri:
if current_uri in devices:
current = devices.pop(current_uri)
current_from_batch = True
elif current_uri.replace (":9100", "") in devices:
current_uri = current_uri.replace (":9100", "")
current = devices.pop(current_uri)
current_from_batch = True
elif no_more:
current = cupshelpers.Device (current_uri)
current.info = "Current device"
else:
current_uri = None

devices = list(devices.values())
devices = dnssdresolve.suppress_legacy_usb_devices (devices, self._legacy_usb_cache)
if current_from_batch and current_uri and \
not any (device.uri == current_uri for device in devices):
current_uri = None
current = None

for device in devices:
if device.type == "socket":
Expand Down Expand Up @@ -2375,6 +2411,29 @@ def replace_generic (device):
"hal", "beh", "smb",
"scsi", "http", "bjnp",
"delete")]

ipp_serials = dnssdresolve.ipp_usb_serials (devices)
if ipp_serials:
to_remove = []
for phys in self.devices:
devs = phys.get_devices ()
if devs and any (getattr (d, 'type', '') == 'usb' for d in devs):
if phys.sn in ipp_serials:
debugprint ("Removing stale USB PhysicalDevice with SN %s" % phys.sn)
to_remove.append (phys)

if to_remove:
model = self.tvNPDevices.get_model ()
for phys in to_remove:
self.devices.remove (phys)
if model:
it = model.get_iter_first ()
while it:
if model.get_value (it, 1) == phys:
model.remove (it)
break
it = model.iter_next (it)

newdevices = []
for device in devices:
debugprint("Adding device with URI %s" % device.uri)
Expand Down Expand Up @@ -2433,13 +2492,18 @@ def replace_generic (device):
# An actual network printer device. Put this at the top.
iter = model.insert_before (network_iter, find_nw_iter,
row=row)

# If this is the currently selected device we need
# to expand the "Network Printer" row so that it
# is visible.
if device == current_device:
network_path = model.get_path (network_iter)
self.tvNPDevices.expand_row (network_path, False)
if device == current_device or dnssdresolve.is_ipp_over_usb_device(devs[0]):
network_path = model.get_path(network_iter)
child_path = model.get_path(iter)
self.tvNPDevices.expand_row(network_path, False)

def _select_ipp_device(tv, path):
tv.scroll_to_cell(path, None, True, 0.5, 0.0)
col = tv.get_column(0)
tv.set_cursor(path, col, False)
return False
GLib.idle_add(_select_ipp_device,
self.tvNPDevices, child_path)
else:
# Just a method of finding one.
iter = model.append (network_iter, row=row)
Expand Down
Loading