| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent da54523 commit 07362e7
2 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -13,7 +13,6 @@ | |||
| 13 | 13 | import logging | |
| 14 | 14 | import os | |
| 15 | 15 | import re | |
| 16 | - import signal | ||
| 17 | 16 | import subprocess | |
| 18 | 17 | from subprocess import DEVNULL, PIPE, Popen | |
| 19 | 18 | import sys | |
@@ -110,7 +109,7 @@ def handle_process_output( | |||
| 110 | 109 | stderr_handler: Union[None, Callable[[AnyStr], None], Callable[[List[AnyStr]], None]], | |
| 111 | 110 | finalizer: Union[None, Callable[[Union[Popen, "Git.AutoInterrupt"]], None]] = None, | |
| 112 | 111 | decode_streams: bool = True, | |
| 113 | - kill_after_timeout: Union[None, float] = None, | ||
| 112 | + kill_after_timeout: float | None = None, | ||
| 114 | 113 | ) -> None: | |
| 115 | 114 | R"""Register for notifications to learn that process output is ready to read, and | |
| 116 | 115 | dispatch lines to the respective line handlers. | |
@@ -139,7 +138,7 @@ def handle_process_output( | |||
| 139 | 138 | - decoding must happen later, such as for :class:`~git.diff.Diff`\s. | |
| 140 | 139 | ||
| 141 | 140 | :param kill_after_timeout: | |
| 142 | - :class:`float` or ``None``, Default = ``None`` | ||
| 141 | + :class:``int``, ``float``, or ``None`` (block indefinitely), Default = ``None``. | ||
| 143 | 142 | ||
| 144 | 143 | To specify a timeout in seconds for the git command, after which the process | |
| 145 | 144 | should be killed. | |
@@ -326,16 +325,22 @@ class _AutoInterrupt: | |||
| 326 | 325 | raise. | |
| 327 | 326 | """ | |
| 328 | 327 | ||
| 329 | - __slots__ = ("proc", "args", "status") | ||
| 328 | + __slots__ = ("proc", "args", "status", "timeout") | ||
| 330 | 329 | ||
| 331 | 330 | # If this is non-zero it will override any status code during _terminate, used | |
| 332 | 331 | # to prevent race conditions in testing. | |
| 333 | 332 | _status_code_if_terminate: int = 0 | |
| 334 | 333 | ||
| 335 | - def __init__(self, proc: Union[None, subprocess.Popen], args: Any) -> None: | ||
| 334 | + def __init__( | ||
| 335 | + self, | ||
| 336 | + proc: subprocess.Popen | None, | ||
| 337 | + args: Any, | ||
| 338 | + timeout: float | None = None, | ||
| 339 | + ) -> None: | ||
| 336 | 340 | self.proc = proc | |
| 337 | 341 | self.args = args | |
| 338 | 342 | self.status: Union[int, None] = None | |
| 343 | + self.timeout = timeout | ||
| 339 | 344 | ||
| 340 | 345 | def _terminate(self) -> None: | |
| 341 | 346 | """Terminate the underlying process.""" | |
@@ -365,15 +370,16 @@ def _terminate(self) -> None: | |||
| 365 | 370 | # Try to kill it. | |
| 366 | 371 | try: | |
| 367 | 372 | proc.terminate() | |
| 368 | - status = proc.wait() # Ensure the process goes away. | ||
| 373 | + status = proc.wait(timeout=self.timeout) # Ensure the process goes away. | ||
| 369 | 374 | ||
| 370 | 375 | self.status = self._status_code_if_terminate or status | |
| 371 | - except (OSError, AttributeError) as ex: | ||
| 376 | + except (OSError, AttributeError, subprocess.TimeoutExpired) as ex: | ||
| 372 | 377 | # On interpreter shutdown (notably on Windows), parts of the stdlib used by | |
| 373 | 378 | # subprocess can already be torn down (e.g. `subprocess._winapi` becomes None), | |
| 374 | 379 | # which can cause AttributeError during terminate(). In that case, we prefer | |
| 375 | 380 | # to silently ignore to avoid noisy "Exception ignored in: __del__" messages. | |
| 376 | 381 | _logger.info("Ignored error while terminating process: %r", ex) | |
| 382 | + | ||
| 377 | 383 | # END exception handling | |
| 378 | 384 | ||
| 379 | 385 | def __del__(self) -> None: | |
@@ -400,7 +406,7 @@ def wait(self, stderr: Union[None, str, bytes] = b"") -> int: | |||
| 400 | 406 | stderr_b = force_bytes(data=stderr, encoding="utf-8") | |
| 401 | 407 | status: Union[int, None] | |
| 402 | 408 | if self.proc is not None: | |
| 403 | - status = self.proc.wait() | ||
| 409 | + status = self.proc.wait(timeout=self.timeout) | ||
| 404 | 410 | p_stderr = self.proc.stderr | |
| 405 | 411 | else: # Assume the underlying proc was killed earlier or never existed. | |
| 406 | 412 | status = self.status | |
@@ -1106,7 +1112,7 @@ def execute( | |||
| 1106 | 1112 | as_process: bool = False, | |
| 1107 | 1113 | output_stream: Union[None, BinaryIO] = None, | |
| 1108 | 1114 | stdout_as_string: bool = True, | |
| 1109 | - kill_after_timeout: Union[None, float] = None, | ||
| 1115 | + kill_after_timeout: float | None = None, | ||
| 1110 | 1116 | with_stdout: bool = True, | |
| 1111 | 1117 | universal_newlines: bool = False, | |
| 1112 | 1118 | shell: Union[None, bool] = None, | |
@@ -1156,18 +1162,10 @@ def execute( | |||
| 1156 | 1162 | should be killed. This will have no effect if `as_process` is set to | |
| 1157 | 1163 | ``True``. It is set to ``None`` by default and will let the process run | |
| 1158 | 1164 | until the timeout is explicitly specified. Uses of this feature should be | |
| 1159 | - carefully considered, due to the following limitations: | ||
| 1165 | + carefully considered, due to the following limitation: | ||
| 1160 | 1166 | ||
| 1161 | - 1. This feature is not supported at all on Windows. | ||
| 1162 | - 2. Effectiveness may vary by operating system. ``ps --ppid`` is used to | ||
| 1163 | - enumerate child processes, which is available on most GNU/Linux systems | ||
| 1164 | - but not most others. | ||
| 1165 | - 3. Deeper descendants do not receive signals, though they may sometimes | ||
| 1167 | + 1. Deeper descendants do not receive signals, though they may sometimes | ||
| 1166 | 1168 | terminate as a consequence of their parent processes being killed. | |
| 1167 | - 4. `kill_after_timeout` uses ``SIGKILL``, which can have negative side | ||
| 1168 | - effects on a repository. For example, stale locks in case of | ||
| 1169 | - :manpage:`git-gc(1)` could render the repository incapable of accepting | ||
| 1170 | - changes until the lock is manually removed. | ||
| 1171 | 1169 | ||
| 1172 | 1170 | :param with_stdout: | |
| 1173 | 1171 | If ``True``, default ``True``, we open stdout on the created process. | |
@@ -1252,15 +1250,7 @@ def execute( | |||
| 1252 | 1250 | if inline_env is not None: | |
| 1253 | 1251 | env.update(inline_env) | |
| 1254 | 1252 | ||
| 1255 | - if sys.platform == "win32": | ||
| 1256 | - if kill_after_timeout is not None: | ||
| 1257 | - raise GitCommandError( | ||
| 1258 | - redacted_command, | ||
| 1259 | - '"kill_after_timeout" feature is not supported on Windows.', | ||
| 1260 | - ) | ||
| 1261 | - cmd_not_found_exception = OSError | ||
| 1262 | - else: | ||
| 1263 | - cmd_not_found_exception = FileNotFoundError | ||
| 1253 | + cmd_not_found_exception = OSError if sys.platform == "win32" else FileNotFoundError | ||
| 1264 | 1254 | # END handle | |
| 1265 | 1255 | ||
| 1266 | 1256 | stdout_sink = PIPE if with_stdout else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb") | |
@@ -1303,66 +1293,14 @@ def execute( | |||
| 1303 | 1293 | if as_process: | |
| 1304 | 1294 | return self.AutoInterrupt(proc, command) | |
| 1305 | 1295 | ||
| 1306 | - if sys.platform != "win32" and kill_after_timeout is not None: | ||
| 1307 | - # Help mypy figure out this is not None even when used inside communicate(). | ||
| 1308 | - timeout = kill_after_timeout | ||
| 1309 | - | ||
| 1310 | - def kill_process(pid: int) -> None: | ||
| 1311 | - """Callback to kill a process. | ||
| 1312 | - | ||
| 1313 | - This callback implementation would be ineffective and unsafe on Windows. | ||
| 1314 | - """ | ||
| 1315 | - p = Popen(["ps", "--ppid", str(pid)], stdout=PIPE) | ||
| 1316 | - child_pids = [] | ||
| 1317 | - if p.stdout is not None: | ||
| 1318 | - for line in p.stdout: | ||
| 1319 | - if len(line.split()) > 0: | ||
| 1320 | - local_pid = (line.split())[0] | ||
| 1321 | - if local_pid.isdigit(): | ||
| 1322 | - child_pids.append(int(local_pid)) | ||
| 1323 | - try: | ||
| 1324 | - os.kill(pid, signal.SIGKILL) | ||
| 1325 | - for child_pid in child_pids: | ||
| 1326 | - try: | ||
| 1327 | - os.kill(child_pid, signal.SIGKILL) | ||
| 1328 | - except OSError: | ||
| 1329 | - pass | ||
| 1330 | - # Tell the main routine that the process was killed. | ||
| 1331 | - kill_check.set() | ||
| 1332 | - except OSError: | ||
| 1333 | - # It is possible that the process gets completed in the duration | ||
| 1334 | - # after timeout happens and before we try to kill the process. | ||
| 1335 | - pass | ||
| 1336 | - return | ||
| 1337 | - | ||
| 1338 | - def communicate() -> Tuple[AnyStr, AnyStr]: | ||
| 1339 | - watchdog.start() | ||
| 1340 | - out, err = proc.communicate() | ||
| 1341 | - watchdog.cancel() | ||
| 1342 | - if kill_check.is_set(): | ||
| 1343 | - err = 'Timeout: the command "%s" did not complete in %d secs.' % ( | ||
| 1344 | - " ".join(redacted_command), | ||
| 1345 | - timeout, | ||
| 1346 | - ) | ||
| 1347 | - if not universal_newlines: | ||
| 1348 | - err = err.encode(defenc) | ||
| 1349 | - return out, err | ||
| 1350 | - | ||
| 1351 | - # END helpers | ||
| 1352 | - | ||
| 1353 | - kill_check = threading.Event() | ||
| 1354 | - watchdog = threading.Timer(timeout, kill_process, args=(proc.pid,)) | ||
| 1355 | - else: | ||
| 1356 | - communicate = proc.communicate | ||
| 1357 | - | ||
| 1358 | 1296 | # Wait for the process to return. | |
| 1359 | 1297 | status = 0 | |
| 1360 | 1298 | stdout_value: Union[str, bytes] = b"" | |
| 1361 | 1299 | stderr_value: Union[str, bytes] = b"" | |
| 1362 | 1300 | newline = "\n" if universal_newlines else b"\n" | |
| 1363 | 1301 | try: | |
| 1364 | 1302 | if output_stream is None: | |
| 1365 | - stdout_value, stderr_value = communicate() | ||
| 1303 | + stdout_value, stderr_value = proc.communicate(timeout=kill_after_timeout) | ||
| 1366 | 1304 | # Strip trailing "\n". | |
| 1367 | 1305 | if stdout_value is not None and stdout_value.endswith(newline) and strip_newline_in_stdout: # type: ignore[arg-type] | |
| 1368 | 1306 | stdout_value = stdout_value[:-1] | |
@@ -1380,8 +1318,17 @@ def communicate() -> Tuple[AnyStr, AnyStr]: | |||
| 1380 | 1318 | # Strip trailing "\n". | |
| 1381 | 1319 | if stderr_value is not None and stderr_value.endswith(newline): # type: ignore[arg-type] | |
| 1382 | 1320 | stderr_value = stderr_value[:-1] | |
| 1383 | - status = proc.wait() | ||
| 1321 | + status = proc.wait(timeout=kill_after_timeout) | ||
| 1384 | 1322 | # END stdout handling | |
| 1323 | + except subprocess.TimeoutExpired as err: | ||
| 1324 | + _logger.info( | ||
| 1325 | + "error: process killed because it timed out. kill_after_timeout=%s seconds", | ||
| 1326 | + kill_after_timeout, | ||
| 1327 | + ) | ||
| 1328 | + proc.terminate() | ||
| 1329 | + stdout_value, stderr_value = proc.communicate() | ||
| 1330 | + if with_exceptions: | ||
| 1331 | + raise GitCommandError(redacted_command, proc.returncode, stderr_value, stdout_value) from err | ||
| 1385 | 1332 | finally: | |
| 1386 | 1333 | if proc.stdout is not None: | |
| 1387 | 1334 | proc.stdout.close() | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -5,6 +5,8 @@ | |||
| 5 | 5 | ||
| 6 | 6 | """Module implementing a remote object allowing easy access to git remotes.""" | |
| 7 | 7 | ||
| 8 | + from __future__ import annotations | ||
| 9 | + | ||
| 8 | 10 | __all__ = ["RemoteProgress", "PushInfo", "FetchInfo", "Remote"] | |
| 9 | 11 | ||
| 10 | 12 | import contextlib | |
@@ -873,7 +875,7 @@ def _get_fetch_info_from_stderr( | |||
| 873 | 875 | self, | |
| 874 | 876 | proc: "Git.AutoInterrupt", | |
| 875 | 877 | progress: Union[Callable[..., Any], RemoteProgress, None], | |
| 876 | - kill_after_timeout: Union[None, float] = None, | ||
| 878 | + kill_after_timeout: float | int | None = None, | ||
| 877 | 879 | ) -> IterableList["FetchInfo"]: | |
| 878 | 880 | progress = to_progress_instance(progress) | |
| 879 | 881 | ||
@@ -944,7 +946,7 @@ def _get_push_info( | |||
| 944 | 946 | self, | |
| 945 | 947 | proc: "Git.AutoInterrupt", | |
| 946 | 948 | progress: Union[Callable[..., Any], RemoteProgress, None], | |
| 947 | - kill_after_timeout: Union[None, float] = None, | ||
| 949 | + kill_after_timeout: float | int | None = None, | ||
| 948 | 950 | ) -> PushInfoList: | |
| 949 | 951 | progress = to_progress_instance(progress) | |
| 950 | 952 | ||
@@ -1002,7 +1004,7 @@ def fetch( | |||
| 1002 | 1004 | refspec: Union[str, List[str], None] = None, | |
| 1003 | 1005 | progress: Union[RemoteProgress, None, "UpdateProgress"] = None, | |
| 1004 | 1006 | verbose: bool = True, | |
| 1005 | - kill_after_timeout: Union[None, float] = None, | ||
| 1007 | + kill_after_timeout: float | int | None = None, | ||
| 1006 | 1008 | allow_unsafe_protocols: bool = False, | |
| 1007 | 1009 | allow_unsafe_options: bool = False, | |
| 1008 | 1010 | **kwargs: Any, | |
@@ -1082,7 +1084,7 @@ def pull( | |||
| 1082 | 1084 | self, | |
| 1083 | 1085 | refspec: Union[str, List[str], None] = None, | |
| 1084 | 1086 | progress: Union[RemoteProgress, "UpdateProgress", None] = None, | |
| 1085 | - kill_after_timeout: Union[None, float] = None, | ||
| 1087 | + kill_after_timeout: float | int | None = None, | ||
| 1086 | 1088 | allow_unsafe_protocols: bool = False, | |
| 1087 | 1089 | allow_unsafe_options: bool = False, | |
| 1088 | 1090 | **kwargs: Any, | |
@@ -1136,7 +1138,7 @@ def push( | |||
| 1136 | 1138 | self, | |
| 1137 | 1139 | refspec: Union[str, List[str], None] = None, | |
| 1138 | 1140 | progress: Union[RemoteProgress, "UpdateProgress", Callable[..., RemoteProgress], None] = None, | |
| 1139 | - kill_after_timeout: Union[None, float] = None, | ||
| 1141 | + kill_after_timeout: float | int | None = None, | ||
| 1140 | 1142 | allow_unsafe_protocols: bool = False, | |
| 1141 | 1143 | allow_unsafe_options: bool = False, | |
| 1142 | 1144 | **kwargs: Any, | |
| Back | FazBrowse Home | New Git URL |
0 commit comments