process_test.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. from __future__ import absolute_import, division, print_function
  2. import logging
  3. import os
  4. import signal
  5. import subprocess
  6. import sys
  7. from tornado.httpclient import HTTPClient, HTTPError
  8. from tornado.httpserver import HTTPServer
  9. from tornado.ioloop import IOLoop
  10. from tornado.log import gen_log
  11. from tornado.process import fork_processes, task_id, Subprocess
  12. from tornado.simple_httpclient import SimpleAsyncHTTPClient
  13. from tornado.testing import bind_unused_port, ExpectLog, AsyncTestCase, gen_test
  14. from tornado.test.util import unittest, skipIfNonUnix
  15. from tornado.web import RequestHandler, Application
  16. try:
  17. import asyncio
  18. except ImportError:
  19. asyncio = None
  20. def skip_if_twisted():
  21. if IOLoop.configured_class().__name__.endswith('TwistedIOLoop'):
  22. raise unittest.SkipTest("Process tests not compatible with TwistedIOLoop")
  23. # Not using AsyncHTTPTestCase because we need control over the IOLoop.
  24. @skipIfNonUnix
  25. class ProcessTest(unittest.TestCase):
  26. def get_app(self):
  27. class ProcessHandler(RequestHandler):
  28. def get(self):
  29. if self.get_argument("exit", None):
  30. # must use os._exit instead of sys.exit so unittest's
  31. # exception handler doesn't catch it
  32. os._exit(int(self.get_argument("exit")))
  33. if self.get_argument("signal", None):
  34. os.kill(os.getpid(),
  35. int(self.get_argument("signal")))
  36. self.write(str(os.getpid()))
  37. return Application([("/", ProcessHandler)])
  38. def tearDown(self):
  39. if task_id() is not None:
  40. # We're in a child process, and probably got to this point
  41. # via an uncaught exception. If we return now, both
  42. # processes will continue with the rest of the test suite.
  43. # Exit now so the parent process will restart the child
  44. # (since we don't have a clean way to signal failure to
  45. # the parent that won't restart)
  46. logging.error("aborting child process from tearDown")
  47. logging.shutdown()
  48. os._exit(1)
  49. # In the surviving process, clear the alarm we set earlier
  50. signal.alarm(0)
  51. super(ProcessTest, self).tearDown()
  52. def test_multi_process(self):
  53. # This test doesn't work on twisted because we use the global
  54. # reactor and don't restore it to a sane state after the fork
  55. # (asyncio has the same issue, but we have a special case in
  56. # place for it).
  57. skip_if_twisted()
  58. with ExpectLog(gen_log, "(Starting .* processes|child .* exited|uncaught exception)"):
  59. sock, port = bind_unused_port()
  60. def get_url(path):
  61. return "http://127.0.0.1:%d%s" % (port, path)
  62. # ensure that none of these processes live too long
  63. signal.alarm(5) # master process
  64. try:
  65. id = fork_processes(3, max_restarts=3)
  66. self.assertTrue(id is not None)
  67. signal.alarm(5) # child processes
  68. except SystemExit as e:
  69. # if we exit cleanly from fork_processes, all the child processes
  70. # finished with status 0
  71. self.assertEqual(e.code, 0)
  72. self.assertTrue(task_id() is None)
  73. sock.close()
  74. return
  75. try:
  76. if asyncio is not None:
  77. # Reset the global asyncio event loop, which was put into
  78. # a broken state by the fork.
  79. asyncio.set_event_loop(asyncio.new_event_loop())
  80. if id in (0, 1):
  81. self.assertEqual(id, task_id())
  82. server = HTTPServer(self.get_app())
  83. server.add_sockets([sock])
  84. IOLoop.current().start()
  85. elif id == 2:
  86. self.assertEqual(id, task_id())
  87. sock.close()
  88. # Always use SimpleAsyncHTTPClient here; the curl
  89. # version appears to get confused sometimes if the
  90. # connection gets closed before it's had a chance to
  91. # switch from writing mode to reading mode.
  92. client = HTTPClient(SimpleAsyncHTTPClient)
  93. def fetch(url, fail_ok=False):
  94. try:
  95. return client.fetch(get_url(url))
  96. except HTTPError as e:
  97. if not (fail_ok and e.code == 599):
  98. raise
  99. # Make two processes exit abnormally
  100. fetch("/?exit=2", fail_ok=True)
  101. fetch("/?exit=3", fail_ok=True)
  102. # They've been restarted, so a new fetch will work
  103. int(fetch("/").body)
  104. # Now the same with signals
  105. # Disabled because on the mac a process dying with a signal
  106. # can trigger an "Application exited abnormally; send error
  107. # report to Apple?" prompt.
  108. # fetch("/?signal=%d" % signal.SIGTERM, fail_ok=True)
  109. # fetch("/?signal=%d" % signal.SIGABRT, fail_ok=True)
  110. # int(fetch("/").body)
  111. # Now kill them normally so they won't be restarted
  112. fetch("/?exit=0", fail_ok=True)
  113. # One process left; watch it's pid change
  114. pid = int(fetch("/").body)
  115. fetch("/?exit=4", fail_ok=True)
  116. pid2 = int(fetch("/").body)
  117. self.assertNotEqual(pid, pid2)
  118. # Kill the last one so we shut down cleanly
  119. fetch("/?exit=0", fail_ok=True)
  120. os._exit(0)
  121. except Exception:
  122. logging.error("exception in child process %d", id, exc_info=True)
  123. raise
  124. @skipIfNonUnix
  125. class SubprocessTest(AsyncTestCase):
  126. @gen_test
  127. def test_subprocess(self):
  128. if IOLoop.configured_class().__name__.endswith('LayeredTwistedIOLoop'):
  129. # This test fails non-deterministically with LayeredTwistedIOLoop.
  130. # (the read_until('\n') returns '\n' instead of 'hello\n')
  131. # This probably indicates a problem with either TornadoReactor
  132. # or TwistedIOLoop, but I haven't been able to track it down
  133. # and for now this is just causing spurious travis-ci failures.
  134. raise unittest.SkipTest("Subprocess tests not compatible with "
  135. "LayeredTwistedIOLoop")
  136. subproc = Subprocess([sys.executable, '-u', '-i'],
  137. stdin=Subprocess.STREAM,
  138. stdout=Subprocess.STREAM, stderr=subprocess.STDOUT)
  139. self.addCleanup(lambda: (subproc.proc.terminate(), subproc.proc.wait()))
  140. self.addCleanup(subproc.stdout.close)
  141. self.addCleanup(subproc.stdin.close)
  142. yield subproc.stdout.read_until(b'>>> ')
  143. subproc.stdin.write(b"print('hello')\n")
  144. data = yield subproc.stdout.read_until(b'\n')
  145. self.assertEqual(data, b"hello\n")
  146. yield subproc.stdout.read_until(b">>> ")
  147. subproc.stdin.write(b"raise SystemExit\n")
  148. data = yield subproc.stdout.read_until_close()
  149. self.assertEqual(data, b"")
  150. @gen_test
  151. def test_close_stdin(self):
  152. # Close the parent's stdin handle and see that the child recognizes it.
  153. subproc = Subprocess([sys.executable, '-u', '-i'],
  154. stdin=Subprocess.STREAM,
  155. stdout=Subprocess.STREAM, stderr=subprocess.STDOUT)
  156. self.addCleanup(lambda: (subproc.proc.terminate(), subproc.proc.wait()))
  157. yield subproc.stdout.read_until(b'>>> ')
  158. subproc.stdin.close()
  159. data = yield subproc.stdout.read_until_close()
  160. self.assertEqual(data, b"\n")
  161. @gen_test
  162. def test_stderr(self):
  163. # This test is mysteriously flaky on twisted: it succeeds, but logs
  164. # an error of EBADF on closing a file descriptor.
  165. skip_if_twisted()
  166. subproc = Subprocess([sys.executable, '-u', '-c',
  167. r"import sys; sys.stderr.write('hello\n')"],
  168. stderr=Subprocess.STREAM)
  169. self.addCleanup(lambda: (subproc.proc.terminate(), subproc.proc.wait()))
  170. data = yield subproc.stderr.read_until(b'\n')
  171. self.assertEqual(data, b'hello\n')
  172. # More mysterious EBADF: This fails if done with self.addCleanup instead of here.
  173. subproc.stderr.close()
  174. def test_sigchild(self):
  175. # Twisted's SIGCHLD handler and Subprocess's conflict with each other.
  176. skip_if_twisted()
  177. Subprocess.initialize()
  178. self.addCleanup(Subprocess.uninitialize)
  179. subproc = Subprocess([sys.executable, '-c', 'pass'])
  180. subproc.set_exit_callback(self.stop)
  181. ret = self.wait()
  182. self.assertEqual(ret, 0)
  183. self.assertEqual(subproc.returncode, ret)
  184. @gen_test
  185. def test_sigchild_future(self):
  186. skip_if_twisted()
  187. Subprocess.initialize()
  188. self.addCleanup(Subprocess.uninitialize)
  189. subproc = Subprocess([sys.executable, '-c', 'pass'])
  190. ret = yield subproc.wait_for_exit()
  191. self.assertEqual(ret, 0)
  192. self.assertEqual(subproc.returncode, ret)
  193. def test_sigchild_signal(self):
  194. skip_if_twisted()
  195. Subprocess.initialize()
  196. self.addCleanup(Subprocess.uninitialize)
  197. subproc = Subprocess([sys.executable, '-c',
  198. 'import time; time.sleep(30)'],
  199. stdout=Subprocess.STREAM)
  200. self.addCleanup(subproc.stdout.close)
  201. subproc.set_exit_callback(self.stop)
  202. os.kill(subproc.pid, signal.SIGTERM)
  203. try:
  204. ret = self.wait(timeout=1.0)
  205. except AssertionError:
  206. # We failed to get the termination signal. This test is
  207. # occasionally flaky on pypy, so try to get a little more
  208. # information: did the process close its stdout
  209. # (indicating that the problem is in the parent process's
  210. # signal handling) or did the child process somehow fail
  211. # to terminate?
  212. subproc.stdout.read_until_close(callback=self.stop)
  213. try:
  214. self.wait(timeout=1.0)
  215. except AssertionError:
  216. raise AssertionError("subprocess failed to terminate")
  217. else:
  218. raise AssertionError("subprocess closed stdout but failed to "
  219. "get termination signal")
  220. self.assertEqual(subproc.returncode, ret)
  221. self.assertEqual(ret, -signal.SIGTERM)
  222. @gen_test
  223. def test_wait_for_exit_raise(self):
  224. skip_if_twisted()
  225. Subprocess.initialize()
  226. self.addCleanup(Subprocess.uninitialize)
  227. subproc = Subprocess([sys.executable, '-c', 'import sys; sys.exit(1)'])
  228. with self.assertRaises(subprocess.CalledProcessError) as cm:
  229. yield subproc.wait_for_exit()
  230. self.assertEqual(cm.exception.returncode, 1)
  231. @gen_test
  232. def test_wait_for_exit_raise_disabled(self):
  233. skip_if_twisted()
  234. Subprocess.initialize()
  235. self.addCleanup(Subprocess.uninitialize)
  236. subproc = Subprocess([sys.executable, '-c', 'import sys; sys.exit(1)'])
  237. ret = yield subproc.wait_for_exit(raise_error=False)
  238. self.assertEqual(ret, 1)