Merge pull request #2 from mstratford/windows-exe
Initial Windows Install Support
This commit is contained in:
commit
6c6f6660a9
27 changed files with 1687 additions and 169 deletions
18
.gitignore
vendored
18
.gitignore
vendored
|
@ -1,6 +1,22 @@
|
|||
|
||||
.vscode/
|
||||
|
||||
|
||||
__pycache__/
|
||||
|
||||
state/
|
||||
|
||||
*.egg-info/
|
||||
|
||||
build/build-exe-config.json
|
||||
|
||||
install/*.exe
|
||||
|
||||
*.pyo
|
||||
|
||||
*.spec
|
||||
|
||||
build/build-exe-pyinstaller-command.bat
|
||||
|
||||
build/build/BAPSicle/
|
||||
|
||||
build/output/
|
||||
|
|
12
.vscode/launch.json
vendored
Normal file
12
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python: Launch Server Standalone",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "./launch_standalone.py",
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
]
|
||||
}
|
48
README.md
48
README.md
|
@ -1 +1,47 @@
|
|||
bapsicle
|
||||
# Bapsicle
|
||||
### a.k.a. The Next-Gen BAPS server
|
||||
|
||||
!["BAPSicle logo, a pink melting ice lolly."](/dev/logo.png "BAPSicle Logo")
|
||||
|
||||
Welcome! This is BAPS. More acurately, this is yet another attempt at a BAPS3 server.
|
||||
|
||||
## Installing
|
||||
|
||||
Just want to install BAPSicle?
|
||||
|
||||
### Windows
|
||||
|
||||
Currently there's just a batch script. Simply run ``install.bat`` as administrator. If you've just built BAPSicle youself, it'll be in ``/install`` folder.
|
||||
|
||||
This will:
|
||||
* Copy BAPSicle into ``C:\Program Files\BAPSicle``
|
||||
* Install BAPSicle.exe as a Windows Service with NSSM.
|
||||
* If all goes well, open [http://localhost:5000](localhost:5000) for the server UI.
|
||||
|
||||
### Linux
|
||||
|
||||
Installed service for linux is comming soon. Testing is primarily on Ubuntu 20.04. Your milage with other distros will vary.
|
||||
|
||||
### MacOS
|
||||
|
||||
Support for MacOS will be the last to come, sorry about that.
|
||||
|
||||
## Developing
|
||||
|
||||
### Requirements
|
||||
|
||||
* Python 3.7 (3.8 may also work, 3.9 is unlikely to.)
|
||||
* Git (Obviously)
|
||||
|
||||
### Running
|
||||
To just run the server standaline without installing, run ``python ./launch_standalone.py``.
|
||||
|
||||
### Building
|
||||
|
||||
Currently mostly Windows focused.
|
||||
|
||||
To build a BAPSicle.exe, run ``build\build-exe.py``. The resulting file will appear in ``build\output``. You can then use the install instructions above to install it, or just run it standalone.
|
||||
|
||||
### Other bits
|
||||
|
||||
Provided is a VScode debug config to let you debug live, as well as ``dev\install-githook.bat`` that will help git to clean your code up as you're committing!
|
|
@ -0,0 +1,5 @@
|
|||
# see
|
||||
import logging
|
||||
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
||||
logging.basicConfig(filename='bapsicle_log.log', level=logging.INFO)
|
||||
logging.info('Started Logging')
|
77
build/build-exe-config.template.json
Normal file
77
build/build-exe-config.template.json
Normal file
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
"version": "auto-py-to-exe-configuration_v1",
|
||||
"pyinstallerOptions": [
|
||||
{
|
||||
"optionDest": "noconfirm",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"optionDest": "filenames",
|
||||
"value": "\\launch_standalone.py"
|
||||
},
|
||||
{
|
||||
"optionDest": "onefile",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"optionDest": "console",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"optionDest": "icon_file",
|
||||
"value": "\\build\\icon.ico"
|
||||
},
|
||||
{
|
||||
"optionDest": "name",
|
||||
"value": "BAPSicle"
|
||||
},
|
||||
{
|
||||
"optionDest": "ascii",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"optionDest": "clean_build",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"optionDest": "strip",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"optionDest": "noupx",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"optionDest": "uac_admin",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"optionDest": "uac_uiaccess",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"optionDest": "win_private_assemblies",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"optionDest": "win_no_prefer_redirects",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"optionDest": "bootloader_ignore_signals",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"optionDest": "datas",
|
||||
"value": "\\templates;templates/"
|
||||
},
|
||||
{
|
||||
"optionDest": "datas",
|
||||
"value": "\\ui-static;ui-static/"
|
||||
}
|
||||
],
|
||||
"nonPyinstallerOptions": {
|
||||
"increaseRecursionLimit": false,
|
||||
"manualArguments": ""
|
||||
}
|
||||
}
|
18
build/build-exe.bat
Normal file
18
build/build-exe.bat
Normal file
|
@ -0,0 +1,18 @@
|
|||
cd /D "%~dp0"
|
||||
pip install -r requirements.txt
|
||||
pip install -r requirements-windows.txt
|
||||
pip install -e ..\
|
||||
|
||||
: Generate the json config in case you wanted to use the gui to regenerate the command below manually.
|
||||
python generate-build-exe-config.py
|
||||
|
||||
: auto-py-to-exe -c build-exe-config.json -o ../install
|
||||
|
||||
python build-exe.py
|
||||
|
||||
build-exe-pyinstaller-command.bat
|
||||
|
||||
del *.spec /q
|
||||
|
||||
echo "Output file should be located in 'output/' folder."
|
||||
TIMEOUT 5
|
43
build/build-exe.py
Normal file
43
build/build-exe.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
import sys
|
||||
import json
|
||||
|
||||
file = open('build-exe-config.json', 'r')
|
||||
config = json.loads(file.read())
|
||||
file.close()
|
||||
|
||||
cmd_str = "pyinstaller "
|
||||
json_dests = ["icon_file", "clean_build"]
|
||||
pyi_dests = ["icon", "clean"]
|
||||
|
||||
for option in config["pyinstallerOptions"]:
|
||||
|
||||
option_dest = option["optionDest"]
|
||||
|
||||
# The json is rather inconsistent :/
|
||||
if option_dest in json_dests:
|
||||
print("in")
|
||||
option_dest = pyi_dests[json_dests.index(option_dest)]
|
||||
|
||||
option_dest = option_dest.replace("_", "-")
|
||||
|
||||
if option_dest == "datas":
|
||||
cmd_str += '--add-data "' + option["value"] + '" '
|
||||
elif option_dest == "filenames":
|
||||
filename = option["value"]
|
||||
elif option["value"] == True:
|
||||
cmd_str += "--" + str(option_dest) + " "
|
||||
elif option["value"] == False:
|
||||
pass
|
||||
else:
|
||||
cmd_str += "--" + str(option_dest) + ' "' + str(option["value"]) + '" '
|
||||
|
||||
|
||||
command = open('build-exe-pyinstaller-command.bat', 'w')
|
||||
|
||||
if filename == "":
|
||||
print("No filename data was found in json file.")
|
||||
command.write("")
|
||||
else:
|
||||
command.write(cmd_str + ' --distpath "output/" --workpath "build/" "' + filename + '"')
|
||||
|
||||
command.close()
|
17
build/generate-build-exe-config.py
Normal file
17
build/generate-build-exe-config.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
import json
|
||||
import os
|
||||
|
||||
dir_path = os.path.dirname(os.path.realpath(__file__))
|
||||
parent_path = os.path.dirname(dir_path)
|
||||
|
||||
in_file = open('build-exe-config.template.json', 'r')
|
||||
config = json.loads(in_file.read())
|
||||
in_file.close()
|
||||
|
||||
for option in config["pyinstallerOptions"]:
|
||||
if option["optionDest"] in ["datas", "filenames", "icon_file"]:
|
||||
option["value"] = os.path.abspath(parent_path + option["value"])
|
||||
|
||||
out_file = open('build-exe-config.json', 'w')
|
||||
out_file.write(json.dumps(config, indent=2))
|
||||
out_file.close()
|
BIN
build/icon.ico
Normal file
BIN
build/icon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 111 KiB |
2
build/requirements-windows.txt
Normal file
2
build/requirements-windows.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
pywin32
|
||||
auto-py-to-exe
|
|
@ -2,4 +2,5 @@ pygame==2.0.0.dev20
|
|||
flask
|
||||
mutagen
|
||||
sounddevice
|
||||
autopep8
|
||||
autopep8
|
||||
setproctitle
|
2
dev/install-githook.bat
Normal file
2
dev/install-githook.bat
Normal file
|
@ -0,0 +1,2 @@
|
|||
cd %~dp0
|
||||
copy /Y ".\pre-commit" "..\.git\hooks\"
|
BIN
dev/logo.png
Normal file
BIN
dev/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 51 KiB |
42
helpers/os_environment.py
Normal file
42
helpers/os_environment.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
import sys
|
||||
import os
|
||||
|
||||
# Check if we're running inside a pyinstaller bundled (it's an exe)
|
||||
|
||||
|
||||
def isBundelled():
|
||||
return getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS')
|
||||
|
||||
|
||||
def isWindows():
|
||||
return sys.platform.startswith('win32')
|
||||
|
||||
|
||||
def isLinux():
|
||||
return sys.platform.startswith('linux')
|
||||
|
||||
|
||||
def isMacOS():
|
||||
return sys.platform.startswith('darwin')
|
||||
|
||||
# This must be used to that relative file paths resolve inside the bundled versions.
|
||||
|
||||
|
||||
def resolve_local_file_path(relative_path):
|
||||
""" Get absolute path to resource, works for dev and for PyInstaller """
|
||||
try:
|
||||
# PyInstaller creates a temp folder and stores path in _MEIPASS
|
||||
base_path = sys._MEIPASS
|
||||
except Exception:
|
||||
base_path = os.path.abspath(".")
|
||||
|
||||
return os.path.join(base_path, relative_path)
|
||||
|
||||
# Use this to resolve paths to resources not bundled within the bundled exe.
|
||||
|
||||
|
||||
def resolve_external_file_path(relative_path):
|
||||
if (not relative_path.startswith("/")):
|
||||
relative_path = "/" + relative_path
|
||||
# Pass through abspath to correct any /'s with \'s on Windows
|
||||
return os.path.abspath(os.getcwd() + relative_path)
|
|
@ -1,98 +0,0 @@
|
|||
'''
|
||||
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()
|
|
@ -1,2 +0,0 @@
|
|||
python C:\Users\matth\Documents\GitHub\bapsicle\install\windows_service.py debug
|
||||
TIMEOUT 10
|
|
@ -1,12 +1,44 @@
|
|||
cd /D "%~dp0"
|
||||
pip install -r requirements.txt
|
||||
pip install -r requirements-windows.txt
|
||||
pip install -e ..\
|
||||
python windows_service.py install
|
||||
set install_path="C:\Program Files\BAPSicle"
|
||||
set exe_name="BAPSicle.exe"
|
||||
set exe_path=%install_path%\\%exe_name%
|
||||
set service_name="BAPSicle"
|
||||
|
||||
mkdir "C:\Program Files\BAPSicle"
|
||||
cd "C:\Program Files\BAPSicle\"
|
||||
mkdir state
|
||||
mkdir %install_path%
|
||||
mkdir %install_path%\state
|
||||
|
||||
copy "C:\Program Files\Python37\Lib\site-packages\pywin32_system32\pywintypes37.dll" "C:\Program Files\Python37\Lib\site-packages\win32\"
|
||||
TIMEOUT 10
|
||||
|
||||
cd %~dp0\nssm
|
||||
nssm stop %service_name%
|
||||
nssm remove %service_name% confirm
|
||||
sc.exe delete %service_name%
|
||||
|
||||
cd %install_path%
|
||||
|
||||
|
||||
copy /Y "%~dp0\uninstall.bat" .
|
||||
copy /Y "%~dp0\..\build\output\%exe_name%" %exe_name%
|
||||
|
||||
mkdir nssm
|
||||
cd nssm
|
||||
copy /Y "%~dp0\nssm\nssm.exe" .
|
||||
nssm install %service_name% %exe_path%
|
||||
nssm set %service_name% AppDirectory %install_path%
|
||||
nssm set %service_name% AppExit Default Restart
|
||||
nssm set %service_name% AppStopMethodConsole 5000
|
||||
nssm set %service_name% AppStopMethodWindow 5000
|
||||
nssm set %service_name% AppStopMethodThreads 5000
|
||||
nssm set %service_name% DisplayName "BAPSicle Server"
|
||||
nssm set %service_name% Description "The next gen Broadcast and Presenting Suite server! Access settings on port 5000."
|
||||
nssm set %service_name% ObjectName LocalSystem
|
||||
nssm set %service_name% Start SERVICE_AUTO_START
|
||||
nssm set %service_name% Type SERVICE_INTERACTIVE_PROCESS
|
||||
|
||||
: usefull tools are edit and dump:
|
||||
|
||||
: nssm edit %service_name%
|
||||
: nssm dump %service_name%
|
||||
nssm start %service_name%
|
||||
|
||||
timeout 4 /nobreak
|
||||
|
||||
explorer "http://localhost:5000/"
|
||||
|
|
257
install/nssm/ChangeLog.txt
Normal file
257
install/nssm/ChangeLog.txt
Normal file
|
@ -0,0 +1,257 @@
|
|||
Changes since 2.24
|
||||
------------------
|
||||
* Allow skipping kill_process_tree().
|
||||
|
||||
* NSSM can now sleep a configurable amount of time after
|
||||
rotating output files.
|
||||
|
||||
* NSSM can now rotate log files by calling CopyFile()
|
||||
followed by SetEndOfFile(), allowing it to rotate files
|
||||
which other processes hold open.
|
||||
|
||||
* NSSM now sets the service environment before querying
|
||||
parameters from the registry, so paths and arguments
|
||||
can reference environment configured in AppEnvironment
|
||||
or AppEnvironmentExtra.
|
||||
|
||||
Changes since 2.23
|
||||
------------------
|
||||
* NSSM once again calls TerminateProcess() correctly.
|
||||
|
||||
Changes since 2.22
|
||||
------------------
|
||||
* NSSM no longer clutters the event log with "The specified
|
||||
procedure could not be found" on legacy Windows releases.
|
||||
|
||||
* Fixed failure to set a local username to run the service.
|
||||
|
||||
Changes since 2.21
|
||||
------------------
|
||||
* Existing services can now be managed using the GUI
|
||||
or on the command line.
|
||||
|
||||
* NSSM can now set the priority class and processor
|
||||
affinity of the managed application.
|
||||
|
||||
* NSSM can now apply an unconditional delay before
|
||||
restarting the application.
|
||||
|
||||
* NSSM can now optionally rotate existing files when
|
||||
redirecting I/O.
|
||||
|
||||
* Unqualified path names are now relative to the
|
||||
application startup directory when redirecting I/O.
|
||||
|
||||
* NSSM can now set the service display name, description,
|
||||
startup type and log on details.
|
||||
|
||||
* All services now receive a standard console window,
|
||||
allowing them to read input correctly (if running in
|
||||
interactive mode).
|
||||
|
||||
Changes since 2.20
|
||||
------------------
|
||||
* Services installed from the GUI no longer have incorrect
|
||||
AppParameters set in the registry.
|
||||
|
||||
Changes since 2.19
|
||||
------------------
|
||||
* Services installed from the commandline without using the
|
||||
GUI no longer have incorrect AppStopMethod* registry
|
||||
entries set.
|
||||
|
||||
Changes since 2.18
|
||||
------------------
|
||||
* Support AppEnvironmentExtra to append to the environment
|
||||
instead of replacing it.
|
||||
|
||||
* The GUI is significantly less sucky.
|
||||
|
||||
Changes since 2.17
|
||||
------------------
|
||||
* Timeouts for each shutdown method can be configured in
|
||||
the registry.
|
||||
|
||||
* The GUI is slightly less sucky.
|
||||
|
||||
Changes since 2.16
|
||||
------------------
|
||||
* NSSM can now redirect the service's I/O streams to any path
|
||||
capable of being opened by CreateFile().
|
||||
|
||||
* Allow building on Visual Studio Express.
|
||||
|
||||
* Silently ignore INTERROGATE control.
|
||||
|
||||
* Try to send Control-C events to console applications when
|
||||
shutting them down.
|
||||
|
||||
Changes since 2.15
|
||||
------------------
|
||||
* Fixed case where NSSM could kill unrelated processes when
|
||||
shutting down.
|
||||
|
||||
Changes since 2.14
|
||||
------------------
|
||||
* NSSM is now translated into Italian.
|
||||
|
||||
* Fixed GUI not allowing paths longer than 256 characters.
|
||||
|
||||
Changes since 2.13
|
||||
------------------
|
||||
* Fixed default GUI language being French not English.
|
||||
|
||||
Changes since 2.12
|
||||
------------------
|
||||
* Fixed failure to run on Windows 2000.
|
||||
|
||||
Changes since 2.11
|
||||
------------------
|
||||
* NSSM is now translated into French.
|
||||
|
||||
* Really ensure systems recovery actions can happen.
|
||||
|
||||
The change supposedly introduced in v2.4 to allow service recovery
|
||||
actions to be activated when the application exits gracefully with
|
||||
a non-zero error code didn't actually work.
|
||||
|
||||
Changes since 2.10
|
||||
------------------
|
||||
* Support AppEnvironment for compatibility with srvany.
|
||||
|
||||
Changes since 2.9
|
||||
-----------------
|
||||
* Fixed failure to compile messages.mc in paths containing spaces.
|
||||
|
||||
* Fixed edge case with CreateProcess().
|
||||
|
||||
Correctly handle the case where the application executable is under
|
||||
a path which contains space and an executable sharing the initial
|
||||
part of that path (up to a space) exists.
|
||||
|
||||
Changes since 2.8
|
||||
-----------------
|
||||
* Fixed failure to run on Windows versions prior to Vista.
|
||||
|
||||
Changes since 2.7
|
||||
-----------------
|
||||
* Read Application, AppDirectory and AppParameters before each restart so
|
||||
a change to any one doesn't require restarting NSSM itself.
|
||||
|
||||
* Fixed messages not being sent to the event log correctly in some
|
||||
cases.
|
||||
|
||||
* Try to handle (strictly incorrect) quotes in AppDirectory.
|
||||
|
||||
Windows directories aren't allowed to contain quotes so CreateProcess()
|
||||
will fail if the AppDirectory is quoted. Note that it succeeds even if
|
||||
Application itself is quoted as the application plus parameters are
|
||||
interpreted as a command line.
|
||||
|
||||
* Fixed failed to write full arguments to AppParameters when
|
||||
installing a service.
|
||||
|
||||
* Throttle restarts.
|
||||
|
||||
Back off from restarting the application immediately if it starts
|
||||
successfully but exits too soon. The default value of "too soon" is
|
||||
1500 milliseconds. This can be configured by adding a DWORD value
|
||||
AppThrottle to the registry.
|
||||
|
||||
Handle resume messages from the service console to restart the
|
||||
application immediately even if it is throttled.
|
||||
|
||||
* Try to kill the process tree gracefully.
|
||||
|
||||
Before calling TerminateProcess() on all processes assocatiated with
|
||||
the monitored application, enumerate all windows and threads and
|
||||
post appropriate messages to them. If the application bothers to
|
||||
listen for such messages it has a chance to shut itself down gracefully.
|
||||
|
||||
Changes since 2.6
|
||||
-----------------
|
||||
* Handle missing registry values.
|
||||
|
||||
Warn if AppParameters is missing. Warn if AppDirectory is missing or
|
||||
unset and choose a fallback directory.
|
||||
First try to find the parent directory of the application. If that
|
||||
fails, eg because the application path is just "notepad" or something,
|
||||
start in the Windows directory.
|
||||
|
||||
* Kill process tree when stopping service.
|
||||
|
||||
Ensure that all child processes of the monitored application are
|
||||
killed when the service stops by recursing through all running
|
||||
processes and terminating those whose parent is the application
|
||||
or one of its descendents.
|
||||
|
||||
Changes since 2.5
|
||||
-----------------
|
||||
* Removed incorrect ExpandEnvironmentStrings() error.
|
||||
|
||||
A log_event() call was inadvertently left in the code causing an error
|
||||
to be set to the eventlog saying that ExpandEnvironmentStrings() had
|
||||
failed when it had actually succeeded.
|
||||
|
||||
Changes since 2.4
|
||||
-----------------
|
||||
* Allow use of REG_EXPAND_SZ values in the registry.
|
||||
|
||||
* Don't suicide on exit status 0 by default.
|
||||
|
||||
Suiciding when the application exits 0 will cause recovery actions to be
|
||||
taken. Usually this is inappropriate. Only suicide if there is an
|
||||
explicit AppExit value for 0 in the registry.
|
||||
|
||||
Technically such behaviour could be abused to do something like run a
|
||||
script after successful completion of a service but in most cases a
|
||||
suicide is undesirable when no actual failure occurred.
|
||||
|
||||
* Don't hang if startup parameters couldn't be determined.
|
||||
Instead, signal that the service entered the STOPPED state.
|
||||
Set START_PENDING state prior to actual startup.
|
||||
|
||||
Changes since 2.3
|
||||
-----------------
|
||||
* Ensure systems recovery actions can happen.
|
||||
|
||||
In Windows versions earlier than Vista the service manager would only
|
||||
consider a service failed (and hence eligible for recovery action) if
|
||||
the service exited without setting its state to SERVICE_STOPPED, even if
|
||||
it signalled an error exit code.
|
||||
In Vista and later the service manager can be configured to treat a
|
||||
graceful shutdown with error code as a failure but this is not the
|
||||
default behaviour.
|
||||
|
||||
Try to configure the service manager to use the new behaviour when
|
||||
starting the service so users who set AppExit to Exit can use recovery
|
||||
actions as expected.
|
||||
|
||||
Also recognise the new AppExit option Suicide for use on pre-Vista
|
||||
systems. When AppExit is Suicide don't stop the service but exit
|
||||
inelegantly, which should be seen as a failure.
|
||||
|
||||
Changes since 2.2
|
||||
-----------------
|
||||
* Send properly formatted messages to the event log.
|
||||
|
||||
* Fixed truncation of very long path lengths in the registry.
|
||||
|
||||
Changes since 2.1
|
||||
-----------------
|
||||
* Decide how to handle application exit.
|
||||
|
||||
When the service exits with exit code n look in
|
||||
HKLM\SYSTEM\CurrentControlSet\Services\<service>\Parameters\AppExit\<n>,
|
||||
falling back to the unnamed value if no such code is listed. Parse the
|
||||
(string) value of this entry as follows:
|
||||
|
||||
Restart: Start the application again (NSSM default).
|
||||
Ignore: Do nothing (srvany default).
|
||||
Exit: Stop the service.
|
||||
|
||||
Changes since 2.0
|
||||
-----------------
|
||||
* Added support for building a 64-bit executable.
|
||||
|
||||
* Added project files for newer versions of Visual Studio.
|
1051
install/nssm/README.txt
Normal file
1051
install/nssm/README.txt
Normal file
File diff suppressed because it is too large
Load diff
BIN
install/nssm/nssm.exe
Normal file
BIN
install/nssm/nssm.exe
Normal file
Binary file not shown.
|
@ -1 +0,0 @@
|
|||
pywin32
|
15
install/uninstall.bat
Normal file
15
install/uninstall.bat
Normal file
|
@ -0,0 +1,15 @@
|
|||
|
||||
set service_name="BAPSicle"
|
||||
|
||||
: We can't 'nssm stop because' we're about to delete it.
|
||||
: The file will remain open, so you'll get access denied.
|
||||
net stop %service_name%
|
||||
sc delete %service_name%
|
||||
|
||||
: We cd out of the folder, just in case we're about to delete
|
||||
: out PWD.
|
||||
cd \
|
||||
rmdir "C:\Program Files\BAPSicle\" /q /s
|
||||
|
||||
|
||||
PAUSE
|
|
@ -1,36 +0,0 @@
|
|||
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()
|
13
launch_standalone.py
Normal file
13
launch_standalone.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
import multiprocessing
|
||||
import time
|
||||
|
||||
from server import BAPSicleServer
|
||||
|
||||
if __name__ == '__main__':
|
||||
# On Windows calling this function is necessary.
|
||||
# Causes all kinds of loops if not present.
|
||||
multiprocessing.freeze_support()
|
||||
server = multiprocessing.Process(target=BAPSicleServer).start()
|
||||
while True:
|
||||
time.sleep(1)
|
||||
pass
|
36
player.py
36
player.py
|
@ -1,10 +1,13 @@
|
|||
import pygame
|
||||
from state_manager import StateManager
|
||||
from mutagen.mp3 import MP3
|
||||
from pygame import mixer
|
||||
import time
|
||||
import json
|
||||
from mutagen.mp3 import MP3
|
||||
import copy
|
||||
import os
|
||||
import setproctitle
|
||||
|
||||
from state_manager import StateManager
|
||||
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
|
||||
|
||||
|
||||
class Player():
|
||||
|
@ -24,54 +27,54 @@ class Player():
|
|||
|
||||
def isInit(self):
|
||||
try:
|
||||
pygame.mixer.music.get_busy()
|
||||
mixer.music.get_busy()
|
||||
except:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def isPlaying(self):
|
||||
return bool(pygame.mixer.music.get_busy())
|
||||
return bool(mixer.music.get_busy())
|
||||
|
||||
def play(self):
|
||||
|
||||
pygame.mixer.music.play(0)
|
||||
mixer.music.play(0)
|
||||
|
||||
def pause(self):
|
||||
pygame.mixer.music.pause()
|
||||
mixer.music.pause()
|
||||
|
||||
def unpause(self):
|
||||
pygame.mixer.music.play(0, self.state.state["pos"])
|
||||
mixer.music.play(0, self.state.state["pos"])
|
||||
|
||||
def stop(self):
|
||||
pygame.mixer.music.stop()
|
||||
mixer.music.stop()
|
||||
|
||||
def seek(self, pos):
|
||||
if self.isPlaying():
|
||||
pygame.mixer.music.play(0, pos)
|
||||
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)
|
||||
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)
|
||||
self.state.update("length", mixer.Sound(filename).get_length()/1000)
|
||||
|
||||
def quit(self):
|
||||
pygame.mixer.quit()
|
||||
mixer.quit()
|
||||
|
||||
def output(self, name=None):
|
||||
self.quit()
|
||||
try:
|
||||
if name:
|
||||
pygame.mixer.init(44100, -16, 1, 1024, devicename=name)
|
||||
mixer.init(44100, -16, 1, 1024, devicename=name)
|
||||
else:
|
||||
pygame.mixer.init(44100, -16, 1, 1024)
|
||||
mixer.init(44100, -16, 1, 1024)
|
||||
except:
|
||||
return "FAIL:Failed to init mixer, check sound devices."
|
||||
else:
|
||||
|
@ -84,7 +87,7 @@ class Player():
|
|||
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("pos", max(0, mixer.music.get_pos()/1000))
|
||||
self.state.update("remaining", self.state.state["length"] - self.state.state["pos"])
|
||||
|
||||
def getDetails(self):
|
||||
|
@ -93,6 +96,7 @@ class Player():
|
|||
|
||||
def __init__(self, channel, in_q, out_q):
|
||||
self.running = True
|
||||
setproctitle.setproctitle("BAPSicle - Player " + str(channel))
|
||||
|
||||
self.state = StateManager("channel" + str(channel), self.__default_state)
|
||||
|
||||
|
|
|
@ -3,6 +3,9 @@ import player
|
|||
from flask import Flask, render_template, send_from_directory, request
|
||||
import json
|
||||
import sounddevice as sd
|
||||
import setproctitle
|
||||
|
||||
setproctitle.setproctitle("BAPSicle - Server")
|
||||
|
||||
|
||||
class BAPSicleServer():
|
||||
|
@ -117,7 +120,6 @@ def seek(channel, pos):
|
|||
@app.route("/player/<int:channel>/output/<name>")
|
||||
def output(channel, name):
|
||||
channel_to_q[channel].put("OUTPUT:" + name)
|
||||
channel_to_q[channel].put("LOAD:test"+str(channel)+".mp3")
|
||||
return ui_status()
|
||||
|
||||
|
||||
|
@ -148,7 +150,6 @@ def startServer():
|
|||
for channel in range(3):
|
||||
channel_to_q.append(multiprocessing.Queue())
|
||||
channel_from_q.append(multiprocessing.Queue())
|
||||
# channel_to_q[-1].put_nowait("LOAD:test"+str(channel)+".mp3")
|
||||
channel_p.append(
|
||||
multiprocessing.Process(
|
||||
target=player.Player,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import json
|
||||
import os
|
||||
from helpers.os_environment import resolve_external_file_path
|
||||
|
||||
|
||||
class StateManager:
|
||||
|
@ -7,7 +8,7 @@ class StateManager:
|
|||
__state = {}
|
||||
|
||||
def __init__(self, name, default_state=None):
|
||||
self.filepath = "C:\Program Files\BAPSicle\state\\" + name + ".json"
|
||||
self.filepath = resolve_external_file_path("/state/" + name + ".json")
|
||||
if not os.path.isfile(self.filepath):
|
||||
self.log("No file found for " + self.filepath)
|
||||
try:
|
||||
|
|
Loading…
Reference in a new issue