aurweb/aurweb/spawn.py
Kevin Morris 19652d6cbe
swap uvicorn out for hypercorn
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>
2021-05-10 23:22:00 -04:00

176 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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 cant 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 dont 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()