##// END OF EJS Templates
more lints
M Bussonnier -
Show More
@@ -1,220 +1,211
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
6 #-----------------------------------------------------------------------------
7 # Copyright (C) 2010-2011 The IPython Development Team
8 #
9 # Distributed under the terms of the BSD License. The full license is in
10 # the file COPYING, distributed as part of this software.
11 #-----------------------------------------------------------------------------
12
13 #-----------------------------------------------------------------------------
14 # Imports
15 #-----------------------------------------------------------------------------
16
17 # stdlib
18 import ctypes
5 import ctypes
19 import os
6 import os
20 import subprocess
7 import subprocess
21 import sys
8 import sys
22 import time
9 import time
23 from ctypes import POINTER, c_int
10 from ctypes import POINTER, c_int
24 from ctypes.wintypes import HLOCAL, LPCWSTR
11 from ctypes.wintypes import HLOCAL, LPCWSTR
25 from subprocess import STDOUT
12 from subprocess import STDOUT
26 from threading import Thread
13 from threading import Thread
27 from types import TracebackType
14 from types import TracebackType
28 from typing import IO, Any, List, Optional
15 from typing import List, Optional
29
16
30 from . import py3compat
17 from . import py3compat
31 from ._process_common import arg_split as py_arg_split
18 from ._process_common import arg_split as py_arg_split
32
19
33 # our own imports
34 from ._process_common import process_handler, read_no_interrupt
20 from ._process_common import process_handler, read_no_interrupt
35 from .encoding import DEFAULT_ENCODING
21 from .encoding import DEFAULT_ENCODING
36
22
37 #-----------------------------------------------------------------------------
23
38 # Function definitions
39 #-----------------------------------------------------------------------------
40
24
41 class AvoidUNCPath:
25 class AvoidUNCPath:
42 """A context manager to protect command execution from UNC paths.
26 """A context manager to protect command execution from UNC paths.
43
27
44 In the Win32 API, commands can't be invoked with the cwd being a UNC path.
28 In the Win32 API, commands can't be invoked with the cwd being a UNC path.
45 This context manager temporarily changes directory to the 'C:' drive on
29 This context manager temporarily changes directory to the 'C:' drive on
46 entering, and restores the original working directory on exit.
30 entering, and restores the original working directory on exit.
47
31
48 The context manager returns the starting working directory *if* it made a
32 The context manager returns the starting working directory *if* it made a
49 change and None otherwise, so that users can apply the necessary adjustment
33 change and None otherwise, so that users can apply the necessary adjustment
50 to their system calls in the event of a change.
34 to their system calls in the event of a change.
51
35
52 Examples
36 Examples
53 --------
37 --------
54 ::
38 ::
55 cmd = 'dir'
39 cmd = 'dir'
56 with AvoidUNCPath() as path:
40 with AvoidUNCPath() as path:
57 if path is not None:
41 if path is not None:
58 cmd = '"pushd %s &&"%s' % (path, cmd)
42 cmd = '"pushd %s &&"%s' % (path, cmd)
59 os.system(cmd)
43 os.system(cmd)
60 """
44 """
61
45
62 def __enter__(self) -> Optional[str]:
46 def __enter__(self) -> Optional[str]:
63 self.path = os.getcwd()
47 self.path = os.getcwd()
64 self.is_unc_path = self.path.startswith(r"\\")
48 self.is_unc_path = self.path.startswith(r"\\")
65 if self.is_unc_path:
49 if self.is_unc_path:
66 # change to c drive (as cmd.exe cannot handle UNC addresses)
50 # change to c drive (as cmd.exe cannot handle UNC addresses)
67 os.chdir("C:")
51 os.chdir("C:")
68 return self.path
52 return self.path
69 else:
53 else:
70 # We return None to signal that there was no change in the working
54 # We return None to signal that there was no change in the working
71 # directory
55 # directory
72 return None
56 return None
73
57
74 def __exit__(
58 def __exit__(
75 self, exc_type: Optional[type[BaseException]], exc_value: Optional[BaseException], traceback:TracebackType
59 self,
60 exc_type: Optional[type[BaseException]],
61 exc_value: Optional[BaseException],
62 traceback: TracebackType,
76 ) -> None:
63 ) -> None:
77 if self.is_unc_path:
64 if self.is_unc_path:
78 os.chdir(self.path)
65 os.chdir(self.path)
79
66
80
67
81 def _system_body(p: subprocess.Popen) -> int:
68 def _system_body(p: subprocess.Popen) -> int:
82 """Callback for _system."""
69 """Callback for _system."""
83 enc = DEFAULT_ENCODING
70 enc = DEFAULT_ENCODING
84
71
85 # Dec 2024: in both of these functions, I'm not sure why we .splitlines()
72 # 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
73 # the bytes and then decode each line individually instead of just decoding
87 # the whole thing at once.
74 # the whole thing at once.
88 def stdout_read() -> None:
75 def stdout_read() -> None:
89 try:
76 try:
90 assert p.stdout is not None
77 assert p.stdout is not None
91 for byte_line in read_no_interrupt(p.stdout).splitlines():
78 for byte_line in read_no_interrupt(p.stdout).splitlines():
92 line = byte_line.decode(enc, "replace")
79 line = byte_line.decode(enc, "replace")
93 print(line, file=sys.stdout)
80 print(line, file=sys.stdout)
94 except Exception as e:
81 except Exception as e:
95 print(f"Error reading stdout: {e}", file=sys.stderr)
82 print(f"Error reading stdout: {e}", file=sys.stderr)
96
83
97 def stderr_read() -> None:
84 def stderr_read() -> None:
98 try:
85 try:
99 assert p.stderr is not None
86 assert p.stderr is not None
100 for byte_line in read_no_interrupt(p.stderr).splitlines():
87 for byte_line in read_no_interrupt(p.stderr).splitlines():
101 line = byte_line.decode(enc, "replace")
88 line = byte_line.decode(enc, "replace")
102 print(line, file=sys.stderr)
89 print(line, file=sys.stderr)
103 except Exception as e:
90 except Exception as e:
104 print(f"Error reading stderr: {e}", file=sys.stderr)
91 print(f"Error reading stderr: {e}", file=sys.stderr)
105
92
106 stdout_thread = Thread(target=stdout_read)
93 stdout_thread = Thread(target=stdout_read)
107 stderr_thread = Thread(target=stderr_read)
94 stderr_thread = Thread(target=stderr_read)
108
95
109 stdout_thread.start()
96 stdout_thread.start()
110 stderr_thread.start()
97 stderr_thread.start()
111
98
112 # Wait to finish for returncode. Unfortunately, Python has a bug where
99 # Wait to finish for returncode. Unfortunately, Python has a bug where
113 # wait() isn't interruptible (https://bugs.python.org/issue28168) so poll in
100 # wait() isn't interruptible (https://bugs.python.org/issue28168) so poll in
114 # a loop instead of just doing `return p.wait()`
101 # a loop instead of just doing `return p.wait()`
115 while True:
102 while True:
116 result = p.poll()
103 result = p.poll()
117 if result is None:
104 if result is None:
118 time.sleep(0.01)
105 time.sleep(0.01)
119 else:
106 else:
120 break
107 break
121
108
122 # Join the threads to ensure they complete before returning
109 # Join the threads to ensure they complete before returning
123 stdout_thread.join()
110 stdout_thread.join()
124 stderr_thread.join()
111 stderr_thread.join()
125
112
126 return result
113 return result
127
114
128
115
129 def system(cmd: str) -> Optional[int]:
116 def system(cmd: str) -> Optional[int]:
130 """Win32 version of os.system() that works with network shares.
117 """Win32 version of os.system() that works with network shares.
131
118
132 Note that this implementation returns None, as meant for use in IPython.
119 Note that this implementation returns None, as meant for use in IPython.
133
120
134 Parameters
121 Parameters
135 ----------
122 ----------
136 cmd : str or list
123 cmd : str or list
137 A command to be executed in the system shell.
124 A command to be executed in the system shell.
138
125
139 Returns
126 Returns
140 -------
127 -------
141 int : child process' exit code.
128 int : child process' exit code.
142 """
129 """
143 # The controller provides interactivity with both
130 # The controller provides interactivity with both
144 # stdin and stdout
131 # stdin and stdout
145 #import _process_win32_controller
132 # import _process_win32_controller
146 #_process_win32_controller.system(cmd)
133 # _process_win32_controller.system(cmd)
147
134
148 with AvoidUNCPath() as path:
135 with AvoidUNCPath() as path:
149 if path is not None:
136 if path is not None:
150 cmd = '"pushd %s &&"%s' % (path, cmd)
137 cmd = '"pushd %s &&"%s' % (path, cmd)
151 return process_handler(cmd, _system_body)
138 return process_handler(cmd, _system_body)
152
139
140
153 def getoutput(cmd: str) -> str:
141 def getoutput(cmd: str) -> str:
154 """Return standard output of executing cmd in a shell.
142 """Return standard output of executing cmd in a shell.
155
143
156 Accepts the same arguments as os.system().
144 Accepts the same arguments as os.system().
157
145
158 Parameters
146 Parameters
159 ----------
147 ----------
160 cmd : str or list
148 cmd : str or list
161 A command to be executed in the system shell.
149 A command to be executed in the system shell.
162
150
163 Returns
151 Returns
164 -------
152 -------
165 stdout : str
153 stdout : str
166 """
154 """
167
155
168 with AvoidUNCPath() as path:
156 with AvoidUNCPath() as path:
169 if path is not None:
157 if path is not None:
170 cmd = '"pushd %s &&"%s' % (path, cmd)
158 cmd = '"pushd %s &&"%s' % (path, cmd)
171 out = process_handler(cmd, lambda p: p.communicate()[0], STDOUT)
159 out = process_handler(cmd, lambda p: p.communicate()[0], STDOUT)
172
160
173 if out is None:
161 if out is None:
174 out = b''
162 out = b""
175 return py3compat.decode(out)
163 return py3compat.decode(out)
176
164
165
177 try:
166 try:
178 windll = ctypes.windll # type: ignore [attr-defined]
167 windll = ctypes.windll # type: ignore [attr-defined]
179 CommandLineToArgvW = windll.shell32.CommandLineToArgvW
168 CommandLineToArgvW = windll.shell32.CommandLineToArgvW
180 CommandLineToArgvW.arg_types = [LPCWSTR, POINTER(c_int)]
169 CommandLineToArgvW.arg_types = [LPCWSTR, POINTER(c_int)]
181 CommandLineToArgvW.restype = POINTER(LPCWSTR)
170 CommandLineToArgvW.restype = POINTER(LPCWSTR)
182 LocalFree = windll.kernel32.LocalFree
171 LocalFree = windll.kernel32.LocalFree
183 LocalFree.res_type = HLOCAL
172 LocalFree.res_type = HLOCAL
184 LocalFree.arg_types = [HLOCAL]
173 LocalFree.arg_types = [HLOCAL]
185
174
186 def arg_split(
175 def arg_split(
187 commandline: str, posix: bool = False, strict: bool = True
176 commandline: str, posix: bool = False, strict: bool = True
188 ) -> List[str]:
177 ) -> List[str]:
189 """Split a command line's arguments in a shell-like manner.
178 """Split a command line's arguments in a shell-like manner.
190
179
191 This is a special version for windows that use a ctypes call to CommandLineToArgvW
180 This is a special version for windows that use a ctypes call to CommandLineToArgvW
192 to do the argv splitting. The posix parameter is ignored.
181 to do the argv splitting. The posix parameter is ignored.
193
182
194 If strict=False, process_common.arg_split(...strict=False) is used instead.
183 If strict=False, process_common.arg_split(...strict=False) is used instead.
195 """
184 """
196 #CommandLineToArgvW returns path to executable if called with empty string.
185 # CommandLineToArgvW returns path to executable if called with empty string.
197 if commandline.strip() == "":
186 if commandline.strip() == "":
198 return []
187 return []
199 if not strict:
188 if not strict:
200 # not really a cl-arg, fallback on _process_common
189 # not really a cl-arg, fallback on _process_common
201 return py_arg_split(commandline, posix=posix, strict=strict)
190 return py_arg_split(commandline, posix=posix, strict=strict)
202 argvn = c_int()
191 argvn = c_int()
203 result_pointer = CommandLineToArgvW(commandline.lstrip(), ctypes.byref(argvn))
192 result_pointer = CommandLineToArgvW(commandline.lstrip(), ctypes.byref(argvn))
204 result_array_type = LPCWSTR * argvn.value
193 result_array_type = LPCWSTR * argvn.value
205 result = [
194 result = [
206 arg
195 arg
207 for arg in result_array_type.from_address(
196 for arg in result_array_type.from_address(
208 ctypes.addressof(result_pointer.contents)
197 ctypes.addressof(result_pointer.contents)
209 )
198 )
210 if arg is not None
199 if arg is not None
211 ]
200 ]
212 LocalFree(result_pointer)
201 # for side effects
202 _ = LocalFree(result_pointer)
213 return result
203 return result
214 except AttributeError:
204 except AttributeError:
215 arg_split = py_arg_split
205 arg_split = py_arg_split
216
206
207
217 def check_pid(pid: int) -> bool:
208 def check_pid(pid: int) -> bool:
218 # OpenProcess returns 0 if no such process (of ours) exists
209 # OpenProcess returns 0 if no such process (of ours) exists
219 # positive int otherwise
210 # positive int otherwise
220 return bool(windll.kernel32.OpenProcess(1, 0, pid))
211 return bool(windll.kernel32.OpenProcess(1, 0, pid))
General Comments 0
You need to be logged in to leave comments. Login now