Skip to content

Commit f691c9d

Browse files
author
Thomas Desveaux
committed
check: revert usage of psutil in check processes
psutil has a dead-lock in it's openfiles function.
1 parent 436f9ba commit f691c9d

File tree

1 file changed

+50
-186
lines changed

1 file changed

+50
-186
lines changed

nimp/base_commands/check.py

Lines changed: 50 additions & 186 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,7 @@
3333
import re
3434
import shutil
3535
import time
36-
from typing import Sequence
3736

38-
import psutil
3937

4038
import nimp.command
4139
import nimp.sys.platform
@@ -47,7 +45,13 @@ class Check(nimp.command.CommandGroup):
4745
'''Check related commands'''
4846

4947
def __init__(self):
50-
super(Check, self).__init__([_Status(), _Processes(), _Disks()])
48+
super(Check, self).__init__(
49+
[
50+
_Status(),
51+
_Processes(),
52+
_Disks(),
53+
]
54+
)
5155

5256
def is_available(self, env):
5357
return True, ''
@@ -135,11 +139,6 @@ def _show_nimp_environment(env):
135139

136140

137141
class _Processes(CheckCommand):
138-
PROCESS_IGNORE_PATTERNS: Sequence[re.Pattern] = (
139-
# re.compile(r'^CrashReportClient\.exe$', re.IGNORECASE),
140-
re.compile(r'^dotnet\.exe$', re.IGNORECASE),
141-
)
142-
143142
def configure_arguments(self, env, parser):
144143
parser.add_argument('-k', '--kill', help='Kill processes that can prevent builds', action='store_true')
145144
parser.add_argument(
@@ -149,11 +148,6 @@ def configure_arguments(self, env, parser):
149148
help='fnmatch filters, defaults to workspace',
150149
default=[os.path.normpath(f'{os.path.abspath(env.root_dir)}/*')],
151150
)
152-
parser.add_argument(
153-
'--all-users',
154-
help='By default, only check processes owned by the current user. Use this to check all running processes.',
155-
action='store_true',
156-
)
157151
return True
158152

159153
def _run_check(self, env: NimpEnvironment):
@@ -165,192 +159,62 @@ def _run_check(self, env: NimpEnvironment):
165159
logging.warning("Command only available on Windows platform")
166160
return True
167161

168-
current_user: str | None = None
169-
if not env.all_users:
170-
current_user = psutil.Process().username()
171-
logging.debug("Only act on processes owned by %s", current_user)
172-
173-
# Find all running processes running a program that any filter match either:
174-
# - the program executable
175-
# - an open file handle
162+
# Find all running binaries launched from the project directory
176163
# and optionally kill them, unless they’re in the exception list.
177164
# We get to try 5 times just in case
178165
for _ in range(5):
179-
checked_processes_count = 0
180-
problematic_processes: list[psutil.Process] = []
181-
182-
# psutil.process_iter caches processes
183-
# we want a fresh list since we might have killed/
184-
# process completed since last iteration
185-
logging.debug("Clear psutil process cache")
186-
psutil.process_iter.cache_clear()
187-
logging.debug("Get current process")
188-
current_process = psutil.Process()
189-
logging.debug("Current process is %d", current_process.pid)
190-
ignore_process_ids = set(
191-
(
192-
current_process.pid,
193-
*(p.pid for p in current_process.parents()),
194-
*(p.pid for p in current_process.children(recursive=True)),
195-
)
196-
)
197-
if logging.getLogger().isEnabledFor(logging.DEBUG):
198-
logging.debug("Ignore processes:")
199-
for pid in ignore_process_ids:
200-
ignored_process = psutil.Process(pid)
201-
logging.debug("\t%s (%s) %s", ignored_process.exe(), ignored_process.pid, ignored_process.cmdline())
202-
203-
for process in psutil.process_iter():
204-
if not process.is_running():
205-
continue
166+
found_problem = False
167+
processes = _Processes._list_windows_processes()
206168

