Merge pull request #5 from UniversityRadioYork/macos-devel

MacOS Building Support
This commit is contained in:
Matthew Stratford 2020-11-01 02:48:52 +00:00 committed by GitHub
commit 6e608a8289
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 5493 additions and 37 deletions

2
.gitignore vendored
View file

@ -20,3 +20,5 @@ build/build-exe-pyinstaller-command.bat
build/build/BAPSicle/
build/output/
build/build-exe-pyinstaller-command.sh

View file

@ -1,7 +1,7 @@
# BAPSicle
### a.k.a. The Next-Gen BAPS server
!["BAPSicle logo, a pink melting ice lolly."](/dev/logo.png "BAPSicle Logo")
!["BAPSicle logo, a pink melting ice lolly."](docs/images/logo.png "BAPSicle Logo")
Welcome! This is BAPS. More acurately, this is yet another attempt at a BAPS3 server.
@ -16,7 +16,7 @@ Currently there's just a batch script. Simply run ``install.bat`` as administrat
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.
* If all goes well, open [http://localhost:5000](localhost:13500) for the server UI.
### Linux
@ -24,24 +24,40 @@ Installed service for linux is comming soon. Testing is primarily on Ubuntu 20.0
### MacOS
Support for MacOS will be the last to come, sorry about that.
Currently there's no installer for MacOS, so you'll have to move the ``build/output/BAPSicle.app`` you've built and make it start automatically (if you want).
Starting and stopping the server, as well as UI links, are available in the System Menu once opening the app.
!["BAPSicle in the MacOS System Menu"](docs/images/system-menu.png "System Menu")
## Developing
### Requirements
On all platforms:
* Python 3.7 (3.8 may also work, 3.9 is unlikely to.)
* Git (Obviously)
On MacOS:
* Homebrew (To install command line Platypus)
### Running
To just run the server standaline without installing, run ``python ./launch_standalone.py``.
### Building
## Building
Currently mostly Windows focused.
### Windows
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.
To build a ``BAPSicle.exe``, run ``build\build-exe.bat``. The resulting file will appear in ``build\output``. You can then use the install instructions above to install it, or just run it standalone.
### Linux
Coming soon...
### MacOS
To build a ``BAPSicle.app``, run ``build/build-macos.sh``. The resulting file will appear in ``build/output``.
### 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!
Provided is a VScode debug config to let you debug live, as well as ``dev\install-githook.{bat,sh}`` that will help git to clean your code up as you're committing!

5297
build/BAPSicle.platypus Normal file

File diff suppressed because it is too large Load diff

View file

@ -7,7 +7,7 @@
},
{
"optionDest": "filenames",
"value": "\\launch_standalone.py"
"value": "/launch_standalone.py"
},
{
"optionDest": "onefile",
@ -15,11 +15,15 @@
},
{
"optionDest": "console",
"value": true
"value": false
},
{
"optionDest": "exclude-module",
"value": "tkinter"
},
{
"optionDest": "icon_file",
"value": "\\build\\icon.ico"
"value": "/build/icon.ico"
},
{
"optionDest": "name",
@ -63,15 +67,15 @@
},
{
"optionDest": "datas",
"value": "\\templates;templates/"
"value": "/templates;templates/"
},
{
"optionDest": "datas",
"value": "\\ui-static;ui-static/"
"value": "/ui-static;ui-static/"
}
],
"nonPyinstallerOptions": {
"increaseRecursionLimit": false,
"manualArguments": ""
}
}
}

View file

@ -32,12 +32,13 @@ for option in config["pyinstallerOptions"]:
cmd_str += "--" + str(option_dest) + ' "' + str(option["value"]) + '" '
command = open('build-exe-pyinstaller-command.bat', 'w')
for format in [".bat", ".sh"]:
command = open('build-exe-pyinstaller-command'+format, 'w')
if filename == "":
print("No filename data was found in json file.")
command.write("")
else:
command.write(cmd_str + ' --distpath "output/" --workpath "build/" "' + filename + '"')
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()
command.close()

