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 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
|
#!/usr/bin/env python3
# This file is run by test_subprocess_child.js. To test this file in isolation:
# python3 -u toolkit/modules/subprocess/test/xpcshell/data_test_child.py spawn_child_and_exit
# Then separately, to the displayed URL "Listening at http://127.0.0.1:12345",
# with 12345 being a random port,
# curl http://127.0.0.1:12345 -X DELETE # request exit of parent
# curl http://127.0.0.1:12345 # request exit of child
import sys
import time
def sleep_for_a_little_bit():
time.sleep(0.2)
def spawn_child_and_exit(is_breakaway_job):
"""
Spawns and exits child processes to allow tests to verify that they detect
specifically the exit of this (parent) process.
The expected sequence of outputs is as follows:
1. parent_start
2. first_child_start_and_exit
3. parent_after_first_child_exit
4. spawned_child_start
5. Listening at http://127.0.0.1:12345 - with 12345 being random port
6. child_received_http_request - DELETE request from test.
7. data_from_child:kill_parent
8. parent_exit
( now the parent has exit)
( child_process_still_alive_1 response sent to request from step 6)
( wait for new request from client to request child to exit )
( child_process_still_alive_2 response sent to that new request )
9. spawned_child_exit
"""
import subprocess
print("1. parent_start", flush=True)
# Start and exit a child process (used to make sure that we do not
# mistakenly detect an exited child process for the parent process).
subprocess.run(
[sys.executable, "-c", "print('2. first_child_start_and_exit')"],
stdout=sys.stdout,
stderr=sys.stderr,
)
# Wait a bit to make sure that the child's exit signal has been processed.
# This is not strictly needed, because we don't expect the child to affect
# the parent, but in case of a flawed implementation, this would enable the
# test to detect a bad implementation (by observing the exit before the
# "parent_after_first_child_exit" message below).
sleep_for_a_little_bit()
print("3. parent_after_first_child_exit", flush=True)
creationflags = 0
if is_breakaway_job:
# See comment in test_subprocess_child.js; in short we need this flag
# to make sure that the child outlives the parent when the subprocess
# implementation calls TerminateJobObject.
creationflags = subprocess.CREATE_BREAKAWAY_FROM_JOB
child_proc = subprocess.Popen(
[sys.executable, "-u", __file__, "spawned_child"],
creationflags=creationflags,
# We don't need this pipe, but when this side of the process exits,
# the pipe is closed, which the child can use to detect that the parent
# has exit.
stdin=subprocess.PIPE,
# We are using stdout as a control channel to allow the child to
# notify us when the process is done.
stdout=subprocess.PIPE,
# stderr is redirected to the real stdout, so that the caller can
# still observe print() from spawned_child.
stderr=sys.stdout,
)
# This blocks until the child has notified us.
data_from_child = child_proc.stdout.readline().decode().rstrip()
print(f"7. data_from_child:{data_from_child}", flush=True)
print("8. parent_exit", flush=True)
# Wait a little bit to make sure that stdout has been flushed (and read by
# the caller) when the process exits.
sleep_for_a_little_bit()
sys.exit(0)
def spawned_child():
import http.server
import socketserver
def print_to_parent_stdout(msg):
# The parent maps our stderr to its stdout.
print(msg, flush=True, file=sys.stderr)
# This is spawned via spawn_child_and_exit.
print_to_parent_stdout("4. spawned_child_start")
class RequestHandler(http.server.BaseHTTPRequestHandler):
def log_message(self, *args):
pass # Disable logging
def do_DELETE(self):
print_to_parent_stdout("6. child_received_http_request")
# Let the caller know that we are responsive.
self.send_response(200)
self.send_header("Connection", "close")
self.end_headers()
# Wait a little bit to allow the network request to be
# processed by the client. If for some reason the termination
# of the parent also kills the child, then at least the client
# has had a chance to become aware of it.
sleep_for_a_little_bit()
# Now ask the parent to exit, and continue here.
print("kill_parent", flush=True)
# When the parent exits, stdin closes, which we detect here:
res = sys.stdin.read(1)
if len(res):
print_to_parent_stdout("spawned_child_UNEXPECTED_STDIN")
# If we make it here, it means that this child outlived the
# parent, and we can let the client know.
# (if the child process is terminated prematurely, the client
# would also know through a disconnected socket).
self.wfile.write(b"child_process_still_alive_1")
def do_GET(self):
self.send_response(200)
self.send_header("Connection", "close")
self.end_headers()
self.wfile.write(b"child_process_still_alive_2")
# Starts a server that handles two requests and then closes the server.
with socketserver.TCPServer(("127.0.0.1", 0), RequestHandler) as server:
host, port = server.server_address[:2]
print_to_parent_stdout(f"5. Listening at http://{host}:{port}")
# Expecting DELETE request (do_DELETE)
server.handle_request()
# Expecting GET request (do_GET)
server.handle_request()
print_to_parent_stdout("9. spawned_child_exit")
sys.exit(0)
cmd = sys.argv[1]
if cmd == "spawn_child_and_exit":
spawn_child_and_exit(is_breakaway_job=False)
elif cmd == "spawn_child_in_breakaway_job_and_exit":
spawn_child_and_exit(is_breakaway_job=True)
elif cmd == "spawned_child":
spawned_child()
else:
raise Exception(f"Unknown command: {cmd}")
|