home *** CD-ROM | disk | FTP | other *** search
/ Clickx 115 / Clickx 115.iso / software / tools / windows / tails-i386-0.16.iso / live / filesystem.squashfs / usr / share / system-config-printer / monitor.py < prev    next >
Encoding:
Python Source  |  2010-09-28  |  28.5 KB  |  760 lines

  1. #!/usr/bin/env python
  2.  
  3. ## Copyright (C) 2007, 2008, 2009, 2010 Red Hat, Inc.
  4. ## Author: Tim Waugh <twaugh@redhat.com>
  5.  
  6. ## This program is free software; you can redistribute it and/or modify
  7. ## it under the terms of the GNU General Public License as published by
  8. ## the Free Software Foundation; either version 2 of the License, or
  9. ## (at your option) any later version.
  10.  
  11. ## This program is distributed in the hope that it will be useful,
  12. ## but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  14. ## GNU General Public License for more details.
  15.  
  16. ## You should have received a copy of the GNU General Public License
  17. ## along with this program; if not, write to the Free Software
  18. ## Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
  19.  
  20. import cups
  21. cups.require("1.9.42")
  22. import dbus
  23. import dbus.glib
  24. import gobject
  25. import time
  26. from debug import *
  27. import pprint
  28.  
  29. global _
  30. _ = lambda x: x
  31. def set_gettext_function (x):
  32.     _ = x
  33. import statereason
  34. from statereason import StateReason
  35. statereason.set_gettext_function (_)
  36.  
  37. CONNECTING_TIMEOUT = 60 # seconds
  38. MIN_REFRESH_INTERVAL = 1 # seconds
  39.  
  40. def state_reason_is_harmless (reason):
  41.     if (reason.startswith ("moving-to-paused") or
  42.         reason.startswith ("paused") or
  43.         reason.startswith ("shutdown") or
  44.         reason.startswith ("stopping") or
  45.         reason.startswith ("stopped-partly")):
  46.         return True
  47.     return False
  48.  
  49. def collect_printer_state_reasons (connection):
  50.     result = {}
  51.     try:
  52.         printers = connection.getPrinters ()
  53.     except cups.IPPError:
  54.         return result
  55.  
  56.     for name, printer in printers.iteritems ():
  57.         reasons = printer["printer-state-reasons"]
  58.         for reason in reasons:
  59.             if reason == "none":
  60.                 break
  61.             if state_reason_is_harmless (reason):
  62.                 continue
  63.             if not result.has_key (name):
  64.                 result[name] = []
  65.             result[name].append (StateReason (connection, name, reason))
  66.     return result
  67.  
  68. class Watcher:
  69.     # Interface definition
  70.     def monitor_exited (self, monitor):
  71.         debugprint (repr (monitor) + " exited")
  72.  
  73.     def state_reason_added (self, monitor, reason):
  74.         debugprint (repr (monitor) + ": +" + repr (reason))
  75.  
  76.     def state_reason_removed (self, monitor, reason):
  77.         debugprint (repr (monitor) + ": -" + repr (reason))
  78.  
  79.     def still_connecting (self, monitor, reason):
  80.         debugprint (repr (monitor) + ": `%s' still connecting" %
  81.                     reason.get_printer ())
  82.  
  83.     def now_connected (self, monitor, printer):
  84.         debugprint (repr (monitor) + ": `%s' now connected" % printer)
  85.  
  86.     def current_printers_and_jobs (self, monitor, printers, jobs):
  87.         debugprint (repr (monitor) + ": printers and jobs lists provided")
  88.  
  89.     def job_added (self, monitor, jobid, eventname, event, jobdata):
  90.         debugprint (repr (monitor) + ": job %d added" % jobid)
  91.  
  92.     def job_event (self, monitor, jobid, eventname, event, jobdata):
  93.         debugprint (repr (monitor) + ": job %d has event `%s'" %
  94.                     (jobid, eventname))
  95.  
  96.     def job_removed (self, monitor, jobid, eventname, event):
  97.         debugprint (repr (monitor) + ": job %d removed" % jobid)
  98.  
  99.     def printer_added (self, monitor, printer):
  100.         debugprint (repr (monitor) + ": printer `%s' added" % printer)
  101.  
  102.     def printer_event (self, monitor, printer, eventname, event):
  103.         debugprint (repr (monitor) + ": printer `%s' has event `%s'" %
  104.                     (printer, eventname))
  105.  
  106.     def printer_removed (self, monitor, printer):
  107.         debugprint (repr (monitor) + ": printer `%s' removed" % printer)
  108.  
  109.     def cups_connection_error (self, monitor):
  110.         debugprint (repr (monitor) + ": CUPS connection error")
  111.  
  112.     def cups_ipp_error (self, monitor, e, m):
  113.         debugprint (repr (monitor) + ": CUPS IPP error (%d, %s)" %
  114.                     (e, repr (m)))
  115.  
  116. class Monitor:
  117.     # Monitor jobs and printers.
  118.     DBUS_PATH="/com/redhat/PrinterSpooler"
  119.     DBUS_IFACE="com.redhat.PrinterSpooler"
  120.  
  121.     def __init__(self, watcher, bus=None, my_jobs=True, specific_dests=None,
  122.                  monitor_jobs=True, host=None, port=None, encryption=None):
  123.         self.watcher = watcher
  124.         self.my_jobs = my_jobs
  125.         self.specific_dests = specific_dests
  126.         self.monitor_jobs = monitor_jobs
  127.         self.jobs = {}
  128.         self.printer_state_reasons = {}
  129.         self.printers = set()
  130.         self.process_pending_events = True
  131.         self.fetch_jobs_timer = None
  132.  
  133.         if host:
  134.             cups.setServer (host)
  135.         if port:
  136.             cups.setPort (port)
  137.         if encryption:
  138.             cups.setEncryption (encryption)
  139.         self.user = cups.getUser ()
  140.         self.host = cups.getServer ()
  141.         self.port = cups.getPort ()
  142.         self.encryption = cups.getEncryption ()
  143.  
  144.         self.which_jobs = "not-completed"
  145.         self.reasons_seen = {}
  146.         self.connecting_timers = {}
  147.         self.still_connecting = set()
  148.         self.connecting_to_device = {}
  149.         self.received_any_dbus_signals = False
  150.         self.update_timer = None
  151.  
  152.         if bus == None:
  153.             try:
  154.                 bus = dbus.SystemBus ()
  155.             except dbus.exceptions.DBusException:
  156.                 # System bus not running.
  157.                 pass
  158.  
  159.         if bus != None:
  160.             bus.add_signal_receiver (self.handle_dbus_signal,
  161.                                      path=self.DBUS_PATH,
  162.                                      dbus_interface=self.DBUS_IFACE)
  163.             self.bus = bus
  164.  
  165.         self.sub_id = -1
  166.         self.refresh ()
  167.  
  168.     def get_jobs (self):
  169.         return self.jobs.copy ()
  170.  
  171.     def cleanup (self):
  172.         if self.sub_id != -1:
  173.             user = cups.getUser ()
  174.             try:
  175.                 cups.setUser (self.user)
  176.                 c = cups.Connection (host=self.host,
  177.                                      port=self.port,
  178.                                      encryption=self.encryption)
  179.                 c.cancelSubscription (self.sub_id)
  180.                 debugprint ("Canceled subscription %d" % self.sub_id)
  181.             except:
  182.                 pass
  183.             cups.setUser (user)
  184.  
  185.         if self.bus != None:
  186.             self.bus.remove_signal_receiver (self.handle_dbus_signal,
  187.                                              path=self.DBUS_PATH,
  188.                                              dbus_interface=self.DBUS_IFACE)
  189.  
  190.         timers = self.connecting_timers.values ()
  191.         for timer in [self.update_timer, self.fetch_jobs_timer]:
  192.             if timer:
  193.                 timers.append (timer)
  194.         for timer in timers:
  195.             gobject.source_remove (timer)
  196.  
  197.         self.watcher.monitor_exited (self)
  198.  
  199.     def set_process_pending (self, whether):
  200.         self.process_pending_events = whether
  201.  
  202.     def check_still_connecting(self, printer):
  203.         """Timer callback to check on connecting-to-device reasons."""
  204.         if not self.process_pending_events:
  205.             # Defer the timer by setting a new one.
  206.             timer = gobject.timeout_add (200, self.check_still_connecting,
  207.                                          printer)
  208.             self.connecting_timers[printer] = timer
  209.             return False
  210.  
  211.         if self.connecting_timers.has_key (printer):
  212.             del self.connecting_timers[printer]
  213.  
  214.         debugprint ("Still-connecting timer fired for `%s'" % printer)
  215.         (printer_jobs, my_printers) = self.sort_jobs_by_printer ()
  216.         self.update_connecting_devices (printer_jobs)
  217.  
  218.         # Don't run this callback again.
  219.         return False
  220.  
  221.     def update_connecting_devices(self, printer_jobs={}):
  222.         """Updates connecting_to_device dict and still_connecting set."""
  223.         time_now = time.time ()
  224.         connecting_to_device = {}
  225.         trouble = False
  226.         for printer, reasons in self.printer_state_reasons.iteritems ():
  227.             connected = True
  228.             for reason in reasons:
  229.                 if reason.get_reason () == "connecting-to-device":
  230.                     have_processing_job = False
  231.                     for job, data in \
  232.                             printer_jobs.get (printer, {}).iteritems ():
  233.                         state = data.get ('job-state',
  234.                                           cups.IPP_JOB_CANCELED)
  235.                         if state == cups.IPP_JOB_PROCESSING:
  236.                             have_processing_job = True
  237.                             break
  238.  
  239.                     if not have_processing_job:
  240.                         debugprint ("Ignoring stale connecting-to-device x")
  241.                         continue
  242.  
  243.                     # Build a new connecting_to_device dict.  If our existing
  244.                     # dict already has an entry for this printer, use that.
  245.                     printer = reason.get_printer ()
  246.                     t = self.connecting_to_device.get (printer, time_now)
  247.                     connecting_to_device[printer] = t
  248.                     debugprint ("Connecting time: %d" % (time_now - t))
  249.                     if time_now - t >= CONNECTING_TIMEOUT:
  250.                         if have_processing_job:
  251.                             if printer not in self.still_connecting:
  252.                                 self.still_connecting.add (printer)
  253.                                 self.watcher.still_connecting (self, reason)
  254.                             if self.connecting_timers.has_key (printer):
  255.                                 gobject.source_remove (self.connecting_timers
  256.                                                        [printer])
  257.                                 del self.connecting_timers[printer]
  258.                                 debugprint ("Stopped connecting timer "
  259.                                             "for `%s'" % printer)
  260.  
  261.                     connected = False
  262.                     break
  263.  
  264.             if connected and self.connecting_timers.has_key (printer):
  265.                 gobject.source_remove (self.connecting_timers[printer])
  266.                 del self.connecting_timers[printer]
  267.                 debugprint ("Stopped connecting timer for `%s'" % printer)
  268.  
  269.         # Clear any previously-notified errors that are now fine.
  270.         remove = set()
  271.         for printer in self.still_connecting:
  272.             if not connecting_to_device.has_key (printer):
  273.                 remove.add (printer)
  274.                 self.watcher.now_connected (self, printer)
  275.                 if self.connecting_timers.has_key (printer):
  276.                     gobject.source_remove (self.connecting_timers[printer])
  277.                     del self.connecting_timers[printer]
  278.                     debugprint ("Stopped connecting timer for `%s'" % printer)
  279.  
  280.         self.still_connecting = self.still_connecting.difference (remove)
  281.         self.connecting_to_device = connecting_to_device
  282.  
  283.     def check_state_reasons(self, my_printers=set(), printer_jobs={}):
  284.         # Look for any new reasons since we last checked.
  285.         old_reasons_seen_keys = self.reasons_seen.keys ()
  286.         reasons_now = set()
  287.         for printer, reasons in self.printer_state_reasons.iteritems ():
  288.             for reason in reasons:
  289.                 tuple = reason.get_tuple ()
  290.                 printer = reason.get_printer ()
  291.                 reasons_now.add (tuple)
  292.                 if not self.reasons_seen.has_key (tuple):
  293.                     # New reason.
  294.                     self.watcher.state_reason_added (self, reason)
  295.                     self.reasons_seen[tuple] = reason
  296.  
  297.                 if (reason.get_reason () == "connecting-to-device" and
  298.                     not self.connecting_to_device.has_key (printer)):
  299.                     # First time we've seen this.
  300.  
  301.                     have_processing_job = False
  302.                     for job, data in \
  303.                             printer_jobs.get (printer, {}).iteritems ():
  304.                         state = data.get ('job-state',
  305.                                           cups.IPP_JOB_CANCELED)
  306.                         if state == cups.IPP_JOB_PROCESSING:
  307.                             have_processing_job = True
  308.                             break
  309.  
  310.                     if have_processing_job:
  311.                         t = gobject.timeout_add_seconds (
  312.                             (1 + CONNECTING_TIMEOUT),
  313.                             self.check_still_connecting,
  314.                             printer)
  315.                         self.connecting_timers[printer] = t
  316.                         debugprint ("Start connecting timer for `%s'" %
  317.                                     printer)
  318.                     else:
  319.                         # Don't notify about this, as it must be stale.
  320.                         debugprint ("Ignoring stale connecting-to-device")
  321.                         if get_debugging ():
  322.                             debugprint (pprint.pformat (printer_jobs))
  323.  
  324.         self.update_connecting_devices (printer_jobs)
  325.         items = self.reasons_seen.keys ()
  326.         for tuple in items:
  327.             if not tuple in reasons_now:
  328.                 # Reason no longer present.
  329.                 reason = self.reasons_seen[tuple]
  330.                 del self.reasons_seen[tuple]
  331.                 self.watcher.state_reason_removed (self, reason)
  332.  
  333.     def get_notifications(self):
  334.         if not self.process_pending_events:
  335.             # Defer the timer callback.
  336.             if self.update_timer:
  337.                 gobject.source_remove (self.update_timer)
  338.  
  339.             self.update_timer = gobject.timeout_add (200,
  340.                                                      self.get_notifications)
  341.             return False
  342.  
  343.         debugprint ("get_notifications")
  344.         user = cups.getUser ()
  345.         try:
  346.             cups.setUser (self.user)
  347.             c = cups.Connection (host=self.host,
  348.                                  port=self.port,
  349.                                  encryption=self.encryption)
  350.  
  351.             try:
  352.                 try:
  353.                     notifications = c.getNotifications ([self.sub_id],
  354.                                                         [self.sub_seq + 1])
  355.                 except AttributeError:
  356.                     notifications = c.getNotifications ([self.sub_id])
  357.             except cups.IPPError, (e, m):
  358.                 cups.setUser (user)
  359.                 if e == cups.IPP_NOT_FOUND:
  360.                     # Subscription lease has expired.
  361.                     self.sub_id = -1
  362.                     self.refresh ()
  363.                     return False
  364.  
  365.                 self.watcher.cups_ipp_error (self, e, m)
  366.                 return True
  367.         except RuntimeError:
  368.             cups.setUser (user)
  369.             self.watcher.cups_connection_error (self)
  370.             return True
  371.  
  372.         cups.setUser (user)
  373.         deferred_calls = []
  374.         jobs = self.jobs.copy ()
  375.         for event in notifications['events']:
  376.             seq = event['notify-sequence-number']
  377.             self.sub_seq = seq
  378.             nse = event['notify-subscribed-event']
  379.             debugprint ("%d %s %s" % (seq, nse, event['notify-text']))
  380.             if get_debugging ():
  381.                 debugprint (pprint.pformat (event))
  382.             if nse.startswith ('printer-'):
  383.                 # Printer events
  384.                 name = event['printer-name']
  385.                 if nse == 'printer-added' and name not in self.printers:
  386.                     self.printers.add (name)
  387.                     deferred_calls.append ((self.watcher.printer_added,
  388.                                             (self, name)))
  389.  
  390.                 elif nse == 'printer-deleted' and name in self.printers:
  391.                     self.printers.remove (name)
  392.                     items = self.reasons_seen.keys ()
  393.                     for tuple in items:
  394.                         if tuple[1] == name:
  395.                             reason = self.reasons_seen[tuple]
  396.                             del self.reasons_seen[tuple]
  397.                             deferred_calls.append ((self.watcher.state_reason_removed,
  398.                                                     (self, reason)))
  399.                             
  400.                     if self.printer_state_reasons.has_key (name):
  401.                         del self.printer_state_reasons[name]
  402.  
  403.                     deferred_calls.append ((self.watcher.printer_removed,
  404.                                             (self, name)))
  405.                 elif name in self.printers:
  406.                     printer_state_reasons = event['printer-state-reasons']
  407.                     reasons = []
  408.                     for reason in printer_state_reasons:
  409.                         if reason == "none":
  410.                             break
  411.                         if state_reason_is_harmless (reason):
  412.                             continue
  413.                         reasons.append (StateReason (c, name, reason))
  414.                     self.printer_state_reasons[name] = reasons
  415.  
  416.                     deferred_calls.append ((self.watcher.printer_event,
  417.                                             (self, name, nse, event)))
  418.                 continue
  419.  
  420.             # Job events
  421.             if not nse.startswith ("job-"):
  422.                 # Some versions of CUPS give empty
  423.                 # notify-subscribed-event attributes (STR #3608).
  424.                 debugprint ("Unhandled nse %s" % repr (nse))
  425.                 continue
  426.  
  427.             jobid = event['notify-job-id']
  428.             if (nse == 'job-created' or
  429.                 (nse == 'job-state-changed' and
  430.                  not jobs.has_key (jobid) and
  431.                  event['job-state'] == cups.IPP_JOB_PROCESSING)):
  432.                 if (self.specific_dests != None and
  433.                     event['printer-name'] not in self.specific_dests):
  434.                     continue
  435.  
  436.                 try:
  437.                     attrs = c.getJobAttributes (jobid)
  438.                     if (self.my_jobs and
  439.                         attrs['job-originating-user-name'] != cups.getUser ()):
  440.                         continue
  441.  
  442.                     jobs[jobid] = attrs
  443.                 except AttributeError:
  444.                     jobs[jobid] = {'job-k-octets': 0}
  445.                 except cups.IPPError, (e, m):
  446.                     self.watcher.cups_ipp_error (self, e, m)
  447.                     jobs[jobid] = {'job-k-octets': 0}
  448.  
  449.                 deferred_calls.append ((self.watcher.job_added,
  450.                                         (self, jobid, nse, event,
  451.                                          jobs[jobid].copy ())))
  452.             elif (nse == 'job-completed' or
  453.                   (nse == 'job-state-changed' and
  454.                    event['job-state'] == cups.IPP_JOB_COMPLETED)):
  455.                 if not (self.which_jobs in ['completed', 'all']):
  456.                     try:
  457.                         del jobs[jobid]
  458.                         deferred_calls.append ((self.watcher.job_removed,
  459.                                                 (self, jobid, nse, event)))
  460.                     except KeyError:
  461.                         pass
  462.                     continue
  463.  
  464.             try:
  465.                 job = jobs[jobid]
  466.             except KeyError:
  467.                 continue
  468.  
  469.             for attribute in ['job-state',
  470.                               'job-name']:
  471.                 job[attribute] = event[attribute]
  472.             if event.has_key ('notify-printer-uri'):
  473.                 job['job-printer-uri'] = event['notify-printer-uri']
  474.  
  475.             deferred_calls.append ((self.watcher.job_event,
  476.                                    (self, jobid, nse, event, job.copy ())))
  477.  
  478.         self.set_process_pending (False)
  479.         self.update_jobs (jobs)
  480.         self.jobs = jobs
  481.  
  482.         for (fn, args) in deferred_calls:
  483.             fn (*args)
  484.         self.set_process_pending (True)
  485.  
  486.         # Update again when we're told to.  If we're getting CUPS
  487.         # D-Bus signals, however, rely on those instead.
  488.         if not self.received_any_dbus_signals:
  489.             if self.update_timer:
  490.                 gobject.source_remove (self.update_timer)
  491.  
  492.             interval = notifications['notify-get-interval']
  493.             t = gobject.timeout_add_seconds (interval,
  494.                                              self.get_notifications)
  495.             self.update_timer = t
  496.  
  497.         return False
  498.  
  499.     def refresh(self, which_jobs=None, refresh_all=True):
  500.         debugprint ("refresh")
  501.  
  502.         if which_jobs != None:
  503.             self.which_jobs = which_jobs
  504.  
  505.         user = cups.getUser ()
  506.         try:
  507.             cups.setUser (self.user)
  508.             c = cups.Connection (host=self.host,
  509.                                  port=self.port,
  510.                                  encryption=self.encryption)
  511.         except RuntimeError:
  512.             self.watcher.cups_connection_error (self)
  513.             cups.setUser (user)
  514.             return
  515.  
  516.         if self.sub_id != -1:
  517.             try:
  518.                 c.cancelSubscription (self.sub_id)
  519.             except cups.IPPError, (e, m):
  520.                 self.watcher.cups_ipp_error (self, e, m)
  521.  
  522.             if self.update_timer:
  523.                 gobject.source_remove (self.update_timer)
  524.  
  525.             debugprint ("Canceled subscription %d" % self.sub_id)
  526.  
  527.         try:
  528.             del self.sub_seq
  529.         except AttributeError:
  530.             pass
  531.  
  532.         events = ["printer-added",
  533.                   "printer-deleted",
  534.                   "printer-state-changed"]
  535.         if self.monitor_jobs:
  536.             events.extend (["job-created",
  537.                             "job-completed",
  538.                             "job-stopped",
  539.                             "job-state-changed",
  540.                             "job-progress"])
  541.  
  542.         try:
  543.             self.sub_id = c.createSubscription ("/", events=events)
  544.         except cups.IPPError, (e, m):
  545.             self.watcher.cups_ipp_error (self, e, m)
  546.  
  547.         cups.setUser (user)
  548.  
  549.         self.update_timer = gobject.timeout_add_seconds (MIN_REFRESH_INTERVAL,
  550.                                                          self.get_notifications)
  551.         debugprint ("Created subscription %d" % self.sub_id)
  552.  
  553.         if self.monitor_jobs:
  554.             jobs = self.jobs.copy ()
  555.             if self.which_jobs not in ['all', 'completed']:
  556.                 # Filter out completed jobs.
  557.                 filtered = {}
  558.                 for jobid, job in jobs.iteritems ():
  559.                     if job.get ('job-state',
  560.                                 cups.IPP_JOB_CANCELED) < cups.IPP_JOB_CANCELED:
  561.                         filtered[jobid] = job
  562.                 jobs = filtered
  563.  
  564.             self.fetch_first_job_id = 1
  565.             if self.fetch_jobs_timer:
  566.                 gobject.source_remove (self.fetch_jobs_timer)
  567.             self.fetch_jobs_timer = gobject.timeout_add (5, self.fetch_jobs,
  568.                                                          refresh_all)
  569.         else:
  570.             jobs = {}
  571.  
  572.         try:
  573.             self.printer_state_reasons = collect_printer_state_reasons (c)
  574.             dests = c.getPrinters ()
  575.             self.printers = set(dests.keys ())
  576.         except cups.IPPError, (e, m):
  577.             self.watcher.cups_ipp_error (self, e, m)
  578.             return
  579.         except RuntimeError:
  580.             self.watcher.cups_connection_error (self)
  581.             return
  582.  
  583.         if self.specific_dests != None:
  584.             for jobid in jobs.keys ():
  585.                 uri = jobs[jobid].get('job-printer-uri', '/')
  586.                 i = uri.rfind ('/')
  587.                 printer = uri[i + 1:]
  588.                 if printer not in self.specific_dests:
  589.                     del jobs[jobid]
  590.  
  591.         self.set_process_pending (False)
  592.         self.watcher.current_printers_and_jobs (self, self.printers.copy (),
  593.                                                 jobs.copy ())
  594.         self.update_jobs (jobs)
  595.         self.jobs = jobs
  596.         self.set_process_pending (True)
  597.         return False
  598.  
  599.     def fetch_jobs (self, refresh_all):
  600.         if not self.process_pending_events:
  601.             # Skip this call.  We'll get called again soon.
  602.             return True
  603.  
  604.         user = cups.getUser ()
  605.         try:
  606.             cups.setUser (self.user)
  607.             c = cups.Connection (host=self.host,
  608.                                  port=self.port,
  609.                                  encryption=self.encryption)
  610.         except RuntimeError:
  611.             self.watcher.cups_connection_error (self)
  612.             self.fetch_jobs_timer = None
  613.             cups.setUser (user)
  614.             return False
  615.  
  616.         limit = 1
  617.         r = ["job-id",
  618.              "job-printer-uri",
  619.              "job-state",
  620.              "job-originating-user-name",
  621.              "job-k-octets",
  622.              "job-name",
  623.              "time-at-creation"]
  624.         try:
  625.             try:
  626.                 fetched = c.getJobs (which_jobs=self.which_jobs,
  627.                                      my_jobs=self.my_jobs,
  628.                                      first_job_id=self.fetch_first_job_id,
  629.                                      limit=limit,
  630.                                      requested_attributes=r)
  631.             except TypeError:
  632.                 # requested_attributes requires pycups 1.9.50
  633.                 fetched = c.getJobs (which_jobs=self.which_jobs,
  634.                                      my_jobs=self.my_jobs,
  635.                                      first_job_id=self.fetch_first_job_id,
  636.                                      limit=limit)
  637.         except cups.IPPError, (e, m):
  638.             self.watcher.cups_ipp_error (self, e, m)
  639.             self.fetch_jobs_timer = None
  640.             cups.setUser (user)
  641.             return False
  642.  
  643.         cups.setUser (user)
  644.         got = len (fetched)
  645.         debugprint ("Got %s jobs, asked for %s" % (got, limit))
  646.  
  647.         deferred_calls = []
  648.         jobs = self.jobs.copy ()
  649.         jobids = fetched.keys ()
  650.         jobids.sort ()
  651.         if got > 0:
  652.             last_jobid = jobids[got - 1]
  653.         else:
  654.             last_jobid = self.fetch_first_job_id + limit
  655.         for jobid in xrange (self.fetch_first_job_id, last_jobid + 1):
  656.             try:
  657.                 job = fetched[jobid]
  658.                 if self.specific_dests != None:
  659.                     uri = job.get('job-printer-uri', '/')
  660.                     i = uri.rfind ('/')
  661.                     printer = uri[i + 1:]
  662.                     if printer not in self.specific_dests:
  663.                         raise KeyError
  664.  
  665.                 if jobs.has_key (jobid):
  666.                     fn = self.watcher.job_event
  667.                 else:
  668.                     fn = self.watcher.job_added
  669.  
  670.                 jobs[jobid] = job
  671.                 deferred_calls.append ((fn,
  672.                                         (self, jobid, '', {}, job.copy ())))
  673.             except KeyError:
  674.                 # No job by that ID.
  675.                 if jobs.has_key (jobid):
  676.                     del jobs[jobid]
  677.                     deferred_calls.append ((self.watcher.job_removed,
  678.                                             (self, jobid, '', {})))
  679.  
  680.         jobids = jobs.keys ()
  681.         jobids.sort ()
  682.         if got < limit:
  683.             trim = False
  684.             for i in range (len (jobids)):
  685.                 jobid = jobids[i]
  686.                 if not trim and jobid > last_jobid:
  687.                     trim = True
  688.             
  689.                 if trim:
  690.                     del jobs[jobid]
  691.                     deferred_calls.append ((self.watcher.job_removed,
  692.                                             (self, jobid, '', {})))
  693.  
  694.         self.update_jobs (jobs)
  695.         self.jobs = jobs
  696.  
  697.         for (fn, args) in deferred_calls:
  698.             fn (*args)
  699.  
  700.         if got < limit:
  701.             # That's all.  Don't run this timer again.
  702.             self.fetch_jobs_timer = None
  703.             return False
  704.  
  705.         # Remember where we got up to and run this timer again.
  706.         next = jobid + 1
  707.  
  708.         while not refresh_all and self.jobs.has_key (next):
  709.             next += 1
  710.  
  711.         self.fetch_first_job_id = next
  712.         return True
  713.  
  714.     def sort_jobs_by_printer (self, jobs=None):
  715.         if jobs == None:
  716.             jobs = self.jobs
  717.  
  718.         my_printers = set()
  719.         printer_jobs = {}
  720.         for job, data in jobs.iteritems ():
  721.             state = data.get ('job-state', cups.IPP_JOB_CANCELED)
  722.             if state >= cups.IPP_JOB_CANCELED:
  723.                 continue
  724.             uri = data.get ('job-printer-uri', '')
  725.             i = uri.rfind ('/')
  726.             if i == -1:
  727.                 continue
  728.             printer = uri[i + 1:]
  729.             my_printers.add (printer)
  730.             if not printer_jobs.has_key (printer):
  731.                 printer_jobs[printer] = {}
  732.             printer_jobs[printer][job] = data
  733.  
  734.         return (printer_jobs, my_printers)
  735.  
  736.     def update_jobs(self, jobs):
  737.         debugprint ("update_jobs")
  738.         (printer_jobs, my_printers) = self.sort_jobs_by_printer (jobs)
  739.         self.check_state_reasons (my_printers, printer_jobs)
  740.  
  741.     def update(self):
  742.         if self.update_timer:
  743.             gobject.source_remove (self.update_timer)
  744.  
  745.         self.update_timer = gobject.timeout_add (200, self.get_notifications)
  746.  
  747.     def handle_dbus_signal(self, *args):
  748.         self.update ()
  749.         if not self.received_any_dbus_signals:
  750.             self.received_any_dbus_signals = True
  751.  
  752. if __name__ == '__main__':
  753.     set_debugging (True)
  754.     m = Monitor (Watcher ())
  755.     loop = gobject.MainLoop ()
  756.     try:
  757.         loop.run ()
  758.     finally:
  759.         m.cleanup ()
  760.