root/branches/0.3/src/main.py

Revision 957, 20.8 kB (checked in by nicfit, 1 month ago)

cleaned up profiling a bit

Line 
1 #!/usr/bin/env python
2 ################################################################################
3 #  Copyright (C) 2006-2008  Travis Shirk <travis@pobox.com>
4 #
5 #  This program is free software; you can redistribute it and/or modify
6 #  it under the terms of the GNU General Public License as published by
7 #  the Free Software Foundation; either version 2 of the License, or
8 #  (at your option) any later version.
9 #
10 #  This program is distributed in the hope that it will be useful,
11 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 #  GNU General Public License for more details.
14 #
15 #  You should have received a copy of the GNU General Public License
16 #  along with this program; if not, write to the Free Software
17 #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18 #
19 #  $Id$
20 ################################################################################
21
22 # XXX: Take care when importing, else gst could be imported too early and would
23 #      mess up --gst-help support
24 import locale
25
26 class RemoteControlException(Exception):
27     pass
28
29 class MeskApp:
30     ##
31     # Importing gstream checks sys.argv for --help. If found it displays
32     # usage and exits. This is a problem for mesk --help so this function
33     # is needed.
34     def _handleGstreamerOpts(self):
35         # This function may or may not return since help, version, etc. options
36         # are intercepted by the import of gst and exit is called.  When it does
37         # return any gst command line options should have been set by the import
38         def handle_gst_option():
39             if 'gst' in dir():
40                 raise RuntimeError("Mesk BUG, gst imported too early for "
41                                    "command line option parsing.")
42             # Import gst checking versions, this should not return if this
43             # is a help/version option.
44             from mesk import audio
45
46
47         if '--gst-help' in sys.argv:
48             # Make gst see --help, so that it does its thing
49             sys.argv.append('--help')
50             handle_gst_option() # This will not return
51             sys.exit(0)
52         else:
53             # Any gst options are parsed during the import.
54             for opt in sys.argv:
55                 if opt.startswith('--gst-') or opt in ["--help-all",
56                                                        "--help-gst"]:
57                     handle_gst_option()
58                     # Don't want to return in this case, a non-help gst option
59
60
61     def __init__(self):
62         self._handleGstreamerOpts()
63
64         gtk.window_set_auto_startup_notification(False)
65
66         # Parse command line
67         self.cmd_line = OptionParser()
68         (self.opts, args, self.remote_opts) = self.cmd_line.parse_args()
69         # Add any command line args as --enqueue remote opts
70         for a in args:
71             self.remote_opts.append(('--enqueue', a))
72         # The profile is used for the GUI and to select a DBus service instance
73         self.profile = self.opts.profile
74
75     def _run_remote_commands(self, interface):
76         for cmd, arg in self.remote_opts:
77             self._remote_control(interface, cmd, arg)
78
79     def run(self):
80         import mesk
81         self._init()
82
83         if not mesk.info.DISABLE_DBUS_SUPPORT:
84             mesk.log.debug('Testing for existing instance; profile \'%s\'' %
85                            self.profile)
86             import dbus_service
87             mesk_running = dbus_service.is_service_running(self.profile)
88             if mesk_running and not self.remote_opts:
89                 mesk.log.debug('mesk is running but no remote commands')
90                 raise mesk.MeskException(
91                     _('Mesk is already running'),
92                     _('Another instance of Mesk is using the profile \'%s\'. '
93                       'Try again with a different profile name '
94                       '(-p/--profile)') % self.profile)
95             elif mesk_running:
96                 mesk.log.debug('mesk is running, execting remote commands')
97                 # Mesk is running and we have some remote control options,
98                 # so execute the commands on the remote service and exit
99                 import dbus
100                 import dbus_service  # This is a local module
101                 obj_path = dbus_service.get_object_path(self.profile)
102                 service = dbus_service.get_service_name(self.profile)
103                 session_bus = dbus.SessionBus()
104                 obj = session_bus.get_object(service, obj_path)
105
106                 mesk_dbus = dbus.Interface(obj, dbus_service.INTERFACE)
107                 self._run_remote_commands(mesk_dbus)
108                 return
109         elif self.remote_opts:
110             raise RemoteControlException('Remote control disabled')
111
112
113         # Gnome session management
114         try:
115             import gnome.ui
116         except ImportError:
117             mesk.log.info('Session management disabled (gnome.ui not found)')
118         else:
119             mesk.log.debug('Enabling Gnome session management')
120             gnome.program_init(mesk.info.APP_NAME.lower(),
121                                mesk.info.APP_VERSION)
122             cli = gnome.ui.master_client()
123             cli.connect('die', lambda: gtk.main_quit())
124             argv = ['mesk'] + sys.argv[1:]
125             try:
126                 cli.set_restart_command(argv)
127             except TypeError:
128                 # Fedora systems have a broken gnome-python wrapper for this
129                 # function.  Credits to Quod Libet:
130                 # http://www.sacredchao.net/quodlibet/ticket/591
131                 cli.set_restart_command(len(argv), argv)
132
133         # Main window
134         from main_window import MainWindow
135         self.main_window = MainWindow(self.profile)
136         self.main_window.show()
137         gtk.gdk.notify_startup_complete()
138
139         # Handle any remote commands we have (once the main loop has started)
140         gobject.idle_add(self._run_remote_commands, self.main_window.mesk_dbus)
141         try:
142             # Main loop
143             gtk.main()
144         except KeyboardInterrupt:
145             mesk.log.debug("Interrupted")
146             self.main_window.quit()
147
148         # Shutdown
149         mesk.config.save(self.config_file)
150
151     def _init(self):
152         # Load configuration
153         if self.profile:
154             self.config_file = '%s/config.%s' % (mesk.MESK_DIR,
155                                                  self.opts.profile)
156         else:
157             self.config_file = '%s/config' % (mesk.MESK_DIR)
158
159         try:
160             mesk.config.load(self.config_file, self.opts.profile)
161         except IOError:
162             # No config, start with a new one
163             mesk.config.save(self.config_file)
164
165         # Initialize logging
166         mesk.log.init(mesk.config)
167         if self.opts.log_level:
168             try:
169                 lvl = mesk.log.string_to_level(self.opts.log_level)
170             except KeyError:
171                 mesk.log.warning('Invalid log level \'%s\' using INFO' %
172                                  self.opts.log_level)
173                 lvl = "INFO"
174             mesk.log.set_logging_level(lvl)
175
176         if self.opts.profile:
177             mesk.log.verbose('Using profile: %s' % self.opts.profile)
178
179         # Update config version
180         config_version = mesk.config.get(mesk.CONFIG_MAIN, 'version')
181         if config_version != mesk.info.APP_VERSION:
182             self._migrate_config(config_version, mesk.info.APP_VERSION)
183         mesk.config.set(mesk.CONFIG_MAIN, 'version', mesk.info.APP_VERSION)
184
185         # Initialize glade i18n
186         import gtk.glade
187         gtk.glade.bindtextdomain('mesk', mesk.i18n.DIR)
188         gtk.glade.textdomain('mesk')
189
190         if self.opts.debug:
191             # Break in pdb on unhandled exceptions
192             try:
193                 from IPython.ultraTB import FormattedTB
194                 sys.excepthook = FormattedTB(mode='Verbose',
195                                              color_scheme='Linux', call_pdb=1)
196             except ImportError:
197                 mesk.log.warn('Unable to use --run-pdb, IPython module not '
198                               'installed')
199
200     def _output(self, s):
201         sys.stdout.write(s.encode(locale.getpreferredencoding()))
202         sys.stdout.flush()
203
204     def _remote_control(self, interface, cmd, arg):
205         cmd_method = cmd[2:].replace('-', '_')
206         cmd_method = getattr(interface, cmd_method)
207         mesk.log.debug("executing remote command: %s, val: %s" % (cmd,
208                                                                   str(arg)))
209
210         if cmd in ['--stop', '--play', '--pause', '--play-pause', '--next',
211                    '--prev', '--toggle-mute', '--toggle-visible',
212                    '--raise-window']:
213             # These commands do not take arguments or require special
214             # processing, they can be invoked directly
215             cmd_method()
216         elif cmd in ['--get-state', '--get-current-uri', '--get-current-title',
217                      '--get-current-artist', '--get-current-album',
218                      '--get-current-year', '--get-current-length',
219                      '--list-playlists', '--get-active-playlist']:
220             # These commands output something and require a trailing '\n'
221             data = cmd_method()
222             if isinstance(data, list):
223                 for item in data:
224                     self._output(item + '\n')
225             elif data:
226                 self._output(data + '\n')
227
228         elif cmd in ['--vol-up', '--vol-down']:
229             # These commands take an argument and output nothing
230             cmd_method(arg)
231         elif cmd in ['--set-active-playlist']:
232             # Non-generic command
233             if interface.set_active_playlist(arg):
234                 self._output(_('Playlist \'%s\' is now active\n') % arg)
235             else:
236                 raise RemoteControlException('Playlist %s does not exist.' %
237                                              arg)
238         elif cmd == '--enqueue':
239             if not interface.enqueue(arg):
240                 raise RemoteControlException('No playlist found to enqueue URI')
241         else:
242             raise RemoteControlException("Unknown command \'%s\'" % cmd)
243
244         return 0
245
246     def _migrate_config(self, old_version, my_version):
247         import mesk.log, mesk.playlist
248         mesk.log.info('Migrating config version %s to %s' % (old_version,
249                                                              my_version))
250         (major, minor, maint) = map(int, old_version.split('.'))
251
252         if major == 0:
253             if minor == 1: # 0.1.x --> 0.2.x
254                 # Sink name changes for Gstreamer 0.10.x
255                 sink = mesk.config.get(mesk.CONFIG_AUDIO, 'gst_sink')
256                 if sink == 'gconf':
257                     sink = 'gconfaudiosink'
258                 elif sink in ['alsa', 'oss', 'esd']:
259                     sink += 'sink'
260                 else:
261                     sink = 'autoaudiosink'
262                 mesk.config.set(mesk.CONFIG_AUDIO, 'gst_sink', sink)
263
264                 # Remove unused 'auto_open' in all playlist configs
265                 for sect in mesk.config.sections():
266                     if sect.startswith(mesk.CONFIG_PLAYLIST):
267                         mesk.config.remove_option(sect, 'auto_open')
268
269                 minor = 2
270
271             if minor == 2: # 0.2.x --> 0.3.x
272                 # Clear user playlists.  The original m3u files are left
273                 # untouched and can be reimported if desired.
274                 import mesk, config
275                 for name in config.get_all_playlist_names():
276                     sect = mesk.CONFIG_PLAYLIST + '.' + name
277                     mesk.config.remove_section(sect)
278                 mesk.config.set(mesk.CONFIG_MAIN, 'playlists', 'Playlist')
279
280 import optparse
281 class OptionParser(optparse.OptionParser):
282     def __init__(self):
283         version_str = \
284 '''%s %s (%s)
285 (C) Copyright 2006-2008 Travis Shirk <travis@pobox.com>
286 This program comes with ABSOLUTELY NO WARRANTY! See COPYING for details.
287 * See --help/-h and/or the manual page for more info.''' % \
288 (mesk.info.APP_NAME, mesk.info.APP_VERSION, mesk.info.APP_CODENAME)
289
290         optparse.OptionParser.__init__(self,
291                                        usage='%s [OPTIONS] [URI ...]' %
292                                              mesk.info.APP_NAME.lower(),
293                                        version=version_str)
294         self.add_option('-p', '--profile', dest='profile',
295                         help=_('Start with profile NAME.'), metavar='NAME')
296
297         # Remote control options
298         rc_opts = optparse.OptionGroup(self, _('Remote Control Options'))
299         rc_opts.set_description(
300             _('The remote control options operate on a running instance of '
301               'Mesk, starting the app if necessary. If multiples instances of '
302               'Mesk are running the profile option can be used to determine '
303               'which instance to pass the command.'))
304         rc_opts.add_option('--stop', action='callback',
305                            callback=self._remote_option_cb,
306                            help=_('Stop playback'))
307         rc_opts.add_option('--play', action='callback',
308                            callback=self._remote_option_cb,
309                            help=_('Start playback'))
310         rc_opts.add_option('--pause', action='callback',
311                            callback=self._remote_option_cb,
312                            help=_('Pause playback'))
313         rc_opts.add_option('--play-pause', action='callback',
314                            callback=self._remote_option_cb,
315                            help=_('If playing, playback is paused. Otherwise '
316                                   'the player is started.'))
317         rc_opts.add_option('--prev', action='callback',
318                            callback=self._remote_option_cb,
319                            help=_('Previous track'))
320         rc_opts.add_option('--next', action='callback',
321                            callback=self._remote_option_cb,
322                            help=_('Next track'))
323         rc_opts.add_option('--toggle-mute', action='callback',
324                            callback=self._remote_option_cb,
325                            dest='toggle_mute', help=_('Mute/Unmute'))
326         rc_opts.add_option('--vol-up', action='callback',
327                            callback=self._remote_option_cb,
328                            type=float, metavar='N',
329                            help=_('Increase the volume by N% '
330                                   '(0.0 <= n <= 1.0)'))
331         rc_opts.add_option('--vol-down', action='callback',
332                            callback=self._remote_option_cb,
333                            type=float, metavar='N',
334                            help=_('Decrease the volume by N%'
335                                   '(0.0 <= n <= 1.0)'))
336         rc_opts.add_option('--get-state', action='callback',
337                            callback=self._remote_option_cb,
338                            help=_('Returns the current state of the audio '
339                                   'player (stopped, playing, paused).'))
340         rc_opts.add_option('--get-current-uri', action='callback',
341                            callback=self._remote_option_cb,
342                            help=_('Returns the URI of the current audio '
343                                   'source.'))
344         rc_opts.add_option('--get-current-title', action='callback',
345                            callback=self._remote_option_cb,
346                            help=_('Returns the title of the current audio '
347                                   'source.'))
348         rc_opts.add_option('--get-current-artist', action='callback',
349                            callback=self._remote_option_cb,
350                            help=_('Returns the artist of the current audio '
351                                   'source.'))
352         rc_opts.add_option('--get-current-album', action='callback',
353                            callback=self._remote_option_cb,
354                            help=_('Returns the album name of the current audio '
355                                   'source.'))
356         rc_opts.add_option('--get-current-year', action='callback',
357                            callback=self._remote_option_cb,
358                            help=_('Returns the year of the current audio '
359                                   'source.'))
360         rc_opts.add_option('--get-current-length', action='callback',
361                            callback=self._remote_option_cb,
362                            help=_('Returns the length (in seconds) of the '
363                                   'current audio source.'))
364         rc_opts.add_option('--list-playlists', action='callback',
365                            callback=self._remote_option_cb,
366                            help=_('List all playlists.'))
367         rc_opts.add_option('--get-active-playlist', action='callback',
368                            callback=self._remote_option_cb,
369                            help=_('List the name of the active '
370                                   'PlaylistControl.  This value may be empty '
371                                   'if no playlists are active (e.g. a CDROM '
372                                   'is active).'))
373         rc_opts.add_option('--set-active-playlist', action='callback',
374                            callback=self._remote_option_cb,
375                            metavar='NAME', type=str,
376                            help=_('Set the active playlist.'))
377         rc_opts.add_option('--enqueue', action='callback',
378                            callback=self._remote_option_cb,
379                            metavar='URI', type=str,
380                            help=_('Enqueue URI to the active playlist.'))
381         rc_opts.add_option('--toggle-visible', action='callback',
382                            callback=self._remote_option_cb,
383                            help=_('Show/hide the main window (minimize to '
384                                   'system tray)'))
385         rc_opts.add_option('--raise-window', action='callback',
386                            callback=self._remote_option_cb,
387                            help=_('Raise the main window (bring to front).'))
388         self.add_option_group(rc_opts)
389
390         # Advanced (developer) options
391         debug_opts = optparse.OptionGroup(self, _('Advanced Options'))
392         debug_opts.add_option('-l', '--log-level', dest='log_level',
393                               help=_('Select the amount of terminal logging. '
394                                      'May be CRITICAL, ERROR, WARNING, INFO, '
395                                      'VERBOSE, or DEBUG'),
396                               metavar='LEVEL')
397         debug_opts.add_option('--debug', action='store_true',
398                               dest='debug',
399                               help=_('Break in python debugger on unhandled '
400                                      'exceptions.'))
401         debug_opts.add_option('--run-profiler', action='store_true',
402                               dest='run_profiler',
403                               help=_('Run using python profiler.'))
404         debug_opts.add_option('--gst-help', action='store_true',
405                               dest='gst_help',
406                               help=_('Display Gstreamer command line options.'))
407         self.add_option_group(debug_opts)
408
409         # Set defaults
410         self.set_defaults(profile='', debug=False, run_profiler=False)
411         self._remote_options = []
412
413     def parse_args(self, args=None, values=None):
414         '''Returns an extra third tuple value which is a list of 2-value tuples.
415         These are the remote commands and argument values as they are not stored
416         in opts.'''
417         opts, args = optparse.OptionParser.parse_args(self, args, values)
418         return (opts, args, self._remote_options)
419
420     def _remote_option_cb(self, option, opt_str, value, parser):
421         self._remote_options.append((option.get_opt_string(), value))
422
423 ### Main ###
424 if __name__ == "__main__":
425     import sys, os
426     import gtk, gobject
427
428     # Init threads
429     os.environ['PYGTK_USE_GIL_STATE_API'] = ''
430     gobject.threads_init()
431
432     import mesk
433     from mesk.i18n import _
434
435     import traceback
436     try:
437         retval = 0
438         app = MeskApp()
439
440         if not app.opts.run_profiler:
441             app.run()
442         else:
443             # Profiling mode.
444             import profile, pstats
445             profile_out = 'mesk-profile.txt'
446             profile.run('app.run()', profile_out)
447             p = pstats.Stats(profile_out)
448             p.sort_stats('cumulative').print_stats(100)
449
450     except RemoteControlException, ex:
451         mesk.log.error('%s' % str(ex))
452         retval = 1
453     except SystemExit, ex:
454         retval = int(str(ex))
455         mesk.log.debug('Caught SystemExit: %s' % retval)
456     except Exception, ex:
457         import dialogs
458         from mesk.gtk_utils import escape_pango_markup
459         dialog = dialogs.ErrorDialog(parent=None)
460         if isinstance(ex, mesk.MeskException):
461             dialog.set_markup('<b>%s</b>' % escape_pango_markup(ex.primary_msg))
462             dialog.format_secondary_text(ex.secondary_msg)
463         else:
464             dialog.set_markup('<b>%s</b>' % escape_pango_markup(str(ex)))
465             dialog.format_secondary_text(traceback.format_exc())
466         dialog.run()
467         retval = 2
468
469     sys.exit(retval)
Note: See TracBrowser for help on using the browser.