Package flumotion :: Package admin :: Package text :: Module view
[hide private]

Source Code for Module flumotion.admin.text.view

  1  # -*- Mode: Python -*- 
  2  # vi:si:et:sw=4:sts=4:ts=4 
  3   
  4  # Flumotion - a streaming media server 
  5  # Copyright (C) 2004,2005,2006,2007,2008,2009 Fluendo, S.L. 
  6  # Copyright (C) 2010,2011 Flumotion Services, S.A. 
  7  # All rights reserved. 
  8  # 
  9  # This file may be distributed and/or modified under the terms of 
 10  # the GNU Lesser General Public License version 2.1 as published by 
 11  # the Free Software Foundation. 
 12  # This file is distributed without any warranty; without even the implied 
 13  # warranty of merchantability or fitness for a particular purpose. 
 14  # See "LICENSE.LGPL" in the source distribution for more information. 
 15  # 
 16  # Headers in this file shall remain intact. 
 17  4 
 18  """main interface for the cursor admin client""" 
 19   
 20  import curses 
 21  import os 
 22  import string 
 23   
 24  import gobject 
 25  from twisted.internet import reactor 
 26  from twisted.python import rebuild 
 27  from zope.interface import implements 
 28   
 29  from flumotion.common import log, errors, common 
 30  from flumotion.twisted import flavors, reflect 
 31  from flumotion.common.planet import moods 
 32   
 33  from flumotion.admin.text import misc_curses 
 34   
 35  __version__ = "$Rev$" 
 36   
 37   
