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