1717import os
1818import resource
1919import selectors
20+ import shutil
2021import signal
2122from pathlib import Path
2223from subprocess import Popen
23- from typing import Dict , List , Optional , Tuple , cast
24+ from typing import Dict , List , Optional , Tuple , Union , cast
2425
2526from granulate_utils .linux .ns import is_running_in_init_pid
2627from psutil import NoSuchProcess , Process
@@ -86,6 +87,10 @@ def __init__(
8687 self ._kernel_offsets : Dict [str , int ] = {}
8788 self ._metadata = python .PythonMetadata (self ._profiler_state .stop_event )
8889 self ._verbose = verbose
90+ self ._pyperf_staticx_tmpdir : Optional [Path ] = None
91+ if os .environ .get ("TMPDIR" , None ) is not None :
92+ # We want to create a new level of hirerachy in our current staticx tempdir.
93+ self ._pyperf_staticx_tmpdir = Path (os .environ ["TMPDIR" ]) / ("pyperf_" + random_prefix ())
8994
9095 @classmethod
9196 def _check_output (cls , process : Popen , output_path : Path ) -> None :
@@ -153,6 +158,14 @@ def _pyperf_base_command(self) -> List[str]:
153158 cmd .extend (["-v" , "4" ])
154159 return cmd
155160
161+ def _staticx_cleanup (self ) -> None :
162+ """
163+ We experienced issues with PyPerf's staticx'd directory, so we want to clean it up on termination.
164+ """
165+ assert not self .is_running (), "_staticx_cleanup is impossible while PyPerf is running"
166+ if self ._pyperf_staticx_tmpdir is not None and self ._pyperf_staticx_tmpdir .exists ():
167+ shutil .rmtree (self ._pyperf_staticx_tmpdir .as_posix ())
168+
156169 def test (self ) -> None :
157170 self ._ebpf_environment ()
158171
@@ -170,14 +183,16 @@ def test(self) -> None:
170183 "--duration" ,
171184 "1" ,
172185 ]
173- process = start_process (cmd )
186+ process = start_process (cmd , tmpdir = self . _pyperf_staticx_tmpdir )
174187 try :
175188 poll_process (process , self ._POLL_TIMEOUT , self ._profiler_state .stop_event )
176189 except TimeoutError :
177190 process .kill ()
178191 raise
179192 else :
180193 self ._check_output (process , self .output_path )
194+ finally :
195+ self ._staticx_cleanup ()
181196
182197 def start (self ) -> None :
183198 logger .info ("Starting profiling of Python processes with PyPerf" )
@@ -201,7 +216,7 @@ def start(self) -> None:
201216 for f in glob .glob (f"{ str (self .output_path )} .*" ):
202217 os .unlink (f )
203218
204- process = start_process (cmd )
219+ process = start_process (cmd , tmpdir = self . _pyperf_staticx_tmpdir )
205220 # wait until the transient data file appears - because once returning from here, PyPerf may
206221 # be polled via snapshot() and we need it to finish installing its signal handler.
207222 try :
@@ -212,6 +227,7 @@ def start(self) -> None:
212227 stdout = process .stdout .read ()
213228 stderr = process .stderr .read ()
214229 logger .error ("PyPerf failed to start" , stdout = stdout , stderr = stderr )
230+ self ._staticx_cleanup ()
215231 raise
216232 else :
217233 self .process = process
@@ -303,16 +319,23 @@ def snapshot(self) -> ProcessToProfileData:
303319 return profiles
304320
305321 def _terminate (self ) -> Tuple [Optional [int ], str , str ]:
322+ exit_status : Optional [int ] = None
323+ stdout : Union [str , bytes ] = ""
324+ stderr : Union [str , bytes ] = ""
325+
306326 self ._unregister_process_selectors ()
307327 if self .is_running ():
308328 assert self .process is not None # for mypy
309329 self .process .terminate () # okay to call even if process is already dead
310330 exit_status , stdout , stderr = reap_process (self .process )
311331 self .process = None
312- return exit_status , stdout .decode (), stderr .decode ()
332+
333+ stdout = stdout .decode () if isinstance (stdout , bytes ) else stdout
334+ stderr = stderr .decode () if isinstance (stderr , bytes ) else stderr
313335
314336 assert self .process is None # means we're not running
315- return None , "" , ""
337+ self ._staticx_cleanup ()
338+ return exit_status , stdout , stderr
316339
317340 def stop (self ) -> None :
318341 exit_status , stdout , stderr = self ._terminate ()
0 commit comments