|
import re
|
|
import time
|
|
from abc import ABC, abstractmethod
|
|
|
|
class ProcessHandler(ABC):
|
|
"""Base class for process-specific handlers."""
|
|
|
|
def __init__(self, multiplexer):
|
|
self.multiplexer = multiplexer
|
|
self.state_machine = {}
|
|
self.current_state = 'initial'
|
|
self.prompt_patterns = []
|
|
self.response_suggestions = {}
|
|
|
|
@abstractmethod
|
|
def get_process_type(self):
|
|
"""Return the process type this handler manages."""
|
|
pass
|
|
|
|
def update_state(self, output):
|
|
"""Update internal state based on output."""
|
|
pass
|
|
|
|
def get_prompt_suggestions(self):
|
|
"""Return suggested responses for current state."""
|
|
return self.response_suggestions.get(self.current_state, [])
|
|
|
|
def is_waiting_for_input(self):
|
|
"""Check if process appears to be waiting for input."""
|
|
return self.current_state in ['waiting_confirmation', 'waiting_input']
|
|
|
|
class AptHandler(ProcessHandler):
|
|
"""Handler for apt package manager interactions."""
|
|
|
|
def __init__(self, multiplexer):
|
|
super().__init__(multiplexer)
|
|
self.state_machine = {
|
|
'initial': ['running_command'],
|
|
'running_command': ['waiting_confirmation', 'completed'],
|
|
'waiting_confirmation': ['confirmed', 'cancelled'],
|
|
'confirmed': ['installing', 'completed'],
|
|
'installing': ['completed', 'error'],
|
|
'completed': [],
|
|
'error': [],
|
|
'cancelled': []
|
|
}
|
|
self.prompt_patterns = [
|
|
(r'Do you want to continue\?', 'confirmation'),
|
|
(r'After this operation.*installed\.', 'size_info'),
|
|
(r'Need to get.*B of archives\.', 'download_info'),
|
|
(r'Unpacking.*Configuring', 'configuring'),
|
|
(r'Setting up', 'setting_up'),
|
|
(r'E:\s', 'error')
|
|
]
|
|
|
|
def get_process_type(self):
|
|
return 'apt'
|
|
|
|
def update_state(self, output):
|
|
"""Update state based on apt output patterns."""
|
|
output_lower = output.lower()
|
|
|
|
# Check for completion
|
|
if 'processing triggers' in output_lower or 'done' in output_lower:
|
|
self.current_state = 'completed'
|
|
# Check for confirmation prompts
|
|
elif 'do you want to continue' in output_lower:
|
|
self.current_state = 'waiting_confirmation'
|
|
# Check for installation progress
|
|
elif 'setting up' in output_lower or 'unpacking' in output_lower:
|
|
self.current_state = 'installing'
|
|
# Check for errors
|
|
elif 'e:' in output_lower or 'error' in output_lower:
|
|
self.current_state = 'error'
|
|
|
|
def get_prompt_suggestions(self):
|
|
"""Return suggested responses for apt prompts."""
|
|
suggestions = super().get_prompt_suggestions()
|
|
if self.current_state == 'waiting_confirmation':
|
|
suggestions.extend(['y', 'yes', 'n', 'no'])
|
|
return suggestions
|
|
|
|
class VimHandler(ProcessHandler):
|
|
"""Handler for vim editor interactions."""
|
|
|
|
def __init__(self, multiplexer):
|
|
super().__init__(multiplexer)
|
|
self.state_machine = {
|
|
'initial': ['normal_mode', 'insert_mode'],
|
|
'normal_mode': ['insert_mode', 'command_mode', 'visual_mode'],
|
|
'insert_mode': ['normal_mode'],
|
|
'command_mode': ['normal_mode'],
|
|
'visual_mode': ['normal_mode'],
|
|
'exiting': []
|
|
}
|
|
self.prompt_patterns = [
|
|
(r'-- INSERT --', 'insert_mode'),
|
|
(r'-- VISUAL --', 'visual_mode'),
|
|
(r':', 'command_mode'),
|
|
(r'Press ENTER', 'waiting_enter'),
|
|
(r'Saved', 'saved')
|
|
]
|
|
self.mode_indicators = {
|
|
'insert': '-- INSERT --',
|
|
'visual': '-- VISUAL --',
|
|
'command': ':'
|
|
}
|
|
|
|
def get_process_type(self):
|
|
return 'vim'
|
|
|
|
def update_state(self, output):
|
|
"""Update state based on vim mode indicators."""
|
|
if '-- INSERT --' in output:
|
|
self.current_state = 'insert_mode'
|
|
elif '-- VISUAL --' in output:
|
|
self.current_state = 'visual_mode'
|
|
elif output.strip().endswith(':'):
|
|
self.current_state = 'command_mode'
|
|
elif 'Press ENTER' in output:
|
|
self.current_state = 'waiting_enter'
|
|
else:
|
|
# Default to normal mode if no specific indicators
|
|
self.current_state = 'normal_mode'
|
|
|
|
def get_prompt_suggestions(self):
|
|
"""Return suggested commands for vim modes."""
|
|
suggestions = super().get_prompt_suggestions()
|
|
if self.current_state == 'command_mode':
|
|
suggestions.extend(['w', 'q', 'wq', 'q!', 'w!'])
|
|
elif self.current_state == 'normal_mode':
|
|
suggestions.extend(['i', 'a', 'o', 'dd', ':w', ':q'])
|
|
elif self.current_state == 'waiting_enter':
|
|
suggestions.extend(['\n'])
|
|
return suggestions
|
|
|
|
class SSHHandler(ProcessHandler):
|
|
"""Handler for SSH connection interactions."""
|
|
|
|
def __init__(self, multiplexer):
|
|
super().__init__(multiplexer)
|
|
self.state_machine = {
|
|
'initial': ['connecting'],
|
|
'connecting': ['auth_prompt', 'connected', 'failed'],
|
|
'auth_prompt': ['connected', 'failed'],
|
|
'connected': ['shell', 'disconnected'],
|
|
'shell': ['disconnected'],
|
|
'failed': [],
|
|
'disconnected': []
|
|
}
|
|
self.prompt_patterns = [
|
|
(r'password:', 'password_prompt'),
|
|
(r'yes/no', 'host_key_prompt'),
|
|
(r'Permission denied', 'auth_failed'),
|
|
(r'Welcome to', 'connected'),
|
|
(r'\$', 'shell_prompt'),
|
|
(r'\#', 'root_shell_prompt'),
|
|
(r'Connection closed', 'disconnected')
|
|
]
|
|
|
|
def get_process_type(self):
|
|
return 'ssh'
|
|
|
|
def update_state(self, output):
|
|
"""Update state based on SSH connection output."""
|
|
output_lower = output.lower()
|
|
|
|
if 'permission denied' in output_lower:
|
|
self.current_state = 'failed'
|
|
elif 'password:' in output_lower:
|
|
self.current_state = 'auth_prompt'
|
|
elif 'yes/no' in output_lower:
|
|
self.current_state = 'auth_prompt'
|
|
elif 'welcome to' in output_lower or 'last login' in output_lower:
|
|
self.current_state = 'connected'
|
|
elif output.strip().endswith('$') or output.strip().endswith('#'):
|
|
self.current_state = 'shell'
|
|
elif 'connection closed' in output_lower:
|
|
self.current_state = 'disconnected'
|
|
|
|
def get_prompt_suggestions(self):
|
|
"""Return suggested responses for SSH prompts."""
|
|
suggestions = super().get_prompt_suggestions()
|
|
if self.current_state == 'auth_prompt':
|
|
if 'password:' in self.multiplexer.get_all_output()['stdout']:
|
|
suggestions.extend(['<password>']) # Placeholder for actual password
|
|
elif 'yes/no' in self.multiplexer.get_all_output()['stdout']:
|
|
suggestions.extend(['yes', 'no'])
|
|
return suggestions
|
|
|
|
class GenericProcessHandler(ProcessHandler):
|
|
"""Fallback handler for unknown process types."""
|
|
|
|
def __init__(self, multiplexer):
|
|
super().__init__(multiplexer)
|
|
self.state_machine = {
|
|
'initial': ['running'],
|
|
'running': ['waiting_input', 'completed'],
|
|
'waiting_input': ['running'],
|
|
'completed': []
|
|
}
|
|
self.prompt_patterns = [
|
|
(r'\?\s*$', 'waiting_input'), # Lines ending with ?
|
|
(r'>\s*$', 'waiting_input'), # Lines ending with >
|
|
(r':\s*$', 'waiting_input'), # Lines ending with :
|
|
(r'done', 'completed'),
|
|
(r'finished', 'completed'),
|
|
(r'exit code', 'completed')
|
|
]
|
|
|
|
def get_process_type(self):
|
|
return 'generic'
|
|
|
|
def update_state(self, output):
|
|
"""Basic state detection for generic processes."""
|
|
output_lower = output.lower()
|
|
|
|
if any(pattern in output_lower for pattern in ['done', 'finished', 'complete']):
|
|
self.current_state = 'completed'
|
|
elif any(output.strip().endswith(char) for char in ['?', '>', ':']):
|
|
self.current_state = 'waiting_input'
|
|
else:
|
|
self.current_state = 'running'
|
|
|
|
# Handler registry
|
|
_handler_classes = {
|
|
'apt': AptHandler,
|
|
'vim': VimHandler,
|
|
'ssh': SSHHandler,
|
|
'generic': GenericProcessHandler
|
|
}
|
|
|
|
def get_handler_for_process(process_type, multiplexer):
|
|
"""Get appropriate handler for a process type."""
|
|
handler_class = _handler_classes.get(process_type, GenericProcessHandler)
|
|
return handler_class(multiplexer)
|
|
|
|
def detect_process_type(command):
|
|
"""Detect process type from command."""
|
|
command_str = ' '.join(command) if isinstance(command, list) else command
|
|
command_lower = command_str.lower()
|
|
|
|
if 'apt' in command_lower or 'apt-get' in command_lower:
|
|
return 'apt'
|
|
elif 'vim' in command_lower or 'vi ' in command_lower:
|
|
return 'vim'
|
|
elif 'ssh' in command_lower:
|
|
return 'ssh'
|
|
else:
|
|
return 'generic'
|
|
return 'ssh'
|
|
def detect_process_type(command):
|
|
"""Detect process type from command."""
|
|
command_str = ' '.join(command) if isinstance(command, list) else command
|
|
command_lower = command_str.lower()
|
|
|
|
if 'apt' in command_lower or 'apt-get' in command_lower:
|
|
return 'apt'
|
|
elif 'vim' in command_lower or 'vi ' in command_lower:
|
|
return 'vim'
|
|
elif 'ssh' in command_lower:
|
|
return 'ssh'
|
|
else:
|
|
return 'generic'
|