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 / asyncipp.py < prev    next >
Encoding:
Python Source  |  2010-09-28  |  22.9 KB  |  660 lines

  1. #!/usr/bin/env python
  2.  
  3. ## Copyright (C) 2007, 2008, 2009, 2010 Red Hat, Inc.
  4. ## Copyright (C) 2008 Novell, Inc.
  5. ## Author: Tim Waugh <twaugh@redhat.com>
  6.  
  7. ## This program is free software; you can redistribute it and/or modify
  8. ## it under the terms of the GNU General Public License as published by
  9. ## the Free Software Foundation; either version 2 of the License, or
  10. ## (at your option) any later version.
  11.  
  12. ## This program is distributed in the hope that it will be useful,
  13. ## but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  15. ## GNU General Public License for more details.
  16.  
  17. ## You should have received a copy of the GNU General Public License
  18. ## along with this program; if not, write to the Free Software
  19. ## Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
  20.  
  21. import threading
  22. import config
  23. import cups
  24. import gobject
  25. import gtk
  26. import Queue
  27.  
  28. import authconn
  29. from debug import *
  30. import debug
  31.  
  32. _ = lambda x: x
  33. N_ = lambda x: x
  34. def set_gettext_function (fn):
  35.     global _
  36.     _ = fn
  37.  
  38. ######
  39. ###### An asynchronous libcups API using IPP with a separate worker
  40. ###### thread.
  41. ######
  42.  
  43. ###
  44. ### This is the worker thread.
  45. ###
  46. class _IPPConnectionThread(threading.Thread):
  47.     def __init__ (self, queue, conn, reply_handler=None, error_handler=None,
  48.                   auth_handler=None, host=None, port=None, encryption=None):
  49.                   
  50.         threading.Thread.__init__ (self)
  51.         self.setDaemon (True)
  52.         self._queue = queue
  53.         self._conn = conn
  54.         self.host = host
  55.         self._port = port
  56.         self._encryption = encryption
  57.         self._reply_handler = reply_handler
  58.         self._error_handler = error_handler
  59.         self._auth_handler = auth_handler
  60.         self._auth_queue = Queue.Queue (1)
  61.         self.user = None
  62.         self._destroyed = False
  63.         debugprint ("+%s" % self)
  64.  
  65.     def __del__ (self):
  66.         debug.debugprint ("-%s" % self)
  67.  
  68.     def set_auth_info (self, password):
  69.         self._auth_queue.put (password)
  70.  
  71.     def run (self):
  72.         if self.host == None:
  73.             self.host = cups.getServer ()
  74.         if self._port == None:
  75.             self._port = cups.getPort ()
  76.         if self._encryption == None:
  77.             self._encryption = cups.getEncryption ()
  78.  
  79.         self.user = cups.getUser ()
  80.  
  81.         try:
  82.             cups.setPasswordCB2 (self._auth)
  83.         except AttributeError:
  84.             # Requires pycups >= 1.9.47.  Fall back to rubbish API.
  85.             cups.setPasswordCB (self._auth)
  86.  
  87.         try:
  88.             conn = cups.Connection (host=self.host,
  89.                                     port=self._port,
  90.                                     encryption=self._encryption)
  91.             self._reply (None)
  92.         except RuntimeError, e:
  93.             conn = None
  94.             self._error (e)
  95.  
  96.         while True:
  97.             # Wait to find out what operation to try.
  98.             debugprint ("Awaiting further instructions")
  99.             item = self._queue.get ()
  100.             debugprint ("Next task: %s" % repr (item))
  101.             if item == None:
  102.                 # Our signal to quit.
  103.                 self._queue.task_done ()
  104.                 break
  105.  
  106.             (fn, args, kwds, rh, eh, ah) = item
  107.             if rh != False:
  108.                 self._reply_handler = rh
  109.             if eh != False:
  110.                 self._error_handler = eh
  111.             if ah != False:
  112.                 self._auth_handler = ah
  113.  
  114.             if fn == True:
  115.                 # Our signal to change user and reconnect.
  116.                 self.user = args[0]
  117.                 cups.setUser (self.user)
  118.                 debugprint ("Set user=%s; reconnecting..." % self.user)
  119.                 try:
  120.                     cups.setPasswordCB2 (self._auth)
  121.                 except AttributeError:
  122.                     # Requires pycups >= 1.9.47.  Fall back to rubbish API.
  123.                     cups.setPasswordCB (self._auth)
  124.  
  125.                 try:
  126.                     conn = cups.Connection (host=self.host,
  127.                                             port=self._port,
  128.                                             encryption=self._encryption)
  129.                     debugprint ("...reconnected")
  130.  
  131.                     self._queue.task_done ()
  132.                     self._reply (None)
  133.                 except RuntimeError, e:
  134.                     debugprint ("...failed")
  135.                     self._queue.task_done ()
  136.                     self._error (e)
  137.  
  138.                 continue
  139.  
  140.             # Normal IPP operation.  Try to perform it.
  141.             try:
  142.                 debugprint ("Call %s" % fn)
  143.                 result = fn (conn, *args, **kwds)
  144.                 if fn == cups.Connection.adminGetServerSettings.__call__:
  145.                     # Special case for a rubbish bit of API.
  146.                     if result == {}:
  147.                         # Authentication failed, but we aren't told that.
  148.                         raise cups.IPPError (cups.IPP_NOT_AUTHORIZED, '')
  149.  
  150.                 debugprint ("...success")
  151.                 self._reply (result)
  152.             except Exception, e:
  153.                 debugprint ("...failure")
  154.                 self._error (e)
  155.  
  156.             self._queue.task_done ()
  157.  
  158.         debugprint ("Thread exiting")
  159.         self._destroyed = True
  160.         del self._conn # already destroyed
  161.         del self._reply_handler
  162.         del self._error_handler
  163.         del self._auth_handler
  164.         del self._queue
  165.         del self._auth_queue
  166.         del conn
  167.  
  168.         try:
  169.             cups.setPasswordCB2 (None)
  170.         except AttributeError:
  171.             # Requires pycups >= 1.9.47.  Fall back to rubbish API.
  172.             cups.setPasswordCB (lambda x: '')
  173.  
  174.     def _auth (self, prompt, conn=None, method=None, resource=None):
  175.         def prompt_auth (prompt):
  176.             gtk.gdk.threads_enter ()
  177.             if conn == None:
  178.                 self._auth_handler (prompt, self._conn)
  179.             else:
  180.                 self._auth_handler (prompt, self._conn, method, resource)
  181.  
  182.             gtk.gdk.threads_leave ()
  183.             return False
  184.  
  185.         if self._auth_handler == None:
  186.             return ""
  187.  
  188.         gobject.idle_add (prompt_auth, prompt)
  189.         password = self._auth_queue.get ()
  190.         return password
  191.  
  192.     def _reply (self, result):
  193.         def send_reply (handler, result):
  194.             if not self._destroyed:
  195.                 gtk.gdk.threads_enter ()
  196.                 handler (self._conn, result)
  197.                 gtk.gdk.threads_leave ()
  198.             return False
  199.  
  200.         if not self._destroyed and self._reply_handler:
  201.             gobject.idle_add (send_reply, self._reply_handler, result)
  202.  
  203.     def _error (self, exc):
  204.         def send_error (handler, exc):
  205.             if not self._destroyed:
  206.                 gtk.gdk.threads_enter ()
  207.                 handler (self._conn, exc)
  208.                 gtk.gdk.threads_leave ()
  209.             return False
  210.  
  211.         if not self._destroyed and self._error_handler:
  212.             debugprint ("Add %s to idle" % self._error_handler)
  213.             gobject.idle_add (send_error, self._error_handler, exc)
  214.  
  215. ###
  216. ### This is the user-visible class.  Although it does not inherit from
  217. ### cups.Connection it implements the same functions.
  218. ###
  219. class IPPConnection:
  220.     """
  221.     This class starts a new thread to handle IPP operations.
  222.  
  223.     Each IPP operation method takes optional reply_handler,
  224.     error_handler and auth_handler parameters.
  225.  
  226.     If an operation requires a password to proceed, the auth_handler
  227.     function will be called.  The operation will continue once
  228.     set_auth_info (in this class) is called.
  229.  
  230.     Once the operation has finished either reply_handler or
  231.     error_handler will be called.
  232.     """
  233.  
  234.     def __init__ (self, reply_handler=None, error_handler=None,
  235.                   auth_handler=None, host=None, port=None, encryption=None,
  236.                   parent=None):
  237.         debugprint ("New IPPConnection")
  238.         self._parent = parent
  239.         self.queue = Queue.Queue ()
  240.         self.thread = _IPPConnectionThread (self.queue, self,
  241.                                             reply_handler=reply_handler,
  242.                                             error_handler=error_handler,
  243.                                             auth_handler=auth_handler,
  244.                                             host=host, port=port,
  245.                                             encryption=encryption)
  246.         self.thread.start ()
  247.  
  248.         methodtype = type (cups.Connection.getPrinters)
  249.         bindings = []
  250.         for fname in dir (cups.Connection):
  251.             if fname[0] == ' ':
  252.                 continue
  253.             fn = getattr (cups.Connection, fname)
  254.             if type (fn) != methodtype:
  255.                 continue
  256.             setattr (self, fname, self._make_binding (fn))
  257.             bindings.append (fname)
  258.  
  259.         self.bindings = bindings
  260.         debugprint ("+%s" % self)
  261.  
  262.     def __del__ (self):
  263.         debug.debugprint ("-%s" % self)
  264.  
  265.     def destroy (self):
  266.         debugprint ("DESTROY: %s" % self)
  267.         if self.thread.isAlive ():
  268.             debugprint ("Putting None on the task queue")
  269.             self.queue.put (None)
  270.             self.queue.join ()
  271.  
  272.         for binding in self.bindings:
  273.             delattr (self, binding)
  274.  
  275.     def set_auth_info (self, password):
  276.         """Call this from your auth_handler function."""
  277.         self.thread.set_auth_info (password)
  278.  
  279.     def reconnect (self, user, reply_handler=None, error_handler=None):
  280.         debugprint ("Reconnect...")
  281.         self.queue.put ((True, (user,), {},
  282.                          reply_handler, error_handler, False))
  283.  
  284.     def _make_binding (self, fn):
  285.         return lambda *args, **kwds: self._call_function (fn, *args, **kwds)
  286.  
  287.     def _call_function (self, fn, *args, **kwds):
  288.         reply_handler = error_handler = auth_handler = False
  289.         if kwds.has_key ("reply_handler"):
  290.             reply_handler = kwds["reply_handler"]
  291.             del kwds["reply_handler"]
  292.         if kwds.has_key ("error_handler"):
  293.             error_handler = kwds["error_handler"]
  294.             del kwds["error_handler"]
  295.         if kwds.has_key ("auth_handler"):
  296.             auth_handler = kwds["auth_handler"]
  297.             del kwds["auth_handler"]
  298.  
  299.         self.queue.put ((fn, args, kwds,
  300.                          reply_handler, error_handler, auth_handler))
  301.  
  302. ######
  303. ###### An asynchronous libcups API with graphical authentication and
  304. ###### retrying.
  305. ######
  306.  
  307. ###
  308. ### A class to take care of an individual operation.
  309. ###
  310. class _IPPAuthOperation:
  311.     def __init__ (self, reply_handler, error_handler, conn,
  312.                   user=None, fn=None, args=None, kwds=None):
  313.         self._auth_called = False
  314.         self._dialog_shown = False
  315.         self._use_password = ''
  316.         self._cancel = False
  317.         self._reconnect = False
  318.         self._reconnected = False
  319.         self._user = user
  320.         self._conn = conn
  321.         self._try_as_root = self._conn.try_as_root
  322.         self._client_fn = fn
  323.         self._client_args = args
  324.         self._client_kwds = kwds
  325.         self._client_reply_handler = reply_handler
  326.         self._client_error_handler = error_handler
  327.         debugprint ("+%s" % self)
  328.  
  329.     def __del__ (self):
  330.         debug.debugprint ("-%s" % self)
  331.  
  332.     def _destroy (self):
  333.         del self._conn
  334.         del self._client_fn
  335.         del self._client_args
  336.         del self._client_kwds
  337.         del self._client_reply_handler
  338.         del self._client_error_handler
  339.  
  340.     def error_handler (self, conn, exc):
  341.         if self._client_fn == None:
  342.             # This is the initial "connection" operation, or a
  343.             # subsequent reconnection attempt.
  344.             debugprint ("Connection/reconnection failed")
  345.             return self._reconnect_error (conn, exc)
  346.  
  347.         if self._cancel:
  348.             return self._error (exc)
  349.  
  350.         if self._reconnect:
  351.             self._reconnect = False
  352.             self._reconnected = True
  353.             conn.reconnect (self._user,
  354.                             reply_handler=self._reconnect_reply,
  355.                             error_handler=self._reconnect_error)
  356.             return
  357.  
  358.         forbidden = False
  359.         if type (exc) == cups.IPPError:
  360.             (e, m) = exc.args
  361.             if (e == cups.IPP_NOT_AUTHORIZED or
  362.                 e == cups.IPP_FORBIDDEN):
  363.                 forbidden = (e == cups.IPP_FORBIDDEN)
  364.             elif e == cups.IPP_SERVICE_UNAVAILABLE:
  365.                 return self._reconnect_error (conn, exc)
  366.             else:
  367.                 return self._error (exc)
  368.         elif type (exc) == cups.HTTPError:
  369.             (s,) = exc.args
  370.             if (s == cups.HTTP_UNAUTHORIZED or
  371.                 s == cups.HTTP_FORBIDDEN):
  372.                 forbidden = (s == cups.HTTP_FORBIDDEN)
  373.             else:
  374.                 return self._error (exc)
  375.         else:
  376.             return self._error (exc)
  377.  
  378.         # Not authorized.
  379.  
  380.         if (self._try_as_root and
  381.             self._user != 'root' and
  382.             (self._conn.thread.host[0] == '/' or forbidden)):
  383.             # This is a UNIX domain socket connection so we should
  384.             # not have needed a password (or it is not a UDS but
  385.             # we got an HTTP_FORBIDDEN response), and so the
  386.             # operation must not be something that the current
  387.             # user is authorised to do.  They need to try as root,
  388.             # and supply the password.  However, to get the right
  389.             # prompt, we need to try as root but with no password
  390.             # first.
  391.             debugprint ("Authentication: Try as root")
  392.             self._user = "root"
  393.             self._try_as_root = False
  394.             conn.reconnect (self._user,
  395.                             reply_handler=self._reconnect_reply,
  396.                             error_handler=self._reconnect_error)
  397.             # Don't submit the task until we've connected.
  398.             return
  399.  
  400.         if not self._auth_called:
  401.             # We aren't even getting a chance to supply credentials.
  402.             return self._error (exc)
  403.  
  404.         # Now reconnect and retry.
  405.         conn.reconnect (self._user,
  406.                         reply_handler=self._reconnect_reply,
  407.                         error_handler=self._reconnect_error)
  408.  
  409.     def auth_handler (self, prompt, conn, method=None, resource=None):
  410.         self._auth_called = True
  411.         if self._reconnected:
  412.             debugprint ("Supplying password after reconnection")
  413.             self._reconnected = False
  414.             conn.set_auth_info (self._use_password)
  415.             return
  416.  
  417.         self._reconnected = False
  418.         if not conn.prompt_allowed:
  419.             conn.set_auth_info (self._use_password)
  420.             return
  421.  
  422.         # If we've previously prompted, explain why we're prompting again.
  423.         if self._dialog_shown:
  424.             d = gtk.MessageDialog (self._conn.parent,
  425.                                    gtk.DIALOG_MODAL |
  426.                                    gtk.DIALOG_DESTROY_WITH_PARENT,
  427.                                    gtk.MESSAGE_ERROR,
  428.                                    gtk.BUTTONS_CLOSE,
  429.                                    _("Not authorized"))
  430.             d.format_secondary_text (_("The password may be incorrect."))
  431.             d.run ()
  432.             d.destroy ()
  433.  
  434.         op = None
  435.         if conn.semantic:
  436.             op = conn.semantic.current_operation ()
  437.  
  438.         if op == None:
  439.             d = authconn.AuthDialog (parent=conn.parent)
  440.         else:
  441.             title = _("Authentication (%s)") % op
  442.             d = authconn.AuthDialog (title=title,
  443.                                      parent=conn.parent)
  444.  
  445.         d.set_prompt (prompt)
  446.         d.set_auth_info ([self._user, ''])
  447.         d.field_grab_focus ('password')
  448.         d.set_keep_above (True)
  449.         d.show_all ()
  450.         d.connect ("response", self._on_auth_dialog_response)
  451.         self._dialog_shown = True
  452.  
  453.     def submit_task (self):
  454.         self._auth_called = False
  455.         self._conn.queue.put ((self._client_fn, self._client_args,
  456.                                self._client_kwds,
  457.                                self._client_reply_handler,
  458.                                
  459.                                # Use our own error and auth handlers.
  460.                                self.error_handler,
  461.                                self.auth_handler))
  462.  
  463.     def _on_auth_dialog_response (self, dialog, response):
  464.         (user, password) = dialog.get_auth_info ()
  465.         self._dialog = dialog
  466.         dialog.hide ()
  467.  
  468.         if (response == gtk.RESPONSE_CANCEL or
  469.             response == gtk.RESPONSE_DELETE_EVENT):
  470.             self._cancel = True
  471.             self._conn.set_auth_info ('')
  472.             debugprint ("Auth canceled")
  473.             return
  474.  
  475.         if user == self._user:
  476.             self._use_password = password
  477.             self._conn.set_auth_info (password)
  478.             debugprint ("Password supplied.")
  479.             return
  480.  
  481.         self._user = user
  482.         self._use_password = password
  483.         self._reconnect = True
  484.         self._conn.set_auth_info ('')
  485.         debugprint ("Will try as %s" % self._user)
  486.  
  487.     def _reconnect_reply (self, conn, result):
  488.         # A different username was given in the authentication dialog,
  489.         # so we've reconnected as that user.  Alternatively, the
  490.         # connection has failed and we're retrying.
  491.         debugprint ("Connected as %s" % self._user)
  492.         if self._client_fn != None:
  493.             self.submit_task ()
  494.  
  495.     def _reconnect_error (self, conn, exc):
  496.         debugprint ("Failed to connect as %s" % self._user)
  497.         if not self._conn.prompt_allowed:
  498.             self._error (exc)
  499.             return
  500.  
  501.         op = None
  502.         if conn.semantic:
  503.             op = conn.semantic.current_operation ()
  504.  
  505.         if op == None:
  506.             msg = _("CUPS server error")
  507.         else:
  508.             msg = _("CUPS server error (%s)") % op
  509.  
  510.         d = gtk.MessageDialog (self._conn.parent,
  511.                                gtk.DIALOG_MODAL |
  512.                                gtk.DIALOG_DESTROY_WITH_PARENT,
  513.                                gtk.MESSAGE_ERROR,
  514.                                gtk.BUTTONS_NONE,
  515.                                msg)
  516.  
  517.         if self._client_fn == None and type (exc) == RuntimeError:
  518.             # This was a connection failure.
  519.             message = 'service-error-service-unavailable'
  520.         elif type (exc) == cups.IPPError:
  521.             message = exc.args[1]
  522.         else:
  523.             message = repr (exc)
  524.  
  525.         d.format_secondary_text (_("There was an error during the "
  526.                                    "CUPS operation: '%s'." % message))
  527.         d.add_buttons (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
  528.                        _("Retry"), gtk.RESPONSE_OK)
  529.         d.set_default_response (gtk.RESPONSE_OK)
  530.         d.connect ("response", self._on_retry_server_error_response)
  531.         d.show ()
  532.  
  533.     def _on_retry_server_error_response (self, dialog, response):
  534.         dialog.destroy ()
  535.         if response == gtk.RESPONSE_OK:
  536.             self._conn.reconnect (self._conn.thread.user,
  537.                                   reply_handler=self._reconnect_reply,
  538.                                   error_handler=self._reconnect_error)
  539.         else:
  540.             self._error (cups.IPPError (0, _("Operation canceled")))
  541.  
  542.     def _error (self, exc):
  543.         if self._client_error_handler:
  544.             self._client_error_handler (self._conn, exc)
  545.             self._destroy ()
  546.  
  547. ###
  548. ### The user-visible class.
  549. ###
  550. class IPPAuthConnection(IPPConnection):
  551.     def __init__ (self, reply_handler=None, error_handler=None,
  552.                   auth_handler=None, host=None, port=None, encryption=None,
  553.                   parent=None, try_as_root=True, prompt_allowed=True,
  554.                   semantic=None):
  555.         self.parent = parent
  556.         self.prompt_allowed = prompt_allowed
  557.         self.try_as_root = try_as_root
  558.         self.semantic = semantic
  559.  
  560.         # The "connect" operation.
  561.         op = _IPPAuthOperation (reply_handler, error_handler, self)
  562.         IPPConnection.__init__ (self, reply_handler=reply_handler,
  563.                                 error_handler=op.error_handler,
  564.                                 auth_handler=op.auth_handler, host=host,
  565.                                 port=port, encryption=encryption)
  566.  
  567.     def destroy (self):
  568.         del self.semantic
  569.         IPPConnection.destroy (self)
  570.  
  571.     def _call_function (self, fn, *args, **kwds):
  572.         reply_handler = error_handler = auth_handler = False
  573.         if kwds.has_key ("reply_handler"):
  574.             reply_handler = kwds["reply_handler"]
  575.             del kwds["reply_handler"]
  576.         if kwds.has_key ("error_handler"):
  577.             error_handler = kwds["error_handler"]
  578.             del kwds["error_handler"]
  579.         if kwds.has_key ("auth_handler"):
  580.             auth_handler = kwds["auth_handler"]
  581.             del kwds["auth_handler"]
  582.  
  583.         # Store enough information about the current operation to
  584.         # restart it if necessary.
  585.         op = _IPPAuthOperation (reply_handler, error_handler, self,
  586.                                 self.thread.user, fn, args, kwds)
  587.  
  588.         # Run the operation but use our own error and auth handlers.
  589.         op.submit_task ()
  590.  
  591. if __name__ == "__main__":
  592.     # Demo
  593.     import gtk
  594.     set_debugging (True)
  595.     gobject.threads_init ()
  596.     class UI:
  597.         def __init__ (self):
  598.             w = gtk.Window ()
  599.             w.connect ("destroy", self.destroy)
  600.             b = gtk.Button ("Connect")
  601.             b.connect ("clicked", self.connect_clicked)
  602.             vbox = gtk.VBox ()
  603.             vbox.pack_start (b)
  604.             w.add (vbox)
  605.             self.get_devices_button = gtk.Button ("Get Devices")
  606.             self.get_devices_button.connect ("clicked", self.get_devices)
  607.             self.get_devices_button.set_sensitive (False)
  608.             vbox.pack_start (self.get_devices_button)
  609.             self.conn = None
  610.             w.show_all ()
  611.  
  612.         def destroy (self, window):
  613.             try:
  614.                 self.conn.destroy ()
  615.             except AttributeError:
  616.                 pass
  617.  
  618.             gtk.main_quit ()
  619.  
  620.         def connect_clicked (self, button):
  621.             if self.conn:
  622.                 self.conn.destroy ()
  623.  
  624.             self.conn = IPPAuthConnection (reply_handler=self.connected,
  625.                                            error_handler=self.connect_failed)
  626.  
  627.         def connected (self, conn, result):
  628.             debugprint ("Success: %s" % result)
  629.             self.get_devices_button.set_sensitive (True)
  630.  
  631.         def connect_failed (self, conn, exc):
  632.             debugprint ("Exc %s" % exc)
  633.             self.get_devices_button.set_sensitive (False)
  634.             self.conn.destroy ()
  635.  
  636.         def get_devices (self, button):
  637.             button.set_sensitive (False)
  638.             debugprint ("Getting devices")
  639.             self.conn.getDevices (reply_handler=self.get_devices_reply,
  640.                                   error_handler=self.get_devices_error)
  641.  
  642.         def get_devices_reply (self, conn, result):
  643.             if conn != self.conn:
  644.                 debugprint ("Ignoring stale reply")
  645.                 return
  646.  
  647.             debugprint ("Got devices: %s" % result)
  648.             self.get_devices_button.set_sensitive (True)
  649.  
  650.         def get_devices_error (self, conn, exc):
  651.             if conn != self.conn:
  652.                 debugprint ("Ignoring stale error")
  653.                 return
  654.  
  655.             debugprint ("Error getting devices: %s" % exc)
  656.             self.get_devices_button.set_sensitive (True)
  657.  
  658.     UI ()
  659.     gtk.main ()
  660.