38 -class AdminTextView(log.Loggable, gobject.GObject, misc_curses.CursesStdIO):
39 40 implements(flavors.IStateListener) 41 42 logCategory = 'admintextview' 43 44 global_commands = ['startall', 'stopall', 'clearall', 'quit'] 45 46 LINES_BEFORE_COMPONENTS = 5 47 LINES_AFTER_COMPONENTS = 6 48
49 - def __init__(self, model, stdscr):
50 self.initialised = False 51 self.stdscr = stdscr 52 self.inputText = '' 53 self.command_result = "" 54 self.lastcommands = [] 55 self.nextcommands = [] 56 self.rows, self.cols = self.stdscr.getmaxyx() 57 self.max_components_per_page = self.rows - \ 58 self.LINES_BEFORE_COMPONENTS - \ 59 self.LINES_AFTER_COMPONENTS 60 self._first_onscreen_component = 0 61 62 self._components = {} 63 self._comptextui = {} 64 self._setAdminModel(model) 65 # get initial info we need 66 self.setPlanetState(self.admin.planet)
67
68 - def _setAdminModel(self, model):
69 self.admin = model 70 71 self.admin.connect('connected', self.admin_connected_cb) 72 self.admin.connect('disconnected', self.admin_disconnected_cb) 73 self.admin.connect('connection-refused', 74 self.admin_connection_refused_cb) 75 self.admin.connect('connection-failed', 76 self.admin_connection_failed_cb) 77 #self.admin.connect('component-property-changed', 78 # self.property_changed_cb) 79 self.admin.connect('update', self.admin_update_cb)
80 81 # show the whole text admin screen 82
83 - def show(self):
84 self.initialised = True 85 self.stdscr.addstr(0, 0, "Main Menu") 86 self.show_components() 87 self.display_status() 88 self.stdscr.move(self.lasty, 0) 89 self.stdscr.clrtoeol() 90 self.stdscr.move(self.lasty+1, 0) 91 self.stdscr.clrtoeol() 92 self.stdscr.addstr(self.lasty+1, 0, "Prompt: %s" % self.inputText) 93 self.stdscr.refresh()
94 #gobject.io_add_watch(0, gobject.IO_IN, self.keyboard_input_cb) 95 96 # show the view of components and their mood 97 # called from show 98
99 - def show_components(self):
100 if self.initialised: 101 self.stdscr.addstr(2, 0, "Components:") 102 # get a dictionary of components 103 names = self._components.keys() 104 names.sort() 105 106 cury = 4 107 108 # if number of components is less than the space add 109 # "press page up for previous components" and 110 # "press page down for next components" lines 111 if len(names) > self.max_components_per_page: 112 if self._first_onscreen_component > 0: 113 self.stdscr.move(cury, 0) 114 self.stdscr.clrtoeol() 115 self.stdscr.addstr(cury, 0, 116 "Press page up to scroll up components list") 117 cury=cury+1 118 cur_component = self._first_onscreen_component 119 for name in names[self._first_onscreen_component:len(names)]: 120 # check if too many components for screen height 121 if cury - self.LINES_BEFORE_COMPONENTS >= \ 122 self.max_components_per_page: 123 self.stdscr.move(cury, 0) 124 self.stdscr.clrtoeol() 125 self.stdscr.addstr(cury, 0, 126 "Press page down to scroll down components list") 127 cury = cury + 1 128 break 129 130 component = self._components[name] 131 mood = component.get('mood') 132 # clear current component line 133 self.stdscr.move(cury, 0) 134 self.stdscr.clrtoeol() 135 # output component name and mood 136 self.stdscr.addstr(cury, 0, "%s: %s" % ( 137 name, moods[mood].name)) 138 cury = cury + 1 139 cur_component = cur_component + 1 140 141 self.lasty = cury
142 #self.stdscr.refresh() 143
144 - def gotEntryCallback(self, result, name):
145 entryPath, filename, methodName = result 146 filepath = os.path.join(entryPath, filename) 147 self.debug('Got the UI for %s and it lives in %s' % (name, filepath)) 148 self.uidir = os.path.split(filepath)[0] 149 #handle = open(filepath, "r") 150 #data = handle.read() 151 #handle.close() 152 153 # try loading the class 154 moduleName = common.pathToModuleName(filename) 155 statement = 'import %s' % moduleName 156 self.debug('running %s' % statement) 157 try: 158 exec(statement) 159 except SyntaxError, e: 160 # the syntax error can happen in the entry file, or any import 161 where = getattr(e, 'filename', "<entry file>") 162 lineno = getattr(e, 'lineno', 0) 163 msg = "Syntax Error at %s:%d while executing %s" % ( 164 where, lineno, filename) 165 self.warning(msg) 166 raise errors.EntrySyntaxError(msg) 167 except NameError, e: 168 # the syntax error can happen in the entry file, or any import 169 msg = "NameError while executing %s: %s" % (filename, 170 " ".join(e.args)) 171 self.warning(msg) 172 raise errors.EntrySyntaxError(msg) 173 except ImportError, e: 174 msg = "ImportError while executing %s: %s" % (filename, 175 " ".join(e.args)) 176 self.warning(msg) 177 raise errors.EntrySyntaxError(msg) 178 179 # make sure we're running the latest version 180 module = reflect.namedAny(moduleName) 181 rebuild.rebuild(module) 182 183 # check if we have the method 184 if not hasattr(module, methodName): 185 self.warning('method %s not found in file %s' % ( 186 methodName, filename)) 187 raise #FIXME: something appropriate 188 klass = getattr(module, methodName) 189 190 # instantiate the GUIClass, giving ourself as the first argument 191 # FIXME: we cheat by giving the view as second for now, 192 # but let's decide for either view or model 193 instance = klass(self._components[name], self.admin) 194 self.debug("Created entry instance %r" % instance) 195 196 #moduleName = common.pathToModuleName(fileName) 197 #statement = 'import %s' % moduleName 198 self._comptextui[name] = instance
199
200 - def gotEntryNoBundleErrback(self, failure, name):
201 failure.trap(errors.NoBundleError) 202 self.debug("No admin ui for component %s" % name)
203
204 - def gotEntrySleepingComponentErrback(self, failure):
205 failure.trap(errors.SleepingComponentError)
206
207 - def getEntry(self, componentState, type):
208 """ 209 Do everything needed to set up the entry point for the given 210 component and type, including transferring and setting up bundles. 211 212 Caller is responsible for adding errbacks to the deferred. 213 214 @returns: a deferred returning (entryPath, filename, methodName) with 215 entryPath: the full local path to the bundle's base 216 fileName: the relative location of the bundled file 217 methodName: the method to instantiate with 218 """ 219 lexicalVariableHack = [] 220 221 def gotEntry(res): 222 fileName, methodName = res 223 lexicalVariableHack.append(res) 224 self.debug("entry for %r of type %s is in file %s and method %s", 225 componentState, type, fileName, methodName) 226 return self.admin.bundleLoader.getBundles(fileName=fileName)
227 228 def gotBundles(res): 229 name, bundlePath = res[-1] 230 fileName, methodName = lexicalVariableHack[0] 231 return (bundlePath, fileName, methodName)
232 233 d = self.admin.callRemote('getEntryByType', 234 componentState.get('type'), type) 235 d.addCallback(gotEntry) 236 d.addCallback(gotBundles) 237 return d 238
239 - def update_components(self, components):
240 for name in self._components.keys(): 241 component = self._components[name] 242 try: 243 component.removeListener(self) 244 except KeyError: 245 # do nothing 246 self.debug("silly") 247 248 def compStateSet(state, key, value): 249 self.log('stateSet: state %r, key %s, value %r' % ( 250 state, key, value)) 251 252 if key == 'mood': 253 # this is needed so UIs load if they change to happy 254 # get bundle for component 255 d = self.getEntry(state, 'admin/text') 256 d.addCallback(self.gotEntryCallback, state.get('name')) 257 d.addErrback(self.gotEntryNoBundleErrback, state.get('name')) 258 d.addErrback(self.gotEntrySleepingComponentErrback) 259 260 self.show() 261 elif key == 'name': 262 if value: 263 self.show()
264 265 self._components = components 266 for name in self._components.keys(): 267 component = self._components[name] 268 component.addListener(self, set_=compStateSet) 269 270 # get bundle for component 271 d = self.getEntry(component, 'admin/text') 272 d.addCallback(self.gotEntryCallback, name) 273 d.addErrback(self.gotEntryNoBundleErrback, name) 274 d.addErrback(self.gotEntrySleepingComponentErrback) 275 276 self.show() 277
278 - def setPlanetState(self, planetState):
279 280 def flowStateAppend(state, key, value): 281 self.debug('flow state append: key %s, value %r' % (key, value)) 282 if state.get('name') != 'default': 283 return 284 if key == 'components': 285 self._components[value.get('name')] = value 286 # FIXME: would be nicer to do this incrementally instead 287 self.update_components(self._components)
288 289 def flowStateRemove(state, key, value): 290 if state.get('name') != 'default': 291 return 292 if key == 'components': 293 name = value.get('name') 294 self.debug('removing component %s' % name) 295 del self._components[name] 296 # FIXME: would be nicer to do this incrementally instead 297 self.update_components(self._components) 298 299 def atmosphereStateAppend(state, key, value): 300 if key == 'components': 301 self._components[value.get('name')] = value 302 # FIXME: would be nicer to do this incrementally instead 303 self.update_components(self._components) 304 305 def atmosphereStateRemove(state, key, value): 306 if key == 'components': 307 name = value.get('name') 308 self.debug('removing component %s' % name) 309 del self._components[name] 310 # FIXME: would be nicer to do this incrementally instead 311 self.update_components(self._components) 312 313 def planetStateAppend(state, key, value): 314 if key == 'flows': 315 if value.get('name') != 'default': 316 return 317 #self.debug('default flow started') 318 value.addListener(self, append=flowStateAppend, 319 remove=flowStateRemove) 320 for c in value.get('components'): 321 flowStateAppend(value, 'components', c) 322 323 def planetStateRemove(state, key, value): 324 self.debug('something got removed from the planet') 325 326 self.debug('parsing planetState %r' % planetState) 327 self._planetState = planetState 328 329 # clear and rebuild list of components that interests us 330 self._components = {} 331 332 planetState.addListener(self, append=planetStateAppend, 333 remove=planetStateRemove) 334 335 a = planetState.get('atmosphere') 336 a.addListener(self, append=atmosphereStateAppend, 337 remove=atmosphereStateRemove) 338 for c in a.get('components'): 339 atmosphereStateAppend(a, 'components', c) 340 341 for f in planetState.get('flows'): 342 planetStateAppend(f, 'flows', f) 343
344 - def _component_stop(self, state):
345 return self._component_do(state, 'Stop', 'Stopping', 'Stopped')
346
347 - def _component_start(self, state):
348 return self._component_do(state, 'Start', 'Starting', 'Started')
349
350 - def _component_do(self, state, action, doing, done):
351 name = state.get('name') 352 if not name: 353 return None 354 355 self.admin.callRemote('component'+action, state)
356
357 - def run_command(self, command):
358 # this decides whether startall, stopall and clearall are allowed 359 can_stop = True 360 can_start = True 361 for x in self._components.values(): 362 mood = moods.get(x.get('mood')) 363 can_stop = can_stop and (mood != moods.lost and 364 mood != moods.sleeping) 365 can_start = can_start and (mood == moods.sleeping) 366 can_clear = can_start and not can_stop 367 368 if string.lower(command) == 'quit': 369 reactor.stop() 370 elif string.lower(command) == 'startall': 371 if can_start: 372 for c in self._components.values(): 373 self._component_start(c) 374 self.command_result = 'Attempting to start all components' 375 else: 376 self.command_result = ( 377 'Components not all in state to be started') 378 379 380 elif string.lower(command) == 'stopall': 381 if can_stop: 382 for c in self._components.values(): 383 self._component_stop(c) 384 self.command_result = 'Attempting to stop all components' 385 else: 386 self.command_result = ( 387 'Components not all in state to be stopped') 388 elif string.lower(command) == 'clearall': 389 if can_clear: 390 self.admin.cleanComponents() 391 self.command_result = 'Attempting to clear all components' 392 else: 393 self.command_result = ( 394 'Components not all in state to be cleared') 395 else: 396 command_split = command.split() 397 # if at least 2 tokens in the command 398 if len(command_split)>1: 399 # check if the first is a component name 400 for c in self._components.values(): 401 if string.lower(c.get('name')) == ( 402 string.lower(command_split[0])): 403 # bingo, we have a component 404 if string.lower(command_split[1]) == 'start': 405 # start component 406 self._component_start(c) 407 elif string.lower(command_split[1]) == 'stop': 408 # stop component 409 self._component_stop(c) 410 else: 411 # component specific commands 412 try: 413 textui = self._comptextui[c.get('name')] 414 415 if textui: 416 d = textui.runCommand( 417 ' '.join(command_split[1:])) 418 self.debug( 419 "textui runcommand defer: %r" % d) 420 # add a callback 421 d.addCallback(self._runCommand_cb) 422 423 except KeyError: 424 pass
425
426 - def _runCommand_cb(self, result):
427 self.command_result = result 428 self.debug("Result received: %s" % result) 429 self.show()
430
431 - def get_available_commands(self, input):
432 input_split = input.split() 433 last_input='' 434 if len(input_split) >0: 435 last_input = input_split[len(input_split)-1] 436 available_commands = [] 437 if len(input_split) <= 1 and not input.endswith(' '): 438 # this decides whether startall, stopall and clearall are allowed 439 can_stop = True 440 can_start = True 441 for x in self._components.values(): 442 mood = moods.get(x.get('mood')) 443 can_stop = can_stop and (mood != moods.lost and 444 mood != moods.sleeping) 445 can_start = can_start and (mood == moods.sleeping) 446 can_clear = can_start and not can_stop 447 448 for command in self.global_commands: 449 command_ok = (command != 'startall' and 450 command != 'stopall' and 451 command != 'clearall') 452 command_ok = command_ok or (command == 'startall' and 453 can_start) 454 command_ok = command_ok or (command == 'stopall' and 455 can_stop) 456 command_ok = command_ok or (command == 'clearall' and 457 can_clear) 458 459 if (command_ok and string.lower(command).startswith( 460 string.lower(last_input))): 461 available_commands.append(command) 462 else: 463 available_commands = (available_commands + 464 self.get_available_commands_for_component( 465 input_split[0], input)) 466 467 return available_commands
468
469 - def get_available_commands_for_component(self, comp, input):
470 self.debug("getting commands for component %s" % comp) 471 commands = [] 472 for c in self._components: 473 if c == comp: 474 component_commands = ['start', 'stop'] 475 textui = None 476 try: 477 textui = self._comptextui[comp] 478 except KeyError: 479 self.debug("no text ui for component %s" % comp) 480 481 input_split = input.split() 482 483 if len(input_split) >= 2 or input.endswith(' '): 484 for command in component_commands: 485 if len(input_split) == 2: 486 if command.startswith(input_split[1]): 487 commands.append(command) 488 elif len(input_split) == 1: 489 commands.append(command) 490 if textui: 491 self.debug( 492 "getting component commands from ui of %s" % comp) 493 comp_input = ' '.join(input_split[1:]) 494 if input.endswith(' '): 495 comp_input = comp_input + ' ' 496 commands = commands + textui.getCompletions(comp_input) 497 498 return commands
499
500 - def get_available_completions(self, input):
501 completions = self.get_available_commands(input) 502 503 # now if input has no spaces, add the names of each component that 504 # starts with input 505 if len(input.split()) <= 1: 506 for c in self._components: 507 if c.startswith(input): 508 completions.append(c) 509 510 return completions
511
512 - def display_status(self):
513 availablecommands = self.get_available_commands(self.inputText) 514 available_commands = ' '.join(availablecommands) 515 #for command in availablecommands: 516 # available_commands = '%s %s' % (available_commands, command) 517 self.stdscr.move(self.lasty+2, 0) 518 self.stdscr.clrtoeol() 519 520 self.stdscr.addstr(self.lasty+2, 0, 521 "Available Commands: %s" % available_commands) 522 # display command results 523 self.stdscr.move(self.lasty+3, 0) 524 self.stdscr.clrtoeol() 525 self.stdscr.move(self.lasty+4, 0) 526 self.stdscr.clrtoeol() 527 528 if self.command_result != "": 529 self.stdscr.addstr(self.lasty+4, 530 0, "Result: %s" % self.command_result) 531 self.stdscr.clrtobot()
532 533 ### admin model callbacks 534
535 - def admin_connected_cb(self, admin):
536 self.info('Connected to manager') 537 538 # get initial info we need 539 self.setPlanetState(self.admin.planet) 540 541 if not self._components: 542 self.debug('no components detected, running wizard') 543 # ensure our window is shown 544 self.show()
545
546 - def admin_disconnected_cb(self, admin):
547 message = "Lost connection to manager, reconnecting ..." 548 print message
549
550 - def admin_connection_refused_cb(self, admin):
551 log.debug('textadminclient', "handling connection-refused") 552 #reactor.callLater(0, self.admin_connection_refused_later, admin) 553 log.debug('textadminclient', "handled connection-refused")
554
555 - def admin_connection_failed_cb(self, admin):
556 log.debug('textadminclient', "handling connection-failed") 557 #reactor.callLater(0, self.admin_connection_failed_later, admin) 558 log.debug('textadminclient', "handled connection-failed")
559
560 - def admin_update_cb(self, admin):
561 self.update_components(self._components)
562
563 - def connectionLost(self, why):
564 # do nothing 565 pass
566
567 - def whsStateAppend(self, state, key, value):
568 if key == 'names': 569 self.debug('Worker %s logged in.' % value)
570
571 - def whsStateRemove(self, state, key, value):
572 if key == 'names': 573 self.debug('Worker %s logged out.' % value)
574 575 # act as keyboard input 576
577 - def doRead(self):
578 """ Input is ready! """ 579 c = self.stdscr.getch() # read a character 580 581 if c == curses.KEY_BACKSPACE or c == 127: 582 self.inputText = self.inputText[:-1] 583 elif c == curses.KEY_STAB or c == 9: 584 available_commands = self.get_available_completions(self.inputText) 585 if len(available_commands) == 1: 586 input_split = self.inputText.split() 587 if len(input_split) > 1: 588 if not self.inputText.endswith(' '): 589 input_split.pop() 590 self.inputText = ( 591 ' '.join(input_split) + ' ' + available_commands[0]) 592 else: 593 self.inputText = available_commands[0] 594 595 elif c == curses.KEY_ENTER or c == 10: 596 # run command 597 self.run_command(self.inputText) 598 # re-display status 599 self.display_status() 600 # clear the prompt line 601 self.stdscr.move(self.lasty+1, 0) 602 self.stdscr.clrtoeol() 603 self.stdscr.addstr(self.lasty+1, 0, 'Prompt: ') 604 self.stdscr.refresh() 605 if len(self.nextcommands) > 0: 606 self.lastcommands = self.lastcommands + self.nextcommands 607 self.nextcommands = [] 608 self.lastcommands.append(self.inputText) 609 self.inputText = '' 610 self.command_result = '' 611 elif c == curses.KEY_UP: 612 lastcommand = "" 613 if len(self.lastcommands) > 0: 614 lastcommand = self.lastcommands.pop() 615 if self.inputText != "": 616 self.nextcommands.append(self.inputText) 617 self.inputText = lastcommand 618 elif c == curses.KEY_DOWN: 619 nextcommand = "" 620 if len(self.nextcommands) > 0: 621 nextcommand = self.nextcommands.pop() 622 if self.inputText != "": 623 self.lastcommands.append(self.inputText) 624 self.inputText = nextcommand 625 elif c == curses.KEY_PPAGE: # page up 626 if self._first_onscreen_component > 0: 627 self._first_onscreen_component = \ 628 self._first_onscreen_component - 1 629 self.show() 630 elif c == curses.KEY_NPAGE: # page down 631 if self._first_onscreen_component < len(self._components) - \ 632 self.max_components_per_page: 633 self._first_onscreen_component = \ 634 self._first_onscreen_component + 1 635 self.show() 636 637 else: 638 # too long 639 if len(self.inputText) == self.cols-2: 640 return 641 # add to input text 642 if c<=256: 643 self.inputText = self.inputText + chr(c) 644 645 # redisplay status 646 self.display_status() 647 648 self.stdscr.move(self.lasty+1, 0) 649 self.stdscr.clrtoeol() 650 651 self.stdscr.addstr(self.lasty+1, 0, 'Prompt: %s' % self.inputText) 652 self.stdscr.refresh()
653 654 655 # remote calls 656 # eg from components notifying changes 657
658 - def componentCall(self, componentState, methodName, *args, **kwargs):
659 # FIXME: for now, we only allow calls to go through that have 660 # their UI currently displayed. In the future, maybe we want 661 # to create all UI's at startup regardless and allow all messages 662 # to be processed, since they're here now anyway 663 self.log("componentCall received for %r.%s ..." % ( 664 componentState, methodName)) 665 localMethodName = "component_%s" % methodName 666 name = componentState.get('name') 667 668 try: 669 textui = self._comptextui[name] 670 except KeyError: 671 return 672 673 if not hasattr(textui, localMethodName): 674 self.log("... but does not have method %s" % localMethodName) 675 self.warning("Component view %s does not implement %s" % ( 676 name, localMethodName)) 677 return 678 self.log("... and executing") 679 method = getattr(textui, localMethodName) 680 681 # call the method, catching all sorts of stuff 682 try: 683 result = method(*args, **kwargs) 684 except TypeError: 685 msg = ("component method %s did not" 686 " accept *a %s and **kwa %s (or TypeError)") % ( 687 methodName, args, kwargs) 688 self.debug(msg) 689 raise errors.RemoteRunError(msg) 690 self.log("component: returning result: %r to caller" % result) 691 return result
692