@@ -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
137143class _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 ('\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
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
356222class _Disks (CheckCommand ):
0 commit comments