import repository from arizona
[raven.git] / apps / ravenpublish / konggui.py
1 #! /usr/bin/env python
2
3 import functools
4 import os
5 import pickle
6 import platform
7 import select
8 import shutil
9 import signal
10 import sys
11 import threading
12 import time
13 import traceback
14 import urllib2
15 import urlparse
16 from PyQt4.QtCore import *
17 from PyQt4.QtGui import *
18
19 from errorparser import ErrorParser
20 import slicerun
21 import container
22 import kongclient
23 import raven
24 import ravenlib.crypto
25 import ravenlib.files.tpparse
26 import ravenlib.package.storkpackage
27
28 import ravengui_passphrase
29
30 def Error(parent, text):
31     QMessageBox.warning(parent, "Error", text)
32
33 def Warning(parent, text):
34     QMessageBox.warning(parent, "Warning", text)
35
36 def Confirm(parent, text):
37     box = QMessageBox(parent)
38     box.setText(text)
39     box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
40     ret = box.exec_()
41     return (ret == QMessageBox.Yes)
42
43 def QStringListToStringList(x):
44     l = list(x)
45     l2 = []
46     for item in l:
47         l2.append(str(item))
48     return l2
49
50 def get_channel_dict(entry, channel):
51     if "channels" in entry:
52         return entry["channels"].get(channel, {})
53     else:
54         return {}
55
56 class DummyWriter():
57     def __init__(self, func):
58         self.writeFunc = func
59         self.messageBuf = ""
60
61     def write(self, x):
62         self.messageBuf = self.messageBuf + x
63         while ("\n" in self.messageBuf):
64             (line, self.messageBuf) = self.messageBuf.split("\n",1)
65             self.writeLine(line)
66
67     def writeLine(self, x):
68         self.writeFunc(x)
69
70     def flush(self): pass
71     def close(self): pass
72
73 class FuncRunnerThread(QThread):
74     def __init__(self, func, parent):
75         super(FuncRunnerThread, self).__init__(parent)
76
77         self.aborted = False
78         self.func = func
79         self.messageBuf = ""
80
81     def run(self):
82         try:
83             self.result = self.func()
84         except kongclient.KongClientTerminated:
85             self.result = None
86             self.aborted = True
87         self.emit(SIGNAL("finished(PyQt_PyObject)"), self)
88
89     def createStream(self):
90         return DummyWriter(self.writeLine)
91
92     def writeLine(self, line):
93         self.emit(SIGNAL("print(QString)"), QString(line))
94
95 class OverlayWidget(QWidget):
96     def __init__(self, parent):
97         super(OverlayWidget, self).__init__(parent)
98         self.font = QFont()
99         self.font.setBold(True)
100         self.font.setPixelSize(48)
101         self.text=""
102
103     def paintEvent(self, event):
104         if self.text:
105             painter = QPainter(self)
106             painter.setFont(self.font)
107             painter.setOpacity(0.2)
108             painter.drawText(self.rect(), Qt.AlignCenter, self.text)
109
110     def setText(self, text):
111         self.text = text
112         # even if we don't paint, if the widget isn't hidden, then it'll
113         # intercept the mouse.
114         if text:
115             self.show()
116         else:
117             self.hide()
118         self.update()
119
120 class TableWithOverlay(QTableWidget):
121     def __init__(self):
122         super(TableWithOverlay, self).__init__()
123         self.overlay = OverlayWidget(self)
124
125     def resizeEvent(self, event):
126         self.overlay.resize(event.size())
127
128     def setOverlayText(self, text):
129         self.overlay.setText(text)
130
131 class KongStatusThread(QThread):
132     def __init__(self, parent):
133         super(KongStatusThread, self).__init__(parent)
134
135         self.container = None
136
137         self.mutex = QMutex()
138         self.semaphoreRun = QSemaphore()
139         self.stopSignal = False
140
141         self.start()
142
143     def run(self):
144         while True:
145             # wait for a signal and get the container
146             self.semaphoreRun.acquire()
147             self.mutex.lock()
148             container = self.container
149             kongtool = self.kongtool
150             self.mutex.unlock()
151
152             if self.stopSignal:
153                 return
154
155             self.emit(SIGNAL("starting()"))
156
157             nodes = kongtool.get_clients()
158             channel = kongtool.get_channel_data()
159
160             self.emit(SIGNAL("newKongData(PyQt_PyObject)"), (nodes, channel))
161
162     def stop(self):
163         # set stopSignal to tell the thread to quit, and signal the semaphore
164         # so it wakes up
165         self.stopSignal=True
166         self.semaphoreRun.release()
167         # now, wait for the thread to exit
168         self.wait()
169
170     def setContainer(self, container, kongtool):
171         self.mutex.lock()
172         self.container = container
173         self.kongtool = kongtool
174         self.mutex.unlock()
175
176     def runNow(self):
177         self.semaphoreRun.release()
178
179 class KongWidget(QWidget):
180     def __init__(self, parent=None):
181         super(KongWidget, self).__init__(parent)
182         self.setup()
183
184         self.kongStatusThread = KongStatusThread(self)
185         self.connect(self.kongStatusThread, SIGNAL("newKongData(PyQt_PyObject)"), self.onNewNodeData)
186         self.connect(self.kongStatusThread, SIGNAL("starting()"), self.onNodeReaderStarting)
187
188         self.container = None
189
190         self.dir = ""
191
192         self.settingsFileName = os.path.expanduser("~/.ravengui")
193
194         self.loadAndOpen()
195
196         # updateNodeData() will run every 5 seconds. If there is no container
197         # (i.e. experiment not loaded yet), then it will do nothing.
198         self.updateTimer = QTimer(self)
199         self.connect(self.updateTimer, SIGNAL("timeout()"), self.updateNodeData)
200         self.updateTimer.start(5000)
201
202     def loadAndOpen(self):
203         self.loadSettings()
204
205         # if we saved a directory from last time, open it
206         if self.dir:
207             self.openContainer()
208
209     def cleanup(self):
210         if self.kongStatusThread:
211             self.kongStatusThread.stop()
212             self.kongStatusThread=None
213
214     def loadSettings(self):
215         if os.path.exists(self.settingsFileName):
216             parser = ErrorParser()
217             parser.Read(self.settingsFileName, True)
218             dir = parser.GetOpt("lastsession", "dir", "")
219
220             if os.path.exists(dir) and os.path.exists(os.path.join(dir,"raven.conf")):
221                 self.dir = dir
222
223     def saveSettings(self):
224         parser = ErrorParser()
225         parser.add_section("lastsession")
226         parser.set("lastsession", "dir", self.dir)
227         parser.write(open(self.settingsFileName, "w"))
228
229     def setup(self):
230         #labelChannel = QLabel("Channel:")
231         #self.labelServerChannel = QLabel("")
232         labelState = QLabel("Server State:")
233         self.labelServerState = QLabel("")
234
235         layoutHeader = QHBoxLayout()
236         #layoutHeader.addWidget(labelChannel)
237         #layoutHeader.addWidget(self.labelServerChannel)
238         layoutHeader.addWidget(labelState)
239         layoutHeader.addWidget(self.labelServerState)
240         layoutHeader.addStretch()
241
242         # node list
243         self.labelNodes = QLabel("Nodes:")
244         self.tableNodes = TableWithOverlay()
245         self.tableNodes.setSelectionBehavior(QAbstractItemView.SelectRows)
246         self.tableNodes.setSelectionMode(QAbstractItemView.SingleSelection)
247         self.tableNodes.setMinimumHeight(150)
248         self.labelNodes.setBuddy(self.tableNodes)
249
250         labelMessages = QLabel("Messages:")
251         self.browserMessages = QTextBrowser()
252         labelMessages.setBuddy(self.browserMessages)
253
254         labelMinNodes = QLabel("Min Nodes:")
255         self.spinBoxMinNodes = QSpinBox()
256         labelMaxNodes = QLabel("Max Nodes:")
257         self.spinBoxMaxNodes = QSpinBox()
258
259         labelExperiment = QLabel("Experiment Configuration:")
260         layoutExperiment = QHBoxLayout()
261         layoutExperiment.addWidget(labelMinNodes)
262         layoutExperiment.addWidget(self.spinBoxMinNodes)
263         layoutExperiment.addWidget(labelMaxNodes)
264         layoutExperiment.addWidget(self.spinBoxMaxNodes)
265         layoutExperiment.addStretch()
266         groupBoxExperiment = QFrame() # QGroupBox("Experiment Configuration")
267         groupBoxExperiment.setLayout(layoutExperiment)
268
269         # button bar
270         buttonInitialize = QPushButton("&Initialize")
271         buttonPrepare = QPushButton("&Prepare")
272         buttonRun = QPushButton("&Run")
273         buttonReset = QPushButton("Reset")
274         buttonAuto = QPushButton("Auto")
275         buttonRefresh = QPushButton("Refresh")
276         self.buttonAbort = QPushButton("Abort")
277         self.buttonAbort.setVisible(False)
278         layoutRavenButtons = QHBoxLayout()
279         layoutRavenButtons.addWidget(buttonInitialize)
280         layoutRavenButtons.addWidget(buttonPrepare)
281         layoutRavenButtons.addWidget(buttonRun)
282         layoutRavenButtons.addWidget(buttonReset)
283         layoutRavenButtons.addWidget(buttonAuto)
284         layoutRavenButtons.addWidget(buttonRefresh)
285         layoutRavenButtons.addWidget(self.buttonAbort)
286         layoutRavenButtons.addStretch()
287
288         layoutBody = QVBoxLayout()
289         layoutBody.addLayout(layoutHeader)
290         layoutBody.addWidget(labelExperiment)
291         layoutBody.addWidget(groupBoxExperiment)
292         layoutBody.addWidget(self.labelNodes)
293         layoutBody.addWidget(self.tableNodes)
294         layoutBody.addWidget(labelMessages)
295         layoutBody.addWidget(self.browserMessages)
296         layoutBody.addLayout(layoutRavenButtons)
297
298         self.frameBody = QFrame()
299         self.frameBody.setLayout(layoutBody)
300         self.frameBody.setFrameStyle(QFrame.NoFrame)
301         self.frameBody.setContentsMargins(0,0,0,0)
302
303         self.labelNoContainer = QLabel("Use the raven tool to create/open an experiment first")
304
305         layoutMain = QVBoxLayout()
306         layoutMain.addWidget(self.labelNoContainer)
307         layoutMain.addWidget(self.frameBody)
308         layoutMain.setContentsMargins(0,0,0,0) # for frameBody
309         self.setLayout(layoutMain)
310
311         self.frameBody.hide()
312
313         self.backgroundOps = []
314
315         self.connect(buttonInitialize, SIGNAL("clicked()"), self.onInitializeClicked)
316         self.connect(buttonPrepare, SIGNAL("clicked()"), self.onPrepareClicked)
317         self.connect(buttonRun, SIGNAL("clicked()"), self.onRunClicked)
318         self.connect(buttonAuto, SIGNAL("clicked()"), self.onAutoClicked)
319         self.connect(buttonReset, SIGNAL("clicked()"), self.onResetClicked)
320         self.connect(self.buttonAbort, SIGNAL("clicked()"), self.onAbortClicked)
321         self.connect(buttonRefresh, SIGNAL("clicked()"), self.onRefreshClicked)
322
323     def openContainer(self):
324         c = container.container()
325         c.set_dir(self.dir)
326         if not c.exists():
327             Error(self, "No container in directory " + self.dir)
328             sys.exit(-1)
329         c.load()
330
331         self.labelNoContainer.hide()
332         self.frameBody.show()
333
334         self.container = c
335
336         self.kongclient = kongclient.KongClient(configdir = self.container.get_kongdir())
337
338         #self.labelExperimentName.setText("Experiment: " + c.get_name())
339
340         self.updateNodeData()
341
342         # save the new starting container directory
343         self.saveSettings()
344
345     def configKongTool(self, kongtool):
346         kongtool.set_minClients(self.spinBoxMinNodes.value())
347         kongtool.set_maxClients(self.spinBoxMaxNodes.value())
348
349     def checkBackground(self):
350         if self.backgroundOps:
351             Warning(self, "Please wait until the current operation has completed")
352             return False
353         return True
354
355     def onInitializeClicked(self, nextCommand=None):
356         if not self.checkBackground():
357             return
358         kongtool = self.kongclient.clone()
359         self.configKongTool(kongtool)
360         self.wrap(None, kongtool, "setstate(initialize)", functools.partial(kongtool.setstate, "initialize"), nextCommand)
361
362     def onPrepareClicked(self, nextCommand=None):
363         if not self.checkBackground():
364             return
365         kongtool = self.kongclient.clone()
366         self.configKongTool(kongtool)
367         self.wrap(None, kongtool, "setstate(prepare)", functools.partial(kongtool.setstate, "prepare"), nextCommand)
368
369     def onRunClicked(self, nextCommand=None):
370         if not self.checkBackground():
371             return
372         kongtool = self.kongclient.clone()
373         self.configKongTool(kongtool)
374         self.wrap(None, kongtool, "setstate(run)", functools.partial(kongtool.setstate, "run"), nextCommand)
375
376     def onResetClicked(self):
377         if not self.checkBackground():
378             return
379         kongtool = self.kongclient.clone()
380         self.configKongTool(kongtool)
381         self.wrap(None, kongtool, "setstate(reset)", functools.partial(kongtool.setstate, "reset"))
382
383     def onAutoClicked(self):
384         if not self.checkBackground():
385             return
386         runCommand = functools.partial(self.onRunClicked)
387         prepareCommand = functools.partial(self.onPrepareClicked, nextCommand=runCommand)
388         initializeCommand = functools.partial(self.onInitializeClicked, nextCommand=prepareCommand)
389
390         initializeCommand()
391
392     def onRefreshClicked(self):
393         self.updateNodeData()
394
395     def onAbortClicked(self):
396         for item in self.backgroundOps:
397             if item.get("nextCommand", None) != None:
398                 item["nextCommand"] = None
399             abortFunc = item.get("abortFunc", None)
400             if abortFunc:
401                 abortFunc()
402
403
404     def updateAbortButton(self):
405         #abortableThreads=[]
406         #for item in self.backgroundOps:
407         #    if item.get("nextCommand", None) != None:
408         #        abortableThreads.append(item)
409
410         self.buttonAbort.setVisible(self.backgroundOps != []) #abortableThreads != [])
411
412     def wrap(self, raventool, kongtool, description, func, nextCommand=None):
413         t = FuncRunnerThread(func, self)
414
415         abortFunc = None
416
417         if raventool:
418             raventool.setStdoutStream(t.createStream())
419
420         if kongtool:
421             abortFunc = kongtool.terminate
422             kongtool.setStdoutStream(t.createStream())
423
424         self.connect(t, SIGNAL("finished(PyQt_PyObject)"), self.onThreadFinished)
425         self.connect(t, SIGNAL("print(QString)"), self.onMessagePrint)
426         self.backgroundOps.append( {"description": description, "thread": t, "nextCommand": nextCommand, "abortFunc": abortFunc} )
427
428         self.updateAbortButton()
429
430         t.start()
431
432     def onMessagePrint(self, line):
433         self.browserMessages.append(line)
434
435     def onThreadFinished(self, thread):
436         for proc in self.backgroundOps:
437             if proc.get("thread", None) == thread:
438                 self.backgroundOps.remove(proc)
439                 if thread.aborted:
440                     self.browserMessages.append("Operation terminated: " + str(proc["description"]))
441                 else:
442                     self.browserMessages.append("Operation completed: " + str(proc["description"]))
443                 self.disconnect(thread, SIGNAL("finished(PyQt_PyObject)"), self.onThreadFinished)
444                 self.disconnect(thread, SIGNAL("print(QString)"), self.onMessagePrint)
445
446                 nextCommand = proc.get("nextCommand", None)
447                 if nextCommand:
448                     nextCommand()
449
450                 self.updateAbortButton()
451
452                 return
453         print "unknown child completed:", thread
454
455     def onChildExit(self, pid):
456         for proc in self.backgroundOps:
457             if proc.get("pid", -1) == pid:
458                 self.backgroundOps.remove(proc)
459                 self.browserMessages.append("Operation completed: " + str(proc["description"]) + " (pid " + str(pid) + ")")
460                 #print "COMPLETE:", proc
461                 return
462         print "unknown child completed:", pid
463
464     def addTableItem(self, table, row, col, val, data=None, readonly=True):
465         item = QTableWidgetItem(str(val))
466         if readonly:
467             item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
468         if data:
469             if not isinstance(data, str):
470                data = pickle.dumps(data)
471             item.setData(Qt.UserRole, QVariant(data))
472         table.setItem(row, col, item)
473
474     def updateNodeData(self):
475         if self.container:
476             self.kongStatusThread.setContainer(self.container, self.kongclient.clone())
477             self.kongStatusThread.runNow()
478
479     def onNodeReaderStarting(self):
480         #self.tableNodes.setOverlayText("Refreshing...")
481         pass
482
483     def onNewNodeData(self, data):
484         (nodes, channel) = data
485
486         #self.labelServerChannel.setText(self.kongclient.channel)
487         self.labelServerState.setText("/".join(channel.get("server_states",[])))
488
489         self.tableNodes.clear()
490
491         nodes = [nodes[key] for key in nodes.keys()]
492         nodes.sort(key = lambda node: (node.get("hostname", node["addr"][0]), node.get("port",0)))
493         userdataKeys = []
494
495         # find all of the userdata keys
496         for node in nodes:
497             channel_dict = get_channel_dict(node, self.kongclient.channel)
498             for key in channel_dict.get("userdata",{}).keys():
499                 if not key in userdataKeys:
500                     userdataKeys.append(key)
501
502         userdataKeys.sort()
503
504         # put the mem_ keys up front
505         userdataKeys = [k for k in userdataKeys if k.startswith("mem_")] + \
506                        [k for k in userdataKeys if not k.startswith("mem_")]
507
508         self.tableNodes.setColumnCount(3 + len(userdataKeys))
509         self.tableNodes.setHorizontalHeaderLabels(["name", "age", "state"] + userdataKeys)
510
511         self.tableNodes.setRowCount(len(nodes))
512
513         row = 0
514         for entry in nodes:
515             (hostname, port) = entry["addr"]
516             if "hostname" in entry:
517                 hostname = entry["hostname"]
518
519             channel_dict = get_channel_dict(entry, self.kongclient.channel)
520
521             self.addTableItem(self.tableNodes, row, 0, hostname + ":" + str(port), entry)
522             self.addTableItem(self.tableNodes, row, 1, str(entry.get("age",0)), entry)
523
524             if channel_dict:
525                 client_states = channel_dict.get("client_states", [])
526                 self.addTableItem(self.tableNodes, row, 2, "/".join(client_states))
527
528                 userdata = channel_dict.get("userdata", {})
529                 for index,key in enumerate(userdataKeys):
530                     if key in userdata:
531                         self.addTableItem(self.tableNodes, row, 3+index, str(userdata[key]))
532
533             row = row + 1
534
535         self.tableNodes.resizeColumnsToContents()
536
537         self.tableNodes.setOverlayText("")
538
539 class KongMainWindow(QMainWindow):
540     def __init__(self, gacks=None, parent=None):
541         super(KongMainWindow, self).__init__(parent)
542         self.widget = KongWidget(parent)
543
544         # quick hack for testing the new dialog
545         if "--fancyaddnamedialog" in [x.lower() for x in sys.argv]:
546            self.widget.fancyAddNameDialog = True
547
548         self.setMinimumWidth(800)
549
550         self.setCentralWidget(self.widget)
551
552         #set_passphrase_callback(ask_passphrase)
553
554         ravengui_passphrase.setup(self)
555
556     def event(self, event):
557         if (event!=None) and (event.type() == ravengui_passphrase.PassPhraseEventType):
558             #print "receive event"
559             if event.wasIncorrect:
560                 QMessageBox.warning(None, "Warning", "Passphrase incorrect. Please try again.")
561             (s,okClicked) = QInputDialog.getText(self, "Passphrase Required", "Passphrase:")
562             if okClicked:\r
563                 event.set_passphrase(str(s))\r
564             event.signal()\r
565             return True
566         else:
567             return QWidget.event(self, event)
568
569     def closeEvent(self, event):
570         self.widget.cleanup()
571         super(KongMainWindow, self).closeEvent(event)
572
573 def main():
574     app = QApplication(sys.argv)
575     form = KongMainWindow()
576     form.show()
577     app.exec_()
578
579 if __name__=="__main__":
580     main()
581