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

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