Skip to content

Commit d4cc7bf

Browse files
committed
issue6559: Adds a pass_fds parameter to subprocess.Popen that allows the caller
to list exactly which file descriptors should be kept open.
1 parent 1acb746 commit d4cc7bf

File tree

2 files changed

+65
-15
lines changed

2 files changed

+65
-15
lines changed

Lib/subprocess.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,8 @@ def __init__(self, args, bufsize=0, executable=None,
608608
preexec_fn=None, close_fds=None, shell=False,
609609
cwd=None, env=None, universal_newlines=False,
610610
startupinfo=None, creationflags=0,
611-
restore_signals=True, start_new_session=False):
611+
restore_signals=True, start_new_session=False,
612+
pass_fds=()):
612613
"""Create new Popen instance."""
613614
_cleanup()
614615

@@ -644,6 +645,9 @@ def __init__(self, args, bufsize=0, executable=None,
644645
raise ValueError("creationflags is only supported on Windows "
645646
"platforms")
646647

648+
if pass_fds and not close_fds:
649+
raise ValueError("pass_fds requires close_fds=True.")
650+
647651
self.stdin = None
648652
self.stdout = None
649653
self.stderr = None
@@ -671,7 +675,7 @@ def __init__(self, args, bufsize=0, executable=None,
671675
errread, errwrite) = self._get_handles(stdin, stdout, stderr)
672676

673677
self._execute_child(args, executable, preexec_fn, close_fds,
674-
cwd, env, universal_newlines,
678+
pass_fds, cwd, env, universal_newlines,
675679
startupinfo, creationflags, shell,
676680
p2cread, p2cwrite,
677681
c2pread, c2pwrite,
@@ -848,14 +852,16 @@ def _find_w9xpopen(self):
848852

849853

850854
def _execute_child(self, args, executable, preexec_fn, close_fds,
851-
cwd, env, universal_newlines,
855+
pass_fds, cwd, env, universal_newlines,
852856
startupinfo, creationflags, shell,
853857
p2cread, p2cwrite,
854858
c2pread, c2pwrite,
855859
errread, errwrite,
856860
unused_restore_signals, unused_start_new_session):
857861
"""Execute program (MS Windows version)"""
858862

863+
assert not pass_fds, "pass_fds not yet supported on Windows"
864+
859865
if not isinstance(args, str):
860866
args = list2cmdline(args)
861867

@@ -1075,8 +1081,19 @@ def _close_fds(self, but):
10751081
os.closerange(but + 1, MAXFD)
10761082

10771083

1084+
def _close_all_but_a_sorted_few_fds(self, fds_to_keep):
1085+
# precondition: fds_to_keep must be sorted and unique
1086+
start_fd = 3
1087+
for fd in fds_to_keep:
1088+
if fd > start_fd:
1089+
os.closerange(start_fd, fd)
1090+
start_fd = fd + 1
1091+
if start_fd <= MAXFD:
1092+
os.closerange(start_fd, MAXFD)
1093+
1094+
10781095
def _execute_child(self, args, executable, preexec_fn, close_fds,
1079-
cwd, env, universal_newlines,
1096+
pass_fds, cwd, env, universal_newlines,
10801097
startupinfo, creationflags, shell,
10811098
p2cread, p2cwrite,
10821099
c2pread, c2pwrite,
@@ -1124,9 +1141,11 @@ def _execute_child(self, args, executable, preexec_fn, close_fds,
11241141
executable_list = tuple(
11251142
os.path.join(os.fsencode(dir), executable)
11261143
for dir in os.get_exec_path(env))
1144+
fds_to_keep = set(pass_fds)
1145+
fds_to_keep.add(errpipe_write)
11271146
self.pid = _posixsubprocess.fork_exec(
11281147
args, executable_list,
1129-
close_fds, cwd, env_list,
1148+
close_fds, sorted(fds_to_keep), cwd, env_list,
11301149
p2cread, p2cwrite, c2pread, c2pwrite,
11311150
errread, errwrite,
11321151
errpipe_read, errpipe_write,
@@ -1180,7 +1199,14 @@ def _execute_child(self, args, executable, preexec_fn, close_fds,
11801199

11811200
# Close all other fds, if asked for
11821201
if close_fds:
1183-
self._close_fds(but=errpipe_write)
1202+
if pass_fds:
1203+
fds_to_keep = set(pass_fds)
1204+
fds_to_keep.add(errpipe_write)
1205+
self._close_all_but_a_sorted_few_fds(
1206+
sorted(fds_to_keep))
1207+
else:
1208+
self._close_fds(but=errpipe_write)
1209+
11841210

11851211
if cwd is not None:
11861212
os.chdir(cwd)

Modules/_posixsubprocess.c

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ static void child_exec(char *const exec_array[],
4242
int errread, int errwrite,
4343
int errpipe_read, int errpipe_write,
4444
int close_fds, int restore_signals,
45-
int call_setsid,
45+
int call_setsid, Py_ssize_t num_fds_to_keep,
46+
PyObject *py_fds_to_keep,
4647
PyObject *preexec_fn,
4748
PyObject *preexec_fn_args_tuple)
4849
{
@@ -91,11 +92,28 @@ static void child_exec(char *const exec_array[],
9192
/* close() is intentionally not checked for errors here as we are closing */
9293
/* a large range of fds, some of which may be invalid. */
9394
if (close_fds) {
94-
for (fd_num = 3; fd_num < errpipe_write; ++fd_num) {
95-
close(fd_num);
95+
Py_ssize_t keep_seq_idx;
96+
int start_fd = 3;
97+
for (keep_seq_idx = 0; keep_seq_idx < num_fds_to_keep; ++keep_seq_idx) {
98+
PyObject* py_keep_fd = PySequence_Fast_GET_ITEM(py_fds_to_keep,
99+
keep_seq_idx);
100+
int keep_fd = PyLong_AsLong(py_keep_fd);
101+
if (keep_fd < 0) { /* Negative number, overflow or not a Long. */
102+
err_msg = "bad value in fds_to_keep.";
103+
errno = 0; /* We don't want to report an OSError. */
104+
goto error;
105+
}
106+
if (keep_fd <= start_fd)
107+
continue;
108+
for (fd_num = start_fd; fd_num < keep_fd; ++fd_num) {
109+
close(fd_num);
110+
}
111+
start_fd = keep_fd + 1;
96112
}
97-
for (fd_num = errpipe_write+1; fd_num < max_fd; ++fd_num) {
98-
close(fd_num);
113+
if (start_fd <= max_fd) {
114+
for (fd_num = start_fd; fd_num < max_fd; ++fd_num) {
115+
close(fd_num);
116+
}
99117
}
100118
}
101119

@@ -170,7 +188,7 @@ static PyObject *
170188
subprocess_fork_exec(PyObject* self, PyObject *args)
171189
{
172190
PyObject *gc_module = NULL;
173-
PyObject *executable_list, *py_close_fds;
191+
PyObject *executable_list, *py_close_fds, *py_fds_to_keep;
174192
PyObject *env_list, *preexec_fn;
175193
PyObject *process_args, *converted_args = NULL, *fast_args = NULL;
176194
PyObject *preexec_fn_args_tuple = NULL;
@@ -182,11 +200,11 @@ subprocess_fork_exec(PyObject* self, PyObject *args)
182200
pid_t pid;
183201
int need_to_reenable_gc = 0;
184202
char *const *exec_array, *const *argv = NULL, *const *envp = NULL;
185-
Py_ssize_t arg_num;
203+
Py_ssize_t arg_num, num_fds_to_keep;
186204

187205
if (!PyArg_ParseTuple(
188-
args, "OOOOOiiiiiiiiiiO:fork_exec",
189-
&process_args, &executable_list, &py_close_fds,
206+
args, "OOOOOOiiiiiiiiiiO:fork_exec",
207+
&process_args, &executable_list, &py_close_fds, &py_fds_to_keep,
190208
&cwd_obj, &env_list,
191209
&p2cread, &p2cwrite, &c2pread, &c2pwrite,
192210
&errread, &errwrite, &errpipe_read, &errpipe_write,
@@ -198,6 +216,11 @@ subprocess_fork_exec(PyObject* self, PyObject *args)
198216
PyErr_SetString(PyExc_ValueError, "errpipe_write must be >= 3");
199217
return NULL;
200218
}
219+
num_fds_to_keep = PySequence_Length(py_fds_to_keep);
220+
if (num_fds_to_keep < 0) {
221+
PyErr_SetString(PyExc_ValueError, "bad fds_to_keep");
222+
return NULL;
223+
}
201224

202225
/* We need to call gc.disable() when we'll be calling preexec_fn */
203226
if (preexec_fn != Py_None) {
@@ -298,6 +321,7 @@ subprocess_fork_exec(PyObject* self, PyObject *args)
298321
p2cread, p2cwrite, c2pread, c2pwrite,
299322
errread, errwrite, errpipe_read, errpipe_write,
300323
close_fds, restore_signals, call_setsid,
324+
num_fds_to_keep, py_fds_to_keep,
301325
preexec_fn, preexec_fn_args_tuple);
302326
_exit(255);
303327
return NULL; /* Dead code to avoid a potential compiler warning. */

0 commit comments

Comments
 (0)