1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 """admin window interface, the main interface of flumotion-admin.
23
24 Here is an overview of the different parts of the admin interface::
25
26 +--------------[ AdminWindow ]-------------+
27 | Menubar |
28 +------------------------------------------+
29 | Toolbar |
30 +--------------------+---------------------+
31 | | |
32 | | |
33 | | |
34 | | |
35 | ComponentList | ComponentView |
36 | | |
37 | | |
38 | | |
39 | | |
40 | | |
41 +--------------------+---------------------+
42 | AdminStatusbar |
43 +-------------------------------------------
44
45 The main class which builds everything together is a L{AdminWindow},
46 which is defined in this file:
47
48 - L{AdminWindow} creates the other UI parts internally, see the
49 L{AdminWindow._createUI}.
50 - Menubar and Toolbar are created by a GtkUIManager, see
51 L{AdminWindow._createUI} and L{MAIN_UI}.
52 - L{ComponentList<flumotion.admin.gtk.componentlist.ComponentList>}
53 is a list of all components, and is created in the
54 L{flumotion.admin.gtk.componentlist} module.
55 - L{ComponentView<flumotion.admin.gtk.componentview.ComponentView>}
56 contains a component specific view, usually a set of tabs, it is
57 created in the L{flumotion.admin.gtk.componentview} module.
58 - L{AdminStatus<flumotion.admin.gtk.statusbar.AdminStatus>} is a
59 statusbar displaying context specific hints and is defined in the
60 L{flumotion.admin.gtk.statusbar} module.
61
62 """
63
64 import gettext
65 import os
66 import sys
67
68 import gobject
69 import gtk
70 from kiwi.ui.delegates import GladeDelegate
71 from kiwi.ui.dialogs import yesno
72 from twisted.internet import defer, reactor
73 from zope.interface import implements
74
75 from flumotion.admin.admin import AdminModel
76 from flumotion.admin.connections import getRecentConnections, \
77 hasRecentConnections
78 from flumotion.admin.gtk.dialogs import AboutDialog, ErrorDialog, \
79 ProgressDialog, showConnectionErrorDialog
80 from flumotion.admin.gtk.connections import ConnectionsDialog
81 from flumotion.admin.gtk.componentlist import getComponentLabel, ComponentList
82 from flumotion.admin.gtk.debugmarkerview import DebugMarkerDialog
83 from flumotion.admin.gtk.statusbar import AdminStatusbar
84 from flumotion.common.common import componentId
85 from flumotion.common.connection import PBConnectionInfo
86 from flumotion.common.errors import ConnectionRefusedError, \
87 ConnectionFailedError, BusyComponentError
88 from flumotion.common.i18n import N_, gettexter
89 from flumotion.common.log import Loggable
90 from flumotion.common.planet import AdminComponentState, moods
91 from flumotion.common.pygobject import gsignal
92 from flumotion.configure import configure
93 from flumotion.manager import admin
94 from flumotion.twisted.flavors import IStateListener
95 from flumotion.ui.trayicon import FluTrayIcon
96 from flumotion.wizard.models import AudioProducer, Porter, VideoProducer
97
98 admin
99
100 __version__ = "$Rev: 7070 $"
101 _ = gettext.gettext
102 T_ = gettexter()
103
104 MAIN_UI = """
105 <ui>
106 <menubar name="Menubar">
107 <menu action="Connection">
108 <menuitem action="OpenRecent"/>
109 <menuitem action="OpenExisting"/>
110 <menuitem action="ImportConfig"/>
111 <menuitem action="ExportConfig"/>
112 <separator name="sep-conn1"/>
113 <placeholder name="Recent"/>
114 <separator name="sep-conn2"/>
115 <menuitem action="Quit"/>
116 </menu>
117 <menu action="Manage">
118 <menuitem action="StartComponent"/>
119 <menuitem action="StopComponent"/>
120 <menuitem action="DeleteComponent"/>
121 <separator name="sep-manage1"/>
122 <menuitem action="StartAll"/>
123 <menuitem action="StopAll"/>
124 <menuitem action="ClearAll"/>
125 <separator name="sep-manage2"/>
126 <menuitem action="AddFormat"/>
127 <separator name="sep-manage3"/>
128 <menuitem action="RunConfigurationWizard"/>
129 </menu>
130 <menu action="Debug">
131 <menuitem action="EnableDebugging"/>
132 <separator name="sep-debug1"/>
133 <menuitem action="StartShell"/>
134 <menuitem action="DumpConfiguration"/>
135 <menuitem action="WriteDebugMarker"/>
136 </menu>
137 <menu action="Help">
138 <menuitem action="About"/>
139 </menu>
140 </menubar>
141 <toolbar name="Toolbar">
142 <toolitem action="OpenRecent"/>
143 <separator name="sep-toolbar1"/>
144 <toolitem action="StartComponent"/>
145 <toolitem action="StopComponent"/>
146 <toolitem action="DeleteComponent"/>
147 <separator name="sep-toolbar2"/>
148 <toolitem action="RunConfigurationWizard"/>
149 </toolbar>
150 <popup name="ComponentContextMenu">
151 <menuitem action="StartComponent"/>
152 <menuitem action="StopComponent"/>
153 <menuitem action="DeleteComponent"/>
154 <menuitem action="KillComponent"/>
155 </popup>
156 </ui>
157 """
158
159 RECENT_UI_TEMPLATE = '''<ui>
160 <menubar name="Menubar">
161 <menu action="Connection">
162 <placeholder name="Recent">
163 %s
164 </placeholder>
165 </menu>
166 </menubar>
167 </ui>'''
168
169 MAX_RECENT_ITEMS = 4
170
171
173 '''Creates the GtkWindow for the user interface.
174 Also connects to the manager on the given host and port.
175 '''
176
177
178 gladefile = 'admin.glade'
179 toplevel_name = 'main_window'
180
181
182 logCategory = 'adminwindow'
183
184
185 implements(IStateListener)
186
187
188 gsignal('connected')
189
191 GladeDelegate.__init__(self)
192
193 self._adminModel = None
194 self._currentComponentStates = None
195 self._componentContextMenu = None
196 self._componentList = None
197 self._componentStates = None
198 self._componentView = None
199 self._debugEnabled = False
200 self._debugActions = None
201 self._debugEnableAction = None
202 self._disconnectedDialog = None
203 self._planetState = None
204 self._recentMenuID = None
205 self._trayicon = None
206 self._configurationWizardIsRunning = False
207
208 self._createUI()
209 self._appendRecentConnections()
210 self.setDebugEnabled(False)
211
212
213
214
215
216
217
218
232
233
234
235
236
237
240
241 def cb(result, self, mid):
242 if mid:
243 self.statusbar.remove('main', mid)
244 if post:
245 self.statusbar.push('main', post % label)
246
247 def eb(failure, self, mid):
248 if mid:
249 self.statusbar.remove('main', mid)
250 self.warning("Failed to execute %s on component %s: %s"
251 % (methodName, label, failure))
252 if fail:
253 self.statusbar.push('main', fail % label)
254 if not state:
255 states = self.components_view.getSelectedStates()
256 if not states:
257 return
258 for state in states:
259 self.componentCallRemoteStatus(state, pre, post, fail,
260 methodName, args, kwargs)
261 else:
262 label = getComponentLabel(state)
263 if not label:
264 return
265
266 mid = None
267 if pre:
268 mid = self.statusbar.push('main', pre % label)
269 d = self._adminModel.componentCallRemote(
270 state, methodName, *args, **kwargs)
271 d.addCallback(cb, self, mid)
272 d.addErrback(eb, self, mid)
273
277
279 if key == 'names':
280 self.statusbar.set(
281 'main', _('Worker %s logged in.') % value)
282
284 if key == 'names':
285 self.statusbar.set(
286 'main', _('Worker %s logged out.') % value)
287
290
292 """Set if debug should be enabled for the admin client window
293 @param enable: if debug should be enabled
294 """
295 self._debugEnabled = enabled
296 self._debugActions.set_sensitive(enabled)
297 self._debugEnableAction.set_active(enabled)
298 self._componentView.setDebugEnabled(enabled)
299 self._killComponentAction.set_property('visible', enabled)
300
302 """Get the gtk window for the admin interface
303 @returns: window
304 @rtype: gtk.Window
305 """
306 return self._window
307
309 """Connects to a manager given a connection info
310 @param info: connection info
311 @type info: L{PBConnectionInfo}
312 """
313 assert isinstance(info, PBConnectionInfo), info
314 return self._openConnection(info)
315
316
317
319 self.debug('creating UI')
320
321
322 self._window = self.toplevel
323 self._componentList = ComponentList(self.component_list)
324 del self.component_list
325 self._componentView = self.component_view
326 del self.component_view
327 self._statusbar = AdminStatusbar(self.statusbar)
328 del self.statusbar
329 self._messageView = self.messages_view
330 del self.messages_view
331
332 self._window.set_name("AdminWindow")
333 self._window.connect('delete-event', self._window_delete_event_cb)
334
335 uimgr = gtk.UIManager()
336 uimgr.connect('connect-proxy',
337 self._on_uimanager__connect_proxy)
338 uimgr.connect('disconnect-proxy',
339 self._on_uimanager__disconnect_proxy)
340
341
342 group = gtk.ActionGroup('Actions')
343 group.add_actions([
344
345 ('Connection', None, _("_Connection")),
346 ('OpenRecent', gtk.STOCK_OPEN, _('_Open Recent Connection...'),
347 None, _('Connect to a recently used connection'),
348 self._connection_open_recent_cb),
349 ('OpenExisting', None, _('Connect to _running manager...'), None,
350 _('Connect to an previously used connection'),
351 self._connection_open_existing_cb),
352 ('ImportConfig', None, _('_Import Configuration...'), None,
353 _('Import configuration from a file'),
354 self._connection_import_configuration_cb),
355 ('ExportConfig', None, _('_Export Configuration...'), None,
356 _('Export current configuration to a file'),
357 self._connection_export_configuration_cb),
358 ('Quit', gtk.STOCK_QUIT, _('_Quit'), None,
359 _('Quit the application and disconnect from the manager'),
360 self._connection_quit_cb),
361
362
363 ('Manage', None, _('_Manage')),
364 ('StartComponent', 'flumotion-play', _('_Start Component(s)'),
365 None, _('Start the selected component(s)'),
366 self._manage_start_component_cb),
367 ('StopComponent', 'flumotion-stop', _('St_op Component(s)'),
368 None, _('Stop the selected component(s)'),
369 self._manage_stop_component_cb),
370 ('DeleteComponent', gtk.STOCK_DELETE, _('_Delete Component(s)'),
371 None, _('Delete the selected component(s)'),
372 self._manage_delete_component_cb),
373 ('StartAll', None, _('Start _All'), None,
374 _('Start all components'),
375 self._manage_start_all_cb),
376 ('StopAll', None, _('Stop A_ll'), None,
377 _('Stop all components'),
378 self._manage_stop_all_cb),
379 ('ClearAll', gtk.STOCK_CLEAR, _('_Clear All'), None,
380 _('Remove all components'),
381 self._manage_clear_all_cb),
382 ('AddFormat', gtk.STOCK_ADD, _('Add new encoding _format...'), None,
383 _('Add a new format to the current stream'),
384 self._manage_add_format_cb),
385 ('RunConfigurationWizard', 'flumotion-wizard', _('Run _Wizard'), None,
386 _('Run the configuration wizard'),
387 self._manage_run_wizard_cb),
388
389
390 ('Debug', None, _('_Debug')),
391
392
393 ('Help', None, _('_Help')),
394 ('About', gtk.STOCK_ABOUT, _('_About'), None,
395 _('Displays an about dialog'),
396 self._help_about_cb),
397
398
399 ('KillComponent', None, _('_Kill Component'), None,
400 _('Kills the currently selected component'),
401 self._kill_component_cb),
402
403 ])
404 group.add_toggle_actions([
405 ('EnableDebugging', None, _('Enable _Debugging'), None,
406 _('Enable debugging in the admin interface'),
407 self._debug_enable_cb),
408 ])
409 self._debugEnableAction = group.get_action('EnableDebugging')
410 uimgr.insert_action_group(group, 0)
411
412
413 self._debugActions = gtk.ActionGroup('Actions')
414 self._debugActions.add_actions([
415
416 ('StartShell', gtk.STOCK_EXECUTE, _('Start _Shell'), None,
417 _('Start an interactive debugging shell'),
418 self._debug_start_shell_cb),
419 ('DumpConfiguration', gtk.STOCK_EXECUTE,
420 _('Dump configuration'), None,
421 _('Dumps the current manager configuration'),
422 self._debug_dump_configuration_cb),
423 ('WriteDebugMarker', gtk.STOCK_EXECUTE,
424 _('Write debug marker...'), None,
425 _('Writes a debug marker to all the logs'),
426 self._debug_write_debug_marker_cb)
427 ])
428 uimgr.insert_action_group(self._debugActions, 0)
429 self._debugActions.set_sensitive(False)
430
431 uimgr.add_ui_from_string(MAIN_UI)
432 self._window.add_accel_group(uimgr.get_accel_group())
433
434 menubar = uimgr.get_widget('/Menubar')
435 self.main_vbox.pack_start(menubar, expand=False)
436 self.main_vbox.reorder_child(menubar, 0)
437
438 toolbar = uimgr.get_widget('/Toolbar')
439 toolbar.set_icon_size(gtk.ICON_SIZE_SMALL_TOOLBAR)
440 toolbar.set_style(gtk.TOOLBAR_ICONS)
441 self.main_vbox.pack_start(toolbar, expand=False)
442 self.main_vbox.reorder_child(toolbar, 1)
443
444 self._componentContextMenu = uimgr.get_widget('/ComponentContextMenu')
445 self._componentContextMenu.show()
446
447 menubar.show_all()
448
449 self._actiongroup = group
450 self._uimgr = uimgr
451 self._openRecentAction = group.get_action("OpenRecent")
452 self._startComponentAction = group.get_action("StartComponent")
453 self._stopComponentAction = group.get_action("StopComponent")
454 self._deleteComponentAction = group.get_action("DeleteComponent")
455 self._stopAllAction = group.get_action("StopAll")
456 assert self._stopAllAction
457 self._startAllAction = group.get_action("StartAll")
458 assert self._startAllAction
459 self._clearAllAction = group.get_action("ClearAll")
460 assert self._clearAllAction
461 self._addFormatAction = group.get_action("AddFormat")
462 assert self._addFormatAction
463 self._killComponentAction = group.get_action("KillComponent")
464 assert self._killComponentAction
465
466 self._trayicon = FluTrayIcon(self._window)
467 self._trayicon.connect("quit", self._trayicon_quit_cb)
468 self._trayicon.set_tooltip(_('Not connected'))
469
470 self._componentList.connect('selection_changed',
471 self._components_selection_changed_cb)
472 self._componentList.connect('show-popup-menu',
473 self._components_show_popup_menu_cb)
474
475 self._updateComponentActions()
476 self._componentList.connect(
477 'notify::can-start-any',
478 self._components_start_stop_notify_cb)
479 self._componentList.connect(
480 'notify::can-stop-any',
481 self._components_start_stop_notify_cb)
482 self._updateComponentActions()
483
484 self._messageView.hide()
485
487 tooltip = action.get_property('tooltip')
488 if not tooltip:
489 return
490
491 if isinstance(widget, gtk.MenuItem):
492 cid = widget.connect('select', self._on_menu_item__select,
493 tooltip)
494 cid2 = widget.connect('deselect', self._on_menu_item__deselect)
495 widget.set_data('pygtk-app::proxy-signal-ids', (cid, cid2))
496 elif isinstance(widget, gtk.ToolButton):
497 cid = widget.child.connect('enter', self._on_tool_button__enter,
498 tooltip)
499 cid2 = widget.child.connect('leave', self._on_tool_button__leave)
500 widget.set_data('pygtk-app::proxy-signal-ids', (cid, cid2))
501
503 cids = widget.get_data('pygtk-app::proxy-signal-ids')
504 if not cids:
505 return
506
507 if isinstance(widget, gtk.ToolButton):
508 widget = widget.child
509
510 for name, cid in cids:
511 widget.disconnect(cid)
512
537
545
546 model = AdminModel()
547 d = model.connectToManager(info)
548 d.addCallback(connected)
549 return d
550
552 d = self._openConnection(info)
553
554 def errorMessageDisplayed(unused):
555 self._window.set_sensitive(True)
556
557 def connected(model):
558 self._window.set_sensitive(True)
559
560 def errbackConnectionRefusedError(failure):
561 failure.trap(ConnectionRefusedError)
562 d = showConnectionErrorDialog(failure, info, parent=self._window)
563 d.addCallback(errorMessageDisplayed)
564
565 def errbackConnectionFailedError(failure):
566 failure.trap(ConnectionFailedError)
567 d = showConnectionErrorDialog(failure, info, parent=self._window)
568 d.addCallback(errorMessageDisplayed)
569 return d
570
571 d.addCallback(connected)
572 d.addErrback(errbackConnectionRefusedError)
573 d.addErrback(errbackConnectionFailedError)
574 self._window.set_sensitive(False)
575 return d
576
596
598 """Quitting the application in a controlled manner"""
599 self._clearAdmin()
600 self._close()
601
604
606 import pprint
607 import cStringIO
608 fd = cStringIO.StringIO()
609 pprint.pprint(configation, fd)
610 fd.seek(0)
611 self.debug('Configuration=%s' % fd.read())
612
617
626
627 - def _setStatusbarText(self, text):
628 return self._statusbar.push('main', text)
629
631 self._statusbar.pop('main')
632
642
644 if componentType is None:
645 raise ValueError
646 componentStates = []
647
648 for state in self._componentStates.values():
649 config = state.get('config')
650 if componentType and config['type'] == componentType:
651 componentStates.append(state)
652
653 if not componentStates:
654 return None
655 elif len(componentStates) == 1:
656 return componentStates[0]
657 else:
658 raise AssertionError(
659 "Attempted to fetch a component state by type %r, "
660 "expected one, but got %r" % (
661 componentType, componentStates))
662
675
677 def _getComponents():
678 for componentState in self._componentStates.values():
679 componentType = componentState.get('config')['type']
680 for entry in entries:
681 if entry.componentType == componentType:
682 yield (componentState, entry)
683
684
685 for componentState, entry in _getComponents():
686 component = componentClass()
687 component.componentType = entry.componentType
688 component.description = entry.description
689 component.exists = True
690 config = componentState.get('config')
691 for key, value in config['properties'].items():
692 component.properties[key] = value
693 yield component
694
710
711 d = self._adminModel.getWizardEntries(
712 wizardTypes=['audio-producer', 'video-producer'])
713 d.addCallback(cb)
714
722
723 if not self._componentStates:
724 runWizard()
725 return
726
727 if yesno(_("Running the Configuration Wizard again will remove "
728 "all components from the current stream and create "
729 "a new one."),
730 parent=self._window,
731 buttons=((_("Keep the current stream"), gtk.RESPONSE_NO),
732 (_("Run the Wizard anyway"), gtk.RESPONSE_YES))
733 ) != gtk.RESPONSE_YES:
734 return
735
736 d = self._clearAllComponents()
737 d.addCallback(lambda unused: runWizard())
738
756
767
770
772 canStart = self._componentList.canStart()
773 canStop = self._componentList.canStop()
774 canDelete = bool(self._currentComponentStates and canStart)
775 self._startComponentAction.set_sensitive(canStart)
776 self._stopComponentAction.set_sensitive(canStop)
777 self._deleteComponentAction.set_sensitive(canDelete)
778 self.debug('can start %r, can stop %r' % (canStart, canStop))
779 canStartAll = self._componentList.get_property('can-start-any')
780 canStopAll = self._componentList.get_property('can-stop-any')
781
782
783 canClearAll = canStartAll and not canStopAll
784 self._stopAllAction.set_sensitive(canStopAll)
785 self._startAllAction.set_sensitive(canStartAll)
786 self._clearAllAction.set_sensitive(canClearAll)
787 self._killComponentAction.set_sensitive(canStop)
788
789 hasProducer = self._hasProducerComponent()
790 self._addFormatAction.set_sensitive(hasProducer)
791
793 self._componentList.clearAndRebuild(self._componentStates)
794 self._trayicon.update(self._componentStates)
795
806
808 self._messageView.clear()
809 pstate = self._planetState
810 if pstate and pstate.hasKey('messages'):
811 for message in pstate.get('messages').values():
812 self._messageView.addMessage(message)
813
815
816 def flowStateAppend(state, key, value):
817 self.debug('flow state append: key %s, value %r' % (key, value))
818 if key == 'components':
819 self._componentStates[value.get('name')] = value
820
821 self._updateComponents()
822
823 def flowStateRemove(state, key, value):
824 if key == 'components':
825 self._removeComponent(value)
826
827 def atmosphereStateAppend(state, key, value):
828 if key == 'components':
829 self._componentStates[value.get('name')] = value
830
831 self._updateComponents()
832
833 def atmosphereStateRemove(state, key, value):
834 if key == 'components':
835 self._removeComponent(value)
836
837 def planetStateAppend(state, key, value):
838 if key == 'flows':
839 if value != state.get('flows')[0]:
840 self.warning('flumotion-admin can only handle one '
841 'flow, ignoring /%s', value.get('name'))
842 return
843 self.debug('%s flow started', value.get('name'))
844 value.addListener(self, append=flowStateAppend,
845 remove=flowStateRemove)
846 for c in value.get('components'):
847 flowStateAppend(value, 'components', c)
848 self._updateComponents()
849
850 def planetStateRemove(state, key, value):
851 self.debug('something got removed from the planet')
852
853 def planetStateSetitem(state, key, subkey, value):
854 if key == 'messages':
855 self._messageView.addMessage(value)
856
857 def planetStateDelitem(state, key, subkey, value):
858 if key == 'messages':
859 self._messageView.clearMessage(value.id)
860
861 self.debug('parsing planetState %r' % planetState)
862 self._planetState = planetState
863
864
865 self._componentStates = {}
866
867 planetState.addListener(self, append=planetStateAppend,
868 remove=planetStateRemove,
869 setitem=planetStateSetitem,
870 delitem=planetStateDelitem)
871
872 self._clearMessages()
873
874 a = planetState.get('atmosphere')
875 a.addListener(self, append=atmosphereStateAppend,
876 remove=atmosphereStateRemove)
877 for c in a.get('components'):
878 atmosphereStateAppend(a, 'components', c)
879
880 for f in planetState.get('flows'):
881 planetStateAppend(planetState, 'flows', f)
882
884 d = self._adminModel.cleanComponents()
885 def busyComponentError(failure):
886 failure.trap(BusyComponentError)
887 self._error(
888 _("Some component(s) are still busy and cannot be removed.\n"
889 "Try again later."))
890 d.addErrback(busyComponentError)
891 return d
892
893
894
909
911 """
912 @returns: a L{twisted.internet.defer.Deferred}
913 """
914 return self._componentDo(state, 'componentStop',
915 'Stop', 'Stopping', 'Stopped')
916
918 """
919 @returns: a L{twisted.internet.defer.Deferred}
920 """
921 return self._componentDo(state, 'componentStart',
922 'Start', 'Starting', 'Started')
923
925 """
926 @returns: a L{twisted.internet.defer.Deferred}
927 """
928 return self._componentDo(state, 'deleteComponent',
929 'Delete', 'Deleting', 'Deleted')
930
931 - def _componentDo(self, state, methodName, action, doing, done):
932 """Do something with a component and update the statusbar
933 @param state: componentState
934 @type state: L{AdminComponentState}
935 @param methodName: name of the method to call
936 @type methodName: str
937 @param action: string used to explain that to do
938 @type action: str
939 @param doing: string used to explain that the action started
940 @type doing: str
941 @param done: string used to explain that the action was completed
942 @type done: str
943 """
944 if state is None:
945 states = self._componentList.getSelectedStates()
946 else:
947 states = [state]
948
949 if not states:
950 return