Skip to content

Commit

Permalink
bpo-35674: Add os.posix_spawnp() (pythonGH-11554)
Browse files Browse the repository at this point in the history
Add a new os.posix_spawnp() function.
  • Loading branch information
nanjekyejoannah authored and vstinner committed Jan 16, 2019
1 parent 9daecf3 commit 92b8322
Show file tree
Hide file tree
Showing 9 changed files with 322 additions and 112 deletions.
17 changes: 17 additions & 0 deletions Doc/library/os.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3396,6 +3396,10 @@ written in Python, such as a mail server's external command delivery program.
The positional-only arguments *path*, *args*, and *env* are similar to
:func:`execve`.

The *path* parameter is the path to the executable file.The *path* should
contain a directory.Use :func:`posix_spawnp` to pass an executable file
without directory.

The *file_actions* argument may be a sequence of tuples describing actions
to take on specific file descriptors in the child process between the C
library implementation's :c:func:`fork` and :c:func:`exec` steps.
Expand Down Expand Up @@ -3459,6 +3463,19 @@ written in Python, such as a mail server's external command delivery program.
.. versionadded:: 3.7


.. function:: posix_spawnp(path, argv, env, *, file_actions=None, \
setpgroup=None, resetids=False, setsigmask=(), \
setsigdef=(), scheduler=None)

Wraps the :c:func:`posix_spawnp` C library API for use from Python.

Similar to :func:`posix_spawn` except that the system searches
for the *executable* file in the list of directories specified by the
:envvar:`PATH` environment variable (in the same way as for ``execvp(3)``).

.. versionadded:: 3.8


.. function:: register_at_fork(*, before=None, after_in_parent=None, \
after_in_child=None)

Expand Down
190 changes: 119 additions & 71 deletions Lib/test/test_posix.py
Original file line number Diff line number Diff line change
Expand Up @@ -1489,10 +1489,10 @@ def test_setgroups(self):
self.assertListEqual(groups, posix.getgroups())


@unittest.skipUnless(hasattr(os, 'posix_spawn'), "test needs os.posix_spawn")
class TestPosixSpawn(unittest.TestCase):
# Program which does nothing and exit with status 0 (success)
class _PosixSpawnMixin:
# Program which does nothing and exits with status 0 (success)
NOOP_PROGRAM = (sys.executable, '-I', '-S', '-c', 'pass')
spawn_func = None

def python_args(self, *args):
# Disable site module to avoid side effects. For example,
Expand All @@ -1511,17 +1511,17 @@ def test_returns_pid(self):
pidfile.write(str(os.getpid()))
"""
args = self.python_args('-c', script)
pid = posix.posix_spawn(args[0], args, os.environ)
pid = self.spawn_func(args[0], args, os.environ)
self.assertEqual(os.waitpid(pid, 0), (pid, 0))
with open(pidfile) as f:
self.assertEqual(f.read(), str(pid))

def test_no_such_executable(self):
no_such_executable = 'no_such_executable'
try:
pid = posix.posix_spawn(no_such_executable,
[no_such_executable],
os.environ)
pid = self.spawn_func(no_such_executable,
[no_such_executable],
os.environ)
except FileNotFoundError as exc:
self.assertEqual(exc.filename, no_such_executable)
else:
Expand All @@ -1538,14 +1538,14 @@ def test_specify_environment(self):
envfile.write(os.environ['foo'])
"""
args = self.python_args('-c', script)
pid = posix.posix_spawn(args[0], args,
{**os.environ, 'foo': 'bar'})
pid = self.spawn_func(args[0], args,
{**os.environ, 'foo': 'bar'})
self.assertEqual(os.waitpid(pid, 0), (pid, 0))
with open(envfile) as f:
self.assertEqual(f.read(), 'bar')

