2

After some research there was no definitive answer as to how to have a child process understand that the parent process has died/crashed/exited under Windows which can make the child process run unattended. There are some proposals as in:

Always involving a known parent which has started a child. But there are cases in which the child doesn't know it is a child, because it is not conceived as a child and the parent makes no effort to kill the children.

Furthermore, there is no control of the parent. Practical case:

  • Cygwin running under Windows
  • Windows Python 1st in the path
  • Python executable installed via setuptools entry_points facility.

As mentioned above the Python to be executed is the Windows one. The setuptools-generated executable will find it and execute it as subprocess with the asocciated script.

Because one is running under Cygwin the following may fail:

  • Pressing Ctrl-c will kill the parent (the stub setuptools executable)
  • But will leave the child running (to be found in the process list as python.exe)

In this case and as mentioned above, it isn't possible to control the parent and the child doesn't know it's a child (because it may also be directly executed as a Python script)

mementum
  • 3,153
  • 13
  • 20
  • The setuptools EXE wrappers are buggy and obsolete. Whenever possible, build/install a wheel package instead, which pip installs using the EXE stubs from distlib. These launchers use a Job object to ensure the child process gets terminated along with the parent. – Eryk Sun Dec 05 '17 at 18:32
  • `pip install -e .` – mementum Dec 06 '17 at 07:41

1 Answers1

1

The solution is to do as follows

import sys

def win_wait_for_parent(raise_exceptions=False):
    if not sys.platform == 'win32':
        return True

    # When started under cygwin, the parent process will die leaving a child
    # hanging around. The process has to be waited upon
    import ctypes
    from ctypes.wintypes import DWORD, BOOL, HANDLE
    import os
    import threading

    INFINITE = -1
    SYNCHRONIZE = 0x00100000

    kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)

    kernel32.OpenProcess.argtypes = (DWORD, BOOL, DWORD)
    kernel32.OpenProcess.restype = HANDLE

    kernel32.WaitForSingleObject.argtypes = (HANDLE, DWORD)
    kernel32.WaitForSingleObject.restype = DWORD

    phandle = kernel32.OpenProcess(SYNCHRONIZE, 0, os.getppid())

    def check_parent():
        # Get a token with right access to parent and wait for it to be
        # signaled (die). Exit ourselves then
            kernel32.WaitForSingleObject(phandle, INFINITE)
            os._exit(0)

    if not phandle:
        if raise_exceptions:
            raise ctypes.WinError(ctypes.get_last_error())

        return False

    threading.Thread(target=check_parent).start()
    return True

which runs in a separate thread if the PID of the process is not the same as the PID of the parent waiting for the parent to be signaled (death). This works under Python 3.3, where os.getppid() does actually returnt the PID of the parent under window

It requires no modification of the parent and the child doesn't need to be coded in advance as a child, because a check is done as to where the thread has to run or no.

-- refactored as a function and added improvements from comments

mementum
  • 3,153
  • 13
  • 20
  • Raising the exception would only make sense, imho, if the acquisition of the handle to the parent is not made in the secondary thread and everything is wrapped in a function. It all makes sense, but the code was meant as a snippet. – mementum Dec 05 '17 at 23:18