From f233cc41badf625a68b533068447ba097531eea8 Mon Sep 17 00:00:00 2001 From: Andrew Kay Date: Thu, 20 Jun 2019 21:10:45 -0500 Subject: [PATCH] Separate responsibilities of controller and session --- oec/controller.py | 71 +++++++-------------------- oec/session.py | 15 ++++++ oec/{emulator.py => vt100.py} | 90 +++++++++++++++++++++++++---------- 3 files changed, 99 insertions(+), 77 deletions(-) create mode 100644 oec/session.py rename oec/{emulator.py => vt100.py} (73%) diff --git a/oec/controller.py b/oec/controller.py index 8a0520e..f47fc48 100644 --- a/oec/controller.py +++ b/oec/controller.py @@ -4,16 +4,14 @@ 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 +from .session import SessionDisconnectedError +from .vt100 import VT100Session class Controller: """The controller.""" @@ -27,20 +25,16 @@ class Controller: self.host_command = host_command self.terminal = None - self.host_process = None - self.emulator = None + self.session = None def run(self): """Run the controller.""" while self.running: - if self.host_process: + if self.session: 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() + self.session.handle_host() + except SessionDisconnectedError: + self._handle_session_disconnected() try: poll_response = poll(self.interface, timeout=1) @@ -93,11 +87,10 @@ class Controller: # Show the attached indicator on the status line. self.terminal.display.status_line.write_string(0, 'S') - # Start the process. - self.host_process = self._start_host_process() + # Start the session. + self.session = VT100Session(self.terminal, self.host_command) - # Initialize the emulator. - self.emulator = VT100Emulator(self.terminal, self.host_process) + self.session.start() def _read_terminal_ids(self): terminal_id = None @@ -131,17 +124,11 @@ class Controller: 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') + if self.session: + self.session.terminate() self.terminal = None - self.host_process = None - self.emulator = None + self.session = None def _handle_poll_response(self, poll_response): if isinstance(poll_response, KeystrokePollResponse): @@ -170,32 +157,10 @@ class Controller: if not key: return - if self.emulator: - self.emulator.handle_key(key, modifiers, scan_code) + if self.session: + self.session.handle_key(key, modifiers, scan_code) - def _start_host_process(self): - environment = os.environ.copy() + def _handle_session_disconnected(self): + self.logger.info('Session disconnected') - environment['TERM'] = 'vt100' - environment['LC_ALL'] = 'C' - - process = PtyProcess.spawn(self.host_command, env=environment, - dimensions=self.terminal.display.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 + self.session = None diff --git a/oec/session.py b/oec/session.py new file mode 100644 index 0000000..ac35bc2 --- /dev/null +++ b/oec/session.py @@ -0,0 +1,15 @@ +class Session: + def start(self): + raise NotImplementedError + + def terminate(self): + raise NotImplementedError + + def handle_host(self): + raise NotImplementedError + + def handle_key(self, key, keyboard_modifiers, scan_code): + raise NotImplementedError + +class SessionDisconnectedError(Exception): + pass diff --git a/oec/emulator.py b/oec/vt100.py similarity index 73% rename from oec/emulator.py rename to oec/vt100.py index 27828f9..762807d 100644 --- a/oec/emulator.py +++ b/oec/vt100.py @@ -1,11 +1,15 @@ """ -oec.emulator -~~~~~~~~~~~~ +oec.vt100 +~~~~~~~~~ """ +import os +from select import select import logging +from ptyprocess import PtyProcess import pyte +from .session import Session, SessionDisconnectedError from .display import encode_ascii_character from .keyboard import Key, get_ascii_character_for_key @@ -65,14 +69,15 @@ VT100_KEY_MAP_ALT = { Key.NEWLINE: b'\n' } -class VT100Emulator: - """VT100 emulator.""" +class VT100Session(Session): + """VT100 session.""" - def __init__(self, terminal, host): + def __init__(self, terminal, host_command): self.logger = logging.getLogger(__name__) self.terminal = terminal - self.host = host + self.host_command = host_command + self.host_process = None # Initialize the VT100 screen. (rows, columns) = self.terminal.display.dimensions @@ -83,6 +88,10 @@ class VT100Emulator: self.vt100_stream = pyte.ByteStream(self.vt100_screen) + def start(self): + # Start the host process. + self._start_host_process() + # Clear the screen. self.terminal.display.clear_screen() @@ -92,28 +101,30 @@ class VT100Emulator: # Load the address counter. self.terminal.display.load_address_counter(index=0) + def terminate(self): + if self.host_process: + self._terminate_host_process() + + def handle_host(self): + try: + if self.host_process in select([self.host_process], [], [], 0)[0]: + data = self.host_process.read() + + self._handle_host_output(data) + + return True + + return False + except EOFError: + self.host_process = None + + raise SessionDisconnectedError + def handle_key(self, key, keyboard_modifiers, scan_code): - """Handle a terminal keystroke.""" bytes_ = self._map_key(key, keyboard_modifiers) if bytes_ is not None: - self.host.write(bytes_) - - def handle_host_output(self, data): - """Handle output from the host process.""" - self.vt100_stream.feed(data) - - self.update() - - def update(self): - """Update the terminal with dirty changes from the VT100 screen - clears - dirty lines after updating terminal. - """ - self._apply(self.vt100_screen) - - self.vt100_screen.dirty.clear() - - self._flush() + self.host_process.write(bytes_) def _map_key(self, key, keyboard_modifiers): if keyboard_modifiers.is_alt(): @@ -142,6 +153,37 @@ class VT100Emulator: return None + def _start_host_process(self): + environment = os.environ.copy() + + environment['TERM'] = 'vt100' + environment['LC_ALL'] = 'C' + + self.host_process = PtyProcess.spawn(self.host_command, env=environment, + dimensions=self.terminal.display.dimensions) + + def _terminate_host_process(self): + 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.host_process = None + + def _handle_host_output(self, data): + if self.logger.isEnabledFor(logging.DEBUG): + self.logger.debug(f'Host process output: {data}') + + self.vt100_stream.feed(data) + + self._apply(self.vt100_screen) + + self.vt100_screen.dirty.clear() + + self._flush() + def _apply(self, screen): for row in screen.dirty: row_buffer = screen.buffer[row]