| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  | from __future__ import annotations | 
					
						
							|  |  |  | import logging | 
					
						
							| 
									
										
										
										
											2021-05-13 21:57:11 +02:00
										 |  |  | import json | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  | import multiprocessing | 
					
						
							|  |  |  | from datetime import timedelta, datetime | 
					
						
							| 
									
										
										
										
											2021-02-21 11:07:02 +01:00
										 |  |  | import concurrent.futures | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  | import sys | 
					
						
							|  |  |  | import typing | 
					
						
							| 
									
										
										
										
											2020-08-18 01:18:37 +02:00
										 |  |  | import time | 
					
						
							| 
									
										
										
										
											2021-07-09 22:47:35 +02:00
										 |  |  | import os | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-18 01:18:37 +02:00
										 |  |  | from pony.orm import db_session, select, commit | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-13 21:57:11 +02:00
										 |  |  | from Utils import restricted_loads | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | class CommonLocker(): | 
					
						
							|  |  |  |     """Uses a file lock to signal that something is already running""" | 
					
						
							| 
									
										
										
										
											2021-06-29 03:11:48 +02:00
										 |  |  |     lock_folder = "file_locks" | 
					
						
							|  |  |  |     def __init__(self, lockname: str, folder=None): | 
					
						
							|  |  |  |         if folder: | 
					
						
							|  |  |  |             self.lock_folder = folder | 
					
						
							|  |  |  |         os.makedirs(self.lock_folder, exist_ok=True) | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  |         self.lockname = lockname | 
					
						
							| 
									
										
										
										
											2021-06-29 03:11:48 +02:00
										 |  |  |         self.lockfile = os.path.join(self.lock_folder, f"{self.lockname}.lck") | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02: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 15:25:57 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  |     class Locker(CommonLocker): | 
					
						
							|  |  |  |         def __enter__(self): | 
					
						
							|  |  |  |             try: | 
					
						
							| 
									
										
										
										
											2020-07-11 16:25:06 +02: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 17:42:22 +02: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-11 00:52:49 +02:00
										 |  |  |     if room.last_activity >= datetime.utcnow() - timedelta(seconds=room.timeout): | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  |         multiworld = multiworlds.get(room.id, None) | 
					
						
							|  |  |  |         if not multiworld: | 
					
						
							|  |  |  |             multiworld = MultiworldInstance(room, config) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         multiworld.start() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-18 01:18:37 +02: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 21:57:11 +02: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-11 00:46:18 +02:00
										 |  |  |                          {"meta": meta, | 
					
						
							| 
									
										
										
										
											2021-05-13 21:57:11 +02:00
										 |  |  |                           "sid": generation.id, | 
					
						
							|  |  |  |                           "owner": generation.owner}, | 
					
						
							|  |  |  |                          handle_generation_success, handle_generation_failure) | 
					
						
							| 
									
										
										
										
											2021-08-30 16:31:56 +02:00
										 |  |  |     except Exception as e: | 
					
						
							| 
									
										
										
										
											2021-05-13 21:57:11 +02:00
										 |  |  |         generation.state = STATE_ERROR | 
					
						
							|  |  |  |         commit() | 
					
						
							| 
									
										
										
										
											2021-08-30 16:31:56 +02:00
										 |  |  |         logging.exception(e) | 
					
						
							| 
									
										
										
										
											2021-05-13 21:57:11 +02:00
										 |  |  |     else: | 
					
						
							|  |  |  |         generation.state = STATE_STARTED | 
					
						
							| 
									
										
										
										
											2020-08-18 01:18:37 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def init_db(pony_config: dict): | 
					
						
							|  |  |  |     db.bind(**pony_config) | 
					
						
							|  |  |  |     db.generate_mapping() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def autohost(config: dict): | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  |     def keep_running(): | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             with Locker("autohost"): | 
					
						
							| 
									
										
										
										
											2021-12-13 05:48:33 +01: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 17:42:22 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-18 01:18:37 +02:00
										 |  |  |                 with multiprocessing.Pool(config["GENERATORS"], initializer=init_db, | 
					
						
							|  |  |  |                                           initargs=(config["PONY"],)) as generator_pool: | 
					
						
							|  |  |  |                     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 05:48:33 +01:00
										 |  |  |                         time.sleep(0.1) | 
					
						
							| 
									
										
										
										
											2020-08-18 01:18:37 +02:00
										 |  |  |                         with db_session: | 
					
						
							|  |  |  |                             to_start = select( | 
					
						
							|  |  |  |                                 generation for generation in Generation if generation.state == STATE_QUEUED) | 
					
						
							|  |  |  |                             for generation in to_start: | 
					
						
							|  |  |  |                                 launch_generator(generator_pool, generation) | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  |         except AlreadyRunningException: | 
					
						
							| 
									
										
										
										
											2021-12-13 05:48:33 +01:00
										 |  |  |             logging.info("Autogen reports as already running, not starting another.") | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     import threading | 
					
						
							| 
									
										
										
										
											2021-12-13 05:48:33 +01:00
										 |  |  |     threading.Thread(target=keep_running, name="AP_Autogen").start() | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | multiworlds = {} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-21 11:07:02 +01:00
										 |  |  | guardians = concurrent.futures.ThreadPoolExecutor(2, thread_name_prefix="Guardian") | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-14 15:25:57 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  | class MultiworldInstance(): | 
					
						
							|  |  |  |     def __init__(self, room: Room, config: dict): | 
					
						
							|  |  |  |         self.room_id = room.id | 
					
						
							|  |  |  |         self.process: typing.Optional[multiprocessing.Process] = None | 
					
						
							|  |  |  |         multiworlds[self.room_id] = self | 
					
						
							|  |  |  |         self.ponyconfig = config["PONY"] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def start(self): | 
					
						
							|  |  |  |         if self.process and self.process.is_alive(): | 
					
						
							|  |  |  |             return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         logging.info(f"Spinning up {self.room_id}") | 
					
						
							|  |  |  |         self.process = multiprocessing.Process(group=None, target=run_server_process, | 
					
						
							|  |  |  |                                                args=(self.room_id, self.ponyconfig), | 
					
						
							|  |  |  |                                                name="MultiHost") | 
					
						
							|  |  |  |         self.process.start() | 
					
						
							| 
									
										
										
										
											2021-02-21 11:07:02 +01:00
										 |  |  |         self.guardian = guardians.submit(self._collect) | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def stop(self): | 
					
						
							|  |  |  |         if self.process: | 
					
						
							|  |  |  |             self.process.terminate() | 
					
						
							|  |  |  |             self.process = None | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-21 11:07:02 +01:00
										 |  |  |     def _collect(self): | 
					
						
							| 
									
										
										
										
											2021-05-14 15:25:57 +02:00
										 |  |  |         self.process.join()  # wait for process to finish | 
					
						
							| 
									
										
										
										
											2021-02-21 11:07:02 +01:00
										 |  |  |         self.process = None | 
					
						
							|  |  |  |         self.guardian = None | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-18 01:18:37 +02:00
										 |  |  | from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  | from .customserver import run_server_process | 
					
						
							| 
									
										
										
										
											2020-08-18 01:18:37 +02:00
										 |  |  | from .generate import gen_game |