def test_empty_file_actions(self):
pid = posix.posix_spawn(
pid = self.spawn_func(
self.NOOP_PROGRAM[0],
self.NOOP_PROGRAM,
os.environ,
Expand All @@ -1554,7 +1554,7 @@ def test_empty_file_actions(self):
self.assertEqual(os.waitpid(pid, 0), (pid, 0))

def test_resetids_explicit_default(self):
pid = posix.posix_spawn(
pid = self.spawn_func(
sys.executable,
[sys.executable, '-c', 'pass'],
os.environ,
Expand All @@ -1563,7 +1563,7 @@ def test_resetids_explicit_default(self):
self.assertEqual(os.waitpid(pid, 0), (pid, 0))

def test_resetids(self):
pid = posix.posix_spawn(
pid = self.spawn_func(
sys.executable,
[sys.executable, '-c', 'pass'],
os.environ,
Expand All @@ -1573,12 +1573,12 @@ def test_resetids(self):

def test_resetids_wrong_type(self):
with self.assertRaises(TypeError):
posix.posix_spawn(sys.executable,
[sys.executable, "-c", "pass"],
os.environ, resetids=None)
self.spawn_func(sys.executable,
[sys.executable, "-c", "pass"],
os.environ, resetids=None)

def test_setpgroup(self):
pid = posix.posix_spawn(
pid = self.spawn_func(
sys.executable,
[sys.executable, '-c', 'pass'],
os.environ,
Expand All @@ -1588,9 +1588,9 @@ def test_setpgroup(self):

def test_setpgroup_wrong_type(self):
with self.assertRaises(TypeError):
posix.posix_spawn(sys.executable,
[sys.executable, "-c", "pass"],
os.environ, setpgroup="023")
self.spawn_func(sys.executable,
[sys.executable, "-c", "pass"],
os.environ, setpgroup="023")

@unittest.skipUnless(hasattr(signal, 'pthread_sigmask'),
'need signal.pthread_sigmask()')
Expand All @@ -1599,7 +1599,7 @@ def test_setsigmask(self):
import signal
signal.raise_signal(signal.SIGUSR1)""")

pid = posix.posix_spawn(
pid = self.spawn_func(
sys.executable,
[sys.executable, '-c', code],
os.environ,
Expand All @@ -1609,18 +1609,18 @@ def test_setsigmask(self):

def test_setsigmask_wrong_type(self):
with self.assertRaises(TypeError):
posix.posix_spawn(sys.executable,
[sys.executable, "-c", "pass"],
os.environ, setsigmask=34)
self.spawn_func(sys.executable,
[sys.executable, "-c", "pass"],
os.environ, setsigmask=34)
with self.assertRaises(TypeError):
posix.posix_spawn(sys.executable,
[sys.executable, "-c", "pass"],
os.environ, setsigmask=["j"])
self.spawn_func(sys.executable,
[sys.executable, "-c", "pass"],
os.environ, setsigmask=["j"])
with self.assertRaises(ValueError):
posix.posix_spawn(sys.executable,
[sys.executable, "-c", "pass"],
os.environ, setsigmask=[signal.NSIG,
signal.NSIG+1])
self.spawn_func(sys.executable,
[sys.executable, "-c", "pass"],
os.environ, setsigmask=[signal.NSIG,
signal.NSIG+1])

@unittest.skipUnless(hasattr(signal, 'pthread_sigmask'),
'need signal.pthread_sigmask()')
Expand All @@ -1630,7 +1630,7 @@ def test_setsigdef(self):
import signal
signal.raise_signal(signal.SIGUSR1)""")
try:
pid = posix.posix_spawn(
pid = self.spawn_func(
sys.executable,
[sys.executable, '-c', code],
os.environ,
Expand All @@ -1646,17 +1646,17 @@ def test_setsigdef(self):

def test_setsigdef_wrong_type(self):
with self.assertRaises(TypeError):
posix.posix_spawn(sys.executable,
[sys.executable, "-c", "pass"],
os.environ, setsigdef=34)
self.spawn_func(sys.executable,
[sys.executable, "-c", "pass"],
os.environ, setsigdef=34)
with self.assertRaises(TypeError):
posix.posix_spawn(sys.executable,
[sys.executable, "-c", "pass"],
os.environ, setsigdef=["j"])
self.spawn_func(sys.executable,
[sys.executable, "-c", "pass"],
os.environ, setsigdef=["j"])
with self.assertRaises(ValueError):
posix.posix_spawn(sys.executable,
[sys.executable, "-c", "pass"],
os.environ, setsigdef=[signal.NSIG, signal.NSIG+1])
self.spawn_func(sys.executable,
[sys.executable, "-c", "pass"],
os.environ, setsigdef=[signal.NSIG, signal.NSIG+1])

@requires_sched
@unittest.skipIf(sys.platform.startswith(('freebsd', 'netbsd')),
Expand All @@ -1670,7 +1670,7 @@ def test_setscheduler_only_param(self):
sys.exit(101)
if os.sched_getparam(0).sched_priority != {priority}:
sys.exit(102)""")
pid = posix.posix_spawn(
pid = self.spawn_func(
sys.executable,
[sys.executable, '-c', code],
os.environ,
Expand All @@ -1690,7 +1690,7 @@ def test_setscheduler_with_policy(self):
sys.exit(101)
if os.sched_getparam(0).sched_priority != {priority}:
sys.exit(102)""")
pid = posix.posix_spawn(
pid = self.spawn_func(
sys.executable,
[sys.executable, '-c', code],
os.environ,
Expand All @@ -1704,40 +1704,40 @@ def test_multiple_file_actions(self):
(os.POSIX_SPAWN_CLOSE, 0),
(os.POSIX_SPAWN_DUP2, 1, 4),
]
pid = posix.posix_spawn(self.NOOP_PROGRAM[0],
self.NOOP_PROGRAM,
os.environ,
file_actions=file_actions)
pid = self.spawn_func(self.NOOP_PROGRAM[0],
self.NOOP_PROGRAM,
os.environ,
file_actions=file_actions)
self.assertEqual(os.waitpid(pid, 0), (pid, 0))

def test_bad_file_actions(self):
args = self.NOOP_PROGRAM
with self.assertRaises(TypeError):
posix.posix_spawn(args[0], args, os.environ,
file_actions=[None])
self.spawn_func(args[0], args, os.environ,
file_actions=[None])
with self.assertRaises(TypeError):
posix.posix_spawn(args[0], args, os.environ,
file_actions=[()])
self.spawn_func(args[0], args, os.environ,
file_actions=[()])
with self.assertRaises(TypeError):
posix.posix_spawn(args[0], args, os.environ,
file_actions=[(None,)])
self.spawn_func(args[0], args, os.environ,
file_actions=[(None,)])
with self.assertRaises(TypeError):
posix.posix_spawn(args[0], args, os.environ,
file_actions=[(12345,)])
self.spawn_func(args[0], args, os.environ,
file_actions=[(12345,)])
with self.assertRaises(TypeError):
posix.posix_spawn(args[0], args, os.environ,
file_actions=[(os.POSIX_SPAWN_CLOSE,)])
self.spawn_func(args[0], args, os.environ,
file_actions=[(os.POSIX_SPAWN_CLOSE,)])
with self.assertRaises(TypeError):
posix.posix_spawn(args[0], args, os.environ,
file_actions=[(os.POSIX_SPAWN_CLOSE, 1, 2)])
self.spawn_func(args[0], args, os.environ,
file_actions=[(os.POSIX_SPAWN_CLOSE, 1, 2)])
with self.assertRaises(TypeError):
posix.posix_spawn(args[0], args, os.environ,
file_actions=[(os.POSIX_SPAWN_CLOSE, None)])
self.spawn_func(args[0], args, os.environ,
file_actions=[(os.POSIX_SPAWN_CLOSE, None)])
with self.assertRaises(ValueError):
posix.posix_spawn(args[0], args, os.environ,
file_actions=[(os.POSIX_SPAWN_OPEN,
3, __file__ + '\0',
os.O_RDONLY, 0)])
self.spawn_func(args[0], args, os.environ,
file_actions=[(os.POSIX_SPAWN_OPEN,
3, __file__ + '\0',
os.O_RDONLY, 0)])

def test_open_file(self):
outfile = support.TESTFN
Expand All @@ -1752,8 +1752,8 @@ def test_open_file(self):
stat.S_IRUSR | stat.S_IWUSR),
]
args = self.python_args('-c', script)
pid = posix.posix_spawn(args[0], args, os.environ,
file_actions=file_actions)
pid = self.spawn_func(args[0], args, os.environ,
file_actions=file_actions)
self.assertEqual(os.waitpid(pid, 0), (pid, 0))
with open(outfile) as f:
self.assertEqual(f.read(), 'hello')
Expand All @@ -1770,8 +1770,8 @@ def test_close_file(self):
closefile.write('is closed %d' % e.errno)
"""
args = self.python_args('-c', script)
pid = posix.posix_spawn(args[0], args, os.environ,
file_actions=[(os.POSIX_SPAWN_CLOSE, 0),])
pid = self.spawn_func(args[0], args, os.environ,
file_actions=[(os.POSIX_SPAWN_CLOSE, 0)])
self.assertEqual(os.waitpid(pid, 0), (pid, 0))
with open(closefile) as f:
self.assertEqual(f.read(), 'is closed %d' % errno.EBADF)
Expand All @@ -1788,16 +1788,64 @@ def test_dup2(self):
(os.POSIX_SPAWN_DUP2, childfile.fileno(), 1),
]
args = self.python_args('-c', script)
pid = posix.posix_spawn(args[0], args, os.environ,
file_actions=file_actions)
pid = self.spawn_func(args[0], args, os.environ,
file_actions=file_actions)
self.assertEqual(os.waitpid(pid, 0), (pid, 0))
with open(dupfile) as f:
self.assertEqual(f.read(), 'hello')


@unittest.skipUnless(hasattr(os, 'posix_spawn'), "test needs os.posix_spawn")
class TestPosixSpawn(unittest.TestCase, _PosixSpawnMixin):
spawn_func = getattr(posix, 'posix_spawn', None)


@unittest.skipUnless(hasattr(os, 'posix_spawnp'), "test needs os.posix_spawnp")
class TestPosixSpawnP(unittest.TestCase, _PosixSpawnMixin):
spawn_func = getattr(posix, 'posix_spawnp', None)

@support.skip_unless_symlink
def test_posix_spawnp(self):
# Use a symlink to create a program in its own temporary directory
temp_dir = tempfile.mkdtemp()
self.addCleanup(support.rmtree, temp_dir)

program = 'posix_spawnp_test_program.exe'
program_fullpath = os.path.join(temp_dir, program)
os.symlink(sys.executable, program_fullpath)

try:
path = os.pathsep.join((temp_dir, os.environ['PATH']))
except KeyError:
path = temp_dir # PATH is not set

spawn_args = (program, '-I', '-S', '-c', 'pass')
code = textwrap.dedent("""
import os
args = %a
pid = os.posix_spawnp(args[0], args, os.environ)
pid2, status = os.waitpid(pid, 0)
if pid2 != pid:
raise Exception(f"pid {pid2} != {pid}")
if status != 0:
raise Exception(f"status {status} != 0")
""" % (spawn_args,))

# Use a subprocess to test os.posix_spawnp() with a modified PATH
# environment variable: posix_spawnp() uses the current environment
# to locate the program, not its environment argument.
args = ('-c', code)
assert_python_ok(*args, PATH=path)


def test_main():
try:
support.run_unittest(PosixTester, PosixGroupsTester, TestPosixSpawn)
support.run_unittest(
PosixTester,
PosixGroupsTester,
TestPosixSpawn,
TestPosixSpawnP,
)
finally:
support.reap_children()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add a new :func:`os.posix_spawnp` function.
Patch by Joannah Nanjekye.
Loading

0 comments on commit 92b8322

Please sign in to comment.