WebHost: some updates (#603)
* WebHost: Make custom server prefer ipv4 for display * WebHost: Make server retry saving in case of connection issues * WebHost: fix autolaunch guardians getting stuck waiting for the oldest two rooms. Probably not related to the issues of the system itself getting stuck, but should be fixed anyway. * WebHost: logfile is meant to be guarded by access cookie * WebHost: set patch target to null if port is not valid, disabling auto-connect
This commit is contained in:
parent
517a2db9d8
commit
e47527087e
|
@ -23,6 +23,11 @@ ModuleUpdate.update()
|
||||||
|
|
||||||
import websockets
|
import websockets
|
||||||
import colorama
|
import colorama
|
||||||
|
try:
|
||||||
|
# ponyorm is a requirement for webhost, not default server, so may not be importable
|
||||||
|
from pony.orm.dbapiprovider import OperationalError
|
||||||
|
except ImportError:
|
||||||
|
OperationalError = ConnectionError
|
||||||
|
|
||||||
import NetUtils
|
import NetUtils
|
||||||
from worlds.AutoWorld import AutoWorldRegister
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
|
@ -404,12 +409,16 @@ class Context:
|
||||||
def save_regularly():
|
def save_regularly():
|
||||||
import time
|
import time
|
||||||
while not self.exit_event.is_set():
|
while not self.exit_event.is_set():
|
||||||
time.sleep(self.auto_save_interval)
|
try:
|
||||||
if self.save_dirty:
|
time.sleep(self.auto_save_interval)
|
||||||
logging.debug("Saving via thread.")
|
if self.save_dirty:
|
||||||
|
logging.debug("Saving via thread.")
|
||||||
|
self._save()
|
||||||
|
except OperationalError as e:
|
||||||
|
logging.exception(e)
|
||||||
|
logging.info(f"Saving failed. Retry in {self.auto_save_interval} seconds.")
|
||||||
|
else:
|
||||||
self.save_dirty = False
|
self.save_dirty = False
|
||||||
self._save()
|
|
||||||
|
|
||||||
self.auto_saver_thread = threading.Thread(target=save_regularly, daemon=True)
|
self.auto_saver_thread = threading.Thread(target=save_regularly, daemon=True)
|
||||||
self.auto_saver_thread.start()
|
self.auto_saver_thread.start()
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ from WebHostLib.autolauncher import autohost, autogen
|
||||||
from WebHostLib.lttpsprites import update_sprites_lttp
|
from WebHostLib.lttpsprites import update_sprites_lttp
|
||||||
from WebHostLib.options import create as create_options_files
|
from WebHostLib.options import create as create_options_files
|
||||||
|
|
||||||
from worlds.AutoWorld import AutoWorldRegister, WebWorld
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
|
|
||||||
configpath = os.path.abspath("config.yaml")
|
configpath = os.path.abspath("config.yaml")
|
||||||
if not os.path.exists(configpath): # fall back to config.yaml in home
|
if not os.path.exists(configpath): # fall back to config.yaml in home
|
||||||
|
@ -75,7 +75,6 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]
|
||||||
for guide in game_data['tutorials']:
|
for guide in game_data['tutorials']:
|
||||||
if guide and tutorial.tutorial_name == guide['name']:
|
if guide and tutorial.tutorial_name == guide['name']:
|
||||||
guide['files'].append(current_tutorial['files'][0])
|
guide['files'].append(current_tutorial['files'][0])
|
||||||
added = True
|
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
game_data['tutorials'].append(current_tutorial)
|
game_data['tutorials'].append(current_tutorial)
|
||||||
|
@ -109,7 +108,6 @@ if __name__ == "__main__":
|
||||||
autogen(app.config)
|
autogen(app.config)
|
||||||
if app.config["SELFHOST"]: # using WSGI, you just want to run get_app()
|
if app.config["SELFHOST"]: # using WSGI, you just want to run get_app()
|
||||||
if app.config["DEBUG"]:
|
if app.config["DEBUG"]:
|
||||||
autohost(app.config)
|
|
||||||
app.run(debug=True, port=app.config["PORT"])
|
app.run(debug=True, port=app.config["PORT"])
|
||||||
else:
|
else:
|
||||||
serve(app, port=app.config["PORT"], threads=app.config["WAITRESS_THREADS"])
|
serve(app, port=app.config["PORT"], threads=app.config["WAITRESS_THREADS"])
|
||||||
|
|
|
@ -170,7 +170,9 @@ def _read_log(path: str):
|
||||||
|
|
||||||
@app.route('/log/<suuid:room>')
|
@app.route('/log/<suuid:room>')
|
||||||
def display_log(room: UUID):
|
def display_log(room: UUID):
|
||||||
return Response(_read_log(os.path.join("logs", str(room) + ".txt")), mimetype="text/plain;charset=UTF-8")
|
if room.owner == session["_id"]:
|
||||||
|
return Response(_read_log(os.path.join("logs", str(room) + ".txt")), mimetype="text/plain;charset=UTF-8")
|
||||||
|
return "Access Denied", 403
|
||||||
|
|
||||||
|
|
||||||
@app.route('/room/<suuid:room>', methods=['GET', 'POST'])
|
@app.route('/room/<suuid:room>', methods=['GET', 'POST'])
|
||||||
|
|
|
@ -2,8 +2,9 @@ from __future__ import annotations
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
|
import threading
|
||||||
from datetime import timedelta, datetime
|
from datetime import timedelta, datetime
|
||||||
import concurrent.futures
|
|
||||||
import sys
|
import sys
|
||||||
import typing
|
import typing
|
||||||
import time
|
import time
|
||||||
|
@ -17,6 +18,7 @@ from Utils import restricted_loads
|
||||||
class CommonLocker():
|
class CommonLocker():
|
||||||
"""Uses a file lock to signal that something is already running"""
|
"""Uses a file lock to signal that something is already running"""
|
||||||
lock_folder = "file_locks"
|
lock_folder = "file_locks"
|
||||||
|
|
||||||
def __init__(self, lockname: str, folder=None):
|
def __init__(self, lockname: str, folder=None):
|
||||||
if folder:
|
if folder:
|
||||||
self.lock_folder = folder
|
self.lock_folder = folder
|
||||||
|
@ -110,6 +112,7 @@ def autohost(config: dict):
|
||||||
def keep_running():
|
def keep_running():
|
||||||
try:
|
try:
|
||||||
with Locker("autohost"):
|
with Locker("autohost"):
|
||||||
|
run_guardian()
|
||||||
while 1:
|
while 1:
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
with db_session:
|
with db_session:
|
||||||
|
@ -162,16 +165,15 @@ def autogen(config: dict):
|
||||||
threading.Thread(target=keep_running, name="AP_Autogen").start()
|
threading.Thread(target=keep_running, name="AP_Autogen").start()
|
||||||
|
|
||||||
|
|
||||||
multiworlds = {}
|
multiworlds: typing.Dict[type(Room.id), MultiworldInstance] = {}
|
||||||
|
|
||||||
guardians = concurrent.futures.ThreadPoolExecutor(2, thread_name_prefix="Guardian")
|
|
||||||
|
|
||||||
|
|
||||||
class MultiworldInstance():
|
class MultiworldInstance():
|
||||||
def __init__(self, room: Room, config: dict):
|
def __init__(self, room: Room, config: dict):
|
||||||
self.room_id = room.id
|
self.room_id = room.id
|
||||||
self.process: typing.Optional[multiprocessing.Process] = None
|
self.process: typing.Optional[multiprocessing.Process] = None
|
||||||
multiworlds[self.room_id] = self
|
with guardian_lock:
|
||||||
|
multiworlds[self.room_id] = self
|
||||||
self.ponyconfig = config["PONY"]
|
self.ponyconfig = config["PONY"]
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
|
@ -179,21 +181,48 @@ class MultiworldInstance():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
logging.info(f"Spinning up {self.room_id}")
|
logging.info(f"Spinning up {self.room_id}")
|
||||||
self.process = multiprocessing.Process(group=None, target=run_server_process,
|
process = multiprocessing.Process(group=None, target=run_server_process,
|
||||||
args=(self.room_id, self.ponyconfig),
|
args=(self.room_id, self.ponyconfig),
|
||||||
name="MultiHost")
|
name="MultiHost")
|
||||||
self.process.start()
|
process.start()
|
||||||
self.guardian = guardians.submit(self._collect)
|
# bind after start to prevent thread sync issues with guardian.
|
||||||
|
self.process = process
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
if self.process:
|
if self.process:
|
||||||
self.process.terminate()
|
self.process.terminate()
|
||||||
self.process = None
|
self.process = None
|
||||||
|
|
||||||
def _collect(self):
|
def done(self):
|
||||||
|
return self.process and not self.process.is_alive()
|
||||||
|
|
||||||
|
def collect(self):
|
||||||
self.process.join() # wait for process to finish
|
self.process.join() # wait for process to finish
|
||||||
self.process = None
|
self.process = None
|
||||||
self.guardian = None
|
|
||||||
|
|
||||||
|
guardian = None
|
||||||
|
guardian_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def run_guardian():
|
||||||
|
global guardian
|
||||||
|
global multiworlds
|
||||||
|
with guardian_lock:
|
||||||
|
if not guardian:
|
||||||
|
def guard():
|
||||||
|
while 1:
|
||||||
|
time.sleep(1)
|
||||||
|
done = []
|
||||||
|
with guardian_lock:
|
||||||
|
for key, instance in multiworlds.items():
|
||||||
|
if instance.done():
|
||||||
|
instance.collect()
|
||||||
|
done.append(key)
|
||||||
|
for key in done:
|
||||||
|
del (multiworlds[key])
|
||||||
|
|
||||||
|
guardian = threading.Thread(name="Guardian", target=guard)
|
||||||
|
|
||||||
|
|
||||||
from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed
|
from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
import logging
|
|
||||||
import websockets
|
import websockets
|
||||||
import asyncio
|
import asyncio
|
||||||
import socket
|
import socket
|
||||||
|
@ -9,6 +8,7 @@ import threading
|
||||||
import time
|
import time
|
||||||
import random
|
import random
|
||||||
import pickle
|
import pickle
|
||||||
|
import logging
|
||||||
|
|
||||||
import Utils
|
import Utils
|
||||||
from .models import *
|
from .models import *
|
||||||
|
@ -128,15 +128,21 @@ def run_server_process(room_id, ponyconfig: dict):
|
||||||
ping_interval=None)
|
ping_interval=None)
|
||||||
|
|
||||||
await ctx.server
|
await ctx.server
|
||||||
|
port = 0
|
||||||
for wssocket in ctx.server.ws_server.sockets:
|
for wssocket in ctx.server.ws_server.sockets:
|
||||||
socketname = wssocket.getsockname()
|
socketname = wssocket.getsockname()
|
||||||
if wssocket.family == socket.AF_INET6:
|
if wssocket.family == socket.AF_INET6:
|
||||||
logging.info(f'Hosting game at [{get_public_ipv6()}]:{socketname[1]}')
|
logging.info(f'Hosting game at [{get_public_ipv6()}]:{socketname[1]}')
|
||||||
with db_session:
|
# Prefer IPv4, as most users seem to not have working ipv6 support
|
||||||
room = Room.get(id=ctx.room_id)
|
if not port:
|
||||||
room.last_port = socketname[1]
|
port = socketname[1]
|
||||||
elif wssocket.family == socket.AF_INET:
|
elif wssocket.family == socket.AF_INET:
|
||||||
logging.info(f'Hosting game at {get_public_ipv4()}:{socketname[1]}')
|
logging.info(f'Hosting game at {get_public_ipv4()}:{socketname[1]}')
|
||||||
|
port = socketname[1]
|
||||||
|
if port:
|
||||||
|
with db_session:
|
||||||
|
room = Room.get(id=ctx.room_id)
|
||||||
|
room.last_port = port
|
||||||
with db_session:
|
with db_session:
|
||||||
ctx.auto_shutdown = Room.get(id=room_id).timeout
|
ctx.auto_shutdown = Room.get(id=room_id).timeout
|
||||||
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
|
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
|
||||||
|
@ -146,6 +152,3 @@ def run_server_process(room_id, ponyconfig: dict):
|
||||||
from .autolauncher import Locker
|
from .autolauncher import Locker
|
||||||
with Locker(room_id):
|
with Locker(room_id):
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
|
|
||||||
|
|
||||||
from WebHostLib import LOGS_FOLDER
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ def download_patch(room_id, patch_id):
|
||||||
with zipfile.ZipFile(filelike, "a") as zf:
|
with zipfile.ZipFile(filelike, "a") as zf:
|
||||||
with zf.open("archipelago.json", "r") as f:
|
with zf.open("archipelago.json", "r") as f:
|
||||||
manifest = json.load(f)
|
manifest = json.load(f)
|
||||||
manifest["server"] = f"{app.config['PATCH_TARGET']}:{last_port}"
|
manifest["server"] = f"{app.config['PATCH_TARGET']}:{last_port}" if last_port else None
|
||||||
with zipfile.ZipFile(new_file, "w") as new_zip:
|
with zipfile.ZipFile(new_file, "w") as new_zip:
|
||||||
for file in zf.infolist():
|
for file in zf.infolist():
|
||||||
if file.filename == "archipelago.json":
|
if file.filename == "archipelago.json":
|
||||||
|
@ -55,7 +55,7 @@ def download_spoiler(seed_id):
|
||||||
def download_slot_file(room_id, player_id: int):
|
def download_slot_file(room_id, player_id: int):
|
||||||
room = Room.get(id=room_id)
|
room = Room.get(id=room_id)
|
||||||
slot_data: Slot = select(patch for patch in room.seed.slots if
|
slot_data: Slot = select(patch for patch in room.seed.slots if
|
||||||
patch.player_id == player_id).first()
|
patch.player_id == player_id).first()
|
||||||
|
|
||||||
if not slot_data:
|
if not slot_data:
|
||||||
return "Slot Data not found"
|
return "Slot Data not found"
|
||||||
|
@ -71,7 +71,7 @@ def download_slot_file(room_id, player_id: int):
|
||||||
with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf:
|
with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf:
|
||||||
for name in zf.namelist():
|
for name in zf.namelist():
|
||||||
if name.endswith("info.json"):
|
if name.endswith("info.json"):
|
||||||
fname = name.rsplit("/", 1)[0]+".zip"
|
fname = name.rsplit("/", 1)[0] + ".zip"
|
||||||
elif slot_data.game == "Ocarina of Time":
|
elif slot_data.game == "Ocarina of Time":
|
||||||
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apz5"
|
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apz5"
|
||||||
elif slot_data.game == "VVVVVV":
|
elif slot_data.game == "VVVVVV":
|
||||||
|
@ -82,6 +82,7 @@ def download_slot_file(room_id, player_id: int):
|
||||||
return "Game download not supported."
|
return "Game download not supported."
|
||||||
return send_file(io.BytesIO(slot_data.data), as_attachment=True, attachment_filename=fname)
|
return send_file(io.BytesIO(slot_data.data), as_attachment=True, attachment_filename=fname)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/templates")
|
@app.route("/templates")
|
||||||
@cache.cached()
|
@cache.cached()
|
||||||
def list_yaml_templates():
|
def list_yaml_templates():
|
||||||
|
@ -90,4 +91,4 @@ def list_yaml_templates():
|
||||||
for world_name, world in AutoWorldRegister.world_types.items():
|
for world_name, world in AutoWorldRegister.world_types.items():
|
||||||
if not world.hidden:
|
if not world.hidden:
|
||||||
files.append(world_name)
|
files.append(world_name)
|
||||||
return render_template("templates.html", files=files)
|
return render_template("templates.html", files=files)
|
||||||
|
|
Loading…
Reference in New Issue