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