2024-06-05 23:54:46 +00: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 15:51:09 +00:00
|
|
|
import pony.orm
|
|
|
|
|
2024-06-05 23:54:46 +00:00
|
|
|
poll_interval = .2
|
|
|
|
|
|
|
|
print(f"Starting room {room_id}")
|
|
|
|
no_timeout = timeout <= 0
|
|
|
|
while no_timeout or timeout > 0:
|
2024-06-08 15:51:09 +00: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-05 23:54:46 +00: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()
|