#!/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')"],
        check=False,
        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}")
