summaryrefslogtreecommitdiffstats
path: root/aurweb/spawn.py
blob: 5fa646b5d2ca0df9cc228973a9922567ac5fad5f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
"""
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 atexit
import argparse
import subprocess
import sys
import time
import urllib

import aurweb.config
import aurweb.schema


children = []
verbosity = 0


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 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)
    aur_location = aurweb.config.get("options", "aur_location")
    aur_location_parts = urllib.parse.urlsplit(aur_location)
    htmldir = aurweb.config.get("options", "htmldir")
    spawn_child(["php", "-S", aur_location_parts.netloc, "-t", htmldir])


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')
    args = parser.parse_args()
    verbosity = args.verbose
    start()
    try:
        while True:
            time.sleep(60)
    except KeyboardInterrupt:
        stop()