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