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  """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: 6990 $" 
 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
73 - def _setAdminModel(self, model):
74 self.admin = model 75 76 self.admin.connect('connected', self.admin_connected_cb) 77 self.admin.connect('disconnected', self.admin_disconnected_cb) 78 self.admin.connect('connection-refused', 79 self.admin_connection_refused_cb) 80 self.admin.connect('connection-failed', 81 self.admin_connection_failed_cb) 82 #self.admin.connect('component-property-changed', 83 # self.property_changed_cb) 84 self.admin.connect('update', self.admin_update_cb)
85 86 # show the whole text admin screen
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 - def show_components(self):
103 if self.initialised: 104 self.stdscr.addstr(2,0, "Components:") 105 # get a dictionary of components 106 names = self._components.keys() 107 names.sort() 108 109 cury = 4 110 111 # if number of components is less than the space add 112 # "press page up for previous components" and 113 # "press page down for next components" lines 114 if len(names) > self.max_components_per_page: 115 if self._first_onscreen_component > 0: 116 self.stdscr.move(cury,0) 117 self.stdscr.clrtoeol() 118 self.stdscr.addstr(cury,0, 119 "Press page up to scroll up components list") 120 cury=cury+1 121 cur_component = self._first_onscreen_component 122 for name in names[self._first_onscreen_component:len(names)]: 123 # check if too many components for screen height 124 if cury - self.LINES_BEFORE_COMPONENTS >= \ 125 self.max_components_per_page: 126 self.stdscr.move(cury,0) 127 self.stdscr.clrtoeol() 128 self.stdscr.addstr(cury,0, 129 "Press page down to scroll down components list") 130 cury = cury + 1 131 break 132 133 component = self._components[name] 134 mood = component.get('mood') 135 # clear current component line 136 self.stdscr.move(cury,0) 137 self.stdscr.clrtoeol() 138 # output component name and mood 139 self.stdscr.addstr(cury,0,"%s: %s" % (name, moods[mood].name)) 140 cury = cury + 1 141 cur_component = cur_component + 1 142 143 self.lasty = cury
144 #self.stdscr.refresh() 145 146
147 - def gotEntryCallback(self, result, name):
148 entryPath, filename, methodName = result 149 filepath = os.path.join(entryPath, filename) 150 self.debug('Got the UI for %s and it lives in %s' % (name,filepath)) 151 self.uidir = os.path.split(filepath)[0] 152 #handle = open(filepath, "r") 153 #data = handle.read() 154 #handle.close() 155 156 # try loading the class 157 moduleName = common.pathToModuleName(filename) 158 statement = 'import %s' % moduleName 159 self.debug('running %s' % statement) 160 try: 161 exec(statement) 162 except SyntaxError, e: 163 # the syntax error can happen in the entry file, or any import 164 where = getattr(e, 'filename', "<entry file>") 165 lineno = getattr(e, 'lineno', 0) 166 msg = "Syntax Error at %s:%d while executing %s" % ( 167 where, lineno, filename) 168 self.warning(msg) 169 raise errors.EntrySyntaxError(msg) 170 except NameError, e: 171 # the syntax error can happen in the entry file, or any import 172 msg = "NameError while executing %s: %s" % (filename, 173 " ".join(e.args)) 174 self.warning(msg) 175 raise errors.EntrySyntaxError(msg) 176 except ImportError, e: 177 msg = "ImportError while executing %s: %s" % (filename, 178 " ".join(e.args)) 179 self.warning(msg) 180 raise errors.EntrySyntaxError(msg) 181 182 # make sure we're running the latest version 183 module = reflect.namedAny(moduleName) 184 rebuild.rebuild(module) 185 186 # check if we have the method 187 if not hasattr(module, methodName): 188 self.warning('method %s not found in file %s' % ( 189 methodName, filename)) 190 raise #FIXME: something appropriate 191 klass = getattr(module, methodName) 192 193 # instantiate the GUIClass, giving ourself as the first argument 194 # FIXME: we cheat by giving the view as second for now, 195 # but let's decide for either view or model 196 instance = klass(self._components[name], self.admin) 197 self.debug("Created entry instance %r" % instance) 198 199 #moduleName = common.pathToModuleName(fileName) 200 #statement = 'import %s' % moduleName 201 self._comptextui[name] = instance
202 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' % (state, key, value)) 254 255 if key == 'mood': 256 # this is needed so UIs load if they change to happy 257 # get bundle for component 258 d = self.getEntry(state, 'admin/text') 259 d.addCallback(self.gotEntryCallback, state.get('name')) 260 d.addErrback(self.gotEntryNoBundleErrback, state.get('name')) 261 d.addErrback(self.gotEntrySleepingComponentErrback) 262 263 self.show() 264 elif key == 'name': 265 if value: 266 self.show()
267 268 self._components = components 269 for name in self._components.keys(): 270 component = self._components[name] 271 component.addListener(self, set_=compStateSet) 272 273 # get bundle for component 274 d = self.getEntry(component, 'admin/text') 275 d.addCallback(self.gotEntryCallback, name) 276 d.addErrback(self.gotEntryNoBundleErrback, name) 277 d.addErrback(self.gotEntrySleepingComponentErrback) 278 279 self.show() 280
281 - def setPlanetState(self, planetState):
282 def flowStateAppend(state, key, value): 283 self.debug('flow state append: key %s, value %r' % (key, value)) 284 if state.get('name') != 'default': 285 return 286 if key == 'components': 287 self._components[value.get('name')] = value 288 # FIXME: would be nicer to do this incrementally instead 289 self.update_components(self._components)
290 291 def flowStateRemove(state, key, value): 292 if state.get('name') != 'default': 293 return 294 if key == 'components': 295 name = value.get('name') 296 self.debug('removing component %s' % name) 297 del self._components[name] 298 # FIXME: would be nicer to do this incrementally instead 299 self.update_components(self._components) 300 301 def atmosphereStateAppend(state, key, value): 302 if key == 'components': 303 self._components[value.get('name')] = value 304 # FIXME: would be nicer to do this incrementally instead 305 self.update_components(self._components) 306 307 def atmosphereStateRemove(state, key, value): 308 if key == 'components': 309 name = value.get('name') 310 self.debug('removing component %s' % name) 311 del self._components[name] 312 # FIXME: would be nicer to do this incrementally instead 313 self.update_components(self._components) 314 315 def planetStateAppend(state, key, value): 316 if key == 'flows': 317 if value.get('name') != 'default': 318 return 319 #self.debug('default flow started') 320 value.addListener(self, append=flowStateAppend, 321 remove=flowStateRemove) 322 for c in value.get('components'): 323 flowStateAppend(value, 'components', c) 324 325 def planetStateRemove(state, key, value): 326 self.debug('something got removed from the planet') 327 328 self.debug('parsing planetState %r' % planetState) 329 self._planetState = planetState 330 331 # clear and rebuild list of components that interests us 332 self._components = {} 333 334 planetState.addListener(self, append=planetStateAppend, 335 remove=planetStateRemove) 336 337 a = planetState.get('atmosphere') 338 a.addListener(self, append=atmosphereStateAppend, 339 remove=atmosphereStateRemove) 340 for c in a.get('components'): 341 atmosphereStateAppend(a, 'components', c) 342 343 for f in planetState.get('flows'): 344 planetStateAppend(f, 'flows', f) 345
346 - def _component_stop(self, state):
347 return self._component_do(state, 'Stop', 'Stopping', 'Stopped')
348
349 - def _component_start(self, state):
350 return self._component_do(state, 'Start', 'Starting', 'Started')
351
352 - def _component_do(self, state, action, doing, done):
353 name = state.get('name') 354 if not name: 355 return None 356 357 self.admin.callRemote('component'+action, state)
358
359 - def run_command(self, command):
360 # this decides whether startall, stopall and clearall are allowed 361 can_stop = True 362 can_start = True 363 for x in self._components.values(): 364 mood = moods.get(x.get('mood')) 365 can_stop = can_stop and (mood != moods.lost and mood != moods.sleeping) 366 can_start = can_start and (mood == moods.sleeping) 367 can_clear = can_start and not can_stop 368 369 if string.lower(command) == 'quit': 370 reactor.stop() 371 elif string.lower(command) == 'startall': 372 if can_start: 373 for c in self._components.values(): 374 self._component_start(c) 375 self.command_result = 'Attempting to start all components' 376 else: 377 self.command_result = '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 = 'Components not all in state to be stopped' 387 elif string.lower(command) == 'clearall': 388 if can_clear: 389 self.admin.cleanComponents() 390 self.command_result = 'Attempting to clear all components' 391 else: 392 self.command_result = 'Components not all in state to be cleared' 393 else: 394 command_split = command.split() 395 # if at least 2 tokens in the command 396 if len(command_split)>1: 397 # check if the first is a component name 398 for c in self._components.values(): 399 if string.lower(c.get('name')) == string.lower(command_split[0]): 400 # bingo, we have a component 401 if string.lower(command_split[1]) == 'start': 402 # start component 403 self._component_start(c) 404 elif string.lower(command_split[1]) == 'stop': 405 # stop component 406 self._component_stop(c) 407 else: 408 # component specific commands 409 try: 410 textui = self._comptextui[c.get('name')] 411 412 if textui: 413 d = textui.runCommand(' '.join(command_split[1:])) 414 self.debug("textui runcommand defer: %r" % d) 415 # add a callback 416 d.addCallback(self._runCommand_cb) 417 418 except KeyError: 419 pass
420 421
422 - def _runCommand_cb(self, result):
423 self.command_result = result 424 self.debug("Result received: %s" % result) 425 self.show()
426 427 428 429
430 - def get_available_commands(self, input):
431 input_split = input.split() 432 last_input='' 433 if len(input_split) >0: 434 last_input = input_split[len(input_split)-1] 435 available_commands = [] 436 if len(input_split) <= 1 and not input.endswith(' '): 437 # this decides whether startall, stopall and clearall are allowed 438 can_stop = True 439 can_start = True 440 for x in self._components.values(): 441 mood = moods.get(x.get('mood')) 442 can_stop = can_stop and (mood != moods.lost and mood != moods.sleeping) 443 can_start = can_start and (mood == moods.sleeping) 444 can_clear = can_start and not can_stop 445 446 for command in self.global_commands: 447 command_ok = (command != 'startall' and command != 'stopall' and command != 'clearall') 448 command_ok = command_ok or (command == 'startall' and can_start) 449 command_ok = command_ok or (command == 'stopall' and can_stop) 450 command_ok = command_ok or (command == 'clearall' and can_clear) 451 452 if command_ok and string.lower(command).startswith(string.lower(last_input)): 453 available_commands.append(command) 454 else: 455 available_commands = available_commands + self.get_available_commands_for_component(input_split[0], input) 456 457 return available_commands
458
459 - def get_available_commands_for_component(self, comp, input):
460 self.debug("getting commands for component %s" % comp) 461 commands = [] 462 for c in self._components: 463 if c == comp: 464 component_commands = [ 'start', 'stop' ] 465 textui = None 466 try: 467 textui = self._comptextui[comp] 468 except KeyError: 469 self.debug("no text ui for component %s" % comp) 470 471 input_split = input.split() 472 473 if len(input_split) >= 2 or input.endswith(' '): 474 for command in component_commands: 475 if len(input_split) == 2: 476 if command.startswith(input_split[1]): 477 commands.append(command) 478 elif len(input_split) == 1: 479 commands.append(command) 480 if textui: 481 self.debug("getting component commands from ui of %s" % comp) 482 comp_input = ' '.join(input_split[1:]) 483 if input.endswith(' '): 484 comp_input = comp_input + ' ' 485 commands = commands + textui.getCompletions(comp_input) 486 487 return commands
488 489
490 - def get_available_completions(self,input):
491 completions = self.get_available_commands(input) 492 493 # now if input has no spaces, add the names of each component that starts with input 494 if len(input.split()) <= 1: 495 for c in self._components: 496 if c.startswith(input): 497 completions.append(c) 498 499 return completions
500
501 - def display_status(self):
502 availablecommands = self.get_available_commands(self.inputText) 503 available_commands = ' '.join(availablecommands) 504 #for command in availablecommands: 505 # available_commands = '%s %s' % (available_commands, command) 506 self.stdscr.move(self.lasty+2,0) 507 self.stdscr.clrtoeol() 508 509 self.stdscr.addstr(self.lasty+2, 0, 510 "Available Commands: %s" % available_commands) 511 # display command results 512 self.stdscr.move(self.lasty+3,0) 513 self.stdscr.clrtoeol() 514 self.stdscr.move(self.lasty+4,0) 515 self.stdscr.clrtoeol() 516 517 if self.command_result != "": 518 self.stdscr.addstr(self.lasty+4, 0, "Result: %s" % self.command_result) 519 self.stdscr.clrtobot()
520 521 ### admin model callbacks
522 - def admin_connected_cb(self, admin):
523 self.info('Connected to manager') 524 525 # get initial info we need 526 self.setPlanetState(self.admin.planet) 527 528 if not self._components: 529 self.debug('no components detected, running wizard') 530 # ensure our window is shown 531 self.show()
532
533 - def admin_disconnected_cb(self, admin):
534 message = "Lost connection to manager, reconnecting ..." 535 print message
536
537 - def admin_connection_refused_cb(self, admin):
538 log.debug('textadminclient', "handling connection-refused") 539 #reactor.callLater(0, self.admin_connection_refused_later, admin) 540 log.debug('textadminclient', "handled connection-refused")
541
542 - def admin_connection_failed_cb(self, admin):
543 log.debug('textadminclient', "handling connection-failed") 544 #reactor.callLater(0, self.admin_connection_failed_later, admin) 545 log.debug('textadminclient', "handled connection-failed")
546
547 - def admin_update_cb(self, admin):
548 self.update_components(self._components)
549
550 - def connectionLost(self, why):
551 # do nothing 552 pass
553
554 - def whsStateAppend(self, state, key, value):
555 if key == 'names': 556 self.debug('Worker %s logged in.' % value)
557
558 - def whsStateRemove(self, state, key, value):
559 if key == 'names': 560 self.debug('Worker %s logged out.' % value)
561 562 # act as keyboard input
563 - def doRead(self):
564 """ Input is ready! """ 565 c = self.stdscr.getch() # read a character 566 567 if c == curses.KEY_BACKSPACE or c == 127: 568 self.inputText = self.inputText[:-1] 569 elif c == curses.KEY_STAB or c == 9: 570 available_commands = self.get_available_completions(self.inputText) 571 if len(available_commands) == 1: 572 input_split = self.inputText.split() 573 if len(input_split) > 1: 574 if not self.inputText.endswith(' '): 575 input_split.pop() 576 self.inputText = ' '.join(input_split) + ' ' + available_commands[0] 577 else: 578 self.inputText = available_commands[0] 579 580 elif c == curses.KEY_ENTER or c == 10: 581 # run command 582 self.run_command(self.inputText) 583 # re-display status 584 self.display_status() 585 # clear the prompt line 586 self.stdscr.move(self.lasty+1,0) 587 self.stdscr.clrtoeol() 588 self.stdscr.addstr(self.lasty+1,0,'Prompt: ') 589 self.stdscr.refresh() 590 if len(self.nextcommands) > 0: 591 self.lastcommands = self.lastcommands + self.nextcommands 592 self.nextcommands = [] 593 self.lastcommands.append(self.inputText) 594 self.inputText = '' 595 self.command_result = '' 596 elif c == curses.KEY_UP: 597 lastcommand = "" 598 if len(self.lastcommands) > 0: 599 lastcommand = self.lastcommands.pop() 600 if self.inputText != "": 601 self.nextcommands.append(self.inputText) 602 self.inputText = lastcommand 603 elif c == curses.KEY_DOWN: 604 nextcommand = "" 605 if len(self.nextcommands) > 0: 606 nextcommand = self.nextcommands.pop() 607 if self.inputText != "": 608 self.lastcommands.append(self.inputText) 609 self.inputText = nextcommand 610 elif c == curses.KEY_PPAGE: # page up 611 if self._first_onscreen_component > 0: 612 self._first_onscreen_component = \ 613 self._first_onscreen_component - 1 614 self.show() 615 elif c == curses.KEY_NPAGE: # page down 616 if self._first_onscreen_component < len(self._components) - \ 617 self.max_components_per_page: 618 self._first_onscreen_component = \ 619 self._first_onscreen_component + 1 620 self.show() 621 622 else: 623 # too long 624 if len(self.inputText) == self.cols-2: return 625 # add to input text 626 if c<=256: 627 self.inputText = self.inputText + chr(c) 628 629 # redisplay status 630 self.display_status() 631 632 self.stdscr.move(self.lasty+1,0) 633 self.stdscr.clrtoeol() 634 635 self.stdscr.addstr(self.lasty+1, 0, 'Prompt: %s' % self.inputText) 636 self.stdscr.refresh()
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