| 1 |
# AsynCluster: Node Display Manager (NDM) |
|---|
| 2 |
# A simple X display manager for cluster nodes that also serve as |
|---|
| 3 |
# access-restricted workstations. |
|---|
| 4 |
# |
|---|
| 5 |
# An NDM client runs on each node and communicates via Twisted's Perspective |
|---|
| 6 |
# Broker to the Aysncluster server, which regulates when and how much each user |
|---|
| 7 |
# can use his account on any of the workstations. The NDM server also |
|---|
| 8 |
# dispatches cluster operations to the nodes via the NDM clients, unbeknownst |
|---|
| 9 |
# to the workstation users. |
|---|
| 10 |
# |
|---|
| 11 |
# Copyright (C) 2006-2007 by Edwin A. Suominen, http://www.eepatents.com |
|---|
| 12 |
# |
|---|
| 13 |
# This program is free software; you can redistribute it and/or modify it under |
|---|
| 14 |
# the terms of the GNU General Public License as published by the Free Software |
|---|
| 15 |
# Foundation; either version 2 of the License, or (at your option) any later |
|---|
| 16 |
# version. |
|---|
| 17 |
# |
|---|
| 18 |
# This program is distributed in the hope that it will be useful, but WITHOUT |
|---|
| 19 |
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|---|
| 20 |
# FOR A PARTICULAR PURPOSE. See the file COPYING for more details. |
|---|
| 21 |
# |
|---|
| 22 |
# You should have received a copy of the GNU General Public License along with |
|---|
| 23 |
# this program; if not, write to the Free Software Foundation, Inc., 51 |
|---|
| 24 |
# Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA |
|---|
| 25 |
|
|---|
| 26 |
""" |
|---|
| 27 |
GUI operation of non-headless NDM application. |
|---|
| 28 |
|
|---|
| 29 |
Installs a PyQt4 QApplication() object into Twisted's qtreactor(). |
|---|
| 30 |
""" |
|---|
| 31 |
|
|---|
| 32 |
# Start PyQt4 with Twisted integration |
|---|
| 33 |
from twisted_goodies.qtwisted import qt4reactor |
|---|
| 34 |
from PyQt4.QtGui import QApplication |
|---|
| 35 |
app = QApplication([]) |
|---|
| 36 |
qt4reactor.install(app) |
|---|
| 37 |
|
|---|
| 38 |
# Now the regular imports |
|---|
| 39 |
import os, pwd |
|---|
| 40 |
from twisted.internet import defer, reactor, protocol |
|---|
| 41 |
from PyQt4 import QtCore, QtGui |
|---|
| 42 |
|
|---|
| 43 |
from asyncluster import util |
|---|
| 44 |
|
|---|
| 45 |
|
|---|
| 46 |
class LoginWindow(QtGui.QWidget): |
|---|
| 47 |
""" |
|---|
| 48 |
I am the senior GUI manager for the NDM application, acting as the main |
|---|
| 49 |
window for the QApplication object. |
|---|
| 50 |
""" |
|---|
| 51 |
def __init__(self, main): |
|---|
| 52 |
QtGui.QWidget.__init__(self) |
|---|
| 53 |
self.main = main |
|---|
| 54 |
# Window setup, then widget setup |
|---|
| 55 |
self.setupWindow() |
|---|
| 56 |
self.setupWidgets() |
|---|
| 57 |
self.show() |
|---|
| 58 |
|
|---|
| 59 |
def setupWindow(self): |
|---|
| 60 |
""" |
|---|
| 61 |
Top-level B{window} setup |
|---|
| 62 |
""" |
|---|
| 63 |
# Fixed Size and centered (initial) position |
|---|
| 64 |
size = [int(x) for x in self.main.config['display']['size']] |
|---|
| 65 |
center = [getattr(app.desktop().size(), x)()/2 |
|---|
| 66 |
for x in ('width', 'height')] |
|---|
| 67 |
rect = QtCore.QRect() |
|---|
| 68 |
rect.setWidth(size[0]); rect.setHeight(size[1]) |
|---|
| 69 |
rect.moveCenter(QtCore.QPoint(*center)) |
|---|
| 70 |
self.setGeometry(rect) |
|---|
| 71 |
self.setFixedSize(*size) |
|---|
| 72 |
|
|---|
| 73 |
def setupWidgets(self): |
|---|
| 74 |
""" |
|---|
| 75 |
Top-level B{widget} setup. |
|---|
| 76 |
""" |
|---|
| 77 |
def policy(widget, *policyNames): |
|---|
| 78 |
policies = [getattr(QtGui.QSizePolicy, x) for x in policyNames] |
|---|
| 79 |
widget.setSizePolicy(*policies) |
|---|
| 80 |
|
|---|
| 81 |
self.layout = QtGui.QGridLayout(self) |
|---|
| 82 |
# Labels |
|---|
| 83 |
self.labels = [] |
|---|
| 84 |
labelSpecs = ( |
|---|
| 85 |
("Node Display Manager - User Login", -1, QtCore.Qt.AlignCenter), |
|---|
| 86 |
("User ID:", 1, QtCore.Qt.AlignRight), |
|---|
| 87 |
("Password:", 1, QtCore.Qt.AlignRight)) |
|---|
| 88 |
for labelText, colspan, alignment in labelSpecs: |
|---|
| 89 |
row = len(self.labels) |
|---|
| 90 |
label = QtGui.QLabel(self.tr(labelText)) |
|---|
| 91 |
policy(label, 'Minimum', 'Fixed') |
|---|
| 92 |
self.layout.addWidget(label, row, 0, 1, colspan, alignment) |
|---|
| 93 |
self.labels.append(label) |
|---|
| 94 |
# Text entry boxes for login |
|---|
| 95 |
entrySpecs = ( |
|---|
| 96 |
("user", QtGui.QLineEdit.Normal, 1), |
|---|
| 97 |
("password", QtGui.QLineEdit.Password, 2)) |
|---|
| 98 |
for lineEditorName, echoMode, row in entrySpecs: |
|---|
| 99 |
lineEditor = CustomLineEditor() |
|---|
| 100 |
lineEditor.setEchoMode(echoMode) |
|---|
| 101 |
policy(lineEditor, 'Minimum', 'Fixed') |
|---|
| 102 |
self.layout.addWidget(lineEditor, row, 1, 1, 3) |
|---|
| 103 |
QtCore.QObject.connect( |
|---|
| 104 |
lineEditor, QtCore.SIGNAL("returnPressed()"), self.login) |
|---|
| 105 |
setattr(self, lineEditorName, lineEditor) |
|---|
| 106 |
self.user.setFocus() |
|---|
| 107 |
# That's all |
|---|
| 108 |
|
|---|
| 109 |
def login(self): |
|---|
| 110 |
""" |
|---|
| 111 |
Attempt a user login with the text in the I{user} and I{password} line |
|---|
| 112 |
editors, disabling further logins until the session is over. |
|---|
| 113 |
""" |
|---|
| 114 |
login = [str(getattr(self, attrName).text()) |
|---|
| 115 |
for attrName in ('user', 'password')] |
|---|
| 116 |
self.password.clear() |
|---|
| 117 |
self.main.sessionBegin(*login) |
|---|
| 118 |
|
|---|
| 119 |
|
|---|
| 120 |
class SessionWindow(QtGui.QWidget): |
|---|
| 121 |
""" |
|---|
| 122 |
I am a standalone window that manages the active session and displays its |
|---|
| 123 |
status. |
|---|
| 124 |
""" |
|---|
| 125 |
def __init__(self, main, user): |
|---|
| 126 |
QtGui.QWidget.__init__(self) |
|---|
| 127 |
self.main, self.user = main, user |
|---|
| 128 |
self.setup() |
|---|
| 129 |
del self.minutesLeft |
|---|
| 130 |
self.wmStart() |
|---|
| 131 |
|
|---|
| 132 |
def _getML(self): |
|---|
| 133 |
return self.progressBar.value() |
|---|
| 134 |
def _setML(self, minutes): |
|---|
| 135 |
if not self._setAlready: |
|---|
| 136 |
self._setAlready = True |
|---|
| 137 |
self.progressBar.setMaximum(minutes) |
|---|
| 138 |
self.progressBar.setValue(minutes) |
|---|
| 139 |
def _delML(self): |
|---|
| 140 |
self._setAlready = False |
|---|
| 141 |
minutesLeft = property(_getML, _setML, _delML) |
|---|
| 142 |
|
|---|
| 143 |
def setup(self): |
|---|
| 144 |
""" |
|---|
| 145 |
Window and widget setup. |
|---|
| 146 |
""" |
|---|
| 147 |
def sp(*policyNames): |
|---|
| 148 |
policies = [ |
|---|
| 149 |
getattr(QtGui.QSizePolicy, x) for x in policyNames] |
|---|
| 150 |
w.setSizePolicy(*policies) |
|---|
| 151 |
|
|---|
| 152 |
self.setWindowTitle(self.tr("NDM Session")) |
|---|
| 153 |
layout = QtGui.QGridLayout(self) |
|---|
| 154 |
# Fixed top label |
|---|
| 155 |
text = self.tr("User Session for <b>%s</b>" % self.user) |
|---|
| 156 |
w = self.topLabel = QtGui.QLabel(text) |
|---|
| 157 |
util.biggerFont(w, 2.0) |
|---|
| 158 |
sp('MinimumExpanding', 'Fixed') |
|---|
| 159 |
layout.addWidget(w, 0, 0, 1, 3) |
|---|
| 160 |
# Status display label |
|---|
| 161 |
w = self.statusLabel = QtGui.QLabel() |
|---|
| 162 |
sp('MinimumExpanding', 'Fixed') |
|---|
| 163 |
layout.addWidget(w, 1, 0, 1, 3) |
|---|
| 164 |
# Progress bar for showing time left |
|---|
| 165 |
w = self.progressBar = QtGui.QProgressBar() |
|---|
| 166 |
sp('MinimumExpanding', 'Fixed') |
|---|
| 167 |
layout.addWidget(w, 2, 0, 1, 3) |
|---|
| 168 |
# Logout button |
|---|
| 169 |
# DISABLED - login box doesn't reappear |
|---|
| 170 |
# w = self.quitButton = QtGui.QPushButton(self.tr("&Logout")) |
|---|
| 171 |
# sp('Fixed', 'Fixed') |
|---|
| 172 |
# QtCore.QObject.connect( |
|---|
| 173 |
# w, QtCore.SIGNAL("clicked()"), self.close) |
|---|
| 174 |
# layout.addWidget(w, 3, 1, 1, 1) |
|---|
| 175 |
|
|---|
| 176 |
def update(self, hoursLeft): |
|---|
| 177 |
""" |
|---|
| 178 |
Call this method to updates the status label and progress bar in |
|---|
| 179 |
accordance with the number of hours left, and to end the session when |
|---|
| 180 |
the time's up. |
|---|
| 181 |
""" |
|---|
| 182 |
hrs, minutes = divmod(int(60*hoursLeft), 60) |
|---|
| 183 |
msg = "Remaining: %d:%02d" % (hrs, minutes) |
|---|
| 184 |
minutesLeft = 60*hrs + minutes |
|---|
| 185 |
if minutesLeft < 10: |
|---|
| 186 |
msg += " !!!" |
|---|
| 187 |
self.status(msg) |
|---|
| 188 |
self.minutesLeft = minutesLeft |
|---|
| 189 |
|
|---|
| 190 |
def status(self, msg): |
|---|
| 191 |
""" |
|---|
| 192 |
Updates the status label with the supplied I{msg}. |
|---|
| 193 |
""" |
|---|
| 194 |
if msg.endswith("!"): |
|---|
| 195 |
msg = "<b>%s</b>" % msg |
|---|
| 196 |
self.activateWindow() |
|---|
| 197 |
self.statusLabel.setText(self.tr(msg)) |
|---|
| 198 |
|
|---|
| 199 |
def wmStart(self): |
|---|
| 200 |
""" |
|---|
| 201 |
Spawns a process for the window manager such that the session is ended |
|---|
| 202 |
when the process ends, or vice versa. |
|---|
| 203 |
|
|---|
| 204 |
Adapted from L{twisted.internet.util}. |
|---|
| 205 |
""" |
|---|
| 206 |
p = WindowManagerProcessProtocol() |
|---|
| 207 |
niceness = int(self.main.config['display']['niceness']) |
|---|
| 208 |
windowManager = self.main.config['display']['window manager'] |
|---|
| 209 |
homeDir = os.path.expanduser("~%s" % self.user) |
|---|
| 210 |
env = {'USER': self.user, |
|---|
| 211 |
'LOGNAME': self.user, |
|---|
| 212 |
'HOME': homeDir} |
|---|
| 213 |
for varName in ('DISPLAY', 'PATH', 'TERM', |
|---|
| 214 |
'SHELL', 'LANG', 'LANGUAGE', 'PS1'): |
|---|
| 215 |
if varName in os.environ: |
|---|
| 216 |
env[varName] = os.environ[varName] |
|---|
| 217 |
uid = pwd.getpwnam(self.user)[2] |
|---|
| 218 |
self.process = reactor.spawnProcess( |
|---|
| 219 |
p, windowManager, (windowManager,), |
|---|
| 220 |
env=env, path=homeDir, uid=uid) |
|---|
| 221 |
os.system("renice +%d -u %s" % (niceness, self.user)) |
|---|
| 222 |
p.d.addCallback(lambda _: self.sessionEnd()) |
|---|
| 223 |
|
|---|
| 224 |
def sessionEnd(self): |
|---|
| 225 |
def ended(null): |
|---|
| 226 |
os.system("killall --user %s" % self.user) |
|---|
| 227 |
|
|---|
| 228 |
return self.main.sessionEnd().addCallback(ended) |
|---|
| 229 |
|
|---|
| 230 |
def wmStop(self): |
|---|
| 231 |
""" |
|---|
| 232 |
Kills the window manager process, if it's running. |
|---|
| 233 |
""" |
|---|
| 234 |
if hasattr(self, 'process'): |
|---|
| 235 |
self.process.loseConnection() |
|---|
| 236 |
del self.process |
|---|
| 237 |
os.system("killall --user %s" % self.user) |
|---|
| 238 |
util.log("Killed all user processes") |
|---|
| 239 |
|
|---|
| 240 |
def closeEvent(self, event): |
|---|
| 241 |
""" |
|---|
| 242 |
Called when a session window has been closed. |
|---|
| 243 |
""" |
|---|
| 244 |
self.sessionEnd() |
|---|
| 245 |
|
|---|
| 246 |
|
|---|
| 247 |
class CustomLineEditor(QtGui.QLineEdit): |
|---|
| 248 |
""" |
|---|
| 249 |
Custom line editor to ward off various hacks of login entry boxes. |
|---|
| 250 |
""" |
|---|
| 251 |
def __init__(self): |
|---|
| 252 |
QtGui.QLineEdit.__init__(self) |
|---|
| 253 |
self.setContextMenuPolicy(QtCore.Qt.NoContextMenu) |
|---|
| 254 |
|
|---|
| 255 |
def isRedoAvailable(self): |
|---|
| 256 |
return False |
|---|
| 257 |
|
|---|
| 258 |
def paste(self): |
|---|
| 259 |
pass |
|---|
| 260 |
|
|---|
| 261 |
|
|---|
| 262 |
class WindowManagerProcessProtocol(protocol.ProcessProtocol): |
|---|
| 263 |
def __init__(self): |
|---|
| 264 |
self.d = defer.Deferred() |
|---|
| 265 |
self.errorData = [] |
|---|
| 266 |
|
|---|
| 267 |
def errReceived(self, data): |
|---|
| 268 |
self.errorData.append(data) |
|---|
| 269 |
|
|---|
| 270 |
def processEnded(self, reason): |
|---|
| 271 |
if reason.value.exitCode == 1: |
|---|
| 272 |
util.log("Error running Window manager:\n%s" % \ |
|---|
| 273 |
''.join(self.errorData)) |
|---|
| 274 |
else: |
|---|
| 275 |
util.log("Window manager shutdown with exit code '%s'" \ |
|---|
| 276 |
% reason.value.exitCode) |
|---|
| 277 |
self.d.callback(None) |
|---|