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

Revision 25632, 21.9 KB checked in by jdreed, 12 years ago (diff)
In lightdm-config: * Support basic accessibility options (read: a High Contrast Theme) * For aesthetic reasons (since the windows have no borders), hide the login window when displaying a dialog box
  • Property svn:executable set to *
Line 
1#!/usr/bin/env python
2#
3
4from gi.repository import GObject
5from gi.repository import GLib
6from gi.repository import Gio
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
17import os.path
18from optparse import OptionParser
19
20# TODO: ConfigParser
21MIN_UID=1
22NOLOGIN_FILE="/var/run/athena-nologin"
23UI_FILE="/usr/share/debathena-lightdm-config/debathena-lightdm-greeter.ui"
24BG_IMG_FILE="/usr/share/debathena-lightdm-config/background.jpg"
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"]
34KIOSK_LAUNCH_CMD="/usr/lib/debathena-kiosk/lightdm-launch-kiosk"
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:
45                print >> sys.stderr, "D: ", args[0]
46
47    def __init__(self, options):
48        self.debugMode = options.debug
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,
66            "browse_cb": self.spawnBrowser,
67        }
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
81 
82        # Save the screen size for various window operations
83        defaultScreen = Gdk.Screen.get_default()
84        self.screenSize = (defaultScreen.width(), defaultScreen.height())
85       
86        self.get_workstation_information()
87
88        # Load the UI and get objects we care about
89        self.builder = Gtk.Builder()
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)
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")
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);
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
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       
136        self.accessDlg = self.builder.get_object("accessibilityDialog")
137
138        self.loginNotebook = self.builder.get_object("notebook1")
139
140        # Scaling factor for smaller displays
141        logoScale = 0.75 if self.screenSize[1] <= 768 else 1.0
142        self.animate = self.setup_owl(logoScale)
143       
144        self.winLogin.set_position(Gtk.WindowPosition.CENTER)
145        self.winLogin.show()
146        self.initBackgroundWindow()
147        self.initPanelWindow()
148        self.initBrandingWindow()
149        # Connect Gtk+ signal handlers
150        self.builder.connect_signals(handlers)
151        # GNOME 3 turns off button images by default.  Turn it on
152        # for the "Panel" window
153        self.gtkSettings = Gtk.Settings.get_default()
154        self.gtkSettings.set_property('gtk-button-images', True)
155        self.origTheme = self.gtkSettings.get_property('gtk-theme-name')
156        # Set a cursor for the root window, otherwise there isn't one
157        rw = Gdk.get_default_root_window()
158        rw.set_cursor(Gdk.Cursor(Gdk.CursorType.LEFT_PTR))
159        self.noLoginMonitor = Gio.File.new_for_path(NOLOGIN_FILE).monitor_file(Gio.FileMonitorFlags.NONE, None)
160        self.noLoginMonitor.connect("changed", self._file_changed)
161
162        if not os.path.exists(KIOSK_LAUNCH_CMD):
163            self.builder.get_object("btnBrowse").hide()
164        # Setup the login window for first login
165        self.resetLoginWindow()
166
167    def initBackgroundWindow(self):
168        # The background image
169        self.winBg = self.builder.get_object("winBg")
170        self.imgBg = self.builder.get_object("imgBg")
171        bg_pixbuf = GdkPixbuf.Pixbuf.new_from_file(BG_IMG_FILE)
172        bg_scaled = bg_pixbuf.scale_simple(self.screenSize[0], self.screenSize[1], GdkPixbuf.InterpType.BILINEAR)
173        self.imgBg.set_from_pixbuf(bg_scaled)
174        self.winBg.show_all()
175
176    def initPanelWindow(self):
177        # A window that looks like the GNOME "panel" at the top of the screen
178        self.winPanel = self.builder.get_object("winPanel")
179        self.lblTime = self.builder.get_object("lblTime")
180        self.winPanel.set_gravity(Gdk.Gravity.NORTH_WEST)
181        self.winPanel.move(0,0)
182        self.winPanel.set_size_request(self.screenSize[0], 2)
183        self.winPanel.show_all()
184
185    def initBrandingWindow(self):
186        # The "branding window", in the bottom right
187        winBranding = self.builder.get_object("winBranding")
188        lblBranding = self.builder.get_object("lblBranding")
189        arch = platform.machine()
190        if arch != "x86_64":
191            arch = "<b>" + arch + "</b>"
192        # Possibly no longer needed, workaround for a Glade bug in Gtk+ 2
193        lblBranding.set_property('can_focus', False)
194        winBranding.set_property('can_focus', False)
195        lblBranding.set_markup(self.metapackage + "\n" + self.baseos + "\n" + arch)
196        winBranding.set_gravity(Gdk.Gravity.SOUTH_EAST)
197        width, height = winBranding.get_size()
198        winBranding.move(self.screenSize[0] - width, self.screenSize[1] - height)
199        winBranding.show_all()
200
201    def showPowerDialog(self, widget):
202        self.winLogin.hide()
203        if self.powerDlg.run() == Gtk.ResponseType.OK:
204            # Just do the action.  The relevant buttons should be
205            # greyed out for things we can't do.  LightDM will
206            # check with ConsoleKit anyway
207            try:
208                if self.builder.get_object("rbShutdown").get_active():
209                    LightDM.shutdown()
210                elif self.builder.get_object("rbReboot").get_active():
211                    LightDM.restart()
212                elif self.builder.get_object("rbHiberate").get_active():
213                    LightDM.hibernate()
214                elif self.builder.get_object("rbSuspend").get_active():
215                    LightDM.suspend()
216            except GLib.GError, e:
217                self.errDialog("An error occurred while trying to perform a power-related operation.  The most common cause of this is trying to shutdown, reboot, or suspend the machine when someone else is logged in remotely or on one of the virtual terminals.  The full error text appears below:\n\n" + str(e))
218        self.powerDlg.hide()
219        self.winLogin.show()
220
221    def showAccessDialog(self, widget):
222        self.winLogin.hide()
223        if self.accessDlg.run() == Gtk.ResponseType.OK:
224            if self.builder.get_object("cbHighContrast").get_active():
225                self.gtkSettings.set_property('gtk-theme-name', 'HighContrastInverse')
226            else:
227                self.gtkSettings.set_property('gtk-theme-name', self.origTheme)
228        self.accessDlg.hide()
229        self.winLogin.show()
230
231
232    def _file_changed(self, monitor, file1, file2, evt_type):
233        if evt_type == Gio.FileMonitorEvent.CREATED:
234            self.loginNotebook.set_current_page(1)
235            self.builder.get_object("lblUpdTime").set_text("Update started at %s" % (time.strftime("%Y-%m-%d %H:%M")))
236        if evt_type == Gio.FileMonitorEvent.DELETED:
237            self.loginNotebook.set_current_page(0)
238
239    # Update the time in the "panel"
240    def updateTime(self):
241        timeFmt="%a, %b %e %Y %l:%M" + ":%S" if self.timePedantry else ""
242        # every second counts
243        timeFmt=timeFmt + " %p"
244        self.lblTime.set_text(time.strftime(timeFmt, time.localtime(time.time())))
245        return True
246
247    # Reset the UI and prepare for a new login
248    def resetLoginWindow(self):
249        self.spin(False)
250        self.clearMessage()
251        self.btnCancel.hide()
252        self.sessionBox.hide()
253        self.prompted=False
254        self.prompt_label.set_text("")
255        self.prompt_entry.set_text("")
256        self.prompt_box.hide()
257        self.btnLogin.grab_focus()
258        # Because there's no WM, we need to focus the actual X window
259        Gdk.Window.focus(self.winLogin.get_window(), Gdk.CURRENT_TIME)
260
261    def getSelectedSession(self):
262        i = self.cmbSession.get_active_iter()
263        session_name = self.cmbSession.get_model().get_value(i, 1)
264        self._debug("selected session is " + session_name)
265        return session_name
266
267    def startOver(self):
268        self.greeter.cancel_authentication()
269        self.greeter.authenticate(None)
270
271    # LightDM Callbacks
272    # The workflow is this:
273    # - call .authenticate() with a username
274    # - lightdm responds with a prompt for password
275    # - call .respond with whatever the user provides
276    # - lightdm responds with authentication-complete
277    #   N.B. complete != successful
278    # - .cancel_authentication will cancel the authentication in progress
279    #   call .authenticate with a new username to restart it
280    #
281    # Calling .authenticate with None (or NULL in C) will cause lightdm
282    # to first prompt for a username, then a password.  This means two
283    # show-prompt callbacks and thus two .respond calls
284   
285    # This callback is called when the authentication process is
286    # complete.  "complete" means a username and password have been
287    # received, and PAM has done its thing.  "complete" does not
288    # mean "successful".
289    def cbAuthenticationComplete(self, greeter):
290        self.spin(False)
291        self._debug("cbAuthenticationComplete: received authentication-complete message")
292        if greeter.get_is_authenticated():
293            self.spin(True)
294            self._debug("Authentication was successful.")
295            session_name = self.getSelectedSession()
296            #FIXME: Make sure it's a valid session
297            self._debug("User has selected '%s' session" % (session_name))
298            if not greeter.start_session_sync(session_name):
299                self._debug("Failed to start session")
300                print >> sys.stderr, "Failed to start session"
301        elif not self.beingCancelled:
302            self._debug("Authentication failed.")
303            self.displayMessage("Authentication failed, please try again")
304            self.greeter.authenticate(None)
305        else:
306            self.beingCancelled=False
307            self.resetLoginWindow()
308
309    # The show-prompt message is emitted when LightDM wants you to
310    # show a prompt to the user, and respond with the user's response.
311    # Currently, the prompts we care about are "login:" and
312    # "Password: " (yes, with the trailing space), which ask for the
313    # username and password respectively.  promptType is one of
314    # LightDM.PromptType.SECRET or LightDM.PromptType.QUESTION, which
315    # mean that the text of the user's response should or should not be
316    # masked/invisible, respectively.
317
318    def cbShowPrompt(self, greeter, text, promptType):
319        self._debug("cbShowPrompt: Received show-prompt message: ",
320                   text, promptType)
321        self.prompted=True
322        # Make things pretty
323        if text == "login:":
324            text = "Username: "
325        # Sanity check the username
326        currUser = self.greeter.get_authentication_user()
327        if currUser:
328            self._debug("Current user being authenticated is " + currUser)
329            # See if the user exists
330            try:
331                passwd=pwd.getpwnam(currUser)
332            except KeyError:
333                # Why are we not using the message label here?
334                # Because what will happen is that someone will quickly
335                # typo their username, and then type their password without
336                # looking at the screen, which would otherwise result in the
337                # window resetting after the first error, and then they end
338                # up typing their password into the username box.
339                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))
340                self.startOver()
341                return True
342            # There's probably a better way
343            if passwd.pw_uid < MIN_UID:
344                self.errDialog("Logging in as '%s' disallowed by configuation" % (currUser))
345                self.startOver()
346                return True
347
348        # Set the label to the value of the prompt
349        self.prompt_label.set_text(text)
350        # clear the entry and get focus
351        self.prompt_entry.set_text("")
352        self.prompt_entry.set_sensitive(True)
353        self.prompt_box.show()
354        self.prompt_entry.grab_focus()
355        # Mask the input if requested
356        if promptType == LightDM.PromptType.SECRET:
357            self.prompt_entry.set_visibility(False)
358        else:
359            self.prompt_entry.set_visibility(True)
360        self.spin(False)
361
362    # show-message is emitted when LightDM has something to say to the user
363    # Typically, these are emitted by PAM modules (e.g. pam_echo)
364    # Note that this is _not_ "authentication failed" (unless a PAM module
365    # specifically says that). 
366    #
367    # The docs which say to check .is_authenticated() in the
368    # authentication-complete callback to determine login success or
369    # failure.
370    #
371    # messageType is one of LightDM.MessageType.{ERROR,INFO}
372    def cbShowMessage(self, greeter, text, messageType):
373        self._debug("cbShowMessage: Received show-messsage message",
374                   text, messageType)
375        # TODO: Wrap text
376        self.displayMessage(text)
377        self.spin(False)
378
379    def cbKeyPress(self, widget, event):
380        if event.keyval == Gdk.KEY_Escape:
381            self.cancelLogin(widget)
382
383    def cancelLogin(self, widget=None):
384        self._debug("Cancelling authentication.  User=",
385                   self.greeter.get_authentication_user())
386        self.beingCancelled=True
387        self.greeter.cancel_authentication()
388        self.resetLoginWindow()
389
390    def displayMessage(self, msg):
391        self.message_label.set_text(msg)
392        self.message_label.show()
393
394    def clearMessage(self):
395        self.message_label.set_text("")
396        self.message_label.hide()
397
398    def spawnBrowser(self, event):
399        subprocess.call(KIOSK_LAUNCH_CMD)
400
401    def errDialog(self, errText):
402        dlg = Gtk.MessageDialog(self.winLogin,
403                                Gtk.DialogFlags.DESTROY_WITH_PARENT,
404                                Gtk.MessageType.ERROR,
405                                Gtk.ButtonsType.CLOSE,
406                                errText)
407        dlg.run()
408        dlg.destroy()
409
410
411    def spin(self, start):
412        if start:
413            self.loginSpinner.show()
414            self.loginSpinner.start()
415        else:
416            self.loginSpinner.stop()
417            self.loginSpinner.hide()
418
419    # Some greeter implementations check .get_is_authenticated() here
420    # and then start the session.  I think that's only relevant
421    # dealing with a user-picker and passwordless users (that is, you
422    # would call .authenticate(joeuser), and then click the button,
423    # and you'd just be logged in.  But we disable the user picker, so
424    # that's not relevant.
425    def cbLogin(self, widget):
426        # Because we just entered some text and are about to send it,
427        # we're no longer in the middle of a cancellation
428        self.beingCancelled=False
429        self.clearMessage()
430        self._debug("In cbLogin")
431        if self.prompted:
432            response = self.prompt_entry.get_text()
433            self._debug("Sending response to prompt", response if self.prompt_entry.get_visibility() else "[redacted]")
434            self.spin(True)
435            self.greeter.respond(response)
436            self.prompted=False
437        else:
438            self._debug("No prompt.  Beginning new authentication process.")
439            # Show the "Cancel" button"
440            self.sessionBox.show()
441            self.btnCancel.show()
442            self.greeter.authenticate(None)
443 
444    # Load the Debathena owl image and generate self.logo_pixbufs, the list of
445    # animation frames.  Returns True if successful, False otherwise.
446    def setup_owl(self,logoScale):
447        self.logo_pixbufs = []
448        num_pixbufs = 0
449        # Eyes go closed.
450        for img in DEBATHENA_LOGO_FILES:
451            try:
452                pixbuf = GdkPixbuf.Pixbuf.new_from_file(img)
453                self.logo_pixbufs.append(pixbuf.scale_simple(int(pixbuf.get_width() * logoScale), int(pixbuf.get_height() * logoScale), GdkPixbuf.InterpType.BILINEAR))
454                num_pixbufs += 1
455            except GLib.GError, e:
456                print >> sys.stderr, "Glib Error:", e
457                return False
458        # Eyes come open.
459        for pixbuf in self.logo_pixbufs[::-1]:
460            self.logo_pixbufs.append(pixbuf)
461            num_pixbufs += 1
462        # Eyes stay open.
463        self.logo_pixbufs.extend([None] * (self.animation_loop_frames - num_pixbufs))
464        self.img_idx = -1
465        # Set it to the first image so that the window can size itself
466        # accordingly
467        self.imgDebathena.set_from_pixbuf(self.logo_pixbufs[0])
468        self._debug("Owl setup done")
469        return True
470   
471    def update_owl(self):
472        if not self.animate:
473            self._debug("Owl loading failed, ending update_owl timer")
474            return False
475
476        self.img_idx = (self.img_idx + 1) % self.animation_loop_frames
477        pixbuf = self.logo_pixbufs[self.img_idx]
478        if pixbuf is not None:
479            self.imgDebathena.set_from_pixbuf(pixbuf)
480            return True
481
482
483    def get_workstation_information(self):
484        try:
485            self.metapackage = subprocess.Popen(["machtype", "-L"], stdout=subprocess.PIPE).communicate()[0].rstrip()
486        except OSError:
487            self.metapackage = '(unknown metapackage)'
488        try:
489            self.baseos = subprocess.Popen(["machtype", "-E"], stdout=subprocess.PIPE).communicate()[0].rstrip()
490        except OSError:
491            self.baseos = '(unknown OS)'
492
493
494
495
496if __name__ == '__main__':
497    parser = OptionParser()
498    parser.set_defaults(debug=False)
499    parser.add_option("--debug", action="store_true", dest="debug")
500    parser.add_option("--ui", action="store", type="string",
501                      default=UI_FILE, dest="ui_file")
502    (options, args) = parser.parse_args()
503    Gtk.init(None);
504    main_loop = GObject.MainLoop ()
505    dagreeter = DebathenaGreeter(options)
506    # Add a timeout for the owl animation
507    GObject.timeout_add(50, dagreeter.update_owl)
508    # Add a timeout for the clock in the panel
509    GObject.timeout_add(30, dagreeter.updateTime)
510
511    main_loop.run ()
Note: See TracBrowser for help on using the repository browser.