3333import re
3434import shutil
3535import time
36- from typing import Sequence
3736
38- import psutil
3937
4038import nimp .command
4139import 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
137141class _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 ('\t Parent 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
356220class _Disks (CheckCommand ):
0 commit comments