Initial working Windows Service.

This commit is contained in:
Matthew Stratford 2020-10-24 21:31:52 +01:00
parent f64840702f
commit 65226e09dc
9 changed files with 293 additions and 119 deletions

98
install/SMWinservice.py Normal file
View file

@ -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()

2
install/debug.bat Normal file
View file

@ -0,0 +1,2 @@
python C:\Users\matth\Documents\GitHub\bapsicle\install\windows_service.py debug
TIMEOUT 10

12
install/install.bat Normal file
View file

@ -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

View file

@ -0,0 +1 @@
pywin32

View file

@ -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()

231
player.py
View file

@ -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)

View file

@ -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.")

View file

@ -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: