1 | #!/usr/bin/env python |
---|
2 | # |
---|
3 | |
---|
4 | from gi.repository import GObject |
---|
5 | from gi.repository import GLib |
---|
6 | from gi.repository import Gio |
---|
7 | from gi.repository import Gtk |
---|
8 | from gi.repository import Gdk |
---|
9 | from gi.repository import GdkPixbuf |
---|
10 | from gi.repository import LightDM |
---|
11 | |
---|
12 | import sys |
---|
13 | import platform |
---|
14 | import subprocess |
---|
15 | import pwd |
---|
16 | import time |
---|
17 | import os.path |
---|
18 | from optparse import OptionParser |
---|
19 | import ConfigParser |
---|
20 | import io |
---|
21 | |
---|
22 | KIOSK_LAUNCH_CMD="/usr/lib/debathena-kiosk/lightdm-launch-kiosk" |
---|
23 | PICKBOARD_CMD="/usr/bin/onboard" |
---|
24 | UI_FILE="/usr/share/debathena-lightdm-config/debathena-lightdm-greeter.ui" |
---|
25 | CONFIG_FILE="/etc/debathena-lightdm-greeter.ini" |
---|
26 | CONFIG_DEFAULTS={"minimum_uid": 1, |
---|
27 | "nologin_file": "/var/run/athena-nologin", |
---|
28 | "background_image": "/usr/share/debathena-lightdm-config/background.jpg", |
---|
29 | "base_logo_file": "/usr/share/debathena-lightdm-config/debathena.png", |
---|
30 | "extra_logo_frames": 8, |
---|
31 | "motd_filename": "/afs/athena.mit.edu/system/config/motd/login.debathena", |
---|
32 | "append_codename_to_motd": "true", |
---|
33 | "time_pedantry": "true"} |
---|
34 | |
---|
35 | # See below, and then go cry. |
---|
36 | _OBJS_TO_RENAME=["miShutdown", "miRestart", "miSuspend", "miHibernate"] |
---|
37 | |
---|
38 | class DebathenaGreeter: |
---|
39 | animation_loop_frames = 300 |
---|
40 | |
---|
41 | |
---|
42 | def _debug(self, *args): |
---|
43 | if self.debugMode: |
---|
44 | if type(args[0]) is str and len(args) > 1: |
---|
45 | print >> sys.stderr, "D: " + args[0], args[1:] |
---|
46 | else: |
---|
47 | print >> sys.stderr, "D: ", args[0] |
---|
48 | |
---|
49 | def __init__(self, options, config): |
---|
50 | self.debugMode = options.debug |
---|
51 | if self.debugMode: |
---|
52 | # Sigh. In theory, APPMENU_DISPLAY_BOTH=1 should give me both |
---|
53 | # an appmenu and the "real" menubar. |
---|
54 | # In reality, it doesn't. |
---|
55 | # QT_X11_NO_NATIVE_MENUBAR=1 is the KDE equivalent |
---|
56 | self._debug("Attempting to turn off appmenu...") |
---|
57 | os.putenv('UBUNTU_MENUPROXY', '') |
---|
58 | |
---|
59 | # Load the configuration, with type checking |
---|
60 | try: |
---|
61 | self.timePedantry = config.getboolean('Greeter', 'time_pedantry') |
---|
62 | except: |
---|
63 | self.timePedantry = config.getboolean('DEFAULT', 'time_pedantry') |
---|
64 | |
---|
65 | try: |
---|
66 | self.appendCodenameToMotd = config.getboolean('Greeter', 'append_codename_to_motd') |
---|
67 | except: |
---|
68 | self.appendCodenameToMotd = config.getboolean('DEFAULT', 'append_codename_to_motd') |
---|
69 | |
---|
70 | try: |
---|
71 | self.minimumUID = config.getint('Greeter', 'minimum_uid') |
---|
72 | except: |
---|
73 | self.minimumUID = config.getint('DEFAULT', 'minimum_uid') |
---|
74 | |
---|
75 | try: |
---|
76 | extraLogoFrames = config.getint('Greeter', 'extra_logo_frames') |
---|
77 | except: |
---|
78 | extraLogoFrames = config.getint('DEFAULT', 'extra_logo_frames') |
---|
79 | |
---|
80 | baseFile = config.get('Greeter', 'base_logo_file') |
---|
81 | self.logoFiles = [baseFile, ] |
---|
82 | fileName, fileExt = os.path.splitext(baseFile) |
---|
83 | if extraLogoFrames > 0: |
---|
84 | for i in range(1,extraLogoFrames + 1): |
---|
85 | self.logoFiles.append("%s%d%s" % (fileName, i, fileExt)) |
---|
86 | |
---|
87 | # No need to check these, they get checked later. |
---|
88 | self.backgroundImageFile = config.get('Greeter', 'background_image') |
---|
89 | self.motdFilename = config.get('Greeter', 'motd_filename') |
---|
90 | self.nologinFile = config.get('Greeter', 'nologin_file') |
---|
91 | |
---|
92 | # Set up and connect to the greeter |
---|
93 | self.greeter = LightDM.Greeter() |
---|
94 | self.greeter.connect("authentication-complete", |
---|
95 | self.cbAuthenticationComplete) |
---|
96 | self.greeter.connect("show-message", self.cbShowMessage) |
---|
97 | self.greeter.connect("show-prompt", self.cbShowPrompt) |
---|
98 | self.greeter.connect_sync() |
---|
99 | |
---|
100 | # Gtk signal handlers |
---|
101 | handlers = { |
---|
102 | "login_cb": self.cbLogin, |
---|
103 | "cancel_cb": self.cancelLogin, |
---|
104 | "kpEvent": self.cbKeyPress, |
---|
105 | "power_cb": self.doPowerOperation, |
---|
106 | "contrast_cb": self.toggleContrast, |
---|
107 | "pickboard_cb": self.togglePickboard, |
---|
108 | "large_font_cb": self.toggleLargeFont, |
---|
109 | "browse_cb": self.spawnBrowser |
---|
110 | } |
---|
111 | |
---|
112 | # Sigh. Pre lightdm-1.1, cancel_authentication() calls the |
---|
113 | # authentication-complete callback, so when we're in that |
---|
114 | # callback, we need to know if we cancelled, or if we typed |
---|
115 | # the password wrong. The lightdm daemon does in fact know |
---|
116 | # the difference (return codes of 7 or 10), but gir has no way |
---|
117 | # to get that info, AFAICT |
---|
118 | # So beingCancelled is set to True when the user hits Cancel |
---|
119 | # (or Esc) and set back to False in the authentication-complete |
---|
120 | # callback or before we try and send anything else to the greeter |
---|
121 | # This only controls UI, and has no effect on whether LightDM |
---|
122 | # thinks the authentication process is being cancelled. |
---|
123 | self.beingCancelled=False |
---|
124 | |
---|
125 | # Save the screen size for various window operations |
---|
126 | defaultScreen = Gdk.Screen.get_default() |
---|
127 | # Kids these days have too many monitors |
---|
128 | self.monitorGeometry = defaultScreen.get_monitor_geometry(defaultScreen.get_primary_monitor()) |
---|
129 | # Don't use this for window centering calculations |
---|
130 | self.screenSize = (self.monitorGeometry.x + self.monitorGeometry.width, |
---|
131 | self.monitorGeometry.y + self.monitorGeometry.height) |
---|
132 | self.get_workstation_information() |
---|
133 | |
---|
134 | # Load the UI and get objects we care about |
---|
135 | self.builder = Gtk.Builder() |
---|
136 | try: |
---|
137 | self.builder.add_from_file(options.ui_file) |
---|
138 | except GLib.GError, e: |
---|
139 | print >> sys.stderr, "FATAL: Unable to load UI: ", e |
---|
140 | sys.exit(-1) |
---|
141 | |
---|
142 | # This is just ridiculous. Due to a GtkBuilder bug, it scribbles over |
---|
143 | # the widget's name when instantiating them (via get_object()), but we |
---|
144 | # can then call set_name to re-name them. In a perfect world, this |
---|
145 | # would be a no-op. |
---|
146 | for obj in _OBJS_TO_RENAME: |
---|
147 | self.builder.get_object(obj).set_name(obj) |
---|
148 | |
---|
149 | # For the pickboard |
---|
150 | self.keyboardWindow = None |
---|
151 | |
---|
152 | # The login window |
---|
153 | self.winLogin = self.builder.get_object("winLogin") |
---|
154 | # A box containing the prompt label, entry, and a spinner |
---|
155 | self.prompt_box = self.builder.get_object("boxPrompt") |
---|
156 | self.prompt_label = self.builder.get_object("lblPrompt") |
---|
157 | self.prompt_entry = self.builder.get_object("entryPrompt") |
---|
158 | self.loginSpinner = self.builder.get_object("loginSpinner") |
---|
159 | # A label where we display messages received from the greeter |
---|
160 | self.message_label = self.builder.get_object("lblMessage") |
---|
161 | # The owl |
---|
162 | self.imgDebathena = self.builder.get_object("imgDebathena") |
---|
163 | # The workstation's hostname |
---|
164 | lblHostname = self.builder.get_object("lblHostname") |
---|
165 | lblHostname.set_text(LightDM.get_hostname()) |
---|
166 | # The buttons |
---|
167 | self.btnCancel = self.builder.get_object("btnCancel") |
---|
168 | self.btnLogin = self.builder.get_object("btnLogin") |
---|
169 | # The session combo box |
---|
170 | self.cmbSession = self.builder.get_object("cmbSession") |
---|
171 | self.sessionBox = self.builder.get_object("sessionBox") |
---|
172 | # Sigh. Needed for Oneiric. No-op on Precise |
---|
173 | # See GNOME Bugzilla #650369 and 653579 |
---|
174 | # GtkBuilder calls g_object_new, not gtk_combo_box_text_new() |
---|
175 | # so the properties don't get set. |
---|
176 | self.cmbSession.set_entry_text_column(0); |
---|
177 | self.cmbSession.set_id_column(1); |
---|
178 | for s in LightDM.get_sessions(): |
---|
179 | self.cmbSession.append(s.get_key(), s.get_name()) |
---|
180 | # Select the first session |
---|
181 | # TODO: Select the configured default session or the user's session |
---|
182 | self.cmbSession.set_active(0) |
---|
183 | |
---|
184 | self.loginNotebook = self.builder.get_object("notebook1") |
---|
185 | |
---|
186 | # Scaling factor for smaller displays |
---|
187 | logoScale = 0.75 if self.screenSize[1] <= 768 else 1.0 |
---|
188 | self.animate = self.setup_owl(logoScale) |
---|
189 | |
---|
190 | self.winLogin.set_position(Gtk.WindowPosition.CENTER) |
---|
191 | self.winLogin.show() |
---|
192 | self.initBackgroundWindow() |
---|
193 | self.initBrandingWindow() |
---|
194 | self.afsMonitor = Gio.File.new_for_path("/afs/athena").monitor_directory(Gio.FileMonitorFlags.WATCH_MOUNTS, None) |
---|
195 | self.afsAvailable = os.path.isdir("/afs/athena") |
---|
196 | self.afsMonitor.connect("changed", self.afsStatusChanged) |
---|
197 | self.initMotdWindow() |
---|
198 | # Connect Gtk+ signal handlers |
---|
199 | self.builder.connect_signals(handlers) |
---|
200 | # GNOME 3 turns off button images by default. Turn it on |
---|
201 | # for the "Panel" window |
---|
202 | self.gtkSettings = Gtk.Settings.get_default() |
---|
203 | self.gtkSettings.set_property('gtk-button-images', True) |
---|
204 | self.origTheme = self.gtkSettings.get_property('gtk-theme-name') |
---|
205 | # Set a cursor for the root window, otherwise there isn't one |
---|
206 | rw = Gdk.get_default_root_window() |
---|
207 | rw.set_cursor(Gdk.Cursor(Gdk.CursorType.LEFT_PTR)) |
---|
208 | self.noLoginMonitor = Gio.File.new_for_path(self.nologinFile).monitor_file(Gio.FileMonitorFlags.NONE, None) |
---|
209 | # Check if the file is there right now... |
---|
210 | if os.path.isfile(self.nologinFile): |
---|
211 | self.loginNotebook.set_current_page(1) |
---|
212 | self.builder.get_object("lblUpdTime").set_text("Update started at %s" % (time.strftime("%Y-%m-%d %H:%M"))) |
---|
213 | # and then connect |
---|
214 | self.noLoginMonitor.connect("changed", self._file_changed) |
---|
215 | |
---|
216 | if not os.path.exists(KIOSK_LAUNCH_CMD): |
---|
217 | self.builder.get_object("mnuBrowse").hide() |
---|
218 | # Setup the login window for first login |
---|
219 | self.resetLoginWindow() |
---|
220 | |
---|
221 | def initMotdWindow(self): |
---|
222 | self.winMotd = self.builder.get_object("winMotd") |
---|
223 | motdFile=None |
---|
224 | if self.appendCodenameToMotd: |
---|
225 | try: |
---|
226 | codename = subprocess.Popen(["lsb_release", "-s", "-c"], stdout=subprocess.PIPE).communicate()[0] |
---|
227 | if codename and os.path.exists(self.motdFilename + "." + codename.strip()): |
---|
228 | self.motdFilename += "." + codename.strip() |
---|
229 | except OSError: |
---|
230 | print >>sys.stderr, "Couldn't get codename to append to motd_filename. Oh well..." |
---|
231 | try: |
---|
232 | motdFile = open(self.motdFilename, "r") |
---|
233 | except IOError, e: |
---|
234 | print >>sys.stderr, "Can't open MOTD file %s: %s" % (self.motdFilename, str(e)) |
---|
235 | motdTxt = '' |
---|
236 | # Avoid huge files messing up the greeter |
---|
237 | # At most 10 lines of 80 characters per line |
---|
238 | # Pango ellipsizing and geometry hints won't accomplish this |
---|
239 | if motdFile: |
---|
240 | lines=0 |
---|
241 | while lines <= 10: |
---|
242 | line = motdFile.readline() |
---|
243 | if not line: |
---|
244 | break |
---|
245 | lines += 1 |
---|
246 | if len(line) > 80: |
---|
247 | line = line[:74] + " [...]\n" |
---|
248 | motdTxt += line |
---|
249 | if motdFile.read(): |
---|
250 | motdTxt += "[...]\n" |
---|
251 | motdFile.close() |
---|
252 | if motdTxt: |
---|
253 | self.builder.get_object('lblMotd').set_markup(motdTxt.strip()) |
---|
254 | width, height = self.winMotd.get_size() |
---|
255 | self.winMotd.set_gravity(Gdk.Gravity.SOUTH) |
---|
256 | self.winMotd.move(self.monitorGeometry.x + ((self.monitorGeometry.width - width )/ 2), self.screenSize[1] - height - 10) |
---|
257 | self.winMotd.show_all() |
---|
258 | |
---|
259 | def initBackgroundWindow(self): |
---|
260 | # The background image |
---|
261 | self.winBg = self.builder.get_object("winBg") |
---|
262 | self.imgBg = self.builder.get_object("imgBg") |
---|
263 | try: |
---|
264 | bg_pixbuf = GdkPixbuf.Pixbuf.new_from_file(self.backgroundImageFile) |
---|
265 | bg_scaled = bg_pixbuf.scale_simple(self.screenSize[0], self.screenSize[1], GdkPixbuf.InterpType.BILINEAR) |
---|
266 | except GLib.GError, e: |
---|
267 | print >> sys.stderr, "Glib Error while loading background image:", e |
---|
268 | # Just a plain black background |
---|
269 | bg_scaled = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, False, 8, self.screenSize[0], self.screenSize[1]) |
---|
270 | self.imgBg.set_from_pixbuf(bg_scaled) |
---|
271 | |
---|
272 | # The menubar |
---|
273 | # LightDM checks with PolKit for the various "get_can_foo()" functions |
---|
274 | # We call .set_name() here because of a GtkBuilder bug |
---|
275 | self.builder.get_object("miShutdown").set_sensitive(LightDM.get_can_shutdown()) |
---|
276 | self.builder.get_object("miRestart").set_sensitive(LightDM.get_can_restart()) |
---|
277 | # self.builder.get_object("miRestart").set_name('restart') |
---|
278 | # We don't allow suspend/hibernate on cluster |
---|
279 | self.builder.get_object("miHibernate").set_sensitive(LightDM.get_can_hibernate() and self.metapackage != "debathena-cluster") |
---|
280 | self.builder.get_object("miSuspend").set_sensitive(LightDM.get_can_suspend() and self.metapackage != "debathena-cluster") |
---|
281 | |
---|
282 | # We just want images. A Glade bug removes the "label" property |
---|
283 | # completely if it's null, which results in bad redrawing artifacts |
---|
284 | # So we set the label to text we don't care about in Glade, and |
---|
285 | # change it here. |
---|
286 | for menu in ("mnuPower", "mnuAccess", "mnuBrowse"): |
---|
287 | self.builder.get_object(menu).set_property('label', '') |
---|
288 | # Used in the updateTime callback |
---|
289 | self.mnuTime = self.builder.get_object("mnuClock") |
---|
290 | |
---|
291 | self.builder.get_object("miPickboard").set_sensitive(os.path.exists(PICKBOARD_CMD)) |
---|
292 | |
---|
293 | self.winBg.show_all() |
---|
294 | |
---|
295 | def initBrandingWindow(self): |
---|
296 | # The "branding window", in the bottom right |
---|
297 | winBranding = self.builder.get_object("winBranding") |
---|
298 | lblBranding = self.builder.get_object("lblBranding") |
---|
299 | arch = platform.machine() |
---|
300 | if arch != "x86_64": |
---|
301 | arch = "<b>" + arch + "</b>" |
---|
302 | # Possibly no longer needed, workaround for a Glade bug in Gtk+ 2 |
---|
303 | lblBranding.set_property('can_focus', False) |
---|
304 | winBranding.set_property('can_focus', False) |
---|
305 | lblBranding.set_markup(self.metapackage + "\n" + self.baseos + "\n" + arch) |
---|
306 | winBranding.set_gravity(Gdk.Gravity.SOUTH_EAST) |
---|
307 | width, height = winBranding.get_size() |
---|
308 | winBranding.move(self.screenSize[0] - width, self.screenSize[1] - height) |
---|
309 | winBranding.show_all() |
---|
310 | |
---|
311 | def doPowerOperation(self, widget): |
---|
312 | # This only works because of the calls to .set_name() above. |
---|
313 | # Other stupid ideas include: |
---|
314 | # if widget == self.builder.get_object('whatever'): |
---|
315 | # |
---|
316 | # N.B. user_data for GtkBuilder is a complete mess and all |
---|
317 | # kinds of wrong (you can only specify objects defined in the |
---|
318 | # builder xml, and it forces the 'swap' flag when |
---|
319 | # autoconnected, AND the object you get _replaces_ the widget |
---|
320 | # in the callback) |
---|
321 | actions = {"miShutdown": LightDM.shutdown, |
---|
322 | "miRestart": LightDM.restart, |
---|
323 | "miHibernate": LightDM.hibernate, |
---|
324 | "miSuspend": LightDM.suspend} |
---|
325 | try: |
---|
326 | actions[widget.get_name()]() |
---|
327 | except KeyError: |
---|
328 | # "won't" happen |
---|
329 | print >>sys.stderr, "ERR: No action for widget name: "+ widget.get_name() |
---|
330 | except GLib.GError, e: |
---|
331 | # It's possible we should look at the error text to see if |
---|
332 | # it's ConsoleKit that's whining? Because you get a |
---|
333 | # (valid) error from UPower if you try to suspend when |
---|
334 | # your hardware doesn't support it |
---|
335 | 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)) |
---|
336 | |
---|
337 | |
---|
338 | def togglePickboard(self, widget): |
---|
339 | if not widget.get_active(): |
---|
340 | if self.keyboardWindow: |
---|
341 | self.keyboardWindow.destroy() |
---|
342 | if self.onboardProc: |
---|
343 | self.onboardProc.terminate() |
---|
344 | else: |
---|
345 | self.onboardProc = None |
---|
346 | xid = None |
---|
347 | try: |
---|
348 | self.onboardProc = subprocess.Popen([PICKBOARD_CMD, "--xid"], |
---|
349 | stdout=subprocess.PIPE) |
---|
350 | xid = int(self.onboardProc.stdout.readline()) |
---|
351 | except OSError, e: |
---|
352 | print >>sys.stderr, "Failed to spawn /usr/bin/onboard", str(e) |
---|
353 | except ValueError: |
---|
354 | print >>sys.stderr, "onboard didn't return an integer xid (shouldn't happen)" |
---|
355 | self.onboardProc.kill() |
---|
356 | |
---|
357 | if self.onboardProc is None or xid is None: |
---|
358 | self.errDialog("An error occurred while starting the on-screen keyboard.") |
---|
359 | widget.set_sensitive(False) |
---|
360 | # Remember, this will call this callback again |
---|
361 | widget.set_active(False) |
---|
362 | return |
---|
363 | |
---|
364 | self.keyboardWindow = Gtk.Window() |
---|
365 | self.keyboardWindow.show() |
---|
366 | self.keyboardWindow.accept_focus = False; |
---|
367 | self.keyboardWindow.focus_on_map = False; |
---|
368 | keyboardSocket = Gtk.Socket() |
---|
369 | keyboardSocket.show() |
---|
370 | self.keyboardWindow.add(keyboardSocket) |
---|
371 | keyboardSocket.add_id(xid) |
---|
372 | self.keyboardWindow.move(0, self.screenSize[1] - 200) |
---|
373 | self.keyboardWindow.resize(int(self.screenSize[0] / 3), 200) |
---|
374 | |
---|
375 | def toggleLargeFont(self, widget): |
---|
376 | pass |
---|
377 | |
---|
378 | def toggleContrast(self, widget): |
---|
379 | if widget.get_active(): |
---|
380 | self.gtkSettings.set_property('gtk-theme-name', 'HighContrastInverse') |
---|
381 | else: |
---|
382 | self.gtkSettings.set_property('gtk-theme-name', self.origTheme) |
---|
383 | |
---|
384 | |
---|
385 | def afsStatusChanged(self, monitor, file1, file2, evt_type): |
---|
386 | if evt_type == Gio.FileMonitorEvent.CREATED: |
---|
387 | if not self.winMotd.get_property("visible"): |
---|
388 | self.initMotdWindow() |
---|
389 | self.afsAvailable = True |
---|
390 | if evt_type == Gio.FileMonitorEvent.UNMOUNTED: |
---|
391 | self.afsAvailable = False |
---|
392 | |
---|
393 | def _file_changed(self, monitor, file1, file2, evt_type): |
---|
394 | if evt_type == Gio.FileMonitorEvent.CREATED: |
---|
395 | self.loginNotebook.set_current_page(1) |
---|
396 | self.builder.get_object("lblUpdTime").set_text("Update started at %s" % (time.strftime("%Y-%m-%d %H:%M"))) |
---|
397 | if evt_type == Gio.FileMonitorEvent.DELETED: |
---|
398 | self.loginNotebook.set_current_page(0) |
---|
399 | |
---|
400 | # Update the time in the "panel" |
---|
401 | def updateTime(self): |
---|
402 | timeFmt="%a, %b %e %Y %l:%M" + ":%S" if self.timePedantry else "" |
---|
403 | # every second counts |
---|
404 | timeFmt=timeFmt + " %p" |
---|
405 | self.mnuTime.set_label(time.strftime(timeFmt, time.localtime(time.time()))) |
---|
406 | return True |
---|
407 | |
---|
408 | # Reset the UI and prepare for a new login |
---|
409 | def resetLoginWindow(self): |
---|
410 | self.spin(False) |
---|
411 | self.clearMessage() |
---|
412 | self.btnCancel.hide() |
---|
413 | self.sessionBox.hide() |
---|
414 | self.prompted=False |
---|
415 | self.prompt_label.set_text("") |
---|
416 | self.prompt_entry.set_text("") |
---|
417 | self.prompt_box.hide() |
---|
418 | self.btnLogin.grab_focus() |
---|
419 | # Because there's no WM, we need to focus the actual X window |
---|
420 | Gdk.Window.focus(self.winLogin.get_window(), Gdk.CURRENT_TIME) |
---|
421 | |
---|
422 | def getSelectedSession(self): |
---|
423 | i = self.cmbSession.get_active_iter() |
---|
424 | session_name = self.cmbSession.get_model().get_value(i, 1) |
---|
425 | self._debug("selected session is " + session_name) |
---|
426 | return session_name |
---|
427 | |
---|
428 | def startOver(self): |
---|
429 | self.greeter.cancel_authentication() |
---|
430 | self.greeter.authenticate(None) |
---|
431 | |
---|
432 | # LightDM Callbacks |
---|
433 | # The workflow is this: |
---|
434 | # - call .authenticate() with a username |
---|
435 | # - lightdm responds with a prompt for password |
---|
436 | # - call .respond with whatever the user provides |
---|
437 | # - lightdm responds with authentication-complete |
---|
438 | # N.B. complete != successful |
---|
439 | # - .cancel_authentication will cancel the authentication in progress |
---|
440 | # call .authenticate with a new username to restart it |
---|
441 | # |
---|
442 | # Calling .authenticate with None (or NULL in C) will cause lightdm |
---|
443 | # to first prompt for a username, then a password. This means two |
---|
444 | # show-prompt callbacks and thus two .respond calls |
---|
445 | |
---|
446 | # This callback is called when the authentication process is |
---|
447 | # complete. "complete" means a username and password have been |
---|
448 | # received, and PAM has done its thing. "complete" does not |
---|
449 | # mean "successful". |
---|
450 | def cbAuthenticationComplete(self, greeter): |
---|
451 | self.spin(False) |
---|
452 | self._debug("cbAuthenticationComplete: received authentication-complete message") |
---|
453 | if greeter.get_is_authenticated(): |
---|
454 | self.spin(True) |
---|
455 | self._debug("Authentication was successful.") |
---|
456 | session_name = self.getSelectedSession() |
---|
457 | #FIXME: Make sure it's a valid session |
---|
458 | self._debug("User has selected '%s' session" % (session_name)) |
---|
459 | if not greeter.start_session_sync(session_name): |
---|
460 | self._debug("Failed to start session") |
---|
461 | print >> sys.stderr, "Failed to start session" |
---|
462 | elif not self.beingCancelled: |
---|
463 | self._debug("Authentication failed.") |
---|
464 | self.displayMessage("Authentication failed, please try again") |
---|
465 | self.greeter.authenticate(None) |
---|
466 | else: |
---|
467 | self.beingCancelled=False |
---|
468 | self.resetLoginWindow() |
---|
469 | |
---|
470 | # The show-prompt message is emitted when LightDM wants you to |
---|
471 | # show a prompt to the user, and respond with the user's response. |
---|
472 | # Currently, the prompts we care about are "login:" and |
---|
473 | # "Password: " (yes, with the trailing space), which ask for the |
---|
474 | # username and password respectively. promptType is one of |
---|
475 | # LightDM.PromptType.SECRET or LightDM.PromptType.QUESTION, which |
---|
476 | # mean that the text of the user's response should or should not be |
---|
477 | # masked/invisible, respectively. |
---|
478 | |
---|
479 | def cbShowPrompt(self, greeter, text, promptType): |
---|
480 | self._debug("cbShowPrompt: Received show-prompt message: ", |
---|
481 | text, promptType) |
---|
482 | self.prompted=True |
---|
483 | # Make things pretty |
---|
484 | if text == "login:": |
---|
485 | text = "Username: " |
---|
486 | # Sanity check the username |
---|
487 | currUser = self.greeter.get_authentication_user() |
---|
488 | if currUser: |
---|
489 | self._debug("Current user being authenticated is " + currUser) |
---|
490 | # See if the user exists |
---|
491 | try: |
---|
492 | passwd=pwd.getpwnam(currUser) |
---|
493 | except KeyError: |
---|
494 | # Why are we not using the message label here? |
---|
495 | # Because what will happen is that someone will quickly |
---|
496 | # typo their username, and then type their password without |
---|
497 | # looking at the screen, which would otherwise result in the |
---|
498 | # window resetting after the first error, and then they end |
---|
499 | # up typing their password into the username box. |
---|
500 | 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)) |
---|
501 | self.startOver() |
---|
502 | return True |
---|
503 | # There's probably a better way |
---|
504 | if passwd.pw_uid < self.minimumUID: |
---|
505 | self.errDialog("Logging in as '%s' disallowed by configuation" % (currUser)) |
---|
506 | self.startOver() |
---|
507 | return True |
---|
508 | if not self.afsAvailable and (passwd.pw_dir.startswith("/mit") or passwd.pw_dir.startswith("/afs")): |
---|
509 | self.errDialog("Your AFS home directory does not appear to be available. This may indicate a problem with this workstation, or with the AFS servers. Please try another workstation.") |
---|
510 | self.startOver() |
---|
511 | return True |
---|
512 | |
---|
513 | # Set the label to the value of the prompt |
---|
514 | self.prompt_label.set_text(text) |
---|
515 | # clear the entry and get focus |
---|
516 | self.prompt_entry.set_text("") |
---|
517 | self.prompt_entry.set_sensitive(True) |
---|
518 | self.prompt_box.show() |
---|
519 | self.prompt_entry.grab_focus() |
---|
520 | # Mask the input if requested |
---|
521 | if promptType == LightDM.PromptType.SECRET: |
---|
522 | self.prompt_entry.set_visibility(False) |
---|
523 | else: |
---|
524 | self.prompt_entry.set_visibility(True) |
---|
525 | self.spin(False) |
---|
526 | |
---|
527 | # show-message is emitted when LightDM has something to say to the user |
---|
528 | # Typically, these are emitted by PAM modules (e.g. pam_echo) |
---|
529 | # Note that this is _not_ "authentication failed" (unless a PAM module |
---|
530 | # specifically says that). |
---|
531 | # |
---|
532 | # The docs which say to check .is_authenticated() in the |
---|
533 | # authentication-complete callback to determine login success or |
---|
534 | # failure. |
---|
535 | # |
---|
536 | # messageType is one of LightDM.MessageType.{ERROR,INFO} |
---|
537 | def cbShowMessage(self, greeter, text, messageType): |
---|
538 | self._debug("cbShowMessage: Received show-messsage message", |
---|
539 | text, messageType) |
---|
540 | # TODO: Wrap text |
---|
541 | self.displayMessage(text) |
---|
542 | self.spin(False) |
---|
543 | |
---|
544 | def cbKeyPress(self, widget, event): |
---|
545 | if event.keyval == Gdk.KEY_Escape: |
---|
546 | self.cancelLogin(widget) |
---|
547 | |
---|
548 | def cancelLogin(self, widget=None): |
---|
549 | self._debug("Cancelling authentication. User=", |
---|
550 | self.greeter.get_authentication_user()) |
---|
551 | self.beingCancelled=True |
---|
552 | self.greeter.cancel_authentication() |
---|
553 | self.resetLoginWindow() |
---|
554 | |
---|
555 | def displayMessage(self, msg): |
---|
556 | self.message_label.set_text(msg) |
---|
557 | self.message_label.show() |
---|
558 | |
---|
559 | def clearMessage(self): |
---|
560 | self.message_label.set_text("") |
---|
561 | self.message_label.hide() |
---|
562 | |
---|
563 | def spawnBrowser(self, event): |
---|
564 | subprocess.call(KIOSK_LAUNCH_CMD) |
---|
565 | |
---|
566 | def errDialog(self, errText): |
---|
567 | dlg = Gtk.MessageDialog(self.winLogin, |
---|
568 | Gtk.DialogFlags.DESTROY_WITH_PARENT, |
---|
569 | Gtk.MessageType.ERROR, |
---|
570 | Gtk.ButtonsType.CLOSE, |
---|
571 | errText) |
---|
572 | dlg.run() |
---|
573 | dlg.destroy() |
---|
574 | |
---|
575 | |
---|
576 | def spin(self, start): |
---|
577 | if start: |
---|
578 | self.loginSpinner.show() |
---|
579 | self.loginSpinner.start() |
---|
580 | else: |
---|
581 | self.loginSpinner.stop() |
---|
582 | self.loginSpinner.hide() |
---|
583 | |
---|
584 | # Some greeter implementations check .get_is_authenticated() here |
---|
585 | # and then start the session. I think that's only relevant |
---|
586 | # dealing with a user-picker and passwordless users (that is, you |
---|
587 | # would call .authenticate(joeuser), and then click the button, |
---|
588 | # and you'd just be logged in. But we disable the user picker, so |
---|
589 | # that's not relevant. |
---|
590 | def cbLogin(self, widget): |
---|
591 | # Because we just entered some text and are about to send it, |
---|
592 | # we're no longer in the middle of a cancellation |
---|
593 | self.beingCancelled=False |
---|
594 | self.clearMessage() |
---|
595 | self._debug("In cbLogin") |
---|
596 | if self.prompted: |
---|
597 | response = self.prompt_entry.get_text() |
---|
598 | self._debug("Sending response to prompt", response if self.prompt_entry.get_visibility() else "[redacted]") |
---|
599 | self.spin(True) |
---|
600 | self.greeter.respond(response) |
---|
601 | self.prompted=False |
---|
602 | else: |
---|
603 | self._debug("No prompt. Beginning new authentication process.") |
---|
604 | # Show the "Cancel" button" |
---|
605 | self.sessionBox.show() |
---|
606 | self.btnCancel.show() |
---|
607 | self.greeter.authenticate(None) |
---|
608 | |
---|
609 | # Load the Debathena owl image and generate self.logo_pixbufs, the list of |
---|
610 | # animation frames. Returns True if successful, False otherwise. |
---|
611 | def setup_owl(self,logoScale): |
---|
612 | self.logo_pixbufs = [] |
---|
613 | num_pixbufs = 0 |
---|
614 | # Eyes go closed. |
---|
615 | |
---|
616 | for img in self.logoFiles: |
---|
617 | try: |
---|
618 | pixbuf = GdkPixbuf.Pixbuf.new_from_file(img) |
---|
619 | self.logo_pixbufs.append(pixbuf.scale_simple(int(pixbuf.get_width() * logoScale), int(pixbuf.get_height() * logoScale), GdkPixbuf.InterpType.BILINEAR)) |
---|
620 | num_pixbufs += 1 |
---|
621 | except GLib.GError, e: |
---|
622 | print >> sys.stderr, "Glib Error:", e |
---|
623 | return False |
---|
624 | # Eyes come open. |
---|
625 | for pixbuf in self.logo_pixbufs[::-1]: |
---|
626 | self.logo_pixbufs.append(pixbuf) |
---|
627 | num_pixbufs += 1 |
---|
628 | # Eyes stay open. |
---|
629 | self.logo_pixbufs.extend([None] * (self.animation_loop_frames - num_pixbufs)) |
---|
630 | self.img_idx = -1 |
---|
631 | # Set it to the first image so that the window can size itself |
---|
632 | # accordingly |
---|
633 | self.imgDebathena.set_from_pixbuf(self.logo_pixbufs[0]) |
---|
634 | self._debug("Owl setup done") |
---|
635 | return True |
---|
636 | |
---|
637 | def update_owl(self): |
---|
638 | if not self.animate: |
---|
639 | self._debug("Owl loading failed, ending update_owl timer") |
---|
640 | return False |
---|
641 | self.img_idx = (self.img_idx + 1) % self.animation_loop_frames |
---|
642 | pixbuf = self.logo_pixbufs[self.img_idx] |
---|
643 | if pixbuf is not None: |
---|
644 | self.imgDebathena.set_from_pixbuf(pixbuf) |
---|
645 | return True |
---|
646 | |
---|
647 | |
---|
648 | def get_workstation_information(self): |
---|
649 | try: |
---|
650 | self.metapackage = subprocess.Popen(["machtype", "-L"], stdout=subprocess.PIPE).communicate()[0].rstrip() |
---|
651 | except OSError: |
---|
652 | self.metapackage = '(unknown metapackage)' |
---|
653 | try: |
---|
654 | self.baseos = subprocess.Popen(["machtype", "-E"], stdout=subprocess.PIPE).communicate()[0].rstrip() |
---|
655 | except OSError: |
---|
656 | self.baseos = '(unknown OS)' |
---|
657 | |
---|
658 | |
---|
659 | |
---|
660 | |
---|
661 | if __name__ == '__main__': |
---|
662 | parser = OptionParser() |
---|
663 | parser.set_defaults(debug=False) |
---|
664 | parser.add_option("--debug", action="store_true", dest="debug") |
---|
665 | parser.add_option("--ui", action="store", type="string", |
---|
666 | default=UI_FILE, dest="ui_file") |
---|
667 | parser.add_option("--cfg", action="store", type="string", |
---|
668 | default=CONFIG_FILE, dest="config_file") |
---|
669 | (options, args) = parser.parse_args() |
---|
670 | config = ConfigParser.RawConfigParser(CONFIG_DEFAULTS) |
---|
671 | # Hack to create a 'Greeter' section so that we can just use that |
---|
672 | # in any calls |
---|
673 | config.readfp(io.BytesIO("[Greeter]\n")) |
---|
674 | config.read(options.config_file) |
---|
675 | Gtk.init(None); |
---|
676 | main_loop = GObject.MainLoop () |
---|
677 | dagreeter = DebathenaGreeter(options, config) |
---|
678 | # Add a timeout for the owl animation |
---|
679 | GObject.timeout_add(50, dagreeter.update_owl) |
---|
680 | # Add a timeout for the clock in the panel |
---|
681 | GObject.timeout_add(30, dagreeter.updateTime) |
---|
682 | |
---|
683 | main_loop.run () |
---|