Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c831ba5ca4 | ||
|
|
3b30b333e5 | ||
|
|
9b85c19d40 | ||
|
|
0b651abb7d | ||
|
|
b1592e8ebb | ||
|
|
3f36826597 | ||
|
|
db1b008fc9 | ||
|
|
9d93d055b8 | ||
|
|
addd517ba7 | ||
|
|
690f9bb190 | ||
|
|
c909317ca0 | ||
|
|
25d039e84e | ||
|
|
117e7ffc18 |
+9
-13
@@ -11,7 +11,8 @@ RUN curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
|
||||
libzmq5 \
|
||||
nginx \
|
||||
gettext-base &&\
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
rm -rf /var/lib/apt/lists/* &&\
|
||||
ln -sf /bin/bash /bin/sh
|
||||
|
||||
COPY requirements.txt /tmp
|
||||
RUN pip3 install -r /tmp/requirements.txt
|
||||
@@ -26,7 +27,7 @@ ENV TFW_PUBLIC_PORT=8888 \
|
||||
|
||||
EXPOSE ${TFW_PUBLIC_PORT}
|
||||
|
||||
ENV PYTHONPATH="/usr/local/lib/" \
|
||||
ENV PYTHONPATH="/usr/local/lib" \
|
||||
TFW_SUPERVISOR_HTTP_URI="http://localhost:${TFW_SUPERVISOR_HTTP_PORT}" \
|
||||
TFW_SUPERVISORD_CONF="/etc/supervisor/supervisord.conf" \
|
||||
TFW_SUPERVISORD_COMPONENTS="/etc/supervisor/conf" \
|
||||
@@ -39,23 +40,18 @@ ENV PYTHONPATH="/usr/local/lib/" \
|
||||
TFW_HISTFILE="/home/${AVATAO_USER}/.bash_history" \
|
||||
PROMPT_COMMAND="history -a"
|
||||
|
||||
RUN echo "shopt -s cmdhist\n" \
|
||||
"shopt -s histappend\n" \
|
||||
"unset HISTCONTROL\n" \
|
||||
"export HISTFILE=$TFW_HISTFILE\n" \
|
||||
"export HISTFILESIZE=1000\n" \
|
||||
"export HISTSIZE=1000\n" \
|
||||
'PROMPT_COMMAND="history -a"\n' \
|
||||
>> /home/${AVATAO_USER}/.bashrc
|
||||
COPY bashrc /tmp
|
||||
RUN echo "export HISTFILE=${TFW_HISTFILE}\n" >> /tmp/bashrc &&\
|
||||
cat /tmp/bashrc >> /home/${AVATAO_USER}/.bashrc
|
||||
|
||||
COPY supervisor/supervisord.conf ${TFW_SUPERVISORD_CONF}
|
||||
COPY nginx/nginx.conf ${TFW_NGINX_CONF}
|
||||
COPY nginx/default.conf ${TFW_NGINX_DEFAULT}
|
||||
COPY nginx/components/ ${TFW_NGINX_COMPONENTS}
|
||||
COPY lib ${TFW_LIB_DIR}
|
||||
COPY lib LICENSE ${TFW_LIB_DIR}
|
||||
|
||||
RUN for dir in "${TFW_LIB_DIR}" "/etc/nginx" "/etc/supervisor"; do \
|
||||
chown -R root:root "$dir" && chmod -R 700 "$dir"; \
|
||||
RUN for dir in "${TFW_LIB_DIR}"/{tfw,tao,envvars.py} "/etc/nginx" "/etc/supervisor"; do \
|
||||
chown -R root:root "$dir" && chmod -R 700 "$dir"; \
|
||||
done
|
||||
|
||||
ONBUILD ARG BUILD_CONTEXT="solvable"
|
||||
|
||||
@@ -50,6 +50,32 @@ This includes context-dependent hints and the automatic typing of commands to a
|
||||
|
||||
Note that our frontend implementation is written in Angular. It is maintained and documented in the `frontend-tutorial-framework` repository.
|
||||
|
||||
### Messaging format
|
||||
|
||||
The framework uses JSON messages internally and in exposed APIs as well.
|
||||
These messages must comply some rules.
|
||||
Don't worry, we are not too fond of rules around these parts.
|
||||
|
||||
The TFW message format:
|
||||
|
||||
```text
|
||||
{
|
||||
"key: "some identifier used for addressing",
|
||||
"data":
|
||||
{
|
||||
...
|
||||
JSON object carrying anything, preferably cats
|
||||
...
|
||||
},
|
||||
"trigger": "FSM action"
|
||||
}
|
||||
```
|
||||
|
||||
- The `key` field is used by TFW for addressing and every message must have one (it can be an empty string though)
|
||||
- The `data` object can contain anything you might want to send
|
||||
- The `trigger` key is an optional field that triggers an FSM action with that name from the current state (whatever that might be)
|
||||
|
||||
|
||||
## Where to go next
|
||||
|
||||
Most of the components you need have docstrings included (hang on tight, this is work in progress) – refer to them for usage info.
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
_postcmd()
|
||||
{
|
||||
AT_PROMPT=1
|
||||
history -a
|
||||
}
|
||||
PROMPT_COMMAND="_postcmd"
|
||||
|
||||
_precmd()
|
||||
{
|
||||
[ -z "$AT_PROMPT" ] && return
|
||||
unset AT_PROMPT
|
||||
history -a
|
||||
}
|
||||
trap _precmd DEBUG
|
||||
|
||||
shopt -s cmdhist
|
||||
shopt -s histappend
|
||||
unset HISTCONTROL
|
||||
export HISTFILESIZE=1000
|
||||
export HISTSIZE=1000
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 40 KiB |
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
|
||||
# All Rights Reserved. See LICENSE file for details.
|
||||
|
||||
from .event_handler_base import TriggerlessEventHandler, TriggeredEventHandler
|
||||
from .event_handler_base import EventHandlerBase, TriggeredEventHandler
|
||||
from .fsm_base import FSMBase
|
||||
from .linear_fsm import LinearFSM
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
from .directory_monitoring_event_handler import DirectoryMonitoringEventHandler
|
||||
from .process_managing_event_handler import ProcessManagingEventHandler
|
||||
from .terminado_event_handler import TerminadoEventHandler
|
||||
from .webide_event_handler import WebideEventHandler
|
||||
from .ide_event_handler import IdeEventHandler
|
||||
from .history_monitor import HistoryMonitor, BashMonitor, GDBMonitor
|
||||
from .terminal_commands import TerminalCommands
|
||||
|
||||
@@ -17,7 +17,7 @@ LOG = logging.getLogger(__name__)
|
||||
class DirectoryMonitor(ObserverMixin):
|
||||
def __init__(self, directory):
|
||||
ObserverMixin.__init__(self)
|
||||
self.eventhandler = WebideReloadWatchdogEventHandler()
|
||||
self.eventhandler = IdeReloadWatchdogEventHandler()
|
||||
self.observer.schedule(self.eventhandler, directory, recursive=True)
|
||||
self.pause, self.resume = self.eventhandler.pause, self.eventhandler.resume
|
||||
|
||||
@@ -42,7 +42,7 @@ class DirectoryMonitor(ObserverMixin):
|
||||
self.directorymonitor.resume()
|
||||
|
||||
|
||||
class WebideReloadWatchdogEventHandler(FileSystemWatchdogEventHandler):
|
||||
class IdeReloadWatchdogEventHandler(FileSystemWatchdogEventHandler):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.uplink = ServerUplinkConnector()
|
||||
@@ -63,7 +63,7 @@ class WebideReloadWatchdogEventHandler(FileSystemWatchdogEventHandler):
|
||||
self.ignore = self.ignore - 1
|
||||
return
|
||||
LOG.debug(event)
|
||||
self.uplink.send({'key': 'webide',
|
||||
self.uplink.send({'key': 'ide',
|
||||
'data': {'command': 'reload'}})
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
from os.path import isdir, exists
|
||||
|
||||
from tfw import TriggerlessEventHandler
|
||||
from tfw import EventHandlerBase
|
||||
from tfw.config.logs import logging
|
||||
from tfw.mixins import MonitorManagerMixin
|
||||
from .directory_monitor import DirectoryMonitor
|
||||
@@ -11,7 +11,7 @@ from .directory_monitor import DirectoryMonitor
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DirectoryMonitoringEventHandler(TriggerlessEventHandler, MonitorManagerMixin):
|
||||
class DirectoryMonitoringEventHandler(EventHandlerBase, MonitorManagerMixin):
|
||||
def __init__(self, key, directory):
|
||||
super().__init__(key)
|
||||
self._directory = directory
|
||||
|
||||
@@ -24,6 +24,17 @@ class CallbackEventHandler(PatternMatchingEventHandler, 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):
|
||||
CallbackMixin.__init__(self)
|
||||
ObserverMixin.__init__(self)
|
||||
@@ -61,6 +72,15 @@ class HistoryMonitor(CallbackMixin, ObserverMixin, ABC):
|
||||
|
||||
|
||||
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
|
||||
def command_pattern(self):
|
||||
return r'.+'
|
||||
@@ -70,6 +90,10 @@ class BashMonitor(HistoryMonitor):
|
||||
|
||||
|
||||
class GDBMonitor(HistoryMonitor):
|
||||
"""
|
||||
HistoryMonitor to monitor GDB sessions.
|
||||
For this to work "set trace-commands on" must be set in GDB.
|
||||
"""
|
||||
@property
|
||||
def command_pattern(self):
|
||||
return r'(?<=\n)\+(.+)\n'
|
||||
|
||||
+54
-4
@@ -6,7 +6,7 @@ from glob import glob
|
||||
from fnmatch import fnmatchcase
|
||||
from collections import Iterable
|
||||
|
||||
from tfw import TriggerlessEventHandler
|
||||
from tfw import EventHandlerBase
|
||||
from tfw.mixins import MonitorManagerMixin
|
||||
from tfw.config.logs import logging
|
||||
from .directory_monitor import DirectoryMonitor
|
||||
@@ -40,9 +40,9 @@ class FileManager: # pylint: disable=too-many-instance-attributes
|
||||
@workdir.setter
|
||||
def workdir(self, directory):
|
||||
if not exists(directory) or not isdir(directory):
|
||||
raise EnvironmentError('"{}" is not a directory!'.format(directory))
|
||||
raise EnvironmentError(f'"{directory}" is not a directory!')
|
||||
if not self._is_in_whitelisted_dir(directory):
|
||||
raise EnvironmentError('Directory "{}" is not in whitelist!'.format(directory))
|
||||
raise EnvironmentError(f'Directory "{directory}" is not in whitelist!')
|
||||
self._workdir = directory
|
||||
|
||||
@property
|
||||
@@ -91,9 +91,25 @@ class FileManager: # pylint: disable=too-many-instance-attributes
|
||||
return relpath(self._filepath(filename), start=self._workdir)
|
||||
|
||||
|
||||
class WebideEventHandler(TriggerlessEventHandler, MonitorManagerMixin):
|
||||
class IdeEventHandler(EventHandlerBase, MonitorManagerMixin):
|
||||
# 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):
|
||||
"""
|
||||
: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)
|
||||
self.filemanager = FileManager(allowed_directories=allowed_directories, working_directory=directory,
|
||||
selected_file=selected_file, exclude=exclude)
|
||||
@@ -106,6 +122,11 @@ class WebideEventHandler(TriggerlessEventHandler, MonitorManagerMixin):
|
||||
'exclude': self.exclude}
|
||||
|
||||
def read(self, data):
|
||||
"""
|
||||
Read the currently selected file.
|
||||
|
||||
:return: message with the contents of the file in data['content']
|
||||
"""
|
||||
try:
|
||||
data['content'] = self.filemanager.file_contents
|
||||
except PermissionError:
|
||||
@@ -117,6 +138,12 @@ class WebideEventHandler(TriggerlessEventHandler, MonitorManagerMixin):
|
||||
return data
|
||||
|
||||
def write(self, data):
|
||||
"""
|
||||
Overwrites a file with the desired string.
|
||||
|
||||
:param data: TFW message data containing keys:
|
||||
|-string: containing the desired file contents
|
||||
"""
|
||||
self.monitor.ignore = self.monitor.ignore + 1
|
||||
try:
|
||||
self.filemanager.file_contents = data['content']
|
||||
@@ -126,6 +153,12 @@ class WebideEventHandler(TriggerlessEventHandler, MonitorManagerMixin):
|
||||
return data
|
||||
|
||||
def select(self, data):
|
||||
"""
|
||||
Selects a file from the current directory.
|
||||
|
||||
:param data: TFW message data containing keys:
|
||||
|-filename: name of file to select relative to the current directory
|
||||
"""
|
||||
try:
|
||||
self.filemanager.filename = data['filename']
|
||||
except EnvironmentError:
|
||||
@@ -133,6 +166,14 @@ class WebideEventHandler(TriggerlessEventHandler, MonitorManagerMixin):
|
||||
return data
|
||||
|
||||
def select_dir(self, data):
|
||||
"""
|
||||
Select a new working directory to display files from.
|
||||
|
||||
:param data: TFW message data containing keys:
|
||||
|-directory: absolute path of diretory to select.
|
||||
must be a path whitelisted in
|
||||
self.allowed_directories
|
||||
"""
|
||||
try:
|
||||
self.filemanager.workdir = data['directory']
|
||||
self.reload_monitor()
|
||||
@@ -146,6 +187,12 @@ class WebideEventHandler(TriggerlessEventHandler, MonitorManagerMixin):
|
||||
return data
|
||||
|
||||
def exclude(self, data):
|
||||
"""
|
||||
Overwrite list of excluded files
|
||||
|
||||
:param data: TFW message data containing keys:
|
||||
|-exclude: list of filename patterns to be excluded, e.g.: ["*.pyc", "*.o"]
|
||||
"""
|
||||
try:
|
||||
self.filemanager.exclude = list(data['exclude'])
|
||||
except TypeError:
|
||||
@@ -153,6 +200,9 @@ class WebideEventHandler(TriggerlessEventHandler, MonitorManagerMixin):
|
||||
return data
|
||||
|
||||
def attach_fileinfo(self, data):
|
||||
"""
|
||||
Basic information included in every response to the frontend.
|
||||
"""
|
||||
data['filename'] = self.filemanager.filename
|
||||
data['files'] = self.filemanager.files
|
||||
data['directory'] = self.filemanager.workdir
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
from xmlrpc.client import Fault as SupervisorFault
|
||||
|
||||
from tfw import TriggerlessEventHandler
|
||||
from tfw import EventHandlerBase
|
||||
from tfw.mixins import SupervisorMixin
|
||||
from tfw.config.logs import logging
|
||||
from .directory_monitor import with_monitor_paused
|
||||
@@ -22,7 +22,19 @@ class ProcessManager(SupervisorMixin):
|
||||
return self.commands[command](process_name)
|
||||
|
||||
|
||||
class ProcessManagingEventHandler(TriggerlessEventHandler):
|
||||
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):
|
||||
super().__init__(key)
|
||||
self.key = key
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
|
||||
# All Rights Reserved. See LICENSE file for details.
|
||||
|
||||
from tfw import TriggerlessEventHandler
|
||||
from tfw import EventHandlerBase
|
||||
from tfw.config import TFWENV
|
||||
from tfw.config.logs import logging
|
||||
from tao.config import TAOENV
|
||||
@@ -10,8 +10,21 @@ from .terminado_mini_server import TerminadoMiniServer
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TerminadoEventHandler(TriggerlessEventHandler):
|
||||
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):
|
||||
"""
|
||||
:param key: key this EventHandler listens to
|
||||
:param monitor: tfw.components.HistoryMonitor instance to read command history from
|
||||
"""
|
||||
super().__init__(key)
|
||||
self.working_directory = TFWENV.TERMINADO_DIR
|
||||
self._historymonitor = monitor
|
||||
@@ -37,9 +50,23 @@ class TerminadoEventHandler(TriggerlessEventHandler):
|
||||
LOG.error('IGNORING MESSAGE: Invalid message received: %s', message)
|
||||
|
||||
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: TFW message data containing keys:
|
||||
|-shellcmd: command to be written to the pty
|
||||
"""
|
||||
self.terminado_server.pty.write(data['shellcmd'])
|
||||
|
||||
def read(self, data):
|
||||
"""
|
||||
Reads the history of commands executed.
|
||||
|
||||
:param data: TFW message data containing keys:
|
||||
|-count: the number of history elements to return
|
||||
:return: message with list of commands in data['history']
|
||||
"""
|
||||
data['count'] = int(data.get('count', 1))
|
||||
if self.historymonitor:
|
||||
data['history'] = self.historymonitor.history[-data['count']:]
|
||||
|
||||
@@ -10,6 +10,24 @@ LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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):
|
||||
self._command_method_regex = r'^command_(.+)$'
|
||||
self.command_implemetations = self._build_command_to_implementation_dict()
|
||||
@@ -22,7 +40,7 @@ class TerminalCommands(ABC):
|
||||
|
||||
def _setup_bashrc_aliases(self, bashrc):
|
||||
with open(bashrc, 'a') as ofile:
|
||||
alias_template = 'alias {0}="{0} > /dev/null 2>&1"\n'
|
||||
alias_template = 'type {0} &> /dev/null || alias {0}="{0} &> /dev/null"\n'
|
||||
for command in self.command_implemetations.keys():
|
||||
ofile.write(alias_template.format(command))
|
||||
|
||||
|
||||
@@ -8,75 +8,97 @@ from tfw.networking.event_handlers import ServerConnector
|
||||
|
||||
|
||||
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):
|
||||
self.server_connector = ServerConnector()
|
||||
self.key = key
|
||||
self.subscriptions = set()
|
||||
self.subscribe(self.key)
|
||||
self.subscribe('reset')
|
||||
self.subscribe(self.key, 'reset')
|
||||
self.server_connector.register_callback(self.event_handler_callback)
|
||||
|
||||
def event_handler_callback(self, msg_parts):
|
||||
"""
|
||||
Callback that is invoked when receiving a message.
|
||||
Dispatches messages to handler methods and sends
|
||||
a response back in case the handler returned something.
|
||||
This is subscribed in __init__().
|
||||
"""
|
||||
message = deserialize_tfw_msg(*msg_parts)
|
||||
response = self.dispatch_handling(message)
|
||||
response['key'] = message['key']
|
||||
if response is None:
|
||||
return
|
||||
self.server_connector.send(response)
|
||||
if response:
|
||||
response['key'] = message['key']
|
||||
self.server_connector.send(response)
|
||||
|
||||
@abstractmethod
|
||||
def dispatch_handling(self, message):
|
||||
raise NotImplementedError
|
||||
|
||||
def _dispatch_handling(self, message):
|
||||
# pylint: disable=no-else-return
|
||||
"""
|
||||
Used to dispatch messages to their specific handlers.
|
||||
"""
|
||||
if message['key'] != 'reset':
|
||||
return self.handle_event(message)
|
||||
else:
|
||||
return self.handle_reset(message)
|
||||
return self.handle_reset(message)
|
||||
|
||||
@abstractmethod
|
||||
def handle_event(self, message):
|
||||
"""
|
||||
Abstract method that implements the handling of messages.
|
||||
|
||||
:param message: the message received
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def handle_reset(self, message):
|
||||
# pylint: disable=unused-argument,no-self-use
|
||||
"""
|
||||
Usually 'reset' events receive some sort of special treatment.
|
||||
|
||||
:param message: the message received
|
||||
"""
|
||||
return None
|
||||
|
||||
def cleanup(self):
|
||||
pass
|
||||
def subscribe(self, *keys):
|
||||
"""
|
||||
Subscribe this EventHandler to receive events for given keys.
|
||||
Note that you can subscribe to the same key several times in which
|
||||
case you will need to unsubscribe multiple times in order to stop
|
||||
receiving events.
|
||||
|
||||
def subscribe(self, key):
|
||||
if key not in self.subscriptions:
|
||||
self.subscriptions.add(key)
|
||||
:param keys: list of keys to subscribe to
|
||||
"""
|
||||
for key in keys:
|
||||
self.server_connector.subscribe(key)
|
||||
|
||||
def unsubscribe(self, key):
|
||||
try:
|
||||
self.subscriptions.remove(key)
|
||||
def unsubscribe(self, *keys):
|
||||
"""
|
||||
Unsubscribe this eventhandler from the given keys.
|
||||
|
||||
:param keys: list of keys to unsubscribe from
|
||||
"""
|
||||
for key in keys:
|
||||
self.server_connector.unsubscribe(key)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def unsubscribe_all(self):
|
||||
for sub in self.subscriptions:
|
||||
self.server_connector.unsubscribe(key=sub)
|
||||
self.subscriptions.clear()
|
||||
|
||||
|
||||
class TriggerlessEventHandler(EventHandlerBase, ABC):
|
||||
# pylint: disable=abstract-method
|
||||
def dispatch_handling(self, message):
|
||||
return self._dispatch_handling(message)
|
||||
def cleanup(self):
|
||||
"""
|
||||
Perform cleanup actions such as releasing database
|
||||
connections and stuff like that.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class TriggeredEventHandler(EventHandlerBase, ABC):
|
||||
# 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):
|
||||
super().__init__(key)
|
||||
self.trigger = trigger
|
||||
|
||||
def dispatch_handling(self, message):
|
||||
if message.get('trigger') == self.trigger:
|
||||
return self._dispatch_handling(message)
|
||||
return super().dispatch_handling(message)
|
||||
return None
|
||||
|
||||
@@ -9,6 +9,13 @@ from tfw.mixins import 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 = [], []
|
||||
|
||||
def __init__(self, initial: str = None, accepted_states: List[str] = None):
|
||||
|
||||
@@ -5,8 +5,14 @@ from .fsm_base import 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):
|
||||
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': f'step_{int(index)+1}', 'source': index, 'dest': str(int(index)+1)}
|
||||
for index in self.states[:-1]]
|
||||
super(LinearFSM, self).__init__()
|
||||
|
||||
@@ -9,6 +9,12 @@ class CallbackMixin:
|
||||
self._callbacks = []
|
||||
|
||||
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)
|
||||
self._callbacks.append(fun)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ class ControllerConnector(ZMQConnectorBase):
|
||||
def __init__(self, zmq_context=None):
|
||||
super(ControllerConnector, self).__init__(zmq_context)
|
||||
self._zmq_rep_socket = self._zmq_context.socket(zmq.REP)
|
||||
self._zmq_rep_socket.connect('tcp://localhost:{}'.format(TFWENV.CONTROLLER_PORT))
|
||||
self._zmq_rep_socket.connect(f'tcp://localhost:{TFWENV.CONTROLLER_PORT}')
|
||||
self._zmq_rep_stream = ZMQStream(self._zmq_rep_socket)
|
||||
|
||||
self.register_callback = self._zmq_rep_stream.on_recv_stream
|
||||
|
||||
@@ -15,7 +15,7 @@ class ServerDownlinkConnector(ZMQConnectorBase):
|
||||
def __init__(self, zmq_context=None):
|
||||
super(ServerDownlinkConnector, self).__init__(zmq_context)
|
||||
self._zmq_sub_socket = self._zmq_context.socket(zmq.SUB)
|
||||
self._zmq_sub_socket.connect('tcp://localhost:{}'.format(TFWENV.PUBLISHER_PORT))
|
||||
self._zmq_sub_socket.connect(f'tcp://localhost:{TFWENV.PUBLISHER_PORT}')
|
||||
self._zmq_sub_stream = ZMQStream(self._zmq_sub_socket)
|
||||
|
||||
self.subscribe = partial(self._zmq_sub_socket.setsockopt_string, zmq.SUBSCRIBE)
|
||||
@@ -24,18 +24,30 @@ class ServerDownlinkConnector(ZMQConnectorBase):
|
||||
|
||||
|
||||
class ServerUplinkConnector(ZMQConnectorBase):
|
||||
"""
|
||||
Class capable of sending messages to the TFW server and event handlers.
|
||||
"""
|
||||
def __init__(self, zmq_context=None):
|
||||
super(ServerUplinkConnector, self).__init__(zmq_context)
|
||||
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(f'tcp://localhost:{TFWENV.RECEIVER_PORT}')
|
||||
|
||||
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')}
|
||||
message['key'] = 'mirror'
|
||||
message['data'] = nested_message
|
||||
self.send(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))
|
||||
|
||||
|
||||
|
||||
@@ -7,11 +7,20 @@ from tfw.networking.event_handlers import ServerUplinkConnector
|
||||
|
||||
|
||||
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):
|
||||
self.server_connector = ServerUplinkConnector()
|
||||
self.key = custom_key or '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 = {
|
||||
'originator': originator,
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
|
||||
@@ -16,7 +16,7 @@ class EventHandlerDownlinkConnector(ZMQConnectorBase):
|
||||
super(EventHandlerDownlinkConnector, self).__init__(zmq_context)
|
||||
self._zmq_pull_socket = self._zmq_context.socket(zmq.PULL)
|
||||
self._zmq_pull_stream = ZMQStream(self._zmq_pull_socket)
|
||||
address = 'tcp://*:{}'.format(TFWENV.RECEIVER_PORT)
|
||||
address = f'tcp://*:{TFWENV.RECEIVER_PORT}'
|
||||
self._zmq_pull_socket.bind(address)
|
||||
LOG.debug('Pull socket bound to %s', address)
|
||||
|
||||
@@ -25,7 +25,7 @@ class EventHandlerUplinkConnector(ZMQConnectorBase):
|
||||
def __init__(self, zmq_context=None):
|
||||
super(EventHandlerUplinkConnector, self).__init__(zmq_context)
|
||||
self._zmq_pub_socket = self._zmq_context.socket(zmq.PUB)
|
||||
address = 'tcp://*:{}'.format(TFWENV.PUBLISHER_PORT)
|
||||
address = f'tcp://*:{TFWENV.PUBLISHER_PORT}'
|
||||
self._zmq_pub_socket.bind(address)
|
||||
LOG.debug('Pub socket bound to %s', address)
|
||||
|
||||
|
||||
@@ -15,7 +15,15 @@ LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
:param fsm_type: the type of FSM you want TFW to use
|
||||
"""
|
||||
self._fsm = fsm_type()
|
||||
self._fsm_updater = FSMUpdater(self._fsm)
|
||||
self._fsm_manager = FSMManager(self._fsm)
|
||||
|
||||
Reference in New Issue
Block a user