include web host for multiple multiworlds

This is in proof of concept stage, but also as a how-to as I get about monthly asked how to run multiple MultiServer instances for Discord Bots.
This commit is contained in:
Fabian Dill 2020-06-13 10:16:29 +02:00
parent 5da5847805
commit 7c5c32c49a
3 changed files with 105 additions and 31 deletions

View File

@ -2,21 +2,23 @@ import os
import logging import logging
import sys import sys
import threading import threading
import typing
import multiprocessing import multiprocessing
import functools import functools
import websockets import websockets
from flask import Flask, flash, request, redirect, url_for, render_template from flask import Flask, flash, request, redirect, url_for, render_template, Response
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
sys.path.append("..") if ".." not in sys.path:
from MultiServer import Context, server sys.path.append("..")
UPLOAD_FOLDER = 'uploads' UPLOAD_FOLDER = 'uploads'
LOGS_FOLDER = 'logs'
multidata_folder = os.path.join(UPLOAD_FOLDER, "multidata") multidata_folder = os.path.join(UPLOAD_FOLDER, "multidata")
os.makedirs(multidata_folder, exist_ok=True) os.makedirs(multidata_folder, exist_ok=True)
os.makedirs("logs", exist_ok=True) os.makedirs(LOGS_FOLDER, exist_ok=True)
def allowed_file(filename): def allowed_file(filename):
@ -29,10 +31,39 @@ app.config['MAX_CONTENT_LENGTH'] = 1 * 1024 * 1024 # 1 megabyte limit
app.config["SECRET_KEY"] = os.urandom(32) app.config["SECRET_KEY"] = os.urandom(32)
name = "localhost" name = "localhost"
portrange = (30000, 40000) portrange = (49152, 65535)
current_port = portrange[0] current_port = portrange[0]
current_ports = {}
current_multiworlds = {}
class Multiworld():
def __init__(self, file: str):
self.port = get_next_port()
self.multidata = file
current_ports[self.port] = self
current_multiworlds[self.multidata] = self
self.process: typing.Optional[multiprocessing.Process] = None
def start(self):
if self.process and self.process.is_alive():
return
logging.info(f"Spinning up {self.multidata}")
self.process = multiprocessing.Process(group=None, target=run_server_process,
args=(self.port, self.multidata),
name="MultiHost" + str(self.port))
self.process.start()
def stop(self):
if self.process:
self.process.terminate()
self.process = None
del (current_ports[self.port])
del (current_multiworlds[self.multidata])
@app.route('/', methods=['GET', 'POST']) @app.route('/', methods=['GET', 'POST'])
def upload_multidata(): def upload_multidata():
if request.method == 'POST': if request.method == 'POST':
@ -65,50 +96,57 @@ def upload_multidata():
portlock = threading.Lock() portlock = threading.Lock()
def get_next_port(): def get_next_port() -> int:
global current_port global current_port
with portlock: with portlock:
while current_port in current_ports:
if current_port >= portrange[1]:
current_port = portrange[0]
else:
current_port += 1 current_port += 1
return current_port return current_port
def _read_log(path: str):
with open(path) as log:
yield from log
@app.route('/log/<filename>') @app.route('/log/<filename>')
def display_log(filename): def display_log(filename: str):
with open(os.path.join("logs", filename + ".txt")) as log: # noinspection PyTypeChecker
return log.read().replace("\n", "<br>") return Response(_read_log(os.path.join("logs", filename + ".txt")), mimetype="text/plain;charset=UTF-8")
processstartlock = threading.Lock()
@app.route('/hosted/<filename>') @app.route('/hosted/<filename>')
def host_multidata(filename=None): def host_multidata(filename: str = None):
if not filename: if not filename:
return redirect(url_for('upload_multidata')) return redirect(url_for('upload_multidata'))
else: else:
multidata = os.path.join(multidata_folder, filename) multidata = os.path.join(multidata_folder, filename)
port = get_next_port() if multidata in current_multiworlds:
queue = multiprocessing.SimpleQueue() multiworld = current_multiworlds[multidata]
process = multiprocessing.Process(group=None, target=run_server_process, else:
args=(port, multidata, filename, queue), with processstartlock:
name="MultiHost" + str(port)) multiworld = Multiworld(multidata)
process.start() multiworld.start()
return "Hosting " + filename + " at " + name + ":" + str(port)
return render_template("host_multidata.html", filename=filename, port=multiworld.port, name=name)
def run_server_process(port, multidata, filename, queue): def run_server_process(port: int, multidata: str):
async def main(): async def main():
logging.basicConfig(format='[%(asctime)s] %(message)s', logging.basicConfig(format='[%(asctime)s] %(message)s',
level=logging.INFO, level=logging.INFO,
filename=os.path.join("logs", filename + ".txt")) filename=os.path.join(LOGS_FOLDER, os.path.split(multidata)[-1] + ".txt"))
ctx = Context(None, port, "", 1, 1000,
ctx = Context("", port, "", 1, 1000,
True, "enabled", "goal") True, "enabled", "goal")
ctx.load(multidata, True)
data_filename = multidata
try:
ctx.load(data_filename, True)
except Exception as e:
logging.exception('Failed to read multiworld data (%s)' % e)
raise
ctx.init_save() ctx.init_save()
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ping_timeout=None, ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ping_timeout=None,
@ -119,9 +157,10 @@ def run_server_process(port, multidata, filename, queue):
await ctx.server await ctx.server
while ctx.running: while ctx.running:
await asyncio.sleep(1) await asyncio.sleep(1)
logging.info("shutting down") logging.info("Shutting down")
import asyncio import asyncio
from MultiServer import Context, server
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
loop.run_until_complete(main()) loop.run_until_complete(main())

1
WebHost/requirements.txt Normal file
View File

@ -0,0 +1 @@
flask>=1.1.2

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Multiworld {{ filename }}</title>
</head>
<body>
Hosting {{ filename }} at {{ name }}:{{ port }}
<div id="logger"></div>
<script>
var xmlhttp = new XMLHttpRequest();
var url = '{{ url_for('display_log', filename = filename) }}';
xmlhttp.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) {
myFunction(this.responseText);
}
};
function request_new() {
xmlhttp.open("GET", url, true);
xmlhttp.send();
}
function myFunction(text) {
document.getElementById("logger").innerText = text;
}
request_new();
window.setInterval(request_new, 3000);
</script>
</body>
</html>