mirror of
https://github.com/avatao-content/baseimage-tutorial-framework
synced 2024-11-22 17:11:32 +00:00
Add a huge bunch of docstrings
This commit is contained in:
parent
690f9bb190
commit
addd517ba7
@ -24,6 +24,17 @@ class CallbackEventHandler(PatternMatchingEventHandler, ABC):
|
|||||||
|
|
||||||
|
|
||||||
class HistoryMonitor(CallbackMixin, ObserverMixin, ABC):
|
class HistoryMonitor(CallbackMixin, ObserverMixin, ABC):
|
||||||
|
"""
|
||||||
|
Abstract class capable of monitoring and parsing a history file such as
|
||||||
|
bash HISTFILEs. Monitoring means detecting when the file was changed and
|
||||||
|
notifying subscribers about new content in the file.
|
||||||
|
|
||||||
|
This is useful for monitoring CLI sessions.
|
||||||
|
|
||||||
|
To specify a custom HistoryMonitor inherit from this class and override the
|
||||||
|
command pattern property and optionally the sanitize_command method.
|
||||||
|
See examples below.
|
||||||
|
"""
|
||||||
def __init__(self, histfile):
|
def __init__(self, histfile):
|
||||||
CallbackMixin.__init__(self)
|
CallbackMixin.__init__(self)
|
||||||
ObserverMixin.__init__(self)
|
ObserverMixin.__init__(self)
|
||||||
@ -61,6 +72,15 @@ class HistoryMonitor(CallbackMixin, ObserverMixin, ABC):
|
|||||||
|
|
||||||
|
|
||||||
class BashMonitor(HistoryMonitor):
|
class BashMonitor(HistoryMonitor):
|
||||||
|
"""
|
||||||
|
HistoryMonitor for monitoring bash CLI sessions.
|
||||||
|
This requires the following to be set in bash
|
||||||
|
(note that this is done automatically by TFW):
|
||||||
|
PROMPT_COMMAND="history -a"
|
||||||
|
shopt -s cmdhist
|
||||||
|
shopt -s histappend
|
||||||
|
unset HISTCONTROL
|
||||||
|
"""
|
||||||
@property
|
@property
|
||||||
def command_pattern(self):
|
def command_pattern(self):
|
||||||
return r'.+'
|
return r'.+'
|
||||||
@ -70,6 +90,10 @@ class BashMonitor(HistoryMonitor):
|
|||||||
|
|
||||||
|
|
||||||
class GDBMonitor(HistoryMonitor):
|
class GDBMonitor(HistoryMonitor):
|
||||||
|
"""
|
||||||
|
HistoryMonitor to monitor GDB sessions.
|
||||||
|
For this to work "set trace-commands on" must be set in GDB.
|
||||||
|
"""
|
||||||
@property
|
@property
|
||||||
def command_pattern(self):
|
def command_pattern(self):
|
||||||
return r'(?<=\n)\+(.+)\n'
|
return r'(?<=\n)\+(.+)\n'
|
||||||
|
@ -23,6 +23,18 @@ class ProcessManager(SupervisorMixin):
|
|||||||
|
|
||||||
|
|
||||||
class ProcessManagingEventHandler(EventHandlerBase):
|
class ProcessManagingEventHandler(EventHandlerBase):
|
||||||
|
"""
|
||||||
|
Event handler that can manage processes managed by supervisor.
|
||||||
|
|
||||||
|
This EventHandler accepts messages that have a data["command"] key specifying
|
||||||
|
a command to be executed.
|
||||||
|
Every message must contain a data["process_name"] field with the name of the
|
||||||
|
process to manage. This is the name specified in supervisor config files like so:
|
||||||
|
[program:someprogram]
|
||||||
|
|
||||||
|
Commands available: start, stop, restart, readlog
|
||||||
|
(the names are as self-documenting as it gets)
|
||||||
|
"""
|
||||||
def __init__(self, key, dirmonitor=None):
|
def __init__(self, key, dirmonitor=None):
|
||||||
super().__init__(key)
|
super().__init__(key)
|
||||||
self.key = key
|
self.key = key
|
||||||
|
@ -11,7 +11,20 @@ LOG = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class TerminadoEventHandler(EventHandlerBase):
|
class TerminadoEventHandler(EventHandlerBase):
|
||||||
|
"""
|
||||||
|
Event handler responsible for managing terminal sessions for frontend xterm
|
||||||
|
sessions to connect to. You need to instanciate this in order for frontend
|
||||||
|
terminals to work.
|
||||||
|
|
||||||
|
This EventHandler accepts messages that have a data["command"] key specifying
|
||||||
|
a command to be executed.
|
||||||
|
The API of each command is documented in their respective handlers.
|
||||||
|
"""
|
||||||
def __init__(self, key, monitor):
|
def __init__(self, key, monitor):
|
||||||
|
"""
|
||||||
|
:param key: key this EventHandler listens to
|
||||||
|
:param monitor: tfw.components.HistoryMonitor instance to read command history from
|
||||||
|
"""
|
||||||
super().__init__(key)
|
super().__init__(key)
|
||||||
self.working_directory = TFWENV.TERMINADO_DIR
|
self.working_directory = TFWENV.TERMINADO_DIR
|
||||||
self._historymonitor = monitor
|
self._historymonitor = monitor
|
||||||
@ -37,9 +50,21 @@ class TerminadoEventHandler(EventHandlerBase):
|
|||||||
LOG.error('IGNORING MESSAGE: Invalid message received: %s', message)
|
LOG.error('IGNORING MESSAGE: Invalid message received: %s', message)
|
||||||
|
|
||||||
def write(self, data):
|
def write(self, data):
|
||||||
|
"""
|
||||||
|
Writes a string to the terminal session (on the pty level).
|
||||||
|
Useful for pre-typing and executing commands for the user.
|
||||||
|
|
||||||
|
:param data['shellcmd']: command to be written to the pty
|
||||||
|
"""
|
||||||
self.terminado_server.pty.write(data['shellcmd'])
|
self.terminado_server.pty.write(data['shellcmd'])
|
||||||
|
|
||||||
def read(self, data):
|
def read(self, data):
|
||||||
|
"""
|
||||||
|
Reads the history of commands executed.
|
||||||
|
|
||||||
|
:param data['count']: the number of history elements to return
|
||||||
|
:return: message with list of commands in data['history']
|
||||||
|
"""
|
||||||
data['count'] = int(data.get('count', 1))
|
data['count'] = int(data.get('count', 1))
|
||||||
if self.historymonitor:
|
if self.historymonitor:
|
||||||
data['history'] = self.historymonitor.history[-data['count']:]
|
data['history'] = self.historymonitor.history[-data['count']:]
|
||||||
|
@ -10,6 +10,24 @@ LOG = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class TerminalCommands(ABC):
|
class TerminalCommands(ABC):
|
||||||
|
"""
|
||||||
|
A class you can use to define hooks for terminal commands. This means that you can
|
||||||
|
have python code executed when the user enters a specific command to the terminal on
|
||||||
|
our frontend.
|
||||||
|
|
||||||
|
To receive events you need to subscribe TerminalCommand.callback to a HistoryMonitor
|
||||||
|
instance.
|
||||||
|
|
||||||
|
Inherit from this class and define methods which start with "command_". When the user
|
||||||
|
executes the command specified after the underscore, your method will be invoked. All
|
||||||
|
such commands must expect the parameter *args which will contain the arguments of the
|
||||||
|
command.
|
||||||
|
|
||||||
|
For example to define a method that runs when someone starts vim in the terminal
|
||||||
|
you have to define a method like: "def command_vim(self, *args)"
|
||||||
|
|
||||||
|
You can also use this class to create new commands similarly.
|
||||||
|
"""
|
||||||
def __init__(self, bashrc=None):
|
def __init__(self, bashrc=None):
|
||||||
self._command_method_regex = r'^command_(.+)$'
|
self._command_method_regex = r'^command_(.+)$'
|
||||||
self.command_implemetations = self._build_command_to_implementation_dict()
|
self.command_implemetations = self._build_command_to_implementation_dict()
|
||||||
|
@ -93,7 +93,23 @@ class FileManager: # pylint: disable=too-many-instance-attributes
|
|||||||
|
|
||||||
class WebideEventHandler(EventHandlerBase, MonitorManagerMixin):
|
class WebideEventHandler(EventHandlerBase, MonitorManagerMixin):
|
||||||
# pylint: disable=too-many-arguments
|
# pylint: disable=too-many-arguments
|
||||||
|
"""
|
||||||
|
Event handler implementing the backend of our browser based IDE.
|
||||||
|
By default all files in the directory specified in __init__ are displayed
|
||||||
|
on the fontend. Note that this is a stateful component.
|
||||||
|
|
||||||
|
This EventHandler accepts messages that have a data["command"] key specifying
|
||||||
|
a command to be executed.
|
||||||
|
The API of each command is documented in their respective handlers.
|
||||||
|
"""
|
||||||
def __init__(self, key, directory, allowed_directories, selected_file=None, exclude=None):
|
def __init__(self, key, directory, allowed_directories, selected_file=None, exclude=None):
|
||||||
|
"""
|
||||||
|
:param key: the key this instance should listen to
|
||||||
|
:param directory: working directory which the EventHandler should serve files from
|
||||||
|
:param allowed_directories: list of directories that can be switched to using the selectdir command
|
||||||
|
:param selected_file: file that is selected by default
|
||||||
|
:param exclude: list of filenames that should not appear between files (for *.o, *.pyc, etc.)
|
||||||
|
"""
|
||||||
super().__init__(key)
|
super().__init__(key)
|
||||||
self.filemanager = FileManager(allowed_directories=allowed_directories, working_directory=directory,
|
self.filemanager = FileManager(allowed_directories=allowed_directories, working_directory=directory,
|
||||||
selected_file=selected_file, exclude=exclude)
|
selected_file=selected_file, exclude=exclude)
|
||||||
@ -106,6 +122,11 @@ class WebideEventHandler(EventHandlerBase, MonitorManagerMixin):
|
|||||||
'exclude': self.exclude}
|
'exclude': self.exclude}
|
||||||
|
|
||||||
def read(self, data):
|
def read(self, data):
|
||||||
|
"""
|
||||||
|
Read the currently selected file.
|
||||||
|
|
||||||
|
:return: message with the contents of the file in data['content']
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
data['content'] = self.filemanager.file_contents
|
data['content'] = self.filemanager.file_contents
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
@ -117,6 +138,11 @@ class WebideEventHandler(EventHandlerBase, MonitorManagerMixin):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def write(self, data):
|
def write(self, data):
|
||||||
|
"""
|
||||||
|
Overwrites a file with the desired string.
|
||||||
|
|
||||||
|
:param data['content']: string containing the desired file contents
|
||||||
|
"""
|
||||||
self.monitor.ignore = self.monitor.ignore + 1
|
self.monitor.ignore = self.monitor.ignore + 1
|
||||||
try:
|
try:
|
||||||
self.filemanager.file_contents = data['content']
|
self.filemanager.file_contents = data['content']
|
||||||
@ -126,6 +152,11 @@ class WebideEventHandler(EventHandlerBase, MonitorManagerMixin):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def select(self, data):
|
def select(self, data):
|
||||||
|
"""
|
||||||
|
Selects a file from the current directory.
|
||||||
|
|
||||||
|
:param data['filename']: name of file to select relative to the current directory
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
self.filemanager.filename = data['filename']
|
self.filemanager.filename = data['filename']
|
||||||
except EnvironmentError:
|
except EnvironmentError:
|
||||||
@ -133,6 +164,13 @@ class WebideEventHandler(EventHandlerBase, MonitorManagerMixin):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def select_dir(self, data):
|
def select_dir(self, data):
|
||||||
|
"""
|
||||||
|
Select a new working directory to display files from.
|
||||||
|
|
||||||
|
:param data['directory']: absolute path of diretory to select.
|
||||||
|
must be a path whitelisted in
|
||||||
|
self.allowed_directories
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
self.filemanager.workdir = data['directory']
|
self.filemanager.workdir = data['directory']
|
||||||
self.reload_monitor()
|
self.reload_monitor()
|
||||||
@ -146,6 +184,11 @@ class WebideEventHandler(EventHandlerBase, MonitorManagerMixin):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def exclude(self, data):
|
def exclude(self, data):
|
||||||
|
"""
|
||||||
|
Overwrite list of excluded files
|
||||||
|
|
||||||
|
:param data['exclude']: list of filename patterns to be excluded, e.g.: ["*.pyc", "*.o"]
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
self.filemanager.exclude = list(data['exclude'])
|
self.filemanager.exclude = list(data['exclude'])
|
||||||
except TypeError:
|
except TypeError:
|
||||||
@ -153,6 +196,9 @@ class WebideEventHandler(EventHandlerBase, MonitorManagerMixin):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def attach_fileinfo(self, data):
|
def attach_fileinfo(self, data):
|
||||||
|
"""
|
||||||
|
Basic information included in every response to the frontend.
|
||||||
|
"""
|
||||||
data['filename'] = self.filemanager.filename
|
data['filename'] = self.filemanager.filename
|
||||||
data['files'] = self.filemanager.files
|
data['files'] = self.filemanager.files
|
||||||
data['directory'] = self.filemanager.workdir
|
data['directory'] = self.filemanager.workdir
|
||||||
|
@ -8,6 +8,12 @@ from tfw.networking.event_handlers import ServerConnector
|
|||||||
|
|
||||||
|
|
||||||
class EventHandlerBase(ABC):
|
class EventHandlerBase(ABC):
|
||||||
|
"""
|
||||||
|
Abstract base class for all Python based EventHandlers. Useful implementation template
|
||||||
|
for other languages.
|
||||||
|
|
||||||
|
Derived classes must implement the handle_event() method
|
||||||
|
"""
|
||||||
def __init__(self, key):
|
def __init__(self, key):
|
||||||
self.server_connector = ServerConnector()
|
self.server_connector = ServerConnector()
|
||||||
self.key = key
|
self.key = key
|
||||||
@ -60,6 +66,10 @@ class EventHandlerBase(ABC):
|
|||||||
|
|
||||||
class TriggeredEventHandler(EventHandlerBase, ABC):
|
class TriggeredEventHandler(EventHandlerBase, ABC):
|
||||||
# pylint: disable=abstract-method
|
# pylint: disable=abstract-method
|
||||||
|
"""
|
||||||
|
Abstract base class for EventHandlers which are only triggered in case
|
||||||
|
TFWServer has successfully triggered an FSM step defined in __init__.
|
||||||
|
"""
|
||||||
def __init__(self, key, trigger):
|
def __init__(self, key, trigger):
|
||||||
super().__init__(key)
|
super().__init__(key)
|
||||||
self.trigger = trigger
|
self.trigger = trigger
|
||||||
|
@ -9,6 +9,13 @@ from tfw.mixins import CallbackMixin
|
|||||||
|
|
||||||
|
|
||||||
class FSMBase(CallbackMixin):
|
class FSMBase(CallbackMixin):
|
||||||
|
"""
|
||||||
|
A general FSM base class you can inherit from to track user progress.
|
||||||
|
See linear_fsm.py for an example use-case.
|
||||||
|
TFW the transitions library for state machines, please refer to their
|
||||||
|
documentation for more information on creating your own machines:
|
||||||
|
https://github.com/pytransitions/transitions
|
||||||
|
"""
|
||||||
states, transitions = [], []
|
states, transitions = [], []
|
||||||
|
|
||||||
def __init__(self, initial: str = None, accepted_states: List[str] = None):
|
def __init__(self, initial: str = None, accepted_states: List[str] = None):
|
||||||
|
@ -5,6 +5,12 @@ from .fsm_base import FSMBase
|
|||||||
|
|
||||||
|
|
||||||
class LinearFSM(FSMBase):
|
class LinearFSM(FSMBase):
|
||||||
|
"""
|
||||||
|
This is a state machine for challenges with linear progression, consisting of
|
||||||
|
a number of steps specified in the constructor. It automatically sets up a single
|
||||||
|
action between states as such:
|
||||||
|
0 ==step_1==> 1 ==step_2==> 2 ==step_3==> 3 ... and so on
|
||||||
|
"""
|
||||||
def __init__(self, number_of_steps):
|
def __init__(self, number_of_steps):
|
||||||
self.states = list(map(str, range(number_of_steps)))
|
self.states = list(map(str, range(number_of_steps)))
|
||||||
self.transitions = [{'trigger': 'step_{}'.format(int(index)+1), 'source': index, 'dest': str(int(index)+1)}
|
self.transitions = [{'trigger': 'step_{}'.format(int(index)+1), 'source': index, 'dest': str(int(index)+1)}
|
||||||
|
@ -9,6 +9,12 @@ class CallbackMixin:
|
|||||||
self._callbacks = []
|
self._callbacks = []
|
||||||
|
|
||||||
def subscribe_callback(self, callback, *args, **kwargs):
|
def subscribe_callback(self, callback, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Subscribe a callable to invoke once an event is triggered.
|
||||||
|
:param callback: callable to be executed on events
|
||||||
|
:param *args: arguments passed to callable
|
||||||
|
:param **kwargs: kwargs passed to callable
|
||||||
|
"""
|
||||||
fun = partial(callback, *args, **kwargs)
|
fun = partial(callback, *args, **kwargs)
|
||||||
self._callbacks.append(fun)
|
self._callbacks.append(fun)
|
||||||
|
|
||||||
|
@ -24,18 +24,30 @@ class ServerDownlinkConnector(ZMQConnectorBase):
|
|||||||
|
|
||||||
|
|
||||||
class ServerUplinkConnector(ZMQConnectorBase):
|
class ServerUplinkConnector(ZMQConnectorBase):
|
||||||
|
"""
|
||||||
|
Class capable of sending messages to the TFW server and event handlers.
|
||||||
|
"""
|
||||||
def __init__(self, zmq_context=None):
|
def __init__(self, zmq_context=None):
|
||||||
super(ServerUplinkConnector, self).__init__(zmq_context)
|
super(ServerUplinkConnector, self).__init__(zmq_context)
|
||||||
self._zmq_push_socket = self._zmq_context.socket(zmq.PUSH)
|
self._zmq_push_socket = self._zmq_context.socket(zmq.PUSH)
|
||||||
self._zmq_push_socket.connect('tcp://localhost:{}'.format(TFWENV.RECEIVER_PORT))
|
self._zmq_push_socket.connect('tcp://localhost:{}'.format(TFWENV.RECEIVER_PORT))
|
||||||
|
|
||||||
def send_to_eventhandler(self, message):
|
def send_to_eventhandler(self, message):
|
||||||
|
"""
|
||||||
|
Send a message to an event handler.
|
||||||
|
:param message: JSON message you want to send
|
||||||
|
:param message['key']: key of event handler you want to address
|
||||||
|
"""
|
||||||
nested_message = {'key': message['key'], 'data': message.pop('data')}
|
nested_message = {'key': message['key'], 'data': message.pop('data')}
|
||||||
message['key'] = 'mirror'
|
message['key'] = 'mirror'
|
||||||
message['data'] = nested_message
|
message['data'] = nested_message
|
||||||
self.send(message)
|
self.send(message)
|
||||||
|
|
||||||
def send(self, message):
|
def send(self, message):
|
||||||
|
"""
|
||||||
|
Send a message to the TFW server
|
||||||
|
:param message: JSON message you want to send
|
||||||
|
"""
|
||||||
self._zmq_push_socket.send_multipart(serialize_tfw_msg(message))
|
self._zmq_push_socket.send_multipart(serialize_tfw_msg(message))
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,11 +7,20 @@ from tfw.networking.event_handlers import ServerUplinkConnector
|
|||||||
|
|
||||||
|
|
||||||
class MessageSender:
|
class MessageSender:
|
||||||
|
"""
|
||||||
|
Provides a mechanism to send messages to our frontend messaging component which
|
||||||
|
displays messages with the key "message".
|
||||||
|
"""
|
||||||
def __init__(self, custom_key: str = None):
|
def __init__(self, custom_key: str = None):
|
||||||
self.server_connector = ServerUplinkConnector()
|
self.server_connector = ServerUplinkConnector()
|
||||||
self.key = custom_key or 'message'
|
self.key = custom_key or 'message'
|
||||||
|
|
||||||
def send(self, originator, message):
|
def send(self, originator, message):
|
||||||
|
"""
|
||||||
|
Sends a message to the key specified in __init__.
|
||||||
|
:param originator: name of sender to be displayed on the frontend
|
||||||
|
:param message: message to send
|
||||||
|
"""
|
||||||
data = {
|
data = {
|
||||||
'originator': originator,
|
'originator': originator,
|
||||||
'timestamp': datetime.now().isoformat(),
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
@ -15,7 +15,15 @@ LOG = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class TFWServer:
|
class TFWServer:
|
||||||
|
"""
|
||||||
|
This class handles the proxying of messages between the frontend and event handers.
|
||||||
|
It proxies messages from the "/ws" route to all event handlers subscribed to a ZMQ
|
||||||
|
SUB socket. It also manages an FSM you can define as a constructor argument.
|
||||||
|
"""
|
||||||
def __init__(self, fsm_type):
|
def __init__(self, fsm_type):
|
||||||
|
"""
|
||||||
|
:param fsm_type: the type of FSM you want TFW to use
|
||||||
|
"""
|
||||||
self._fsm = fsm_type()
|
self._fsm = fsm_type()
|
||||||
self._fsm_updater = FSMUpdater(self._fsm)
|
self._fsm_updater = FSMUpdater(self._fsm)
|
||||||
self._fsm_manager = FSMManager(self._fsm)
|
self._fsm_manager = FSMManager(self._fsm)
|
||||||
|
Loading…
Reference in New Issue
Block a user