fix windows
This commit is contained in:
parent
f14863a06d
commit
804ebd76a5
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue