mirror of
https://gitlab.archlinux.org/archlinux/aurweb.git
synced 2025-02-03 10:43:03 +01:00
uvicorn is subjectively nicer to play with for local dev work, but hypercorn is required in order to do HTTP/2 which is fairly performance-important. Signed-off-by: Kevin Morris <kevr@0cost.org> Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org> Co-authored-by: Eli Schwartz <eschwartz@archlinux.org> Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
176 lines
5.4 KiB
Python
176 lines
5.4 KiB
Python
"""
|
||
Provide an automatic way of spawing an HTTP test server running aurweb.
|
||
|
||
It can be called from the command-line or from another Python module.
|
||
|
||
This module uses a global state, since you can’t open two servers with the same
|
||
configuration anyway.
|
||
"""
|
||
|
||
|
||
import argparse
|
||
import atexit
|
||
import os
|
||
import os.path
|
||
import subprocess
|
||
import sys
|
||
import tempfile
|
||
import time
|
||
import urllib
|
||
|
||
import aurweb.config
|
||
import aurweb.schema
|
||
|
||
children = []
|
||
temporary_dir = None
|
||
verbosity = 0
|
||
asgi_backend = ''
|
||
|
||
|
||
class ProcessExceptions(Exception):
|
||
"""
|
||
Compound exception used by stop() to list all the errors that happened when
|
||
terminating child processes.
|
||
"""
|
||
|
||
def __init__(self, message, exceptions):
|
||
self.message = message
|
||
self.exceptions = exceptions
|
||
messages = [message] + [str(e) for e in exceptions]
|
||
super().__init__("\n- ".join(messages))
|
||
|
||
|
||
def generate_nginx_config():
|
||
"""
|
||
Generate an nginx configuration based on aurweb's configuration.
|
||
The file is generated under `temporary_dir`.
|
||
Returns the path to the created configuration file.
|
||
"""
|
||
aur_location = aurweb.config.get("options", "aur_location")
|
||
aur_location_parts = urllib.parse.urlsplit(aur_location)
|
||
config_path = os.path.join(temporary_dir, "nginx.conf")
|
||
config = open(config_path, "w")
|
||
# We double nginx's braces because they conflict with Python's f-strings.
|
||
config.write(f"""
|
||
events {{}}
|
||
daemon off;
|
||
error_log /dev/stderr info;
|
||
pid {os.path.join(temporary_dir, "nginx.pid")};
|
||
http {{
|
||
access_log /dev/stdout;
|
||
server {{
|
||
listen {aur_location_parts.netloc};
|
||
location / {{
|
||
proxy_pass http://{aurweb.config.get("php", "bind_address")};
|
||
}}
|
||
location /sso {{
|
||
proxy_pass http://{aurweb.config.get("fastapi", "bind_address")};
|
||
}}
|
||
}}
|
||
}}
|
||
""")
|
||
return config_path
|
||
|
||
|
||
def spawn_child(args):
|
||
"""Open a subprocess and add it to the global state."""
|
||
if verbosity >= 1:
|
||
print(f":: Spawning {args}", file=sys.stderr)
|
||
children.append(subprocess.Popen(args))
|
||
|
||
|
||
def start():
|
||
"""
|
||
Spawn the test server. If it is already running, do nothing.
|
||
|
||
The server can be stopped with stop(), or is automatically stopped when the
|
||
Python process ends using atexit.
|
||
"""
|
||
if children:
|
||
return
|
||
atexit.register(stop)
|
||
|
||
if 'AUR_CONFIG' in os.environ:
|
||
os.environ['AUR_CONFIG'] = os.path.realpath(os.environ['AUR_CONFIG'])
|
||
|
||
try:
|
||
terminal_width = os.get_terminal_size().columns
|
||
except OSError:
|
||
terminal_width = 80
|
||
print("{ruler}\n"
|
||
"Spawing PHP and FastAPI, then nginx as a reverse proxy.\n"
|
||
"Check out {aur_location}\n"
|
||
"Hit ^C to terminate everything.\n"
|
||
"{ruler}"
|
||
.format(ruler=("-" * terminal_width),
|
||
aur_location=aurweb.config.get('options', 'aur_location')))
|
||
|
||
# PHP
|
||
php_address = aurweb.config.get("php", "bind_address")
|
||
htmldir = aurweb.config.get("php", "htmldir")
|
||
spawn_child(["php", "-S", php_address, "-t", htmldir])
|
||
|
||
# FastAPI
|
||
host, port = aurweb.config.get("fastapi", "bind_address").rsplit(":", 1)
|
||
if asgi_backend == "hypercorn":
|
||
portargs = ["-b", f"{host}:{port}"]
|
||
elif asgi_backend == "uvicorn":
|
||
portargs = ["--host", host, "--port", port]
|
||
spawn_child(["python", "-m", asgi_backend] + portargs + ["aurweb.asgi:app"])
|
||
|
||
# nginx
|
||
spawn_child(["nginx", "-p", temporary_dir, "-c", generate_nginx_config()])
|
||
|
||
|
||
def stop():
|
||
"""
|
||
Stop all the child processes.
|
||
|
||
If an exception occurs during the process, the process continues anyway
|
||
because we don’t want to leave runaway processes around, and all the
|
||
exceptions are finally raised as a single ProcessExceptions.
|
||
"""
|
||
global children
|
||
atexit.unregister(stop)
|
||
exceptions = []
|
||
for p in children:
|
||
try:
|
||
p.terminate()
|
||
if verbosity >= 1:
|
||
print(f":: Sent SIGTERM to {p.args}", file=sys.stderr)
|
||
except Exception as e:
|
||
exceptions.append(e)
|
||
for p in children:
|
||
try:
|
||
rc = p.wait()
|
||
if rc != 0 and rc != -15:
|
||
# rc = -15 indicates the process was terminated with SIGTERM,
|
||
# which is to be expected since we called terminate on them.
|
||
raise Exception(f"Process {p.args} exited with {rc}")
|
||
except Exception as e:
|
||
exceptions.append(e)
|
||
children = []
|
||
if exceptions:
|
||
raise ProcessExceptions("Errors terminating the child processes:",
|
||
exceptions)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
parser = argparse.ArgumentParser(
|
||
prog='python -m aurweb.spawn',
|
||
description='Start aurweb\'s test server.')
|
||
parser.add_argument('-v', '--verbose', action='count', default=0,
|
||
help='increase verbosity')
|
||
parser.add_argument('-b', '--backend', choices=['hypercorn', 'uvicorn'], default='hypercorn',
|
||
help='asgi backend used to launch the python server')
|
||
args = parser.parse_args()
|
||
verbosity = args.verbose
|
||
asgi_backend = args.backend
|
||
with tempfile.TemporaryDirectory(prefix="aurweb-") as tmpdirname:
|
||
temporary_dir = tmpdirname
|
||
start()
|
||
try:
|
||
while True:
|
||
time.sleep(60)
|
||
except KeyboardInterrupt:
|
||
stop()
|