Compare commits

..
Author SHA1 Message Date
Kristóf Tóth 137d1dbce2 Add .drone.yml 2018-04-16 17:10:28 +02:00
Kristóf Tóth 3b1f42858d Make default BUILD_CONTEXT == 'solvable' 2018-04-16 15:13:12 +02:00
Bokros Bálint 18124bf8c3 Merge pull request #31 from avatao-content/networking_refactor
Networking refactor
2018-04-16 13:04:56 +02:00
Kristóf Tóth d8988ea097 Rename make_eventhandler_response in TFWServer to increase readibility 2018-04-16 12:57:08 +02:00
Kristóf Tóth 986a67f5db Simplify bool returning function 2018-04-16 12:51:44 +02:00
Kristóf Tóth 39243811dc Remove unnecessary variable 2018-04-16 12:51:01 +02:00
Kristóf Tóth 22cdbaac2c Fix typo 2018-04-16 12:50:08 +02:00
Kristóf Tóth 743fab366b Extract variable to silence retarded linter 2018-04-14 23:15:44 +02:00
Kristóf Tóth efb8af4cd1 Remove double trailing newline 2018-04-14 23:09:02 +02:00
Kristóf Tóth a65d3127ba Refactor DirectoryMonitor managing code to a mixin 2018-04-14 23:00:43 +02:00
Kristóf Tóth 1c29b700c2 Move mixins and decorators to tfw module from tfw.components 2018-04-14 21:15:30 +02:00
Kristóf Tóth e2bb126e6f Make simple not if condition more pythonic 2018-04-14 21:07:33 +02:00
Kristóf Tóth 586c435cc2 Refactor watchdog Observer handling to a mixin 2018-04-14 20:36:43 +02:00
Kristóf Tóth b082279916 Implement message validation in TFW proxying 2018-04-14 10:45:23 +02:00
Kristóf Tóth ecec1cde59 Root out remaining ambiguous key arguments in EventHandlers 2018-04-14 10:45:23 +02:00
Kristóf Tóth 0cd11f9d64 Remove ambiguous key parameter from proxy module 2018-04-14 10:45:23 +02:00
Kristóf Tóth fe7bd4a678 Remove ambiguous key parameter from EventHanderConnector 2018-04-14 10:45:23 +02:00
Kristóf Tóth 3c12b5503e Mark old serialization methods as module internal 2018-04-14 10:45:23 +02:00
Kristóf Tóth 499b575adf Add module docstring to serialization.py 2018-04-14 10:45:23 +02:00
Kristóf Tóth 9eb6717972 Refactor 'mirror' message enveloping to use new serialization 2018-04-14 10:45:23 +02:00
Kristóf Tóth aabaadc4c4 Refactor proxying to use new deserialization 2018-04-14 10:45:23 +02:00
Kristóf Tóth cd36b6c22c Refactor ServerUplinkConnector to use new serializers 2018-04-14 10:45:23 +02:00
Kristóf Tóth 67a23166c6 Introduce [de]serialize_tfw_msg() methods to abstract away enveloping 2018-04-14 10:45:23 +02:00
Kristóf Tóth 18e9ee5f52 Rename make_response in networking to avoid confusion 2018-04-14 10:45:23 +02:00
Kristóf Tóth d04479157d Refactor ServerUplinkConnector.send() signature 2018-04-14 10:45:23 +02:00
Kristóf Tóth b8cdb30318 Add missing legal boilerplate to terminal_commands.py 2018-04-14 10:44:54 +02:00
Kristóf Tóth 512a965328 Update old logging code to use TFW global logging 2018-04-13 16:46:11 +02:00
Kristóf Tóth c33a2f8103 Add .pylintrc 2018-04-13 16:28:30 +02:00
Kristóf Tóth 9f4364ec10 Remove obsolete .pyenvrc from "compiling python3 cause ubuntu" times 2018-04-13 16:26:55 +02:00
Kristóf Tóth 7877e977ab Extract magic dict building to method in TerminalCommands 2018-04-13 10:01:45 +02:00
Kristóf Tóth 5d8d541394 Remove dead code from EventHandlerBase 2018-04-12 14:30:27 +02:00
Bokros Bálint 7e991426e7 Merge pull request #30 from avatao-content/terminalcommands
Terminalcommands
2018-04-12 11:20:04 +02:00
Kristóf Tóth 16c1fe7b50 Implement appending command aliases to bashrc to avoid command not found 2018-04-12 11:07:56 +02:00
Kristóf Tóth 8caf879ffd Integrate terminal command magic class from test repo to TFW 2018-04-12 10:56:15 +02:00
Bokros Bálint 5f48de037d Merge pull request #29 from avatao-content/fsmapi
Fsmapi
2018-04-11 15:33:46 +02:00
Kristóf Tóth 73fa65c6e8 Implement triggering of FSM from ServerUplinkConnector 2018-04-11 15:22:54 +02:00
Kristóf Tóth 4842574cf3 Refactor LinearFSM triggers to have different names 2018-04-11 15:20:14 +02:00
Kristóf Tóth 6113149c58 Rework serialization module to work regardless of message format 2018-04-10 17:37:58 +02:00
Kristóf Tóth 87fa86d314 Refactor FSM triggering in TFWServer 2018-04-10 17:37:58 +02:00
Bokros Bálint ba66188d80 Merge pull request #28 from avatao-content/linearfsm
Implement first version of LinearFSM
2018-04-10 17:34:46 +02:00
Kristóf Tóth dbe6fe0f92 Add license boilerplate to linear_fsm.py 2018-04-10 12:48:53 +02:00
Kristóf Tóth 8a255053aa Update architectural diagram 2018-04-10 08:34:05 +02:00
Kristóf Tóth 29da0a7ca6 Implement first version of LinearFSM 2018-04-07 15:21:13 +02:00
Kristóf Tóth 1922050e87 Make semantics of whitelist and blacklist more explicit in webide 2018-04-07 15:00:31 +02:00
Kristóf Tóth 8bd6005b5d Refactor blacklisting into a method in webide 2018-04-07 14:35:42 +02:00
Kristóf Tóth 4a216d8875 Rename SourceCodeEventHandler to WebideEventHandler 2018-04-07 14:18:15 +02:00
Bokros Bálint 8043338e58 Merge pull request #27 from avatao-content/eh-stateupdates
Implement FSMUpdates to EventHandlers
2018-04-06 17:33:45 +02:00
Kristóf Tóth de8c2ae6e2 Implement FSMUpdates to EventHandlers 2018-04-06 17:14:37 +02:00
Kristóf Tóth b785b88af6 Pass processed message to FSM trigger predicates 2018-04-06 17:14:06 +02:00
Kristóf Tóth 96c322c17c Silence unjust critique by pylint 2018-04-06 16:52:23 +02:00
33 changed files with 297 additions and 155 deletions
+13
View File
@@ -0,0 +1,13 @@
pipeline:
build:
image: eu.gcr.io/avatao-public/docker:el7
volumes:
- /etc/docker:/etc/docker:ro
- /root/.docker:/root/.docker:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
commands:
- docker build --pull -t eu.gcr.io/avatao-challengestore/tutorial-framework:${DRONE_TAG} .
- docker push eu.gcr.io/avatao-challengestore/tutorial-framework:${DRONE_TAG}
when:
event: 'tag'
branch: refs/tags/egyptianmau-20*
-4
View File
@@ -1,4 +0,0 @@
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
+5
View File
@@ -0,0 +1,5 @@
[TYPECHECK]
ignored-modules = zmq
max-line-length = 150
disable = missing-docstring, too-few-public-methods
+1 -1
View File
@@ -57,7 +57,7 @@ RUN for dir in "${TFW_LIB_DIR}" "/etc/nginx" "/etc/supervisor"; do \
chown -R root:root "$dir" && chmod -R 700 "$dir"; \ chown -R root:root "$dir" && chmod -R 700 "$dir"; \
done done
ONBUILD ARG BUILD_CONTEXT="." ONBUILD ARG BUILD_CONTEXT="solvable"
ONBUILD ARG NOFRONTEND="" ONBUILD ARG NOFRONTEND=""
ONBUILD COPY ${BUILD_CONTEXT}/nginx/components/ ${TFW_NGINX_COMPONENTS} ONBUILD COPY ${BUILD_CONTEXT}/nginx/components/ ${TFW_NGINX_COMPONENTS}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 38 KiB

