Package flumotion :: Package extern :: Package command :: Package command :: Module manholecmd
[hide private]

Source Code for Module flumotion.extern.command.command.manholecmd

  1  # -*- test-case-name: twisted.conch.test.test_manhole -*- 
  2  # Copyright (c) 2001-2007 Twisted Matrix Laboratories. 
  3  # See LICENSE for details. 
  4   
  5  """ 
  6  Line-input oriented interactive interpreter loop. 
  7   
  8  Provides classes for handling Python source input and arbitrary output 
  9  interactively from a Twisted application.  Also included is syntax coloring 
 10  code with support for VT102 terminals, control code handling (^C, ^D, ^Q), 
 11  and reasonable handling of Deferreds. 
 12   
 13  @author: Jp Calderone 
 14  """ 
 15   
 16  import os 
 17  import sys 
 18  import cmd 
 19  import code 
 20  import termios 
 21  import tty 
 22   
 23  from twisted.conch import recvline 
 24  from twisted.internet import stdio, defer 
 25   
 26  from twisted.conch.insults.insults import ServerProtocol 
 27   
 28   
29 -class FileWrapper:
30 """Minimal write-file-like object. 31 32 Writes are translated into addOutput calls on an object passed to 33 __init__. Newlines are also converted from network to local style. 34 """ 35 36 softspace = 0 37 state = 'normal' 38
39 - def __init__(self, o):
40 self.o = o
41
42 - def flush(self):
43 pass
44
45 - def write(self, data):
46 self.o.addOutput(data.replace('\r\n', '\n'))
47
48 - def writelines(self, lines):
49 self.write(''.join(lines))
50 51
52 -class Interpreter(object):
53
54 - def __init__(self, handler, namespace=None):
55 self.handler = handler
56
57 - def push(self, line):
58 raise NotImplementedError
59 60
61 -class ManholeInterpreter(Interpreter, code.InteractiveInterpreter):
62 """Interactive Interpreter with special output and Deferred support. 63 64 Aside from the features provided by L{code.InteractiveInterpreter}, this 65 class captures sys.stdout output and redirects it to the appropriate 66 location (the Manhole protocol instance). It also treats Deferreds 67 which reach the top-level specially: each is formatted to the user with 68 a unique identifier and a new callback and errback added to it, each of 69 which will format the unique identifier and the result with which the 70 Deferred fires and then pass it on to the next participant in the 71 callback chain. 72 """ 73 74 numDeferreds = 0 75 buffer = None 76
77 - def __init__(self, handler, locals=None, filename="<console>"):
78 Interpreter.__init__(self, handler) 79 code.InteractiveInterpreter.__init__(self, locals) 80 self._pendingDeferreds = {} 81 self.filename = filename 82 self.resetBuffer()
83 84 ### code.InteractiveInterpreter methods 85
86 - def runcode(self, *a, **kw):
87 orighook, sys.displayhook = sys.displayhook, self.displayhook 88 try: 89 origout, sys.stdout = sys.stdout, FileWrapper(self.handler) 90 try: 91 code.InteractiveInterpreter.runcode(self, *a, **kw) 92 finally: 93 sys.stdout = origout 94 finally: 95 sys.displayhook = orighook
96
97 - def write(self, data, async=False):
98 self.handler.addOutput(data, async)
99 100 ### Interpreter methods 101
102 - def resetBuffer(self):
103 """Reset the input buffer.""" 104 self.buffer = []
105
106 - def push(self, line):
107 """Push a line to the interpreter. 108 109 The line should not have a trailing newline; it may have 110 internal newlines. The line is appended to a buffer and the 111 interpreter's runsource() method is called with the 112 concatenated contents of the buffer as source. If this 113 indicates that the command was executed or invalid, the buffer 114 is reset; otherwise, the command is incomplete, and the buffer 115 is left as it was after the line was appended. The return 116 value is 1 if more input is required, 0 if the line was dealt 117 with in some way (this is the same as runsource()). 118 119 """ 120 self.buffer.append(line) 121 source = "\n".join(self.buffer) 122 more = self.runsource(source, self.filename) 123 if not more: 124 self.resetBuffer() 125 return more
126 127 128 # FIXME: privatize 129
130 - def displayhook(self, obj):
131 self.locals['_'] = obj 132 if isinstance(obj, defer.Deferred): 133 # XXX Ick, where is my "hasFired()" interface? 134 if hasattr(obj, "result"): 135 self.write(repr(obj)) 136 elif id(obj) in self._pendingDeferreds: 137 self.write("<Deferred #%d>" % ( 138 self._pendingDeferreds[id(obj)][0], )) 139 else: 140 d = self._pendingDeferreds 141 k = self.numDeferreds 142 d[id(obj)] = (k, obj) 143 self.numDeferreds += 1 144 obj.addCallbacks( 145 self._cbDisplayDeferred, self._ebDisplayDeferred, 146 callbackArgs=(k, obj), errbackArgs=(k, obj)) 147 self.write("<Deferred #%d>" % (k, )) 148 elif obj is not None: 149 self.write(repr(obj))
150
151 - def _cbDisplayDeferred(self, result, k, obj):
152 self.write("Deferred #%d called back: %r" % (k, result), True) 153 del self._pendingDeferreds[id(obj)] 154 return result
155
156 - def _ebDisplayDeferred(self, failure, k, obj):
157 self.write("Deferred #%d failed: %r" % ( 158 k, failure.getErrorMessage()), True) 159 del self._pendingDeferreds[id(obj)] 160 return failure
161 162 CTRL_C = '\x03' 163 CTRL_D = '\x04' 164 CTRL_BACKSLASH = '\x1c' 165 CTRL_L = '\x0c' 166 167
168 -class Manhole(recvline.HistoricRecvLine):
169 """Mediator between a fancy line source and an interactive interpreter. 170 171 This accepts lines from its transport and passes them on to a 172 L{ManholeInterpreter}. Control commands (^C, ^D, ^\) are also handled 173 with something approximating their normal terminal-mode behavior. It 174 can optionally be constructed with a dict which will be used as the 175 local namespace for any code executed. 176 """ 177 178 namespace = None 179 interpreterClass = ManholeInterpreter 180
181 - def __init__(self, namespace=None):
182 recvline.HistoricRecvLine.__init__(self) 183 if namespace is not None: 184 self.namespace = namespace.copy() 185 186 self._setupInterpreter()
187
188 - def connectionMade(self):
189 recvline.HistoricRecvLine.connectionMade(self) 190 self.keyHandlers[CTRL_C] = self.handle_INT 191 self.keyHandlers[CTRL_D] = self.handle_EOF 192 self.keyHandlers[CTRL_L] = self.handle_FF 193 self.keyHandlers[CTRL_BACKSLASH] = self.handle_QUIT
194 195 # FIXME: this was in connectionMade, but why ? 196 # Doing it earlier allows us to set prompts from the interpreter 197
198 - def _setupInterpreter(self):
199 self.interpreter = self.interpreterClass(self, self.namespace)
200
201 - def handle_INT(self):
202 """ 203 Handle ^C as an interrupt keystroke by resetting the current input 204 variables to their initial state. 205 """ 206 self.pn = 0 207 self.lineBuffer = [] 208 self.lineBufferIndex = 0 209 self.interpreter.resetBuffer() 210 211 self.terminal.nextLine() 212 self.terminal.write("KeyboardInterrupt") 213 self.terminal.nextLine() 214 self.terminal.write(self.ps[self.pn])
215
216 - def handle_EOF(self):
217 if self.lineBuffer: 218 self.terminal.write('\a') 219 else: 220 self.handle_QUIT()
221
222 - def handle_FF(self):
223 """ 224 Handle a 'form feed' byte - generally used to request a screen 225 refresh/redraw. 226 """ 227 self.terminal.eraseDisplay() 228 self.terminal.cursorHome() 229 self.drawInputLine()
230
231 - def handle_QUIT(self):
232 self.terminal.loseConnection()
233
234 - def _needsNewline(self):
235 w = self.terminal.lastWrite 236 return not w.endswith('\n') and not w.endswith('\x1bE')
237
238 - def addOutput(self, bytes, async=False):
239 if async: 240 self.terminal.eraseLine() 241 self.terminal.cursorBackward( 242 len(self.lineBuffer) + len(self.ps[self.pn])) 243 244 self.terminal.write(bytes) 245 246 if async: 247 if self._needsNewline(): 248 self.terminal.nextLine() 249 250 self.terminal.write(self.ps[self.pn]) 251 252 if self.lineBuffer: 253 oldBuffer = self.lineBuffer 254 self.lineBuffer = [] 255 self.lineBufferIndex = 0 256 257 self._deliverBuffer(oldBuffer)
258
259 - def lineReceived(self, line):
260 d = defer.maybeDeferred(self.interpreter.push, line) 261 262 def cb(more): 263 self.pn = bool(more) 264 if self._needsNewline(): 265 self.terminal.nextLine() 266 self.terminal.write(self.ps[self.pn])
267 d.addCallback(cb) 268 return d
269 270 # gets instantiated when the first command is entered and passed 271 272
273 -class CmdInterpreter(Interpreter):
274 cmdClass = None # subclasses should set this 275 276 _cmd = None 277
278 - def __init__(self, handler, localss=None):
279 Interpreter.__init__(self, handler, localss) 280 # this instantiation is so we can get the prompt; but we don't 281 # have self.handler.terminal yet 282 self._cmd = self.cmdClass() 283 self.handler.ps = (self._cmd.prompt, '... ')
284 285 # FIXME: integrate into Twisted 286
287 - def push(self, line):
288 """ 289 This version of push returns a deferred that will fire when the command 290 is done and the interpreter can show the next prompt. 291 """ 292 293 assert type(line) is not unicode 294 # now we have self.handler.terminal 295 self._cmd = self.cmdClass(stdout=self.handler.terminal) 296 # set stdout on the root command too 297 # FIXME: pokes in internals 298 if hasattr(self._cmd, 'command'): 299 self._cmd.command.getRootCommand()._stdout = self.handler.terminal 300 r = self._cmd.onecmd(line) 301 return r
302 303 # push should only return something if it wants a more prompt 304 305
306 -class CmdManhole(Manhole):
307 308 interpreterClass = CmdInterpreter 309
310 - def __init__(self, namespace=None, connectionLostDeferred=None):
311 """ 312 @param connectionLostDeferred: a deferred that will be fired when 313 the connection is lost, with the reason. 314 """ 315 Manhole.__init__(self, namespace) 316 317 self.connectionLostDeferred = connectionLostDeferred
318
319 - def connectionLost(self, reason):
320 """ 321 When the connection is lost, there is nothing more to do. Stop the 322 reactor so that the process can exit. 323 324 Override me for custom behaviour. 325 """ 326 if not self.connectionLostDeferred: 327 from twisted.internet import reactor 328 reactor.stop() 329 else: 330 self.connectionLostDeferred.callback(reason)
331 332 # we do not want loseConnection to self.reset() and clear the screen 333 334
335 -class CmdServerProtocol(ServerProtocol):
336
337 - def loseConnection(self):
338 self.transport.loseConnection()
339 340
341 -class Stdio(object):
342
343 - def setup(self):
344 self._fd = sys.__stdin__.fileno() 345 self._oldSettings = termios.tcgetattr(self._fd) 346 tty.setraw(self._fd)
347
348 - def connect(self, klass, *args, **kwargs):
349 350 p = CmdServerProtocol(klass, *args, **kwargs) 351 stdio.StandardIO(p)
352
353 - def teardown(self):
354 os.system('stty sane') 355 # we did not actually carriage return the ended prompt 356 print 357 termios.tcsetattr(self._fd, termios.TCSANOW, self._oldSettings)
358 # this clears the screen, 359 # but also fixes some problem when editing history lines 360 # ESC c resets terminal 361 #os.write(self._fd, "\r\x1bc\r") 362 363
364 -def runWithProtocol(klass, *args, **kwargs):
365 s = Stdio() 366 367 s.setup() 368 try: 369 s.connect(klass, *args, **kwargs) 370 371 from twisted.internet import reactor 372 reactor.run() 373 finally: 374 s.teardown()
375 376 if __name__ == '__main__': 377 # classes defined in if to not pollute module namespace 378
379 - class MyCmd(cmd.Cmd):
380 381 prompt = 'My Command Prompt >>> ' 382
383 - def do_test(self, args):
384 self.stdout.write('this is a test\n')
385
386 - def do_defer(self, args):
387 self.stdout.write('this is a test that returns a deferred\n') 388 389 from twisted.internet import defer 390 d = defer.Deferred() 391 392 def cb(_): 393 self.stdout.write('the deferred fired\n')
394 d.addCallback(cb) 395 396 from twisted.internet import reactor 397 reactor.callLater(1, d.callback, None) 398 399 return d
400
401 - class MyCmdInterpreter(CmdInterpreter):
402 cmdClass = MyCmd
403
404 - class MyManhole(CmdManhole):
405 interpreterClass = MyCmdInterpreter
406 407 408 runWithProtocol(MyManhole) 409