File: data_test_child.py

package info (click to toggle)
firefox 144.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 4,637,504 kB
  • sloc: cpp: 7,576,692; javascript: 6,430,831; ansic: 3,748,119; python: 1,398,978; xml: 628,810; asm: 438,679; java: 186,194; sh: 63,212; makefile: 19,159; objc: 13,086; perl: 12,986; yacc: 4,583; cs: 3,846; pascal: 3,448; lex: 1,720; ruby: 1,003; exp: 762; php: 436; lisp: 258; awk: 247; sql: 66; sed: 53; csh: 10
file content (159 lines) | stat: -rw-r--r-- 6,211 bytes parent folder | download | duplicates (10)
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}")