Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoInstallSign in
Socket

running-process

Package Overview
Dependencies
Maintainers
1
Versions
11
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

running-process - npm Package Compare versions

Comparing version
1.0.1
to
1.0.2
+253
running_process/pty.py
"""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()