running-process
Advanced tools
| """Cross-platform PTY (Pseudo-Terminal) wrapper. | ||
| This module provides a unified interface for pseudo-terminal functionality | ||
| across different platforms (Windows, Unix/Linux/macOS). | ||
| """ | ||
| import os | ||
| import signal | ||
| import subprocess | ||
| import sys | ||
| from typing import TYPE_CHECKING, Any | ||
| if TYPE_CHECKING: | ||
| from typing import Protocol | ||
| class PtyProcessProtocol(Protocol): | ||
| """Protocol for PTY process implementations.""" | ||
| pid: int | ||
| returncode: int | None | ||
| stdout: Any # PTY processes handle stdout differently | ||
| def poll(self) -> int | None: ... | ||
| def kill(self) -> None: ... | ||
| def terminate(self) -> None: ... | ||
| def wait(self, timeout: float | None = None) -> int | None: ... | ||
| def read(self, size: int = 1024) -> bytes: ... | ||
| def write(self, data: bytes) -> int: ... | ||
| def close(self) -> None: ... | ||
| class PtyNotAvailableError(Exception): | ||
| """Raised when PTY functionality is not available on the current platform.""" | ||
| class Pty: | ||
| """Unified cross-platform PTY wrapper. | ||
| This class provides a consistent interface for pseudo-terminal operations | ||
| across Windows (using winpty) and Unix-like systems (using pty module). | ||
| """ | ||
| def __init__(self) -> None: | ||
| self._pty_proc: Any = None | ||
| self._master_fd: int | None = None | ||
| self._slave_fd: int | None = None | ||
| self._platform = sys.platform | ||
| @classmethod | ||
| def is_available(cls) -> bool: | ||
| """Check if PTY support is available on the current platform.""" | ||
| if sys.platform == "win32": | ||
| try: | ||
| import winpty # noqa: F401, PLC0415 # type: ignore[import-untyped,import-not-found] | ||
| except ImportError: | ||
| return False | ||
| else: | ||
| return True | ||
| else: | ||
| try: | ||
| import pty # noqa: F401, PLC0415 | ||
| except ImportError: | ||
| return False | ||
| else: | ||
| return True | ||
| def spawn_process( | ||
| self, | ||
| command: str | list[str], | ||
| cwd: str | None = None, | ||
| env: dict[str, str] | None = None, | ||
| shell: bool = False, | ||
| ) -> "PtyProcessProtocol": | ||
| """Spawn a process with PTY support. | ||
| Args: | ||
| command: Command to execute (string or list) | ||
| cwd: Working directory for the process | ||
| env: Environment variables | ||
| shell: Whether to use shell for execution | ||
| Returns: | ||
| A process object with PTY support | ||
| Raises: | ||
| PtyNotAvailableError: If PTY is not available on this platform | ||
| """ | ||
| if not self.is_available(): | ||
| msg = f"PTY not available on {self._platform}" | ||
| raise PtyNotAvailableError(msg) | ||
| if self._platform == "win32": | ||
| return self._spawn_windows_process(command, cwd, env, shell) | ||
| return self._spawn_unix_process(command, cwd, env, shell) | ||
| def _spawn_windows_process( | ||
| self, | ||
| command: str | list[str], | ||
| cwd: str | None = None, | ||
| env: dict[str, str] | None = None, | ||
| shell: bool = False, | ||
| ) -> "WindowsPtyProcess": | ||
| """Spawn a Windows PTY process using winpty.""" | ||
| import winpty # noqa: PLC0415 # type: ignore[import-untyped,import-not-found] | ||
| # Prepare command for winpty | ||
| pty_command = (["cmd", "/c", command] if shell else command.split()) if isinstance(command, str) else command | ||
| # Use current environment if none provided | ||
| if env is None: | ||
| env = os.environ.copy() | ||
| # Create PTY process | ||
| self._pty_proc = winpty.PtyProcess.spawn( | ||
| pty_command, | ||
| cwd=cwd, | ||
| env=env, | ||
| ) | ||
| return WindowsPtyProcess(self._pty_proc) | ||
| def _spawn_unix_process( | ||
| self, | ||
| command: str | list[str], | ||
| cwd: str | None = None, | ||
| env: dict[str, str] | None = None, | ||
| shell: bool = False, | ||
| ) -> "UnixPtyProcess": | ||
| """Spawn a Unix PTY process using pty module.""" | ||
| import pty # noqa: PLC0415 | ||
| # Create PTY master and slave | ||
| master_fd, slave_fd = pty.openpty() # type: ignore[attr-defined] | ||
| self._master_fd = master_fd | ||
| self._slave_fd = slave_fd | ||
| # Prepare command - both list and str are handled the same way for Unix | ||
| popen_command = command | ||
| # Create process with PTY | ||
| proc = subprocess.Popen( # noqa: S603 | ||
| popen_command, | ||
| shell=shell, | ||
| cwd=cwd, | ||
| env=env, | ||
| stdin=slave_fd, | ||
| stdout=slave_fd, | ||
| stderr=slave_fd, # All streams use PTY | ||
| text=False, # Use binary mode for PTY | ||
| preexec_fn=os.setsid if sys.platform != "win32" else None, # noqa: PLW1509 | ||
| ) | ||
| # Close slave fd in parent process | ||
| os.close(slave_fd) | ||
| self._slave_fd = None | ||
| return UnixPtyProcess(proc, master_fd) | ||
| class WindowsPtyProcess: | ||
| """Windows PTY process wrapper using winpty.""" | ||
| def __init__(self, pty_proc: Any) -> None: | ||
| self._pty = pty_proc | ||
| self.pid = pty_proc.pid | ||
| self.returncode: int | None = None | ||
| self.stdout = None # PTY handles output differently | ||
| def poll(self) -> int | None: | ||
| """Check if process has terminated.""" | ||
| if self._pty.isalive(): | ||
| return None | ||
| if self.returncode is None: | ||
| self.returncode = self._pty.exitstatus | ||
| return self.returncode | ||
| def terminate(self) -> None: | ||
| """Terminate the process gracefully.""" | ||
| self._pty.terminate() | ||
| def kill(self) -> None: | ||
| """Forcefully kill the process.""" | ||
| self._pty.kill(signal.SIGTERM) | ||
| def wait(self, timeout: float | None = None) -> int | None: | ||
| """Wait for process to complete.""" | ||
| self._pty.wait(timeout) | ||
| return self.poll() | ||
| def read(self, size: int = 1024) -> bytes: | ||
| """Read data from PTY.""" | ||
| return self._pty.read(size) | ||
| def write(self, data: bytes) -> int: | ||
| """Write data to PTY.""" | ||
| return self._pty.write(data) | ||
| def close(self) -> None: | ||
| """Close PTY resources.""" | ||
| if hasattr(self._pty, "close"): | ||
| self._pty.close() | ||
| class UnixPtyProcess: | ||
| """Unix PTY process wrapper using pty module.""" | ||
| def __init__(self, proc: subprocess.Popen[Any], master_fd: int) -> None: | ||
| self._proc = proc | ||
| self._master_fd = master_fd | ||
| self.pid = proc.pid | ||
| self.returncode: int | None = None | ||
| self.stdout = None # PTY handles output differently | ||
| def poll(self) -> int | None: | ||
| """Check if process has terminated.""" | ||
| result = self._proc.poll() | ||
| if result is not None: | ||
| self.returncode = result | ||
| return result | ||
| def terminate(self) -> None: | ||
| """Terminate the process gracefully.""" | ||
| self._proc.terminate() | ||
| def kill(self) -> None: | ||
| """Forcefully kill the process.""" | ||
| self._proc.kill() | ||
| def wait(self, timeout: float | None = None) -> int | None: | ||
| """Wait for process to complete.""" | ||
| result = self._proc.wait(timeout) | ||
| self.returncode = result | ||
| return result | ||
| def read(self, size: int = 1024) -> bytes: | ||
| """Read data from PTY master.""" | ||
| if self._master_fd is None: | ||
| msg = "PTY master fd is closed" | ||
| raise ValueError(msg) | ||
| return os.read(self._master_fd, size) | ||
| def write(self, data: bytes) -> int: | ||
| """Write data to PTY master.""" | ||
| if self._master_fd is None: | ||
| msg = "PTY master fd is closed" | ||
| raise ValueError(msg) | ||
| return os.write(self._master_fd, data) | ||
| def close(self) -> None: | ||
| """Close PTY resources.""" | ||
| if self._master_fd is not None: | ||
| os.close(self._master_fd) | ||
| self._master_fd = None |
+1
-1
| Metadata-Version: 2.4 | ||
| Name: running_process | ||
| Version: 1.0.1 | ||
| Version: 1.0.2 | ||
| Summary: A modern subprocess.Popen wrapper with improved process management | ||
@@ -5,0 +5,0 @@ Project-URL: Homepage, https://github.com/yourusername/running-process |
+7
-6
| running_process/__init__.py,sha256=gTVGqReRvViI2XHrk9ahRSkAxm_bmeFgu3EvXuFgQAE,670 | ||
| running_process/line_iterator.py,sha256=lbsjssk0yKjCRCb2knOuU5C2hbvdqKEgDjv2hPwIqrQ,1544 | ||
| running_process/output_formatter.py,sha256=ie8gRQSZRGpBcNuZt5ns-yK6DDjO_SzAsiQAqLo71D0,1917 | ||
| running_process/process_output_reader.py,sha256=k7tRcqOFdl6H_KAPlAHyVLhMMfvbiXInqLjUQTejEzk,6022 | ||
| running_process/process_output_reader.py,sha256=Oe-0oljcu-fHnjeA8JaUtETuj0aVPiE5DbTZobvbjsQ,11201 | ||
| running_process/process_utils.py,sha256=fNCdxfebjzhK-4t6K6x6Vhil7luIHpVQIVZJAQa98u8,2255 | ||
| running_process/process_watcher.py,sha256=i8YpgJlQVvY0NbuEbs2N39HP9kITb0uyd0I2i29u6sg,3110 | ||
| running_process/running_process.py,sha256=1CsxvKZgzoA1aFdexU-l6cinrBZ_YygIE65g4kb_68I,36444 | ||
| running_process/pty.py,sha256=KfRutEF4dZyLRc4h0Wvl7nRbiM3zqHuibX4MaV7SEYY,8147 | ||
| running_process/running_process.py,sha256=EvMLfLhmDhHh33JPxvsBoMIPC_tYl_O805LhEtPQK-s,39904 | ||
| running_process/running_process_manager.py,sha256=xx_kmXw9j-hjOI_pRrlTKdc9yekKBVQHhUq1eDpBGJU,2467 | ||
| running_process/subprocess_runner.py,sha256=O_Wwe2vrXWnYkiNEpauNLre3PqNO3uDogrRz0JCkH7M,3223 | ||
| running_process/assets/example.txt,sha256=lTBovRjiz0_TgtAtbA1C5hNi2ffbqnNPqkKg6UiKCT8,54 | ||
| running_process-1.0.1.dist-info/METADATA,sha256=qhA7xeu5DqbT76ZYNDuydLta5PmK2yYhh76scQORh-M,8465 | ||
| running_process-1.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87 | ||
| running_process-1.0.1.dist-info/licenses/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064 | ||
| running_process-1.0.1.dist-info/RECORD,, | ||
| running_process-1.0.2.dist-info/METADATA,sha256=Y4PPq4m5pF4TK1SmTGiHKWGtOD9QPCPOluTLlX0abwE,8465 | ||
| running_process-1.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87 | ||
| running_process-1.0.2.dist-info/licenses/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064 | ||
| running_process-1.0.2.dist-info/RECORD,, |
@@ -9,2 +9,6 @@ """Process output reader module. | ||
| import logging | ||
| import os | ||
| import re | ||
| import signal | ||
| import sys | ||
| import threading | ||
@@ -16,4 +20,7 @@ import time | ||
| from subprocess import Popen | ||
| from typing import Any | ||
| from typing import TYPE_CHECKING, Any, Union | ||
| if TYPE_CHECKING: | ||
| from running_process.pty import PtyProcessProtocol | ||
| from running_process.output_formatter import NullOutputFormatter, OutputFormatter | ||
@@ -38,3 +45,3 @@ | ||
| self, | ||
| proc: Popen[Any], | ||
| proc: Union[Popen[Any], "PtyProcessProtocol"], | ||
| shutdown: threading.Event, | ||
@@ -44,2 +51,5 @@ output_formatter: OutputFormatter | None, | ||
| on_end: Callable[[], None], | ||
| use_pty: bool = False, | ||
| pty_proc: Any = None, | ||
| pty_master_fd: int | None = None, | ||
| ) -> None: | ||
@@ -54,2 +64,7 @@ output_formatter = output_formatter or NullOutputFormatter() | ||
| self._eos_emitted: bool = False | ||
| self._use_pty = use_pty | ||
| self._pty_proc = pty_proc | ||
| self._pty_master_fd = pty_master_fd | ||
| # Compile ANSI escape sequence regex for PTY output filtering | ||
| self._ansi_escape = re.compile(r"\x1b\[[^a-zA-Z]*[a-zA-Z]") if use_pty else None | ||
@@ -72,2 +87,9 @@ def _emit_eos_once(self) -> None: | ||
| """Process stdout lines and forward them to output.""" | ||
| if self._use_pty: | ||
| self._process_pty_output() | ||
| else: | ||
| self._process_pipe_output() | ||
| def _process_pipe_output(self) -> None: | ||
| """Process standard pipe output.""" | ||
| assert self._proc.stdout is not None | ||
@@ -87,2 +109,87 @@ | ||
| def _read_pty_chunk(self) -> str | None: | ||
| """Read a chunk of data from PTY.""" | ||
| if sys.platform == "win32" and self._pty_proc: | ||
| # Windows: read from winpty | ||
| chunk = self._pty_proc.read() | ||
| return chunk if chunk else None | ||
| if self._pty_master_fd is not None: | ||
| # Unix: read from PTY file descriptor | ||
| import select # noqa: PLC0415 | ||
| # Use select to check if data is available with timeout | ||
| ready, _, _ = select.select([self._pty_master_fd], [], [], 0.1) | ||
| if not ready: | ||
| return "" # No data available, continue | ||
| chunk_bytes = os.read(self._pty_master_fd, 4096) | ||
| if not chunk_bytes: | ||
| return None # EOF | ||
| return chunk_bytes.decode("utf-8", errors="replace") | ||
| return None # No PTY available | ||
| def _process_pty_chunk(self, chunk: str, buffer: str) -> str: | ||
| """Process a chunk of PTY data and return updated buffer.""" | ||
| self.last_stdout_ts = time.time() | ||
| # Filter ANSI escape sequences if regex is available | ||
| if self._ansi_escape: | ||
| chunk = self._ansi_escape.sub("", chunk) | ||
| # Normalize line endings and add to buffer | ||
| chunk = chunk.replace("\r\n", "\n").replace("\r", "\n") | ||
| buffer += chunk | ||
| # Process complete lines from buffer | ||
| while "\n" in buffer: | ||
| line, buffer = buffer.split("\n", 1) | ||
| line = line.rstrip() | ||
| if line: | ||
| transformed_line = self._output_formatter.transform(line) | ||
| self._on_output(transformed_line) | ||
| return buffer | ||
| def _process_pty_output(self) -> None: # noqa: C901 | ||
| """Process PTY output with ANSI filtering.""" | ||
| buffer = "" | ||
| while not self._shutdown.is_set(): | ||
| try: | ||
| chunk = self._read_pty_chunk() | ||
| if chunk is None: | ||
| break # EOF or no PTY | ||
| if chunk == "": | ||
| continue # No data available, continue | ||
| if chunk: | ||
| buffer = self._process_pty_chunk(chunk, buffer) | ||
| except KeyboardInterrupt: | ||
| # CRITICAL: Handle KeyboardInterrupt in PTY mode | ||
| logger.warning("KeyboardInterrupt in PTY output reader - cleaning up PTY process") | ||
| # Clean up PTY process immediately | ||
| if sys.platform == "win32" and self._pty_proc: | ||
| try: | ||
| self._pty_proc.kill(signal.SIGTERM) | ||
| except (OSError, ValueError, RuntimeError) as e: | ||
| logger.warning("Failed to kill winpty process on KeyboardInterrupt: %s", e) | ||
| # Re-raise to be handled by the main handler | ||
| raise | ||
| except (OSError, ValueError) as e: | ||
| # PTY closed or error reading | ||
| logger.debug("PTY read error (normal on close): %s", e) | ||
| break | ||
| except Exception as e: # noqa: BLE001 | ||
| # Unexpected error, log it for debugging | ||
| logger.warning("Unexpected error in PTY reader: %s", e) | ||
| break | ||
| # Process any remaining data in buffer | ||
| if buffer and buffer.strip(): | ||
| self.last_stdout_ts = time.time() | ||
| for raw_line in buffer.split("\n"): | ||
| line = raw_line.rstrip() | ||
| if line: | ||
| transformed_line = self._output_formatter.transform(line) | ||
| self._on_output(transformed_line) | ||
| def _handle_keyboard_interrupt(self) -> None: | ||
@@ -118,3 +225,18 @@ """Handle KeyboardInterrupt in reader thread.""" | ||
| """Close stdout stream safely.""" | ||
| if self._proc.stdout and not self._proc.stdout.closed: | ||
| if self._use_pty: | ||
| # PTY cleanup | ||
| if sys.platform == "win32" and self._pty_proc: | ||
| try: | ||
| self._pty_proc.close() | ||
| except (ValueError, OSError) as err: | ||
| reader_error_msg = f"PTY reader encountered error: {err}" | ||
| warnings.warn(reader_error_msg, stacklevel=2) | ||
| elif self._pty_master_fd is not None: | ||
| try: | ||
| os.close(self._pty_master_fd) | ||
| except (ValueError, OSError) as err: | ||
| reader_error_msg = f"PTY reader encountered error: {err}" | ||
| warnings.warn(reader_error_msg, stacklevel=2) | ||
| # Standard pipe cleanup | ||
| elif self._proc.stdout and not self._proc.stdout.closed: | ||
| try: | ||
@@ -121,0 +243,0 @@ self._proc.stdout.close() |
@@ -71,2 +71,16 @@ """Enhanced subprocess execution with timeout protection, output streaming, and process tree management. | ||
| ### PTY Support for Interactive Commands | ||
| ```python | ||
| # Use pseudo-terminal for commands requiring interactive terminal | ||
| process = RunningProcess( | ||
| command=["ssh", "user@host", "ls"], | ||
| use_pty=True, # Enables PTY mode | ||
| timeout=30 | ||
| ) | ||
| exit_code = process.wait() | ||
| # PTY automatically filters ANSI escape sequences | ||
| # and handles commands that behave differently in terminals | ||
| ``` | ||
| ### Simple subprocess.run() Replacement | ||
@@ -91,2 +105,4 @@ ```python | ||
| - **Output streaming**: Real-time line iteration and non-blocking access | ||
| - **PTY support**: Pseudo-terminal support for interactive commands (requires winpty on Windows) | ||
| - **ANSI filtering**: Automatic filtering of escape sequences in PTY mode | ||
| - **Flexible callbacks**: Custom timeout and completion handlers | ||
@@ -103,2 +119,3 @@ - **Cross-platform**: Works on Windows, macOS, and Linux | ||
| import queue | ||
| import signal | ||
| import subprocess | ||
@@ -113,4 +130,8 @@ import sys | ||
| from queue import Queue | ||
| from typing import Any | ||
| from typing import TYPE_CHECKING, Any | ||
| if TYPE_CHECKING: | ||
| from running_process.pty import PtyProcessProtocol | ||
| from running_process.line_iterator import _RunningProcessLineIterator | ||
@@ -121,2 +142,3 @@ from running_process.output_formatter import NullOutputFormatter, OutputFormatter | ||
| from running_process.process_watcher import ProcessWatcher | ||
| from running_process.pty import Pty, PtyNotAvailableError | ||
| from running_process.running_process_manager import RunningProcessManagerSingleton | ||
@@ -200,2 +222,3 @@ from running_process.subprocess_runner import execute_subprocess_run | ||
| output_formatter: OutputFormatter | None = None, | ||
| use_pty: bool = False, # Use pseudo-terminal for process execution | ||
| ) -> None: | ||
@@ -217,2 +240,3 @@ """ | ||
| output_formatter: Optional formatter for transforming output lines. | ||
| use_pty: If True, use pseudo-terminal for process execution (supports interactive commands). | ||
| """ | ||
@@ -247,3 +271,3 @@ # Validate command/shell combination | ||
| self.accumulated_output: list[str] = [] # Store all output for later retrieval | ||
| self.proc: subprocess.Popen[Any] | None = None | ||
| self.proc: subprocess.Popen[Any] | PtyProcessProtocol | None = None | ||
| self.check = check | ||
@@ -263,5 +287,13 @@ self.auto_run = auto_run | ||
| self._termination_notified: bool = False | ||
| # PTY support fields | ||
| self.use_pty = use_pty and self._pty_available() | ||
| self._pty_proc: Any = None # winpty.PtyProcess or None | ||
| self._pty_master_fd: int | None = None # Unix PTY master file descriptor | ||
| if auto_run: | ||
| self.start() | ||
| def _pty_available(self) -> bool: | ||
| """Check PTY support for current platform.""" | ||
| return Pty.is_available() | ||
| def get_command_str(self) -> str: | ||
@@ -360,2 +392,13 @@ if isinstance(self.command, list): | ||
| """Create the subprocess with proper configuration.""" | ||
| if self.use_pty: | ||
| self._create_process_with_pty() | ||
| else: | ||
| self._create_process_with_pipe() | ||
| # Track start time after process is successfully created | ||
| # This excludes process creation overhead from timing measurements | ||
| self._start_time = time.time() | ||
| def _create_process_with_pipe(self) -> None: | ||
| """Create subprocess with standard pipes.""" | ||
| popen_command = self._prepare_command() | ||
@@ -374,5 +417,19 @@ | ||
| # Track start time after process is successfully created | ||
| # This excludes process creation overhead from timing measurements | ||
| self._start_time = time.time() | ||
| def _create_process_with_pty(self) -> None: | ||
| """Create subprocess with PTY allocation using unified PTY wrapper.""" | ||
| try: | ||
| pty_wrapper = Pty() | ||
| pty_process = pty_wrapper.spawn_process( | ||
| command=self.command, | ||
| cwd=self.cwd, | ||
| env=os.environ.copy(), | ||
| shell=self.shell, | ||
| ) | ||
| self.proc = pty_process # type: ignore[assignment] | ||
| self._pty_proc = pty_process | ||
| except PtyNotAvailableError: | ||
| # Fall back to regular pipe-based process if PTY is not available | ||
| logger.warning("PTY requested but not available, falling back to pipes") | ||
| self.use_pty = False | ||
| self._create_process_with_pipe() | ||
@@ -423,2 +480,5 @@ def _register_with_manager(self) -> None: | ||
| on_end=on_end, | ||
| use_pty=self.use_pty, | ||
| pty_proc=self._pty_proc, | ||
| pty_master_fd=self._pty_master_fd, | ||
| ) | ||
@@ -753,3 +813,3 @@ | ||
| def kill(self) -> None: | ||
| def kill(self) -> None: # noqa: C901 | ||
| """ | ||
@@ -773,2 +833,19 @@ Immediately terminate the process and all child processes. | ||
| # PTY-specific cleanup must happen before killing process tree | ||
| if self.use_pty: | ||
| logger.debug("Cleaning up PTY resources before process termination") | ||
| if sys.platform == "win32" and self._pty_proc: | ||
| try: | ||
| self._pty_proc.kill(signal.SIGTERM) | ||
| logger.debug("Killed winpty process") | ||
| except (OSError, ValueError, RuntimeError) as e: | ||
| logger.warning("Failed to kill winpty process: %s", e) | ||
| elif self._pty_master_fd is not None: | ||
| try: | ||
| os.close(self._pty_master_fd) | ||
| self._pty_master_fd = None | ||
| logger.debug("Closed Unix PTY master fd") | ||
| except (OSError, ValueError) as e: | ||
| logger.warning("Failed to close PTY master fd: %s", e) | ||
| # Kill the entire process tree (parent + all children) | ||
@@ -779,4 +856,7 @@ # This prevents orphaned clang++ processes from hanging the system | ||
| except KeyboardInterrupt: | ||
| logger.info("Keyboard interrupt detected, interrupting main thread") | ||
| logger.info("Keyboard interrupt detected in kill(), interrupting main thread") | ||
| _thread.interrupt_main() | ||
| # Extra cleanup for PTY on KeyboardInterrupt | ||
| if self.use_pty: | ||
| logger.warning("KeyboardInterrupt during PTY process kill - forcing cleanup") | ||
| try: | ||
@@ -783,0 +863,0 @@ self.proc.kill() |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.