sous-processus: suppression de processus enfants dans Windows
-
22-07-2019 - |
Question
Sous Windows, subprocess.Popen.terminate
appelle le ProcessProcessus
de Win32. Cependant, le problème que je constate est que les processus enfants du processus que je tente de terminer sont toujours en cours d'exécution. Pourquoi donc? Comment puis-je m'assurer que tous les processus enfants lancés par le processus sont supprimés?
La solution
En utilisant psutil :
import psutil, os
def kill_proc_tree(pid, including_parent=True):
parent = psutil.Process(pid)
children = parent.children(recursive=True)
for child in children:
child.kill()
gone, still_alive = psutil.wait_procs(children, timeout=5)
if including_parent:
parent.kill()
parent.wait(5)
me = os.getpid()
kill_proc_tree(me)
Autres conseils
Utilisez taskkill
avec l'indicateur / T
p = subprocess.Popen(...)
<wait>
subprocess.call(['taskkill', '/F', '/T', '/PID', str(p.pid)])
Les indicateurs de taskkill ont les documents suivants:
TASKKILL [/S system [/U username [/P [password]]]]
{ [/FI filter] [/PID processid | /IM imagename] } [/T] [/F]
/S system Specifies the remote system to connect to.
/U [domain\]user Specifies the user context under which the
command should execute.
/P [password] Specifies the password for the given user
context. Prompts for input if omitted.
/FI filter Applies a filter to select a set of tasks.
Allows "*" to be used. ex. imagename eq acme*
/PID processid Specifies the PID of the process to be terminated.
Use TaskList to get the PID.
/IM imagename Specifies the image name of the process
to be terminated. Wildcard '*' can be used
to specify all tasks or image names.
/T Terminates the specified process and any
child processes which were started by it.
/F Specifies to forcefully terminate the process(es).
/? Displays this help message.
Ou parcourez l'arborescence de processus à l'aide de comtypes et de win32api:
def killsubprocesses(parent_pid):
'''kill parent and all subprocess using COM/WMI and the win32api'''
log = logging.getLogger('killprocesses')
try:
import comtypes.client
except ImportError:
log.debug("comtypes not present, not killing subprocesses")
return
logging.getLogger('comtypes').setLevel(logging.INFO)
log.debug('Querying process tree...')
# get pid and subprocess pids for all alive processes
WMI = comtypes.client.CoGetObject('winmgmts:')
processes = WMI.InstancesOf('Win32_Process')
subprocess_pids = {} # parent pid -> list of child pids
for process in processes:
pid = process.Properties_('ProcessID').Value
parent = process.Properties_('ParentProcessId').Value
log.trace("process %i's parent is: %s" % (pid, parent))
subprocess_pids.setdefault(parent, []).append(pid)
subprocess_pids.setdefault(pid, [])
# find which we need to kill
log.debug('Determining subprocesses for pid %i...' % parent_pid)
processes_to_kill = []
parent_processes = [parent_pid]
while parent_processes:
current_pid = parent_processes.pop()
subps = subprocess_pids[current_pid]
log.debug("process %i children are: %s" % (current_pid, subps))
parent_processes.extend(subps)
processes_to_kill.extend(subps)
# kill the subprocess tree
if processes_to_kill:
log.info('Process pid %i spawned %i subprocesses, terminating them...' %
(parent_pid, len(processes_to_kill)))
else:
log.debug('Process pid %i had no subprocesses.' % parent_pid)
import ctypes
kernel32 = ctypes.windll.kernel32
for pid in processes_to_kill:
hProcess = kernel32.OpenProcess(PROCESS_TERMINATE, FALSE, pid)
if not hProcess:
log.warning('Unable to open process pid %i for termination' % pid)
else:
log.debug('Terminating pid %i' % pid)
kernel32.TerminateProcess(hProcess, 3)
kernel32.CloseHandle(hProcess)
C’est une chose difficile à faire. Windows ne stocke pas réellement d'arborescence de processus dans l'espace de processus. Il n'est pas non plus possible de terminer un processus et de spécifier que ses enfants doivent également mourir.
Une solution consiste à utiliser taskkill et à lui dire de cingler tout l’arbre.
Une autre façon de le faire (en supposant que vous créiez le processus de niveau supérieur) consiste à utiliser un module développé avec ce genre de choses à l'esprit: http://benjamin.smedbergs.us/blog/tag/killableprocess/
Pour pouvoir le faire de manière générique, vous devez passer du temps à construire la liste à l’arrière. C'est-à-dire qu'un processus stocke des pointeurs sur son PARENT, mais les parents semblent ne pas stocker d'informations sur les enfants.
Vous devez donc examiner tous les processus du système (ce qui n’est vraiment pas si difficile), puis connecter manuellement les points vous-même en consultant le champ du processus parent. Ensuite, vous sélectionnez l’arbre qui vous intéresse et parcourez l’ensemble, en tuant chaque nœud un à un.
Notez que Windows ne met pas à jour le pointeur parent d'un enfant lorsque le parent meurt. Il peut donc y avoir des lacunes dans votre arborescence. Je ne suis au courant de rien que vous puissiez faire à ce sujet.
Voici un exemple de code pour la méthode de l'objet Job, mais au lieu du sous-processus
, il utilise win32api.CreateProcess
import win32process
import win32job
startup = win32process.STARTUPINFO()
(hProcess, hThread, processId, threadId) = win32process.CreateProcess(None, command, None, None, True, win32process.CREATE_BREAKAWAY_FROM_JOB, None, None, startup)
hJob = win32job.CreateJobObject(None, '')
extended_info = win32job.QueryInformationJobObject(hJob, win32job.JobObjectExtendedLimitInformation)
extended_info['BasicLimitInformation']['LimitFlags'] = win32job.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
win32job.SetInformationJobObject(hJob, win32job.JobObjectExtendedLimitInformation, extended_info)
win32job.AssignProcessToJobObject(hJob, hProcess)
Mettez les enfants dans un NT Objet de travail , alors vous pouvez tuer tous les enfants
J'ai eu le même problème et je viens de tuer le processus via la commande windows avec l'option de tuer des enfants "/ T"
.def kill_command_windows(pid):
'''Run command via subprocess'''
dev_null = open(os.devnull, 'w')
command = ['TASKKILL', '/F', '/T', '/PID', str(pid)]
proc = subprocess.Popen(command, stdin=dev_null, stdout=sys.stdout, stderr=sys.stderr)
J'ai utilisé la réponse de kevin-smyth pour créer un remplacement immédiat pour . subprocess.Popen
qui limite le processus enfant créé dans un objet de travail anonyme, configuré pour se terminer à la fermeture:
# coding: utf-8
from subprocess import Popen
import subprocess
import win32job
import win32process
import win32api
class JobPopen(Popen):
"""Start a process in a new Win32 job object.
This `subprocess.Popen` subclass takes the same arguments as Popen and
behaves the same way. In addition to that, created processes will be
assigned to a new anonymous Win32 job object on startup, which will
guarantee that the processes will be terminated by the OS as soon as
either the Popen object, job object handle or parent Python process are
closed.
"""
class _winapijobhandler(object):
"""Patches the native CreateProcess function in the subprocess module
to assign created threads to the given job"""
def __init__(self, oldapi, job):
self._oldapi = oldapi
self._job = job
def __getattr__(self, key):
if key != "CreateProcess":
return getattr(self._oldapi, key) # Any other function is run as before
else:
return self.CreateProcess # CreateProcess will call the function below
def CreateProcess(self, *args, **kwargs):
hp, ht, pid, tid = self._oldapi.CreateProcess(*args, **kwargs)
win32job.AssignProcessToJobObject(self._job, hp)
win32process.ResumeThread(ht)
return hp, ht, pid, tid
def __init__(self, *args, **kwargs):
"""Start a new process using an anonymous job object. Takes the same arguments as Popen"""
# Create a new job object
self._win32_job = self._create_job_object()
# Temporarily patch the subprocess creation logic to assign created
# processes to the new job, then resume execution normally.
CREATE_SUSPENDED = 0x00000004
kwargs.setdefault("creationflags", 0)
kwargs["creationflags"] |= CREATE_SUSPENDED
try:
_winapi = subprocess._winapi # Python 3
_winapi_key = "_winapi"
except AttributeError:
_winapi = subprocess._subprocess # Python 2
_winapi_key = "_subprocess"
try:
setattr(subprocess, _winapi_key, JobPopen._winapijobhandler(_winapi, self._win32_job))
super(JobPopen, self).__init__(*args, **kwargs)
finally:
setattr(subprocess, _winapi_key, _winapi)
def _create_job_object(self):
"""Create a new anonymous job object"""
hjob = win32job.CreateJobObject(None, "")
extended_info = win32job.QueryInformationJobObject(hjob, win32job.JobObjectExtendedLimitInformation)
extended_info['BasicLimitInformation']['LimitFlags'] = win32job.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
win32job.SetInformationJobObject(hjob, win32job.JobObjectExtendedLimitInformation, extended_info)
return hjob
def _close_job_object(self, hjob):
"""Close the handle to a job object, terminating all processes inside it"""
if self._win32_job:
win32api.CloseHandle(self._win32_job)
self._win32_job = None
# This ensures that no remaining subprocesses are found when the process
# exits from a `with JobPopen(...)` block.
def __exit__(self, exc_type, value, traceback):
super(JobPopen, self).__exit__(exc_type, value, traceback)
self._close_job_object(self._win32_job)
# Python does not keep a reference outside of the parent class when the
# interpreter exits, which is why we keep it here.
_Popen = subprocess.Popen
def __del__(self):
self._Popen.__del__(self)
self._close_job_object(self._win32_job)