source: trunk/debathena/config/lightdm-config/debian/debathena-lightdm-greeter @ 25558

Revision 25558, 21.0 KB checked in by jdreed, 12 years ago (diff)
In lightdm-config: * Explicitly set the text and id columns of the ComboBox to work around GtkBuilder bugs * The greeter needs to keep track of whether the user hit Cancel or not in LightDM 1.0 (oneiric)
  • Property svn:executable set to *
RevLine 
[25494]1#!/usr/bin/env python
2#
3
4from gi.repository import GObject
5from gi.repository import GLib
[25539]6from gi.repository import Gio
[25494]7from gi.repository import Gtk
8from gi.repository import Gdk
9from gi.repository import GdkPixbuf
10from gi.repository import LightDM
11
12import sys
13import platform
14import subprocess
15import pwd
16import time
[25500]17import os.path
18from optparse import OptionParser
[25494]19
20# TODO: ConfigParser
21MIN_UID=1
[25539]22NOLOGIN_FILE="/var/run/athena-nologin"
[25494]23UI_FILE="/usr/share/debathena-lightdm-config/debathena-lightdm-greeter.ui"
24BG_IMG_FILE="/usr/share/debathena-lightdm-config/background.jpg"
[25501]25DEBATHENA_LOGO_FILES=["/usr/share/debathena-lightdm-config/debathena.png",
26                      "/usr/share/debathena-lightdm-config/debathena1.png",
27                      "/usr/share/debathena-lightdm-config/debathena2.png",
28                      "/usr/share/debathena-lightdm-config/debathena3.png",
29                      "/usr/share/debathena-lightdm-config/debathena4.png",
30                      "/usr/share/debathena-lightdm-config/debathena5.png",
31                      "/usr/share/debathena-lightdm-config/debathena6.png",
32                      "/usr/share/debathena-lightdm-config/debathena7.png",
33                      "/usr/share/debathena-lightdm-config/debathena8.png"]
[25556]34KIOSK_LAUNCH_CMD="/usr/lib/debathena-kiosk/lightdm-launch-kiosk"
[25494]35
36class DebathenaGreeter:
37    animation_loop_frames = 300
38   
39   
40    def _debug(self, *args):
41        if self.debugMode:
42            if type(args[0]) is str and len(args) > 1:
43                print >> sys.stderr, "D: " + args[0], args[1:]
44            else:
[25500]45                print >> sys.stderr, "D: ", args[0]
[25494]46
[25500]47    def __init__(self, options):
48        self.debugMode = options.debug
[25494]49        self.timePedantry = True
50
51        # Set up and connect to the greeter
52        self.greeter = LightDM.Greeter()
53        self.greeter.connect("authentication-complete",
54                             self.cbAuthenticationComplete)
55        self.greeter.connect("show-message", self.cbShowMessage)
56        self.greeter.connect("show-prompt", self.cbShowPrompt)
57        self.greeter.connect_sync()
58
59        # Gtk signal handlers
60        handlers = {
61            "login_cb": self.cbLogin,
62            "cancel_cb": self.cancelLogin,
63            "kpEvent": self.cbKeyPress,
64            "power_cb": self.showPowerDialog,
65            "access_cb": self.showAccessDialog,
[25556]66            "browse_cb": self.spawnBrowser,
[25494]67        }
[25558]68
69        # Sigh.  Pre lightdm-1.1, cancel_authentication() calls the
70        # authentication-complete callback, so when we're in that
71        # callback, we need to know if we cancelled, or if we typed
72        # the password wrong.  The lightdm daemon does in fact know
73        # the difference (return codes of 7 or 10), but gir has no way
74        # to get that info, AFAICT
75        # So beingCancelled is set to True when the user hits Cancel
76        # (or Esc) and set back to False in the authentication-complete
77        # callback or before we try and send anything else to the greeter
78        # This only controls UI, and has no effect on whether LightDM
79        # thinks the authentication process is being cancelled.
80        self.beingCancelled=False
[25494]81 
82        # Save the screen size for various window operations
83        defaultScreen = Gdk.Screen.get_default()
84        self.screenSize = (defaultScreen.width(), defaultScreen.height())
[25500]85       
86        self.get_workstation_information()
[25494]87
88        # Load the UI and get objects we care about
89        self.builder = Gtk.Builder()
[25500]90        try:
91            self.builder.add_from_file(options.ui_file)
92        except GLib.GError, e:
93            print >> sys.stderr, "FATAL: Unable to load UI: ", e
94            sys.exit(-1)
[25494]95
96        # The login window
97        self.winLogin = self.builder.get_object("winLogin")
98        # A box containing the prompt label, entry, and a spinner
99        self.prompt_box = self.builder.get_object("boxPrompt")
100        self.prompt_label = self.builder.get_object("lblPrompt")
101        self.prompt_entry = self.builder.get_object("entryPrompt")
102        self.loginSpinner = self.builder.get_object("loginSpinner")
103        # A label where we display messages received from the greeter
104        self.message_label = self.builder.get_object("lblMessage")
105        # The owl
106        self.imgDebathena = self.builder.get_object("imgDebathena")
107        # The workstation's hostname
108        lblHostname = self.builder.get_object("lblHostname")
109        lblHostname.set_text(LightDM.get_hostname())
110        # The buttons
111        self.btnCancel = self.builder.get_object("btnCancel")
112        self.btnLogin = self.builder.get_object("btnLogin")
113        # The session combo box
114        self.cmbSession = self.builder.get_object("cmbSession")
115        self.sessionBox = self.builder.get_object("sessionBox")
[25558]116        # Sigh.  Needed for Oneiric.  No-op on Precise
117        # See GNOME Bugzilla #650369 and 653579
118        # GtkBuilder calls g_object_new, not gtk_combo_box_text_new()
119        # so the properties don't get set.
120        self.cmbSession.set_entry_text_column(0);
121        self.cmbSession.set_id_column(1);
[25494]122        for s in LightDM.get_sessions():
123            self.cmbSession.append(s.get_key(), s.get_name())
124        # Select the first session
125        # TODO: Select the configured default session or the user's session
126        self.cmbSession.set_active(0)
127
[25500]128        self.powerDlg = self.builder.get_object("powerDialog")
129        # LightDM checks with PolKit for the various "get_can_foo()" functions
130        self.builder.get_object("rbShutdown").set_sensitive(LightDM.get_can_shutdown())
131        self.builder.get_object("rbReboot").set_sensitive(LightDM.get_can_restart())
132        # We don't allow suspend/hibernate on cluster
133        self.builder.get_object("rbHibernate").set_sensitive(LightDM.get_can_hibernate() and self.metapackage != "debathena-cluster")
134        self.builder.get_object("rbSuspend").set_sensitive(LightDM.get_can_suspend() and self.metapackage != "debathena-cluster")
135       
[25539]136        self.loginNotebook = self.builder.get_object("notebook1")
137
[25494]138        # Scaling factor for smaller displays
[25539]139        logoScale = 0.75 if self.screenSize[1] <= 768 else 1.0
[25494]140        self.animate = self.setup_owl(logoScale)
141       
142        self.winLogin.set_position(Gtk.WindowPosition.CENTER)
143        self.winLogin.show()
144        self.initBackgroundWindow()
145        self.initPanelWindow()
146        self.initBrandingWindow()
147        # Connect Gtk+ signal handlers
148        self.builder.connect_signals(handlers)
149        # GNOME 3 turns off button images by default.  Turn it on
150        # for the "Panel" window
151        s = Gtk.Settings.get_default()
152        s.set_property('gtk-button-images', True)
153        # Set a cursor for the root window, otherwise there isn't one
154        rw = Gdk.get_default_root_window()
155        rw.set_cursor(Gdk.Cursor(Gdk.CursorType.LEFT_PTR))
[25539]156        self.noLoginMonitor = Gio.File.new_for_path(NOLOGIN_FILE).monitor_file(Gio.FileMonitorFlags.NONE, None)
157        self.noLoginMonitor.connect("changed", self._file_changed)
[25556]158
159        if not os.path.exists(KIOSK_LAUNCH_CMD):
160            self.builder.get_object("btnBrowse").hide()
[25494]161        # Setup the login window for first login
162        self.resetLoginWindow()
163
164    def initBackgroundWindow(self):
165        # The background image
166        self.winBg = self.builder.get_object("winBg")
167        self.imgBg = self.builder.get_object("imgBg")
[25539]168        bg_pixbuf = GdkPixbuf.Pixbuf.new_from_file(BG_IMG_FILE)
169        bg_scaled = bg_pixbuf.scale_simple(self.screenSize[0], self.screenSize[1], GdkPixbuf.InterpType.BILINEAR)
170        self.imgBg.set_from_pixbuf(bg_scaled)
[25494]171        self.winBg.show_all()
172
173    def initPanelWindow(self):
174        # A window that looks like the GNOME "panel" at the top of the screen
175        self.winPanel = self.builder.get_object("winPanel")
176        self.lblTime = self.builder.get_object("lblTime")
177        self.winPanel.set_gravity(Gdk.Gravity.NORTH_WEST)
178        self.winPanel.move(0,0)
179        self.winPanel.set_size_request(self.screenSize[0], 2)
180        self.winPanel.show_all()
181
182    def initBrandingWindow(self):
183        # The "branding window", in the bottom right
184        winBranding = self.builder.get_object("winBranding")
185        lblBranding = self.builder.get_object("lblBranding")
186        arch = platform.machine()
187        if arch != "x86_64":
188            arch = "<b>" + arch + "</b>"
189        # Possibly no longer needed, workaround for a Glade bug in Gtk+ 2
190        lblBranding.set_property('can_focus', False)
191        winBranding.set_property('can_focus', False)
[25500]192        lblBranding.set_markup(self.metapackage + "\n" + self.baseos + "\n" + arch)
[25494]193        winBranding.set_gravity(Gdk.Gravity.SOUTH_EAST)
194        width, height = winBranding.get_size()
195        winBranding.move(self.screenSize[0] - width, self.screenSize[1] - height)
196        winBranding.show_all()
197
198    def showPowerDialog(self, widget):
[25500]199        if self.powerDlg.run() == Gtk.ResponseType.OK:
200            # Just do the action.  The relevant buttons should be
201            # greyed out for things we can't do.  LightDM will
202            # check with ConsoleKit anyway
203            try:
204                if self.builder.get_object("rbShutdown").get_active():
205                    LightDM.shutdown()
206                elif self.builder.get_object("rbReboot").get_active():
207                    LightDM.restart()
208                elif self.builder.get_object("rbHiberate").get_active():
209                    LightDM.hibernate()
210                elif self.builder.get_object("rbSuspend").get_active():
211                    LightDM.suspend()
212            except Glib.Gerror, e:
213                self.errDialog(str(e))
214        self.powerDlg.hide()
[25494]215
216    def showAccessDialog(self, widget):
217        pass
218
[25539]219    def _file_changed(self, monitor, file1, file2, evt_type):
220        if evt_type == Gio.FileMonitorEvent.CREATED:
221            self.loginNotebook.set_current_page(1)
222            self.builder.get_object("lblUpdTime").set_text("Update started at %s" % (time.strftime("%Y-%m-%d %H:%M")))
223        if evt_type == Gio.FileMonitorEvent.DELETED:
224            self.loginNotebook.set_current_page(0)
225
[25494]226    # Update the time in the "panel"
227    def updateTime(self):
228        timeFmt="%a, %b %e %Y %l:%M" + ":%S" if self.timePedantry else ""
229        # every second counts
230        timeFmt=timeFmt + " %p"
231        self.lblTime.set_text(time.strftime(timeFmt, time.localtime(time.time())))
232        return True
233
234    # Reset the UI and prepare for a new login
235    def resetLoginWindow(self):
236        self.spin(False)
237        self.clearMessage()
238        self.btnCancel.hide()
239        self.sessionBox.hide()
240        self.prompted=False
241        self.prompt_label.set_text("")
242        self.prompt_entry.set_text("")
243        self.prompt_box.hide()
244        self.btnLogin.grab_focus()
245        # Because there's no WM, we need to focus the actual X window
246        Gdk.Window.focus(self.winLogin.get_window(), Gdk.CURRENT_TIME)
247
248    def getSelectedSession(self):
249        i = self.cmbSession.get_active_iter()
250        session_name = self.cmbSession.get_model().get_value(i, 1)
251        self._debug("selected session is " + session_name)
252        return session_name
253
254    def startOver(self):
255        self.greeter.cancel_authentication()
256        self.greeter.authenticate(None)
257
258    # LightDM Callbacks
259    # The workflow is this:
260    # - call .authenticate() with a username
261    # - lightdm responds with a prompt for password
262    # - call .respond with whatever the user provides
263    # - lightdm responds with authentication-complete
264    #   N.B. complete != successful
265    # - .cancel_authentication will cancel the authentication in progress
266    #   call .authenticate with a new username to restart it
267    #
268    # Calling .authenticate with None (or NULL in C) will cause lightdm
269    # to first prompt for a username, then a password.  This means two
270    # show-prompt callbacks and thus two .respond calls
271   
272    # This callback is called when the authentication process is
273    # complete.  "complete" means a username and password have been
274    # received, and PAM has done its thing.  "complete" does not
275    # mean "successful".
276    def cbAuthenticationComplete(self, greeter):
277        self.spin(False)
278        self._debug("cbAuthenticationComplete: received authentication-complete message")
279        if greeter.get_is_authenticated():
280            self.spin(True)
281            self._debug("Authentication was successful.")
282            session_name = self.getSelectedSession()
283            #FIXME: Make sure it's a valid session
284            self._debug("User has selected '%s' session" % (session_name))
285            if not greeter.start_session_sync(session_name):
286                self._debug("Failed to start session")
287                print >> sys.stderr, "Failed to start session"
[25558]288        elif not self.beingCancelled:
[25494]289            self._debug("Authentication failed.")
290            self.displayMessage("Authentication failed, please try again")
291            self.greeter.authenticate(None)
[25558]292        else:
293            self.beingCancelled=False
294            self.resetLoginWindow()
[25494]295
296    # The show-prompt message is emitted when LightDM wants you to
297    # show a prompt to the user, and respond with the user's response.
298    # Currently, the prompts we care about are "login:" and
299    # "Password: " (yes, with the trailing space), which ask for the
300    # username and password respectively.  promptType is one of
301    # LightDM.PromptType.SECRET or LightDM.PromptType.QUESTION, which
302    # mean that the text of the user's response should or should not be
303    # masked/invisible, respectively.
304
305    def cbShowPrompt(self, greeter, text, promptType):
306        self._debug("cbShowPrompt: Received show-prompt message: ",
307                   text, promptType)
308        self.prompted=True
309        # Make things pretty
310        if text == "login:":
311            text = "Username: "
312        # Sanity check the username
313        currUser = self.greeter.get_authentication_user()
314        if currUser:
315            self._debug("Current user being authenticated is " + currUser)
316            # See if the user exists
317            try:
318                passwd=pwd.getpwnam(currUser)
319            except KeyError:
320                # Why are we not using the message label here?
321                # Because what will happen is that someone will quickly
322                # typo their username, and then type their password without
323                # looking at the screen, which would otherwise result in the
324                # window resetting after the first error, and then they end
325                # up typing their password into the username box.
[25541]326                self.errDialog("The username '%s' is invalid.\n\n(Tip: Please ensure you're typing your username in lowercase letters.\nDo not add '@mit.edu' or any other suffix to your username.)" % (currUser))
[25494]327                self.startOver()
328                return True
329            # There's probably a better way
330            if passwd.pw_uid < MIN_UID:
331                self.errDialog("Logging in as '%s' disallowed by configuation" % (currUser))
332                self.startOver()
333                return True
334
335        # Set the label to the value of the prompt
336        self.prompt_label.set_text(text)
337        # clear the entry and get focus
338        self.prompt_entry.set_text("")
339        self.prompt_entry.set_sensitive(True)
340        self.prompt_box.show()
341        self.prompt_entry.grab_focus()
342        # Mask the input if requested
343        if promptType == LightDM.PromptType.SECRET:
344            self.prompt_entry.set_visibility(False)
345        else:
346            self.prompt_entry.set_visibility(True)
347        self.spin(False)
348
349    # show-message is emitted when LightDM has something to say to the user
[25545]350    # Typically, these are emitted by PAM modules (e.g. pam_echo)
351    # Note that this is _not_ "authentication failed" (unless a PAM module
352    # specifically says that). 
353    #
354    # The docs which say to check .is_authenticated() in the
355    # authentication-complete callback to determine login success or
356    # failure.
[25494]357    #
[25545]358    # messageType is one of LightDM.MessageType.{ERROR,INFO}
359    def cbShowMessage(self, greeter, text, messageType):
[25494]360        self._debug("cbShowMessage: Received show-messsage message",
361                   text, messageType)
[25545]362        # TODO: Wrap text
[25494]363        self.displayMessage(text)
364        self.spin(False)
365
366    def cbKeyPress(self, widget, event):
367        if event.keyval == Gdk.KEY_Escape:
368            self.cancelLogin(widget)
369
370    def cancelLogin(self, widget=None):
371        self._debug("Cancelling authentication.  User=",
372                   self.greeter.get_authentication_user())
[25558]373        self.beingCancelled=True
[25494]374        self.greeter.cancel_authentication()
375        self.resetLoginWindow()
376
377    def displayMessage(self, msg):
378        self.message_label.set_text(msg)
379        self.message_label.show()
380
381    def clearMessage(self):
382        self.message_label.set_text("")
383        self.message_label.hide()
384
[25556]385    def spawnBrowser(self, event):
386        subprocess.call(KIOSK_LAUNCH_CMD)
387
[25494]388    def errDialog(self, errText):
389        dlg = Gtk.MessageDialog(self.winLogin,
390                                Gtk.DialogFlags.DESTROY_WITH_PARENT,
391                                Gtk.MessageType.ERROR,
392                                Gtk.ButtonsType.CLOSE,
393                                errText)
394        dlg.run()
395        dlg.destroy()
396
397
398    def spin(self, start):
399        if start:
400            self.loginSpinner.show()
401            self.loginSpinner.start()
402        else:
403            self.loginSpinner.stop()
404            self.loginSpinner.hide()
405
406    # Some greeter implementations check .get_is_authenticated() here
407    # and then start the session.  I think that's only relevant
408    # dealing with a user-picker and passwordless users (that is, you
409    # would call .authenticate(joeuser), and then click the button,
410    # and you'd just be logged in.  But we disable the user picker, so
411    # that's not relevant.
412    def cbLogin(self, widget):
[25558]413        # Because we just entered some text and are about to send it,
414        # we're no longer in the middle of a cancellation
415        self.beingCancelled=False
[25494]416        self.clearMessage()
417        self._debug("In cbLogin")
418        if self.prompted:
419            response = self.prompt_entry.get_text()
420            self._debug("Sending response to prompt", response if self.prompt_entry.get_visibility() else "[redacted]")
421            self.spin(True)
422            self.greeter.respond(response)
423            self.prompted=False
424        else:
425            self._debug("No prompt.  Beginning new authentication process.")
426            # Show the "Cancel" button"
427            self.sessionBox.show()
428            self.btnCancel.show()
429            self.greeter.authenticate(None)
430 
431    # Load the Debathena owl image and generate self.logo_pixbufs, the list of
432    # animation frames.  Returns True if successful, False otherwise.
433    def setup_owl(self,logoScale):
434        self.logo_pixbufs = []
435        num_pixbufs = 0
436        # Eyes go closed.
437        for img in DEBATHENA_LOGO_FILES:
438            try:
439                pixbuf = GdkPixbuf.Pixbuf.new_from_file(img)
440                self.logo_pixbufs.append(pixbuf.scale_simple(int(pixbuf.get_width() * logoScale), int(pixbuf.get_height() * logoScale), GdkPixbuf.InterpType.BILINEAR))
441                num_pixbufs += 1
442            except Glib.Gerror, e:
443                print >> sys.stderr, "Glib Error:", e
444                return False
445        # Eyes come open.
446        for pixbuf in self.logo_pixbufs[::-1]:
447            self.logo_pixbufs.append(pixbuf)
448            num_pixbufs += 1
449        # Eyes stay open.
450        self.logo_pixbufs.extend([None] * (self.animation_loop_frames - num_pixbufs))
451        self.img_idx = -1
452        # Set it to the first image so that the window can size itself
453        # accordingly
454        self.imgDebathena.set_from_pixbuf(self.logo_pixbufs[0])
455        self._debug("Owl setup done")
456        return True
457   
458    def update_owl(self):
459        if not self.animate:
460            self._debug("Owl loading failed, ending update_owl timer")
461            return False
462
463        self.img_idx = (self.img_idx + 1) % self.animation_loop_frames
464        pixbuf = self.logo_pixbufs[self.img_idx]
465        if pixbuf is not None:
466            self.imgDebathena.set_from_pixbuf(pixbuf)
467            return True
468
469
470    def get_workstation_information(self):
471        try:
[25500]472            self.metapackage = subprocess.Popen(["machtype", "-L"], stdout=subprocess.PIPE).communicate()[0].rstrip()
[25494]473        except OSError:
[25500]474            self.metapackage = '(unknown metapackage)'
[25494]475        try:
[25500]476            self.baseos = subprocess.Popen(["machtype", "-E"], stdout=subprocess.PIPE).communicate()[0].rstrip()
[25494]477        except OSError:
[25500]478            self.baseos = '(unknown OS)'
[25494]479
480
481
482
483if __name__ == '__main__':
[25500]484    parser = OptionParser()
485    parser.set_defaults(debug=False)
486    parser.add_option("--debug", action="store_true", dest="debug")
487    parser.add_option("--ui", action="store", type="string",
488                      default=UI_FILE, dest="ui_file")
489    (options, args) = parser.parse_args()
[25494]490    Gtk.init(None);
491    main_loop = GObject.MainLoop ()
[25500]492    dagreeter = DebathenaGreeter(options)
[25494]493    # Add a timeout for the owl animation
494    GObject.timeout_add(50, dagreeter.update_owl)
495    # Add a timeout for the clock in the panel
496    GObject.timeout_add(30, dagreeter.updateTime)
497
498    main_loop.run ()
Note: See TracBrowser for help on using the repository browser.