##// END OF EJS Templates
Backport PR #12137: Fix inability to interrupt processes on Windows
Matthias Bussonnier -
Show More
@@ -1,191 +1,205 b''
1 1 """Windows-specific implementation of process utilities.
2 2
3 3 This file is only meant to be imported by process.py, not by end-users.
4 4 """
5 5
6 6 #-----------------------------------------------------------------------------
7 7 # Copyright (C) 2010-2011 The IPython Development Team
8 8 #
9 9 # Distributed under the terms of the BSD License. The full license is in
10 10 # the file COPYING, distributed as part of this software.
11 11 #-----------------------------------------------------------------------------
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16
17 17 # stdlib
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
28 30 from . import py3compat
29 31 from .encoding import DEFAULT_ENCODING
30 32
31 33 #-----------------------------------------------------------------------------
32 34 # Function definitions
33 35 #-----------------------------------------------------------------------------
34 36
35 37 class AvoidUNCPath(object):
36 38 """A context manager to protect command execution from UNC paths.
37 39
38 40 In the Win32 API, commands can't be invoked with the cwd being a UNC path.
39 41 This context manager temporarily changes directory to the 'C:' drive on
40 42 entering, and restores the original working directory on exit.
41 43
42 44 The context manager returns the starting working directory *if* it made a
43 45 change and None otherwise, so that users can apply the necessary adjustment
44 46 to their system calls in the event of a change.
45 47
46 48 Examples
47 49 --------
48 50 ::
49 51 cmd = 'dir'
50 52 with AvoidUNCPath() as path:
51 53 if path is not None:
52 54 cmd = '"pushd %s &&"%s' % (path, cmd)
53 55 os.system(cmd)
54 56 """
55 57 def __enter__(self):
56 58 self.path = os.getcwd()
57 59 self.is_unc_path = self.path.startswith(r"\\")
58 60 if self.is_unc_path:
59 61 # change to c drive (as cmd.exe cannot handle UNC addresses)
60 62 os.chdir("C:")
61 63 return self.path
62 64 else:
63 65 # We return None to signal that there was no change in the working
64 66 # directory
65 67 return None
66 68
67 69 def __exit__(self, exc_type, exc_value, traceback):
68 70 if self.is_unc_path:
69 71 os.chdir(self.path)
70 72
71 73
72 74 def _find_cmd(cmd):
73 75 """Find the full path to a .bat or .exe using the win32api module."""
74 76 try:
75 77 from win32api import SearchPath
76 78 except ImportError:
77 79 raise ImportError('you need to have pywin32 installed for this to work')
78 80 else:
79 81 PATH = os.environ['PATH']
80 82 extensions = ['.exe', '.com', '.bat', '.py']
81 83 path = None
82 84 for ext in extensions:
83 85 try:
84 86 path = SearchPath(PATH, cmd, ext)[0]
85 87 except:
86 88 pass
87 89 if path is None:
88 90 raise OSError("command %r not found" % cmd)
89 91 else:
90 92 return path
91 93
92 94
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):
108 124 """Win32 version of os.system() that works with network shares.
109 125
110 126 Note that this implementation returns None, as meant for use in IPython.
111 127
112 128 Parameters
113 129 ----------
114 130 cmd : str or list
115 131 A command to be executed in the system shell.
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
125 139 #import _process_win32_controller
126 140 #_process_win32_controller.system(cmd)
127 141
128 142 with AvoidUNCPath() as path:
129 143 if path is not None:
130 144 cmd = '"pushd %s &&"%s' % (path, cmd)
131 145 return process_handler(cmd, _system_body)
132 146
133 147 def getoutput(cmd):
134 148 """Return standard output of executing cmd in a shell.
135 149
136 150 Accepts the same arguments as os.system().
137 151
138 152 Parameters
139 153 ----------
140 154 cmd : str or list
141 155 A command to be executed in the system shell.
142 156
143 157 Returns
144 158 -------
145 159 stdout : str
146 160 """
147 161
148 162 with AvoidUNCPath() as path:
149 163 if path is not None:
150 164 cmd = '"pushd %s &&"%s' % (path, cmd)
151 165 out = process_handler(cmd, lambda p: p.communicate()[0], STDOUT)
152 166
153 167 if out is None:
154 168 out = b''
155 169 return py3compat.decode(out)
156 170
157 171 try:
158 172 CommandLineToArgvW = ctypes.windll.shell32.CommandLineToArgvW
159 173 CommandLineToArgvW.arg_types = [LPCWSTR, POINTER(c_int)]
160 174 CommandLineToArgvW.restype = POINTER(LPCWSTR)
161 175 LocalFree = ctypes.windll.kernel32.LocalFree
162 176 LocalFree.res_type = HLOCAL
163 177 LocalFree.arg_types = [HLOCAL]
164 178
165 179 def arg_split(commandline, posix=False, strict=True):
166 180 """Split a command line's arguments in a shell-like manner.
167 181
168 182 This is a special version for windows that use a ctypes call to CommandLineToArgvW
169 183 to do the argv splitting. The posix parameter is ignored.
170 184
171 185 If strict=False, process_common.arg_split(...strict=False) is used instead.
172 186 """
173 187 #CommandLineToArgvW returns path to executable if called with empty string.
174 188 if commandline.strip() == "":
175 189 return []
176 190 if not strict:
177 191 # not really a cl-arg, fallback on _process_common
178 192 return py_arg_split(commandline, posix=posix, strict=strict)
179 193 argvn = c_int()
180 194 result_pointer = CommandLineToArgvW(py3compat.cast_unicode(commandline.lstrip()), ctypes.byref(argvn))
181 195 result_array_type = LPCWSTR * argvn.value
182 196 result = [arg for arg in result_array_type.from_address(ctypes.addressof(result_pointer.contents))]
183 197 retval = LocalFree(result_pointer)
184 198 return result
185 199 except AttributeError:
186 200 arg_split = py_arg_split
187 201
188 202 def check_pid(pid):
189 203 # OpenProcess returns 0 if no such process (of ours) exists
190 204 # positive int otherwise
191 205 return bool(ctypes.windll.kernel32.OpenProcess(1,0,pid))
@@ -1,144 +1,195 b''
1 1 # encoding: utf-8
2 2 """
3 3 Tests for platutils.py
4 4 """
5 5
6 6 #-----------------------------------------------------------------------------
7 7 # Copyright (C) 2008-2011 The IPython Development Team
8 8 #
9 9 # Distributed under the terms of the BSD License. The full license is in
10 10 # the file COPYING, distributed as part of this software.
11 11 #-----------------------------------------------------------------------------
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
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
28 34 python = os.path.basename(sys.executable)
29 35
30 36 #-----------------------------------------------------------------------------
31 37 # Tests
32 38 #-----------------------------------------------------------------------------
33 39
34 40
35 41 @dec.skip_win32
36 42 def test_find_cmd_ls():
37 43 """Make sure we can find the full path to ls."""
38 44 path = find_cmd('ls')
39 45 nt.assert_true(path.endswith('ls'))
40 46
41 47
42 48 def has_pywin32():
43 49 try:
44 50 import win32api
45 51 except ImportError:
46 52 return False
47 53 return True
48 54
49 55
50 56 @dec.onlyif(has_pywin32, "This test requires win32api to run")
51 57 def test_find_cmd_pythonw():
52 58 """Try to find pythonw on Windows."""
53 59 path = find_cmd('pythonw')
54 60 assert path.lower().endswith('pythonw.exe'), path
55 61
56 62
57 63 @dec.onlyif(lambda : sys.platform != 'win32' or has_pywin32(),
58 64 "This test runs on posix or in win32 with win32api installed")
59 65 def test_find_cmd_fail():
60 66 """Make sure that FindCmdError is raised if we can't find the cmd."""
61 67 nt.assert_raises(FindCmdError,find_cmd,'asdfasdf')
62 68
63 69
64 70 @dec.skip_win32
65 71 def test_arg_split():
66 72 """Ensure that argument lines are correctly split like in a shell."""
67 73 tests = [['hi', ['hi']],
68 74 [u'hi', [u'hi']],
69 75 ['hello there', ['hello', 'there']],
70 76 # \u01ce == \N{LATIN SMALL LETTER A WITH CARON}
71 77 # Do not use \N because the tests crash with syntax error in
72 78 # some cases, for example windows python2.6.
73 79 [u'h\u01cello', [u'h\u01cello']],
74 80 ['something "with quotes"', ['something', '"with quotes"']],
75 81 ]
76 82 for argstr, argv in tests:
77 83 nt.assert_equal(arg_split(argstr), argv)
78 84
79 85 @dec.skip_if_not_win32
80 86 def test_arg_split_win32():
81 87 """Ensure that argument lines are correctly split like in a shell."""
82 88 tests = [['hi', ['hi']],
83 89 [u'hi', [u'hi']],
84 90 ['hello there', ['hello', 'there']],
85 91 [u'h\u01cello', [u'h\u01cello']],
86 92 ['something "with quotes"', ['something', 'with quotes']],
87 93 ]
88 94 for argstr, argv in tests:
89 95 nt.assert_equal(arg_split(argstr), argv)
90 96
91 97
92 98 class SubProcessTestCase(tt.TempFileMixin):
93 99 def setUp(self):
94 100 """Make a valid python temp file."""
95 101 lines = [ "import sys",
96 102 "print('on stdout', end='', file=sys.stdout)",
97 103 "print('on stderr', end='', file=sys.stderr)",
98 104 "sys.stdout.flush()",
99 105 "sys.stderr.flush()"]
100 106 self.mktmp('\n'.join(lines))
101 107
102 108 def test_system(self):
103 109 status = system('%s "%s"' % (python, self.fname))
104 110 self.assertEqual(status, 0)
105 111
106 112 def test_system_quotes(self):
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
113 162 try:
114 163 self.assertEqual(out, 'on stderron stdout')
115 164 except AssertionError:
116 165 self.assertEqual(out, 'on stdouton stderr')
117 166
118 167 def test_getoutput_quoted(self):
119 168 out = getoutput('%s -c "print (1)"' % python)
120 169 self.assertEqual(out.strip(), '1')
121 170
122 171 #Invalid quoting on windows
123 172 @dec.skip_win32
124 173 def test_getoutput_quoted2(self):
125 174 out = getoutput("%s -c 'print (1)'" % python)
126 175 self.assertEqual(out.strip(), '1')
127 176 out = getoutput("%s -c 'print (\"1\")'" % python)
128 177 self.assertEqual(out.strip(), '1')
129 178
130 179 def test_getoutput_error(self):
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)
138 187 self.assertEqual(out, '')
139 188 self.assertEqual(err, '')
140 189 self.assertEqual(code, 1)
141 190 out, err, code = get_output_error_code('%s "%s"' % (python, self.fname))
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