Package elisa :: Package core :: Module launcher

Source Code for Module elisa.core.launcher

  1  #!/usr/bin/python 
  2  # -*- coding: utf-8 -*- 
  3  # Moovida - Home multimedia server 
  4  # Copyright (C) 2007-2009 Fluendo Embedded S.L. (www.fluendo.com). 
  5  # All rights reserved. 
  6  # 
  7  # This file is available under one of two license agreements. 
  8  # 
  9  # This file is licensed under the GPL version 3. 
 10  # See "LICENSE.GPL" in the root of this distribution including a special 
 11  # exception to use Moovida with Fluendo's plugins. 
 12  # 
 13  # The GPL part of Moovida is also available under a commercial licensing 
 14  # agreement from Fluendo. 
 15  # See "LICENSE.Moovida" in the root directory of this distribution package 
 16  # for details on that license. 
 17  # 
 18  # Author: Alessandro Decina <alessandro@fluendo.com> 
 19   
 20  import os 
 21  import pkg_resources 
 22  import sys 
 23  from distutils.version import LooseVersion 
 24   
 25   
 26  elisa_local_directory = os.path.join(os.path.expanduser('~'), '.moovida') 
 27  if not os.path.exists(elisa_local_directory): 
 28      os.makedirs(elisa_local_directory) 
 29   
 30   
 31  plugin_directories = [os.path.join(elisa_local_directory, 'plugins')] 
 32  plugin_path_environment = 'ELISA_PLUGIN_PATH' 
 33  if plugin_path_environment in os.environ: 
 34      plugin_directories += \ 
 35          os.environ[plugin_path_environment].split(os.path.pathsep) 
 36   
 37   
