Skip to content

Commit ef34753

Browse files
bpo-20104: Improve error handling and fix a reference leak in os.posix_spawn(). (#6332)
1 parent 7508a54 commit ef34753

File tree

5 files changed

+313
-152
lines changed

5 files changed

+313
-152
lines changed

Doc/library/os.rst

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3365,25 +3365,41 @@ written in Python, such as a mail server's external command delivery program.
33653365

33663366
.. function:: posix_spawn(path, argv, env, file_actions=None)
33673367

3368-
Wraps the posix_spawn() C library API for use from Python.
3368+
Wraps the :c:func:`posix_spawn` C library API for use from Python.
33693369

3370-
Most users should use :class:`subprocess.run` instead of posix_spawn.
3370+
Most users should use :func:`subprocess.run` instead of :func:`posix_spawn`.
33713371

33723372
The *path*, *args*, and *env* arguments are similar to :func:`execve`.
33733373

33743374
The *file_actions* argument may be a sequence of tuples describing actions
33753375
to take on specific file descriptors in the child process between the C
3376-
library implementation's fork and exec steps. The first item in each tuple
3377-
must be one of the three type indicator listed below describing the
3378-
remaining tuple elements:
3376+
library implementation's :c:func:`fork` and :c:func:`exec` steps.
3377+
The first item in each tuple must be one of the three type indicator
3378+
listed below describing the remaining tuple elements:
33793379

3380-
(os.POSIX_SPAWN_OPEN, fd, path, open flags, mode)
3381-
(os.POSIX_SPAWN_CLOSE, fd)
3382-
(os.POSIX_SPAWN_DUP2, fd, new_fd)
3380+
.. data:: POSIX_SPAWN_OPEN
33833381

3384-
These tuples correspond to the C library posix_spawn_file_actions_addopen,
3385-
posix_spawn_file_actions_addclose, and posix_spawn_file_actions_adddup2 API
3386-
calls used to prepare for the posix_spawn call itself.
3382+
(``os.POSIX_SPAWN_OPEN``, *fd*, *path*, *flags*, *mode*)
3383+
3384+
Performs ``os.dup2(os.open(path, flags, mode), fd)``.
3385+
3386+
.. data:: POSIX_SPAWN_CLOSE
3387+
3388+
(``os.POSIX_SPAWN_CLOSE``, *fd*)
3389+
3390+
Performs ``os.close(fd)``.
3391+
3392+
.. data:: POSIX_SPAWN_DUP2
3393+
3394+
(``os.POSIX_SPAWN_DUP2``, *fd*, *new_fd*)
3395+
3396+
Performs ``os.dup2(fd, new_fd)``.
3397+
3398+
These tuples correspond to the C library
3399+
:c:func:`posix_spawn_file_actions_addopen`,
3400+
:c:func:`posix_spawn_file_actions_addclose`, and
3401+
:c:func:`posix_spawn_file_actions_adddup2` API calls used to prepare
3402+
for the :c:func:`posix_spawn` call itself.
33873403

33883404
.. versionadded:: 3.7
33893405

Lib/test/test_posix.py

Lines changed: 160 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -177,22 +177,6 @@ def test_fexecve(self):
177177
os.close(fp)
178178

179179

180-
@unittest.skipUnless(hasattr(os, 'posix_spawn'), "test needs os.posix_spawn")
181-
def test_posix_spawn(self):
182-
pid = posix.posix_spawn(sys.executable, [sys.executable, "-c", "pass"], os.environ,[])
183-
self.assertEqual(os.waitpid(pid,0),(pid,0))
184-
185-
186-
@unittest.skipUnless(hasattr(os, 'posix_spawn'), "test needs os.posix_spawn")
187-
def test_posix_spawn_file_actions(self):
188-
file_actions = []
189-
file_actions.append((0,3,os.path.realpath(__file__),0,0))
190-
file_actions.append((os.POSIX_SPAWN_CLOSE,2))
191-
file_actions.append((os.POSIX_SPAWN_DUP2,1,4))
192-
pid = posix.posix_spawn(sys.executable, [sys.executable, "-c", "pass"], os.environ, file_actions)
193-
self.assertEqual(os.waitpid(pid,0),(pid,0))
194-
195-
196180
@unittest.skipUnless(hasattr(posix, 'waitid'), "test needs posix.waitid()")
197181
@unittest.skipUnless(hasattr(os, 'fork'), "test needs os.fork()")
198182
def test_waitid(self):
@@ -1437,9 +1421,168 @@ def test_setgroups(self):
14371421
posix.setgroups(groups)
14381422
self.assertListEqual(groups, posix.getgroups())
14391423

1424+
1425+
@unittest.skipUnless(hasattr(os, 'posix_spawn'), "test needs os.posix_spawn")
1426+
class TestPosixSpawn(unittest.TestCase):
1427+
def test_returns_pid(self):
1428+
pidfile = support.TESTFN
1429+
self.addCleanup(support.unlink, pidfile)
1430+
script = f"""if 1:
1431+
import os
1432+
with open({pidfile!r}, "w") as pidfile:
1433+
pidfile.write(str(os.getpid()))
1434+
"""
1435+
pid = posix.posix_spawn(sys.executable,
1436+
[sys.executable, '-c', script],
1437+
os.environ)
1438+
self.assertEqual(os.waitpid(pid, 0), (pid, 0))
1439+
with open(pidfile) as f:
1440+
self.assertEqual(f.read(), str(pid))
1441+
1442+
def test_no_such_executable(self):
1443+
no_such_executable = 'no_such_executable'
1444+
try:
1445+
pid = posix.posix_spawn(no_such_executable,
1446+
[no_such_executable],
1447+
os.environ)
1448+
except FileNotFoundError as exc:
1449+
self.assertEqual(exc.filename, no_such_executable)
1450+
else:
1451+
pid2, status = os.waitpid(pid, 0)
1452+
self.assertEqual(pid2, pid)
1453+
self.assertNotEqual(status, 0)
1454+
1455+
def test_specify_environment(self):
1456+
envfile = support.TESTFN
1457+
self.addCleanup(support.unlink, envfile)
1458+
script = f"""if 1:
1459+
import os
1460+
with open({envfile!r}, "w") as envfile:
1461+
envfile.write(os.environ['foo'])
1462+
"""
1463+
pid = posix.posix_spawn(sys.executable,
1464+
[sys.executable, '-c', script],
1465+
{'foo': 'bar'})
1466+
self.assertEqual(os.waitpid(pid, 0), (pid, 0))
1467+
with open(envfile) as f:
1468+
self.assertEqual(f.read(), 'bar')
1469+
1470+
def test_empty_file_actions(self):
1471+
pid = posix.posix_spawn(
1472+
sys.executable,
1473+
[sys.executable, '-c', 'pass'],
1474+
os.environ,
1475+
[]
1476+
)
1477+
self.assertEqual(os.waitpid(pid, 0), (pid, 0))
1478+
1479+
def test_multiple_file_actions(self):
1480+
file_actions = [
1481+
(os.POSIX_SPAWN_OPEN, 3, os.path.realpath(__file__), os.O_RDONLY, 0),
1482+
(os.POSIX_SPAWN_CLOSE, 0),
1483+
(os.POSIX_SPAWN_DUP2, 1, 4),
1484+
]
1485+
pid = posix.posix_spawn(sys.executable,
1486+
[sys.executable, "-c", "pass"],
1487+
os.environ, file_actions)
1488+
self.assertEqual(os.waitpid(pid, 0), (pid, 0))
1489+
1490+
def test_bad_file_actions(self):
1491+
with self.assertRaises(TypeError):
1492+
posix.posix_spawn(sys.executable,
1493+
[sys.executable, "-c", "pass"],
1494+
os.environ, [None])
1495+
with self.assertRaises(TypeError):
1496+
posix.posix_spawn(sys.executable,
1497+
[sys.executable, "-c", "pass"],
1498+
os.environ, [()])
1499+
with self.assertRaises(TypeError):
1500+
posix.posix_spawn(sys.executable,
1501+
[sys.executable, "-c", "pass"],
1502+
os.environ, [(None,)])
1503+
with self.assertRaises(TypeError):
1504+
posix.posix_spawn(sys.executable,
1505+
[sys.executable, "-c", "pass"],
1506+
os.environ, [(12345,)])
1507+
with self.assertRaises(TypeError):
1508+
posix.posix_spawn(sys.executable,
1509+
[sys.executable, "-c", "pass"],
1510+
os.environ, [(os.POSIX_SPAWN_CLOSE,)])
1511+
with self.assertRaises(TypeError):
1512+
posix.posix_spawn(sys.executable,
1513+
[sys.executable, "-c", "pass"],
1514+
os.environ, [(os.POSIX_SPAWN_CLOSE, 1, 2)])
1515+
with self.assertRaises(TypeError):
1516+
posix.posix_spawn(sys.executable,
1517+
[sys.executable, "-c", "pass"],
1518+
os.environ, [(os.POSIX_SPAWN_CLOSE, None)])
1519+
with self.assertRaises(ValueError):
1520+
posix.posix_spawn(sys.executable,
1521+
[sys.executable, "-c", "pass"],
1522+
os.environ,
1523+
[(os.POSIX_SPAWN_OPEN, 3, __file__ + '\0',
1524+
os.O_RDONLY, 0)])
1525+
1526+
def test_open_file(self):
1527+
outfile = support.TESTFN
1528+
self.addCleanup(support.unlink, outfile)
1529+
script = """if 1:
1530+
import sys
1531+
sys.stdout.write("hello")
1532+
"""
1533+
file_actions = [
1534+
(os.POSIX_SPAWN_OPEN, 1, outfile,
1535+
os.O_WRONLY | os.O_CREAT | os.O_TRUNC,
1536+
stat.S_IRUSR | stat.S_IWUSR),
1537+
]
1538+
pid = posix.posix_spawn(sys.executable,
1539+
[sys.executable, '-c', script],
1540+
os.environ, file_actions)
1541+
self.assertEqual(os.waitpid(pid, 0), (pid, 0))
1542+
with open(outfile) as f:
1543+
self.assertEqual(f.read(), 'hello')
1544+
1545+
def test_close_file(self):
1546+
closefile = support.TESTFN
1547+
self.addCleanup(support.unlink, closefile)
1548+
script = f"""if 1:
1549+
import os
1550+
try:
1551+
os.fstat(0)
1552+
except OSError as e:
1553+
with open({closefile!r}, 'w') as closefile:
1554+
closefile.write('is closed %d' % e.errno)
1555+
"""
1556+
pid = posix.posix_spawn(sys.executable,
1557+
[sys.executable, '-c', script],
1558+
os.environ,
1559+
[(os.POSIX_SPAWN_CLOSE, 0),])
1560+
self.assertEqual(os.waitpid(pid, 0), (pid, 0))
1561+
with open(closefile) as f:
1562+
self.assertEqual(f.read(), 'is closed %d' % errno.EBADF)
1563+
1564+
def test_dup2(self):
1565+
dupfile = support.TESTFN
1566+
self.addCleanup(support.unlink, dupfile)
1567+
script = """if 1:
1568+
import sys
1569+
sys.stdout.write("hello")
1570+
"""
1571+
with open(dupfile, "wb") as childfile:
1572+
file_actions = [
1573+
(os.POSIX_SPAWN_DUP2, childfile.fileno(), 1),
1574+
]
1575+
pid = posix.posix_spawn(sys.executable,
1576+
[sys.executable, '-c', script],
1577+
os.environ, file_actions)
1578+
self.assertEqual(os.waitpid(pid, 0), (pid, 0))
1579+
with open(dupfile) as f:
1580+
self.assertEqual(f.read(), 'hello')
1581+
1582+
14401583
def test_main():
14411584
try:
1442-
support.run_unittest(PosixTester, PosixGroupsTester)
1585+
support.run_unittest(PosixTester, PosixGroupsTester, TestPosixSpawn)
14431586
finally:
14441587
support.reap_children()
14451588

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improved error handling and fixed a reference leak in :func:`os.posix_spawn()`.

Modules/clinic/posixmodule.c.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1742,7 +1742,7 @@ PyDoc_STRVAR(os_posix_spawn__doc__,
17421742
" env\n"
17431743
" Dictionary of strings mapping to strings.\n"
17441744
" file_actions\n"
1745-
" FileActions object.");
1745+
" A sequence of file action tuples.");
17461746

17471747
#define OS_POSIX_SPAWN_METHODDEF \
17481748
{"posix_spawn", (PyCFunction)os_posix_spawn, METH_FASTCALL, os_posix_spawn__doc__},
@@ -6589,4 +6589,4 @@ os_getrandom(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject
65896589
#ifndef OS_GETRANDOM_METHODDEF
65906590
#define OS_GETRANDOM_METHODDEF
65916591
#endif /* !defined(OS_GETRANDOM_METHODDEF) */
6592-
/*[clinic end generated code: output=fc603214822bdda6 input=a9049054013a1b77]*/
6592+
/*[clinic end generated code: output=8d3d9dddf254c3c2 input=a9049054013a1b77]*/

0 commit comments

Comments
 (0)