From 65226e09dc716576111d3e7596068bee40016a37 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Sat, 24 Oct 2020 21:31:52 +0100 Subject: [PATCH] Initial working Windows Service. --- install/SMWinservice.py | 98 ++++++++ install/debug.bat | 2 + install/install.bat | 12 + install/requirements-windows.txt | 1 + requirements.txt => install/requirements.txt | 0 install/windows_service.py | 36 +++ player.py | 231 ++++++++++--------- server.py | 30 ++- state_manager.py | 2 +- 9 files changed, 293 insertions(+), 119 deletions(-) create mode 100644 install/SMWinservice.py create mode 100644 install/debug.bat create mode 100644 install/install.bat create mode 100644 install/requirements-windows.txt rename requirements.txt => install/requirements.txt (100%) create mode 100644 install/windows_service.py diff --git a/install/SMWinservice.py b/install/SMWinservice.py new file mode 100644 index 0000000..def79ca --- /dev/null +++ b/install/SMWinservice.py @@ -0,0 +1,98 @@ +''' +SMWinservice +by Davide Mastromatteo + +Base class to create winservice in Python +----------------------------------------- + +Instructions: + +1. Just create a new class that inherits from this base class +2. Define into the new class the variables + _svc_name_ = "nameOfWinservice" + _svc_display_name_ = "name of the Winservice that will be displayed in scm" + _svc_description_ = "description of the Winservice that will be displayed in scm" +3. Override the three main methods: + def start(self) : if you need to do something at the service initialization. + A good idea is to put here the inizialization of the running condition + def stop(self) : if you need to do something just before the service is stopped. + A good idea is to put here the invalidation of the running condition + def main(self) : your actual run loop. Just create a loop based on your running condition +4. Define the entry point of your module calling the method "parse_command_line" of the new class +5. Enjoy +''' + +import socket + +import win32serviceutil + +import servicemanager +import win32event +import win32service + + +class SMWinservice(win32serviceutil.ServiceFramework): + '''Base class to create winservice in Python''' + + _svc_name_ = 'pythonService' + _svc_display_name_ = 'Python Service' + _svc_description_ = 'Python Service Description' + + @classmethod + def parse_command_line(cls): + ''' + ClassMethod to parse the command line + ''' + win32serviceutil.HandleCommandLine(cls) + + def __init__(self, args): + ''' + Constructor of the winservice + ''' + win32serviceutil.ServiceFramework.__init__(self, args) + self.hWaitStop = win32event.CreateEvent(None, 0, 0, None) + socket.setdefaulttimeout(60) + + def SvcStop(self): + ''' + Called when the service is asked to stop + ''' + self.stop() + self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) + win32event.SetEvent(self.hWaitStop) + + def SvcDoRun(self): + ''' + Called when the service is asked to start + ''' + self.start() + servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE, + servicemanager.PYS_SERVICE_STARTED, + (self._svc_name_, '')) + self.main() + + def start(self): + ''' + Override to add logic before the start + eg. running condition + ''' + pass + + def stop(self): + ''' + Override to add logic before the stop + eg. invalidating running condition + ''' + pass + + def main(self): + ''' + Main class to be ovverridden to add logic + ''' + pass + + +# entry point of the module: copy and paste into the new module +# ensuring you are calling the "parse_command_line" of the new created class +if __name__ == '__main__': + SMWinservice.parse_command_line() diff --git a/install/debug.bat b/install/debug.bat new file mode 100644 index 0000000..a140f65 --- /dev/null +++ b/install/debug.bat @@ -0,0 +1,2 @@ +python C:\Users\matth\Documents\GitHub\bapsicle\install\windows_service.py debug +TIMEOUT 10 \ No newline at end of file diff --git a/install/install.bat b/install/install.bat new file mode 100644 index 0000000..e6e4225 --- /dev/null +++ b/install/install.bat @@ -0,0 +1,12 @@ +cd /D "%~dp0" +pip install -r requirements.txt +pip install -r requirements-windows.txt +pip install -e ..\ +python windows_service.py install + +mkdir "C:\Program Files\BAPSicle" +cd "C:\Program Files\BAPSicle\" +mkdir state + +copy "C:\Program Files\Python37\Lib\site-packages\pywin32_system32\pywintypes37.dll" "C:\Program Files\Python37\Lib\site-packages\win32\" +TIMEOUT 10 \ No newline at end of file diff --git a/install/requirements-windows.txt b/install/requirements-windows.txt new file mode 100644 index 0000000..afd24f6 --- /dev/null +++ b/install/requirements-windows.txt @@ -0,0 +1 @@ +pywin32 \ No newline at end of file diff --git a/requirements.txt b/install/requirements.txt similarity index 100% rename from requirements.txt rename to install/requirements.txt diff --git a/install/windows_service.py b/install/windows_service.py new file mode 100644 index 0000000..f0df182 --- /dev/null +++ b/install/windows_service.py @@ -0,0 +1,36 @@ +from server import BAPSicleServer +from pathlib import Path +from SMWinservice import SMWinservice +import time +import multiprocessing + +import sys +sys.path.append("..\\") + + +class BAPScileAsAService(SMWinservice): + _svc_name_ = "BAPSicle" + _svc_display_name_ = "BAPSicle Server" + _svc_description_ = "BAPS development has been frozen for a while, but this new spike of progress is dripping." + + def start(self): + self.isrunning = True + self.server = multiprocessing.Process(target=BAPSicleServer).start() + + def stop(self): + print("stopping") + self.isrunning = False + try: + self.server.terminate() + self.server.join() + except: + pass + + def main(self): + while self.isrunning: + time.sleep(1) + print("BAPSicle is running.") + + +if __name__ == '__main__': + BAPScileAsAService.parse_command_line() diff --git a/player.py b/player.py index a34eee6..d119954 100644 --- a/player.py +++ b/player.py @@ -6,144 +6,147 @@ import copy from state_manager import StateManager + class Player(): - state = None + state = None + running = False - __default_state = { - "filename": "", - "channel": -1, - "playing": False, - "pos": 0, - "remaining": 0, - "length": 0, - "loop": False, - "output": None - } + __default_state = { + "filename": "", + "channel": -1, + "playing": False, + "pos": 0, + "remaining": 0, + "length": 0, + "loop": False, + "output": None + } - def isInit(self): - try: - pygame.mixer.music.get_busy() - except: - return False - else: - return True + def isInit(self): + try: + pygame.mixer.music.get_busy() + except: + return False + else: + return True - def isPlaying(self): - return bool(pygame.mixer.music.get_busy()) + def isPlaying(self): + return bool(pygame.mixer.music.get_busy()) - def play(self): + def play(self): + pygame.mixer.music.play(0) - pygame.mixer.music.play(0) + def pause(self): + pygame.mixer.music.pause() - def pause(self): - pygame.mixer.music.pause() + def unpause(self): + pygame.mixer.music.play(0, self.state.state["pos"]) - def unpause(self): - pygame.mixer.music.play(0, self.state.state["pos"]) + def stop(self): + pygame.mixer.music.stop() - def stop(self): - pygame.mixer.music.stop() + def seek(self, pos): + if self.isPlaying(): + pygame.mixer.music.play(0, pos) + else: + self.updateState(pos) - def seek(self, pos): - if self.isPlaying(): - pygame.mixer.music.play(0, pos) - else: - self.updateState(pos) + def load(self, filename): + if not self.isPlaying(): + self.state.update("filename", filename) + pygame.mixer.music.load(filename) + if ".mp3" in filename: + song = MP3(filename) + self.state.update("length", song.info.length) + else: + self.state.update("length", pygame.mixer.Sound(filename).get_length()/1000) - def load(self, filename): - if not self.isPlaying(): - self.state.update("filename",filename) - pygame.mixer.music.load(filename) - if ".mp3" in filename: - song = MP3(filename) - self.state.update("length",song.info.length) - else: - self.state.update("length",pygame.mixer.Sound(filename).get_length()/1000) + def quit(self): + pygame.mixer.quit() - def output(self, name = None): - pygame.mixer.quit() - try: - if name: - pygame.mixer.init(44100, -16, 1, 1024, devicename=name) - else: - pygame.mixer.init(44100, -16, 1, 1024) - except: - return "FAIL:Failed to init mixer, check sound devices." - else: - self.state.update("output",name) - - return "OK" + def output(self, name=None): + self.quit() + try: + if name: + pygame.mixer.init(44100, -16, 1, 1024, devicename=name) + else: + pygame.mixer.init(44100, -16, 1, 1024) + except: + return "FAIL:Failed to init mixer, check sound devices." + else: + self.state.update("output", name) + return "OK" - def updateState(self, pos = None): - self.state.update("playing",self.isPlaying()) - if (pos): - self.state.update("pos",max(0,pos)) - else: - self.state.update("pos",max(0,pygame.mixer.music.get_pos()/1000)) - self.state.update("remaining",self.state.state["length"] - self.state.state["pos"]) + def updateState(self, pos=None): + self.state.update("playing", self.isPlaying()) + if (pos): + self.state.update("pos", max(0, pos)) + else: + self.state.update("pos", max(0, pygame.mixer.music.get_pos()/1000)) + self.state.update("remaining", self.state.state["length"] - self.state.state["pos"]) - def getDetails(self): - res = "RESP:DETAILS: " + json.dumps(self.state.state) - return res + def getDetails(self): + res = "RESP:DETAILS: " + json.dumps(self.state.state) + return res + def __init__(self, channel, in_q, out_q): + self.running = True - def __init__(self, channel, in_q, out_q): + self.state = StateManager("channel" + str(channel), self.__default_state) - self.state = StateManager("channel" + str(channel), self.__default_state) + self.state.update("channel", channel) - self.state.update("channel", channel) + loaded_state = copy.copy(self.state.state) - loaded_state = copy.copy(self.state.state) + if loaded_state["output"]: + print("Setting output to: " + loaded_state["output"]) + self.output(loaded_state["output"]) + else: + self.output() - if loaded_state["output"]: - print("Setting output to: " + loaded_state["output"]) - self.output(loaded_state["output"]) - else: - self.output() + if loaded_state["filename"]: + print("Loading filename: " + loaded_state["filename"]) + self.load(loaded_state["filename"]) - if loaded_state["filename"]: - print("Loading filename: " + loaded_state["filename"]) - self.load(loaded_state["filename"]) + if loaded_state["pos"] != 0: + print("Seeking to pos: " + str(loaded_state["pos"])) + self.seek(loaded_state["pos"]) - if loaded_state["pos"] != 0: - print("Seeking to pos: " + str(loaded_state["pos"])) - self.seek(loaded_state["pos"]) + if loaded_state["playing"] == True: + print("Resuming.") + self.unpause() - if loaded_state["playing"] == True: - print("Resuming.") - self.unpause() + while self.running: + time.sleep(0.01) + incoming_msg = in_q.get() + if (not incoming_msg): + continue + if self.isInit(): + self.updateState() + if (incoming_msg == 'PLAY'): + self.play() + if (incoming_msg == 'PAUSE'): + self.pause() + if (incoming_msg == 'UNPAUSE'): + self.unpause() + if (incoming_msg == 'STOP'): + self.stop() + if (incoming_msg == 'QUIT'): + self.quit() + self.running = False + if (incoming_msg.startswith("SEEK")): + split = incoming_msg.split(":") + self.seek(float(split[1])) + if (incoming_msg.startswith("LOAD")): + split = incoming_msg.split(":") + self.load(split[1]) + if (incoming_msg == 'DETAILS'): + out_q.put(self.getDetails()) + if (incoming_msg.startswith("OUTPUT")): + split = incoming_msg.split(":") + out_q.put(self.output(split[1])) - - - while True: - time.sleep(0.01) - incoming_msg = in_q.get() - if (not incoming_msg): - continue - if self.isInit(): - self.updateState() - if (incoming_msg == 'PLAY'): - self.play() - if (incoming_msg == 'PAUSE'): - self.pause() - if (incoming_msg == 'UNPAUSE'): - self.unpause() - if (incoming_msg == 'STOP'): - self.stop() - if (incoming_msg.startswith("SEEK")): - split = incoming_msg.split(":") - self.seek(float(split[1])) - if (incoming_msg.startswith("LOAD")): - split = incoming_msg.split(":") - self.load(split[1]) - if (incoming_msg == 'DETAILS'): - out_q.put(self.getDetails()) - - - if (incoming_msg.startswith("OUTPUT")): - split = incoming_msg.split(":") - out_q.put(self.output(split[1])) - + print("Quiting player ", channel) diff --git a/server.py b/server.py index b12280b..e03680c 100644 --- a/server.py +++ b/server.py @@ -1,9 +1,18 @@ import multiprocessing import player -from flask import Flask, render_template, send_from_directory +from flask import Flask, render_template, send_from_directory, request import json import sounddevice as sd + +class BAPSicleServer(): + def __init__(self): + startServer() + + def __del__(self): + stopServer() + + app = Flask(__name__, static_url_path='') channel_to_q = [] @@ -135,8 +144,7 @@ def send_static(path): return send_from_directory('ui-static', path) -if __name__ == "__main__": - +def startServer(): for channel in range(3): channel_to_q.append(multiprocessing.Queue()) channel_from_q.append(multiprocessing.Queue()) @@ -144,10 +152,24 @@ if __name__ == "__main__": channel_p.append( multiprocessing.Process( target=player.Player, - args=(channel, channel_to_q[-1], channel_from_q[-1]) + args=(channel, channel_to_q[-1], channel_from_q[-1]), + daemon=True ) ) channel_p[channel].start() # Don't use reloader, it causes Nested Processes! app.run(host='0.0.0.0', port=5000, debug=True, use_reloader=False) + + +def stopServer(): + print("Stopping server.py") + for q in channel_to_q: + q.put("QUIT") + for player in channel_p: + player.join() + app = None + + +if __name__ == "__main__": + print("BAPSicle is a service. Please run it like one.") diff --git a/state_manager.py b/state_manager.py index 16d213d..139bdaa 100644 --- a/state_manager.py +++ b/state_manager.py @@ -7,7 +7,7 @@ class StateManager: __state = {} def __init__(self, name, default_state=None): - self.filepath = "state/" + name + ".json" + self.filepath = "C:\Program Files\BAPSicle\state\\" + name + ".json" if not os.path.isfile(self.filepath): self.log("No file found for " + self.filepath) try: