'Terminate child process on subprocess.TimeoutExpired

I have the following snippet of code:

    def terminal_command(command, timeout=5*60):
        """Executes a terminal command."""
        cmd = command.split(" ")

        timer = time.strftime('%Hh %Mm %Ss', time.gmtime(timeout))

        proc = None

        try:
            proc = subprocess.run(cmd, timeout=timeout, capture_output=True)
        except subprocess.TimeoutExpired:
            print("Timeout")
            proc.terminate()
            reason = "timeout"
            stdout = b'error'
            stderr = b'error'

        if proc != None:
            # Finished!

            stdout = proc.stdout
            stderr = proc.stderr
            reason = "finished"

        return stdout.decode('utf-8').strip(), stderr.decode('utf-8').strip(), reason

I ran a command which takes significantly longer than 5 minutes. In this instance, subprocess.run raises an exception, but proc is now None so I cannot use proc.terminate(). When the code terminates, as has been well documented elsewhere, the child process continues to run. I would like to terminate it.

Is there any way to terminate a subprocess on a TimeoutExpired, whilst redirecting output? I am on a Linux system so am open to requiring Popen but ideally I would like this to be cross-platform.



Solution 1:[1]

Four months later: I got it.

The core issue appears to be that using os.kill with signal.SIGKILL doesn't properly kill the process.

Modifying my code to the following works.

    def custom_terminal_command(self, command, timeout=5*60, cwd=None):
        with subprocess.Popen(command.split(" "), preexec_fn=os.setsid) as process:
            wd = os.getcwd()
            try:
                if cwd is not None:
                    # Man fuck linux
                    for d in cwd.split("/"):
                        os.chdir(d)
                stdout, stderr = process.communicate(None, timeout=timeout)
            except subprocess.TimeoutExpired as exc:
                import signal

                os.killpg(os.getpgid(process.pid), signal.SIGTERM)

                try:
                    import msvcrt
                except ModuleNotFoundError:
                    _mswindows = False
                else:
                    _mswindows = True

                if _mswindows:
                    # Windows accumulates the output in a single blocking
                    # read() call run on child threads, with the timeout
                    # being done in a join() on those threads.  communicate()
                    # _after_ kill() is required to collect that and add it
                    # to the exception.
                    exc.stdout, exc.stderr = process.communicate()
                else:
                    # POSIX _communicate already populated the output so
                    # far into the TimeoutExpired exception.
                    process.wait()
                reason = 'timeout'
                stdout, stderr = process.communicate()
            except:  # Including KeyboardInterrupt, communicate handled that.
                process.kill()
                # We don't call process.wait() as .__exit__ does that for us.
                reason = 'other'
                stdout, stderr = process.communicate()
                raise
            else:
                reason = 'finished'
            finally:
                os.chdir(wd)

            try:
                return stdout.decode('utf-8').strip(), stderr.decode('utf-8').strip(), reason
            except AttributeError:
                try:
                    return stdout.strip(), stderr.strip(), reason
                except AttributeError:
                    return stdout, stderr, reason

See the following SO post for a short discussion: How to terminate a python subprocess launched with shell=True

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Seabody