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

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