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

Revision 25624, 21.3 KB checked in by jdreed, 12 years ago (diff)
In lightdm-config: * Be clearer about what a DBus error probably means when initiating a restart/shutdown from lightdm
  • 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()
[25574]212            except GLib.GError, e:
[25624]213                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))
[25500]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
[25574]442            except GLib.GError, e:
[25494]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.