-1
View File
@@ -1,3 +1,2 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft. # Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details. # All Rights Reserved. See LICENSE file for details.
+1
View File
@@ -3,3 +3,4 @@
from .event_handler_base import TriggerlessEventHandler, TriggeredEventHandler from .event_handler_base import TriggerlessEventHandler, TriggeredEventHandler
from .fsm_base import FSMBase from .fsm_base import FSMBase
from .linear_fsm import LinearFSM
+2 -1
View File
@@ -4,5 +4,6 @@
from .directory_monitoring_event_handler import DirectoryMonitoringEventHandler from .directory_monitoring_event_handler import DirectoryMonitoringEventHandler
from .process_managing_event_handler import ProcessManagingEventHandler from .process_managing_event_handler import ProcessManagingEventHandler
from .terminado_event_handler import TerminadoEventHandler from .terminado_event_handler import TerminadoEventHandler
from .source_code_event_handler import SourceCodeEventHandler from .webide_event_handler import WebideEventHandler
from .history_monitor import HistoryMonitor, BashMonitor, GDBMonitor from .history_monitor import HistoryMonitor, BashMonitor, GDBMonitor
from .terminal_commands import TerminalCommands
+6 -13
View File
@@ -3,31 +3,24 @@
from functools import wraps from functools import wraps
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler as FileSystemWatchdogEventHandler from watchdog.events import FileSystemEventHandler as FileSystemWatchdogEventHandler
from tfw.networking.event_handlers import ServerUplinkConnector from tfw.networking.event_handlers import ServerUplinkConnector
from tfw.components.decorators import RateLimiter from tfw.decorators import RateLimiter
from tfw.mixins import ObserverMixin
from tfw.config.logs import logging from tfw.config.logs import logging
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class DirectoryMonitor: class DirectoryMonitor(ObserverMixin):
def __init__(self, directory): def __init__(self, directory):
self.observer = Observer() ObserverMixin.__init__(self)
self.eventhandler = WebideReloadWatchdogEventHandler() self.eventhandler = WebideReloadWatchdogEventHandler()
self.observer.schedule(self.eventhandler, directory, recursive=True) self.observer.schedule(self.eventhandler, directory, recursive=True)
self.pause, self.resume = self.eventhandler.pause, self.eventhandler.resume self.pause, self.resume = self.eventhandler.pause, self.eventhandler.resume
def watch(self):
self.observer.start()
def stop(self):
self.observer.stop()
self.observer.join()
@property @property
def ignore(self): def ignore(self):
return self.eventhandler.ignore return self.eventhandler.ignore
@@ -70,8 +63,8 @@ class WebideReloadWatchdogEventHandler(FileSystemWatchdogEventHandler):
self.ignore = self.ignore - 1 self.ignore = self.ignore - 1
return return
LOG.debug(event) LOG.debug(event)
key = 'webide' self.uplink.send({'key': 'webide',
self.uplink.send(key, {'data': {'command': 'reload'}}) 'data': {'command': 'reload'}})
def with_monitor_paused(fun): def with_monitor_paused(fun):
@@ -5,17 +5,18 @@ from os.path import isdir, exists
from tfw import TriggerlessEventHandler from tfw import TriggerlessEventHandler
from tfw.config.logs import logging from tfw.config.logs import logging
from tfw.mixins import MonitorManagerMixin
from .directory_monitor import DirectoryMonitor from .directory_monitor import DirectoryMonitor
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class DirectoryMonitoringEventHandler(TriggerlessEventHandler): class DirectoryMonitoringEventHandler(TriggerlessEventHandler, MonitorManagerMixin):
def __init__(self, key, directory): def __init__(self, key, directory):
super().__init__(key) super().__init__(key)
self._directory = directory self._directory = directory
self._monitor = None MonitorManagerMixin.__init__(self, DirectoryMonitor, self._directory)
self.reload_monitor()
self.commands = {'pause': self.pause, self.commands = {'pause': self.pause,
'resume': self.resume, 'resume': self.resume,
'ignore': self.ignore, 'ignore': self.ignore,
@@ -31,20 +32,7 @@ class DirectoryMonitoringEventHandler(TriggerlessEventHandler):
raise EnvironmentError('No such directory!') raise EnvironmentError('No such directory!')
self._directory = directory self._directory = directory
@property def handle_event(self, message):
def monitor(self):
return self._monitor
def reload_monitor(self):
if self._monitor:
try:
self._monitor.stop()
except KeyError:
logging.debug('Working directory was removed ignoring...')
self._monitor = DirectoryMonitor(self._directory)
self._monitor.watch() # This runs on a separate thread
def handle_event(self, key, message):
try: try:
message['data'] = self.commands[message['data']['command']](message['data']) message['data'] = self.commands[message['data']['command']](message['data'])
return message return message
+4 -12
View File
@@ -6,11 +6,10 @@ from re import findall
from re import compile as compileregex from re import compile as compileregex
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from watchdog.observers import Observer
from watchdog.events import PatternMatchingEventHandler from watchdog.events import PatternMatchingEventHandler
from tfw.components.mixins import CallbackMixin from tfw.mixins import CallbackMixin, ObserverMixin
from tfw.components.decorators import RateLimiter from tfw.decorators import RateLimiter
class CallbackEventHandler(PatternMatchingEventHandler, ABC): class CallbackEventHandler(PatternMatchingEventHandler, ABC):
@@ -24,13 +23,13 @@ class CallbackEventHandler(PatternMatchingEventHandler, ABC):
callback() callback()
class HistoryMonitor(CallbackMixin, ABC): class HistoryMonitor(CallbackMixin, ObserverMixin, ABC):
def __init__(self, histfile): def __init__(self, histfile):
CallbackMixin.__init__(self) CallbackMixin.__init__(self)
ObserverMixin.__init__(self)
self.histfile = histfile self.histfile = histfile
self._history = [] self._history = []
self._last_length = len(self._history) self._last_length = len(self._history)
self.observer = Observer()
self.observer.schedule(CallbackEventHandler([self.histfile], self.observer.schedule(CallbackEventHandler([self.histfile],
self._fetch_history, self._fetch_history,
self._invoke_callbacks), self._invoke_callbacks),
@@ -60,13 +59,6 @@ class HistoryMonitor(CallbackMixin, ABC):
if self._last_length < len(self._history): if self._last_length < len(self._history):
self._execute_callbacks(self.history) self._execute_callbacks(self.history)
def watch(self):
self.observer.start()
def stop(self):
self.observer.stop()
self.observer.join()
class BashMonitor(HistoryMonitor): class BashMonitor(HistoryMonitor):
@property @property
@@ -4,7 +4,7 @@
from xmlrpc.client import Fault as SupervisorFault from xmlrpc.client import Fault as SupervisorFault
from tfw import TriggerlessEventHandler from tfw import TriggerlessEventHandler
from tfw.components.mixins import SupervisorMixin from tfw.mixins import SupervisorMixin
from tfw.config.logs import logging from tfw.config.logs import logging
from .directory_monitor import with_monitor_paused from .directory_monitor import with_monitor_paused
@@ -30,7 +30,7 @@ class ProcessManagingEventHandler(TriggerlessEventHandler):
self.processmanager = ProcessManager() self.processmanager = ProcessManager()
@with_monitor_paused @with_monitor_paused
def handle_event(self, key, message): def handle_event(self, message):
try: try:
data = message['data'] data = message['data']
self.processmanager(data['command'], data['process_name']) self.processmanager(data['command'], data['process_name'])
@@ -27,10 +27,11 @@ class TerminadoEventHandler(TriggerlessEventHandler):
def historymonitor(self): def historymonitor(self):
return self._historymonitor return self._historymonitor
def handle_event(self, key, message): def handle_event(self, message):
LOG.debug('TerminadoEventHandler received event: %s', message) LOG.debug('TerminadoEventHandler received event: %s', message)
try: try:
message['data'] = self.commands[message['data']['command']](message['data']) data = message['data']
message['data'] = self.commands[data['command']](data)
return message return message
except KeyError: except KeyError:
LOG.error('IGNORING MESSAGE: Invalid message received: %s', message) LOG.error('IGNORING MESSAGE: Invalid message received: %s', message)
+48
View File
@@ -0,0 +1,48 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from abc import ABC
from re import match
from tfw.config.logs import logging
LOG = logging.getLogger(__name__)
class TerminalCommands(ABC):
def __init__(self, bashrc=None):
self._command_method_regex = r'^command_(.+)$'
self.command_implemetations = self._build_command_to_implementation_dict()
if bashrc is not None:
self._setup_bashrc_aliases(bashrc)
def _build_command_to_implementation_dict(self):
return {self._parse_command_name(fun): getattr(self, fun) for fun in dir(self)
if callable(getattr(self, fun)) and self._is_command_implementation(fun)}
def _setup_bashrc_aliases(self, bashrc):
with open(bashrc, 'a') as ofile:
alias_template = 'alias {0}="{0} > /dev/null 2>&1"\n'
for command in self.command_implemetations.keys():
ofile.write(alias_template.format(command))
def _is_command_implementation(self, method_name):
return bool(self._match_command_regex(method_name))
def _parse_command_name(self, method_name):
try:
return self._match_command_regex(method_name).groups()[0]
except AttributeError:
return ''
def _match_command_regex(self, string):
return match(self._command_method_regex, string)
def callback(self, history):
parts = history[-1].split()
command = parts[0]
if command in self.command_implemetations.keys():
try:
self.command_implemetations[command](*parts[1:])
except IndexError:
LOG.debug('Command "%s" failed!', command)
@@ -7,6 +7,7 @@ from fnmatch import fnmatchcase
from collections import Iterable from collections import Iterable
from tfw import TriggerlessEventHandler from tfw import TriggerlessEventHandler
from tfw.mixins import MonitorManagerMixin
from tfw.config.logs import logging from tfw.config.logs import logging
from .directory_monitor import DirectoryMonitor from .directory_monitor import DirectoryMonitor
@@ -40,7 +41,7 @@ class FileManager: # pylint: disable=too-many-instance-attributes
def workdir(self, directory): def workdir(self, directory):
if not exists(directory) or not isdir(directory): if not exists(directory) or not isdir(directory):
raise EnvironmentError('"{}" is not a directory!'.format(directory)) raise EnvironmentError('"{}" is not a directory!'.format(directory))
if not self._is_whitelisted(directory): if not self._is_in_whitelisted_dir(directory):
raise EnvironmentError('Directory "{}" is not in whitelist!'.format(directory)) raise EnvironmentError('Directory "{}" is not in whitelist!'.format(directory))
self._workdir = directory self._workdir = directory
@@ -58,16 +59,14 @@ class FileManager: # pylint: disable=too-many-instance-attributes
@filename.setter @filename.setter
def filename(self, filename): def filename(self, filename):
if not filename in self.files: if filename not in self.files:
raise EnvironmentError('No such file in workdir!') raise EnvironmentError('No such file in workdir!')
self._filename = filename self._filename = filename
@property @property
def files(self): def files(self):
return [self._relpath(file) for file in glob(join(self._workdir, '**/*'), recursive=True) return [self._relpath(file) for file in glob(join(self._workdir, '**/*'), recursive=True)
if isfile(file) if isfile(file) and self._is_in_whitelisted_dir(file) and not self._is_blacklisted(file)]
and self._is_whitelisted(file)
and not any(fnmatchcase(file, blacklisted) for blacklisted in self.exclude)]
@property @property
def file_contents(self): def file_contents(self):
@@ -79,8 +78,11 @@ class FileManager: # pylint: disable=too-many-instance-attributes
with open(self._filepath(self.filename), 'w', errors='surrogateescape') as ofile: with open(self._filepath(self.filename), 'w', errors='surrogateescape') as ofile:
ofile.write(value) ofile.write(value)
def _is_whitelisted(self, file): def _is_in_whitelisted_dir(self, path):
return any(realpath(file).startswith(allowed_dir) for allowed_dir in self.allowed_directories) return any(realpath(path).startswith(allowed_dir) for allowed_dir in self.allowed_directories)
def _is_blacklisted(self, file):
return any(fnmatchcase(file, blacklisted) for blacklisted in self.exclude)
def _filepath(self, filename): def _filepath(self, filename):
return join(self._workdir, filename) return join(self._workdir, filename)
@@ -89,12 +91,13 @@ class FileManager: # pylint: disable=too-many-instance-attributes
return relpath(self._filepath(filename), start=self._workdir) return relpath(self._filepath(filename), start=self._workdir)
class SourceCodeEventHandler(TriggerlessEventHandler): class WebideEventHandler(TriggerlessEventHandler, MonitorManagerMixin):
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
def __init__(self, key, directory, allowed_directories, selected_file=None, exclude=None): def __init__(self, key, directory, allowed_directories, selected_file=None, exclude=None):
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)
MonitorManagerMixin.__init__(self, DirectoryMonitor, self.filemanager.workdir)
self.commands = {'read': self.read, self.commands = {'read': self.read,
'write': self.write, 'write': self.write,
@@ -102,22 +105,6 @@ class SourceCodeEventHandler(TriggerlessEventHandler):
'selectdir': self.select_dir, 'selectdir': self.select_dir,
'exclude': self.exclude} 'exclude': self.exclude}
self._monitor = None
self.reload_monitor()
@property
def monitor(self):
return self._monitor
def reload_monitor(self):
if self._monitor:
try:
self._monitor.stop()
except KeyError:
logging.debug('Working directory was removed ignoring...')
self._monitor = DirectoryMonitor(self.filemanager.workdir)
self._monitor.watch() # This runs on a separate thread
def read(self, data): def read(self, data):
try: try:
data['content'] = self.filemanager.file_contents data['content'] = self.filemanager.file_contents
@@ -170,7 +157,7 @@ class SourceCodeEventHandler(TriggerlessEventHandler):
data['files'] = self.filemanager.files data['files'] = self.filemanager.files
data['directory'] = self.filemanager.workdir data['directory'] = self.filemanager.workdir
def handle_event(self, key, message): def handle_event(self, message):
try: try:
data = message['data'] data = message['data']
message['data'] = self.commands[data['command']](data) message['data'] = self.commands[data['command']](data)
+14 -20
View File
@@ -3,7 +3,7 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from tfw.networking import deserialize_all from tfw.networking import deserialize_tfw_msg
from tfw.networking.event_handlers import ServerConnector from tfw.networking.event_handlers import ServerConnector
@@ -17,25 +17,26 @@ class EventHandlerBase(ABC):
self.server_connector.register_callback(self.event_handler_callback) self.server_connector.register_callback(self.event_handler_callback)
def event_handler_callback(self, msg_parts): def event_handler_callback(self, msg_parts):
key, message = deserialize_all(*msg_parts) message = deserialize_tfw_msg(*msg_parts)
response = self.dispatch_handling(key, message) response = self.dispatch_handling(message)
response['key'] = message['key']
if response is None: if response is None:
return return
self.server_connector.send(key, response) self.server_connector.send(response)
@abstractmethod @abstractmethod
def dispatch_handling(self, key, message): def dispatch_handling(self, message):
raise NotImplementedError raise NotImplementedError
def _dispatch_handling(self, key, message): def _dispatch_handling(self, message):
# pylint: disable=no-else-return # pylint: disable=no-else-return
if key != 'reset': if message['key'] != 'reset':
return self.handle_event(key, message) return self.handle_event(message)
else: else:
return self.handle_reset(message) return self.handle_reset(message)
@abstractmethod @abstractmethod
def handle_event(self, key, message): def handle_event(self, message):
raise NotImplementedError raise NotImplementedError
def handle_reset(self, message): def handle_reset(self, message):
@@ -45,13 +46,6 @@ class EventHandlerBase(ABC):
def cleanup(self): def cleanup(self):
pass pass
def message_other(self, key, data):
message = {
'key': key,
'data': data
}
self.server_connector.send(key, message)
def subscribe(self, key): def subscribe(self, key):
if key not in self.subscriptions: if key not in self.subscriptions:
self.subscriptions.add(key) self.subscriptions.add(key)
@@ -72,8 +66,8 @@ class EventHandlerBase(ABC):
class TriggerlessEventHandler(EventHandlerBase, ABC): class TriggerlessEventHandler(EventHandlerBase, ABC):
# pylint: disable=abstract-method # pylint: disable=abstract-method
def dispatch_handling(self, key, message): def dispatch_handling(self, message):
return self._dispatch_handling(key, message) return self._dispatch_handling(message)
class TriggeredEventHandler(EventHandlerBase, ABC): class TriggeredEventHandler(EventHandlerBase, ABC):
@@ -82,7 +76,7 @@ class TriggeredEventHandler(EventHandlerBase, ABC):
super().__init__(key) super().__init__(key)
self.trigger = trigger self.trigger = trigger
def dispatch_handling(self, key, message): def dispatch_handling(self, message):
if message.get('trigger') == self.trigger: if message.get('trigger') == self.trigger:
return self._dispatch_handling(key, message) return self._dispatch_handling(message)
return None return None
+1 -1
View File
@@ -5,7 +5,7 @@ from typing import List
from transitions import Machine from transitions import Machine
from tfw.components.mixins import CallbackMixin from tfw.mixins import CallbackMixin
class FSMBase(CallbackMixin): class FSMBase(CallbackMixin):
+12
View File
@@ -0,0 +1,12 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from .fsm_base import FSMBase
class LinearFSM(FSMBase):
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)}
for index in self.states[:-1]]
super(LinearFSM, self).__init__()
@@ -3,3 +3,5 @@
from .supervisor_mixin import SupervisorMixin from .supervisor_mixin import SupervisorMixin
from .callback_mixin import CallbackMixin from .callback_mixin import CallbackMixin
from .observer_mixin import ObserverMixin
from .monitor_manager_mixin import MonitorManagerMixin
+27
View File
@@ -0,0 +1,27 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from tfw.config.logs import logging
LOG = logging.getLogger(__name__)
class MonitorManagerMixin:
def __init__(self, monitor_type, directory):
self._monitor_type = monitor_type
self._monitor = None
self._monitored_directory = directory
self.reload_monitor()
@property
def monitor(self):
return self._monitor
def reload_monitor(self):
if self._monitor:
try:
self._monitor.stop()
except KeyError:
LOG.debug('Working directory was removed ignoring...')
self._monitor = self._monitor_type(self._monitored_directory)
self._monitor.watch() # This runs on a separate thread
+16
View File
@@ -0,0 +1,16 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from watchdog.observers import Observer
class ObserverMixin:
def __init__(self):
self.observer = Observer()
def watch(self):
self.observer.start()
def stop(self):
self.observer.stop()
self.observer.join()
+2 -2
View File
@@ -1,9 +1,9 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft. # Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details. # All Rights Reserved. See LICENSE file for details.
from .serialization import decode_if_needed, encode_if_needed, serialize_all, deserialize_all from .serialization import serialize_tfw_msg, deserialize_tfw_msg, validate_message
from .zmq_connector_base import ZMQConnectorBase from .zmq_connector_base import ZMQConnectorBase
from .controller_connector import ControllerConnector # from .controller_connector import ControllerConnector # TODO: readd once controller stuff is resolved
from .message_sender import MessageSender from .message_sender import MessageSender
from .event_handlers.server_connector import ServerUplinkConnector as TFWServerConnector from .event_handlers.server_connector import ServerUplinkConnector as TFWServerConnector
from .server.tfw_server import TFWServer from .server.tfw_server import TFWServer
@@ -6,7 +6,7 @@ from functools import partial
import zmq import zmq
from zmq.eventloop.zmqstream import ZMQStream from zmq.eventloop.zmqstream import ZMQStream
from tfw.networking import serialize_all from tfw.networking import serialize_tfw_msg
from tfw.networking import ZMQConnectorBase from tfw.networking import ZMQConnectorBase
from tfw.config import TFWENV from tfw.config import TFWENV
@@ -29,13 +29,14 @@ class ServerUplinkConnector(ZMQConnectorBase):
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, key, message): def send_to_eventhandler(self, message):
message['data']['key'] = key nested_message = {'key': message['key'], 'data': message.pop('data')}
self.send('mirror', message) message['key'] = 'mirror'
message['data'] = nested_message
self.send(message)
def send(self, key, message): def send(self, message):
message['key'] = key self._zmq_push_socket.send_multipart(serialize_tfw_msg(message))
self._zmq_push_socket.send_multipart(serialize_all(key, message))
class ServerConnector(ServerUplinkConnector, ServerDownlinkConnector): class ServerConnector(ServerUplinkConnector, ServerDownlinkConnector):
+2 -2
View File
@@ -17,5 +17,5 @@ class MessageSender:
'timestamp': datetime.now().isoformat(), 'timestamp': datetime.now().isoformat(),
'message': message 'message': message
} }
response = {'data': data} self.server_connector.send({'key': self.key,
self.server_connector.send(self.key, response) 'data': data})
+55 -10
View File
@@ -1,24 +1,69 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft. # Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details. # All Rights Reserved. See LICENSE file for details.
"""
TFW JSON message format
message:
{
"key": string, # addressing
"data": {...}, # payload
"trigger": string # FSM trigger
}
ZeroMQ's sub-pub sockets use enveloped messages
(http://zguide.zeromq.org/page:all#Pub-Sub-Message-Envelopes)
and TFW also uses them internally. This means that on ZMQ sockets
we always send the messages key separately and then the actual
message (which contains the key as well) like so:
socket.send_multipart([message['key'], message])
The purpose of this module is abstracting away this low level behaviour.
"""
import json import json
def encode_if_needed(value): def validate_message(message):
return 'key' in message
def serialize_tfw_msg(message):
return _serialize_all(message['key'], message)
def deserialize_tfw_msg(*args):
return _deserialize_all(*args)[1]
def _serialize_all(*args):
return tuple(_serialize_single(arg) for arg in args)
def _deserialize_all(*args):
return tuple(_deserialize_single(arg) for arg in args)
def _serialize_single(data):
if not isinstance(data, str):
data = json.dumps(data)
return _encode_if_needed(data)
def _deserialize_single(data):
try:
return json.loads(data)
except ValueError:
return _decode_if_needed(data)
def _encode_if_needed(value):
if isinstance(value, str): if isinstance(value, str):
value = value.encode('utf-8') value = value.encode('utf-8')
return value return value
def decode_if_needed(value): def _decode_if_needed(value):
if isinstance(value, (bytes, bytearray)): if isinstance(value, (bytes, bytearray)):
value = value.decode('utf-8') value = value.decode('utf-8')
return value return value
def serialize_all(key, data):
return [encode_if_needed(frame) for frame in (key, json.dumps(data))]
def deserialize_all(key, data):
return decode_if_needed(key), json.loads(data)
+1 -1
View File
@@ -4,4 +4,4 @@
from .event_handler_connector import EventHandlerConnector, EventHandlerUplinkConnector, EventHandlerDownlinkConnector from .event_handler_connector import EventHandlerConnector, EventHandlerUplinkConnector, EventHandlerDownlinkConnector
from .tfw_server import TFWServer from .tfw_server import TFWServer
from .zmq_websocket_handler import ZMQWebSocketProxy from .zmq_websocket_handler import ZMQWebSocketProxy
from .controller_responder import ControllerResponder # from .controller_responder import ControllerResponder # TODO: readd once controller stuff is resolved
@@ -4,7 +4,7 @@
import zmq import zmq
from zmq.eventloop.zmqstream import ZMQStream from zmq.eventloop.zmqstream import ZMQStream
from tfw.networking import ZMQConnectorBase, serialize_all from tfw.networking import ZMQConnectorBase, serialize_tfw_msg
from tfw.config import TFWENV from tfw.config import TFWENV
from tfw.config.logs import logging from tfw.config.logs import logging
@@ -34,7 +34,5 @@ class EventHandlerConnector(EventHandlerDownlinkConnector, EventHandlerUplinkCon
def register_callback(self, callback): def register_callback(self, callback):
self._zmq_pull_stream.on_recv(callback) self._zmq_pull_stream.on_recv(callback)
def send_message(self, message: dict, key: str = None): def send_message(self, message: dict):
if not key: self._zmq_pub_socket.send_multipart(serialize_tfw_msg(message))
key = message.get('key', '')
self._zmq_pub_socket.send_multipart(serialize_all(key, message))
+21 -10
View File
@@ -23,8 +23,9 @@ class TFWServer:
self._event_handler_connector = EventHandlerConnector() self._event_handler_connector = EventHandlerConnector()
self.application = Application( self.application = Application(
[(r'/ws', ZMQWebSocketProxy, {'make_response': self.make_response, [(r'/ws', ZMQWebSocketProxy, {'make_eventhandler_message': self.make_eventhandler_message,
'proxy_filter': self.proxy_filter, 'proxy_filter': self.proxy_filter,
'handle_trigger': self.handle_trigger,
'event_handler_connector': self._event_handler_connector})] 'event_handler_connector': self._event_handler_connector})]
) )
#self.controller_responder = ControllerResponder(self.fsm) TODO: add this once controller stuff is resolved #self.controller_responder = ControllerResponder(self.fsm) TODO: add this once controller stuff is resolved
@@ -37,13 +38,21 @@ class TFWServer:
def fsm_manager(self): def fsm_manager(self):
return self._fsm_manager return self._fsm_manager
def make_response(self, message): def make_eventhandler_message(self, message):
self.trigger_fsm(message)
message['FSMUpdate'] = self._fsm_updater.get_fsm_state_and_transitions()
return message
def handle_trigger(self, message):
LOG.debug('Executing handler for trigger "%s"', message.get('trigger', ''))
self.trigger_fsm(message)
def trigger_fsm(self, message):
trigger = message.get('trigger', '') trigger = message.get('trigger', '')
try: try:
self._fsm_manager.trigger(trigger, message) self._fsm_manager.trigger(trigger, message)
except AttributeError: except AttributeError:
LOG.debug('FSM failed to execute nonexistent trigger: "%s"', trigger) LOG.debug('FSM failed to execute nonexistent trigger: "%s"', trigger)
return message
def proxy_filter(self, message): def proxy_filter(self, message):
# pylint: disable=unused-argument,no-self-use # pylint: disable=unused-argument,no-self-use
@@ -66,7 +75,7 @@ class FSMManager:
def trigger(self, trigger, message): def trigger(self, trigger, message):
predicate_results = [] predicate_results = []
for predicate in self.trigger_predicates[trigger]: for predicate in self.trigger_predicates[trigger]:
success, message = predicate() success, message = predicate(message)
predicate_results.append(success) predicate_results.append(success)
self.messenge_sender.send('FSM', message) self.messenge_sender.send('FSM', message)
@@ -91,11 +100,13 @@ class FSMUpdater:
def update(self, kwargs_dict): def update(self, kwargs_dict):
# pylint: disable=unused-argument # pylint: disable=unused-argument
self.uplink.send(*self.generate_fsm_update()) self.uplink.send(self.generate_fsm_update())
def generate_fsm_update(self): def generate_fsm_update(self):
key = 'FSMUpdate' return {'key': 'FSMUpdate',
response = {'data': {'current_state': self.fsm.state, 'data': self.get_fsm_state_and_transitions()}
'valid_transitions':
[{'trigger': trigger} for trigger in self.fsm.machine.get_triggers(self.fsm.state)]}} def get_fsm_state_and_transitions(self):
return key, response state = self.fsm.state
valid_transitions = [{'trigger': trigger} for trigger in self.fsm.machine.get_triggers(self.fsm.state)]
return {'current_state': state, 'valid_transitions': valid_transitions}
@@ -6,7 +6,7 @@ from abc import ABC, abstractmethod
from tornado.websocket import WebSocketHandler from tornado.websocket import WebSocketHandler
from tfw.networking import deserialize_all from tfw.networking import deserialize_tfw_msg, validate_message
from tfw.config.logs import logging from tfw.config.logs import logging
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@@ -15,7 +15,7 @@ LOG = logging.getLogger(__name__)
class ZMQWebSocketHandler(WebSocketHandler, ABC): class ZMQWebSocketHandler(WebSocketHandler, ABC):
instances = set() instances = set()
def initialize(self, **kwargs): def initialize(self, **kwargs): # pylint: disable=arguments-differ
self._event_handler_connector = kwargs['event_handler_connector'] self._event_handler_connector = kwargs['event_handler_connector']
def prepare(self): def prepare(self):
@@ -31,31 +31,40 @@ class ZMQWebSocketHandler(WebSocketHandler, ABC):
def zmq_callback(self, msg_parts): def zmq_callback(self, msg_parts):
keyhandlers = {'mirror': self.mirror} keyhandlers = {'mirror': self.mirror}
key, data = deserialize_all(*msg_parts) message = deserialize_tfw_msg(*msg_parts)
LOG.debug('Received on pull socket: %s', data) LOG.debug('Received on pull socket: %s', message)
if key not in keyhandlers: if not validate_message(message):
return
self.handle_trigger(message)
if message['key'] not in keyhandlers:
for instance in ZMQWebSocketHandler.instances: for instance in ZMQWebSocketHandler.instances:
instance.write_message(data) instance.write_message(message)
else: else:
try: try:
keyhandlers[key](data['data']) keyhandlers[message['key']](message)
except KeyError: except KeyError:
LOG.error('Invalid mirror message format! Ignoring.') LOG.error('Invalid mirror message format! Ignoring.')
def mirror(self, data): def mirror(self, message):
key = data['key'] message = message['data']
self._event_handler_connector.send_message({'data': data}, key) self._event_handler_connector.send_message(message)
def on_message(self, message): def on_message(self, message):
LOG.debug('Received on WebSocket: %s', message) LOG.debug('Received on WebSocket: %s', message)
self.send_message(self.make_response(message)) if validate_message(message):
self.send_message(self.make_eventhandler_message(message))
@abstractmethod @abstractmethod
def make_response(self, message): def make_eventhandler_message(self, message):
raise NotImplementedError raise NotImplementedError
def send_message(self, message: dict, key: str = None): def send_message(self, message: dict):
self._event_handler_connector.send_message(message, key) self._event_handler_connector.send_message(message)
@abstractmethod
def handle_trigger(self, message):
raise NotImplementedError
# much secure, very cors, wow # much secure, very cors, wow
def check_origin(self, origin): def check_origin(self, origin):
@@ -64,16 +73,19 @@ class ZMQWebSocketHandler(WebSocketHandler, ABC):
class ZMQWebSocketProxy(ZMQWebSocketHandler): class ZMQWebSocketProxy(ZMQWebSocketHandler):
# pylint: disable=abstract-method # pylint: disable=abstract-method
def initialize(self, **kwargs): def initialize(self, **kwargs): # pylint: disable=arguments-differ
# pylint: disable=arguments-differ
super(ZMQWebSocketProxy, self).initialize(**kwargs) super(ZMQWebSocketProxy, self).initialize(**kwargs)
self._make_response = kwargs['make_response'] self._make_eventhandler_message = kwargs['make_eventhandler_message']
self._proxy_filter = kwargs['proxy_filter'] self._proxy_filter = kwargs['proxy_filter']
self._handle_trigger = kwargs['handle_trigger']
def on_message(self, message): def on_message(self, message):
message = json.loads(message) message = json.loads(message)
if self._proxy_filter(message): if self._proxy_filter(message):
super().on_message(message) super().on_message(message)
def make_response(self, message): def make_eventhandler_message(self, message):
return self._make_response(message) return self._make_eventhandler_message(message)
def handle_trigger(self, message):
self._handle_trigger(message)