207-
logging.debug("Checking process %d", process.pid)
208-
if process.pid in ignore_process_ids:
209-
logging.debug("[Process(%d)] ignore process (self, parent or child)", process.pid)
169+
for pid, info in processes.items():
170+
if not any(fnmatch.fnmatch(info[0], filter) for filter in env.filters):
210171
continue
211-
212-
if current_user is not None:
213-
if not _Processes._process_owned_by_user(process, current_user):
214-
continue
215-
216-
checked_processes_count += 1
217-
if not _Processes._process_matches_filters(process, env.filters):
218-
continue
219-
220-
try:
221-
process_exe = process.exe()
222-
except psutil.Error as exc:
223-
logging.debug('[Process(%d)] failed to retrieve process executable', process.pid, exc_info=exc)
224-
process_exe = "[UNKOWN]"
225-
226-
if _Processes._should_ignore_process(process):
227-
logging.info('[Process(%d)] process (%s) will be kept alive', process.pid, process_exe)
172+
process_basename = os.path.basename(info[0])
173+
processes_ignore_patterns = _Processes.get_processes_ignore_patterns()
174+
if any([re.match(p, process_basename, re.IGNORECASE) for p in processes_ignore_patterns]):
175+
logging.info(f'process {pid} {info[0]} will be kept alive')
228176
continue
229-
230-
problematic_processes.append(process)
231-
logging.warning('[Process(%d)] Found problematic process (%s)', process.pid, process_exe)
232-
try:
233-
if (parent_process := process.parent()) is not None:
234-
logging.warning('\tParent is %s (%s)', parent_process.pid, parent_process.exe())
235-
except psutil.Error as exc:
236-
logging.debug(
237-
'[Process(%d)] failed to get parent process information for process "%s"',
238-
process.pid,
239-
process_exe,
240-
exc_info=exc,
241-
)
242-
243-
logging.info('%d processes checked.', checked_processes_count)
244-
if not problematic_processes:
245-
# no problematic processes running, nothing to do.
246-
return True
247-
248-
sleep_time = 5.0
177+
logging.warning('Found problematic process %s (%s)', pid, info[0])
178+
found_problem = True
179+
if info[1] in processes:
180+
logging.warning('Parent is %s (%s)', info[1], processes[info[1]][0])
181+
if env.kill:
182+
logging.info('Killing process…')
183+
nimp.sys.process.call(['wmic', 'process', 'where', 'processid=' + pid, 'delete'])
184+
logging.info('%s processes checked.', len(processes))
249185
if not env.kill:
250-
# Wait a bit, give a chance to problematic processes to end,
251-
# even if not killed
252-
253-
logging.debug("Wait %.2fs. Giving a chance to processes for a natural exit", sleep_time)
254-
time.sleep(sleep_time)
255-
else:
256-
for p in problematic_processes:
257-
if p.is_running():
258-
logging.info('Requesting process %s termination', p.pid)
259-
p.terminate()
260-
_, alive = psutil.wait_procs(problematic_processes, timeout=sleep_time)
261-
for p in alive:
262-
if p.is_running():
263-
logging.info('Process %s not terminated. Send kill.', p.pid)
264-
p.kill()
186+
return not found_problem
187+
if not found_problem:
188+
return True
189+
time.sleep(5)
265190

266191
return False
267192

268193
@staticmethod
269-
def _process_owned_by_user(process: psutil.Process, username: str):
270-
try:
271-
process_user = process.username()
272-
except psutil.Error as exception:
273-
logging.debug(
274-
"[Process(%d)] Failed to retrieve process user",
275-
process.pid,
276-
exc_info=exception,
277-
)
278-
return False
279-
280-
is_same_user = username == process_user
281-
282-
logging.debug(
283-
"[Process(%d)] ignore process from other user (self: %s, process user: %s)",
284-
process.pid,
285-
username,
286-
process_user,
287-
)
288-
289-
return is_same_user
290-
291-
@staticmethod
292-
def _process_matches_filters(process: psutil.Process, filters: list[str]) -> bool:
293-
"""Returns True if the process should be filtered out"""
294-
if not process.is_running():
295-
return False
296-
297-
try:
298-
process_exe = process.exe()
299-
except Exception as exc:
300-
logging.debug("[Process(%d)] Failed to determine process exe!", process.pid, exc_info=exc)
301-
# failed to access a property of the process,
302-
# assume it does not match to be safe
303-
return False
304-
305-
logging.debug("[Process(%d)] Check process against filters", process.pid)
306-
for pattern in filters:
307-
if fnmatch.fnmatch(process_exe, pattern):
308-
logging.debug(
309-
"process %s (%s), match filter '%s' with exe '%s'",
310-
process.pid,
311-
process_exe,
312-
pattern,
313-
process_exe,
314-
)
315-
return True
316-
317-
if not process.is_running():
318-
return False
319-
320-
try:
321-
open_files = process.open_files()
322-
except Exception as exc:
323-
logging.debug("[Process(%d)] Failed to query process open_files", process.pid, exc_info=exc)
324-
# failed to access a property of the process,
325-
# assume it does not match to be safe
326-
return False
327-
328-
for pattern in filters:
329-
for popen_file in open_files:
330-
if fnmatch.fnmatch(popen_file.path, pattern):
331-
logging.debug(
332-
"process %s (%s), match filter '%s' with popen file '%s'",
333-
process.pid,
334-
process_exe,
335-
pattern,
336-
popen_file.path,
337-
)
338-
return True
339-
340-
return False
194+
def get_processes_ignore_patterns():
195+
return [
196+
# r'^CrashReportClient\.exe$',
197+
r'^dotnet\.exe$',
198+
]
341199

342200
@staticmethod
343-
def _should_ignore_process(process: psutil.Process) -> bool:
344-
try:
345-
process_executable_path = process.exe()
346-
process_basename = os.path.basename(process_executable_path)
347-
except psutil.Error:
348-
logging.debug(
349-
"[Process(%d)] failed to retrieve process exe/basename",
350-
process.pid,
351-
)
352-
return True
353-
return any(p.match(process_basename) for p in _Processes.PROCESS_IGNORE_PATTERNS)
201+
def _list_windows_processes():
202+
processes = {}
203+
# List all processes
204+
cmd = ['wmic', 'process', 'get', 'executablepath,parentprocessid,processid', '/value']
205+
result, output, _ = nimp.sys.process.call(cmd, capture_output=True)
206+
if result == 0:
207+
# Build a dictionary of all processes
208+
path, pid, ppid = '', 0, 0
209+
for line in [line.strip() for line in output.splitlines()]:
210+
if line.lower().startswith('executablepath='):
211+
path = re.sub('[^=]*=', '', line)
212+
if line.lower().startswith('parentprocessid='):
213+
ppid = re.sub('[^=]*=', '', line)
214+
if line.lower().startswith('processid='):
215+
pid = re.sub('[^=]*=', '', line)
216+
processes[pid] = (path, ppid)
217+
return processes
354218

355219

356220
class _Disks(CheckCommand):

0 commit comments

Comments
 (0)