mirror of https://github.com/MagicStack/uvloop.git
975 lines
30 KiB
Python
975 lines
30 KiB
Python
import asyncio
|
|
import contextlib
|
|
import gc
|
|
import os
|
|
import pathlib
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import textwrap
|
|
import time
|
|
import unittest
|
|
|
|
import psutil
|
|
|
|
from uvloop import _testbase as tb
|
|
|
|
|
|
class _RedirectFD(contextlib.AbstractContextManager):
|
|
def __init__(self, old_file, new_file):
|
|
self._old_fd = old_file.fileno()
|
|
self._old_fd_save = os.dup(self._old_fd)
|
|
self._new_fd = new_file.fileno()
|
|
|
|
def __enter__(self):
|
|
os.dup2(self._new_fd, self._old_fd)
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
os.dup2(self._old_fd_save, self._old_fd)
|
|
os.close(self._old_fd_save)
|
|
|
|
|
|
class _TestProcess:
|
|
def get_num_fds(self):
|
|
return psutil.Process(os.getpid()).num_fds()
|
|
|
|
def test_process_env_1(self):
|
|
async def test():
|
|
cmd = 'echo $FOO$BAR'
|
|
env = {'FOO': 'sp', 'BAR': 'am'}
|
|
proc = await asyncio.create_subprocess_shell(
|
|
cmd,
|
|
env=env,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
|
|
out, _ = await proc.communicate()
|
|
self.assertEqual(out, b'spam\n')
|
|
self.assertEqual(proc.returncode, 0)
|
|
|
|
self.loop.run_until_complete(test())
|
|
|
|
def test_process_env_2(self):
|
|
async def test():
|
|
cmd = 'env'
|
|
env = {} # empty environment
|
|
proc = await asyncio.create_subprocess_exec(
|
|
cmd,
|
|
env=env,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
|
|
out, _ = await proc.communicate()
|
|
self.assertEqual(out, b'')
|
|
self.assertEqual(proc.returncode, 0)
|
|
|
|
self.loop.run_until_complete(test())
|
|
|
|
def test_process_cwd_1(self):
|
|
async def test():
|
|
cmd = 'pwd'
|
|
env = {}
|
|
cwd = '/'
|
|
proc = await asyncio.create_subprocess_shell(
|
|
cmd,
|
|
cwd=cwd,
|
|
env=env,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
|
|
out, _ = await proc.communicate()
|
|
self.assertEqual(out, b'/\n')
|
|
self.assertEqual(proc.returncode, 0)
|
|
|
|
self.loop.run_until_complete(test())
|
|
|
|
@unittest.skipUnless(hasattr(os, 'fspath'), 'no os.fspath()')
|
|
def test_process_cwd_2(self):
|
|
async def test():
|
|
cmd = 'pwd'
|
|
env = {}
|
|
cwd = pathlib.Path('/')
|
|
proc = await asyncio.create_subprocess_shell(
|
|
cmd,
|
|
cwd=cwd,
|
|
env=env,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
|
|
out, _ = await proc.communicate()
|
|
self.assertEqual(out, b'/\n')
|
|
self.assertEqual(proc.returncode, 0)
|
|
|
|
self.loop.run_until_complete(test())
|
|
|
|
def test_process_preexec_fn_1(self):
|
|
# Copied from CPython/test_suprocess.py
|
|
|
|
# DISCLAIMER: Setting environment variables is *not* a good use
|
|
# of a preexec_fn. This is merely a test.
|
|
|
|
async def test():
|
|
cmd = sys.executable
|
|
proc = await asyncio.create_subprocess_exec(
|
|
cmd, b'-W', b'ignore', '-c',
|
|
'import os,sys;sys.stdout.write(os.getenv("FRUIT"))',
|
|
stdout=subprocess.PIPE,
|
|
preexec_fn=lambda: os.putenv("FRUIT", "apple"))
|
|
|
|
out, _ = await proc.communicate()
|
|
self.assertEqual(out, b'apple')
|
|
self.assertEqual(proc.returncode, 0)
|
|
|
|
self.loop.run_until_complete(test())
|
|
|
|
def test_process_preexec_fn_2(self):
|
|
# Copied from CPython/test_suprocess.py
|
|
|
|
def raise_it():
|
|
raise ValueError("spam")
|
|
|
|
async def test():
|
|
cmd = sys.executable
|
|
proc = await asyncio.create_subprocess_exec(
|
|
cmd, b'-W', b'ignore', '-c', 'import time; time.sleep(10)',
|
|
preexec_fn=raise_it)
|
|
|
|
await proc.communicate()
|
|
|
|
started = time.time()
|
|
try:
|
|
self.loop.run_until_complete(test())
|
|
except subprocess.SubprocessError as ex:
|
|
self.assertIn('preexec_fn', ex.args[0])
|
|
if ex.__cause__ is not None:
|
|
# uvloop will set __cause__
|
|
self.assertIs(type(ex.__cause__), ValueError)
|
|
self.assertEqual(ex.__cause__.args[0], 'spam')
|
|
else:
|
|
self.fail(
|
|
'exception in preexec_fn did not propagate to the parent')
|
|
|
|
if time.time() - started > 5:
|
|
self.fail(
|
|
'exception in preexec_fn did not kill the child process')
|
|
|
|
def test_process_executable_1(self):
|
|
async def test():
|
|
proc = await asyncio.create_subprocess_exec(
|
|
b'doesnotexist', b'-W', b'ignore', b'-c', b'print("spam")',
|
|
executable=sys.executable,
|
|
stdout=subprocess.PIPE)
|
|
|
|
out, err = await proc.communicate()
|
|
self.assertEqual(out, b'spam\n')
|
|
|
|
self.loop.run_until_complete(test())
|
|
|
|
@unittest.skipIf(sys.version_info < (3, 8, 0),
|
|
"3.5 to 3.7 does not support path-like objects "
|
|
"in the asyncio subprocess API")
|
|
def test_process_executable_2(self):
|
|
async def test():
|
|
proc = await asyncio.create_subprocess_exec(
|
|
pathlib.Path(sys.executable),
|
|
b'-W', b'ignore', b'-c', b'print("spam")',
|
|
stdout=subprocess.PIPE)
|
|
|
|
out, err = await proc.communicate()
|
|
self.assertEqual(out, b'spam\n')
|
|
|
|
self.loop.run_until_complete(test())
|
|
|
|
def test_process_pid_1(self):
|
|
async def test():
|
|
prog = '''\
|
|
import os
|
|
print(os.getpid())
|
|
'''
|
|
|
|
cmd = sys.executable
|
|
proc = await asyncio.create_subprocess_exec(
|
|
cmd, b'-W', b'ignore', b'-c', prog,
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE)
|
|
|
|
pid = proc.pid
|
|
expected_result = '{}\n'.format(pid).encode()
|
|
|
|
out, err = await proc.communicate()
|
|
self.assertEqual(out, expected_result)
|
|
|
|
self.loop.run_until_complete(test())
|
|
|
|
def test_process_send_signal_1(self):
|
|
async def test():
|
|
prog = '''\
|
|
import signal
|
|
|
|
def handler(signum, frame):
|
|
if signum == signal.SIGUSR1:
|
|
print('WORLD')
|
|
|
|
signal.signal(signal.SIGUSR1, handler)
|
|
a = input()
|
|
print(a)
|
|
a = input()
|
|
print(a)
|
|
exit(11)
|
|
'''
|
|
|
|
cmd = sys.executable
|
|
proc = await asyncio.create_subprocess_exec(
|
|
cmd, b'-W', b'ignore', b'-c', prog,
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
|
|
proc.stdin.write(b'HELLO\n')
|
|
await proc.stdin.drain()
|
|
|
|
self.assertEqual(await proc.stdout.readline(), b'HELLO\n')
|
|
|
|
proc.send_signal(signal.SIGUSR1)
|
|
|
|
proc.stdin.write(b'!\n')
|
|
await proc.stdin.drain()
|
|
|
|
self.assertEqual(await proc.stdout.readline(), b'WORLD\n')
|
|
self.assertEqual(await proc.stdout.readline(), b'!\n')
|
|
self.assertEqual(await proc.wait(), 11)
|
|
|
|
self.loop.run_until_complete(test())
|
|
|
|
def test_process_streams_basic_1(self):
|
|
async def test():
|
|
|
|
prog = '''\
|
|
import sys
|
|
while True:
|
|
a = input()
|
|
if a == 'stop':
|
|
exit(20)
|
|
elif a == 'stderr':
|
|
print('OUCH', file=sys.stderr)
|
|
else:
|
|
print('>' + a + '<')
|
|
'''
|
|
|
|
cmd = sys.executable
|
|
proc = await asyncio.create_subprocess_exec(
|
|
cmd, b'-W', b'ignore', b'-c', prog,
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
|
|
self.assertGreater(proc.pid, 0)
|
|
self.assertIs(proc.returncode, None)
|
|
|
|
transp = proc._transport
|
|
with self.assertRaises(NotImplementedError):
|
|
# stdin is WriteTransport
|
|
transp.get_pipe_transport(0).pause_reading()
|
|
with self.assertRaises((NotImplementedError, AttributeError)):
|
|
# stdout is ReadTransport
|
|
transp.get_pipe_transport(1).write(b'wat')
|
|
|
|
proc.stdin.write(b'foobar\n')
|
|
await proc.stdin.drain()
|
|
out = await proc.stdout.readline()
|
|
self.assertEqual(out, b'>foobar<\n')
|
|
|
|
proc.stdin.write(b'stderr\n')
|
|
await proc.stdin.drain()
|
|
out = await proc.stderr.readline()
|
|
self.assertEqual(out, b'OUCH\n')
|
|
|
|
proc.stdin.write(b'stop\n')
|
|
await proc.stdin.drain()
|
|
|
|
exitcode = await proc.wait()
|
|
self.assertEqual(exitcode, 20)
|
|
|
|
self.loop.run_until_complete(test())
|
|
|
|
def test_process_streams_stderr_to_stdout(self):
|
|
async def test():
|
|
prog = '''\
|
|
import sys
|
|
print('out', flush=True)
|
|
print('err', file=sys.stderr, flush=True)
|
|
'''
|
|
|
|
proc = await asyncio.create_subprocess_exec(
|
|
sys.executable, b'-W', b'ignore', b'-c', prog,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT)
|
|
|
|
out, err = await proc.communicate()
|
|
self.assertIsNone(err)
|
|
self.assertEqual(out, b'out\nerr\n')
|
|
|
|
self.loop.run_until_complete(test())
|
|
|
|
def test_process_streams_devnull(self):
|
|
async def test():
|
|
prog = '''\
|
|
import sys
|
|
print('out', flush=True)
|
|
print('err', file=sys.stderr, flush=True)
|
|
'''
|
|
|
|
proc = await asyncio.create_subprocess_exec(
|
|
sys.executable, b'-W', b'ignore', b'-c', prog,
|
|
stdin=subprocess.DEVNULL,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL)
|
|
|
|
out, err = await proc.communicate()
|
|
self.assertIsNone(err)
|
|
self.assertIsNone(out)
|
|
|
|
self.loop.run_until_complete(test())
|
|
|
|
def test_process_streams_pass_fds(self):
|
|
async def test():
|
|
prog = '''\
|
|
import sys, os
|
|
assert sys.argv[1] == '--'
|
|
inherited = int(sys.argv[2])
|
|
non_inherited = int(sys.argv[3])
|
|
|
|
os.fstat(inherited)
|
|
|
|
try:
|
|
os.fstat(non_inherited)
|
|
except:
|
|
pass
|
|
else:
|
|
raise RuntimeError()
|
|
|
|
print("OK")
|
|
'''
|
|
|
|
with tempfile.TemporaryFile() as inherited, \
|
|
tempfile.TemporaryFile() as non_inherited:
|
|
|
|
proc = await asyncio.create_subprocess_exec(
|
|
sys.executable, b'-W', b'ignore', b'-c', prog, '--',
|
|
str(inherited.fileno()),
|
|
str(non_inherited.fileno()),
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
pass_fds=(inherited.fileno(),))
|
|
|
|
out, err = await proc.communicate()
|
|
self.assertEqual(err, b'')
|
|
self.assertEqual(out, b'OK\n')
|
|
|
|
self.loop.run_until_complete(test())
|
|
|
|
def test_subprocess_fd_leak_1(self):
|
|
async def main(n):
|
|
for i in range(n):
|
|
try:
|
|
await asyncio.create_subprocess_exec(
|
|
'nonexistant',
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL)
|
|
except FileNotFoundError:
|
|
pass
|
|
await asyncio.sleep(0)
|
|
|
|
self.loop.run_until_complete(main(10))
|
|
num_fd_1 = self.get_num_fds()
|
|
self.loop.run_until_complete(main(10))
|
|
num_fd_2 = self.get_num_fds()
|
|
|
|
self.assertEqual(num_fd_1, num_fd_2)
|
|
|
|
def test_subprocess_fd_leak_2(self):
|
|
async def main(n):
|
|
for i in range(n):
|
|
try:
|
|
p = await asyncio.create_subprocess_exec(
|
|
'ls',
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL)
|
|
finally:
|
|
await p.wait()
|
|
await asyncio.sleep(0)
|
|
|
|
self.loop.run_until_complete(main(10))
|
|
num_fd_1 = self.get_num_fds()
|
|
self.loop.run_until_complete(main(10))
|
|
num_fd_2 = self.get_num_fds()
|
|
|
|
self.assertEqual(num_fd_1, num_fd_2)
|
|
|
|
def test_subprocess_invalid_stdin(self):
|
|
fd = None
|
|
for tryfd in range(10000, 1000, -1):
|
|
try:
|
|
tryfd = os.dup(tryfd)
|
|
except OSError:
|
|
fd = tryfd
|
|
break
|
|
else:
|
|
os.close(tryfd)
|
|
else:
|
|
self.fail('could not find a free FD')
|
|
|
|
async def main():
|
|
with self.assertRaises(OSError):
|
|
await asyncio.create_subprocess_exec(
|
|
'ls',
|
|
stdin=fd)
|
|
|
|
with self.assertRaises(OSError):
|
|
await asyncio.create_subprocess_exec(
|
|
'ls',
|
|
stdout=fd)
|
|
|
|
with self.assertRaises(OSError):
|
|
await asyncio.create_subprocess_exec(
|
|
'ls',
|
|
stderr=fd)
|
|
|
|
self.loop.run_until_complete(main())
|
|
|
|
def test_process_streams_redirect(self):
|
|
async def test():
|
|
prog = bR'''
|
|
import sys
|
|
print('out', flush=True)
|
|
print('err', file=sys.stderr, flush=True)
|
|
'''
|
|
|
|
proc = await asyncio.create_subprocess_exec(
|
|
sys.executable, b'-W', b'ignore', b'-c', prog)
|
|
|
|
out, err = await proc.communicate()
|
|
self.assertIsNone(out)
|
|
self.assertIsNone(err)
|
|
|
|
with tempfile.NamedTemporaryFile('w') as stdout:
|
|
with tempfile.NamedTemporaryFile('w') as stderr:
|
|
with _RedirectFD(sys.stdout, stdout):
|
|
with _RedirectFD(sys.stderr, stderr):
|
|
self.loop.run_until_complete(test())
|
|
|
|
stdout.flush()
|
|
stderr.flush()
|
|
|
|
with open(stdout.name, 'rb') as so:
|
|
self.assertEqual(so.read(), b'out\n')
|
|
|
|
with open(stderr.name, 'rb') as se:
|
|
self.assertEqual(se.read(), b'err\n')
|
|
|
|
|
|
class _AsyncioTests:
|
|
|
|
# Program blocking
|
|
PROGRAM_BLOCKED = [sys.executable, b'-W', b'ignore',
|
|
b'-c', b'import time; time.sleep(3600)']
|
|
|
|
# Program copying input to output
|
|
PROGRAM_CAT = [
|
|
sys.executable, b'-c',
|
|
b';'.join((b'import sys',
|
|
b'data = sys.stdin.buffer.read()',
|
|
b'sys.stdout.buffer.write(data)'))]
|
|
|
|
PROGRAM_ERROR = [
|
|
sys.executable, b'-W', b'ignore', b'-c', b'1/0'
|
|
]
|
|
|
|
def test_stdin_not_inheritable(self):
|
|
# asyncio issue #209: stdin must not be inheritable, otherwise
|
|
# the Process.communicate() hangs
|
|
async def len_message(message):
|
|
code = 'import sys; data = sys.stdin.read(); print(len(data))'
|
|
proc = await asyncio.create_subprocess_exec(
|
|
sys.executable, b'-W', b'ignore', b'-c', code,
|
|
stdin=asyncio.subprocess.PIPE,
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE,
|
|
close_fds=False)
|
|
stdout, stderr = await proc.communicate(message)
|
|
exitcode = await proc.wait()
|
|
return (stdout, exitcode)
|
|
|
|
output, exitcode = self.loop.run_until_complete(len_message(b'abc'))
|
|
self.assertEqual(output.rstrip(), b'3')
|
|
self.assertEqual(exitcode, 0)
|
|
|
|
def test_stdin_stdout_pipe(self):
|
|
args = self.PROGRAM_CAT
|
|
|
|
async def run(data):
|
|
proc = await asyncio.create_subprocess_exec(
|
|
*args,
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE)
|
|
|
|
# feed data
|
|
proc.stdin.write(data)
|
|
await proc.stdin.drain()
|
|
proc.stdin.close()
|
|
|
|
# get output and exitcode
|
|
data = await proc.stdout.read()
|
|
exitcode = await proc.wait()
|
|
return (exitcode, data)
|
|
|
|
task = run(b'some data')
|
|
task = asyncio.wait_for(task, 60.0)
|
|
exitcode, stdout = self.loop.run_until_complete(task)
|
|
self.assertEqual(exitcode, 0)
|
|
self.assertEqual(stdout, b'some data')
|
|
|
|
def test_stdin_stdout_file(self):
|
|
args = self.PROGRAM_CAT
|
|
|
|
async def run(data, stdout):
|
|
proc = await asyncio.create_subprocess_exec(
|
|
*args,
|
|
stdin=subprocess.PIPE,
|
|
stdout=stdout)
|
|
|
|
# feed data
|
|
proc.stdin.write(data)
|
|
await proc.stdin.drain()
|
|
proc.stdin.close()
|
|
|
|
exitcode = await proc.wait()
|
|
return exitcode
|
|
|
|
with tempfile.TemporaryFile('w+b') as new_stdout:
|
|
task = run(b'some data', new_stdout)
|
|
task = asyncio.wait_for(task, 60.0)
|
|
exitcode = self.loop.run_until_complete(task)
|
|
self.assertEqual(exitcode, 0)
|
|
|
|
new_stdout.seek(0)
|
|
self.assertEqual(new_stdout.read(), b'some data')
|
|
|
|
def test_stdin_stderr_file(self):
|
|
args = self.PROGRAM_ERROR
|
|
|
|
async def run(stderr):
|
|
proc = await asyncio.create_subprocess_exec(
|
|
*args,
|
|
stdin=subprocess.PIPE,
|
|
stderr=stderr)
|
|
|
|
exitcode = await proc.wait()
|
|
return exitcode
|
|
|
|
with tempfile.TemporaryFile('w+b') as new_stderr:
|
|
task = run(new_stderr)
|
|
task = asyncio.wait_for(task, 60.0)
|
|
exitcode = self.loop.run_until_complete(task)
|
|
self.assertEqual(exitcode, 1)
|
|
|
|
new_stderr.seek(0)
|
|
self.assertIn(b'ZeroDivisionError', new_stderr.read())
|
|
|
|
def test_communicate(self):
|
|
args = self.PROGRAM_CAT
|
|
|
|
async def run(data):
|
|
proc = await asyncio.create_subprocess_exec(
|
|
*args,
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE)
|
|
stdout, stderr = await proc.communicate(data)
|
|
return proc.returncode, stdout
|
|
|
|
task = run(b'some data')
|
|
task = asyncio.wait_for(task, 60.0)
|
|
exitcode, stdout = self.loop.run_until_complete(task)
|
|
self.assertEqual(exitcode, 0)
|
|
self.assertEqual(stdout, b'some data')
|
|
|
|
def test_start_new_session(self):
|
|
# start the new process in a new session
|
|
create = asyncio.create_subprocess_shell('exit 8',
|
|
start_new_session=True)
|
|
proc = self.loop.run_until_complete(create)
|
|
exitcode = self.loop.run_until_complete(proc.wait())
|
|
self.assertEqual(exitcode, 8)
|
|
|
|
def test_shell(self):
|
|
create = asyncio.create_subprocess_shell('exit 7')
|
|
proc = self.loop.run_until_complete(create)
|
|
exitcode = self.loop.run_until_complete(proc.wait())
|
|
self.assertEqual(exitcode, 7)
|
|
|
|
def test_kill(self):
|
|
args = self.PROGRAM_BLOCKED
|
|
create = asyncio.create_subprocess_exec(*args)
|
|
proc = self.loop.run_until_complete(create)
|
|
proc.kill()
|
|
returncode = self.loop.run_until_complete(proc.wait())
|
|
self.assertEqual(-signal.SIGKILL, returncode)
|
|
|
|
def test_terminate(self):
|
|
args = self.PROGRAM_BLOCKED
|
|
create = asyncio.create_subprocess_exec(*args)
|
|
proc = self.loop.run_until_complete(create)
|
|
proc.terminate()
|
|
returncode = self.loop.run_until_complete(proc.wait())
|
|
self.assertEqual(-signal.SIGTERM, returncode)
|
|
|
|
def test_send_signal(self):
|
|
code = 'import time; print("sleeping", flush=True); time.sleep(3600)'
|
|
args = [sys.executable, b'-W', b'ignore', b'-c', code]
|
|
create = asyncio.create_subprocess_exec(*args,
|
|
stdout=subprocess.PIPE)
|
|
proc = self.loop.run_until_complete(create)
|
|
|
|
async def send_signal(proc):
|
|
# basic synchronization to wait until the program is sleeping
|
|
line = await proc.stdout.readline()
|
|
self.assertEqual(line, b'sleeping\n')
|
|
|
|
proc.send_signal(signal.SIGHUP)
|
|
returncode = (await proc.wait())
|
|
return returncode
|
|
|
|
returncode = self.loop.run_until_complete(send_signal(proc))
|
|
self.assertEqual(-signal.SIGHUP, returncode)
|
|
|
|
def test_cancel_process_wait(self):
|
|
# Issue #23140: cancel Process.wait()
|
|
|
|
async def cancel_wait():
|
|
proc = await asyncio.create_subprocess_exec(
|
|
*self.PROGRAM_BLOCKED)
|
|
|
|
# Create an internal future waiting on the process exit
|
|
task = self.loop.create_task(proc.wait())
|
|
self.loop.call_soon(task.cancel)
|
|
try:
|
|
await task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
|
|
# Cancel the future
|
|
task.cancel()
|
|
|
|
# Kill the process and wait until it is done
|
|
proc.kill()
|
|
await proc.wait()
|
|
|
|
self.loop.run_until_complete(cancel_wait())
|
|
|
|
def test_cancel_make_subprocess_transport_exec(self):
|
|
async def cancel_make_transport():
|
|
coro = asyncio.create_subprocess_exec(*self.PROGRAM_BLOCKED)
|
|
task = self.loop.create_task(coro)
|
|
|
|
self.loop.call_soon(task.cancel)
|
|
try:
|
|
await task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
|
|
# Give the process handler some time to close itself
|
|
await asyncio.sleep(0.3)
|
|
gc.collect()
|
|
|
|
# ignore the log:
|
|
# "Exception during subprocess creation, kill the subprocess"
|
|
with tb.disable_logger():
|
|
self.loop.run_until_complete(cancel_make_transport())
|
|
|
|
def test_cancel_post_init(self):
|
|
async def cancel_make_transport():
|
|
coro = self.loop.subprocess_exec(asyncio.SubprocessProtocol,
|
|
*self.PROGRAM_BLOCKED)
|
|
task = self.loop.create_task(coro)
|
|
|
|
self.loop.call_soon(task.cancel)
|
|
try:
|
|
await task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
|
|
# Give the process handler some time to close itself
|
|
await asyncio.sleep(0.3)
|
|
gc.collect()
|
|
|
|
# ignore the log:
|
|
# "Exception during subprocess creation, kill the subprocess"
|
|
with tb.disable_logger():
|
|
self.loop.run_until_complete(cancel_make_transport())
|
|
tb.run_briefly(self.loop)
|
|
|
|
def test_close_gets_process_closed(self):
|
|
|
|
loop = self.loop
|
|
|
|
class Protocol(asyncio.SubprocessProtocol):
|
|
|
|
def __init__(self):
|
|
self.closed = loop.create_future()
|
|
|
|
def connection_lost(self, exc):
|
|
self.closed.set_result(1)
|
|
|
|
async def test_subprocess():
|
|
transport, protocol = await loop.subprocess_exec(
|
|
Protocol, *self.PROGRAM_BLOCKED)
|
|
pid = transport.get_pid()
|
|
transport.close()
|
|
self.assertIsNone(transport.get_returncode())
|
|
await protocol.closed
|
|
self.assertIsNotNone(transport.get_returncode())
|
|
with self.assertRaises(ProcessLookupError):
|
|
os.kill(pid, 0)
|
|
|
|
loop.run_until_complete(test_subprocess())
|
|
|
|
def test_communicate_large_stdout_65536(self):
|
|
self._test_communicate_large_stdout(65536)
|
|
|
|
def test_communicate_large_stdout_65537(self):
|
|
self._test_communicate_large_stdout(65537)
|
|
|
|
def test_communicate_large_stdout_1000000(self):
|
|
self._test_communicate_large_stdout(1000000)
|
|
|
|
def _test_communicate_large_stdout(self, size):
|
|
async def copy_stdin_to_stdout(stdin):
|
|
# See https://github.com/MagicStack/uvloop/issues/363
|
|
# A program that copies stdin to stdout character by character
|
|
code = ('import sys, shutil; '
|
|
'shutil.copyfileobj(sys.stdin, sys.stdout, 1)')
|
|
proc = await asyncio.create_subprocess_exec(
|
|
sys.executable, b'-W', b'ignore', b'-c', code,
|
|
stdin=asyncio.subprocess.PIPE,
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE)
|
|
stdout, _stderr = await asyncio.wait_for(proc.communicate(stdin),
|
|
60.0)
|
|
return stdout
|
|
|
|
stdin = b'x' * size
|
|
stdout = self.loop.run_until_complete(copy_stdin_to_stdout(stdin))
|
|
self.assertEqual(stdout, stdin)
|
|
|
|
def test_write_huge_stdin_8192(self):
|
|
self._test_write_huge_stdin(8192)
|
|
|
|
def test_write_huge_stdin_8193(self):
|
|
self._test_write_huge_stdin(8193)
|
|
|
|
def test_write_huge_stdin_219263(self):
|
|
self._test_write_huge_stdin(219263)
|
|
|
|
def test_write_huge_stdin_219264(self):
|
|
self._test_write_huge_stdin(219264)
|
|
|
|
def _test_write_huge_stdin(self, buf_size):
|
|
code = '''
|
|
import sys
|
|
n = 0
|
|
while True:
|
|
line = sys.stdin.readline()
|
|
if not line:
|
|
print("unexpected EOF", file=sys.stderr)
|
|
break
|
|
if line == "END\\n":
|
|
break
|
|
n+=1
|
|
print(n)'''
|
|
num_lines = buf_size - len(b"END\n")
|
|
args = [sys.executable, b'-W', b'ignore', b'-c', code]
|
|
|
|
async def test():
|
|
proc = await asyncio.create_subprocess_exec(
|
|
*args,
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stdin=asyncio.subprocess.PIPE)
|
|
data = b"\n" * num_lines + b"END\n"
|
|
self.assertEqual(len(data), buf_size)
|
|
proc.stdin.write(data)
|
|
await asyncio.wait_for(proc.stdin.drain(), timeout=5.0)
|
|
try:
|
|
await asyncio.wait_for(proc.wait(), timeout=5.0)
|
|
except asyncio.TimeoutError:
|
|
proc.kill()
|
|
proc.stdin.close()
|
|
await proc.wait()
|
|
raise
|
|
out = await proc.stdout.read()
|
|
self.assertEqual(int(out), num_lines)
|
|
|
|
self.loop.run_until_complete(test())
|
|
|
|
|
|
class Test_UV_Process(_TestProcess, tb.UVTestCase):
|
|
def test_process_double_close(self):
|
|
script = textwrap.dedent("""
|
|
import os
|
|
import sys
|
|
from unittest import mock
|
|
|
|
import asyncio
|
|
|
|
pipes = []
|
|
original_os_pipe = os.pipe
|
|
def log_pipes():
|
|
pipe = original_os_pipe()
|
|
pipes.append(pipe)
|
|
return pipe
|
|
|
|
dups = []
|
|
original_os_dup = os.dup
|
|
def log_dups(*args, **kwargs):
|
|
dup = original_os_dup(*args, **kwargs)
|
|
dups.append(dup)
|
|
return dup
|
|
|
|
with mock.patch(
|
|
"os.close", wraps=os.close
|
|
) as os_close, mock.patch(
|
|
"os.pipe", new=log_pipes
|
|
), mock.patch(
|
|
"os.dup", new=log_dups
|
|
):
|
|
import uvloop
|
|
|
|
|
|
async def test():
|
|
proc = await asyncio.create_subprocess_exec(
|
|
sys.executable, "-c", "pass"
|
|
)
|
|
await proc.communicate()
|
|
|
|
uvloop.install()
|
|
asyncio.run(test())
|
|
|
|
stdin, stdout, stderr = dups
|
|
(r, w), = pipes
|
|
assert os_close.mock_calls == [
|
|
mock.call(w),
|
|
mock.call(r),
|
|
mock.call(stderr),
|
|
mock.call(stdout),
|
|
mock.call(stdin),
|
|
]
|
|
""")
|
|
subprocess.run([sys.executable, '-c', script], check=True)
|
|
|
|
|
|
class Test_AIO_Process(_TestProcess, tb.AIOTestCase):
|
|
pass
|
|
|
|
|
|
class TestAsyncio_UV_Process(_AsyncioTests, tb.UVTestCase):
|
|
pass
|
|
|
|
|
|
class TestAsyncio_AIO_Process(_AsyncioTests, tb.AIOTestCase):
|
|
pass
|
|
|
|
|
|
class Test_UV_Process_Delayed(tb.UVTestCase):
|
|
|
|
class TestProto:
|
|
def __init__(self):
|
|
self.lost = 0
|
|
self.stages = []
|
|
|
|
def connection_made(self, transport):
|
|
self.stages.append(('CM', transport))
|
|
|
|
def pipe_data_received(self, fd, data):
|
|
if fd == 1:
|
|
self.stages.append(('STDOUT', data))
|
|
|
|
def pipe_connection_lost(self, fd, exc):
|
|
if fd == 1:
|
|
self.stages.append(('STDOUT', 'LOST'))
|
|
|
|
def process_exited(self):
|
|
self.stages.append('PROC_EXIT')
|
|
|
|
def connection_lost(self, exc):
|
|
self.stages.append(('CL', self.lost, exc))
|
|
self.lost += 1
|
|
|
|
async def run_sub(self, **kwargs):
|
|
return await self.loop.subprocess_shell(
|
|
lambda: self.TestProto(),
|
|
'echo 1',
|
|
**kwargs)
|
|
|
|
def test_process_delayed_stdio__paused__stdin_pipe(self):
|
|
transport, proto = self.loop.run_until_complete(
|
|
self.run_sub(
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
__uvloop_sleep_after_fork=True))
|
|
self.assertIsNot(transport, None)
|
|
self.assertEqual(transport.get_returncode(), 0)
|
|
self.assertEqual(
|
|
set(proto.stages),
|
|
{
|
|
('CM', transport),
|
|
'PROC_EXIT',
|
|
('STDOUT', b'1\n'),
|
|
('STDOUT', 'LOST'),
|
|
('CL', 0, None)
|
|
})
|
|
|
|
def test_process_delayed_stdio__paused__no_stdin(self):
|
|
transport, proto = self.loop.run_until_complete(
|
|
self.run_sub(
|
|
stdin=None,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
__uvloop_sleep_after_fork=True))
|
|
self.assertIsNot(transport, None)
|
|
self.assertEqual(transport.get_returncode(), 0)
|
|
self.assertEqual(
|
|
set(proto.stages),
|
|
{
|
|
('CM', transport),
|
|
'PROC_EXIT',
|
|
('STDOUT', b'1\n'),
|
|
('STDOUT', 'LOST'),
|
|
('CL', 0, None)
|
|
})
|
|
|
|
def test_process_delayed_stdio__not_paused__no_stdin(self):
|
|
if ((os.environ.get('TRAVIS_OS_NAME')
|
|
or os.environ.get('GITHUB_WORKFLOW'))
|
|
and sys.platform == 'darwin'):
|
|
# Randomly crashes on Travis, can't reproduce locally.
|
|
raise unittest.SkipTest()
|
|
|
|
transport, proto = self.loop.run_until_complete(
|
|
self.run_sub(
|
|
stdin=None,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE))
|
|
self.loop.run_until_complete(transport._wait())
|
|
self.assertEqual(transport.get_returncode(), 0)
|
|
self.assertIsNot(transport, None)
|
|
self.assertEqual(
|
|
set(proto.stages),
|
|
{
|
|
('CM', transport),
|
|
'PROC_EXIT',
|
|
('STDOUT', b'1\n'),
|
|
('STDOUT', 'LOST'),
|
|
('CL', 0, None)
|
|
})
|