Skip to content

Commit 1e5b6ea

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 1e5b6ea

File tree

1 file changed

+50
-184
lines changed

1 file changed

+50
-184
lines changed

nimp/base_commands/check.py

Lines changed: 50 additions & 184 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,13 @@ class Check(nimp.command.CommandGroup):
4747
'''Check related commands'''
4848

4949
def __init__(self):
50-
super(Check, self).__init__([_Status(), _Processes(), _Disks()])
50+
super(Check, self).__init__(
51+
[
52+
_Status(),
53+
_Processes(),
54+
_Disks(),
55+
]
56+
)
5157

5258
def is_available(self, env):
5359
return True, ''
@@ -135,11 +141,6 @@ def _show_nimp_environment(env):
135141

136142

137143
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-
143144
def configure_arguments(self, env, parser):
144145
parser.add_argument('-k', '--kill', help='Kill processes that can prevent builds', action='store_true')
145146
parser.add_argument(
@@ -149,11 +150,6 @@ def configure_arguments(self, env, parser):
149150
help='fnmatch filters, defaults to workspace',
150151
default=[os.path.normpath(f'{os.path.abspath(env.root_dir)}/*')],
151152
)
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-
)
157153
return True
158154

159155
def _run_check(self, env: NimpEnvironment):
@@ -165,192 +161,62 @@ def _run_check(self, env: NimpEnvironment):
165161
logging.warning("Command only available on Windows platform")
166162
return True
167163

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
164+
# Find all running binaries launched from the project directory
176165
# and optionally kill them, unless they’re in the exception list.
177166
# We get to try 5 times just in case
178167
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
168+
found_problem = False
169+
processes = _Processes._list_windows_processes()
206170

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)
171+
for pid, info in processes.items():
172+
if not any(fnmatch.fnmatch(info[0], filter) for filter in env.filters):
210173
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)
174+
process_basename = os.path.basename(info[0])
175+
processes_ignore_patterns = _Processes.get_processes_ignore_patterns()
176+
if any([re.match(p, process_basename, re.IGNORECASE) for p in processes_ignore_patterns]):
177+
logging.info(f'process {pid} {info[0]} will be kept alive')
228178
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
179+
logging.warning('Found problematic process %s (%s)', pid, info[0])
180+
found_problem = True
181+
if info[1] in processes:
182+
logging.warning('Parent is %s (%s)', info[1], processes[info[1]][0])
183+
if env.kill:
184+
logging.info('Killing process…')
185+
nimp.sys.process.call(['wmic', 'process', 'where', 'processid=' + pid, 'delete'])
186+
logging.info('%s processes checked.', len(processes))
249187
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()
188+
return not found_problem
189+
if not found_problem:
190+
return True
191+
time.sleep(5)
265192

266193
return False
267194

268195
@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
196+
def get_processes_ignore_patterns():
197+
return [
198+
# r'^CrashReportClient\.exe$',
199+
r'^dotnet\.exe$',
200+
]
341201

342202
@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)
203+
def _list_windows_processes():
204+
processes = {}
205+
# List all processes
206+
cmd = ['wmic', 'process', 'get', 'executablepath,parentprocessid,processid', '/value']
207+
result, output, _ = nimp.sys.process.call(cmd, capture_output=True)
208+
if result == 0:
209+
# Build a dictionary of all processes
210+
path, pid, ppid = '', 0, 0
211+
for line in [line.strip() for line in output.splitlines()]:
212+
if line.lower().startswith('executablepath='):
213+
path = re.sub('[^=]*=', '', line)
214+
if line.lower().startswith('parentprocessid='):
215+
ppid = re.sub('[^=]*=', '', line)
216+
if line.lower().startswith('processid='):
217+
pid = re.sub('[^=]*=', '', line)
218+
processes[pid] = (path, ppid)
219+
return processes
354220

355221

356222
class _Disks(CheckCommand):

0 commit comments

Comments
 (0)