| 
									
										
										
										
											2024-06-06 01:54:46 +02:00
										 |  |  | import re | 
					
						
							|  |  |  | from pathlib import Path | 
					
						
							|  |  |  | from typing import TYPE_CHECKING, Optional, cast | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if TYPE_CHECKING: | 
					
						
							|  |  |  |     from flask import Flask | 
					
						
							|  |  |  |     from werkzeug.test import Client as FlaskClient | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | __all__ = [ | 
					
						
							|  |  |  |     "get_app", | 
					
						
							|  |  |  |     "upload_multidata", | 
					
						
							|  |  |  |     "create_room", | 
					
						
							|  |  |  |     "start_room", | 
					
						
							|  |  |  |     "stop_room", | 
					
						
							|  |  |  |     "set_room_timeout", | 
					
						
							|  |  |  |     "get_multidata_for_room", | 
					
						
							|  |  |  |     "set_multidata_for_room", | 
					
						
							|  |  |  |     "stop_autohost", | 
					
						
							|  |  |  | ] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_app(tempdir: str) -> "Flask": | 
					
						
							|  |  |  |     from WebHostLib import app as raw_app | 
					
						
							|  |  |  |     from WebHost import get_app | 
					
						
							|  |  |  |     raw_app.config["PONY"] = { | 
					
						
							|  |  |  |         "provider": "sqlite", | 
					
						
							|  |  |  |         "filename": str(Path(tempdir) / "host.db"), | 
					
						
							|  |  |  |         "create_db": True, | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     raw_app.config.update({ | 
					
						
							|  |  |  |         "TESTING": True, | 
					
						
							|  |  |  |         "HOST_ADDRESS": "localhost", | 
					
						
							|  |  |  |         "HOSTERS": 1, | 
					
						
							|  |  |  |     }) | 
					
						
							|  |  |  |     return get_app() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def upload_multidata(app_client: "FlaskClient", multidata: Path) -> str: | 
					
						
							|  |  |  |     response = app_client.post("/uploads", data={ | 
					
						
							|  |  |  |         "file": multidata.open("rb"), | 
					
						
							|  |  |  |     }) | 
					
						
							|  |  |  |     assert response.status_code < 400, f"Upload of {multidata} failed: status {response.status_code}" | 
					
						
							|  |  |  |     assert "Location" in response.headers, f"Upload of {multidata} failed: no redirect" | 
					
						
							|  |  |  |     location = response.headers["Location"] | 
					
						
							|  |  |  |     assert isinstance(location, str) | 
					
						
							|  |  |  |     assert location.startswith("/seed/"), f"Upload of {multidata} failed: unexpected redirect" | 
					
						
							|  |  |  |     return location[6:] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def create_room(app_client: "FlaskClient", seed: str, auto_start: bool = False) -> str: | 
					
						
							|  |  |  |     response = app_client.get(f"/new_room/{seed}") | 
					
						
							|  |  |  |     assert response.status_code < 400, f"Creating room for {seed} failed: status {response.status_code}" | 
					
						
							|  |  |  |     assert "Location" in response.headers, f"Creating room for {seed} failed: no redirect" | 
					
						
							|  |  |  |     location = response.headers["Location"] | 
					
						
							|  |  |  |     assert isinstance(location, str) | 
					
						
							|  |  |  |     assert location.startswith("/room/"), f"Creating room for {seed} failed: unexpected redirect" | 
					
						
							|  |  |  |     room_id = location[6:] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if not auto_start: | 
					
						
							|  |  |  |         # by default, creating a room will auto-start it, so we update last activity here | 
					
						
							|  |  |  |         stop_room(app_client, room_id, simulate_idle=False) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return room_id | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def start_room(app_client: "FlaskClient", room_id: str, timeout: float = 30) -> str: | 
					
						
							|  |  |  |     from time import sleep | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-08 17:51:09 +02:00
										 |  |  |     import pony.orm | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-06 01:54:46 +02:00
										 |  |  |     poll_interval = .2 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     print(f"Starting room {room_id}") | 
					
						
							|  |  |  |     no_timeout = timeout <= 0 | 
					
						
							|  |  |  |     while no_timeout or timeout > 0: | 
					
						
							| 
									
										
										
										
											2024-06-08 17:51:09 +02:00
										 |  |  |         try: | 
					
						
							|  |  |  |             response = app_client.get(f"/room/{room_id}") | 
					
						
							|  |  |  |         except pony.orm.core.OptimisticCheckError: | 
					
						
							|  |  |  |             # hoster wrote to room during our transaction | 
					
						
							|  |  |  |             continue | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-06 01:54:46 +02:00
										 |  |  |         assert response.status_code == 200, f"Starting room for {room_id} failed: status {response.status_code}" | 
					
						
							|  |  |  |         match = re.search(r"/connect ([\w:.\-]+)", response.text) | 
					
						
							|  |  |  |         if match: | 
					
						
							|  |  |  |             return match[1] | 
					
						
							|  |  |  |         timeout -= poll_interval | 
					
						
							|  |  |  |         sleep(poll_interval) | 
					
						
							|  |  |  |     raise TimeoutError("Room did not start") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def stop_room(app_client: "FlaskClient", | 
					
						
							|  |  |  |               room_id: str, | 
					
						
							|  |  |  |               timeout: Optional[float] = None, | 
					
						
							|  |  |  |               simulate_idle: bool = True) -> None: | 
					
						
							|  |  |  |     from datetime import datetime, timedelta | 
					
						
							|  |  |  |     from time import sleep | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     from pony.orm import db_session | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     from WebHostLib.models import Command, Room | 
					
						
							|  |  |  |     from WebHostLib import app | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     poll_interval = 2 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     print(f"Stopping room {room_id}") | 
					
						
							|  |  |  |     room_uuid = app.url_map.converters["suuid"].to_python(None, room_id)  # type: ignore[arg-type] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if timeout is not None: | 
					
						
							|  |  |  |         sleep(.1)  # should not be required, but other things might use threading | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     with db_session: | 
					
						
							|  |  |  |         room: Room = Room.get(id=room_uuid) | 
					
						
							|  |  |  |         if simulate_idle: | 
					
						
							|  |  |  |             new_last_activity = datetime.utcnow() - timedelta(seconds=room.timeout + 5) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             new_last_activity = datetime.utcnow() - timedelta(days=3) | 
					
						
							|  |  |  |         room.last_activity = new_last_activity | 
					
						
							|  |  |  |         address = f"localhost:{room.last_port}" if room.last_port > 0 else None | 
					
						
							|  |  |  |         if address: | 
					
						
							|  |  |  |             original_timeout = room.timeout | 
					
						
							|  |  |  |             room.timeout = 1  # avoid spinning it up again | 
					
						
							|  |  |  |             Command(room=room, commandtext="/exit") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     try: | 
					
						
							|  |  |  |         if address and timeout is not None: | 
					
						
							|  |  |  |             print("waiting for shutdown") | 
					
						
							|  |  |  |             import socket | 
					
						
							|  |  |  |             host_str, port_str = tuple(address.split(":")) | 
					
						
							|  |  |  |             address_tuple = host_str, int(port_str) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             no_timeout = timeout <= 0 | 
					
						
							|  |  |  |             while no_timeout or timeout > 0: | 
					
						
							|  |  |  |                 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | 
					
						
							|  |  |  |                 try: | 
					
						
							|  |  |  |                     s.connect(address_tuple) | 
					
						
							|  |  |  |                     s.close() | 
					
						
							|  |  |  |                 except ConnectionRefusedError: | 
					
						
							|  |  |  |                     return | 
					
						
							|  |  |  |                 sleep(poll_interval) | 
					
						
							|  |  |  |                 timeout -= poll_interval | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             raise TimeoutError("Room did not stop") | 
					
						
							|  |  |  |     finally: | 
					
						
							|  |  |  |         with db_session: | 
					
						
							|  |  |  |             room = Room.get(id=room_uuid) | 
					
						
							|  |  |  |             room.last_port = 0  # easier to detect when the host is up this way | 
					
						
							|  |  |  |             if address: | 
					
						
							|  |  |  |                 room.timeout = original_timeout | 
					
						
							|  |  |  |                 room.last_activity = new_last_activity | 
					
						
							|  |  |  |                 print("timeout restored") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def set_room_timeout(room_id: str, timeout: float) -> None: | 
					
						
							|  |  |  |     from pony.orm import db_session | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     from WebHostLib.models import Room | 
					
						
							|  |  |  |     from WebHostLib import app | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     room_uuid = app.url_map.converters["suuid"].to_python(None, room_id)  # type: ignore[arg-type] | 
					
						
							|  |  |  |     with db_session: | 
					
						
							|  |  |  |         room: Room = Room.get(id=room_uuid) | 
					
						
							|  |  |  |         room.timeout = timeout | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_multidata_for_room(webhost_client: "FlaskClient", room_id: str) -> bytes: | 
					
						
							|  |  |  |     from pony.orm import db_session | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     from WebHostLib.models import Room | 
					
						
							|  |  |  |     from WebHostLib import app | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     room_uuid = app.url_map.converters["suuid"].to_python(None, room_id)  # type: ignore[arg-type] | 
					
						
							|  |  |  |     with db_session: | 
					
						
							|  |  |  |         room: Room = Room.get(id=room_uuid) | 
					
						
							|  |  |  |         return cast(bytes, room.seed.multidata) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def set_multidata_for_room(webhost_client: "FlaskClient", room_id: str, data: bytes) -> None: | 
					
						
							|  |  |  |     from pony.orm import db_session | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     from WebHostLib.models import Room | 
					
						
							|  |  |  |     from WebHostLib import app | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     room_uuid = app.url_map.converters["suuid"].to_python(None, room_id)  # type: ignore[arg-type] | 
					
						
							|  |  |  |     with db_session: | 
					
						
							|  |  |  |         room: Room = Room.get(id=room_uuid) | 
					
						
							|  |  |  |         room.seed.multidata = data | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def stop_autohost(graceful: bool = True) -> None: | 
					
						
							|  |  |  |     import os | 
					
						
							|  |  |  |     import signal | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     import multiprocessing | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     from WebHostLib.autolauncher import stop | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     stop() | 
					
						
							|  |  |  |     proc: multiprocessing.process.BaseProcess | 
					
						
							|  |  |  |     for proc in filter(lambda child: child.name.startswith("MultiHoster"), multiprocessing.active_children()): | 
					
						
							|  |  |  |         if graceful and proc.pid: | 
					
						
							|  |  |  |             os.kill(proc.pid, getattr(signal, "CTRL_C_EVENT", signal.SIGINT)) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             proc.kill() | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             proc.join(30) | 
					
						
							|  |  |  |         except TimeoutError: | 
					
						
							|  |  |  |             proc.kill() | 
					
						
							|  |  |  |             proc.join() |