##// 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 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
98
99 def stdout_read():
96 for line in read_no_interrupt(p.stdout).splitlines():
100 for line in read_no_interrupt(p.stdout).splitlines():
97 line = line.decode(enc, 'replace')
101 line = line.decode(enc, 'replace')
98 print(line, file=sys.stdout)
102 print(line, file=sys.stdout)
103
104 def stderr_read():
99 for line in read_no_interrupt(p.stderr).splitlines():
105 for line in read_no_interrupt(p.stderr).splitlines():
100 line = line.decode(enc, 'replace')
106 line = line.decode(enc, 'replace')
101 print(line, file=sys.stderr)
107 print(line, file=sys.stderr)
102
108
103 # Wait to finish for returncode
109 Thread(target=stdout_read).start()
104 return p.wait()
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
@@ -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