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