##// END OF EJS Templates
Short in the dark windows issue
M Bussonnier -
Show More
@@ -1,210 +1,213
1 1 """Common utilities for the various process_* implementations.
2 2
3 3 This file is only meant to be imported by the platform-specific implementations
4 4 of subprocess utilities, and it contains tools that are common to all of them.
5 5 """
6 6
7 7 #-----------------------------------------------------------------------------
8 8 # Copyright (C) 2010-2011 The IPython Development Team
9 9 #
10 10 # Distributed under the terms of the BSD License. The full license is in
11 11 # the file COPYING, distributed as part of this software.
12 12 #-----------------------------------------------------------------------------
13 13
14 14 #-----------------------------------------------------------------------------
15 15 # Imports
16 16 #-----------------------------------------------------------------------------
17 17 import subprocess
18 18 import shlex
19 19 import sys
20 20 import os
21
21 from typing import Callable, Optional, Union, List
22 22 from IPython.utils import py3compat
23 23
24 24 #-----------------------------------------------------------------------------
25 25 # Function definitions
26 26 #-----------------------------------------------------------------------------
27 27
28 def read_no_interrupt(p):
28 def read_no_interrupt(p: subprocess.Popen):
29 29 """Read from a pipe ignoring EINTR errors.
30 30
31 31 This is necessary because when reading from pipes with GUI event loops
32 32 running in the background, often interrupts are raised that stop the
33 33 command from completing."""
34 34 import errno
35 35
36 36 try:
37 37 return p.read()
38 38 except IOError as err:
39 39 if err.errno != errno.EINTR:
40 40 raise
41 41
42 42
43 def process_handler(cmd, callback, stderr=subprocess.PIPE):
43 def process_handler(cmd:Union[str, List[str]], callback:Callable[[subprocess.Popen], int], stderr=subprocess.PIPE) -> Optional[int]:
44 44 """Open a command in a shell subprocess and execute a callback.
45 45
46 46 This function provides common scaffolding for creating subprocess.Popen()
47 47 calls. It creates a Popen object and then calls the callback with it.
48 48
49 49 Parameters
50 50 ----------
51 51 cmd : str or list
52 52 A command to be executed by the system, using :class:`subprocess.Popen`.
53 53 If a string is passed, it will be run in the system shell. If a list is
54 54 passed, it will be used directly as arguments.
55 55 callback : callable
56 56 A one-argument function that will be called with the Popen object.
57 57 stderr : file descriptor number, optional
58 58 By default this is set to ``subprocess.PIPE``, but you can also pass the
59 59 value ``subprocess.STDOUT`` to force the subprocess' stderr to go into
60 60 the same file descriptor as its stdout. This is useful to read stdout
61 61 and stderr combined in the order they are generated.
62 62
63 63 Returns
64 64 -------
65 65 The return value of the provided callback is returned.
66 66 """
67 67 sys.stdout.flush()
68 68 sys.stderr.flush()
69 69 # On win32, close_fds can't be true when using pipes for stdin/out/err
70 close_fds = sys.platform != 'win32'
70 if sys.platform == "win32" and stderr != subprocess.PIPE:
71 close_fds = False
72 else:
73 close_fds = True
71 74 # Determine if cmd should be run with system shell.
72 75 shell = isinstance(cmd, str)
73 76 # On POSIX systems run shell commands with user-preferred shell.
74 77 executable = None
75 78 if shell and os.name == 'posix' and 'SHELL' in os.environ:
76 79 executable = os.environ['SHELL']
77 80 p = subprocess.Popen(cmd, shell=shell,
78 81 executable=executable,
79 82 stdin=subprocess.PIPE,
80 83 stdout=subprocess.PIPE,
81 84 stderr=stderr,
82 85 close_fds=close_fds)
83 86
84 87 try:
85 88 out = callback(p)
86 89 except KeyboardInterrupt:
87 90 print('^C')
88 91 sys.stdout.flush()
89 92 sys.stderr.flush()
90 93 out = None
91 94 finally:
92 95 # Make really sure that we don't leave processes behind, in case the
93 96 # call above raises an exception
94 97 # We start by assuming the subprocess finished (to avoid NameErrors
95 98 # later depending on the path taken)
96 99 if p.returncode is None:
97 100 try:
98 101 p.terminate()
99 102 p.poll()
100 103 except OSError:
101 104 pass
102 105 # One last try on our way out
103 106 if p.returncode is None:
104 107 try:
105 108 p.kill()
106 109 except OSError:
107 110 pass
108 111
109 112 return out
110 113
111 114
112 115 def getoutput(cmd):
113 116 """Run a command and return its stdout/stderr as a string.
114 117
115 118 Parameters
116 119 ----------
117 120 cmd : str or list
118 121 A command to be executed in the system shell.
119 122
120 123 Returns
121 124 -------
122 125 output : str
123 126 A string containing the combination of stdout and stderr from the
124 127 subprocess, in whatever order the subprocess originally wrote to its
125 128 file descriptors (so the order of the information in this string is the
126 129 correct order as would be seen if running the command in a terminal).
127 130 """
128 131 out = process_handler(cmd, lambda p: p.communicate()[0], subprocess.STDOUT)
129 132 if out is None:
130 133 return ''
131 134 return py3compat.decode(out)
132 135
133 136
134 137 def getoutputerror(cmd):
135 138 """Return (standard output, standard error) of executing cmd in a shell.
136 139
137 140 Accepts the same arguments as os.system().
138 141
139 142 Parameters
140 143 ----------
141 144 cmd : str or list
142 145 A command to be executed in the system shell.
143 146
144 147 Returns
145 148 -------
146 149 stdout : str
147 150 stderr : str
148 151 """
149 152 return get_output_error_code(cmd)[:2]
150 153
151 154 def get_output_error_code(cmd):
152 155 """Return (standard output, standard error, return code) of executing cmd
153 156 in a shell.
154 157
155 158 Accepts the same arguments as os.system().
156 159
157 160 Parameters
158 161 ----------
159 162 cmd : str or list
160 163 A command to be executed in the system shell.
161 164
162 165 Returns
163 166 -------
164 167 stdout : str
165 168 stderr : str
166 169 returncode: int
167 170 """
168 171
169 172 out_err, p = process_handler(cmd, lambda p: (p.communicate(), p))
170 173 if out_err is None:
171 174 return '', '', p.returncode
172 175 out, err = out_err
173 176 return py3compat.decode(out), py3compat.decode(err), p.returncode
174 177
175 178 def arg_split(s, posix=False, strict=True):
176 179 """Split a command line's arguments in a shell-like manner.
177 180
178 181 This is a modified version of the standard library's shlex.split()
179 182 function, but with a default of posix=False for splitting, so that quotes
180 183 in inputs are respected.
181 184
182 185 if strict=False, then any errors shlex.split would raise will result in the
183 186 unparsed remainder being the last element of the list, rather than raising.
184 187 This is because we sometimes use arg_split to parse things other than
185 188 command-line args.
186 189 """
187 190
188 191 lex = shlex.shlex(s, posix=posix)
189 192 lex.whitespace_split = True
190 193 # Extract tokens, ensuring that things like leaving open quotes
191 194 # does not cause this to raise. This is important, because we
192 195 # sometimes pass Python source through this (e.g. %timeit f(" ")),
193 196 # and it shouldn't raise an exception.
194 197 # It may be a bad idea to parse things that are not command-line args
195 198 # through this function, but we do, so let's be safe about it.
196 199 lex.commenters='' #fix for GH-1269
197 200 tokens = []
198 201 while True:
199 202 try:
200 203 tokens.append(next(lex))
201 204 except StopIteration:
202 205 break
203 206 except ValueError:
204 207 if strict:
205 208 raise
206 209 # couldn't parse, get remaining blob as last token
207 210 tokens.append(lex.token)
208 211 break
209 212
210 213 return tokens
@@ -1,184 +1,200
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 21 import time
22 22
23 23 from ctypes import c_int, POINTER
24 24 from ctypes.wintypes import LPCWSTR, HLOCAL
25 25 from subprocess import STDOUT, TimeoutExpired
26 26 from threading import Thread
27 import subprocess
27 28
28 29 # our own imports
29 30 from ._process_common import read_no_interrupt, process_handler, arg_split as py_arg_split
30 31 from . import py3compat
31 32 from .encoding import DEFAULT_ENCODING
32 33
33 34 #-----------------------------------------------------------------------------
34 35 # Function definitions
35 36 #-----------------------------------------------------------------------------
36 37
37 class AvoidUNCPath(object):
38 class AvoidUNCPath:
38 39 """A context manager to protect command execution from UNC paths.
39 40
40 41 In the Win32 API, commands can't be invoked with the cwd being a UNC path.
41 42 This context manager temporarily changes directory to the 'C:' drive on
42 43 entering, and restores the original working directory on exit.
43 44
44 45 The context manager returns the starting working directory *if* it made a
45 46 change and None otherwise, so that users can apply the necessary adjustment
46 47 to their system calls in the event of a change.
47 48
48 49 Examples
49 50 --------
50 51 ::
51 52 cmd = 'dir'
52 53 with AvoidUNCPath() as path:
53 54 if path is not None:
54 55 cmd = '"pushd %s &&"%s' % (path, cmd)
55 56 os.system(cmd)
56 57 """
57 58 def __enter__(self):
58 59 self.path = os.getcwd()
59 60 self.is_unc_path = self.path.startswith(r"\\")
60 61 if self.is_unc_path:
61 62 # change to c drive (as cmd.exe cannot handle UNC addresses)
62 63 os.chdir("C:")
63 64 return self.path
64 65 else:
65 66 # We return None to signal that there was no change in the working
66 67 # directory
67 68 return None
68 69
69 70 def __exit__(self, exc_type, exc_value, traceback):
70 71 if self.is_unc_path:
71 72 os.chdir(self.path)
72 73
73 74
74 def _system_body(p):
75 def _system_body(p: subprocess.Popen) -> int:
75 76 """Callback for _system."""
76 77 enc = DEFAULT_ENCODING
77 78
78 79 def stdout_read():
79 for line in read_no_interrupt(p.stdout).splitlines():
80 line = line.decode(enc, 'replace')
81 print(line, file=sys.stdout)
80 try:
81 for line in read_no_interrupt(p.stdout).splitlines():
82 line = line.decode(enc, 'replace')
83 print(line, file=sys.stdout)
84 except Exception as e:
85 print(f"Error reading stdout: {e}", file=sys.stderr)
82 86
83 87 def stderr_read():
84 for line in read_no_interrupt(p.stderr).splitlines():
85 line = line.decode(enc, 'replace')
86 print(line, file=sys.stderr)
88 try:
89 for line in read_no_interrupt(p.stderr).splitlines():
90 line = line.decode(enc, 'replace')
91 print(line, file=sys.stderr)
92 except Exception as e:
93 print(f"Error reading stderr: {e}", file=sys.stderr)
87 94
88 Thread(target=stdout_read).start()
89 Thread(target=stderr_read).start()
95 stdout_thread = Thread(target=stdout_read)
96 stderr_thread = Thread(target=stderr_read)
97
98 stdout_thread.start()
99 stderr_thread.start()
90 100
91 101 # Wait to finish for returncode. Unfortunately, Python has a bug where
92 102 # wait() isn't interruptible (https://bugs.python.org/issue28168) so poll in
93 # a loop instead of just doing `return p.wait()`.
103 # a loop instead of just doing `return p.wait()`
94 104 while True:
95 105 result = p.poll()
96 106 if result is None:
97 107 time.sleep(0.01)
98 108 else:
99 return result
109 break
110
111 # Join the threads to ensure they complete before returning
112 stdout_thread.join()
113 stderr_thread.join()
114
115 return result
100 116
101 117
102 def system(cmd):
118 def system(cmd: str):
103 119 """Win32 version of os.system() that works with network shares.
104 120
105 121 Note that this implementation returns None, as meant for use in IPython.
106 122
107 123 Parameters
108 124 ----------
109 125 cmd : str or list
110 126 A command to be executed in the system shell.
111 127
112 128 Returns
113 129 -------
114 130 int : child process' exit code.
115 131 """
116 132 # The controller provides interactivity with both
117 133 # stdin and stdout
118 134 #import _process_win32_controller
119 135 #_process_win32_controller.system(cmd)
120 136
121 137 with AvoidUNCPath() as path:
122 138 if path is not None:
123 139 cmd = '"pushd %s &&"%s' % (path, cmd)
124 140 return process_handler(cmd, _system_body)
125 141
126 142 def getoutput(cmd):
127 143 """Return standard output of executing cmd in a shell.
128 144
129 145 Accepts the same arguments as os.system().
130 146
131 147 Parameters
132 148 ----------
133 149 cmd : str or list
134 150 A command to be executed in the system shell.
135 151
136 152 Returns
137 153 -------
138 154 stdout : str
139 155 """
140 156
141 157 with AvoidUNCPath() as path:
142 158 if path is not None:
143 159 cmd = '"pushd %s &&"%s' % (path, cmd)
144 160 out = process_handler(cmd, lambda p: p.communicate()[0], STDOUT)
145 161
146 162 if out is None:
147 163 out = b''
148 164 return py3compat.decode(out)
149 165
150 166 try:
151 167 CommandLineToArgvW = ctypes.windll.shell32.CommandLineToArgvW
152 168 CommandLineToArgvW.arg_types = [LPCWSTR, POINTER(c_int)]
153 169 CommandLineToArgvW.restype = POINTER(LPCWSTR)
154 170 LocalFree = ctypes.windll.kernel32.LocalFree
155 171 LocalFree.res_type = HLOCAL
156 172 LocalFree.arg_types = [HLOCAL]
157 173
158 174 def arg_split(commandline, posix=False, strict=True):
159 175 """Split a command line's arguments in a shell-like manner.
160 176
161 177 This is a special version for windows that use a ctypes call to CommandLineToArgvW
162 178 to do the argv splitting. The posix parameter is ignored.
163 179
164 180 If strict=False, process_common.arg_split(...strict=False) is used instead.
165 181 """
166 182 #CommandLineToArgvW returns path to executable if called with empty string.
167 183 if commandline.strip() == "":
168 184 return []
169 185 if not strict:
170 186 # not really a cl-arg, fallback on _process_common
171 187 return py_arg_split(commandline, posix=posix, strict=strict)
172 188 argvn = c_int()
173 189 result_pointer = CommandLineToArgvW(commandline.lstrip(), ctypes.byref(argvn))
174 190 result_array_type = LPCWSTR * argvn.value
175 191 result = [arg for arg in result_array_type.from_address(ctypes.addressof(result_pointer.contents))]
176 192 retval = LocalFree(result_pointer)
177 193 return result
178 194 except AttributeError:
179 195 arg_split = py_arg_split
180 196
181 197 def check_pid(pid):
182 198 # OpenProcess returns 0 if no such process (of ours) exists
183 199 # positive int otherwise
184 200 return bool(ctypes.windll.kernel32.OpenProcess(1,0,pid))
General Comments 0
You need to be logged in to leave comments. Login now