Source code for musicdb.mdbapi.server

# MusicDB,  a music manager with web-bases UI that focus on music.
# Copyright (C) 2017 - 2021  Ralf Stemmer <ralf.stemmer@gmx.net>
# 
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""
This is the main module to run the MusicDB Server.

To start and run the server, the following sequence of function calls is necessary:

    .. code-block:: python

        Initialize(mdbconfig, musicdatabase)
        StartWebSocketServer()
        Run()


This module maintain global instances of the following classes.
Those objects can be used inside the thread the server runs. 
Usually the main thread.
Using these objects saves a lot of memory.
If the server is not started, the objects are all ``None``.

    * :class:`musicdb.lib.db.musicdb.MusicDatabase` as ``database``
    * :class:`musicdb.mdbapi.mise.MusicDBMicroSearchEngine` as ``mise``
    * :class:`musicdb.lib.cfg.musicdb.MusicDBConfig` as ``cfg``

"""

import traceback
import random
import time
import signal
from musicdb.lib.cfg.musicdb    import MusicDBConfig
from musicdb.lib.db.musicdb     import MusicDatabase
from musicdb.lib.filesystem     import Filesystem
from musicdb.lib.ws.server      import MusicDBWebSocketServer
from musicdb.mdbapi.mise        import MusicDBMicroSearchEngine
from musicdb.mdbapi.audiostream import StartAudioStreamingThread, StopAudioStreamingThread
from musicdb.mdbapi.videostream import StartVideoStreamingThread, StopVideoStreamingThread
from musicdb.taskmanagement.managementthread    import StartTaskManagementThread, StopTaskManagementThread
import logging

# Global objects
# This is the server environment that needs to be accessed by many objects
# Instances
database    = None  # music.db object
mise        = None  # micro search engine object
cfg         = None  # overall configuration file
# WS Server
tlswsserver = None
shutdown    = False


def SendSignalToServer(signum):
    """
    .. warning::

        This method requires root privileges!

    This method can be used by any MusicDB module to send a signal to the MusicDB server.
    The signals are listen in the `Python Signal Module <https://docs.python.org/3/library/signal.html>`_.

    .. Example:

        .. code-block:: python

            import signal
            from musicdb.mdbapi.server import SendSignal

            SendSignalToServer(signal.SIGTERM)  # Trigger server to shut down

    The bash-equivalent of this call is:

    .. code-block:: bash

        systemctl kill -s $signum --kill-who=main musicdb.service

    Args:
        signum (Signals): A signal as listed in the `Python Signal Module <https://docs.python.org/3/library/signal.html>`_

    Returns:
        *Nothing*
    """
    fs = Filesystem()
    try:
        logging.debug("Sending signal %i to musicdb.service", signum)
        fs.Execute(["systemctl", "kill", "-s", str(signum), "--kill-who=main", "musicdb.service"])
    except ChildProcessError as e:
        logging.error("Sending signal %i to musicdb.service failed with error: %s", signum, str(e))
    return 



[docs]def SignalHandler(signum, stack): """ This is the general signal handle for the MusicDB Server. This function reacts on system signals and calls the handler of a specific signal. Args: signum: signal number stack: current stack frame Returns: Nothing """ if signum == signal.SIGTERM: logging.debug("Got signal TERM") SIGTERM_Handler() else: logging.warning("Got unexpected signal %s"%str(signum))
[docs]def UpdateCaches(): """ This function handles the signal to refresh the server cache. Its task is to trigger updating the servers cache and to inform the clients to update. On server side: * The MiSE Cache gets updated by calling :meth:`musicdb.mdbapi.mise.MusicDBMicroSearchEngine.UpdateCache` To inform the clients a broadcast packet get sent with the following content: ``{method:"broadcast", fncname:"sys:refresh", fncsig:"UpdateCaches", arguments:null, pass:null}`` Example: .. code-block:: javascript if(fnc == "sys:refresh" && sig == "UpdateCaches") { MusicDB_Request("GetTags", "UpdateTagsCache"); // Update tag cache MusicDB_Request("GetFilteredArtistsWithAlbums", "ShowArtists"); // Update artist view } """ global mise global tlswsserver try: mise.UpdateCache() except Exception as e: logging.warning("Unexpected error updating MiSE cache: %s \033[0;33m(will be ignored)\033[0m", str(e)) try: packet = {} packet["method"] = "broadcast" packet["fncname"] = "sys:refresh" packet["fncsig"] = "UpdateCaches" packet["arguments"] = None packet["pass"] = None tlswsserver.factory.BroadcastPacket(packet) except Exception as e: logging.warning("Unexpected error broadcasting a tags-update: %s \033[0;33m(will be ignored)\033[0m", str(e))
[docs]def SIGTERM_Handler(): """ This function is the handler for the system signal TERM. It signals the server to shut down. """ logging.info("\033[1;36mSIGTERM:\033[1;34m Initiate Shutdown …\033[0m") global shutdown shutdown = True
[docs]def Initialize(configobj, databaseobj): """ This function initializes the whole server. It initializes lots of global objects that get shared between multiple connections. The following things happen when this method gets called: #. Assign the *configobj* and *databaseobj* to global variables ``cfg`` and ``database`` to share them between multiple connections #. Seed Python's random number generator #. Instantiate a global :meth:`musicdb.mdbapi.mise.MusicDBMicroSearchEngine` object #. Starting the upload, integration and import management via :meth:`musicdb.taskmanagement.managementthread.StartTaskManagementThread` #. Start the Audio Streaming Thread via :meth:`musicdb.mdbapi.audiostream.StartAudioStreamingThread` (see :doc:`/mdbapi/audiostream` for details) #. Start the Video Streaming Thread via :meth:`musicdb.mdbapi.videostream.StartVideoStreamingThread` (see :doc:`/mdbapi/audiostream` for details) #. Update MiSE cache via :meth:`musicdb.mdbapi.mise.MusicDBMicroSearchEngine.UpdateCache` Args: configobj: :class:`~musicdb.lib.cfg.musicdb.MusicDBConfig` that gets shared between connections databaseobj: :class:`~musicdb.lib.db.musicdb.MusicDatabase` that gets shared between connections Returns: ``None`` Raises: TypeError: When *configobj* is not of type :class:`~musicdb.lib.cfg.musicdb.MusicDBConfig` TypeError: When *databaseobj* is not of type :class:`~musicdb.lib.cfg.musicdb.MusicDatabase` """ global cfg global database if type(configobj) != MusicDBConfig: logging.critical("Config-class of unknown type!") raise TypeError("configobj must be a valid MusicDBConfig object!") if type(databaseobj) != MusicDatabase: logging.critical("Database-class of unknown type!") raise TypeError("databaseobj must be a valid MusicDatabase object") cfg = configobj database = databaseobj random.seed() # Initialize all interfaces logging.debug("Initializing MicroSearchEngine…") global mise mise = MusicDBMicroSearchEngine(cfg) logging.debug("Starting Task Management…") StartTaskManagementThread(cfg, database) # Start/Connect all interfaces logging.debug("Starting Streaming Thread…") StartAudioStreamingThread(cfg, database) StartVideoStreamingThread(cfg, database) logging.debug("Updating MiSE Cache…") mise.UpdateCache() # Signal Handler signal.signal(signal.SIGTERM, SignalHandler) signal.signal(signal.SIGUSR1, SignalHandler) return None
[docs]def StartWebSocketServer(): """ This function creates and starts the actual MusicDB Websocket Server. Returns: ``True`` on success, otherwise ``False`` """ global tlswsserver tlswsserver = MusicDBWebSocketServer() retval = tlswsserver.Setup( cfg.websocket.bind, cfg.websocket.port, cfg.websocket.cert, cfg.websocket.key) if retval == False: logging.critical("Setup for websocket server failed!") return False retval = tlswsserver.Start() if retval == False: logging.critical("Starting websocket server failed!") return False return True
[docs]def Shutdown(): """ This function stops the server and all its dependent threads. The following things happen when this function gets called: #. Stopping task management via :meth:`musicdb.taskmanagement.managementthread.StopTaskManagementThread` #. Stop the Audio Streaming Thread via :meth:`musicdb.mdbapi.audiostream.StopAudioStreamingThread` #. Stop the Video Streaming Thread via :meth:`musicdb.mdbapi.videostream.StopVideoStreamingThread` #. Stop the websocket server At the end, the program gets terminated. So, this function gets never left. The exit code will be ``0`` if this was a regular shutdown. If the shutdown got forced, for example due to a critical error, the exit code is ``1``. This function should also be called when :meth:`~musicdb.mdbapi.server.Initialize` fails or when it raises an exception. """ logging.info("Shutting down MusicDB-Server") global tlswsserver if tlswsserver: logging.debug("Disconnect from clients…") tlswsserver.factory.CloseConnections() logging.debug("Stopping Task Management Thread…") StopTaskManagementThread() logging.debug("Stopping Streaming Threads…") StopAudioStreamingThread() StopVideoStreamingThread() if tlswsserver: logging.debug("Stopping TLS WS Server…") tlswsserver.Stop() # dead end global shutdown if shutdown: exit(0) else: exit(1)
[docs]def Run(): """ This is the servers main loop. Inside the loop all MusicDB Websocket Server events get handled by calling :meth:`musicdb.lib.ws.server.MusicDBWebSocketServer.HandleEvents`. When a shutdown gets triggered the :meth:`~mdbapi.server.Shutdown` function gets called and the server stops. The :meth:`~musicdb.mdbapi.server.Shutdown` gets also called the user presses *Ctrl-C* This leads to a regular shutdown. In as an exception occurs the :meth:`~musicdb.mdbapi.server.Shutdown` gets called, too. In this case the exit-code will be ``1``. """ logging.info("Setup complete. \033[1;37mExecuting server.\033[1;34m") # enter event loop global tlswsserver if not tlswsserver: logging.critical("TLS Websocket Server was not started!") return try: global shutdown while True: tlswsserver.HandleEvents() if shutdown: Shutdown() time.sleep(.1) # Avoid high CPU load except KeyboardInterrupt: logging.warning("user initiated server shutdown"); shutdown = True # signal that this is a correct shutdown and no crash Shutdown() except Exception as e: logging.critical("FATAL ERROR (shutting down server!!):"); logging.critical(e) traceback.print_exc() Shutdown()
# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4