More work on the pty driver.
This commit is contained in:
@ -25,6 +25,26 @@ from fenrirscreenreader.core.eventData import FenrirEventType
|
|||||||
from fenrirscreenreader.core.screenDriver import ScreenDriver as screenDriver
|
from fenrirscreenreader.core.screenDriver import ScreenDriver as screenDriver
|
||||||
from fenrirscreenreader.utils import screen_utils
|
from fenrirscreenreader.utils import screen_utils
|
||||||
|
|
||||||
|
# PTY Driver Constants
|
||||||
|
class PTYConstants:
|
||||||
|
# Timeouts (in seconds)
|
||||||
|
DEFAULT_READ_TIMEOUT = 0.3
|
||||||
|
INPUT_READ_TIMEOUT = 0.01
|
||||||
|
SELECT_TIMEOUT = 0.05
|
||||||
|
PROCESS_TERMINATION_TIMEOUT = 3.0
|
||||||
|
PROCESS_KILL_DELAY = 0.5
|
||||||
|
|
||||||
|
# Polling intervals (in seconds)
|
||||||
|
MIN_POLL_INTERVAL = 0.001
|
||||||
|
|
||||||
|
# Limits
|
||||||
|
MAX_TERMINAL_LINES = 10000
|
||||||
|
DEFAULT_READ_BUFFER_SIZE = 65536
|
||||||
|
INPUT_BUFFER_SIZE = 4096
|
||||||
|
|
||||||
|
# Error codes
|
||||||
|
IO_ERROR_ERRNO = 5
|
||||||
|
|
||||||
|
|
||||||
class FenrirScreen(pyte.Screen):
|
class FenrirScreen(pyte.Screen):
|
||||||
def set_margins(self, *args, **kwargs):
|
def set_margins(self, *args, **kwargs):
|
||||||
@ -91,10 +111,9 @@ class Terminal:
|
|||||||
self.attributes = [[] for _ in range(self.screen.lines)]
|
self.attributes = [[] for _ in range(self.screen.lines)]
|
||||||
for y in lines:
|
for y in lines:
|
||||||
# Validate y is within reasonable bounds (prevent memory exhaustion)
|
# Validate y is within reasonable bounds (prevent memory exhaustion)
|
||||||
max_lines = 10000 # Reasonable maximum for terminal applications
|
if y >= PTYConstants.MAX_TERMINAL_LINES:
|
||||||
if y >= max_lines:
|
|
||||||
self._log_error(
|
self._log_error(
|
||||||
f"Line index {y} exceeds maximum {max_lines}, "
|
f"Line index {y} exceeds maximum {PTYConstants.MAX_TERMINAL_LINES}, "
|
||||||
f"skipping attribute update",
|
f"skipping attribute update",
|
||||||
debug.DebugLevel.WARNING
|
debug.DebugLevel.WARNING
|
||||||
)
|
)
|
||||||
@ -104,8 +123,10 @@ class Terminal:
|
|||||||
if y not in buffer:
|
if y not in buffer:
|
||||||
# Only log this occasionally to prevent spam
|
# Only log this occasionally to prevent spam
|
||||||
if y % 10 == 0: # Log every 10th missing line
|
if y % 10 == 0: # Log every 10th missing line
|
||||||
|
# Pre-format string to avoid repeated f-string operations
|
||||||
|
line_range = f"{y}-{y+9}"
|
||||||
self._log_error(
|
self._log_error(
|
||||||
f"Lines {y}-{y+9} not found in buffer, skipping attribute updates",
|
f"Lines {line_range} not found in buffer, skipping attribute updates",
|
||||||
debug.DebugLevel.WARNING
|
debug.DebugLevel.WARNING
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
@ -147,7 +168,9 @@ class Terminal:
|
|||||||
|
|
||||||
def get_screen_content(self):
|
def get_screen_content(self):
|
||||||
cursor = self.screen.cursor
|
cursor = self.screen.cursor
|
||||||
self.text = "\n".join(self.screen.display)
|
# Only regenerate text if screen is dirty or text doesn't exist
|
||||||
|
if not hasattr(self, 'text') or self.screen.dirty:
|
||||||
|
self.text = "\n".join(self.screen.display)
|
||||||
self.update_attributes(self.attributes is None)
|
self.update_attributes(self.attributes is None)
|
||||||
self.screen.dirty.clear()
|
self.screen.dirty.clear()
|
||||||
|
|
||||||
@ -174,12 +197,58 @@ class driver(screenDriver):
|
|||||||
self.p_pid = -1
|
self.p_pid = -1
|
||||||
self.terminal_lock = threading.Lock() # Synchronize terminal operations
|
self.terminal_lock = threading.Lock() # Synchronize terminal operations
|
||||||
signal.signal(signal.SIGWINCH, self.handle_sigwinch)
|
signal.signal(signal.SIGWINCH, self.handle_sigwinch)
|
||||||
|
|
||||||
|
# Runtime configuration storage
|
||||||
|
self.pty_config = {}
|
||||||
|
|
||||||
|
def _load_pty_settings(self):
|
||||||
|
"""Load PTY-specific settings from configuration with fallbacks to defaults."""
|
||||||
|
try:
|
||||||
|
settings_manager = self.env["runtime"]["SettingsManager"]
|
||||||
|
|
||||||
|
# Load timeout settings with defaults
|
||||||
|
self.pty_config = {
|
||||||
|
'input_timeout': float(settings_manager.get_setting(
|
||||||
|
'screen', 'ptyInputTimeout', PTYConstants.INPUT_READ_TIMEOUT
|
||||||
|
)),
|
||||||
|
'select_timeout': float(settings_manager.get_setting(
|
||||||
|
'screen', 'ptySelectTimeout', PTYConstants.SELECT_TIMEOUT
|
||||||
|
)),
|
||||||
|
'process_termination_timeout': float(settings_manager.get_setting(
|
||||||
|
'screen', 'ptyProcessTimeout', PTYConstants.PROCESS_TERMINATION_TIMEOUT
|
||||||
|
)),
|
||||||
|
'poll_interval': float(settings_manager.get_setting(
|
||||||
|
'screen', 'ptyPollInterval', PTYConstants.MIN_POLL_INTERVAL
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||||
|
f"PTY screenDriver: Loaded configuration: {self.pty_config}",
|
||||||
|
debug.DebugLevel.INFO
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Fallback to constants if settings fail
|
||||||
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||||
|
f"PTY screenDriver: Failed to load settings, using defaults: {e}",
|
||||||
|
debug.DebugLevel.WARNING
|
||||||
|
)
|
||||||
|
self.pty_config = {
|
||||||
|
'input_timeout': PTYConstants.INPUT_READ_TIMEOUT,
|
||||||
|
'select_timeout': PTYConstants.SELECT_TIMEOUT,
|
||||||
|
'process_termination_timeout': PTYConstants.PROCESS_TERMINATION_TIMEOUT,
|
||||||
|
'poll_interval': PTYConstants.MIN_POLL_INTERVAL
|
||||||
|
}
|
||||||
|
|
||||||
def initialize(self, environment):
|
def initialize(self, environment):
|
||||||
self.env = environment
|
self.env = environment
|
||||||
self.command = self.env["runtime"]["SettingsManager"].get_setting(
|
self.command = self.env["runtime"]["SettingsManager"].get_setting(
|
||||||
"general", "shell"
|
"general", "shell"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Load configurable timeouts from settings
|
||||||
|
self._load_pty_settings()
|
||||||
|
|
||||||
self.shortcutType = self.env["runtime"][
|
self.shortcutType = self.env["runtime"][
|
||||||
"InputManager"
|
"InputManager"
|
||||||
].get_shortcut_type()
|
].get_shortcut_type()
|
||||||
@ -203,7 +272,7 @@ class driver(screenDriver):
|
|||||||
self.env["general"]["prev_user"] = getpass.getuser()
|
self.env["general"]["prev_user"] = getpass.getuser()
|
||||||
self.env["general"]["curr_user"] = getpass.getuser()
|
self.env["general"]["curr_user"] = getpass.getuser()
|
||||||
|
|
||||||
def read_all(self, fd, timeout=0.3, interruptFd=None, len=65536):
|
def read_all(self, fd, timeout=PTYConstants.DEFAULT_READ_TIMEOUT, interruptFd=None, len=PTYConstants.DEFAULT_READ_BUFFER_SIZE):
|
||||||
"""Read all available data from file descriptor with efficient polling.
|
"""Read all available data from file descriptor with efficient polling.
|
||||||
|
|
||||||
Uses progressively longer wait times to balance responsiveness with CPU usage.
|
Uses progressively longer wait times to balance responsiveness with CPU usage.
|
||||||
@ -214,7 +283,7 @@ class driver(screenDriver):
|
|||||||
fd_list.append(interruptFd)
|
fd_list.append(interruptFd)
|
||||||
|
|
||||||
starttime = time.time()
|
starttime = time.time()
|
||||||
poll_timeout = 0.001 # Start with 1ms, stay responsive
|
poll_timeout = self.pty_config.get('poll_interval', PTYConstants.MIN_POLL_INTERVAL) # Use configured interval
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
# Use consistent short polling for responsiveness
|
# Use consistent short polling for responsiveness
|
||||||
@ -238,7 +307,7 @@ class driver(screenDriver):
|
|||||||
debug.DebugLevel.ERROR
|
debug.DebugLevel.ERROR
|
||||||
)
|
)
|
||||||
# For I/O errors, exit immediately to prevent endless retry loops
|
# For I/O errors, exit immediately to prevent endless retry loops
|
||||||
if e.errno == 5: # Input/output error
|
if e.errno == PTYConstants.IO_ERROR_ERRNO: # Input/output error
|
||||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||||
"PTY screenDriver: Terminal connection lost, stopping read loop",
|
"PTY screenDriver: Terminal connection lost, stopping read loop",
|
||||||
debug.DebugLevel.ERROR
|
debug.DebugLevel.ERROR
|
||||||
@ -308,7 +377,7 @@ class driver(screenDriver):
|
|||||||
self.terminal.resize(lines, columns)
|
self.terminal.resize(lines, columns)
|
||||||
fd_list = [sys.stdin, self.p_out, self.signalPipe[0]]
|
fd_list = [sys.stdin, self.p_out, self.signalPipe[0]]
|
||||||
while active.value:
|
while active.value:
|
||||||
r, _, _ = select(fd_list, [], [], 0.05) # 50ms timeout for responsiveness
|
r, _, _ = select(fd_list, [], [], self.pty_config.get('select_timeout', PTYConstants.SELECT_TIMEOUT)) # Configurable timeout
|
||||||
# none
|
# none
|
||||||
if r == []:
|
if r == []:
|
||||||
continue
|
continue
|
||||||
@ -320,7 +389,7 @@ class driver(screenDriver):
|
|||||||
# input
|
# input
|
||||||
if sys.stdin in r:
|
if sys.stdin in r:
|
||||||
try:
|
try:
|
||||||
msg_bytes = self.read_all(sys.stdin.fileno(), timeout=0.01, len=4096)
|
msg_bytes = self.read_all(sys.stdin.fileno(), timeout=self.pty_config.get('input_timeout', PTYConstants.INPUT_READ_TIMEOUT), len=PTYConstants.INPUT_BUFFER_SIZE)
|
||||||
except (EOFError, OSError):
|
except (EOFError, OSError):
|
||||||
event_queue.put(
|
event_queue.put(
|
||||||
{
|
{
|
||||||
@ -424,8 +493,8 @@ class driver(screenDriver):
|
|||||||
)
|
)
|
||||||
os.kill(self.p_pid, signal.SIGTERM)
|
os.kill(self.p_pid, signal.SIGTERM)
|
||||||
|
|
||||||
# Wait up to 3 seconds for graceful termination
|
# Wait for graceful termination
|
||||||
timeout = 3.0
|
timeout = self.pty_config.get('process_termination_timeout', PTYConstants.PROCESS_TERMINATION_TIMEOUT)
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
while time.time() - start_time < timeout:
|
while time.time() - start_time < timeout:
|
||||||
try:
|
try:
|
||||||
@ -442,7 +511,7 @@ class driver(screenDriver):
|
|||||||
debug.DebugLevel.WARNING
|
debug.DebugLevel.WARNING
|
||||||
)
|
)
|
||||||
os.kill(self.p_pid, signal.SIGKILL)
|
os.kill(self.p_pid, signal.SIGKILL)
|
||||||
time.sleep(0.5) # Give it a moment
|
time.sleep(PTYConstants.PROCESS_KILL_DELAY) # Give it a moment
|
||||||
|
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||||
|
Reference in New Issue
Block a user