mirror of
https://github.com/lowobservable/oec.git
synced 2026-01-13 15:27:20 +00:00
193 lines
6.3 KiB
Python
193 lines
6.3 KiB
Python
"""
|
|
oec.controller
|
|
~~~~~~~~~~~~~~
|
|
"""
|
|
|
|
import time
|
|
import os
|
|
from select import select
|
|
import logging
|
|
from ptyprocess import PtyProcess
|
|
from coax import poll, poll_ack, read_terminal_id, read_extended_id, \
|
|
KeystrokePollResponse, ReceiveTimeout, ReceiveError, \
|
|
ProtocolError
|
|
|
|
from .terminal import Terminal
|
|
from .emulator import VT100Emulator
|
|
|
|
class Controller:
|
|
"""The controller."""
|
|
|
|
def __init__(self, interface, host_command):
|
|
self.logger = logging.getLogger(__name__)
|
|
|
|
self.running = True
|
|
|
|
self.interface = interface
|
|
self.host_command = host_command
|
|
|
|
self.terminal = None
|
|
self.host_process = None
|
|
self.emulator = None
|
|
|
|
def run(self):
|
|
"""Run the controller."""
|
|
while self.running:
|
|
if self.host_process:
|
|
try:
|
|
if self.host_process in select([self.host_process], [], [], 0)[0]:
|
|
data = self.host_process.read()
|
|
|
|
self._handle_host_process_output(data)
|
|
except EOFError:
|
|
self._handle_host_process_terminated()
|
|
|
|
try:
|
|
poll_response = poll(self.interface, timeout=1)
|
|
except ReceiveTimeout:
|
|
if self.terminal:
|
|
self._handle_terminal_detached()
|
|
|
|
continue
|
|
except ReceiveError as error:
|
|
self.logger.warning(f'POLL receive error: {error}', exc_info=error)
|
|
continue
|
|
except ProtocolError as error:
|
|
self.logger.warning(f'POLL protocol error: {error}', exc_info=error)
|
|
continue
|
|
|
|
if poll_response:
|
|
try:
|
|
poll_ack(self.interface)
|
|
except ReceiveError as error:
|
|
self.logger.warning(f'POLL_ACK receive error: {error}', exc_info=error)
|
|
except ProtocolError as error:
|
|
self.logger.warning(f'POLL_ACK protocol error: {error}', exc_info=error)
|
|
|
|
if not self.terminal:
|
|
self._handle_terminal_attached(poll_response)
|
|
|
|
if poll_response:
|
|
self._handle_poll_response(poll_response)
|
|
else:
|
|
time.sleep(0.1)
|
|
|
|
def _handle_terminal_attached(self, poll_response):
|
|
self.logger.info('Terminal attached')
|
|
|
|
# Read the terminal identifiers.
|
|
(terminal_id, extended_id) = self._read_terminal_ids()
|
|
|
|
self.logger.info(f'Terminal ID = {terminal_id}, Extended ID = {extended_id}')
|
|
|
|
# Initialize the terminal.
|
|
self.terminal = Terminal(self.interface, terminal_id, extended_id)
|
|
|
|
(rows, columns) = self.terminal.dimensions
|
|
keymap_name = self.terminal.keyboard.keymap.name
|
|
|
|
self.logger.info(f'Rows = {rows}, Columns = {columns}, Keymap = {keymap_name}')
|
|
|
|
self.terminal.clear_screen()
|
|
|
|
# Show the attached indicator on the status line.
|
|
self.terminal.status_line.write_string(0, 'S')
|
|
|
|
# Start the process.
|
|
self.host_process = self._start_host_process()
|
|
|
|
# Initialize the emulator.
|
|
self.emulator = VT100Emulator(self.terminal, self.host_process)
|
|
|
|
def _read_terminal_ids(self):
|
|
terminal_id = None
|
|
extended_id = None
|
|
|
|
try:
|
|
terminal_id = read_terminal_id(self.interface)
|
|
except ReceiveError as error:
|
|
self.logger.warning(f'READ_TERMINAL_ID receive error: {error}', exc_info=error)
|
|
except ProtocolError as error:
|
|
self.logger.warning(f'READ_TERMINAL_ID protocol error: {error}', exc_info=error)
|
|
|
|
try:
|
|
extended_id = read_extended_id(self.interface)
|
|
except ReceiveError as error:
|
|
self.logger.warning(f'READ_EXTENDED_ID receive error: {error}', exc_info=error)
|
|
except ProtocolError as error:
|
|
self.logger.warning(f'READ_EXTENDED_ID protocol error: {error}', exc_info=error)
|
|
|
|
return (terminal_id, extended_id.hex() if extended_id is not None else None)
|
|
|
|
def _handle_terminal_detached(self):
|
|
self.logger.info('Terminal detached')
|
|
|
|
if self.host_process:
|
|
self.logger.debug('Terminating host process')
|
|
|
|
if not self.host_process.terminate(force=True):
|
|
self.logger.error('Unable to terminate host process')
|
|
else:
|
|
self.logger.debug('Host process terminated')
|
|
|
|
self.terminal = None
|
|
self.host_process = None
|
|
self.emulator = None
|
|
|
|
def _handle_poll_response(self, poll_response):
|
|
if isinstance(poll_response, KeystrokePollResponse):
|
|
self._handle_keystroke_poll_response(poll_response)
|
|
|
|
def _handle_keystroke_poll_response(self, poll_response):
|
|
scan_code = poll_response.scan_code
|
|
|
|
(key, modifiers, modifiers_changed) = self.terminal.keyboard.get_key(scan_code)
|
|
|
|
if self.logger.isEnabledFor(logging.DEBUG):
|
|
self.logger.debug((f'Keystroke detected: Scan Code = {scan_code}, '
|
|
f'Key = {key}, Modifiers = {modifiers}'))
|
|
|
|
# Update the status line if modifiers have changed.
|
|
if modifiers_changed:
|
|
indicators = bytearray(1)
|
|
|
|
if modifiers.is_shift():
|
|
indicators[0] = 0xda
|
|
else:
|
|
indicators[0] = 0x00
|
|
|
|
self.terminal.status_line.write(35, indicators)
|
|
|
|
if not key:
|
|
return
|
|
|
|
if self.emulator:
|
|
self.emulator.handle_key(key, modifiers, scan_code)
|
|
|
|
def _start_host_process(self):
|
|
environment = os.environ.copy()
|
|
|
|
environment['TERM'] = 'vt100'
|
|
environment['LC_ALL'] = 'C'
|
|
|
|
process = PtyProcess.spawn(self.host_command, env=environment,
|
|
dimensions=self.terminal.dimensions)
|
|
|
|
return process
|
|
|
|
def _handle_host_process_output(self, data):
|
|
if self.logger.isEnabledFor(logging.DEBUG):
|
|
self.logger.debug(f'Output from host process: {data}')
|
|
|
|
if self.emulator:
|
|
self.emulator.handle_host_output(data)
|
|
|
|
def _handle_host_process_terminated(self):
|
|
self.logger.info('Host process terminated')
|
|
|
|
if self.host_process.isalive():
|
|
self.logger.error('Host process is reporting as alive')
|
|
|
|
self.host_process = None
|
|
self.emulator = None
|