38 -class Launcher(object):
39 # Relative paths we set when running under windows. If you are the lucky 40 # maintainer of d-dependencies these days, you probably want to keep this 41 # part updated. 42 win_pgm_path = r'deps\lib\pigment-0.3' 43 win_pixbuf_module_file = r'etc\gtk-2.0\gdk-pixbuf.loaders' 44 win_library_paths = ['.', 45 r'deps\bin', 46 r'deps\lib\site-packages\pywin32_system32'] 47 win_python_paths = ['elisa-core', 48 r'deps\lib', 49 r'deps\lib\site-packages', 50 r'deps\lib\site-packages\gtk-2.0', 51 r'deps\lib\site-packages\gtk-2.0\\gobject', 52 r'deps\lib\site-packages\win32', 53 r'deps\lib\site-packages\win32com', 54 r'deps\lib\site-packages\PIL', 55 r'deps\lib\site-packages\gst-0.10', 56 r'deps\lib\site-packages\win32\\lib', 57 r'deps\libsite-packages\setuptools-0.6c8-py2.5.egg', 58 r'deps\bin\DLLs', 59 r'deps\lib\site-packages\pywin32_system32'] 60
61 - def main(self, argv):
62 """ 63 --> This is where you should start reading the source code <-- 64 65 The main entry point is split in two parts: main_before_voodoo() and 66 main_after_voodoo(). 67 68 The first function does all kinds of black magic to try and load the 69 most recent version of elisa.core. It should be AS MINIMAL AS IT CAN BE 70 and SHOULD NEVER BE MODIFIED. When we ship a new version of the core in 71 ~/.moovida/plugins, an OLD VERSION OF THE FUNCTION (the one installed 72 in a systemwide path) WILL BE EXECUTED anyway. 73 74 The second function does the actual initialization. If 75 main_before_voodoo() is done right (as we're sure it is since I wrote 76 it), the latest available version of main_after_voodoo() will be 77 executed. 78 79 Yes, that's right. I'm awesome. 80 """ 81 self.main_before_voodoo() 82 # from now on it's safe to import stuff 83 return self.main_after_voodoo(argv)
84
86 # See http://www.py2exe.org/index.cgi/StderrLog 87 log_file = os.path.join(elisa_local_directory, 'moovida.log') 88 sys.stdout = sys.stderr = open(log_file, 'w')
89
91 if hasattr(sys, 'frozen'): 92 if not sys.executable.endswith('moovida_console.exe'): 93 self.setup_win_output_redirection() 94 95 # delete GST_PLUGIN_PATH as it can conflict with our own plugins 96 environment = self.get_environment() 97 if environment.get('GST_PLUGIN_PATH', ''): 98 environment['GST_PLUGIN_PATH'] = '' 99 100 prefix = self.get_win_prefix() 101 102 # set PATH as soon as possible as we import 103 # twisted.internet.glib2reactor that glib and other dlls 104 library_paths = [os.path.join(prefix, entry) 105 for entry in self.win_library_paths] 106 self.add_environment_path('PATH', library_paths, prepend = True) 107 108 # FIXME: python in the build system is BROKEN so we have to do this. We 109 # REALLY need to fix it, but it's not going to happen anytime soon so... 110 python_paths = [os.path.join(prefix, entry) 111 for entry in self.win_python_paths] 112 sys.path.extend(python_paths)
113
114 - def main_before_voodoo(self):
115 import elisa 116 import elisa.core 117 import elisa.core.launcher 118 import elisa.plugins 119 120 if self.get_platform() == 'Windows': 121 self.set_win_environment_pre_voodoo() 122 123 launcher_version_info = elisa.core.version_info 124 self.info('Launcher core version: %s' % elisa.core.__version__) 125 126 # get the set of elisa.* modules imported at this time 127 our_modules = set([module for module in sys.modules.itervalues() 128 if module is not None and module.__name__.startswith('elisa')]) 129 130 expected = set([elisa, elisa.core, elisa.core.launcher, elisa.plugins]) 131 132 unexpected = our_modules - expected 133 if unexpected: 134 self.error('WARNING: some unexpected modules are loaded: %s' 135 % (' '.join([module.__name__ for module in unexpected]))) 136 137 requirement = pkg_resources.Requirement.parse('elisa') 138 core_dist = pkg_resources.working_set.find(requirement) 139 140 working_set = pkg_resources.working_set 141 142 # remove the core distribution from the working_set 143 144 # what we want to do here is this: 145 # working_set.entry_keys[core_dist.location].remove(core_dist.key) 146 # unfortunately some versions of pkg_resources don't normalize 147 # path entries so the same distribution could be under different paths 148 entries = [(path_entry, keys) 149 for path_entry, keys in working_set.entry_keys.iteritems() 150 if core_dist.key in keys] 151 for entry, keys in entries: 152 keys.remove(core_dist.key) 153 154 del working_set.by_key[core_dist.key] 155 156 # empty elisa.__path__, it will be populated again when we add again the 157 # core to the working_set. 158 # NOTE: when you do: 159 # python setup.py install --root=/ --single-version-externally-managed 160 # which is what packagers are doing (god forbid them), namespaces are 161 # not used so we have to put back elisa.__path__ ourselves 162 old_path, elisa.__path__ = list(elisa.__path__), [] 163 164 env = pkg_resources.Environment(plugin_directories) 165 distributions, errors = pkg_resources.working_set.find_plugins(env) 166 new_core_dist = None 167 for dist in distributions: 168 if dist.project_name == 'elisa': 169 # Is this core more recent than the current one? 170 if LooseVersion(dist.version) > LooseVersion(core_dist.version): 171 new_core_dist = dist 172 break 173 174 if new_core_dist is None: 175 # oh well, put back the old one 176 working_set.add(core_dist) 177 else: 178 # found a new core 179 working_set.add(new_core_dist) 180 181 if not elisa.__path__: 182 # no namespace packages? put back the old path 183 elisa.__path__ = old_path 184 185 # we need to install the glib2reactor before importing 186 # twisted.python.rebuild else it will load another reactor 187 import gobject 188 gobject.threads_init() 189 from twisted.internet import glib2reactor 190 glib2reactor.install() 191 192 from twisted.python.rebuild import rebuild 193 # reload this very same code... 194 rebuild(elisa.core) 195 rebuild(elisa.core.launcher) 196 # BAM. 197 198 self.info('Current core version: %s' % elisa.core.__version__) 199 200 current_version_info = elisa.core.version_info 201 if current_version_info < launcher_version_info: 202 # THIS SHOULD NEVER HAPPEN 203 self.error('PANIC! Current core older than the previous?\n' 204 'Expect massive breakage.')
205 206 # oh well.. let's go on and give it a shot anyway... 207
208 - def main_after_voodoo(self, argv):
209 if self.get_platform() == 'Windows': 210 self.set_win_environment_after_voodoo() 211 212 from elisa.core.options import Options 213 from twisted.python.usage import UsageError 214 215 # This part should probably be moved to Application. Since I don't want 216 # to touch Application now, leave it as something for another day (and 217 # another dev). 218 self.options = Options() 219 try: 220 self.options.parseOptions(argv[1:]) 221 except UsageError, errortext: 222 self.error('%s: %s' % (argv[0], errortext)) 223 self.error('%s: Try --help for usage details.' % (argv[0])) 224 sys.exit(1) 225 226 if self.poke_running_instance(): 227 self.info('An instance of Moovida is already running, exiting.') 228 self.exit(2) 229 230 self.run_application()
231
233 prefix = self.get_win_prefix() 234 environment = self.get_environment() 235 236 # note we use add_environment_path and not set_environment to allow 237 # developers to override our settings, which is what we usually do when 238 # running elisa uninstalled in windows 239 pgm_plugin_path = os.path.join(prefix, self.win_pgm_path) 240 self.add_environment_path('PGM_PLUGIN_PATH', [pgm_plugin_path]) 241 242 # set GDK_PIXBUF_MODULE_FILE if it isn't set yet 243 pixbuf_loaders = os.path.join(prefix, 'deps', 244 'etc', 'gtk-2.0', 'gdk-pixbuf.loaders') 245 if not environment.get('GDK_PIXBUF_MODULE_FILE', ''): 246 environment['GDK_PIXBUF_MODULE_FILE'] = pixbuf_loaders
247
248 - def poke_running_instance(self):
249 if self.get_platform() == 'Windows': 250 res = self.poke_win_running_instance() 251 else: 252 res = self.poke_unix_running_instance() 253 254 return res
255
257 try: 258 import dbus 259 import dbus.mainloop.glib 260 except ImportError: 261 # can't detect if an instance is running 262 return False 263 264 dbus.set_default_main_loop(dbus.mainloop.glib.DBusGMainLoop()) 265 bus = dbus.SessionBus() 266 daemon = bus.get_object(dbus.BUS_DAEMON_NAME, dbus.BUS_DAEMON_PATH) 267 daemon_iface = dbus.Interface(daemon, dbus.BUS_DAEMON_IFACE) 268 269 if self.options.get('force-startup', False): 270 return 271 272 if 'com.fluendo.Elisa' not in daemon_iface.ListNames(): 273 return False 274 275 if len(self.options['files']) > 0: 276 bus = dbus.SessionBus() 277 file_player = bus.get_object('com.fluendo.Elisa', 278 '/com/fluendo/Elisa/Plugins/Poblesec/FilePlayer') 279 file_player_iface = dbus.Interface(file_player, 280 'com.fluendo.Elisa.Plugins.Poblesec.FilePlayer') 281 file_player_iface.play_file(self.options['files']) 282 283 # Set focus to the window 284 frontend = bus.get_object('com.fluendo.Elisa', 285 '/com/fluendo/Elisa/Plugins/Pigment/Frontend') 286 frontend_iface = dbus.Interface(frontend, 287 'com.fluendo.Elisa.Plugins.Pigment.Frontend') 288 frontend_iface.show() 289 290 return True
291
293 from win32gui import FindWindow 294 from win32api import SendMessage 295 import pywintypes 296 from ctypes import Structure, POINTER, byref, c_void_p, c_char, sizeof, cast 297 from ctypes.wintypes import ULONG 298 from elisa.core.utils.mswin32.structures import COPYDATASTRUCT 299 300 from win32con import WM_COPYDATA 301 302 try: 303 handle = FindWindow("PIGMENT_CLASS", "Moovida Media Center") 304 except pywintypes.error: 305 # As stated in the MSDN docs on: 306 # http://msdn.microsoft.com/en-us/library/ms633499.aspx 307 # FindWindow call should return a NULL handle if the 308 # window wasn't found but it's not the case on windows < 7 309 return False 310 311 # on windows7 (experimental, build 7100), FindWindow doesn't 312 # raise an exception but returns an NULL handle if the window 313 # wasn't found, as correctly stated in the above MSDN link. 314 if handle == 0: 315 return False 316 317 if len(self.options['files']) > 0: 318 # We use '|' as a file separator 319 command_line = '|'.join(self.options['files']) 320 321 class COMMANDLINEDATA(Structure): 322 _fields_ = [("data", c_char * len(command_line))]
323 324 commandlinedata = COMMANDLINEDATA() 325 commandlinedata.data = command_line 326 copydata = COPYDATASTRUCT() 327 COMMAND_LINE = POINTER(ULONG)() 328 COMMAND_LINE.value = 1 329 copydata.dwData = COMMAND_LINE 330 copydata.cbData = sizeof(commandlinedata) 331 copydata.lpData = cast(byref(commandlinedata), c_void_p) 332 SendMessage(handle, WM_COPYDATA, 0, copydata) 333 334 return True
335 336 # some trivial utility functions used to make mocking in tests easier
337 - def info(self, message):
338 print message
339
340 - def error(self, message):
341 print >> sys.stderr, message
342
343 - def get_platform(self):
344 import platform 345 return platform.system()
346
347 - def get_environment(self):
348 return os.environ
349
350 - def get_win_prefix(self):
351 # assume we're running with a py2exe launcher in windows 352 return os.path.dirname(sys.executable)
353
354 - def add_environment_path(self, variable, entries, prepend=False):
355 environment = self.get_environment() 356 value = environment.get(variable, '').split(os.pathsep) 357 if prepend: 358 value = entries + value 359 else: 360 value.extend(entries) 361 environment[variable] = os.pathsep.join(value)
362
363 - def run_application(self):
364 # FIXME: this should all be put in Application 365 from elisa.core.application import Application 366 from elisa.core import common 367 from twisted.internet import reactor 368 369 application = Application(self.options) 370 common.set_application(application) 371 372 373 def start_app(): 374 def initialize_done(result): 375 return application.start()
376 377 def initialize_failure(failure): 378 print 'Moovida failed to initialize:', failure 379 reactor.stop() 380 381 dfr = application.initialize() 382 dfr.addCallbacks(initialize_done, initialize_failure) 383 384 reactor.callWhenRunning(start_app) 385 386 # this could lead to an ugly "'NoneType' object is 387 # unsubscriptable" in twisted, when the reactor gets killed from 388 # the outside. Bug is reported and fixed in twisted-svn/trunk: 389 # http://twistedmatrix.com/trac/ticket/2265 390 reactor.addSystemEventTrigger('before', 'shutdown', application.stop) 391 reactor.run() 392
393 - def exit(self, code):
394 sys.exit(code)
395 396
397 -def main():
398 Launcher().main(sys.argv)
399 400 401 if __name__ == '__main__': 402 main() 403