Show More
@@ -18,10 +18,12 b' This file is only meant to be imported by process.py, not by end-users.' | |||||
18 | import os |
|
18 | import os | |
19 | import sys |
|
19 | import sys | |
20 | import ctypes |
|
20 | import ctypes | |
|
21 | import time | |||
21 |
|
22 | |||
22 | from ctypes import c_int, POINTER |
|
23 | from ctypes import c_int, POINTER | |
23 | from ctypes.wintypes import LPCWSTR, HLOCAL |
|
24 | from ctypes.wintypes import LPCWSTR, HLOCAL | |
24 | from subprocess import STDOUT |
|
25 | from subprocess import STDOUT, TimeoutExpired | |
|
26 | from threading import Thread | |||
25 |
|
27 | |||
26 | # our own imports |
|
28 | # our own imports | |
27 | from ._process_common import read_no_interrupt, process_handler, arg_split as py_arg_split |
|
29 | from ._process_common import read_no_interrupt, process_handler, arg_split as py_arg_split | |
@@ -93,15 +95,29 b' def _find_cmd(cmd):' | |||||
93 | def _system_body(p): |
|
95 | def _system_body(p): | |
94 | """Callback for _system.""" |
|
96 | """Callback for _system.""" | |
95 | enc = DEFAULT_ENCODING |
|
97 | enc = DEFAULT_ENCODING | |
96 | for line in read_no_interrupt(p.stdout).splitlines(): |
|
|||
97 | line = line.decode(enc, 'replace') |
|
|||
98 | print(line, file=sys.stdout) |
|
|||
99 | for line in read_no_interrupt(p.stderr).splitlines(): |
|
|||
100 | line = line.decode(enc, 'replace') |
|
|||
101 | print(line, file=sys.stderr) |
|
|||
102 |
|
98 | |||
103 | # Wait to finish for returncode |
|
99 | def stdout_read(): | |
104 | return p.wait() |
|
100 | for line in read_no_interrupt(p.stdout).splitlines(): | |
|
101 | line = line.decode(enc, 'replace') | |||
|
102 | print(line, file=sys.stdout) | |||
|
103 | ||||
|
104 | def stderr_read(): | |||
|
105 | for line in read_no_interrupt(p.stderr).splitlines(): | |||
|
106 | line = line.decode(enc, 'replace') | |||
|
107 | print(line, file=sys.stderr) | |||
|
108 | ||||
|
109 | Thread(target=stdout_read).start() | |||
|
110 | Thread(target=stderr_read).start() | |||
|
111 | ||||
|
112 | # Wait to finish for returncode. Unfortunately, Python has a bug where | |||
|
113 | # wait() isn't interruptible (https://bugs.python.org/issue28168) so poll in | |||
|
114 | # a loop instead of just doing `return p.wait()`. | |||
|
115 | while True: | |||
|
116 | result = p.poll() | |||
|
117 | if result is None: | |||
|
118 | time.sleep(0.01) | |||
|
119 | else: | |||
|
120 | return result | |||
105 |
|
121 | |||
106 |
|
122 | |||
107 | def system(cmd): |
|
123 | def system(cmd): | |
@@ -116,9 +132,7 b' def system(cmd):' | |||||
116 |
|
132 | |||
117 | Returns |
|
133 | Returns | |
118 | ------- |
|
134 | ------- | |
119 | None : we explicitly do NOT return the subprocess status code, as this |
|
135 | int : child process' exit code. | |
120 | utility is meant to be used extensively in IPython, where any return value |
|
|||
121 | would trigger :func:`sys.displayhook` calls. |
|
|||
122 | """ |
|
136 | """ | |
123 | # The controller provides interactivity with both |
|
137 | # The controller provides interactivity with both | |
124 | # stdin and stdout |
|
138 | # stdin and stdout |
@@ -15,13 +15,19 b' Tests for platutils.py' | |||||
15 | #----------------------------------------------------------------------------- |
|
15 | #----------------------------------------------------------------------------- | |
16 |
|
16 | |||
17 | import sys |
|
17 | import sys | |
|
18 | import signal | |||
18 | import os |
|
19 | import os | |
|
20 | import time | |||
|
21 | from _thread import interrupt_main # Py 3 | |||
|
22 | import threading | |||
|
23 | from unittest import SkipTest | |||
19 |
|
24 | |||
20 | import nose.tools as nt |
|
25 | import nose.tools as nt | |
21 |
|
26 | |||
22 | from IPython.utils.process import (find_cmd, FindCmdError, arg_split, |
|
27 | from IPython.utils.process import (find_cmd, FindCmdError, arg_split, | |
23 | system, getoutput, getoutputerror, |
|
28 | system, getoutput, getoutputerror, | |
24 | get_output_error_code) |
|
29 | get_output_error_code) | |
|
30 | from IPython.utils.capture import capture_output | |||
25 | from IPython.testing import decorators as dec |
|
31 | from IPython.testing import decorators as dec | |
26 | from IPython.testing import tools as tt |
|
32 | from IPython.testing import tools as tt | |
27 |
|
33 | |||
@@ -107,6 +113,49 b' class SubProcessTestCase(tt.TempFileMixin):' | |||||
107 | status = system('%s -c "import sys"' % python) |
|
113 | status = system('%s -c "import sys"' % python) | |
108 | self.assertEqual(status, 0) |
|
114 | self.assertEqual(status, 0) | |
109 |
|
115 | |||
|
116 | def assert_interrupts(self, command): | |||
|
117 | """ | |||
|
118 | Interrupt a subprocess after a second. | |||
|
119 | """ | |||
|
120 | if threading.main_thread() != threading.current_thread(): | |||
|
121 | raise nt.SkipTest("Can't run this test if not in main thread.") | |||
|
122 | ||||
|
123 | # Some tests can overwrite SIGINT handler (by using pdb for example), | |||
|
124 | # which then breaks this test, so just make sure it's operating | |||
|
125 | # normally. | |||
|
126 | signal.signal(signal.SIGINT, signal.default_int_handler) | |||
|
127 | ||||
|
128 | def interrupt(): | |||
|
129 | # Wait for subprocess to start: | |||
|
130 | time.sleep(0.5) | |||
|
131 | interrupt_main() | |||
|
132 | ||||
|
133 | threading.Thread(target=interrupt).start() | |||
|
134 | start = time.time() | |||
|
135 | try: | |||
|
136 | result = command() | |||
|
137 | except KeyboardInterrupt: | |||
|
138 | # Success! | |||
|
139 | pass | |||
|
140 | end = time.time() | |||
|
141 | self.assertTrue( | |||
|
142 | end - start < 2, "Process didn't die quickly: %s" % (end - start) | |||
|
143 | ) | |||
|
144 | return result | |||
|
145 | ||||
|
146 | def test_system_interrupt(self): | |||
|
147 | """ | |||
|
148 | When interrupted in the way ipykernel interrupts IPython, the | |||
|
149 | subprocess is interrupted. | |||
|
150 | """ | |||
|
151 | def command(): | |||
|
152 | return system('%s -c "import time; time.sleep(5)"' % python) | |||
|
153 | ||||
|
154 | status = self.assert_interrupts(command) | |||
|
155 | self.assertNotEqual( | |||
|
156 | status, 0, "The process wasn't interrupted. Status: %s" % (status,) | |||
|
157 | ) | |||
|
158 | ||||
110 | def test_getoutput(self): |
|
159 | def test_getoutput(self): | |
111 | out = getoutput('%s "%s"' % (python, self.fname)) |
|
160 | out = getoutput('%s "%s"' % (python, self.fname)) | |
112 | # we can't rely on the order the line buffered streams are flushed |
|
161 | # we can't rely on the order the line buffered streams are flushed | |
@@ -131,7 +180,7 b' class SubProcessTestCase(tt.TempFileMixin):' | |||||
131 | out, err = getoutputerror('%s "%s"' % (python, self.fname)) |
|
180 | out, err = getoutputerror('%s "%s"' % (python, self.fname)) | |
132 | self.assertEqual(out, 'on stdout') |
|
181 | self.assertEqual(out, 'on stdout') | |
133 | self.assertEqual(err, 'on stderr') |
|
182 | self.assertEqual(err, 'on stderr') | |
134 |
|
183 | |||
135 | def test_get_output_error_code(self): |
|
184 | def test_get_output_error_code(self): | |
136 | quiet_exit = '%s -c "import sys; sys.exit(1)"' % python |
|
185 | quiet_exit = '%s -c "import sys; sys.exit(1)"' % python | |
137 | out, err, code = get_output_error_code(quiet_exit) |
|
186 | out, err, code = get_output_error_code(quiet_exit) | |
@@ -142,3 +191,5 b' class SubProcessTestCase(tt.TempFileMixin):' | |||||
142 | self.assertEqual(out, 'on stdout') |
|
191 | self.assertEqual(out, 'on stdout') | |
143 | self.assertEqual(err, 'on stderr') |
|
192 | self.assertEqual(err, 'on stderr') | |
144 | self.assertEqual(code, 0) |
|
193 | self.assertEqual(code, 0) | |
|
194 | ||||
|
195 |
General Comments 0
You need to be logged in to leave comments.
Login now