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   
 22  import gobject 
 23  import os 
 24  import string 
 25  import curses 
 26   
 27  from twisted.internet import reactor 
 28  from twisted.python import rebuild 
 29  from zope.interface import implements 
 30   
 31  from flumotion.admin.admin import AdminModel 
 32  from flumotion.common import log, errors, worker, planet, common 
 33  from flumotion.configure import configure 
 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 -class AdminTextView(log.Loggable, gobject.GObject, misc_curses.CursesStdIO):
40 41 implements(flavors.IStateListener) 42 43 logCategory = 'admintextview' 44 45 global_commands = [ 'startall', 'stopall', 'clearall', 'quit' ] 46 47 LINES_BEFORE_COMPONENTS = 5 48 LINES_AFTER_COMPONENTS = 6 49
50 - def __init__(self, model, stdscr):
51 self.initialised = False 52 self.stdscr = stdscr 53 self.inputText = '' 54 self.command_result = "" 55 self.lastcommands = [] 56 self.nextcommands = [] 57 self.rows, self.cols = self.stdscr.getmaxyx() 58 self.max_components_per_page = self.rows - \ 59 self.LINES_BEFORE_COMPONENTS - \ 60 self.LINES_AFTER_COMPONENTS 61 self._first_onscreen_component = 0 62 63 self._components = {} 64 self._comptextui = {} 65 self._setAdminModel(model) 66 # get initial info we need 67 self.setPlanetState(self.admin.planet)
68 69
70 - def _setAdminModel(self, model):
71 self.admin = model 72 73 self.admin.connect('connected', self.admin_connected_cb) 74 self.admin.connect('disconnected', self.admin_disconnected_cb) 75 self.admin.connect('connection-refused', 76 self.admin_connection_refused_cb) 77 self.admin.connect('connection-failed', 78 self.admin_connection_failed_cb) 79 #self.admin.connect('component-property-changed', 80 # self.property_changed_cb) 81 self.admin.connect('update', self.admin_update_cb)
82 83 # show the whole text admin screen
84 - def show(self):
85 self.initialised = True 86 self.stdscr.addstr(0,0, "Main Menu") 87 self.show_components() 88 self.display_status() 89 self.stdscr.move(self.lasty,0) 90 self.stdscr.clrtoeol() 91 self.stdscr.move(self.lasty+1,0) 92 self.stdscr.clrtoeol() 93 self.stdscr.addstr(self.lasty+1,0, "Prompt: %s" % self.inputText) 94 self.stdscr.refresh()
95 #gobject.io_add_watch(0, gobject.IO_IN, self.keyboard_input_cb) 96 97 # show the view of components and their mood 98 # called from show
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" % (name, moods[mood].name)) 137 cury = cury + 1 138 cur_component = cur_component + 1 139 140 self.lasty = cury
141 #self.stdscr.refresh() 142 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
201 - def gotEntryNoBundleErrback(self, failure, name):
202 failure.trap(errors.NoBundleError) 203 self.debug("No admin ui for component %s" % name)
204
205 - def gotEntrySleepingComponentErrback(self, failure):
206 failure.trap(errors.SleepingComponentError)
207
208 - def getEntry(self, componentState, type):
209 """ 210 Do everything needed to set up the entry point for the given 211 component and type, including transferring and setting up bundles. 212 213 Caller is responsible for adding errbacks to the deferred. 214 215 @returns: a deferred returning (entryPath, filename, methodName) with 216 entryPath: the full local path to the bundle's base 217 fileName: the relative location of the bundled file 218 methodName: the method to instantiate with 219 """ 220 lexicalVariableHack = [] 221 222 def gotEntry(res): 223 fileName, methodName = res 224 lexicalVariableHack.append(res) 225 self.debug("entry for %r of type %s is in file %s and method %s", 226 componentState, type, fileName, methodName) 227 return self.bundleLoader.getBundles(fileName=fileName)
228 229 def gotBundles(res): 230 name, bundlePath = res[-1] 231 fileName, methodName = lexicalVariableHack[0] 232 return (bundlePath, fileName, methodName)
233 234 d = self.admin.callRemote('getEntryByType', componentState, 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' % (state, key, value)) 250 251 if key == 'mood': 252 # this is needed so UIs load if they change to happy 253 # get bundle for component 254 d = self.admin.getEntry(state, 'admin/text') 255 d.addCallback(self.gotEntryCallback, state.get('name')) 256 d.addErrback(self.gotEntryNoBundleErrback, state.get('name')) 257 d.addErrback(self.gotEntrySleepingComponentErrback) 258 259 self.show() 260 elif key == 'name': 261 if value: 262 self.show()
263 264 self._components = components 265 for name in self._components.keys(): 266 component = self._components[name] 267 component.addListener(self, compStateSet) 268 269 # get bundle for component 270 d = self.admin.getEntry(component, 'admin/text') 271 d.addCallback(self.gotEntryCallback, name) 272 d.addErrback(self.gotEntryNoBundleErrback, name) 273 d.addErrback(self.gotEntrySleepingComponentErrback) 274 275 self.show() 276
277 - def setPlanetState(self, planetState):
278 def flowStateAppend(state, key, value): 279 self.debug('flow state append: key %s, value %r' % (key, value)) 280 if state.get('name') != 'default': 281 return 282 if key == 'components': 283 self._components[value.get('name')] = value 284 # FIXME: would be nicer to do this incrementally instead 285 self.update_components(self._components)
286 287 def flowStateRemove(state, key, value): 288 if state.get('name') != 'default': 289 return 290 if key == 'components': 291 name = value.get('name') 292 self.debug('removing component %s' % name) 293 del self._components[name] 294 # FIXME: would be nicer to do this incrementally instead 295 self.update_components(self._components) 296 297 def atmosphereStateAppend(state, key, value): 298 if key == 'components': 299 self._components[value.get('name')] = value 300 # FIXME: would be nicer to do this incrementally instead 301 self.update_components(self._components) 302 303 def atmosphereStateRemove(state, key, value): 304 if key == 'components': 305 name = value.get('name') 306 self.debug('removing component %s' % name) 307 del self._components[name] 308 # FIXME: would be nicer to do this incrementally instead 309 self.update_components(self._components) 310 311 def planetStateAppend(state, key, value): 312 if key == 'flows': 313 if value.get('name') != 'default': 314 return 315 #self.debug('default flow started') 316 value.addListener(self, flowStateAppend, 317 flowStateRemove) 318 for c in value.get('components'): 319 flowStateAppend(value, 'components', c) 320 321 def planetStateRemove(state, key, value): 322 self.debug('something got removed from the planet') 323 324 self.debug('parsing planetState %r' % planetState) 325 self._planetState = planetState 326 327 # clear and rebuild list of components that interests us 328 self._components = {} 329 330 planetState.addListener(self, append=planetStateAppend, 331 remove=planetStateRemove) 332 333 a = planetState.get('atmosphere') 334 a.addListener(self, append=atmosphereStateAppend, 335 remove=atmosphereStateRemove) 336 for c in a.get('components'): 337 atmosphereStateAppend(a, 'components', c) 338 339 for f in planetState.get('flows'): 340 planetStateAppend(f, 'flows', f) 341
342 - def _component_stop(self, state):
343 return self._component_do(state, 'Stop', 'Stopping', 'Stopped')
344
345 - def _component_start(self, state):
346 return self._component_do(state, 'Start', 'Starting', 'Started')
347
348 - def _component_do(self, state, action, doing, done):
349 name = state.get('name') 350 if not name: 351 return None 352 353 self.admin.callRemote('component'+action, state)
354
355 - def run_command(self, command):
356 # this decides whether startall, stopall and clearall are allowed 357 can_stop = True 358 can_start = True 359 for x in self._components.values(): 360 mood = moods.get(x.get('mood')) 361 can_stop = can_stop and (mood != moods.lost and mood != moods.sleeping) 362 can_start = can_start and (mood == moods.sleeping) 363 can_clear = can_start and not can_stop 364 365 if string.lower(command) == 'quit': 366 reactor.stop() 367 elif string.lower(command) == 'startall': 368 if can_start: 369 for c in self._components.values(): 370 self._component_start(c) 371 self.command_result = 'Attempting to start all components' 372 else: 373 self.command_result = 'Components not all in state to be started' 374 375 376 elif string.lower(command) == 'stopall': 377 if can_stop: 378 for c in self._components.values(): 379 self._component_stop(c) 380 self.command_result = 'Attempting to stop all components' 381 else: 382 self.command_result = 'Components not all in state to be stopped' 383 elif string.lower(command) == 'clearall': 384 if can_clear: 385 self.admin.cleanComponents() 386 self.command_result = 'Attempting to clear all components' 387 else: 388 self.command_result = 'Components not all in state to be cleared' 389 else: 390 command_split = command.split() 391 # if at least 2 tokens in the command 392 if len(command_split)>1: 393 # check if the first is a component name 394 for c in self._components.values(): 395 if string.lower(c.get('name')) == string.lower(command_split[0]): 396 # bingo, we have a component 397 if string.lower(command_split[1]) == 'start': 398 # start component 399 self._component_start(c) 400 elif string.lower(command_split[1]) == 'stop': 401 # stop component 402 self._component_stop(c) 403 else: 404 # component specific commands 405 try: 406 textui = self._comptextui[c.get('name')] 407 408 if textui: 409 d = textui.runCommand(' '.join(command_split[1:])) 410 self.debug("textui runcommand defer: %r" % d) 411 # add a callback 412 d.addCallback(self._runCommand_cb) 413 414 except KeyError: 415 pass
416 417
418 - def _runCommand_cb(self, result):
419 self.command_result = result 420 self.debug("Result received: %s" % result) 421 self.show()
422 423 424 425
426 - def get_available_commands(self, input):
427 input_split = input.split() 428 last_input='' 429 if len(input_split) >0: 430 last_input = input_split[len(input_split)-1] 431 available_commands = [] 432 if len(input_split) <= 1 and not input.endswith(' '): 433 # this decides whether startall, stopall and clearall are allowed 434 can_stop = True 435 can_start = True 436 for x in self._components.values(): 437 mood = moods.get(x.get('mood')) 438 can_stop = can_stop and (mood != moods.lost and mood != moods.sleeping) 439 can_start = can_start and (mood == moods.sleeping) 440 can_clear = can_start and not can_stop 441 442 for command in self.global_commands: 443 command_ok = (command != 'startall' and command != 'stopall' and command != 'clearall') 444 command_ok = command_ok or (command == 'startall' and can_start) 445 command_ok = command_ok or (command == 'stopall' and can_stop) 446 command_ok = command_ok or (command == 'clearall' and can_clear) 447 448 if command_ok and string.lower(command).startswith(string.lower(last_input)): 449 available_commands.append(command) 450 else: 451 available_commands = available_commands + self.get_available_commands_for_component(input_split[0], input) 452 453 return available_commands
454
455 - def get_available_commands_for_component(self, comp, input):
456 self.debug("getting commands for component %s" % comp) 457 commands = [] 458 for c in self._components: 459 if c == comp: 460 component_commands = [ 'start', 'stop' ] 461 textui = None 462 try: 463 textui = self._comptextui[comp] 464 except KeyError: 465 self.debug("no text ui for component %s" % comp) 466 467 input_split = input.split() 468 469 if len(input_split) >= 2 or input.endswith(' '): 470 for command in component_commands: 471 if len(input_split) == 2: 472 if command.startswith(input_split[1]): 473 commands.append(command) 474 elif len(input_split) == 1: 475 commands.append(command) 476 if textui: 477 self.debug("getting component commands from ui of %s" % comp) 478 comp_input = ' '.join(input_split[1:]) 479 if input.endswith(' '): 480 comp_input = comp_input + ' ' 481 commands = commands + textui.getCompletions(comp_input) 482 483 return commands
484 485
486 - def get_available_completions(self,input):
487 completions = self.get_available_commands(input) 488 489 # now if input has no spaces, add the names of each component that starts with input 490 if len(input.split()) <= 1: 491 for c in self._components: 492 if c.startswith(input): 493 completions.append(c) 494 495 return completions
496
497 - def display_status(self):
498 availablecommands = self.get_available_commands(self.inputText) 499 available_commands = ' '.join(availablecommands) 500 #for command in availablecommands: 501 # available_commands = '%s %s' % (available_commands, command) 502 self.stdscr.move(self.lasty+2,0) 503 self.stdscr.clrtoeol() 504 505 self.stdscr.addstr(self.lasty+2, 0, 506 "Available Commands: %s" % available_commands) 507 # display command results 508 self.stdscr.move(self.lasty+3,0) 509 self.stdscr.clrtoeol() 510 self.stdscr.move(self.lasty+4,0) 511 self.stdscr.clrtoeol() 512 513 if self.command_result != "": 514 self.stdscr.addstr(self.lasty+4, 0, "Result: %s" % self.command_result) 515 self.stdscr.clrtobot()
516 517 ### admin model callbacks
518 - def admin_connected_cb(self, admin):
519 self.info('Connected to manager') 520 521 # get initial info we need 522 self.setPlanetState(self.admin.planet) 523 524 if not self._components: 525 self.debug('no components detected, running wizard') 526 # ensure our window is shown 527 self.show()
528
529 - def admin_disconnected_cb(self, admin):
530 message = "Lost connection to manager, reconnecting ..." 531 print message
532
533 - def admin_connection_refused_cb(self, admin):
534 log.debug('textadminclient', "handling connection-refused") 535 #reactor.callLater(0, self.admin_connection_refused_later, admin) 536 log.debug('textadminclient', "handled connection-refused")
537
538 - def admin_connection_failed_cb(self, admin):
539 log.debug('textadminclient', "handling connection-failed") 540 #reactor.callLater(0, self.admin_connection_failed_later, admin) 541 log.debug('textadminclient', "handled connection-failed")
542
543 - def admin_update_cb(self, admin):
544 self.update_components(self._components)
545
546 - def connectionLost(self, why):
547 # do nothing 548 pass
549
550 - def whsStateAppend(self, state, key, value):
551 if key == 'names': 552 self.debug('Worker %s logged in.' % value)
553
554 - def whsStateRemove(self, state, key, value):
555 if key == 'names': 556 self.debug('Worker %s logged out.' % value)
557 558 # act as keyboard input
559 - def doRead(self):
560 """ Input is ready! """ 561 try: 562 c = self.stdscr.getch() # read a character 563 564 if c == curses.KEY_BACKSPACE or c == 127: 565 self.inputText = self.inputText[:-1] 566 elif c == curses.KEY_STAB or c == 9: 567 available_commands = self.get_available_completions(self.inputText) 568 if len(available_commands) == 1: 569 input_split = self.inputText.split() 570 if len(input_split) > 1: 571 if not self.inputText.endswith(' '): 572 input_split.pop() 573 self.inputText = ' '.join(input_split) + ' ' + available_commands[0] 574 else: 575 self.inputText = available_commands[0] 576 577 elif c == curses.KEY_ENTER or c == 10: 578 # run command 579 self.run_command(self.inputText) 580 # re-display status 581 self.display_status() 582 # clear the prompt line 583 self.stdscr.move(self.lasty+1,0) 584 self.stdscr.clrtoeol() 585 self.stdscr.addstr(self.lasty+1,0,'Prompt: ') 586 self.stdscr.refresh() 587 if len(self.nextcommands) > 0: 588 self.lastcommands = self.lastcommands + self.nextcommands 589 self.nextcommands = [] 590 self.lastcommands.append(self.inputText) 591 self.inputText = '' 592 self.command_result = '' 593 elif c == curses.KEY_UP: 594 lastcommand = "" 595 if len(self.lastcommands) > 0: 596 lastcommand = self.lastcommands.pop() 597 if self.inputText != "": 598 self.nextcommands.append(self.inputText) 599 self.inputText = lastcommand 600 elif c == curses.KEY_DOWN: 601 nextcommand = "" 602 if len(self.nextcommands) > 0: 603 nextcommand = self.nextcommands.pop() 604 if self.inputText != "": 605 self.lastcommands.append(self.inputText) 606 self.inputText = nextcommand 607 elif c == curses.KEY_PPAGE: # page up 608 if self._first_onscreen_component > 0: 609 self._first_onscreen_component = \ 610 self._first_onscreen_component - 1 611 self.show() 612 elif c == curses.KEY_NPAGE: # page down 613 if self._first_onscreen_component < len(self._components) - \ 614 self.max_components_per_page: 615 self._first_onscreen_component = \ 616 self._first_onscreen_component + 1 617 self.show() 618 619 else: 620 # too long 621 if len(self.inputText) == self.cols-2: return 622 # add to input text 623 if c<=256: 624 self.inputText = self.inputText + chr(c) 625 626 # redisplay status 627 self.display_status() 628 629 self.stdscr.move(self.lasty+1,0) 630 self.stdscr.clrtoeol() 631 632 self.stdscr.addstr(self.lasty+1, 0, 633 'Prompt: %s' % self.inputText) 634 self.stdscr.refresh() 635 except Exception, e: 636 print e
637 638 639 # remote calls 640 # eg from components notifying changes
641 - def componentCall(self, componentState, methodName, *args, **kwargs):
642 # FIXME: for now, we only allow calls to go through that have 643 # their UI currently displayed. In the future, maybe we want 644 # to create all UI's at startup regardless and allow all messages 645 # to be processed, since they're here now anyway 646 self.log("componentCall received for %r.%s ..." % ( 647 componentState, methodName)) 648 localMethodName = "component_%s" % methodName 649 name = componentState.get('name') 650 651 try: 652 textui = self._comptextui[name] 653 except KeyError: 654 return 655 656 if not hasattr(textui, localMethodName): 657 self.log("... but does not have method %s" % localMethodName) 658 self.warning("Component view %s does not implement %s" % ( 659 name, localMethodName)) 660 return 661 self.log("... and executing") 662 method = getattr(textui, localMethodName) 663 664 # call the method, catching all sorts of stuff 665 try: 666 result = method(*args, **kwargs) 667 except TypeError: 668 msg = "component method %s did not accept *a %s and **kwa %s (or TypeError)" % ( 669 methodName, args, kwargs) 670 self.debug(msg) 671 raise errors.RemoteRunError(msg) 672 self.log("component: returning result: %r to caller" % result) 673 return result
674