Skip to content

Commit d93b3b8

Browse files
authored
Merge pull request #3556 from vkarak/bugfix/proc-resource-leaks
[bugfix] Fix resource leaks in `osext.run_command()` and in `_ProcFuture`
2 parents 3302371 + 4fed708 commit d93b3b8

File tree

3 files changed

+35
-14
lines changed

3 files changed

+35
-14
lines changed

reframe/utility/osext.py

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ class _ProcFuture:
4848

4949
def __init__(self, check=False, *args, **kwargs):
5050
self._proc = None
51+
self._stdout = None
52+
self._stderr = None
5153
self._exitcode = None
5254
self._signal = None
5355
self._check = check
@@ -77,6 +79,17 @@ def start(self):
7779

7880
return self
7981

82+
def _shutdown(self):
83+
'''Shut down cleanly the underlying `Popen` object.'''
84+
if self._stdout is None and self._proc.stdout:
85+
self._stdout = self._proc.stdout.read()
86+
87+
if self._stderr is None and self._proc.stderr:
88+
self._stderr = self._proc.stderr.read()
89+
90+
self._proc.wait()
91+
self._proc.communicate()
92+
8093
@property
8194
def pid(self):
8295
'''The PID of the spawned process.'''
@@ -179,6 +192,7 @@ def _wait(self, *, nohang=False):
179192
except OSError as e:
180193
if e.errno == errno.ECHILD:
181194
self._completed = True
195+
self._shutdown()
182196
return self._completed
183197
else:
184198
raise e
@@ -191,16 +205,19 @@ def _wait(self, *, nohang=False):
191205
elif os.WIFSIGNALED(status):
192206
self._signal = os.WTERMSIG(status)
193207

208+
# Retrieve the stdout/stderr and clean up the underlying Popen object
194209
self._completed = True
210+
self._shutdown()
195211

196212
# Call any done callbacks
197213
for func in self._done_callbacks:
198214
func(self)
199215

200216
# Start the next futures in the chain
201-
for fut, cond in self._next:
202-
if cond(self):
203-
fut.start()
217+
if not self._cancelled:
218+
for fut, cond in self._next:
219+
if cond(self):
220+
fut.start()
204221

205222
return self._completed
206223

@@ -230,29 +247,28 @@ def exception(self):
230247
if not self._check:
231248
return
232249

233-
if self._proc.returncode == 0:
250+
if self._exitcode == 0:
234251
return
235252

236253
return SpawnedProcessError(self._proc.args,
237-
self._proc.stdout.read(),
238-
self._proc.stderr.read(),
239-
self._proc.returncode)
254+
self._stdout, self._stderr,
255+
self._exitcode)
240256

241257
def stdout(self):
242258
'''Retrieve the standard output of the spawned process.
243259
244260
This is a blocking call and will wait until the future finishes.
245261
'''
246262
self._wait()
247-
return self._proc.stdout
263+
return self._stdout
248264

249265
def stderr(self):
250266
'''Retrieve the standard error of the spawned process.
251267
252268
This is a blocking call and will wait until the future finishes.
253269
'''
254270
self._wait()
255-
return self._proc.stderr
271+
return self._stderr
256272

257273

258274
def run_command(cmd, check=False, timeout=None, **kwargs):
@@ -281,9 +297,12 @@ def run_command(cmd, check=False, timeout=None, **kwargs):
281297
proc_stdout, proc_stderr = proc.communicate(timeout=timeout)
282298
except subprocess.TimeoutExpired as e:
283299
os.killpg(proc.pid, signal.SIGKILL)
284-
raise SpawnedProcessTimeout(e.cmd,
285-
proc.stdout.read(),
286-
proc.stderr.read(), timeout) from None
300+
os.waitpid(proc.pid, 0)
301+
proc_stdout = proc.stdout.read()
302+
proc_stderr = proc.stderr.read()
303+
proc.communicate()
304+
raise SpawnedProcessTimeout(e.cmd, proc_stdout,
305+
proc_stderr, timeout) from None
287306

288307
completed = subprocess.CompletedProcess(cmd,
289308
returncode=proc.returncode,

unittests/test_shell.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ def test_trap_signal(script_file):
130130

131131
# We kill the whole spawned process group (we are launching a shell)
132132
os.killpg(proc.pid, 15)
133-
proc.wait()
133+
proc.communicate()
134134

135135
f_stdout.flush()
136136
f_stdout.seek(0)

unittests/test_utility.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ def test_command_async():
7878
t_launch = time.time() - t_launch
7979

8080
proc.wait()
81+
proc.communicate()
8182
t_sleep = time.time() - t_sleep
8283

8384
# Now check the timings
@@ -110,7 +111,7 @@ def test_command_futures():
110111
assert not proc.is_session()
111112

112113
# stdout must block
113-
assert proc.stdout().read() == 'hello\n'
114+
assert proc.stdout() == 'hello\n'
114115
assert proc.exitcode == 0
115116
assert proc.signal is None
116117

@@ -270,6 +271,7 @@ def test_command_futures_chain_cancel():
270271

271272
assert proc1.started()
272273
proc1.cancel()
274+
proc1.wait()
273275
assert proc1.cancelled()
274276
assert not proc2.started()
275277

0 commit comments

Comments
 (0)