19
build/build-macos.sh Executable file
View file

@ -0,0 +1,19 @@
#!/bin/bash
cd "$(dirname "$0")"
pip3 install -r requirements.txt
pip3 install -r requirements-macos.txt
pip3 install -e ..\
python3 ./generate-build-exe-config.py
python3 ./build-exe.py
bash ./build-exe-pyinstaller-command.sh
rm ./*.spec
brew install platypus
platypus --load-profile ./BAPSicle.platypus --overwrite ./output/BAPSicle.app

View file

@ -1,5 +1,6 @@
import json
import os
from helpers.os_environment import isWindows
dir_path = os.path.dirname(os.path.realpath(__file__))
parent_path = os.path.dirname(dir_path)
@ -11,6 +12,8 @@ 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"])
if not isWindows():
option["value"] = option["value"].replace(";",":")
out_file = open('build-exe-config.json', 'w')
out_file.write(json.dumps(config, indent=2))

BIN
build/icon.icns Normal file

Binary file not shown.

BIN
build/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

27
build/macos-platypus.sh Normal file
View file

@ -0,0 +1,27 @@
#!/bin/bash
if [ "$1" == "" ]
then
echo "DISABLED|BAPSicle Server"
echo "----"
if curl --output /dev/null --silent --head --fail --max-time 1 "http://localhost:13500"
then
echo "Status"
echo "Config"
echo "Logs"
echo "----"
echo "Stop Server"
else
echo "DISABLED|Status"
echo "DISABLED|Config"
echo "DISABLED|Logs"
echo "----"
echo "Start Server"
fi
exit
fi
if [ "$1" == "Stop Server" ]
then
curl "http://localhost:13500/quit"
else
./BAPSicle "$1"
fi

View file

@ -0,0 +1 @@
pyinstaller

4
dev/install-githook.sh Normal file
View file

@ -0,0 +1,4 @@
#!/bin/bash
cd "$(dirname "$0")"
cp "./pre-commit" "../.git/hooks/"

View file

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

BIN
docs/images/system-menu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View file

@ -28,7 +28,7 @@ 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% Description "The next gen Broadcast and Presenting Suite server! Access settings on port 13500."
nssm set %service_name% ObjectName LocalSystem
nssm set %service_name% Start SERVICE_AUTO_START
nssm set %service_name% Type SERVICE_INTERACTIVE_PROCESS
@ -41,4 +41,4 @@ nssm start %service_name%
timeout 4 /nobreak
explorer "http://localhost:5000/"
explorer "http://localhost:13500/"

View file

@ -1,13 +1,48 @@
import multiprocessing
import time
import sys
import webbrowser
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()
def startServer():
server = multiprocessing.Process(target=BAPSicleServer)
server.start()
while True:
time.sleep(1)
pass
time.sleep(5)
if server and server.is_alive():
pass
else:
print("Server dead. Exiting.")
sys.exit(0)
if __name__ == '__main__':
# On Windows, calling this function is necessary.
# Causes all kinds of loops if not present.
# IT HAS TO BE RIGHT HERE, AT THE TOP OF __MAIN__
# NOT INSIDE AN IF STATEMENT. RIGHT. HERE.
# If it's not here, multiprocessing just doesn't run in the package.
# Freeze support refers to being packaged with Pyinstaller.
multiprocessing.freeze_support()
if len(sys.argv) > 1:
# We got an argument! It's probably Platypus's UI.
try:
if (sys.argv[1]) == "Start Server":
print("NOTIFICATION:Welcome to BAPSicle!")
webbrowser.open("http://localhost:13500/")
startServer()
if (sys.argv[1] == "Status"):
webbrowser.open("http://localhost:13500/status")
if (sys.argv[1] == "Config"):
webbrowser.open("http://localhost:13500/config")
if (sys.argv[1] == "Logs"):
webbrowser.open("http://localhost:13500/logs")
except Exception as e:
print("ALERT:BAPSicle failed with exception:\n", e)
sys.exit(0)
else:
startServer()

View file

@ -10,11 +10,15 @@ import setproctitle
import copy
import json
import time
from pygame import mixer
from state_manager import StateManager
from mutagen.mp3 import MP3
import os
import sys
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
from pygame import mixer
from mutagen.mp3 import MP3
from state_manager import StateManager
from helpers.os_environment import isMacOS
class Player():
@ -150,7 +154,13 @@ class Player():
def load(self, filename):
if not self.isPlaying:
self.unload()
# Fix any OS specific / or \'s
if os.path.sep == "/":
filename = filename.replace("\\", '/')
else:
filename = filename.replace("/", '\\')
print(filename)
self.state.update("filename", filename)
try:
@ -337,6 +347,7 @@ class Player():
print("Quiting player ", channel)
self.quit()
self._retMsg("EXIT")
sys.exit(0)
def showOutput(in_q, out_q):
@ -348,6 +359,8 @@ def showOutput(in_q, out_q):
if __name__ == "__main__":
if isMacOS:
multiprocessing.set_start_method("spawn", True)
in_q = multiprocessing.Queue()
out_q = multiprocessing.Queue()
@ -365,7 +378,7 @@ if __name__ == "__main__":
# Do some testing
in_q.put("LOADED?")
in_q.put("PLAY")
in_q.put("LOAD:\\Users\\matth\\Documents\\GitHub\\bapsicle\\dev\\test.mp3")
in_q.put("LOAD:\\Users\\mstratford\\Documents\\Dev\\GitHub\\bapsicle\\dev\\test.mp3")
in_q.put("LOADED?")
in_q.put("PLAY")
print("Entering infinite loop.")

View file

@ -4,6 +4,8 @@ from flask import Flask, render_template, send_from_directory, request
import json
import sounddevice as sd
import setproctitle
import logging
from helpers.os_environment import isMacOS
setproctitle.setproctitle("BAPSicle - Server")
@ -18,10 +20,16 @@ class BAPSicleServer():
app = Flask(__name__, static_url_path='')
log = logging.getLogger('werkzeug')
log.disabled = True
app.logger.disabled = True
channel_to_q = []
channel_from_q = []
channel_p = []
stopping = False
@app.errorhandler(404)
def page_not_found(e):
@ -148,6 +156,12 @@ def status(channel):
return response
@app.route("/quit")
def quit():
stopServer()
return "Shutting down..."
@app.route("/player/all/stop")
def all_stop():
for channel in channel_to_q:
@ -161,7 +175,10 @@ def send_static(path):
def startServer():
if isMacOS():
multiprocessing.set_start_method("spawn", True)
for channel in range(3):
channel_to_q.append(multiprocessing.Queue())
channel_from_q.append(multiprocessing.Queue())
channel_p.append(
@ -174,7 +191,7 @@ def startServer():
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)
app.run(host='0.0.0.0', port=13500, debug=True, use_reloader=False)
def stopServer():
@ -182,9 +199,21 @@ def stopServer():
for q in channel_to_q:
q.put("QUIT")
for player in channel_p:
player.join()
global app
app = None
try:
player.join()
except:
pass
print("Stopped all players.")
global stopping
if stopping == False:
stopping = True
shutdown = request.environ.get('werkzeug.server.shutdown')
if shutdown is None:
print("Shutting down Server.")
else:
print("Shutting down Flask.")
shutdown()
if __name__ == "__main__":

View file

@ -8,6 +8,11 @@ class StateManager:
__state = {}
def __init__(self, name, default_state=None):
try:
os.mkdir(resolve_external_file_path("/state"))
except FileExistsError:
pass
self.filepath = resolve_external_file_path("/state/" + name + ".json")
if not os.path.isfile(self.filepath):
self.log("No file found for " + self.filepath)