diff --git a/docs/changes.rst b/docs/changes.rst index 7c2ce19d..4fdc8c1f 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -39,8 +39,6 @@ greatly improved MPD client support. the packages created by ``setup.py`` for i.e. PyPI. - MPD frontend: - - Relocate from :mod:`mopidy.mpd` to :mod:`mopidy.frontends.mpd`. - - Split gigantic protocol implementation into eleven modules. - Search improvements, including support for multi-word search. - Fixed ``play "-1"`` and ``playid "-1"`` behaviour when playlist is empty or when a current track is set. @@ -56,11 +54,14 @@ greatly improved MPD client support. - Fix ``load`` so that one can append a playlist to the current playlist, and make it return the correct error message if the playlist is not found. - Support for single track repeat added. (Fixes: :issue:`4`) + - Relocate from :mod:`mopidy.mpd` to :mod:`mopidy.frontends.mpd`. + - Split gigantic protocol implementation into eleven modules. - Rename ``mopidy.frontends.mpd.{serializer => translator}`` to match naming in backends. - Remove setting :attr:`mopidy.settings.SERVER` and :attr:`mopidy.settings.FRONTEND` in favour of the new :attr:`mopidy.settings.FRONTENDS`. + - Run MPD server in its own process. - Backends: diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 1c0318e7..20e78f5a 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -1,85 +1,17 @@ -import asyncore -import logging -import logging.handlers -import multiprocessing -import optparse import os import sys +# Add ../ to the path so we can run Mopidy from a Git checkout without +# installing it on the system. sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))) -from mopidy import get_version, settings, SettingsError -from mopidy.process import CoreProcess -from mopidy.utils import get_class -from mopidy.utils.path import get_or_create_folder -from mopidy.utils.settings import list_settings_optparse_callback - -logger = logging.getLogger('mopidy.main') +from mopidy.core import CoreProcess def main(): - options = _parse_options() - _setup_logging(options.verbosity_level, options.dump) - settings.validate() - logger.info('-- Starting Mopidy --') - get_or_create_folder('~/.mopidy/') - core_queue = multiprocessing.Queue() - output_class = get_class(settings.OUTPUT) - backend_class = get_class(settings.BACKENDS[0]) - frontend = get_class(settings.FRONTENDS[0])() - frontend.start_server(core_queue) - core = CoreProcess(core_queue, output_class, backend_class, frontend) - core.start() - asyncore.loop() - -def _parse_options(): - parser = optparse.OptionParser(version='Mopidy %s' % get_version()) - parser.add_option('-q', '--quiet', - action='store_const', const=0, dest='verbosity_level', - help='less output (warning level)') - parser.add_option('-v', '--verbose', - action='store_const', const=2, dest='verbosity_level', - help='more output (debug level)') - parser.add_option('--dump', - action='store_true', dest='dump', - help='dump debug log to file') - parser.add_option('--list-settings', - action='callback', callback=list_settings_optparse_callback, - help='list current settings') - return parser.parse_args()[0] - -def _setup_logging(verbosity_level, dump): - _setup_console_logging(verbosity_level) - if dump: - _setup_dump_logging() - -def _setup_console_logging(verbosity_level): - if verbosity_level == 0: - level = logging.WARNING - elif verbosity_level == 2: - level = logging.DEBUG - else: - level = logging.INFO - logging.basicConfig(format=settings.CONSOLE_LOG_FORMAT, level=level) - -def _setup_dump_logging(): - root = logging.getLogger('') - root.setLevel(logging.DEBUG) - formatter = logging.Formatter(settings.DUMP_LOG_FORMAT) - handler = logging.handlers.RotatingFileHandler( - settings.DUMP_LOG_FILENAME, maxBytes=102400, backupCount=3) - handler.setFormatter(formatter) - root.addHandler(handler) + # Explictly call run() instead of start(), since we don't need to start + # another process. + CoreProcess().run() if __name__ == '__main__': - try: - main() - except KeyboardInterrupt: - logger.info(u'Interrupted by user') - sys.exit(0) - except SettingsError, e: - logger.error(e) - sys.exit(1) - except SystemExit, e: - logger.error(e) - sys.exit(1) + main() diff --git a/mopidy/backends/libspotify/playback.py b/mopidy/backends/libspotify/playback.py index 1195e9bc..ed5ba697 100644 --- a/mopidy/backends/libspotify/playback.py +++ b/mopidy/backends/libspotify/playback.py @@ -4,7 +4,7 @@ import multiprocessing from spotify import Link, SpotifyError from mopidy.backends.base import BasePlaybackController -from mopidy.process import pickle_connection +from mopidy.utils.process import pickle_connection logger = logging.getLogger('mopidy.backends.libspotify.playback') diff --git a/mopidy/backends/libspotify/session_manager.py b/mopidy/backends/libspotify/session_manager.py index 707423aa..22cbb0a0 100644 --- a/mopidy/backends/libspotify/session_manager.py +++ b/mopidy/backends/libspotify/session_manager.py @@ -18,7 +18,10 @@ class LibspotifySessionManager(SpotifySessionManager, threading.Thread): def __init__(self, username, password, core_queue, output_queue): SpotifySessionManager.__init__(self, username, password) - threading.Thread.__init__(self) + threading.Thread.__init__(self, name='LibspotifySessionManagerThread') + # Run as a daemon thread, so Mopidy won't wait for this thread to exit + # before Mopidy exits. + self.daemon = True self.core_queue = core_queue self.output_queue = output_queue self.connected = threading.Event() diff --git a/mopidy/core.py b/mopidy/core.py new file mode 100644 index 00000000..d823bfa5 --- /dev/null +++ b/mopidy/core.py @@ -0,0 +1,88 @@ +import logging +import multiprocessing +import optparse + +from mopidy import get_version, settings +from mopidy.utils import get_class +from mopidy.utils.log import setup_logging +from mopidy.utils.path import get_or_create_folder +from mopidy.utils.process import BaseProcess, unpickle_connection +from mopidy.utils.settings import list_settings_optparse_callback + +logger = logging.getLogger('mopidy.core') + +class CoreProcess(BaseProcess): + def __init__(self): + super(CoreProcess, self).__init__(name='CoreProcess') + self.core_queue = multiprocessing.Queue() + self.options = self.parse_options() + self.output_queue = None + self.backend = None + self.frontend = None + + def parse_options(self): + parser = optparse.OptionParser(version='Mopidy %s' % get_version()) + parser.add_option('-q', '--quiet', + action='store_const', const=0, dest='verbosity_level', + help='less output (warning level)') + parser.add_option('-v', '--verbose', + action='store_const', const=2, dest='verbosity_level', + help='more output (debug level)') + parser.add_option('--dump', + action='store_true', dest='dump', + help='dump debug log to file') + parser.add_option('--list-settings', + action='callback', callback=list_settings_optparse_callback, + help='list current settings') + return parser.parse_args()[0] + + def run_inside_try(self): + logger.info(u'-- Starting Mopidy --') + self.setup() + while True: + message = self.core_queue.get() + self.process_message(message) + + def setup(self): + self.setup_logging() + self.setup_settings() + self.output_queue = self.setup_output(self.core_queue) + self.backend = self.setup_backend(self.core_queue, self.output_queue) + self.frontend = self.setup_frontend(self.core_queue, self.backend) + + def setup_logging(self): + setup_logging(self.options.verbosity_level, self.options.dump) + + def setup_settings(self): + get_or_create_folder('~/.mopidy/') + settings.validate() + + def setup_output(self, core_queue): + output_queue = multiprocessing.Queue() + get_class(settings.OUTPUT)(core_queue, output_queue) + return output_queue + + def setup_backend(self, core_queue, output_queue): + return get_class(settings.BACKENDS[0])(core_queue, output_queue) + + def setup_frontend(self, core_queue, backend): + frontend = get_class(settings.FRONTENDS[0])() + frontend.start_server(core_queue) + frontend.create_dispatcher(backend) + return frontend + + def process_message(self, message): + if message.get('to') == 'output': + self.output_queue.put(message) + elif message['command'] == 'mpd_request': + response = self.frontend.dispatcher.handle_request(message['request']) + connection = unpickle_connection(message['reply_to']) + connection.send(response) + elif message['command'] == 'end_of_track': + self.backend.playback.on_end_of_track() + elif message['command'] == 'stop_playback': + self.backend.playback.stop() + elif message['command'] == 'set_stored_playlists': + self.backend.stored_playlists.playlists = message['playlists'] + else: + logger.warning(u'Cannot handle message: %s', message) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 53f2003f..048f5748 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -1,5 +1,5 @@ from mopidy.frontends.mpd.dispatcher import MpdDispatcher -from mopidy.frontends.mpd.server import MpdServer +from mopidy.frontends.mpd.process import MpdProcess class MpdFrontend(object): """ @@ -17,8 +17,8 @@ class MpdFrontend(object): :param core_queue: the core queue :type core_queue: :class:`multiprocessing.Queue` """ - self.server = MpdServer(core_queue) - self.server.start() + self.process = MpdProcess(core_queue) + self.process.start() def create_dispatcher(self, backend): """ @@ -28,6 +28,5 @@ class MpdFrontend(object): :type backend: :class:`mopidy.backends.base.BaseBackend` :rtype: :class:`mopidy.frontends.mpd.dispatcher.MpdDispatcher` """ - self.dispatcher = MpdDispatcher(backend) return self.dispatcher diff --git a/mopidy/frontends/mpd/process.py b/mopidy/frontends/mpd/process.py new file mode 100644 index 00000000..7bd95900 --- /dev/null +++ b/mopidy/frontends/mpd/process.py @@ -0,0 +1,18 @@ +import asyncore +import logging + +from mopidy.frontends.mpd.server import MpdServer +from mopidy.utils.process import BaseProcess + +logger = logging.getLogger('mopidy.frontends.mpd.process') + +class MpdProcess(BaseProcess): + def __init__(self, core_queue): + super(MpdProcess, self).__init__(name='MpdProcess') + self.core_queue = core_queue + + def run_inside_try(self): + logger.debug(u'Starting MPD server process') + server = MpdServer(self.core_queue) + server.start() + asyncore.loop() diff --git a/mopidy/frontends/mpd/session.py b/mopidy/frontends/mpd/session.py index e5d2d12d..a9eba73e 100644 --- a/mopidy/frontends/mpd/session.py +++ b/mopidy/frontends/mpd/session.py @@ -3,8 +3,8 @@ import logging import multiprocessing from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR, VERSION -from mopidy.process import pickle_connection from mopidy.utils import indent +from mopidy.utils.process import pickle_connection logger = logging.getLogger('mopidy.frontends.mpd.session') diff --git a/mopidy/mixers/gstreamer_software.py b/mopidy/mixers/gstreamer_software.py index 2910ef72..1225cafd 100644 --- a/mopidy/mixers/gstreamer_software.py +++ b/mopidy/mixers/gstreamer_software.py @@ -1,7 +1,7 @@ import multiprocessing from mopidy.mixers import BaseMixer -from mopidy.process import pickle_connection +from mopidy.utils.process import pickle_connection class GStreamerSoftwareMixer(BaseMixer): """Mixer which uses GStreamer to control volume in software.""" diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py index d78863aa..f859791b 100644 --- a/mopidy/mixers/nad.py +++ b/mopidy/mixers/nad.py @@ -3,9 +3,9 @@ from serial import Serial from multiprocessing import Pipe from mopidy.mixers import BaseMixer -from mopidy.process import BaseProcess from mopidy.settings import (MIXER_EXT_PORT, MIXER_EXT_SOURCE, MIXER_EXT_SPEAKERS_A, MIXER_EXT_SPEAKERS_B) +from mopidy.utils.process import BaseProcess logger = logging.getLogger('mopidy.mixers.nad') @@ -74,7 +74,7 @@ class NadTalker(BaseProcess): _nad_volume = None def __init__(self, pipe=None): - super(NadTalker, self).__init__() + super(NadTalker, self).__init__(name='NadTalker') self.pipe = pipe self._device = None diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index ca5a98c5..a1544f87 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -8,7 +8,7 @@ import gst import logging import threading -from mopidy.process import BaseProcess, unpickle_connection +from mopidy.utils.process import BaseProcess, unpickle_connection logger = logging.getLogger('mopidy.outputs.gstreamer') @@ -49,7 +49,7 @@ class GStreamerProcess(BaseProcess): ]) def __init__(self, core_queue, output_queue): - super(GStreamerProcess, self).__init__() + super(GStreamerProcess, self).__init__(name='GStreamerProcess') self.core_queue = core_queue self.output_queue = output_queue self.gst_pipeline = None diff --git a/mopidy/process.py b/mopidy/process.py deleted file mode 100644 index 008129b3..00000000 --- a/mopidy/process.py +++ /dev/null @@ -1,79 +0,0 @@ -import logging -import multiprocessing -from multiprocessing.reduction import reduce_connection -import pickle -import sys - -from mopidy import SettingsError - -logger = logging.getLogger('mopidy.process') - -def pickle_connection(connection): - return pickle.dumps(reduce_connection(connection)) - -def unpickle_connection(pickled_connection): - # From http://stackoverflow.com/questions/1446004 - (func, args) = pickle.loads(pickled_connection) - return func(*args) - - -class BaseProcess(multiprocessing.Process): - def run(self): - try: - self.run_inside_try() - except KeyboardInterrupt: - logger.info(u'Interrupted by user') - sys.exit(0) - except SettingsError as e: - logger.error(e.message) - sys.exit(1) - except ImportError as e: - logger.error(e) - sys.exit(1) - except Exception as e: - logger.exception(e) - raise e - - def run_inside_try(self): - raise NotImplementedError - - -class CoreProcess(BaseProcess): - def __init__(self, core_queue, output_class, backend_class, frontend): - super(CoreProcess, self).__init__() - self.core_queue = core_queue - self.output_queue = None - self.output_class = output_class - self.backend_class = backend_class - self.output = None - self.backend = None - self.frontend = frontend - self.dispatcher = None - - def run_inside_try(self): - self.setup() - while True: - message = self.core_queue.get() - self.process_message(message) - - def setup(self): - self.output_queue = multiprocessing.Queue() - self.output = self.output_class(self.core_queue, self.output_queue) - self.backend = self.backend_class(self.core_queue, self.output_queue) - self.dispatcher = self.frontend.create_dispatcher(self.backend) - - def process_message(self, message): - if message.get('to') == 'output': - self.output_queue.put(message) - elif message['command'] == 'mpd_request': - response = self.dispatcher.handle_request(message['request']) - connection = unpickle_connection(message['reply_to']) - connection.send(response) - elif message['command'] == 'end_of_track': - self.backend.playback.on_end_of_track() - elif message['command'] == 'stop_playback': - self.backend.playback.stop() - elif message['command'] == 'set_stored_playlists': - self.backend.stored_playlists.playlists = message['playlists'] - else: - logger.warning(u'Cannot handle message: %s', message) diff --git a/mopidy/utils/log.py b/mopidy/utils/log.py new file mode 100644 index 00000000..e75a6df6 --- /dev/null +++ b/mopidy/utils/log.py @@ -0,0 +1,28 @@ +import logging +import logging.handlers + +from mopidy import settings + +def setup_logging(verbosity_level, dump): + setup_console_logging(verbosity_level) + if dump: + setup_dump_logging() + +def setup_console_logging(verbosity_level): + if verbosity_level == 0: + level = logging.WARNING + elif verbosity_level == 2: + level = logging.DEBUG + else: + level = logging.INFO + logging.basicConfig(format=settings.CONSOLE_LOG_FORMAT, level=level) + +def setup_dump_logging(): + root = logging.getLogger('') + root.setLevel(logging.DEBUG) + formatter = logging.Formatter(settings.DUMP_LOG_FORMAT) + handler = logging.handlers.RotatingFileHandler( + settings.DUMP_LOG_FILENAME, maxBytes=102400, backupCount=3) + handler.setFormatter(formatter) + root.addHandler(handler) + diff --git a/mopidy/utils/process.py b/mopidy/utils/process.py new file mode 100644 index 00000000..73224840 --- /dev/null +++ b/mopidy/utils/process.py @@ -0,0 +1,39 @@ +import logging +import multiprocessing +from multiprocessing.reduction import reduce_connection +import pickle +import sys + +from mopidy import SettingsError + +logger = logging.getLogger('mopidy.utils.process') + +def pickle_connection(connection): + return pickle.dumps(reduce_connection(connection)) + +def unpickle_connection(pickled_connection): + # From http://stackoverflow.com/questions/1446004 + (func, args) = pickle.loads(pickled_connection) + return func(*args) + + +class BaseProcess(multiprocessing.Process): + def run(self): + logger.debug(u'%s: Starting process', self.name) + try: + self.run_inside_try() + except KeyboardInterrupt: + logger.info(u'%s: Interrupted by user', self.name) + sys.exit(0) + except SettingsError as e: + logger.error(e.message) + sys.exit(1) + except ImportError as e: + logger.error(e) + sys.exit(1) + except Exception as e: + logger.exception(e) + raise e + + def run_inside_try(self): + raise NotImplementedError diff --git a/tests/outputs/gstreamer_test.py b/tests/outputs/gstreamer_test.py index c063aaee..5f681f23 100644 --- a/tests/outputs/gstreamer_test.py +++ b/tests/outputs/gstreamer_test.py @@ -2,8 +2,8 @@ import multiprocessing import unittest from mopidy.outputs.gstreamer import GStreamerOutput -from mopidy.process import pickle_connection from mopidy.utils.path import path_to_uri +from mopidy.utils.process import pickle_connection from tests import data_folder, SkipTest