##// END OF EJS Templates
Backport PR #12137: Fix inability to interrupt processes on Windows
Matthias Bussonnier -
Show More
@@ -18,10 +18,12 b' This file is only meant to be imported by process.py, not by end-users.'
18 18 import os
19 19 import sys
20 20 import ctypes
21 import time
21 22
22 23 from ctypes import c_int, POINTER
23 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 28 # our own imports
27 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 95 def _system_body(p):
94 96 """Callback for _system."""
95 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
104 return p.wait()
99 def stdout_read():
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 123 def system(cmd):
@@ -116,9 +132,7 b' def system(cmd):'
116 132
117 133 Returns
118 134 -------
119 None : we explicitly do NOT return the subprocess status code, as this
120 utility is meant to be used extensively in IPython, where any return value
121 would trigger :func:`sys.displayhook` calls.
135 int : child process' exit code.
122 136 """
123 137 # The controller provides interactivity with both
124 138 # stdin and stdout
@@ -15,13 +15,19 b' Tests for platutils.py'
15 15 #-----------------------------------------------------------------------------
16 16
17 17 import sys
18 import signal
18 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 25 import nose.tools as nt
21 26
22 27 from IPython.utils.process import (find_cmd, FindCmdError, arg_split,
23 28 system, getoutput, getoutputerror,
24 29 get_output_error_code)
30 from IPython.utils.capture import capture_output
25 31 from IPython.testing import decorators as dec
26 32 from IPython.testing import tools as tt
27 33
@@ -107,6 +113,49 b' class SubProcessTestCase(tt.TempFileMixin):'
107 113 status = system('%s -c "import sys"' % python)
108 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 159 def test_getoutput(self):
111 160 out = getoutput('%s "%s"' % (python, self.fname))
112 161 # we can't rely on the order the line buffered streams are flushed
@@ -131,7 +180,7 b' class SubProcessTestCase(tt.TempFileMixin):'
131 180 out, err = getoutputerror('%s "%s"' % (python, self.fname))
132 181 self.assertEqual(out, 'on stdout')
133 182 self.assertEqual(err, 'on stderr')
134
183
135 184 def test_get_output_error_code(self):
136 185 quiet_exit = '%s -c "import sys; sys.exit(1)"' % python
137 186 out, err, code = get_output_error_code(quiet_exit)
@@ -142,3 +191,5 b' class SubProcessTestCase(tt.TempFileMixin):'
142 191 self.assertEqual(out, 'on stdout')
143 192 self.assertEqual(err, 'on stderr')
144 193 self.assertEqual(code, 0)
194
195
General Comments 0
You need to be logged in to leave comments. Login now