root/projects/AsynCluster/trunk/console

Revision 175, 16.7 kB (checked in by edsuom, 4 months ago)

Got D-Kernel PMC running with what I hope is Rao-Blackwellization

Line 
1 #!/usr/bin/env python
2 #
3 # AsynCluster: Master
4 # A cluster management server based on Twisted's Perspective Broker. Dispatches
5 # cluster jobs and regulates when and how much each user can use his account on
6 # any of the cluster node workstations.
7 #
8 # Copyright (C) 2006-2007 by Edwin A. Suominen, http://www.eepatents.com
9 #
10 # This program is free software; you can redistribute it and/or modify it under
11 # the terms of the GNU General Public License as published by the Free Software
12 # Foundation; either version 2 of the License, or (at your option) any later
13 # version.
14 #
15 # This program is distributed in the hope that it will be useful, but WITHOUT
16 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
17 # FOR A PARTICULAR PURPOSE.  See the file COPYING for more details.
18 #
19 # You should have received a copy of the GNU General Public License along with
20 # this program; if not, write to the Free Software Foundation, Inc., 51
21 # Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22
23 """
24 An interface console to the controller.
25 """
26
27 import os, sys, re
28 from textwrap import wrap
29 from optparse import OptionParser
30 from signal import signal, SIGWINCH
31 from fcntl import ioctl
32 from tty import TIOCGWINSZ
33 from struct import unpack
34
35 import configobj
36
37 from twisted.python.failure import Failure
38 from twisted.internet import reactor, defer
39 from twisted.spread import pb
40
41 from twisted.conch import stdio
42 from twisted.conch.insults.insults import TerminalProtocol, privateModes
43 from twisted.conch.insults.window import TopWindow, VBox, TextInput, TextOutput
44
45
46 CONNECT_DELAY = 2.0
47 CONFIG_PATH = "/etc/asyncluster.conf"
48
49
50 class Client(object):
51     """
52     I connect to the master TCP server via the UNIX socket control interface.
53     """
54     root = None
55    
56     def __init__(self, socket):
57         if not os.path.exists(socket):
58             raise RuntimeError(
59                 "No UNIX socket available at '%s'" % socket)
60         self.socket = socket
61    
62     def connect(self):
63         """
64         Makes the UNIX socket connection, storing a remote reference to the
65         server's control root object if the connection is successful. Returns a
66         deferred that fires C{True} if so, and I am thus ready to accept
67         commands as a result, or C{False} otherwise.
68         """
69         def gotAnswer(answer):
70             if pb.IUnjellyable.providedBy(answer):
71                 self.root = answer
72                 return True
73             self.connector.disconnect()
74             return False
75
76         factory = pb.PBClientFactory()
77         self.connector = reactor.connectUNIX(self.socket, factory)
78         return factory.getRootObject().addBoth(gotAnswer)
79
80     def disconnect(self):
81         """
82         Disconnects from the master TCP server, returning a deferred that fires
83         when the disconnection is complete. Before the TCP disconnection
84         occurs, any jobs that are running are allowed to finish and any active
85         session is ended.
86         """
87         self.root = None
88         self.connector.disconnect()
89
90     def _jobCode(self, filePath):
91         """
92         """
93         fh = open(filePath)
94         result = fh.read()
95         fh.close()
96         return result
97
98     def oops(self, msg):
99         return defer.succeed("ERROR: %s" % msg)
100
101     def command(self, cmd, *args):
102         """
103         """
104         caller = getattr(self.root, 'callRemote', None)
105         if not callable(caller):
106             return self.oops("No control connection available")
107
108         if cmd == 'user':
109             cmd = 'userAction'
110         elif cmd == 'wall':
111             args = (" ".join(args),)
112         elif cmd == 'resetup':
113             srcDir = args[0]
114             if not os.path.isdir(srcDir):
115                 return self.oops("Source directory '%s' not found" % srcDir)
116         return caller(cmd, *args)
117
118
119 class History(object):
120     """
121     Self-contained input history representation.
122
123     @type beforeLines: C{list} of C{str}
124     @ivar beforeLines: The lines in this history which come before the current position.
125
126     @type afterLines: C{list} of C{str}
127     @ivar afterLines: The lines in this history which come after the current position.
128     """
129     def __init__(self, lines=None):
130         if lines is None:
131             lines = []
132         self.beforeLines = lines
133         self.afterLines = []
134
135     def nextLine(self):
136         """
137         Advance the position by one and return the line there, or an empty
138         string if there is no next line.
139         """
140         if self.afterLines:
141             self.beforeLines.append(self.afterLines.pop(0))
142             if self.afterLines:
143                 return self.afterLines[0]
144             return ""
145         return ""
146
147     def previousLine(self):
148         """
149         Rewind the position by one and return the line there, or an empty
150         string if there is no previous line.
151         """
152         if self.beforeLines:
153             self.afterLines.insert(0, self.beforeLines.pop())
154             return self.afterLines[0]
155         return ""
156
157     def allLines(self):
158         """
159         Return a list of all lines in this history object.
160         """
161         return self.beforeLines + self.afterLines
162
163     def addLine(self, line):
164         """
165         Add a new line to the end of this history object.
166         """
167         if self.afterLines:
168             self.afterLines.append(line)
169         else:
170             self.beforeLines.append(line)
171
172     def resetPosition(self):
173         """
174         Set the position in the input history to the end.
175         """
176         self.beforeLines.extend(self.afterLines)
177         self.afterLines = []
178
179
180 class LineInputWidget(TextInput):
181     """
182     Single-line input area with history and function keys.
183
184     @ivar previousKeystroke: A reference to the most recently received
185     keystroke, updated after each keystroke is processed.
186
187     @ivar killRing: A list of killed strings, in order of oldest to newest.
188
189     @type savedBuffer: C{NoneType} or C{str}
190     @ivar savedBuffer: The string in the edit buffer at the time a history
191     traversal command was first invoked, or C{None} if the history is not
192     currently being traversed.
193
194     """
195     previousKeystroke = None
196     savedBuffer = None
197
198     def __init__(self, maxWidth, onSubmit):
199         self._realSubmit = onSubmit
200         self.killRing = []
201         self.setInputHistory(History())
202         super(LineInputWidget, self).__init__(maxWidth, self._onSubmit)
203
204     def setInputHistory(self, history):
205         """
206         Set the complete input history to the given history object.
207         """
208         self.inputHistory = history
209
210     def getInputHistory(self):
211         """
212         Retrieve a list of lines representing the current input history.
213         """
214         return self.inputHistory.allLines()
215
216     def _onSubmit(self, line):
217         """
218         Clear the current buffer and call the submit handler specified when
219         this widget was created.
220         """
221         if line:
222             self.inputHistory.addLine(line)
223             self.inputHistory.resetPosition()
224             self.setText('')
225         self._realSubmit(line)
226
227     def func_HOME(self, modifier):
228         """
229         Handle the home function key by repositioning the cursor at the
230         beginning of the input area.
231         """
232         self.cursor = 0
233
234     def func_CTRL_a(self):
235         """
236         Handle C-a in the same way as the home function key.
237         """
238         return self.func_HOME(None)
239
240     def func_CTRL_f(self):
241         """
242         Handle C-f to move the cursor forward one position.
243         """
244         self.cursor = min(self.cursor + 1, len(self.buffer))
245
246     def func_CTRL_b(self):
247         """
248         Handle C-b to move the cursor forward one position.
249         """
250         self.cursor = max(self.cursor - 1, 0)
251
252     def func_END(self, modifier):
253         """
254         Handle the end function key by repositioning the cursor just past the
255         end of the text in the input area.
256         """
257         self.cursor = len(self.buffer)
258
259     def func_CTRL_e(self):
260         """
261         Handle C-e in the same way as the end function key.
262         """
263         return self.func_END(None)
264
265     def func_ALT_b(self):
266         """
267         Handle M-b by moving the cursor to the beginning of the word under the
268         current cursor position.  Do nothing at the beginning of the line.
269         Words are considered non-whitespace characters delimited by whitespace
270         characters.
271         """
272         while self.cursor > 0 and self.buffer[self.cursor - 1].isspace():
273             self.cursor -= 1
274         while self.cursor > 0 and not self.buffer[self.cursor - 1].isspace():
275             self.cursor -= 1
276
277     def func_ALT_f(self):
278         """
279         Handle M-f by moving the cursor to just after the end of the word under
280         the current cursor position.  Do nothing at the end of the line.  Words
281         are considered non-whitespace characters delimited by whitespace
282         characters.
283         """
284         n = len(self.buffer)
285         while self.cursor < n and self.buffer[self.cursor].isspace():
286             self.cursor += 1
287         while self.cursor < n and not self.buffer[self.cursor].isspace():
288             self.cursor += 1
289
290     def func_CTRL_k(self):
291         """
292         Handle C-k by truncating the line from the character beneath the cursor
293         and adding the removed text to the kill ring.
294         """
295         chopped = self.buffer[self.cursor:]
296         self.buffer = self.buffer[:self.cursor]
297         if chopped:
298             self.killRing.append(chopped)
299
300     def func_CTRL_y(self):
301         """
302         Handle C-y by inserting an element from the kill ring at the current
303         cursor position, moving the cursor to the end of the inserted text.
304         """
305         if self.killRing:
306             insert = self.killRing[-1]
307             self.buffer = self.buffer[:self.cursor] + insert + self.buffer[self.cursor:]
308             self.cursor += len(insert)
309
310     def func_ALT_y(self):
311         """
312         Handle M-y by cycling the kill ring and replacing the previously yanked
313         text with the new final element in the ring.
314         """
315         if self.previousKeystroke in (('\x19', None), ('y', ServerProtocol.ALT)): # C-y and M-y
316             previous = self.killRing.pop()
317             self.killRing.insert(0, previous)
318             next = self.killRing[-1]
319
320             self.cursor -= len(previous)
321             self.buffer  = self.buffer[:self.cursor] \
322                            + next + self.buffer[self.cursor + len(previous):]
323             self.cursor += len(next)
324
325     def func_CTRL_p(self):
326         """
327         Handle C-p to swap the current input buffer with the previous line from
328         input history.
329         """
330         if not self.inputHistory.afterLines:
331             # Going from normal editing to history traversal - save the edit
332             # buffer.
333             self.savedBuffer = self.buffer
334         previousLine = self.inputHistory.previousLine()
335         if previousLine:
336             self.buffer = previousLine
337
338     def func_CTRL_n(self):
339         """
340         Handle C-n to swap the current input buffer with the next line from
341         input history.
342         """
343         nextLine = self.inputHistory.nextLine()
344         if nextLine:
345             self.buffer = nextLine
346         else:
347             if self.savedBuffer is not None:
348                 self.buffer = self.savedBuffer
349                 self.savedBuffer = None
350
351     def keystrokeReceived(self, keyID, modifier):
352         """
353         Override the inherited behavior to track whether either the cursor
354         position or buffer contents change and automatically request a repaint
355         if either does.
356         """
357         buffer = self.buffer
358         cursor = self.cursor
359         super(LineInputWidget, self).keystrokeReceived(keyID, modifier)
360         self.previousKeystroke = (keyID, modifier)
361         if self.buffer != buffer or self.cursor != cursor:
362             self.repaint()
363
364     def characterReceived(self, keyID, modifier):
365         """
366         Handle a single non-function key, possibly with a modifier.
367
368         If there is no modifier, let the super class handle this.  Otherwise,
369         dispatch to a function for the specific key and modifier present.
370         """
371         if modifier is not None:
372             f = getattr(self, 'func_' + modifier.name + '_' + keyID, None)
373             if f is not None:
374                 f()
375         elif ord(keyID) <= 26 and keyID != '\r':
376             f = getattr(self, 'func_CTRL_' + chr(ord(keyID) + ord('a') - 1), None)
377             if f is not None:
378                 f()
379         else:
380             super(LineInputWidget, self).characterReceived(keyID, modifier)
381
382
383 class OutputWidget(TextOutput):
384     def __init__(self, size=None):
385         super(OutputWidget, self).__init__(size)
386         self.messages = []
387
388     def formatMessage(self, s, width):
389         return wrap(s, width=width, subsequent_indent="  ")
390
391     def addMessage(self, message):
392         self.messages.append(message)
393         self.repaint()
394
395     def render(self, width, height, terminal):
396         output = []
397         for i in xrange(len(self.messages) - 1, -1, -1):
398             output[:0] = self.formatMessage(self.messages[i], width - 2)
399             if len(output) >= height:
400                 break
401         if len(output) < height:
402             output[:0] = [''] * (height - len(output))
403         for n, L in enumerate(output):
404             terminal.cursorPosition(0, n)
405             terminal.write(L + ' ' * (width - len(L)))
406
407
408 class UserInterface(TerminalProtocol):
409     """
410     Set up an input area and an output area for the console.
411     """
412     width, height = 80, 24
413    
414     reactor = reactor
415     ready = False
416
417     def _get_ID(self):
418         self._ID = (getattr(self, '_ID', 0) + 1) % 99
419         return self._ID
420     ID = property(_get_ID)
421
422     def _get_config(self):
423         return configobj.ConfigObj(CONFIG_PATH)
424     config = property(_get_config)
425
426     def connectionMade(self):
427         """
428         Called when the terminal is connected.
429         """
430         super(UserInterface, self).connectionMade()
431         self.terminal.eraseDisplay()
432         self.terminal.resetPrivateModes([privateModes.CURSOR_MODE])
433
434         self.rootWidget = TopWindow(
435             self._painter, lambda f: self.reactor.callLater(0, f))
436         self.rootWidget.reactor = self.reactor
437         vbox = VBox()
438         vbox.addChild(OutputWidget())
439         vbox.addChild(LineInputWidget(self.width-2, self.parseInputLine))
440         self.rootWidget.addChild(vbox)
441         self.reactor.callLater(CONNECT_DELAY, self.startup)
442
443     def _painter(self):
444         self.rootWidget.draw(self.width, self.height, self.terminal)
445
446     def addOutputMessage(self, msg):
447         return self.rootWidget.children[0].children[0].addMessage(msg)
448
449     def startup(self):
450         def doneTrying(success):
451             self.ready = success
452             if success:
453                 msg = "+++ Connected to Master Control Server +++"
454             else:
455                 msg = "--- Couldn't connect to Master Control Server! ---"
456             self.addOutputMessage(msg)
457
458         self.client = Client(self.config['common']['socket'])
459         return self.client.connect().addCallback(doneTrying)
460
461     def shutdown(self):
462         def disconnected(null):
463             self.terminal.setPrivateModes([privateModes.CURSOR_MODE])
464             self.terminal.loseConnection()
465
466         return self.client.disconnect().addCallback(disconnected)
467
468     def parseInputLine(self, line):
469         """
470         """
471         def gotResult(result, ID):
472             self.addOutputMessage("<- %02d: %s" % (ID, str(result)))
473
474         def gotFailure(failure, ID):
475             msg = failure.getTraceback()
476             self.addOutputMessage("<- %02d: ERROR: %s" % (ID, msg))
477
478         tokens = line.split()
479         if tokens[0] in ('q', 'quit'):
480             return self.shutdown()
481
482         ID = self.ID
483         self.addOutputMessage("%02d -> %s" % (ID, line))
484        
485         d = self.client.command(*tokens)
486         d.addCallback(gotResult, ID)
487         d.addErrback(gotFailure, ID)
488
489     def keystrokeReceived(self, keyID, modifier):
490         self.rootWidget.keystrokeReceived(keyID, modifier)
491
492     def terminalSize(self, width, height):
493         self.width = width
494         self.height = height
495         self._painter()
496
497
498 class CommandLineUserInterface(UserInterface):
499     """
500     """
501     def connectionMade(self):
502         signal(SIGWINCH, self.windowChanged)
503         winSize = self.getWindowSize()
504         self.width = winSize[0]
505         self.height = winSize[1]
506         super(CommandLineUserInterface, self).connectionMade()
507
508     def connectionLost(self, reason):
509         reactor.stop()
510
511     # XXX Should be part of runWithProtocol
512     def getWindowSize(self):
513         winsz = ioctl(0, TIOCGWINSZ, '12345678')
514         winSize = unpack('4H', winsz)
515         newSize = winSize[1], winSize[0], winSize[3], winSize[2]
516         return newSize
517
518     def windowChanged(self, signum, frame):
519         winSize = self.getWindowSize()
520         self.terminalSize(winSize[0], winSize[1])
521
522
523 if __name__ == '__main__':
524     stdio.runWithProtocol(CommandLineUserInterface)
Note: See TracBrowser for help on using the browser.