1import argparse
2import logging
3import os
4import sys
5import subprocess
6import threading
7
8from typing import Sequence, Optional, List
9
10import orangecanvas.utils.shtools as sh
11
12
13def run_after_exit(
14        command: Sequence[str] = (), log: Optional[str] = None
15) -> None:
16    """
17    Run the `command` after this process exits.
18    """
19    # pass read end of a pipe to subprocess. It blocks to read from it
20    # and will not succeed until the write end is closed which will happen at
21    # this process's exit (assuming `w` is not leaked in a fork).
22    command = ["--arg=" + c for c in command]
23    if log is not None:
24        command.append("--log=" + log)
25    command = ["-m", __name__, *command]
26    with __write_fds_lock:
27        r, w = os.pipe()
28        __write_fds.append(w)
29    p = sh.python_process(
30        command,
31        stdin=r, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
32    )
33    # close the read end of the pipe
34    os.close(r)
35    # Popen warns in __del__ if child did not complete yet (should
36    # double fork to do this right, but since we exit immediately anyways).
37    __run_after_exit_processes.append(p)
38
39
40__run_after_exit_processes: List[subprocess.Popen] = []
41__write_fds: List[int] = []
42__write_fds_lock = threading.Lock()
43
44
45def __close_write_fd_after_fork():
46    while __write_fds:
47        w = __write_fds.pop()
48        try:
49            os.close(w)
50        except OSError:
51            pass
52    __write_fds_lock.release()
53
54
55if hasattr(os, "register_at_fork"):
56    os.register_at_fork(
57        before=__write_fds_lock.acquire,
58        after_in_child=__close_write_fd_after_fork,
59        after_in_parent=__write_fds_lock.release,
60    )
61
62
63def main(argv):
64    ap = argparse.ArgumentParser()
65    ap.add_argument("-f", default=0, type=int)
66    ap.add_argument("-a", "--arg", action='append', default=[])
67    ap.add_argument("--log", help="Log file", type=argparse.FileType("w"))
68    ns, rest = ap.parse_known_args(argv)
69
70    if ns.log is not None:
71        logging.basicConfig(level=logging.INFO, stream=ns.log)
72    log = logging.getLogger(__name__)
73
74    if ns.f is not None:
75        readfd = int(ns.f)
76    else:
77        readfd = 0
78
79    # read form readfd (an os.pipe read end) until EOF indicating parent
80    # closed the pipe (i.e. did exit)
81    log.info("Blocking on read from fd: %d", readfd)
82    c = os.read(readfd, 1)
83    if c != b"":
84        log.error("Unexpected content %r from parent", c)
85    else:
86        log.info("Parent closed fd; %d")
87
88    if ns.arg:
89        log.info("Starting new process with cmd: %r", ns.arg)
90        p = sh.create_process(
91            ns.arg, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
92        )
93        main.p = p
94    return 0
95
96
97if __name__ == "__main__":
98    sys.exit(main(sys.argv))
99