Back to index

system-config-printer  1.3.9+20120706
asyncipp.py
Go to the documentation of this file.
00001 #!/usr/bin/python
00002 
00003 ## Copyright (C) 2007, 2008, 2009, 2010, 2011 Red Hat, Inc.
00004 ## Copyright (C) 2008 Novell, Inc.
00005 ## Author: Tim Waugh <twaugh@redhat.com>
00006 
00007 ## This program is free software; you can redistribute it and/or modify
00008 ## it under the terms of the GNU General Public License as published by
00009 ## the Free Software Foundation; either version 2 of the License, or
00010 ## (at your option) any later version.
00011 
00012 ## This program is distributed in the hope that it will be useful,
00013 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
00014 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00015 ## GNU General Public License for more details.
00016 
00017 ## You should have received a copy of the GNU General Public License
00018 ## along with this program; if not, write to the Free Software
00019 ## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
00020 
00021 import threading
00022 import config
00023 import cups
00024 import gobject
00025 import gtk
00026 import Queue
00027 
00028 cups.require ("1.9.60")
00029 
00030 import authconn
00031 from debug import *
00032 import debug
00033 from gettext import gettext as _
00034 
00035 ######
00036 ###### An asynchronous libcups API using IPP with a separate worker
00037 ###### thread.
00038 ######
00039 
00040 ###
00041 ### This is the worker thread.
00042 ###
00043 class _IPPConnectionThread(threading.Thread):
00044     def __init__ (self, queue, conn, reply_handler=None, error_handler=None,
00045                   auth_handler=None, user=None, host=None, port=None,
00046                   encryption=None):
00047                   
00048         threading.Thread.__init__ (self)
00049         self.setDaemon (True)
00050         self._queue = queue
00051         self._conn = conn
00052         self.host = host
00053         self.port = port
00054         self._encryption = encryption
00055         self._reply_handler = reply_handler
00056         self._error_handler = error_handler
00057         self._auth_handler = auth_handler
00058         self._auth_queue = Queue.Queue (1)
00059         self.user = user
00060         self._destroyed = False
00061         debugprint ("+%s" % self)
00062 
00063     def __del__ (self):
00064         debug.debugprint ("-%s" % self)
00065 
00066     def set_auth_info (self, password):
00067         self._auth_queue.put (password)
00068 
00069     def run (self):
00070         if self.host == None:
00071             self.host = cups.getServer ()
00072         if self.port == None:
00073             self.port = cups.getPort ()
00074         if self._encryption == None:
00075             self._encryption = cups.getEncryption ()
00076 
00077         if self.user:
00078             cups.setUser (self.user)
00079         else:
00080             self.user = cups.getUser ()
00081 
00082         cups.setPasswordCB2 (self._auth)
00083 
00084         try:
00085             conn = cups.Connection (host=self.host,
00086                                     port=self.port,
00087                                     encryption=self._encryption)
00088             self._reply (None)
00089         except RuntimeError, e:
00090             conn = None
00091             self._error (e)
00092 
00093         while True:
00094             # Wait to find out what operation to try.
00095             debugprint ("Awaiting further instructions")
00096             self.idle = self._queue.empty ()
00097             item = self._queue.get ()
00098             debugprint ("Next task: %s" % repr (item))
00099             if item == None:
00100                 # Our signal to quit.
00101                 self._queue.task_done ()
00102                 break
00103 
00104             self.idle = False
00105             (fn, args, kwds, rh, eh, ah) = item
00106             if rh != False:
00107                 self._reply_handler = rh
00108             if eh != False:
00109                 self._error_handler = eh
00110             if ah != False:
00111                 self._auth_handler = ah
00112 
00113             if fn == True:
00114                 # Our signal to change user and reconnect.
00115                 self.user = args[0]
00116                 cups.setUser (self.user)
00117                 debugprint ("Set user=%s; reconnecting..." % self.user)
00118                 cups.setPasswordCB2 (self._auth)
00119 
00120                 try:
00121                     conn = cups.Connection (host=self.host,
00122                                             port=self.port,
00123                                             encryption=self._encryption)
00124                     debugprint ("...reconnected")
00125 
00126                     self._queue.task_done ()
00127                     self._reply (None)
00128                 except RuntimeError, e:
00129                     debugprint ("...failed")
00130                     self._queue.task_done ()
00131                     self._error (e)
00132 
00133                 continue
00134 
00135             # Normal IPP operation.  Try to perform it.
00136             try:
00137                 debugprint ("Call %s" % fn)
00138                 result = fn (conn, *args, **kwds)
00139                 if fn == cups.Connection.adminGetServerSettings.__call__:
00140                     # Special case for a rubbish bit of API.
00141                     if result == {}:
00142                         # Authentication failed, but we aren't told that.
00143                         raise cups.IPPError (cups.IPP_NOT_AUTHORIZED, '')
00144 
00145                 debugprint ("...success")
00146                 self._reply (result)
00147             except Exception, e:
00148                 debugprint ("...failure")
00149                 self._error (e)
00150 
00151             self._queue.task_done ()
00152 
00153         debugprint ("Thread exiting")
00154         self._destroyed = True
00155         del self._conn # already destroyed
00156         del self._reply_handler
00157         del self._error_handler
00158         del self._auth_handler
00159         del self._queue
00160         del self._auth_queue
00161         del conn
00162 
00163         cups.setPasswordCB2 (None)
00164 
00165     def _auth (self, prompt, conn=None, method=None, resource=None):
00166         def prompt_auth (prompt):
00167             gtk.gdk.threads_enter ()
00168             if conn == None:
00169                 self._auth_handler (prompt, self._conn)
00170             else:
00171                 self._auth_handler (prompt, self._conn, method, resource)
00172 
00173             gtk.gdk.threads_leave ()
00174             return False
00175 
00176         if self._auth_handler == None:
00177             return ""
00178 
00179         gobject.idle_add (prompt_auth, prompt)
00180         password = self._auth_queue.get ()
00181         return password
00182 
00183     def _reply (self, result):
00184         def send_reply (handler, result):
00185             if not self._destroyed:
00186                 gtk.gdk.threads_enter ()
00187                 handler (self._conn, result)
00188                 gtk.gdk.threads_leave ()
00189             return False
00190 
00191         if not self._destroyed and self._reply_handler:
00192             gobject.idle_add (send_reply, self._reply_handler, result)
00193 
00194     def _error (self, exc):
00195         def send_error (handler, exc):
00196             if not self._destroyed:
00197                 gtk.gdk.threads_enter ()
00198                 handler (self._conn, exc)
00199                 gtk.gdk.threads_leave ()
00200             return False
00201 
00202         if not self._destroyed and self._error_handler:
00203             debugprint ("Add %s to idle" % self._error_handler)
00204             gobject.idle_add (send_error, self._error_handler, exc)
00205 
00206 ###
00207 ### This is the user-visible class.  Although it does not inherit from
00208 ### cups.Connection it implements the same functions.
00209 ###
00210 class IPPConnection:
00211     """
00212     This class starts a new thread to handle IPP operations.
00213 
00214     Each IPP operation method takes optional reply_handler,
00215     error_handler and auth_handler parameters.
00216 
00217     If an operation requires a password to proceed, the auth_handler
00218     function will be called.  The operation will continue once
00219     set_auth_info (in this class) is called.
00220 
00221     Once the operation has finished either reply_handler or
00222     error_handler will be called.
00223     """
00224 
00225     def __init__ (self, reply_handler=None, error_handler=None,
00226                   auth_handler=None, user=None, host=None, port=None,
00227                   encryption=None, parent=None):
00228         debugprint ("New IPPConnection")
00229         self._parent = parent
00230         self.queue = Queue.Queue ()
00231         self.thread = _IPPConnectionThread (self.queue, self,
00232                                             reply_handler=reply_handler,
00233                                             error_handler=error_handler,
00234                                             auth_handler=auth_handler,
00235                                             user=user, host=host, port=port,
00236                                             encryption=encryption)
00237         self.thread.start ()
00238 
00239         methodtype = type (cups.Connection.getPrinters)
00240         bindings = []
00241         for fname in dir (cups.Connection):
00242             if fname[0] == ' ':
00243                 continue
00244             fn = getattr (cups.Connection, fname)
00245             if type (fn) != methodtype:
00246                 continue
00247             setattr (self, fname, self._make_binding (fn))
00248             bindings.append (fname)
00249 
00250         self.bindings = bindings
00251         debugprint ("+%s" % self)
00252 
00253     def __del__ (self):
00254         debug.debugprint ("-%s" % self)
00255 
00256     def destroy (self):
00257         debugprint ("DESTROY: %s" % self)
00258         for binding in self.bindings:
00259             delattr (self, binding)
00260 
00261         if self.thread.isAlive ():
00262             gobject.timeout_add_seconds (1, self._reap_thread)
00263 
00264     def _reap_thread (self):
00265         if self.thread.idle:
00266             debugprint ("Putting None on the task queue")
00267             self.queue.put (None)
00268             self.queue.join ()
00269             return False
00270 
00271         debugprint ("Thread %s still processing tasks" % self.thread)
00272         return True
00273 
00274     def set_auth_info (self, password):
00275         """Call this from your auth_handler function."""
00276         self.thread.set_auth_info (password)
00277 
00278     def reconnect (self, user, reply_handler=None, error_handler=None):
00279         debugprint ("Reconnect...")
00280         self.queue.put ((True, (user,), {},
00281                          reply_handler, error_handler, False))
00282 
00283     def _make_binding (self, fn):
00284         return lambda *args, **kwds: self._call_function (fn, *args, **kwds)
00285 
00286     def _call_function (self, fn, *args, **kwds):
00287         reply_handler = error_handler = auth_handler = False
00288         if kwds.has_key ("reply_handler"):
00289             reply_handler = kwds["reply_handler"]
00290             del kwds["reply_handler"]
00291         if kwds.has_key ("error_handler"):
00292             error_handler = kwds["error_handler"]
00293             del kwds["error_handler"]
00294         if kwds.has_key ("auth_handler"):
00295             auth_handler = kwds["auth_handler"]
00296             del kwds["auth_handler"]
00297 
00298         self.queue.put ((fn, args, kwds,
00299                          reply_handler, error_handler, auth_handler))
00300 
00301 ######
00302 ###### An asynchronous libcups API with graphical authentication and
00303 ###### retrying.
00304 ######
00305 
00306 ###
00307 ### A class to take care of an individual operation.
00308 ###
00309 class _IPPAuthOperation:
00310     def __init__ (self, reply_handler, error_handler, conn,
00311                   user=None, fn=None, args=None, kwds=None):
00312         self._auth_called = False
00313         self._dialog_shown = False
00314         self._use_password = ''
00315         self._cancel = False
00316         self._reconnect = False
00317         self._reconnected = False
00318         self._user = user
00319         self._conn = conn
00320         self._try_as_root = self._conn.try_as_root
00321         self._client_fn = fn
00322         self._client_args = args
00323         self._client_kwds = kwds
00324         self._client_reply_handler = reply_handler
00325         self._client_error_handler = error_handler
00326 
00327         debugprint ("+%s" % self)
00328 
00329     def __del__ (self):
00330         debug.debugprint ("-%s" % self)
00331 
00332     def _destroy (self):
00333         del self._conn
00334         del self._client_fn
00335         del self._client_args
00336         del self._client_kwds
00337         del self._client_reply_handler
00338         del self._client_error_handler
00339 
00340     def error_handler (self, conn, exc):
00341         if self._client_fn == None:
00342             # This is the initial "connection" operation, or a
00343             # subsequent reconnection attempt.
00344             debugprint ("Connection/reconnection failed")
00345             return self._reconnect_error (conn, exc)
00346 
00347         if self._cancel:
00348             return self._error (exc)
00349 
00350         if self._reconnect:
00351             self._reconnect = False
00352             self._reconnected = True
00353             conn.reconnect (self._user,
00354                             reply_handler=self._reconnect_reply,
00355                             error_handler=self._reconnect_error)
00356             return
00357 
00358         forbidden = False
00359         if type (exc) == cups.IPPError:
00360             (e, m) = exc.args
00361             if (e == cups.IPP_NOT_AUTHORIZED or
00362                 e == cups.IPP_FORBIDDEN or
00363                 e == cups.IPP_AUTHENTICATION_CANCELED):
00364                 forbidden = (e == cups.IPP_FORBIDDEN)
00365             elif e == cups.IPP_SERVICE_UNAVAILABLE:
00366                 return self._reconnect_error (conn, exc)
00367             else:
00368                 return self._error (exc)
00369         elif type (exc) == cups.HTTPError:
00370             (s,) = exc.args
00371             if (s == cups.HTTP_UNAUTHORIZED or
00372                 s == cups.HTTP_FORBIDDEN):
00373                 forbidden = (s == cups.HTTP_FORBIDDEN)
00374             else:
00375                 return self._error (exc)
00376         else:
00377             return self._error (exc)
00378 
00379         # Not authorized.
00380 
00381         if (self._try_as_root and
00382             self._user != 'root' and
00383             (self._conn.thread.host[0] == '/' or forbidden)):
00384             # This is a UNIX domain socket connection so we should
00385             # not have needed a password (or it is not a UDS but
00386             # we got an HTTP_FORBIDDEN response), and so the
00387             # operation must not be something that the current
00388             # user is authorised to do.  They need to try as root,
00389             # and supply the password.  However, to get the right
00390             # prompt, we need to try as root but with no password
00391             # first.
00392             debugprint ("Authentication: Try as root")
00393             self._user = "root"
00394             conn.reconnect (self._user,
00395                             reply_handler=self._reconnect_reply,
00396                             error_handler=self._reconnect_error)
00397             # Don't submit the task until we've connected.
00398             return
00399 
00400         if not self._auth_called:
00401             # We aren't even getting a chance to supply credentials.
00402             return self._error (exc)
00403 
00404         # Now reconnect and retry.
00405         host = conn.thread.host
00406         port = conn.thread.port
00407         authconn.global_authinfocache.remove_auth_info (host=host,
00408                                                         port=port)
00409         self._use_password = ''
00410         conn.reconnect (self._user,
00411                         reply_handler=self._reconnect_reply,
00412                         error_handler=self._reconnect_error)
00413 
00414     def auth_handler (self, prompt, conn, method=None, resource=None):
00415         if self._auth_called == False:
00416             if self._user == None:
00417                 self._user = cups.getUser()
00418             if self._user:
00419                 host = conn.thread.host
00420                 port = conn.thread.port
00421                 creds = authconn.global_authinfocache.lookup_auth_info (host=host,
00422                                                                         port=port)
00423                 if creds:
00424                     if creds[0] == self._user:
00425                         self._use_password = creds[1]
00426                         self._reconnected = True
00427                     del creds
00428         else:
00429             host = conn.thread.host
00430             port = conn.thread.port
00431             authconn.global_authinfocache.remove_auth_info (host=host,
00432                                                             port=port)
00433             self._use_password = ''
00434 
00435         self._auth_called = True
00436         if self._reconnected:
00437             debugprint ("Supplying password after reconnection")
00438             self._reconnected = False
00439             conn.set_auth_info (self._use_password)
00440             return
00441 
00442         self._reconnected = False
00443         if not conn.prompt_allowed:
00444             conn.set_auth_info (self._use_password)
00445             return
00446 
00447         # If we've previously prompted, explain why we're prompting again.
00448         if self._dialog_shown:
00449             d = gtk.MessageDialog (self._conn.parent,
00450                                    gtk.DIALOG_MODAL |
00451                                    gtk.DIALOG_DESTROY_WITH_PARENT,
00452                                    gtk.MESSAGE_ERROR,
00453                                    gtk.BUTTONS_CLOSE,
00454                                    _("Not authorized"))
00455             d.format_secondary_text (_("The password may be incorrect."))
00456             d.run ()
00457             d.destroy ()
00458 
00459         op = None
00460         if conn.semantic:
00461             op = conn.semantic.current_operation ()
00462 
00463         if op == None:
00464             d = authconn.AuthDialog (parent=conn.parent)
00465         else:
00466             title = _("Authentication (%s)") % op
00467             d = authconn.AuthDialog (title=title,
00468                                      parent=conn.parent)
00469 
00470         d.set_prompt (prompt)
00471         if self._user == None:
00472             self._user = cups.getUser()
00473         d.set_auth_info ([self._user, ''])
00474         d.field_grab_focus ('password')
00475         d.set_keep_above (True)
00476         d.show_all ()
00477         d.connect ("response", self._on_auth_dialog_response)
00478         self._dialog_shown = True
00479 
00480     def submit_task (self):
00481         self._auth_called = False
00482         self._conn.queue.put ((self._client_fn, self._client_args,
00483                                self._client_kwds,
00484                                self._client_reply_handler,
00485                                
00486                                # Use our own error and auth handlers.
00487                                self.error_handler,
00488                                self.auth_handler))
00489 
00490     def _on_auth_dialog_response (self, dialog, response):
00491         (user, password) = dialog.get_auth_info ()
00492         if user == '':
00493             user = self._user;
00494         authconn.global_authinfocache.cache_auth_info ((user,
00495                                                         password),
00496                                                        host=self._conn.thread.host,
00497                                                        port=self._conn.thread.port)
00498         self._dialog = dialog
00499         dialog.hide ()
00500 
00501         if (response == gtk.RESPONSE_CANCEL or
00502             response == gtk.RESPONSE_DELETE_EVENT):
00503             self._cancel = True
00504             self._conn.set_auth_info ('')
00505             authconn.global_authinfocache.remove_auth_info (host=self._conn.thread.host,
00506                                                             port=self._conn.thread.port)
00507             debugprint ("Auth canceled")
00508             return
00509 
00510         if user == self._user:
00511             self._use_password = password
00512             self._conn.set_auth_info (password)
00513             debugprint ("Password supplied.")
00514             return
00515 
00516         self._user = user
00517         self._use_password = password
00518         self._reconnect = True
00519         self._conn.set_auth_info ('')
00520         debugprint ("Will try as %s" % self._user)
00521 
00522     def _reconnect_reply (self, conn, result):
00523         # A different username was given in the authentication dialog,
00524         # so we've reconnected as that user.  Alternatively, the
00525         # connection has failed and we're retrying.
00526         debugprint ("Connected as %s" % self._user)
00527         if self._client_fn != None:
00528             self.submit_task ()
00529 
00530     def _reconnect_error (self, conn, exc):
00531         debugprint ("Failed to connect as %s" % self._user)
00532         if not self._conn.prompt_allowed:
00533             self._error (exc)
00534             return
00535 
00536         op = None
00537         if conn.semantic:
00538             op = conn.semantic.current_operation ()
00539 
00540         if op == None:
00541             msg = _("CUPS server error")
00542         else:
00543             msg = _("CUPS server error (%s)") % op
00544 
00545         d = gtk.MessageDialog (self._conn.parent,
00546                                gtk.DIALOG_MODAL |
00547                                gtk.DIALOG_DESTROY_WITH_PARENT,
00548                                gtk.MESSAGE_ERROR,
00549                                gtk.BUTTONS_NONE,
00550                                msg)
00551 
00552         if self._client_fn == None and type (exc) == RuntimeError:
00553             # This was a connection failure.
00554             message = 'service-error-service-unavailable'
00555         elif type (exc) == cups.IPPError:
00556             message = exc.args[1]
00557         else:
00558             message = repr (exc)
00559 
00560         d.format_secondary_text (_("There was an error during the "
00561                                    "CUPS operation: '%s'." % message))
00562         d.add_buttons (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
00563                        _("Retry"), gtk.RESPONSE_OK)
00564         d.set_default_response (gtk.RESPONSE_OK)
00565         d.connect ("response", self._on_retry_server_error_response)
00566         d.show ()
00567 
00568     def _on_retry_server_error_response (self, dialog, response):
00569         dialog.destroy ()
00570         if response == gtk.RESPONSE_OK:
00571             self._conn.reconnect (self._conn.thread.user,
00572                                   reply_handler=self._reconnect_reply,
00573                                   error_handler=self._reconnect_error)
00574         else:
00575             self._error (cups.IPPError (0, _("Operation canceled")))
00576 
00577     def _error (self, exc):
00578         if self._client_error_handler:
00579             self._client_error_handler (self._conn, exc)
00580             self._destroy ()
00581 
00582 ###
00583 ### The user-visible class.
00584 ###
00585 class IPPAuthConnection(IPPConnection):
00586     def __init__ (self, reply_handler=None, error_handler=None,
00587                   auth_handler=None, host=None, port=None, encryption=None,
00588                   parent=None, try_as_root=True, prompt_allowed=True,
00589                   semantic=None):
00590         self.parent = parent
00591         self.prompt_allowed = prompt_allowed
00592         self.try_as_root = try_as_root
00593         self.semantic = semantic
00594 
00595         user = None
00596         creds = authconn.global_authinfocache.lookup_auth_info (host=host,
00597                                                                 port=port)
00598         if creds:
00599             if creds[0] != 'root' or try_as_root:
00600                 user = creds[0]
00601             del creds
00602 
00603         # The "connect" operation.
00604         op = _IPPAuthOperation (reply_handler, error_handler, self)
00605         IPPConnection.__init__ (self, reply_handler=reply_handler,
00606                                 error_handler=op.error_handler,
00607                                 auth_handler=op.auth_handler, user=user,
00608                                 host=host, port=port, encryption=encryption)
00609 
00610     def destroy (self):
00611         self.semantic = None
00612         IPPConnection.destroy (self)
00613 
00614     def _call_function (self, fn, *args, **kwds):
00615         reply_handler = error_handler = auth_handler = False
00616         if kwds.has_key ("reply_handler"):
00617             reply_handler = kwds["reply_handler"]
00618             del kwds["reply_handler"]
00619         if kwds.has_key ("error_handler"):
00620             error_handler = kwds["error_handler"]
00621             del kwds["error_handler"]
00622         if kwds.has_key ("auth_handler"):
00623             auth_handler = kwds["auth_handler"]
00624             del kwds["auth_handler"]
00625 
00626         # Store enough information about the current operation to
00627         # restart it if necessary.
00628         op = _IPPAuthOperation (reply_handler, error_handler, self,
00629                                 self.thread.user, fn, args, kwds)
00630 
00631         # Run the operation but use our own error and auth handlers.
00632         op.submit_task ()
00633 
00634 if __name__ == "__main__":
00635     # Demo
00636     import gtk
00637     set_debugging (True)
00638     gobject.threads_init ()
00639     class UI:
00640         def __init__ (self):
00641             w = gtk.Window ()
00642             w.connect ("destroy", self.destroy)
00643             b = gtk.Button ("Connect")
00644             b.connect ("clicked", self.connect_clicked)
00645             vbox = gtk.VBox ()
00646             vbox.pack_start (b)
00647             w.add (vbox)
00648             self.get_devices_button = gtk.Button ("Get Devices")
00649             self.get_devices_button.connect ("clicked", self.get_devices)
00650             self.get_devices_button.set_sensitive (False)
00651             vbox.pack_start (self.get_devices_button)
00652             self.conn = None
00653             w.show_all ()
00654 
00655         def destroy (self, window):
00656             try:
00657                 self.conn.destroy ()
00658             except AttributeError:
00659                 pass
00660 
00661             gtk.main_quit ()
00662 
00663         def connect_clicked (self, button):
00664             if self.conn:
00665                 self.conn.destroy ()
00666 
00667             self.conn = IPPAuthConnection (reply_handler=self.connected,
00668                                            error_handler=self.connect_failed)
00669 
00670         def connected (self, conn, result):
00671             debugprint ("Success: %s" % result)
00672             self.get_devices_button.set_sensitive (True)
00673 
00674         def connect_failed (self, conn, exc):
00675             debugprint ("Exc %s" % exc)
00676             self.get_devices_button.set_sensitive (False)
00677             self.conn.destroy ()
00678 
00679         def get_devices (self, button):
00680             button.set_sensitive (False)
00681             debugprint ("Getting devices")
00682             self.conn.getDevices (reply_handler=self.get_devices_reply,
00683                                   error_handler=self.get_devices_error)
00684 
00685         def get_devices_reply (self, conn, result):
00686             if conn != self.conn:
00687                 debugprint ("Ignoring stale reply")
00688                 return
00689 
00690             debugprint ("Got devices: %s" % result)
00691             self.get_devices_button.set_sensitive (True)
00692 
00693         def get_devices_error (self, conn, exc):
00694             if conn != self.conn:
00695                 debugprint ("Ignoring stale error")
00696                 return
00697 
00698             debugprint ("Error getting devices: %s" % exc)
00699             self.get_devices_button.set_sensitive (True)
00700 
00701     UI ()
00702     gtk.main ()