2020-07-10 15:42:22 +00:00
|
|
|
from __future__ import annotations
|
2022-10-16 23:08:31 +00:00
|
|
|
|
2021-05-13 19:57:11 +00:00
|
|
|
import json
|
2022-10-16 23:08:31 +00:00
|
|
|
import logging
|
2020-07-10 15:42:22 +00:00
|
|
|
import multiprocessing
|
2022-10-16 23:08:31 +00:00
|
|
|
import os
|
2020-07-10 15:42:22 +00:00
|
|
|
import sys
|
2022-10-16 23:08:31 +00:00
|
|
|
import threading
|
2020-08-17 23:18:37 +00:00
|
|
|
import time
|
2022-10-16 23:08:31 +00:00
|
|
|
import typing
|
|
|
|
from datetime import timedelta, datetime
|
2020-07-10 15:42:22 +00:00
|
|
|
|
2020-08-17 23:18:37 +00:00
|
|
|
from pony.orm import db_session, select, commit
|
2020-07-10 15:42:22 +00:00
|
|
|
|
2021-05-13 19:57:11 +00:00
|
|
|
from Utils import restricted_loads
|
|
|
|
|
2020-07-10 15:42:22 +00:00
|
|
|
|
|
|
|
class CommonLocker():
|
|
|
|
"""Uses a file lock to signal that something is already running"""
|
2021-06-29 01:11:48 +00:00
|
|
|
lock_folder = "file_locks"
|
2022-06-07 22:35:35 +00:00
|
|
|
|
2021-06-29 01:11:48 +00:00
|
|
|
def __init__(self, lockname: str, folder=None):
|
|
|
|
if folder:
|
|
|
|
self.lock_folder = folder
|
|
|
|
os.makedirs(self.lock_folder, exist_ok=True)
|
2020-07-10 15:42:22 +00:00
|
|
|
self.lockname = lockname
|
2021-06-29 01:11:48 +00:00
|
|
|
self.lockfile = os.path.join(self.lock_folder, f"{self.lockname}.lck")
|
2020-07-10 15:42:22 +00:00
|
|
|
|
|
|
|
|
|
|
|
class AlreadyRunningException(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
if sys.platform == 'win32':
|
|
|
|
class Locker(CommonLocker):
|
|
|
|
def __enter__(self):
|
|
|
|
try:
|
|
|
|
if os.path.exists(self.lockfile):
|
|
|
|
os.unlink(self.lockfile)
|
|
|
|
self.fp = os.open(
|
|
|
|
self.lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR)
|
|
|
|
except OSError as e:
|
|
|
|
raise AlreadyRunningException() from e
|
|
|
|
|
|
|
|
def __exit__(self, _type, value, tb):
|
|
|
|
fp = getattr(self, "fp", None)
|
|
|
|
if fp:
|
|
|
|
os.close(self.fp)
|
|
|
|
os.unlink(self.lockfile)
|
|
|
|
else: # unix
|
|
|
|
import fcntl
|
|
|
|
|
2021-05-14 13:25:57 +00:00
|
|
|
|
2020-07-10 15:42:22 +00:00
|
|
|
class Locker(CommonLocker):
|
|
|
|
def __enter__(self):
|
|
|
|
try:
|
2020-07-11 14:25:06 +00:00
|
|
|
self.fp = open(self.lockfile, "wb")
|
2022-05-29 07:43:53 +00:00
|
|
|
fcntl.flock(self.fp.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
2020-07-10 15:42:22 +00:00
|
|
|
except OSError as e:
|
|
|
|
raise AlreadyRunningException() from e
|
|
|
|
|
|
|
|
def __exit__(self, _type, value, tb):
|
|
|
|
fcntl.flock(self.fp.fileno(), fcntl.LOCK_UN)
|
|
|
|
self.fp.close()
|
|
|
|
|
|
|
|
|
|
|
|
def launch_room(room: Room, config: dict):
|
|
|
|
# requires db_session!
|
2020-07-10 22:52:49 +00:00
|
|
|
if room.last_activity >= datetime.utcnow() - timedelta(seconds=room.timeout):
|
2020-07-10 15:42:22 +00:00
|
|
|
multiworld = multiworlds.get(room.id, None)
|
|
|
|
if not multiworld:
|
|
|
|
multiworld = MultiworldInstance(room, config)
|
|
|
|
|
|
|
|
multiworld.start()
|
|
|
|
|
|
|
|
|
2020-08-17 23:18:37 +00:00
|
|
|
def handle_generation_success(seed_id):
|
|
|
|
logging.info(f"Generation finished for seed {seed_id}")
|
|
|
|
|
|
|
|
|
|
|
|
def handle_generation_failure(result: BaseException):
|
|
|
|
try: # hacky way to get the full RemoteTraceback
|
|
|
|
raise result
|
|
|
|
except Exception as e:
|
|
|
|
logging.exception(e)
|
|
|
|
|
|
|
|
|
|
|
|
def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
|
2021-05-13 19:57:11 +00:00
|
|
|
try:
|
|
|
|
meta = json.loads(generation.meta)
|
|
|
|
options = restricted_loads(generation.options)
|
|
|
|
logging.info(f"Generating {generation.id} for {len(options)} players")
|
|
|
|
pool.apply_async(gen_game, (options,),
|
2021-10-10 22:46:18 +00:00
|
|
|
{"meta": meta,
|
2021-05-13 19:57:11 +00:00
|
|
|
"sid": generation.id,
|
|
|
|
"owner": generation.owner},
|
|
|
|
handle_generation_success, handle_generation_failure)
|
2021-08-30 14:31:56 +00:00
|
|
|
except Exception as e:
|
2021-05-13 19:57:11 +00:00
|
|
|
generation.state = STATE_ERROR
|
|
|
|
commit()
|
2021-08-30 14:31:56 +00:00
|
|
|
logging.exception(e)
|
2021-05-13 19:57:11 +00:00
|
|
|
else:
|
|
|
|
generation.state = STATE_STARTED
|
2020-08-17 23:18:37 +00:00
|
|
|
|
|
|
|
|
|
|
|
def init_db(pony_config: dict):
|
|
|
|
db.bind(**pony_config)
|
|
|
|
db.generate_mapping()
|
|
|
|
|
|
|
|
|
|
|
|
def autohost(config: dict):
|
2020-07-10 15:42:22 +00:00
|
|
|
def keep_running():
|
|
|
|
try:
|
|
|
|
with Locker("autohost"):
|
2022-06-07 22:35:35 +00:00
|
|
|
run_guardian()
|
2021-12-13 04:48:33 +00:00
|
|
|
while 1:
|
|
|
|
time.sleep(0.1)
|
|
|
|
with db_session:
|
|
|
|
rooms = select(
|
|
|
|
room for room in Room if
|
|
|
|
room.last_activity >= datetime.utcnow() - timedelta(days=3))
|
|
|
|
for room in rooms:
|
|
|
|
launch_room(room, config)
|
|
|
|
|
|
|
|
except AlreadyRunningException:
|
|
|
|
logging.info("Autohost reports as already running, not starting another.")
|
|
|
|
|
|
|
|
import threading
|
|
|
|
threading.Thread(target=keep_running, name="AP_Autohost").start()
|
|
|
|
|
|
|
|
|
|
|
|
def autogen(config: dict):
|
|
|
|
def keep_running():
|
|
|
|
try:
|
|
|
|
with Locker("autogen"):
|
2020-07-10 15:42:22 +00:00
|
|
|
|
2020-08-17 23:18:37 +00:00
|
|
|
with multiprocessing.Pool(config["GENERATORS"], initializer=init_db,
|
2023-04-30 05:13:24 +00:00
|
|
|
initargs=(config["PONY"],), maxtasksperchild=10) as generator_pool:
|
2020-08-17 23:18:37 +00:00
|
|
|
with db_session:
|
|
|
|
to_start = select(generation for generation in Generation if generation.state == STATE_STARTED)
|
|
|
|
|
|
|
|
if to_start:
|
|
|
|
logging.info("Resuming generation")
|
|
|
|
for generation in to_start:
|
|
|
|
sid = Seed.get(id=generation.id)
|
|
|
|
if sid:
|
|
|
|
generation.delete()
|
|
|
|
else:
|
|
|
|
launch_generator(generator_pool, generation)
|
|
|
|
|
|
|
|
commit()
|
|
|
|
select(generation for generation in Generation if generation.state == STATE_ERROR).delete()
|
|
|
|
|
|
|
|
while 1:
|
2021-12-13 04:48:33 +00:00
|
|
|
time.sleep(0.1)
|
2020-08-17 23:18:37 +00:00
|
|
|
with db_session:
|
2022-07-06 23:38:50 +00:00
|
|
|
# for update locks the database row(s) during transaction, preventing writes from elsewhere
|
2020-08-17 23:18:37 +00:00
|
|
|
to_start = select(
|
2022-07-06 23:38:50 +00:00
|
|
|
generation for generation in Generation
|
|
|
|
if generation.state == STATE_QUEUED).for_update()
|
2020-08-17 23:18:37 +00:00
|
|
|
for generation in to_start:
|
|
|
|
launch_generator(generator_pool, generation)
|
2020-07-10 15:42:22 +00:00
|
|
|
except AlreadyRunningException:
|
2021-12-13 04:48:33 +00:00
|
|
|
logging.info("Autogen reports as already running, not starting another.")
|
2020-07-10 15:42:22 +00:00
|
|
|
|
|
|
|
import threading
|
2021-12-13 04:48:33 +00:00
|
|
|
threading.Thread(target=keep_running, name="AP_Autogen").start()
|
2020-07-10 15:42:22 +00:00
|
|
|
|
|
|
|
|
2022-06-07 22:35:35 +00:00
|
|
|
multiworlds: typing.Dict[type(Room.id), MultiworldInstance] = {}
|
2020-07-10 15:42:22 +00:00
|
|
|
|
2021-05-14 13:25:57 +00:00
|
|
|
|
2020-07-10 15:42:22 +00:00
|
|
|
class MultiworldInstance():
|
|
|
|
def __init__(self, room: Room, config: dict):
|
|
|
|
self.room_id = room.id
|
|
|
|
self.process: typing.Optional[multiprocessing.Process] = None
|
2022-06-07 22:35:35 +00:00
|
|
|
with guardian_lock:
|
|
|
|
multiworlds[self.room_id] = self
|
2020-07-10 15:42:22 +00:00
|
|
|
self.ponyconfig = config["PONY"]
|
2023-01-21 16:29:27 +00:00
|
|
|
self.cert = config["SELFLAUNCHCERT"]
|
|
|
|
self.key = config["SELFLAUNCHKEY"]
|
2023-03-09 20:31:00 +00:00
|
|
|
self.host = config["HOST_ADDRESS"]
|
2020-07-10 15:42:22 +00:00
|
|
|
|
|
|
|
def start(self):
|
|
|
|
if self.process and self.process.is_alive():
|
|
|
|
return False
|
|
|
|
|
|
|
|
logging.info(f"Spinning up {self.room_id}")
|
2022-06-07 22:35:35 +00:00
|
|
|
process = multiprocessing.Process(group=None, target=run_server_process,
|
2023-01-21 16:29:27 +00:00
|
|
|
args=(self.room_id, self.ponyconfig, get_static_server_data(),
|
2023-03-09 20:31:00 +00:00
|
|
|
self.cert, self.key, self.host),
|
2022-06-07 22:35:35 +00:00
|
|
|
name="MultiHost")
|
|
|
|
process.start()
|
|
|
|
# bind after start to prevent thread sync issues with guardian.
|
|
|
|
self.process = process
|
2020-07-10 15:42:22 +00:00
|
|
|
|
|
|
|
def stop(self):
|
|
|
|
if self.process:
|
|
|
|
self.process.terminate()
|
|
|
|
self.process = None
|
|
|
|
|
2022-06-07 22:35:35 +00:00
|
|
|
def done(self):
|
|
|
|
return self.process and not self.process.is_alive()
|
|
|
|
|
|
|
|
def collect(self):
|
2021-05-14 13:25:57 +00:00
|
|
|
self.process.join() # wait for process to finish
|
2021-02-21 10:07:02 +00:00
|
|
|
self.process = None
|
2022-06-07 22:35:35 +00:00
|
|
|
|
|
|
|
|
|
|
|
guardian = None
|
|
|
|
guardian_lock = threading.Lock()
|
|
|
|
|
|
|
|
|
|
|
|
def run_guardian():
|
|
|
|
global guardian
|
|
|
|
global multiworlds
|
|
|
|
with guardian_lock:
|
|
|
|
if not guardian:
|
2022-06-09 20:14:12 +00:00
|
|
|
try:
|
|
|
|
import resource
|
|
|
|
except ModuleNotFoundError:
|
|
|
|
pass # unix only module
|
|
|
|
else:
|
|
|
|
# Each Server is another file handle, so request as many as we can from the system
|
|
|
|
file_limit = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
|
|
|
|
# set soft limit to hard limit
|
|
|
|
resource.setrlimit(resource.RLIMIT_NOFILE, (file_limit, file_limit))
|
|
|
|
|
2022-06-07 22:35:35 +00:00
|
|
|
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)
|
2021-02-21 10:07:02 +00:00
|
|
|
|
2020-07-10 15:42:22 +00:00
|
|
|
|
2020-08-17 23:18:37 +00:00
|
|
|
from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed
|
2022-08-07 16:28:50 +00:00
|
|
|
from .customserver import run_server_process, get_static_server_data
|
2020-08-17 23:18:37 +00:00
|
|
|
from .generate import gen_game
|