| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  | from __future__ import annotations | 
					
						
							| 
									
										
										
										
											2022-10-17 01:08:31 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-13 21:57:11 +02:00
										 |  |  | import json | 
					
						
							| 
									
										
										
										
											2022-10-17 01:08:31 +02:00
										 |  |  | import logging | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  | import multiprocessing | 
					
						
							| 
									
										
										
										
											2022-10-17 01:08:31 +02:00
										 |  |  | import typing | 
					
						
							|  |  |  | from datetime import timedelta, datetime | 
					
						
							| 
									
										
										
										
											2024-05-19 20:40:36 +02:00
										 |  |  | from threading import Event, Thread | 
					
						
							| 
									
										
										
										
											2024-12-10 02:44:41 +01:00
										 |  |  | from typing import Any | 
					
						
							| 
									
										
										
										
											2024-05-19 20:40:36 +02:00
										 |  |  | from uuid import UUID | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-06 13:50:24 +02:00
										 |  |  | from pony.orm import db_session, select, commit, PrimaryKey | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-13 21:57:11 +02:00
										 |  |  | from Utils import restricted_loads | 
					
						
							| 
									
										
										
										
											2023-09-20 16:05:56 +02:00
										 |  |  | from .locker import Locker, AlreadyRunningException | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-19 20:40:36 +02:00
										 |  |  | _stop_event = Event() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def stop(): | 
					
						
							|  |  |  |     """Stops previously launched threads""" | 
					
						
							|  |  |  |     global _stop_event | 
					
						
							|  |  |  |     stop_event = _stop_event | 
					
						
							|  |  |  |     _stop_event = Event()  # new event for new threads | 
					
						
							|  |  |  |     stop_event.set() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											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) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-06 13:50:24 +02:00
										 |  |  | def _mp_gen_game(gen_options: dict, meta: dict[str, Any] | None = None, owner=None, sid=None) -> PrimaryKey | None: | 
					
						
							|  |  |  |     from setproctitle import setproctitle | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     setproctitle(f"Generator ({sid})") | 
					
						
							|  |  |  |     res = gen_game(gen_options, meta=meta, owner=owner, sid=sid) | 
					
						
							|  |  |  |     setproctitle(f"Generator (idle)") | 
					
						
							|  |  |  |     return res | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-18 01:18:37 +02:00
										 |  |  | 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") | 
					
						
							| 
									
										
										
										
											2025-04-06 13:50:24 +02:00
										 |  |  |         pool.apply_async(_mp_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
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-10 02:44:41 +01:00
										 |  |  | def init_generator(config: dict[str, Any]) -> None: | 
					
						
							| 
									
										
										
										
											2025-04-06 13:50:24 +02:00
										 |  |  |     from setproctitle import setproctitle | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     setproctitle("Generator (idle)") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-10 02:44:41 +01:00
										 |  |  |     try: | 
					
						
							|  |  |  |         import resource | 
					
						
							|  |  |  |     except ModuleNotFoundError: | 
					
						
							|  |  |  |         pass  # unix only module | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         # set soft limit for memory to from config (default 4GiB) | 
					
						
							|  |  |  |         soft_limit = config["GENERATOR_MEMORY_LIMIT"] | 
					
						
							|  |  |  |         old_limit, hard_limit = resource.getrlimit(resource.RLIMIT_AS) | 
					
						
							|  |  |  |         if soft_limit != old_limit: | 
					
						
							|  |  |  |             resource.setrlimit(resource.RLIMIT_AS, (soft_limit, hard_limit)) | 
					
						
							|  |  |  |             logging.debug(f"Changed AS mem limit {old_limit} -> {soft_limit}") | 
					
						
							|  |  |  |         del resource, soft_limit, hard_limit | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     pony_config = config["PONY"] | 
					
						
							| 
									
										
										
										
											2020-08-18 01:18:37 +02:00
										 |  |  |     db.bind(**pony_config) | 
					
						
							|  |  |  |     db.generate_mapping() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  | def cleanup(): | 
					
						
							|  |  |  |     """delete unowned user-content""" | 
					
						
							|  |  |  |     with db_session: | 
					
						
							|  |  |  |         # >>> bool(uuid.UUID(int=0)) | 
					
						
							|  |  |  |         # True | 
					
						
							|  |  |  |         rooms = Room.select(lambda room: room.owner == UUID(int=0)).delete(bulk=True) | 
					
						
							|  |  |  |         seeds = Seed.select(lambda seed: seed.owner == UUID(int=0) and not seed.rooms).delete(bulk=True) | 
					
						
							|  |  |  |         slots = Slot.select(lambda slot: not slot.seed).delete(bulk=True) | 
					
						
							|  |  |  |         # Command gets deleted by ponyorm Cascade Delete, as Room is Required | 
					
						
							|  |  |  |     if rooms or seeds or slots: | 
					
						
							|  |  |  |         logging.info(f"{rooms} Rooms, {seeds} Seeds and {slots} Slots have been deleted.") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-18 01:18:37 +02:00
										 |  |  | def autohost(config: dict): | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  |     def keep_running(): | 
					
						
							| 
									
										
										
										
											2024-05-19 20:40:36 +02:00
										 |  |  |         stop_event = _stop_event | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  |         try: | 
					
						
							|  |  |  |             with Locker("autohost"): | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |                 cleanup() | 
					
						
							|  |  |  |                 hosters = [] | 
					
						
							|  |  |  |                 for x in range(config["HOSTERS"]): | 
					
						
							|  |  |  |                     hoster = MultiworldInstance(config, x) | 
					
						
							|  |  |  |                     hosters.append(hoster) | 
					
						
							|  |  |  |                     hoster.start() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-19 20:40:36 +02:00
										 |  |  |                 while not stop_event.wait(0.1): | 
					
						
							| 
									
										
										
										
											2021-12-13 05:48:33 +01:00
										 |  |  |                     with db_session: | 
					
						
							|  |  |  |                         rooms = select( | 
					
						
							|  |  |  |                             room for room in Room if | 
					
						
							|  |  |  |                             room.last_activity >= datetime.utcnow() - timedelta(days=3)) | 
					
						
							|  |  |  |                         for room in rooms: | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |                             # we have to filter twice, as the per-room timeout can't currently be PonyORM transpiled. | 
					
						
							| 
									
										
										
										
											2024-05-19 15:17:55 +02:00
										 |  |  |                             if room.last_activity >= datetime.utcnow() - timedelta(seconds=room.timeout + 5): | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |                                 hosters[room.id.int % len(hosters)].start_room(room.id) | 
					
						
							| 
									
										
										
										
											2021-12-13 05:48:33 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |         except AlreadyRunningException: | 
					
						
							|  |  |  |             logging.info("Autohost reports as already running, not starting another.") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-19 20:40:36 +02:00
										 |  |  |     Thread(target=keep_running, name="AP_Autohost").start() | 
					
						
							| 
									
										
										
										
											2021-12-13 05:48:33 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def autogen(config: dict): | 
					
						
							|  |  |  |     def keep_running(): | 
					
						
							| 
									
										
										
										
											2024-05-19 20:40:36 +02:00
										 |  |  |         stop_event = _stop_event | 
					
						
							| 
									
										
										
										
											2021-12-13 05:48:33 +01:00
										 |  |  |         try: | 
					
						
							|  |  |  |             with Locker("autogen"): | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-10 02:44:41 +01:00
										 |  |  |                 with multiprocessing.Pool(config["GENERATORS"], initializer=init_generator, | 
					
						
							|  |  |  |                                           initargs=(config,), maxtasksperchild=10) as generator_pool: | 
					
						
							| 
									
										
										
										
											2020-08-18 01:18:37 +02: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() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-19 20:40:36 +02:00
										 |  |  |                     while not stop_event.wait(0.1): | 
					
						
							| 
									
										
										
										
											2020-08-18 01:18:37 +02:00
										 |  |  |                         with db_session: | 
					
						
							| 
									
										
										
										
											2022-07-07 01:38:50 +02:00
										 |  |  |                             # for update locks the database row(s) during transaction, preventing writes from elsewhere | 
					
						
							| 
									
										
										
										
											2020-08-18 01:18:37 +02:00
										 |  |  |                             to_start = select( | 
					
						
							| 
									
										
										
										
											2022-07-07 01:38:50 +02:00
										 |  |  |                                 generation for generation in Generation | 
					
						
							|  |  |  |                                 if generation.state == STATE_QUEUED).for_update() | 
					
						
							| 
									
										
										
										
											2020-08-18 01:18:37 +02:00
										 |  |  |                             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
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-19 20:40:36 +02:00
										 |  |  |     Thread(target=keep_running, name="AP_Autogen").start() | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-08 00:35:35 +02:00
										 |  |  | multiworlds: typing.Dict[type(Room.id), MultiworldInstance] = {} | 
					
						
							| 
									
										
										
										
											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(): | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |     def __init__(self, config: dict, id: int): | 
					
						
							|  |  |  |         self.room_ids = set() | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  |         self.process: typing.Optional[multiprocessing.Process] = None | 
					
						
							|  |  |  |         self.ponyconfig = config["PONY"] | 
					
						
							| 
									
										
										
										
											2023-01-21 17:29:27 +01:00
										 |  |  |         self.cert = config["SELFLAUNCHCERT"] | 
					
						
							|  |  |  |         self.key = config["SELFLAUNCHKEY"] | 
					
						
							| 
									
										
										
										
											2023-03-09 21:31:00 +01:00
										 |  |  |         self.host = config["HOST_ADDRESS"] | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |         self.rooms_to_start = multiprocessing.Queue() | 
					
						
							|  |  |  |         self.rooms_shutting_down = multiprocessing.Queue() | 
					
						
							|  |  |  |         self.name = f"MultiHoster{id}" | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def start(self): | 
					
						
							|  |  |  |         if self.process and self.process.is_alive(): | 
					
						
							|  |  |  |             return False | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-08 00:35:35 +02:00
										 |  |  |         process = multiprocessing.Process(group=None, target=run_server_process, | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |                                           args=(self.name, self.ponyconfig, get_static_server_data(), | 
					
						
							|  |  |  |                                                 self.cert, self.key, self.host, | 
					
						
							|  |  |  |                                                 self.rooms_to_start, self.rooms_shutting_down), | 
					
						
							|  |  |  |                                           name=self.name) | 
					
						
							| 
									
										
										
										
											2022-06-08 00:35:35 +02:00
										 |  |  |         process.start() | 
					
						
							|  |  |  |         self.process = process | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |     def start_room(self, room_id): | 
					
						
							|  |  |  |         while not self.rooms_shutting_down.empty(): | 
					
						
							|  |  |  |             self.room_ids.remove(self.rooms_shutting_down.get(block=True, timeout=None)) | 
					
						
							|  |  |  |         if room_id in self.room_ids: | 
					
						
							|  |  |  |             pass  # should already be hosted currently. | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             self.room_ids.add(room_id) | 
					
						
							|  |  |  |             self.rooms_to_start.put(room_id) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  |     def stop(self): | 
					
						
							|  |  |  |         if self.process: | 
					
						
							|  |  |  |             self.process.terminate() | 
					
						
							|  |  |  |             self.process = None | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-08 00:35:35 +02:00
										 |  |  |     def done(self): | 
					
						
							|  |  |  |         return self.process and not self.process.is_alive() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     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 | 
					
						
							| 
									
										
										
										
											2022-06-08 00:35:35 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-02 16:45:07 +02:00
										 |  |  | from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed, Slot | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  | from .customserver import run_server_process, get_static_server_data | 
					
						
							| 
									
										
										
										
											2020-08-18 01:18:37 +02:00
										 |  |  | from .generate import gen_game |