| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  | from __future__ import annotations | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-20 20:03:06 +02:00
										 |  |  | import asyncio | 
					
						
							| 
									
										
										
										
											2022-09-18 11:49:31 +02:00
										 |  |  | import collections | 
					
						
							|  |  |  | import datetime | 
					
						
							|  |  |  | import functools | 
					
						
							|  |  |  | import logging | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  | import multiprocessing | 
					
						
							| 
									
										
										
										
											2022-09-18 11:49:31 +02:00
										 |  |  | import pickle | 
					
						
							|  |  |  | import random | 
					
						
							| 
									
										
										
										
											2020-06-20 20:03:06 +02:00
										 |  |  | import socket | 
					
						
							|  |  |  | import threading | 
					
						
							|  |  |  | import time | 
					
						
							| 
									
										
										
										
											2023-01-21 17:29:27 +01:00
										 |  |  | import typing | 
					
						
							| 
									
										
										
										
											2023-09-27 11:26:08 +02:00
										 |  |  | import sys | 
					
						
							| 
									
										
										
										
											2022-10-17 01:08:31 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-18 11:49:31 +02:00
										 |  |  | import websockets | 
					
						
							| 
									
										
										
										
											2023-01-02 19:26:34 -06:00
										 |  |  | from pony.orm import commit, db_session, select | 
					
						
							| 
									
										
										
										
											2020-06-20 20:03:06 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-10 15:35:43 +01:00
										 |  |  | import Utils | 
					
						
							| 
									
										
										
										
											2023-01-21 17:29:27 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert | 
					
						
							| 
									
										
										
										
											2023-06-18 15:19:48 +02:00
										 |  |  | from Utils import restricted_loads, cache_argsless | 
					
						
							| 
									
										
										
										
											2024-05-19 16:31:35 +02:00
										 |  |  | from .locker import Locker | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  | from .models import Command, GameDataPackage, Room, db | 
					
						
							| 
									
										
										
										
											2020-06-20 20:03:06 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-27 13:52:03 +02:00
										 |  |  | class CustomClientMessageProcessor(ClientMessageProcessor): | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  |     ctx: WebHostContext | 
					
						
							| 
									
										
										
										
											2021-12-03 07:01:21 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-18 12:29:35 -06:00
										 |  |  |     def _cmd_video(self, platform: str, user: str): | 
					
						
							|  |  |  |         """Set a link for your name in the WebHostLib tracker pointing to a video stream.
 | 
					
						
							|  |  |  |         Currently, only YouTube and Twitch platforms are supported. | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2020-06-27 13:52:03 +02:00
										 |  |  |         if platform.lower().startswith("t"):  # twitch | 
					
						
							|  |  |  |             self.ctx.video[self.client.team, self.client.slot] = "Twitch", user | 
					
						
							|  |  |  |             self.ctx.save() | 
					
						
							|  |  |  |             self.output(f"Registered Twitch Stream https://www.twitch.tv/{user}") | 
					
						
							| 
									
										
										
										
											2020-06-27 14:16:51 +02:00
										 |  |  |             return True | 
					
						
							| 
									
										
										
										
											2021-04-10 18:45:11 +02:00
										 |  |  |         elif platform.lower().startswith("y"):  # youtube | 
					
						
							| 
									
										
										
										
											2020-07-25 22:40:24 +02:00
										 |  |  |             self.ctx.video[self.client.team, self.client.slot] = "Youtube", user | 
					
						
							|  |  |  |             self.ctx.save() | 
					
						
							|  |  |  |             self.output(f"Registered Youtube Stream for {user}") | 
					
						
							|  |  |  |             return True | 
					
						
							| 
									
										
										
										
											2020-06-27 14:16:51 +02:00
										 |  |  |         return False | 
					
						
							| 
									
										
										
										
											2020-06-27 13:52:03 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # inject | 
					
						
							|  |  |  | import MultiServer | 
					
						
							| 
									
										
										
										
											2021-12-03 07:01:21 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-27 13:52:03 +02:00
										 |  |  | MultiServer.client_message_processor = CustomClientMessageProcessor | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  | del MultiServer | 
					
						
							| 
									
										
										
										
											2020-06-27 13:52:03 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-20 20:03:06 +02:00
										 |  |  | class DBCommandProcessor(ServerCommandProcessor): | 
					
						
							|  |  |  |     def output(self, text: str): | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |         self.ctx.logger.info(text) | 
					
						
							| 
									
										
										
										
											2020-06-20 20:03:06 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class WebHostContext(Context): | 
					
						
							| 
									
										
										
										
											2022-09-18 11:49:31 +02:00
										 |  |  |     room_id: int | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |     def __init__(self, static_server_data: dict, logger: logging.Logger): | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |         # static server data is used during _load_game_data to load required data, | 
					
						
							|  |  |  |         # without needing to import worlds system, which takes quite a bit of memory | 
					
						
							|  |  |  |         self.static_server_data = static_server_data | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |         super(WebHostContext, self).__init__("", 0, "", "", 1, | 
					
						
							|  |  |  |                                              40, True, "enabled", "enabled", | 
					
						
							|  |  |  |                                              "enabled", 0, 2, logger=logger) | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |         del self.static_server_data | 
					
						
							| 
									
										
										
										
											2020-06-22 01:04:12 +02:00
										 |  |  |         self.main_loop = asyncio.get_running_loop() | 
					
						
							| 
									
										
										
										
											2020-06-27 13:52:03 +02:00
										 |  |  |         self.video = {} | 
					
						
							| 
									
										
										
										
											2020-10-18 23:07:48 +02:00
										 |  |  |         self.tags = ["AP", "WebHost"] | 
					
						
							| 
									
										
										
										
											2020-06-16 11:26:54 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-01 20:34:50 +02:00
										 |  |  |     def __del__(self): | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             import psutil | 
					
						
							|  |  |  |             from Utils import format_SI_prefix | 
					
						
							|  |  |  |             self.logger.debug(f"Context destroyed, Mem: {format_SI_prefix(psutil.Process().memory_info().rss, 1024)}iB") | 
					
						
							|  |  |  |         except ImportError: | 
					
						
							|  |  |  |             self.logger.debug("Context destroyed") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |     def _load_game_data(self): | 
					
						
							|  |  |  |         for key, value in self.static_server_data.items(): | 
					
						
							| 
									
										
										
										
											2024-05-19 15:32:11 +02:00
										 |  |  |             # NOTE: attributes are mutable and shared, so they will have to be copied before being modified | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |             setattr(self, key, value) | 
					
						
							| 
									
										
										
										
											2022-09-18 11:49:31 +02:00
										 |  |  |         self.non_hintable_names = collections.defaultdict(frozenset, self.non_hintable_names) | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-20 20:03:06 +02:00
										 |  |  |     def listen_to_db_commands(self): | 
					
						
							|  |  |  |         cmdprocessor = DBCommandProcessor(self) | 
					
						
							| 
									
										
										
										
											2020-06-16 11:26:54 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-28 04:06:30 +01:00
										 |  |  |         while not self.exit_event.is_set(): | 
					
						
							| 
									
										
										
										
											2020-06-20 20:03:06 +02:00
										 |  |  |             with db_session: | 
					
						
							|  |  |  |                 commands = select(command for command in Command if command.room.id == self.room_id) | 
					
						
							|  |  |  |                 if commands: | 
					
						
							|  |  |  |                     for command in commands: | 
					
						
							| 
									
										
										
										
											2020-06-22 01:04:12 +02:00
										 |  |  |                         self.main_loop.call_soon_threadsafe(cmdprocessor, command.commandtext) | 
					
						
							| 
									
										
										
										
											2020-06-20 20:03:06 +02:00
										 |  |  |                         command.delete() | 
					
						
							|  |  |  |                     commit() | 
					
						
							|  |  |  |             time.sleep(5) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @db_session | 
					
						
							|  |  |  |     def load(self, room_id: int): | 
					
						
							|  |  |  |         self.room_id = room_id | 
					
						
							| 
									
										
										
										
											2020-06-21 15:32:31 +02:00
										 |  |  |         room = Room.get(id=room_id) | 
					
						
							|  |  |  |         if room.last_port: | 
					
						
							|  |  |  |             self.port = room.last_port | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             self.port = get_random_port() | 
					
						
							| 
									
										
										
										
											2020-10-26 00:04:58 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |         multidata = self.decompress(room.seed.multidata) | 
					
						
							|  |  |  |         game_data_packages = {} | 
					
						
							| 
									
										
										
										
											2024-05-19 15:32:11 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         static_gamespackage = self.gamespackage  # this is shared across all rooms | 
					
						
							|  |  |  |         static_item_name_groups = self.item_name_groups | 
					
						
							|  |  |  |         static_location_name_groups = self.location_name_groups | 
					
						
							| 
									
										
										
										
											2024-05-30 18:16:13 +02:00
										 |  |  |         self.gamespackage = {"Archipelago": static_gamespackage.get("Archipelago", {})}  # this may be modified by _load | 
					
						
							|  |  |  |         self.item_name_groups = {"Archipelago": static_item_name_groups.get("Archipelago", {})} | 
					
						
							|  |  |  |         self.location_name_groups = {"Archipelago": static_location_name_groups.get("Archipelago", {})} | 
					
						
							| 
									
										
										
										
											2024-05-19 15:32:11 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-01 22:40:14 +02:00
										 |  |  |         for game in list(multidata.get("datapackage", {})): | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |             game_data = multidata["datapackage"][game] | 
					
						
							|  |  |  |             if "checksum" in game_data: | 
					
						
							| 
									
										
										
										
											2024-05-19 15:32:11 +02:00
										 |  |  |                 if static_gamespackage.get(game, {}).get("checksum") == game_data["checksum"]: | 
					
						
							|  |  |  |                     # non-custom. remove from multidata and use static data | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |                     # games package could be dropped from static data once all rooms embed data package | 
					
						
							|  |  |  |                     del multidata["datapackage"][game] | 
					
						
							|  |  |  |                 else: | 
					
						
							| 
									
										
										
										
											2023-04-01 22:40:14 +02:00
										 |  |  |                     row = GameDataPackage.get(checksum=game_data["checksum"]) | 
					
						
							|  |  |  |                     if row:  # None if rolled on >= 0.3.9 but uploaded to <= 0.3.8. multidata should be complete | 
					
						
							|  |  |  |                         game_data_packages[game] = Utils.restricted_loads(row.data) | 
					
						
							| 
									
										
										
										
											2024-05-19 15:32:11 +02:00
										 |  |  |                         continue | 
					
						
							|  |  |  |                     else: | 
					
						
							|  |  |  |                         self.logger.warning(f"Did not find game_data_package for {game}: {game_data['checksum']}") | 
					
						
							|  |  |  |             self.gamespackage[game] = static_gamespackage.get(game, {}) | 
					
						
							|  |  |  |             self.item_name_groups[game] = static_item_name_groups.get(game, {}) | 
					
						
							|  |  |  |             self.location_name_groups[game] = static_location_name_groups.get(game, {}) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if not game_data_packages: | 
					
						
							|  |  |  |             # all static -> use the static dicts directly | 
					
						
							|  |  |  |             self.gamespackage = static_gamespackage | 
					
						
							|  |  |  |             self.item_name_groups = static_item_name_groups | 
					
						
							|  |  |  |             self.location_name_groups = static_location_name_groups | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |         return self._load(multidata, game_data_packages, True) | 
					
						
							| 
									
										
										
										
											2020-06-20 20:03:06 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     @db_session | 
					
						
							|  |  |  |     def init_save(self, enabled: bool = True): | 
					
						
							|  |  |  |         self.saving = enabled | 
					
						
							|  |  |  |         if self.saving: | 
					
						
							| 
									
										
										
										
											2021-04-10 15:26:30 +02:00
										 |  |  |             savegame_data = Room.get(id=self.room_id).multisave | 
					
						
							|  |  |  |             if savegame_data: | 
					
						
							|  |  |  |                 self.set_save(restricted_loads(Room.get(id=self.room_id).multisave)) | 
					
						
							| 
									
										
										
										
											2024-05-19 18:25:56 +02:00
										 |  |  |             self._start_async_saving(atexit_save=False) | 
					
						
							| 
									
										
										
										
											2020-06-20 20:03:06 +02:00
										 |  |  |         threading.Thread(target=self.listen_to_db_commands, daemon=True).start() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @db_session | 
					
						
							| 
									
										
										
										
											2021-12-03 07:01:21 +01:00
										 |  |  |     def _save(self, exit_save: bool = False) -> bool: | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  |         room = Room.get(id=self.room_id) | 
					
						
							| 
									
										
										
										
											2021-04-04 03:18:19 +02:00
										 |  |  |         room.multisave = pickle.dumps(self.get_save()) | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  |         # saving only occurs on activity, so we can "abuse" this information to mark this as last_activity | 
					
						
							| 
									
										
										
										
											2021-12-03 07:01:21 +01:00
										 |  |  |         if not exit_save:  # we don't want to count a shutdown as activity, which would restart the server again | 
					
						
							| 
									
										
										
										
											2022-08-23 22:18:24 +02:00
										 |  |  |             room.last_activity = datetime.datetime.utcnow() | 
					
						
							| 
									
										
										
										
											2020-06-20 20:03:06 +02:00
										 |  |  |         return True | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-27 13:52:03 +02:00
										 |  |  |     def get_save(self) -> dict: | 
					
						
							|  |  |  |         d = super(WebHostContext, self).get_save() | 
					
						
							|  |  |  |         d["video"] = [(tuple(playerslot), videodata) for playerslot, videodata in self.video.items()] | 
					
						
							|  |  |  |         return d | 
					
						
							| 
									
										
										
										
											2020-06-20 20:03:06 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-03 07:01:21 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-21 15:32:31 +02:00
										 |  |  | def get_random_port(): | 
					
						
							|  |  |  |     return random.randint(49152, 65535) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-29 03:11:48 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  | @cache_argsless | 
					
						
							|  |  |  | def get_static_server_data() -> dict: | 
					
						
							|  |  |  |     import worlds | 
					
						
							|  |  |  |     data = { | 
					
						
							| 
									
										
										
										
											2024-06-06 01:54:46 +02:00
										 |  |  |         "non_hintable_names": { | 
					
						
							|  |  |  |             world_name: world.hint_blacklist | 
					
						
							|  |  |  |             for world_name, world in worlds.AutoWorldRegister.world_types.items() | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         "gamespackage": { | 
					
						
							|  |  |  |             world_name: { | 
					
						
							|  |  |  |                 key: value | 
					
						
							|  |  |  |                 for key, value in game_package.items() | 
					
						
							|  |  |  |                 if key not in ("item_name_groups", "location_name_groups") | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             for world_name, game_package in worlds.network_data_package["games"].items() | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         "item_name_groups": { | 
					
						
							|  |  |  |             world_name: world.item_name_groups | 
					
						
							|  |  |  |             for world_name, world in worlds.AutoWorldRegister.world_types.items() | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         "location_name_groups": { | 
					
						
							|  |  |  |             world_name: world.location_name_groups | 
					
						
							|  |  |  |             for world_name, world in worlds.AutoWorldRegister.world_types.items() | 
					
						
							|  |  |  |         }, | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return data | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  | def set_up_logging(room_id) -> logging.Logger: | 
					
						
							|  |  |  |     import os | 
					
						
							|  |  |  |     # logger setup | 
					
						
							|  |  |  |     logger = logging.getLogger(f"RoomLogger {room_id}") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # this *should* be empty, but just in case. | 
					
						
							|  |  |  |     for handler in logger.handlers[:]: | 
					
						
							|  |  |  |         logger.removeHandler(handler) | 
					
						
							|  |  |  |         handler.close() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     file_handler = logging.FileHandler( | 
					
						
							|  |  |  |         os.path.join(Utils.user_path("logs"), f"{room_id}.txt"), | 
					
						
							|  |  |  |         "a", | 
					
						
							|  |  |  |         encoding="utf-8-sig") | 
					
						
							|  |  |  |     file_handler.setFormatter(logging.Formatter("[%(asctime)s]: %(message)s")) | 
					
						
							|  |  |  |     logger.setLevel(logging.INFO) | 
					
						
							|  |  |  |     logger.addHandler(file_handler) | 
					
						
							|  |  |  |     return logger | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def run_server_process(name: str, ponyconfig: dict, static_server_data: dict, | 
					
						
							| 
									
										
										
										
											2023-03-09 21:31:00 +01:00
										 |  |  |                        cert_file: typing.Optional[str], cert_key_file: typing.Optional[str], | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |                        host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue): | 
					
						
							|  |  |  |     Utils.init_logging(name) | 
					
						
							|  |  |  |     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)) | 
					
						
							|  |  |  |         del resource, file_limit | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-20 20:03:06 +02:00
										 |  |  |     # establish DB connection for multidata and multisave | 
					
						
							|  |  |  |     db.bind(**ponyconfig) | 
					
						
							|  |  |  |     db.generate_mapping(check_tables=False) | 
					
						
							| 
									
										
										
										
											2020-06-16 11:26:54 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |     if "worlds" in sys.modules: | 
					
						
							|  |  |  |         raise Exception("Worlds system should not be loaded in the custom server.") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     import gc | 
					
						
							|  |  |  |     ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None | 
					
						
							|  |  |  |     del cert_file, cert_key_file, ponyconfig | 
					
						
							|  |  |  |     gc.collect()  # free intermediate objects used during setup | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     loop = asyncio.get_event_loop() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async def start_room(room_id): | 
					
						
							| 
									
										
										
										
											2024-05-19 16:31:35 +02:00
										 |  |  |         with Locker(f"RoomLocker {room_id}"): | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |             try: | 
					
						
							| 
									
										
										
										
											2024-05-19 16:31:35 +02:00
										 |  |  |                 logger = set_up_logging(room_id) | 
					
						
							|  |  |  |                 ctx = WebHostContext(static_server_data, logger) | 
					
						
							|  |  |  |                 ctx.load(room_id) | 
					
						
							|  |  |  |                 ctx.init_save() | 
					
						
							| 
									
										
										
										
											2024-09-01 20:34:50 +02:00
										 |  |  |                 assert ctx.server is None | 
					
						
							| 
									
										
										
										
											2024-05-19 16:31:35 +02:00
										 |  |  |                 try: | 
					
						
							|  |  |  |                     ctx.server = websockets.serve( | 
					
						
							|  |  |  |                         functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                     await ctx.server | 
					
						
							|  |  |  |                 except OSError:  # likely port in use | 
					
						
							|  |  |  |                     ctx.server = websockets.serve( | 
					
						
							|  |  |  |                         functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                     await ctx.server | 
					
						
							|  |  |  |                 port = 0 | 
					
						
							|  |  |  |                 for wssocket in ctx.server.ws_server.sockets: | 
					
						
							|  |  |  |                     socketname = wssocket.getsockname() | 
					
						
							|  |  |  |                     if wssocket.family == socket.AF_INET6: | 
					
						
							|  |  |  |                         # Prefer IPv4, as most users seem to not have working ipv6 support | 
					
						
							|  |  |  |                         if not port: | 
					
						
							|  |  |  |                             port = socketname[1] | 
					
						
							|  |  |  |                     elif wssocket.family == socket.AF_INET: | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |                         port = socketname[1] | 
					
						
							| 
									
										
										
										
											2024-05-19 16:31:35 +02:00
										 |  |  |                 if port: | 
					
						
							|  |  |  |                     ctx.logger.info(f'Hosting game at {host}:{port}') | 
					
						
							|  |  |  |                     with db_session: | 
					
						
							|  |  |  |                         room = Room.get(id=ctx.room_id) | 
					
						
							|  |  |  |                         room.last_port = port | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     ctx.logger.exception("Could not determine port. Likely hosting failure.") | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |                 with db_session: | 
					
						
							| 
									
										
										
										
											2024-05-19 16:31:35 +02:00
										 |  |  |                     ctx.auto_shutdown = Room.get(id=room_id).timeout | 
					
						
							| 
									
										
										
										
											2024-06-06 01:54:46 +02:00
										 |  |  |                 if ctx.saving: | 
					
						
							|  |  |  |                     setattr(asyncio.current_task(), "save", lambda: ctx._save(True)) | 
					
						
							| 
									
										
										
										
											2024-09-01 20:34:50 +02:00
										 |  |  |                 assert ctx.shutdown_task is None | 
					
						
							| 
									
										
										
										
											2024-05-19 16:31:35 +02:00
										 |  |  |                 ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, [])) | 
					
						
							|  |  |  |                 await ctx.shutdown_task | 
					
						
							| 
									
										
										
										
											2023-12-06 18:24:13 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-19 16:31:35 +02:00
										 |  |  |             except (KeyboardInterrupt, SystemExit): | 
					
						
							| 
									
										
										
										
											2024-05-21 14:08:59 +02:00
										 |  |  |                 if ctx.saving: | 
					
						
							|  |  |  |                     ctx._save() | 
					
						
							| 
									
										
										
										
											2024-06-06 01:54:46 +02:00
										 |  |  |                     setattr(asyncio.current_task(), "save", None) | 
					
						
							| 
									
										
										
										
											2024-05-21 14:08:59 +02:00
										 |  |  |             except Exception as e: | 
					
						
							| 
									
										
										
										
											2024-05-19 16:31:35 +02:00
										 |  |  |                 with db_session: | 
					
						
							| 
									
										
										
										
											2024-05-19 15:17:55 +02:00
										 |  |  |                     room = Room.get(id=room_id) | 
					
						
							| 
									
										
										
										
											2024-05-19 16:31:35 +02:00
										 |  |  |                     room.last_port = -1 | 
					
						
							| 
									
										
										
										
											2024-05-21 14:08:59 +02:00
										 |  |  |                 logger.exception(e) | 
					
						
							| 
									
										
										
										
											2024-05-19 16:31:35 +02:00
										 |  |  |                 raise | 
					
						
							| 
									
										
										
										
											2024-05-21 14:08:59 +02:00
										 |  |  |             else: | 
					
						
							|  |  |  |                 if ctx.saving: | 
					
						
							|  |  |  |                     ctx._save() | 
					
						
							| 
									
										
										
										
											2024-06-06 01:54:46 +02:00
										 |  |  |                     setattr(asyncio.current_task(), "save", None) | 
					
						
							| 
									
										
										
										
											2024-05-19 15:17:55 +02:00
										 |  |  |             finally: | 
					
						
							| 
									
										
										
										
											2024-05-19 16:31:35 +02:00
										 |  |  |                 try: | 
					
						
							| 
									
										
										
										
											2024-06-06 01:54:46 +02:00
										 |  |  |                     ctx.save_dirty = False  # make sure the saving thread does not write to DB after final wakeup | 
					
						
							|  |  |  |                     ctx.exit_event.set()  # make sure the saving thread stops at some point | 
					
						
							|  |  |  |                     # NOTE: async saving should probably be an async task and could be merged with shutdown_task | 
					
						
							| 
									
										
										
										
											2024-05-19 16:31:35 +02:00
										 |  |  |                     with (db_session): | 
					
						
							|  |  |  |                         # ensure the Room does not spin up again on its own, minute of safety buffer | 
					
						
							|  |  |  |                         room = Room.get(id=room_id) | 
					
						
							|  |  |  |                         room.last_activity = datetime.datetime.utcnow() - \ | 
					
						
							|  |  |  |                                              datetime.timedelta(minutes=1, seconds=room.timeout) | 
					
						
							|  |  |  |                     logging.info(f"Shutting down room {room_id} on {name}.") | 
					
						
							|  |  |  |                 finally: | 
					
						
							|  |  |  |                     await asyncio.sleep(5) | 
					
						
							|  |  |  |                     rooms_shutting_down.put(room_id) | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     class Starter(threading.Thread): | 
					
						
							| 
									
										
										
										
											2024-06-06 01:54:46 +02:00
										 |  |  |         _tasks: typing.List[asyncio.Future] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         def __init__(self): | 
					
						
							|  |  |  |             super().__init__() | 
					
						
							|  |  |  |             self._tasks = [] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         def _done(self, task: asyncio.Future): | 
					
						
							|  |  |  |             self._tasks.remove(task) | 
					
						
							|  |  |  |             task.result() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |         def run(self): | 
					
						
							|  |  |  |             while 1: | 
					
						
							|  |  |  |                 next_room = rooms_to_run.get(block=True,  timeout=None) | 
					
						
							| 
									
										
										
										
											2024-09-01 20:34:50 +02:00
										 |  |  |                 gc.collect() | 
					
						
							| 
									
										
										
										
											2024-06-06 01:54:46 +02:00
										 |  |  |                 task = asyncio.run_coroutine_threadsafe(start_room(next_room), loop) | 
					
						
							|  |  |  |                 self._tasks.append(task) | 
					
						
							|  |  |  |                 task.add_done_callback(self._done) | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |                 logging.info(f"Starting room {next_room} on {name}.") | 
					
						
							| 
									
										
										
										
											2024-07-14 13:56:56 +02:00
										 |  |  |                 del task  # delete reference to task object | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     starter = Starter() | 
					
						
							|  |  |  |     starter.daemon = True | 
					
						
							|  |  |  |     starter.start() | 
					
						
							| 
									
										
										
										
											2024-06-06 01:54:46 +02:00
										 |  |  |     try: | 
					
						
							|  |  |  |         loop.run_forever() | 
					
						
							|  |  |  |     finally: | 
					
						
							|  |  |  |         # save all tasks that want to be saved during shutdown | 
					
						
							|  |  |  |         for task in asyncio.all_tasks(loop): | 
					
						
							|  |  |  |             save: typing.Optional[typing.Callable[[], typing.Any]] = getattr(task, "save", None) | 
					
						
							|  |  |  |             if save: | 
					
						
							|  |  |  |                 save() |