fix windows

This commit is contained in:
AI Christianson 2025-02-25 14:15:28 -05:00
parent f14863a06d
commit 804ebd76a5
1 changed files with 226 additions and 72 deletions

View File

@ -12,21 +12,88 @@ where output is a bytes object (UTF-8 encoded).
import errno import errno
import io import io
import os import os
import select
import shutil import shutil
import signal import signal
import subprocess import subprocess
import sys import sys
import time import time
from typing import List, Tuple from typing import List, Optional, Tuple, Union
import pyte import pyte
from pyte.screens import HistoryScreen from pyte.screens import HistoryScreen
# Platform-specific imports
if sys.platform == "win32":
import msvcrt
import threading
else:
import select
import termios import termios
import tty import tty
def create_process(
cmd: List[str], env: Optional[dict] = None, cols: Optional[int] = None, rows: Optional[int] = None
) -> Tuple[subprocess.Popen, Optional[int]]:
"""
Create a subprocess with appropriate settings for the current platform.
Args:
cmd: Command to execute as a list of strings
env: Environment variables dictionary, defaults to os.environ.copy()
cols: Number of columns for the terminal, defaults to current terminal width
rows: Number of rows for the terminal, defaults to current terminal height
Returns:
On Unix: (process, master_fd) where master_fd is the file descriptor for the pty master
On Windows: (process, None) as Windows doesn't use ptys
"""
# Set default values if not provided
if env is None:
env = os.environ.copy()
if cols is None or rows is None:
default_cols, default_rows = get_terminal_size()
if cols is None:
cols = default_cols
if rows is None:
rows = default_rows
if sys.platform == "win32":
# Windows-specific process creation
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
proc = subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=0,
env=env,
startupinfo=startupinfo,
universal_newlines=False,
)
return proc, None
else:
# Unix-specific process creation with pty
master_fd, slave_fd = os.openpty()
# Set master_fd to non-blocking to avoid indefinite blocking
os.set_blocking(master_fd, False)
proc = subprocess.Popen(
cmd,
stdin=slave_fd,
stdout=slave_fd,
stderr=slave_fd,
bufsize=0,
close_fds=True,
env=env,
preexec_fn=os.setsid, # Create new process group for proper signal handling
)
os.close(slave_fd) # Close slave end in the parent process
return proc, master_fd
def get_terminal_size() -> Tuple[int, int]: def get_terminal_size() -> Tuple[int, int]:
""" """
Get the current terminal size in a cross-platform way. Get the current terminal size in a cross-platform way.
@ -121,16 +188,6 @@ def run_interactive_command(
screen = HistoryScreen(cols, rows, history=2000, ratio=0.5) screen = HistoryScreen(cols, rows, history=2000, ratio=0.5)
stream = pyte.Stream(screen) stream = pyte.Stream(screen)
# Open a new pseudo-tty.
master_fd, slave_fd = os.openpty()
# Set master_fd to non-blocking to avoid indefinite blocking.
os.set_blocking(master_fd, False)
try:
stdin_fd = sys.stdin.fileno()
except (AttributeError, io.UnsupportedOperation):
stdin_fd = None
# Set up environment variables for the subprocess using detected terminal size. # Set up environment variables for the subprocess using detected terminal size.
env = os.environ.copy() env = os.environ.copy()
env.update( env.update(
@ -150,17 +207,8 @@ def run_interactive_command(
} }
) )
proc = subprocess.Popen( # Create process based on platform
cmd, proc, master_fd = create_process(cmd, env, cols, rows)
stdin=slave_fd,
stdout=slave_fd,
stderr=slave_fd,
bufsize=0,
close_fds=True,
env=env,
preexec_fn=os.setsid, # Create new process group for proper signal handling.
)
os.close(slave_fd) # Close slave end in the parent process.
captured_data = [] captured_data = []
start_time = time.time() start_time = time.time()
@ -169,13 +217,119 @@ def run_interactive_command(
def check_timeout(): def check_timeout():
elapsed = time.time() - start_time elapsed = time.time() - start_time
if elapsed > 3 * expected_runtime_seconds: if elapsed > 3 * expected_runtime_seconds:
if sys.platform == "win32":
proc.kill()
else:
os.killpg(os.getpgid(proc.pid), signal.SIGKILL) os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
return True return True
elif elapsed > 2 * expected_runtime_seconds: elif elapsed > 2 * expected_runtime_seconds:
if sys.platform == "win32":
proc.terminate()
else:
os.killpg(os.getpgid(proc.pid), signal.SIGTERM) os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
return True return True
return False return False
if sys.platform == "win32":
# Windows implementation using threads for I/O
running = True
stdin_thread = None
def read_stdout():
nonlocal running
while running and proc.poll() is None:
try:
data = proc.stdout.read(1024)
if not data:
break
captured_data.append(data)
decoded = data.decode("utf-8", errors="ignore")
stream.feed(decoded)
sys.stdout.buffer.write(data)
sys.stdout.buffer.flush()
except (OSError, IOError):
break
except Exception as e:
print(f"Error reading stdout: {e}", file=sys.stderr)
break
def read_stderr():
nonlocal running
while running and proc.poll() is None:
try:
data = proc.stderr.read(1024)
if not data:
break
captured_data.append(data)
decoded = data.decode("utf-8", errors="ignore")
stream.feed(decoded)
sys.stderr.buffer.write(data)
sys.stderr.buffer.flush()
except (OSError, IOError):
break
except Exception as e:
print(f"Error reading stderr: {e}", file=sys.stderr)
break
def handle_input():
nonlocal running
try:
while running and proc.poll() is None:
if msvcrt.kbhit():
char = msvcrt.getch()
proc.stdin.write(char)
proc.stdin.flush()
time.sleep(0.01) # Small sleep to prevent CPU hogging
except (OSError, IOError):
pass
except Exception as e:
print(f"Error handling input: {e}", file=sys.stderr)
# Start I/O threads
stdout_thread = threading.Thread(target=read_stdout)
stderr_thread = threading.Thread(target=read_stderr)
stdout_thread.daemon = True
stderr_thread.daemon = True
stdout_thread.start()
stderr_thread.start()
# Only start stdin thread if we're in an interactive terminal
if sys.stdin.isatty():
stdin_thread = threading.Thread(target=handle_input)
stdin_thread.daemon = True
stdin_thread.start()
try:
# Main thread monitors timeout
while proc.poll() is None:
if check_timeout():
was_terminated = True
break
time.sleep(0.1)
except KeyboardInterrupt:
proc.terminate()
finally:
running = False
# Wait for threads to finish
stdout_thread.join(1.0)
stderr_thread.join(1.0)
if stdin_thread:
stdin_thread.join(1.0)
# Close pipes
if proc.stdout:
proc.stdout.close()
if proc.stderr:
proc.stderr.close()
if proc.stdin:
proc.stdin.close()
else:
# Unix implementation using select and pty
try:
stdin_fd = sys.stdin.fileno()
except (AttributeError, io.UnsupportedOperation):
stdin_fd = None
# Interactive mode: forward input if running in a TTY. # Interactive mode: forward input if running in a TTY.
if stdin_fd is not None and sys.stdin.isatty(): if stdin_fd is not None and sys.stdin.isatty():
old_settings = termios.tcgetattr(stdin_fd) old_settings = termios.tcgetattr(stdin_fd)