Compare commits

...
Author SHA1 Message Date
Kristóf Tóth 09bcb7de6b Merge branch 'fsm_as_eventhandler' 2018-07-11 10:34:49 +02:00
Kristóf Tóth 0e8f529726 Optimize FSMBase by using generators for predicate logic 2018-07-10 15:40:10 +02:00
Kristóf Tóth f78f01d6e1 Provide full event handler API documentation 2018-07-10 14:37:20 +02:00
Kristóf Tóth 5262401b18 Update README and explain TFW server behaviour better 2018-07-07 21:30:24 +02:00
Kristóf Tóth 57d2475ebc Fix invalid trigger killing FSMBase 2018-07-06 15:40:27 +01:00
Kristóf Tóth 7cfa63bacf Rename config transformation method for easier understanding 2018-07-06 12:31:25 +01:00
Kristóf Tóth 7f583d8d1f Improve YamlFSM initialization logic 2018-07-06 12:27:26 +01:00
Kristóf Tóth c7ee97f0c6 Simplify predicate finding logic YamlFSM 2018-07-04 21:58:30 +02:00
Kristóf Tóth 1beb419b09 Remove subprocess spawning stuff from YamlFSM for SRP 2018-07-04 18:15:34 +02:00
Kristóf Tóth d71a25e30a Implement subscribing predicates found in yaml 2018-07-04 18:11:42 +02:00
Kristóf Tóth ea76a19595 Refactor YamlFSM moar 2018-07-04 18:00:41 +02:00
Kristóf Tóth bfa1bffbc5 Refactor YamlFSM 2018-07-04 17:48:31 +02:00
Kristóf Tóth 7a92d88b73 Refactor FSMBase to subclass transitions.Machine 2018-07-04 15:48:16 +02:00
Kristóf Tóth 91c257554f Simplify callback monkeypatching logic in YamlFSM 2018-07-03 20:09:47 +02:00
Kristóf Tóth 022a997dc2 Implement monkey patching callbacks in YamlFSM config 2018-07-03 19:06:54 +02:00
Kristóf Tóth f575a80bd4 Implement optional delay in bash history command appending 2018-07-03 15:22:29 +02:00
Kristóf Tóth 5e4303ac06 Add first version of YamlFSM 2018-07-03 15:14:00 +02:00
Kristóf Tóth 7c0e6d49bc Handle starting TFWServer in baseimage 2018-06-29 22:53:44 +02:00
Kristóf Tóth 708c920784 Move FSM handling logic to an event handler 2018-06-29 22:03:19 +02:00
Kristóf Tóth a6563bcd89 Implement event handler base class that broadcasts everything 2018-06-29 22:02:26 +02:00
Kristóf Tóth 196e753fb9 Fix typo in comment 2018-06-29 15:59:03 +02:00
Kristóf Tóth 427694623f Rename file to harmonize with new networking code structure 2018-06-29 15:40:07 +02:00
Kristóf Tóth 7ed0715f4c Reorder stuff to follow the teachings of Uncle Bob 2018-06-29 15:33:45 +02:00
Kristóf Tóth b217ac59c8 Move message validation code to where it belongs 2018-06-29 12:06:08 +02:00
Kristóf Tóth 36a86b0454 Hide ZMQ serialization magic from EHConnector clients 2018-06-29 11:58:05 +02:00
Kristóf Tóth 934f8ec74c Hide ZMQ serialization magic from ServerConnector clients 2018-06-29 11:50:36 +02:00
Kristóf Tóth 1b65bd4d3d Implement message broadcasting 2018-06-29 10:54:08 +02:00
Kristóf Tóth f8233d51a9 Completely rework TFWServer networking 🍰 2018-06-28 17:33:20 +02:00
Kristóf Tóth 3ba56a8096 Implement batch callback subscription in CallbackMixin 2018-06-28 17:31:55 +02:00
Kristóf Tóth 9eacf3f79c Add message queueing capabilities to MessageSender 2018-06-27 15:52:17 +02:00
Kristóf Tóth 83dd6ae836 Fix IdeEventHandler not working with arbitrary keys 2018-06-18 15:13:39 +02:00
therealkrispet 20f8cdb00c Merge pull request #36 from avatao-content/sphinx-static
Fix a Sphinx warning message
2018-06-15 11:16:47 +02:00
therealkrispet 26adada2db Merge pull request #34 from avatao-content/docs-dep
Add Sphinx as an extra requirement to `setup.py`
2018-06-15 11:16:03 +02:00
Paul-Emmanuel Raoul fa3bd1334a Fix a Sphinx warning message 2018-06-14 16:28:30 +01:00
Paul-Emmanuel Raoul cd2b732b8f Add Sphinx as an extra requirement to 'setup.py' 2018-06-14 15:56:40 +01:00
Kristóf Tóth 2fc54832f3 Include new docs building method in readme 2018-06-08 14:15:44 +02:00
Kristóf Tóth 24780a9285 Strip ANSI color sequences from supervisor logs 2018-06-08 11:07:29 +02:00
Kristóf Tóth 2e3b49f8b5 Use update_wrapper in lazy_property update attribute info 2018-06-05 00:16:31 +02:00
therealkrispet ab966f6d27 Merge pull request #33 from avatao-content/message-types
Message types
2018-06-04 22:18:25 +02:00
Kristóf Tóth d0667253c2 Harmonize code formatting style across project 2018-06-04 22:16:44 +02:00
Kristóf Tóth afc84e1d1a Conciliate FileManager attribute names and formatting 2018-06-04 21:47:10 +02:00
Kristóf Tóth 92e9812776 Remove unnecessary additional_dirs IdeEH.__init__ argument 2018-06-04 21:20:36 +02:00
Kristóf Tóth edc46a8ae6 Rename LazyInitialise to make IDEs recognise it as a property 2018-06-02 11:48:34 +02:00
Kristóf Tóth d5b0bb4d32 Avoid potential TypeErrors using LazyInitialise 2018-06-02 11:25:48 +02:00
Kristóf Tóth a743b01bcf Reduce line length to 120 2018-06-01 17:19:58 +02:00
Kristóf Tóth 7a67b68cba Add info on Sphinx-docs to README.md 2018-06-01 16:23:44 +02:00
Kristóf Tóth e80cce00f3 Add sphinx API documentation 2018-06-01 16:20:20 +02:00
Kristóf Tóth c0fb28c46c Stop LogMonitor on EventHandler.cleanup() in LogMonitoringEH 2018-06-01 15:15:07 +02:00
Kristóf Tóth 469c1e7217 Name new TFW version due to API breaking 2018-06-01 14:24:00 +02:00
Kristóf Tóth 6f3db18146 Use power of LazyInitialise to replace CallbackMixin.__init__() 2018-06-01 14:21:32 +02:00
Kristóf Tóth cebacb15e6 Use power of LazyInitialise to replace ObserverMixin.__init__() 2018-06-01 14:06:40 +02:00
Kristóf Tóth 9f3a3b501f Initialise supervisor xmlrpc client lazily to avoid globals 2018-06-01 14:01:32 +02:00
Kristóf Tóth 8817f991a5 Use power of LazyInitialise to murder TFW/TAOENV global state 2018-06-01 14:01:15 +02:00
Kristóf Tóth 06c2fc97ad Implement decorator to lazy initialise a property 2018-06-01 13:58:50 +02:00
Kristóf Tóth e98c41d3cf Comply new API style in TerminalEH 2018-05-31 14:08:29 +02:00
Kristóf Tóth ea251ef474 Document LogMonitoringEventHandler 2018-05-31 14:03:11 +02:00
Kristóf Tóth b68ceeb394 Rework LogMonitoringEH API to adhere new conventions 2018-05-31 13:53:11 +02:00
Kristóf Tóth 9abd5b72f4 Hotfix supervisor infinite log recursion if no LogMonitor 2018-05-30 16:48:55 +02:00
Kristóf Tóth 2e953ea098 Implement LogMonitorEH API 2018-05-30 15:21:36 +02:00
Kristóf Tóth 211ae1e2d9 Allow changing MonitorManagerMixin.monitor_args dynamically 2018-05-30 15:19:59 +02:00
therealkrispet 84ea46ae76 Merge pull request #32 from avatao-content/console
Console
2018-05-30 14:30:17 +02:00
Kristóf Tóth 73f6c6635b Rename my bootiful threadlocalise.. SupervisorBaseMixin method 2018-05-30 14:07:40 +02:00
Kristóf Tóth babb14288c Use contextlib.suppress() to suppress stuff in SupervisorLogMixin 2018-05-30 14:05:13 +02:00
Kristóf Tóth 3bb97a6dcc Implement LogMonitoringEventHandler to manage a LogMonitor via API 2018-05-30 13:14:25 +02:00
Kristóf Tóth 9e6cd57532 Refactor MonitorManagerMixin to allow arbitrary Monitor arguments 2018-05-30 13:06:09 +02:00
Kristóf Tóth 28c16e1127 Rework log deletion – use log_tail arguments instead 2018-05-29 17:59:16 +02:00
Kristóf Tóth 4815b97200 Fix shared xmlrpc client raising HTTP errors in LogMonitor 2018-05-29 16:40:58 +02:00
Kristóf Tóth 9f6418bf60 Support creating thread-local xmlrpc clients in SupervisorBaseMixin 2018-05-29 16:40:58 +02:00
Kristóf Tóth 780d6a9b3c Avoid pylint stupidity ¯\_(ツ)_/¯ 2018-05-29 16:40:58 +02:00
Kristóf Tóth 32e3c2860d Fix inotify event logs triggering themselves (infinite log recursion) 2018-05-29 16:40:58 +02:00
Kristóf Tóth 029f4a9eb2 Implement watchdog observer to watch the logs of a supervisor process 2018-05-29 16:40:58 +02:00
Kristóf Tóth a6d9b50b93 Log everything from supervisor to stdout to avoid log recursion 2018-05-29 16:40:53 +02:00
Kristóf Tóth 9723f98950 Rework ProcessManagingEH log API 2018-05-28 16:36:59 +02:00
Kristóf Tóth 1a93dad562 Separate log clearing from reading 2018-05-28 15:02:53 +02:00
Kristóf Tóth 44df95a434 Refactor supervisor logging logic to separate mixin 2018-05-28 14:32:11 +02:00
Kristóf Tóth fd56f8ca63 Make it impossible for TerminalCommands commands to kill TFW 2018-05-26 22:48:28 +02:00
Kristóf Tóth 77fabf28f6 Fix TerminalCommands parsing unix-style arguments incorrectly 2018-05-26 22:39:27 +02:00
58 changed files with 1367 additions and 388 deletions
+1 -1
View File
@@ -10,4 +10,4 @@ pipeline:
- docker push eu.gcr.io/avatao-challengestore/tutorial-framework:${DRONE_TAG}
when:
event: 'tag'
branch: refs/tags/egyptianmau-20*
branch: refs/tags/mainecoon-20*
+1 -1
View File
@@ -1,5 +1,5 @@
[TYPECHECK]
ignored-modules = zmq
max-line-length = 150
max-line-length = 120
disable = missing-docstring, too-few-public-methods, invalid-name
+4 -1
View File
@@ -37,6 +37,7 @@ ENV PYTHONPATH="/usr/local/lib" \
TFW_LIB_DIR="/usr/local/lib/" \
TFW_TERMINADO_DIR="/tmp/terminado_server" \
TFW_FRONTEND_DIR="/srv/frontend" \
TFW_SERVER_DIR="/srv/.tfw" \
TFW_HISTFILE="/home/${AVATAO_USER}/.bash_history" \
PROMPT_COMMAND="history -a"
@@ -45,13 +46,15 @@ RUN echo "export HISTFILE=${TFW_HISTFILE}" >> /tmp/bashrc &&\
cat /tmp/bashrc >> /home/${AVATAO_USER}/.bashrc
COPY supervisor/supervisord.conf ${TFW_SUPERVISORD_CONF}
COPY supervisor/components/ ${TFW_SUPERVISORD_COMPONENTS}
COPY nginx/nginx.conf ${TFW_NGINX_CONF}
COPY nginx/default.conf ${TFW_NGINX_DEFAULT}
COPY nginx/components/ ${TFW_NGINX_COMPONENTS}
COPY lib LICENSE ${TFW_LIB_DIR}
COPY supervisor/tfw_server.py ${TFW_SERVER_DIR}/
RUN for dir in "${TFW_LIB_DIR}"/{tfw,tao,envvars} "/etc/nginx" "/etc/supervisor"; do \
chown -R root:root "$dir" && chmod -R 700 "$dir"; \
chown -R root:root "$dir" && chmod -R 700 "$dir"; \
done
ONBUILD ARG BUILD_CONTEXT="solvable"
+186 -1
View File
@@ -20,6 +20,20 @@ Frontend components use websockets to connect to the TFW server, to which you ca
![TFW architecture](docs/tfw_architecture.png)
### Networking details
Event handlers connect to the TFW server using ZMQ.
They receive messages on their `SUB`(scribe) sockets, which are connected to the `PUB`(lish) socket of the server.
Event handlers reply on their `PUSH` socket, then their messages are received on the `PULL` socket of the server.
The TFW server is basically just a fancy proxy.
It's behaviour is quite simple: it proxies every message received from the fontend to the event handlers and vice versa.
The server is also capable of "mirroring" messages back to their source.
This is useful for communication between event handlers or frontend components (event handler to event handler or frontend component to frontend component communication).
Components can also broadcast messages (broadcasted messages are received both by event handlers and the frontend as well).
### Event handlers
Imagine event handlers as callbacks that are invoked when TFW receives a specific type of message. For instance, you could send a message to the framework when the user does something of note.
@@ -75,9 +89,180 @@ The TFW message format:
- 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)
To mirror messages back to their sources you can use a special messaging format, in which the message to be mirrored is enveloped inside the `data` field of the outer message:
```text
"key": "mirror",
"data":
{
...
The message you want to mirror (with it's own "key" and "data" fields)
...
}
```
Broadcasting messages is possible in a similar manner by using `"key": "broadcast"` in the outer message.
## 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.
To get started you should take a look at the [test-tutorial-framework](https://github.com/avatao-content/test-tutorial-framework) repository, which serves as an example project as well.
In the `docs` folder you can find our Sphinx-based documentation, which you can build using the `hack/tfw.sh` script in the [test-tutorial-framework](https://github.com/avatao-content/test-tutorial-framework) repository.
To get started you should take a look at [test-tutorial-framework](https://github.com/avatao-content/test-tutorial-framework), which serves as an example project as well.
## API
APIs exposed by our pre-witten event handlers are documented here.
### IdeEventHandler
You can read the content of the currently selected file like so:
```
{
"key": "ide",
"data":
{
"command": "read"
}
}
```
Use the following message to overwrite the content of the currently selected file:
```
{
"key": "ide",
"data":
{
"command": "write",
"content": ...string...
}
}
```
To select a file use the following message:
```
{
"key": "ide",
"data":
{
"command": "select",
"filename": ...string...
}
}
```
You can switch to a new working directory using this message (note that the directory must be in `allowed_directories`):
```
{
"key": "ide",
"data":
{
"command": "selectdir",
"directory": ...string...
}
}
```
Overwriting the current list of excluded file patterns is possible with this message:
```
{
"key": "ide",
"data":
{
"command": "exclude",
"exclude": ...array of strings...
}
}
```
### TerminalEventHandler
Writing to the terminal:
```
{
"key": "shell",
"data":
{
"command": "write",
"value": ...string...
}
}
```
You can read terminal command history like so:
```
{
"key": "shell",
"data":
{
"command": "read",
"count": ...number...
}
}
```
### ProcessManagingEventHandler
Starting, stopping and restarting supervisor processes can be done using similar messages (where `command` is `start`, `stop` or `restart`):
```
{
"key": "processmanager",
"data":
{
"command": ...string...,
"process_name": ...string...
}
}
```
### LogMonitoringEventHandler
To change which supervisor process is monitored use this message:
```
{
"key": "logmonitor",
"data" :
{
"command": "process_name",
"value": ...string...
}
}
```
To set the tail length of logs (the monitor will send back the last `value` characters of the log):
```
{
"key": "logmonitor",
"data" :
{
"command": "log_tail",
"value": ...number...
}
}
```
### FSMManagingEventHandler
To attempt executing a trigger on the FSM use:
```
{
"key": "fsm",
"data" :
{
"command": "trigger",
"value": ...string...
}
}
```
To force the broadcasting of an FSM update you can use this message:
```
{
"key": "fsm",
"data" :
{
"command": "update"
}
}
```
+1 -1
View File
@@ -1 +1 @@
egyptianmau
mainecoon
+1 -1
View File
@@ -9,7 +9,7 @@ _precmd()
{
[ -z "$AT_PROMPT" ] && return
unset AT_PROMPT
history -a
[ -z "$TFW_DELAY_HISTAPPEND" ] && history -a
}
trap _precmd DEBUG
+20
View File
@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXPROJ = baseimage-tutorial-framework
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+36
View File
@@ -0,0 +1,36 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
set SPHINXPROJ=baseimage-tutorial-framework
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
:end
popd
View File
+25
View File
@@ -0,0 +1,25 @@
Components
----------
.. automodule:: tfw.components
.. autoclass:: IdeEventHandler
:members:
.. autoclass:: TerminalEventHandler
:members:
.. autoclass:: ProcessManagingEventHandler
:members:
.. autoclass:: LogMonitoringEventHandler
:members:
.. autoclass:: TerminalCommands
:members:
.. autoclass:: HistoryMonitor
:members:
.. autoclass:: BashMonitor
:members:
+166
View File
@@ -0,0 +1,166 @@
# -*- coding: utf-8 -*-
#
# Configuration file for the Sphinx documentation builder.
#
# This file does only contain a selection of the most common options. For a
# full list see the documentation:
# http://www.sphinx-doc.org/en/master/config
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
sys.path.insert(0, os.path.abspath('../../lib'))
# -- Project information -----------------------------------------------------
project = 'baseimage-tutorial-framework'
copyright = '2018, Avatao Innovative Learning Kft'
author = 'Kristóf Tóth'
# The short X.Y version
version = ''
# The full version, including alpha/beta/rc tags
release = 'mainecoon'
# -- General configuration ---------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path .
exclude_patterns = []
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# The default sidebars (for documents that don't match any pattern) are
# defined by theme itself. Builtin themes are using these templates by
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
# 'searchbox.html']``.
#
# html_sidebars = {}
# -- Options for HTMLHelp output ---------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'baseimage-tutorial-frameworkdoc'
# -- Options for LaTeX output ------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'baseimage-tutorial-framework.tex', 'baseimage-tutorial-framework Documentation',
'Kristóf Tóth', 'manual'),
]
# -- Options for manual page output ------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'baseimage-tutorial-framework', 'baseimage-tutorial-framework Documentation',
[author], 1)
]
# -- Options for Texinfo output ----------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'baseimage-tutorial-framework', 'baseimage-tutorial-framework Documentation',
author, 'baseimage-tutorial-framework', 'One line description of project.',
'Miscellaneous'),
]
# -- Extension configuration -------------------------------------------------
def skip(app, what, name, obj, skip, options):
if name == "__init__":
return False
return skip
def setup(app):
app.connect("autodoc-skip-member", skip)
+12
View File
@@ -0,0 +1,12 @@
Event handler base classes
--------------------------
Subclass these to create your cusom event handlers.
.. automodule:: tfw
.. autoclass:: EventHandlerBase
:members:
.. autoclass:: TriggeredEventHandler
:members:
+12
View File
@@ -0,0 +1,12 @@
FSM base classes
----------------
Subclass these to create an FSM that fits your tutorial/challenge.
.. automodule:: tfw
.. autoclass:: FSMBase
:members:
.. autoclass:: LinearFSM
:members:
+7
View File
@@ -0,0 +1,7 @@
TFWServer
---------
.. automodule:: tfw.networking
.. autoclass:: TFWServer
:members:
+45
View File
@@ -0,0 +1,45 @@
.. baseimage-tutorial-framework documentation master file, created by
sphinx-quickstart on Fri Jun 1 14:29:07 2018.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to baseimage-tutorial-framework's documentation!
========================================================
Foundations
-----------
This part covers the soil which the framework is based on and stuff you will need to develop your own challenges.
.. toctree::
:glob:
foundations/*
Networking
----------
You can use these to send messages to the frontend or the event handlers through TFWServer.
.. toctree::
:glob:
networking/*
Components
----------
These are pre-written components for you to use, such as our IDE, terminal or console.
.. toctree::
:glob:
components/*
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
+17
View File
@@ -0,0 +1,17 @@
Networking
----------
.. automodule:: tfw.networking
.. autoclass:: TFWServerConnector
:members:
.. automodule:: tfw.networking.event_handlers
.. autoclass:: ServerUplinkConnector
:members:
.. automodule:: tfw.networking
.. autoclass:: MessageSender
:members:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 46 KiB

+18 -5
View File
@@ -4,9 +4,22 @@
from collections import namedtuple
from os import environ
from tfw.decorators import lazy_property
def prefixed_envvars_to_namedtuple(prefix: str, tuple_name: str):
envvars = {envvar.replace(prefix, '', 1): environ.get(envvar)
for envvar in environ.keys()
if envvar.startswith(prefix)}
return namedtuple(tuple_name, envvars)(**envvars)
class LazyEnvironment:
def __init__(self, prefix, tuple_name):
self._prefix = prefix
self._tuple_name = tuple_name
@lazy_property
def environment(self):
return self.prefixed_envvars_to_namedtuple()
def prefixed_envvars_to_namedtuple(self):
envvars = {
envvar.replace(self._prefix, '', 1): environ.get(envvar)
for envvar in environ.keys()
if envvar.startswith(self._prefix)
}
return namedtuple(self._tuple_name, envvars)(**envvars)
+2 -2
View File
@@ -1,6 +1,6 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from envvars import prefixed_envvars_to_namedtuple
from envvars import LazyEnvironment
TAOENV = prefixed_envvars_to_namedtuple('AVATAO_', 'taoenvtuple')
TAOENV = LazyEnvironment('AVATAO_', 'taoenvtuple').environment
+2 -1
View File
@@ -1,6 +1,7 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from .event_handler_base import EventHandlerBase, TriggeredEventHandler
from .event_handler_base import EventHandlerBase, TriggeredEventHandler, BroadcastingEventHandler
from .fsm_base import FSMBase
from .linear_fsm import LinearFSM
from .yaml_fsm import YamlFSM
+2
View File
@@ -7,3 +7,5 @@ from .terminal_event_handler import TerminalEventHandler
from .ide_event_handler import IdeEventHandler
from .history_monitor import HistoryMonitor, BashMonitor, GDBMonitor
from .terminal_commands import TerminalCommands
from .log_monitoring_event_handler import LogMonitoringEventHandler
from .fsm_managing_event_handler import FSMManagingEventHandler
+8 -6
View File
@@ -15,9 +15,8 @@ LOG = logging.getLogger(__name__)
class DirectoryMonitor(ObserverMixin):
def __init__(self, directories):
ObserverMixin.__init__(self)
self.eventhandler = IdeReloadWatchdogEventHandler()
def __init__(self, ide_key, directories):
self.eventhandler = IdeReloadWatchdogEventHandler(ide_key)
for directory in directories:
self.observer.schedule(self.eventhandler, directory, recursive=True)
@@ -45,8 +44,9 @@ class DirectoryMonitor(ObserverMixin):
class IdeReloadWatchdogEventHandler(FileSystemWatchdogEventHandler):
def __init__(self):
def __init__(self, ide_key):
super().__init__()
self.ide_key = ide_key
self.uplink = ServerUplinkConnector()
self._paused = False
self.ignore = 0
@@ -65,8 +65,10 @@ class IdeReloadWatchdogEventHandler(FileSystemWatchdogEventHandler):
self.ignore = self.ignore - 1
return
LOG.debug(event)
self.uplink.send({'key': 'ide',
'data': {'command': 'reload'}})
self.uplink.send({
'key': self.ide_key,
'data': {'command': 'reload'}
})
def with_monitor_paused(fun):
@@ -15,12 +15,19 @@ class DirectoryMonitoringEventHandler(EventHandlerBase, MonitorManagerMixin):
def __init__(self, key, directory):
super().__init__(key)
self._directory = directory
MonitorManagerMixin.__init__(self, DirectoryMonitor, self._directory)
MonitorManagerMixin.__init__(
self,
DirectoryMonitor,
key,
self._directory
)
self.commands = {'pause': self.pause,
'resume': self.resume,
'ignore': self.ignore,
'selectdir': self.selectdir}
self.commands = {
'pause': self.pause,
'resume': self.resume,
'ignore': self.ignore,
'selectdir': self.selectdir
}
@property
def directory(self):
@@ -0,0 +1,56 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from tfw import BroadcastingEventHandler
from tfw.config.logs import logging
LOG = logging.getLogger(__name__)
class FSMManagingEventHandler(BroadcastingEventHandler):
def __init__(self, key, fsm_type):
super().__init__(key)
self.fsm = fsm_type()
self._fsm_updater = FSMUpdater(self.fsm)
self.command_handlers = {
'trigger': self.handle_trigger,
'update': self.handle_update
}
def handle_event(self, message):
try:
data = message['data']
message['data'] = self.command_handlers[data['command']](data)
return message
except KeyError:
LOG.error('IGNORING MESSAGE: Invalid message received: %s', message)
def handle_trigger(self, data):
self.fsm.step(data['value'])
return self.with_fsm_update(data)
def with_fsm_update(self, data):
return {
**data,
**self._fsm_updater.get_fsm_state_and_transitions()
}
def handle_update(self, data):
return self.with_fsm_update(data)
class FSMUpdater:
def __init__(self, fsm):
self.fsm = fsm
def get_fsm_state_and_transitions(self):
state = self.fsm.state
valid_transitions = [
{'trigger': trigger}
for trigger in self.fsm.get_triggers(self.fsm.state)
]
return {
'current_state': state,
'valid_transitions': valid_transitions
}
+16 -11
View File
@@ -36,15 +36,17 @@ class HistoryMonitor(CallbackMixin, ObserverMixin, ABC):
See examples below.
"""
def __init__(self, histfile):
CallbackMixin.__init__(self)
ObserverMixin.__init__(self)
self.histfile = histfile
self._history = []
self._last_length = len(self._history)
self.observer.schedule(CallbackEventHandler([self.histfile],
self._fetch_history,
self._invoke_callbacks),
dirname(self.histfile))
self.observer.schedule(
CallbackEventHandler(
[self.histfile],
self._fetch_history,
self._invoke_callbacks
),
dirname(self.histfile)
)
@property
def history(self):
@@ -55,7 +57,10 @@ class HistoryMonitor(CallbackMixin, ObserverMixin, ABC):
with open(self.histfile, 'r') as ifile:
pattern = compileregex(self.command_pattern)
data = ifile.read()
self._history = [self.sanitize_command(command) for command in findall(pattern, data)]
self._history = [
self.sanitize_command(command)
for command in findall(pattern, data)
]
@property
@abstractmethod
@@ -76,10 +81,10 @@ 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
PROMPT_COMMAND="history -a"
shopt -s cmdhist
shopt -s histappend
unset HISTCONTROL
"""
@property
def command_pattern(self):
+59 -39
View File
@@ -41,8 +41,8 @@ class FileManager: # pylint: disable=too-many-instance-attributes
def workdir(self, directory):
if not exists(directory) or not isdir(directory):
raise EnvironmentError(f'"{directory}" is not a directory!')
if not self._is_in_whitelisted_dir(directory):
raise EnvironmentError(f'Directory "{directory}" is not in whitelist!')
if not self._is_in_allowed_dir(directory):
raise EnvironmentError(f'Directory "{directory}" is not allowed!')
self._workdir = directory
@property
@@ -65,8 +65,13 @@ class FileManager: # pylint: disable=too-many-instance-attributes
@property
def files(self):
return [self._relpath(file) for file in glob(join(self._workdir, '**/*'), recursive=True)
if isfile(file) and self._is_in_whitelisted_dir(file) and not self._is_blacklisted(file)]
return [
self._relpath(file)
for file in glob(join(self._workdir, '**/*'), recursive=True)
if isfile(file)
and self._is_in_allowed_dir(file)
and not self._is_blacklisted(file)
]
@property
def file_contents(self):
@@ -78,11 +83,17 @@ class FileManager: # pylint: disable=too-many-instance-attributes
with open(self._filepath(self.filename), 'w', errors='surrogateescape') as ofile:
ofile.write(value)
def _is_in_whitelisted_dir(self, path):
return any(realpath(path).startswith(allowed_dir) for allowed_dir in self.allowed_directories)
def _is_in_allowed_dir(self, path):
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)
return any(
fnmatchcase(file, blacklisted)
for blacklisted in self.exclude
)
def _filepath(self, filename):
return join(self._workdir, filename)
@@ -101,45 +112,52 @@ class IdeEventHandler(EventHandlerBase, MonitorManagerMixin):
When any file in the selected directory changes they are automatically refreshed
on the frontend (this is done by listening to inotify events).
This EventHandler accepts messages that have a data["command"] key specifying
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.
The API of each command is documented in their respective handler.
"""
def __init__(self, key, directory, allowed_directories, selected_file=None, exclude=None,
additional_watched_directories=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 allowed_directories: list of directories that can be switched to using selectdir
:param selected_file: file that is selected by default
:param exclude: list of filenames that should not appear between files (for *.o, *.pyc, etc.)
:param additional_watched_directories: refresh the selected file when files change in these directories
(the working directory is watched by default, this is useful for
symlinks and such)
:param exclude: list of filenames that should not appear between files (for .o, .pyc, etc.)
"""
super().__init__(key)
try:
self.filemanager = FileManager(allowed_directories=allowed_directories, working_directory=directory,
selected_file=selected_file, exclude=exclude)
self.filemanager = FileManager(
allowed_directories=allowed_directories,
working_directory=directory,
selected_file=selected_file,
exclude=exclude
)
except IndexError:
raise EnvironmentError(f'No file(s) in IdeEventHandler working_directory "{directory}"!')
raise EnvironmentError(
f'No file(s) in IdeEventHandler working_directory "{directory}"!'
)
self.watched_directories = [self.filemanager.workdir]
if additional_watched_directories:
self.watched_directories.extend(additional_watched_directories)
MonitorManagerMixin.__init__(self, DirectoryMonitor, self.watched_directories)
MonitorManagerMixin.__init__(
self,
DirectoryMonitor,
self.key,
self.filemanager.allowed_directories
)
self.commands = {'read': self.read,
'write': self.write,
'select': self.select,
'selectdir': self.select_dir,
'exclude': self.exclude}
self.commands = {
'read': self.read,
'write': self.write,
'select': self.select,
'selectdir': self.select_dir,
'exclude': self.exclude
}
def read(self, data):
"""
Read the currently selected file.
:return: message with the contents of the file in data['content']
:return dict: message with the contents of the file in data['content']
"""
try:
data['content'] = self.filemanager.file_contents
@@ -155,8 +173,9 @@ class IdeEventHandler(EventHandlerBase, MonitorManagerMixin):
"""
Overwrites a file with the desired string.
:param data: TFW message data containing keys:
|-string: containing the desired file contents
:param data: TFW message data containing key 'content'
(new file content)
"""
self.monitor.ignore = self.monitor.ignore + 1
try:
@@ -170,8 +189,8 @@ class IdeEventHandler(EventHandlerBase, MonitorManagerMixin):
"""
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
:param data: TFW message data containing 'filename'
(name of file to select relative to the current directory)
"""
try:
self.filemanager.filename = data['filename']
@@ -183,10 +202,10 @@ class IdeEventHandler(EventHandlerBase, MonitorManagerMixin):
"""
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
:param data: TFW message data containing 'directory'
(absolute path of diretory to select.
must be a path whitelisted in
self.allowed_directories)
"""
try:
self.filemanager.workdir = data['directory']
@@ -204,8 +223,9 @@ class IdeEventHandler(EventHandlerBase, MonitorManagerMixin):
"""
Overwrite list of excluded files
:param data: TFW message data containing keys:
|-exclude: list of filename patterns to be excluded, e.g.: ["*.pyc", "*.o"]
:param data: TFW message data containing 'exclude'
(list of unix-style filename patterns to be excluded,
e.g.: ["\*.pyc", "\*.o")
"""
try:
self.filemanager.exclude = list(data['exclude'])
+56
View File
@@ -0,0 +1,56 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
import logging
from os.path import dirname
from watchdog.events import PatternMatchingEventHandler as PatternMatchingWatchdogEventHandler
from tfw.networking.event_handlers import ServerUplinkConnector
from tfw.decorators import RateLimiter
from tfw.mixins import ObserverMixin, SupervisorLogMixin
class LogMonitor(ObserverMixin):
def __init__(self, process_name, log_tail=0):
self.prevent_log_recursion()
event_handler = SendLogWatchdogEventHandler(
process_name,
log_tail=log_tail
)
self.observer.schedule(
event_handler,
event_handler.path
)
@staticmethod
def prevent_log_recursion():
# This is done to prevent inotify event logs triggering themselves (infinite log recursion)
logging.getLogger('watchdog.observers.inotify_buffer').propagate = False
class SendLogWatchdogEventHandler(PatternMatchingWatchdogEventHandler, SupervisorLogMixin):
def __init__(self, process_name, log_tail=0):
self.process_name = process_name
self.procinfo = self.supervisor.getProcessInfo(self.process_name)
super().__init__([
self.procinfo['stdout_logfile'],
self.procinfo['stderr_logfile']
])
self.uplink = ServerUplinkConnector()
self.log_tail = log_tail
@property
def path(self):
return dirname(self.procinfo['stdout_logfile'])
@RateLimiter(rate_per_second=5)
def on_modified(self, event):
self.uplink.send({
'key': 'processlog',
'data': {
'command': 'new_log',
'stdout': self.read_stdout(self.process_name, tail=self.log_tail),
'stderr': self.read_stderr(self.process_name, tail=self.log_tail)
}
})
@@ -0,0 +1,67 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from tfw import EventHandlerBase
from tfw.mixins import MonitorManagerMixin
from tfw.config.logs import logging
from .log_monitor import LogMonitor
LOG = logging.getLogger(__name__)
class LogMonitoringEventHandler(EventHandlerBase, MonitorManagerMixin):
"""
Monitors the output of a supervisor process (stdout, stderr) and
sends the results to the frontend.
Accepts messages that have a data['command'] key specifying
a command to be executed.
The API of each command is documented in their respective handler.
"""
def __init__(self, key, process_name, log_tail=0):
super().__init__(key)
self.process_name = process_name
self.log_tail = log_tail
MonitorManagerMixin.__init__(
self,
LogMonitor,
self.process_name,
self.log_tail
)
self.command_handlers = {
'process_name': self.handle_process_name,
'log_tail': self.handle_log_tail
}
def handle_event(self, message):
try:
data = message['data']
self.command_handlers[data['command']](data)
self.reload_monitor()
except KeyError:
LOG.error('IGNORING MESSAGE: Invalid message received: %s', message)
def handle_process_name(self, data):
"""
Changes the monitored process.
:param data: TFW message data containing 'value'
(name of the process to monitor)
"""
self.set_monitor_args(data['value'], self.log_tail)
def handle_log_tail(self, data):
"""
Sets tail length of the log the monitor will send
to the frontend (the monitor will send back the last
'value' characters of the log).
:param data: TFW message data containing 'value'
(new tail length)
"""
self.set_monitor_args(self.process_name, data['value'])
def cleanup(self):
self.monitor.stop()
@@ -4,18 +4,20 @@
from xmlrpc.client import Fault as SupervisorFault
from tfw import EventHandlerBase
from tfw.mixins import SupervisorMixin
from tfw.mixins import SupervisorMixin, SupervisorLogMixin
from tfw.config.logs import logging
from .directory_monitor import with_monitor_paused
LOG = logging.getLogger(__name__)
class ProcessManager(SupervisorMixin):
class ProcessManager(SupervisorMixin, SupervisorLogMixin):
def __init__(self):
self.commands = {'start': self.start_process,
'stop': self.stop_process,
'restart': self.restart_process}
self.commands = {
'start': self.start_process,
'stop': self.stop_process,
'restart': self.restart_process
}
def __call__(self, command, process_name):
return self.commands[command](process_name)
@@ -25,31 +27,39 @@ class ProcessManagingEventHandler(EventHandlerBase):
"""
Event handler that can manage processes managed by supervisor.
This EventHandler accepts messages that have a data["command"] key specifying
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
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, log_tail=0):
super().__init__(key)
self.key = key
self.monitor = dirmonitor
self.processmanager = ProcessManager()
self.log_tail = log_tail
@with_monitor_paused
def handle_event(self, message):
try:
data = message['data']
self.processmanager(data['command'], data['process_name'])
message['data']['log'] = self.processmanager.read_log_stdout(message['data']['process_name'])
try:
self.processmanager(data['command'], data['process_name'])
except SupervisorFault as fault:
message['data']['error'] = fault.faultString
finally:
message['data']['stdout'] = self.processmanager.read_stdout(
data['process_name'],
self.log_tail
)
message['data']['stderr'] = self.processmanager.read_stderr(
data['process_name'],
self.log_tail
)
return message
except KeyError:
LOG.error('IGNORING MESSAGE: Invalid message received: %s', message)
except SupervisorFault as fault:
message['data']['error'] = fault.faultString
message['data']['log'] = self.processmanager.read_log_stderr(message['data']['process_name'])
return message
+14 -9
View File
@@ -14,15 +14,15 @@ LOG = logging.getLogger(__name__)
class TerminadoMiniServer:
def __init__(self, url, port, workdir, shellcmd):
self.port = port
self._term_manager = SingleTermManager(shell_command=shellcmd,
term_settings={'cwd': workdir})
self.application = Application(
[(
url,
TerminadoMiniServer.ResetterTermSocket,
{'term_manager': self._term_manager}
)]
self._term_manager = SingleTermManager(
shell_command=shellcmd,
term_settings={'cwd': workdir}
)
self.application = Application([(
url,
TerminadoMiniServer.ResetterTermSocket,
{'term_manager': self._term_manager}
)])
@property
def term_manager(self):
@@ -46,5 +46,10 @@ class TerminadoMiniServer:
if __name__ == '__main__':
LOG.info('Terminado Mini Server listening on %s', TFWENV.TERMINADO_PORT)
TerminadoMiniServer('/terminal', TFWENV.TERMINADO_PORT, TFWENV.TERMINADO_WD, ['bash']).listen()
TerminadoMiniServer(
'/terminal',
TFWENV.TERMINADO_PORT,
TFWENV.TERMINADO_WD,
['bash']
).listen()
IOLoop.instance().start()
+13 -8
View File
@@ -3,6 +3,7 @@
from abc import ABC
from re import match
from shlex import split
from tfw.config.logs import logging
@@ -18,13 +19,13 @@ class TerminalCommands(ABC):
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
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
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 have to define a method like: "def command_vim(self, \*args)"
You can also use this class to create new commands similarly.
"""
@@ -35,8 +36,12 @@ class TerminalCommands(ABC):
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)}
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:
@@ -57,10 +62,10 @@ class TerminalCommands(ABC):
return match(self._command_method_regex, string)
def callback(self, history):
parts = history[-1].split()
parts = split(history[-1])
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)
except Exception: # pylint: disable=broad-except
LOG.exception('Command "%s" failed:', command)
+21 -11
View File
@@ -16,9 +16,9 @@ class TerminalEventHandler(EventHandlerBase):
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
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.
The API of each command is documented in their respective handler.
"""
def __init__(self, key, monitor):
"""
@@ -29,9 +29,19 @@ class TerminalEventHandler(EventHandlerBase):
self.working_directory = TFWENV.TERMINADO_DIR
self._historymonitor = monitor
bash_as_user_cmd = ['sudo', '-u', TAOENV.USER, 'bash']
self.terminado_server = TerminadoMiniServer('/terminal', TFWENV.TERMINADO_PORT, TFWENV.TERMINADO_WD, bash_as_user_cmd)
self.commands = {'write': self.write,
'read': self.read}
self.terminado_server = TerminadoMiniServer(
'/terminal',
TFWENV.TERMINADO_PORT,
TFWENV.TERMINADO_WD,
bash_as_user_cmd
)
self.commands = {
'write': self.write,
'read': self.read
}
if self._historymonitor:
self._historymonitor.watch()
self.terminado_server.listen()
@@ -54,18 +64,18 @@ class TerminalEventHandler(EventHandlerBase):
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
:param data: TFW message data containing 'value'
(command to be written to the pty)
"""
self.terminado_server.pty.write(data['shellcmd'])
self.terminado_server.pty.write(data['value'])
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']
:param data: TFW message data containing 'count'
(the number of history elements to return)
:return dict: message with list of commands in data['history']
"""
data['count'] = int(data.get('count', 1))
if self.historymonitor:
+2 -2
View File
@@ -1,6 +1,6 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from envvars import prefixed_envvars_to_namedtuple
from envvars import LazyEnvironment
TFWENV = prefixed_envvars_to_namedtuple('TFW_', 'tfwenvtuple')
TFWENV = LazyEnvironment('TFW_', 'tfwenvtuple').environment
+1
View File
@@ -2,3 +2,4 @@
# All Rights Reserved. See LICENSE file for details.
from .rate_limiter import RateLimiter
from .lazy_property import lazy_property
+21
View File
@@ -0,0 +1,21 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from functools import update_wrapper
class lazy_property:
"""
Decorator that replaces a function with the value
it calculates on the first call.
"""
def __init__(self, func):
self.func = func
update_wrapper(self, func)
def __get__(self, instance, owner):
if instance is None:
return self # avoids potential __new__ TypeError
value = self.func(instance)
setattr(instance, self.func.__name__, value)
return value
+46 -4
View File
@@ -2,9 +2,13 @@
# All Rights Reserved. See LICENSE file for details.
from abc import ABC, abstractmethod
from json import dumps
from hashlib import md5
from tfw.networking import deserialize_tfw_msg
from tfw.networking.event_handlers import ServerConnector
from tfw.config.logs import logging
LOG = logging.getLogger(__name__)
class EventHandlerBase(ABC):
@@ -20,22 +24,23 @@ class EventHandlerBase(ABC):
self.subscribe(self.key, 'reset')
self.server_connector.register_callback(self.event_handler_callback)
def event_handler_callback(self, msg_parts):
def event_handler_callback(self, message):
"""
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)
if response:
response['key'] = message['key']
self.server_connector.send(response)
def dispatch_handling(self, message):
"""
Used to dispatch messages to their specific handlers.
:param message: the message received
:returns: the message to send back
"""
if message['key'] != 'reset':
return self.handle_event(message)
@@ -47,6 +52,7 @@ class EventHandlerBase(ABC):
Abstract method that implements the handling of messages.
:param message: the message received
:returns: the message to send back
"""
raise NotImplementedError
@@ -56,6 +62,7 @@ class EventHandlerBase(ABC):
Usually 'reset' events receive some sort of special treatment.
:param message: the message received
:returns: the message to send back
"""
return None
@@ -102,3 +109,38 @@ class TriggeredEventHandler(EventHandlerBase, ABC):
if message.get('trigger') == self.trigger:
return super().dispatch_handling(message)
return None
class BroadcastingEventHandler(EventHandlerBase, ABC):
# pylint: disable=abstract-method
"""
Abstract base class for EventHandlers which broadcast responses
and intelligently ignore their own broadcasted messages they receive.
"""
def __init__(self, key):
super().__init__(key)
self.own_message_hashes = []
def event_handler_callback(self, message):
message_hash = self.hash_message(message)
if message_hash in self.own_message_hashes:
self.own_message_hashes.remove(message_hash)
return
response = self.dispatch_handling(message)
if response:
self.own_message_hashes.append(self.hash_message(response))
self.server_connector.send(self.make_broadcast_message(response))
@staticmethod
def hash_message(message):
message_bytes = dumps(message, sort_keys=True).encode()
return md5(message_bytes).hexdigest()
@staticmethod
def make_broadcast_message(message):
return {
'key': 'broadcast',
'data': message
}
+43 -14
View File
@@ -1,36 +1,65 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from typing import List
from collections import defaultdict
from transitions import Machine
from transitions import Machine, MachineError
from tfw.mixins import CallbackMixin
from tfw.config.logs import logging
LOG = logging.getLogger(__name__)
class FSMBase(CallbackMixin):
class FSMBase(Machine, 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
TFW uses 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):
CallbackMixin.__init__(self)
def __init__(self, initial=None, accepted_states=None):
self.accepted_states = accepted_states or [self.states[-1]]
self.machine = Machine(model=self,
states=self.states,
transitions=self.transitions,
initial=initial or self.states[0],
send_event=True,
ignore_invalid_triggers=True,
after_state_change='execute_callbacks')
self.trigger_predicates = defaultdict(list)
Machine.__init__(
self,
states=self.states,
transitions=self.transitions,
initial=initial or self.states[0],
send_event=True,
ignore_invalid_triggers=True,
after_state_change='execute_callbacks'
)
def execute_callbacks(self, event_data):
self._execute_callbacks(event_data.kwargs)
def is_solved(self):
return self.state in self.accepted_states # pylint: disable=no-member
return self.state in self.accepted_states # pylint: disable=no-member
def subscribe_predicate(self, trigger, *predicates):
self.trigger_predicates[trigger].extend(predicates)
def unsubscribe_predicate(self, trigger, *predicates):
self.trigger_predicates[trigger] = [
predicate
for predicate in self.trigger_predicates[trigger]
not in predicates
]
def step(self, trigger):
predicate_results = (
predicate()
for predicate in self.trigger_predicates[trigger]
)
# TODO: think about what could we do when this prevents triggering
if all(predicate_results):
try:
self.trigger(trigger)
except (AttributeError, MachineError):
LOG.debug('FSM failed to execute nonexistent trigger: "%s"', trigger)
+10 -3
View File
@@ -11,12 +11,19 @@ class LinearFSM(FSMBase):
a number of steps specified in the constructor. It automatically sets up 2
actions (triggers) between states as such:
(0) -- step_1 --> (1) -- step_2 --> (2) -- step_3 --> (3) ... and so on
\-step_next-/ \-step_next-/ \-step_next-/
"""
def __init__(self, number_of_steps):
self.states = list(map(str, range(number_of_steps)))
self.transitions = []
for index in self.states[:-1]:
self.transitions.append({'trigger': f'step_{int(index)+1}', 'source': index, 'dest': str(int(index)+1)})
self.transitions.append({'trigger': 'step_next', 'source': index, 'dest': str(int(index)+1)})
self.transitions.append({
'trigger': f'step_{int(index)+1}',
'source': index,
'dest': str(int(index)+1)
})
self.transitions.append({
'trigger': 'step_next',
'source': index,
'dest': str(int(index)+1)
})
super(LinearFSM, self).__init__()
+1 -1
View File
@@ -1,7 +1,7 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from .supervisor_mixin import SupervisorMixin
from .supervisor_mixin import SupervisorMixin, SupervisorLogMixin
from .callback_mixin import CallbackMixin
from .observer_mixin import ObserverMixin
from .monitor_manager_mixin import MonitorManagerMixin
+15 -4
View File
@@ -3,21 +3,32 @@
from functools import partial
from tfw.decorators import lazy_property
class CallbackMixin:
def __init__(self):
self._callbacks = []
@lazy_property
def _callbacks(self):
return []
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
:param args: arguments passed to callable
:param kwargs: kwargs passed to callable
"""
fun = partial(callback, *args, **kwargs)
self._callbacks.append(fun)
def subscribe_callbacks(self, *callbacks):
"""
Subscribe a list of callbacks to incoke once an event is triggered.
:param callbacks: callbacks to be subscribed
"""
for callback in callbacks:
self.subscribe_callback(callback)
def unsubscribe_callback(self, callback):
self._callbacks.remove(callback)
+6 -3
View File
@@ -7,21 +7,24 @@ LOG = logging.getLogger(__name__)
class MonitorManagerMixin:
def __init__(self, monitor_type, directories):
def __init__(self, monitor_type, *monitor_args):
self._monitor_type = monitor_type
self._monitor = None
self._monitored_directories = directories
self.monitor_args = monitor_args
self.reload_monitor()
@property
def monitor(self):
return self._monitor
def set_monitor_args(self, *monitor_args):
self.monitor_args = monitor_args
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_directories)
self._monitor = self._monitor_type(*self.monitor_args)
self._monitor.watch() # This runs on a separate thread
+5 -2
View File
@@ -3,10 +3,13 @@
from watchdog.observers import Observer
from tfw.decorators import lazy_property
class ObserverMixin:
def __init__(self):
self.observer = Observer()
@lazy_property
def observer(self):
return Observer()
def watch(self):
self.observer.start()
+21 -18
View File
@@ -6,12 +6,17 @@ from xmlrpc.client import Fault as SupervisorFault
from contextlib import suppress
from os import remove
from tfw.decorators import lazy_property
from tfw.config import TFWENV
class SupervisorMixin:
supervisor = xmlrpc.client.ServerProxy(TFWENV.SUPERVISOR_HTTP_URI).supervisor
class SupervisorBaseMixin:
@lazy_property
def supervisor(self):
return xmlrpc.client.ServerProxy(TFWENV.SUPERVISOR_HTTP_URI).supervisor
class SupervisorMixin(SupervisorBaseMixin):
def stop_process(self, process_name):
with suppress(SupervisorFault):
self.supervisor.stopProcess(process_name)
@@ -19,22 +24,20 @@ class SupervisorMixin:
def start_process(self, process_name):
self.supervisor.startProcess(process_name)
def read_log_stdout(self, process_name):
return self._read_log_internal(self.supervisor.readProcessStdoutLog, process_name)
def read_log_stderr(self, process_name):
return self._read_log_internal(self.supervisor.readProcessStderrLog, process_name)
def _read_log_internal(self, read_method, process_name):
log = read_method(process_name, 0, 0)
self.clear_logs(process_name)
return log
def clear_logs(self, process_name):
for logfile in ('stdout_logfile', 'stderr_logfile'):
remove(self.supervisor.getProcessInfo(process_name)[logfile])
self.supervisor.clearProcessLogs(process_name)
def restart_process(self, process_name):
self.stop_process(process_name)
self.start_process(process_name)
class SupervisorLogMixin(SupervisorBaseMixin):
def read_stdout(self, process_name, tail=0):
return self.supervisor.readProcessStdoutLog(process_name, -tail, 0)
def read_stderr(self, process_name, tail=0):
return self.supervisor.readProcessStderrLog(process_name, -tail, 0)
def clear_logs(self, process_name):
for logfile in ('stdout_logfile', 'stderr_logfile'):
with suppress(FileNotFoundError):
remove(self.supervisor.getProcessInfo(process_name)[logfile])
self.supervisor.clearProcessLogs(process_name)
+1 -1
View File
@@ -1,7 +1,7 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from .serialization import serialize_tfw_msg, deserialize_tfw_msg, validate_message
from .serialization import serialize_tfw_msg, deserialize_tfw_msg, with_deserialize_tfw_msg
from .zmq_connector_base import ZMQConnectorBase
# from .controller_connector import ControllerConnector # TODO: readd once controller stuff is resolved
from .message_sender import MessageSender
@@ -6,9 +6,12 @@ from functools import partial
import zmq
from zmq.eventloop.zmqstream import ZMQStream
from tfw.networking import serialize_tfw_msg
from tfw.networking import serialize_tfw_msg, with_deserialize_tfw_msg
from tfw.networking import ZMQConnectorBase
from tfw.config import TFWENV
from tfw.config.logs import logging
LOG = logging.getLogger(__name__)
class ServerDownlinkConnector(ZMQConnectorBase):
@@ -20,7 +23,10 @@ class ServerDownlinkConnector(ZMQConnectorBase):
self.subscribe = partial(self._zmq_sub_socket.setsockopt_string, zmq.SUBSCRIBE)
self.unsubscribe = partial(self._zmq_sub_socket.setsockopt_string, zmq.UNSUBSCRIBE)
self.register_callback = self._zmq_sub_stream.on_recv
def register_callback(self, callback):
callback = with_deserialize_tfw_msg(callback)
self._zmq_sub_stream.on_recv(callback)
class ServerUplinkConnector(ZMQConnectorBase):
+26 -10
View File
@@ -1,30 +1,46 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from datetime import datetime
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".
Provides mechanisms to send messages to our frontend messaging component.
"""
def __init__(self, custom_key: str = None):
def __init__(self):
self.server_connector = ServerUplinkConnector()
self.key = custom_key or 'message'
self.key = 'message'
self.queue_key = 'queueMessages'
def send(self, originator, message):
"""
Sends a message to the key specified in __init__.
Sends a message.
:param originator: name of sender to be displayed on the frontend
:param message: message to send
"""
data = {
'originator': originator,
'timestamp': datetime.now().isoformat(),
'message': message
}
self.server_connector.send({'key': self.key,
'data': data})
self.server_connector.send({
'key': self.key,
'data': data
})
def queue_messages(self, originator, messages):
"""
Queues a list of messages to be displayed in a chatbot-like manner.
:param originator: name of sender to be displayed on the frontend
:param messages: list of messages to queue
"""
data = {
'messages': [
{'message': message, 'originator': originator}
for message in messages
]
}
self.server_connector.send({
'key': self.queue_key,
'data': data
})
+17 -6
View File
@@ -22,10 +22,7 @@ The purpose of this module is abstracting away this low level behaviour.
"""
import json
def validate_message(message):
return 'key' in message
from functools import wraps
def serialize_tfw_msg(message):
@@ -35,6 +32,14 @@ def serialize_tfw_msg(message):
return _serialize_all(message['key'], message)
def with_deserialize_tfw_msg(fun):
@wraps(fun)
def wrapper(message_parts):
message = deserialize_tfw_msg(*message_parts)
return fun(message)
return wrapper
def deserialize_tfw_msg(*args):
"""
Return message from TFW multipart data
@@ -43,11 +48,17 @@ def deserialize_tfw_msg(*args):
def _serialize_all(*args):
return tuple(_serialize_single(arg) for arg in args)
return tuple(
_serialize_single(arg)
for arg in args
)
def _deserialize_all(*args):
return tuple(_deserialize_single(arg) for arg in args)
return tuple(
_deserialize_single(arg)
for arg in args
)
def _serialize_single(data):
-1
View File
@@ -3,5 +3,4 @@
from .event_handler_connector import EventHandlerConnector, EventHandlerUplinkConnector, EventHandlerDownlinkConnector
from .tfw_server import TFWServer
from .zmq_websocket_handler import ZMQWebSocketProxy
# from .controller_responder import ControllerResponder # TODO: readd once controller stuff is resolved
@@ -4,7 +4,7 @@
import zmq
from zmq.eventloop.zmqstream import ZMQStream
from tfw.networking import ZMQConnectorBase, serialize_tfw_msg
from tfw.networking import ZMQConnectorBase, serialize_tfw_msg, with_deserialize_tfw_msg
from tfw.config import TFWENV
from tfw.config.logs import logging
@@ -32,6 +32,7 @@ class EventHandlerUplinkConnector(ZMQConnectorBase):
class EventHandlerConnector(EventHandlerDownlinkConnector, EventHandlerUplinkConnector):
def register_callback(self, callback):
callback = with_deserialize_tfw_msg(callback)
self._zmq_pull_stream.on_recv(callback)
def send_message(self, message: dict):
+18 -95
View File
@@ -1,15 +1,12 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from collections import defaultdict
from tornado.web import Application
from tfw.networking import MessageSender
from tfw.networking.event_handlers import ServerUplinkConnector
from tfw.networking.server import EventHandlerConnector
from tfw.config.logs import logging
from .zmq_websocket_handler import ZMQWebSocketProxy
from .zmq_websocket_proxy import ZMQWebSocketProxy
LOG = logging.getLogger(__name__)
@@ -18,103 +15,29 @@ 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.
SUB socket.
"""
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)
self._fsm.subscribe_callback(self._fsm_updater.update)
def __init__(self):
self._event_handler_connector = EventHandlerConnector()
self._uplink_connector = ServerUplinkConnector()
self.application = Application(
[(r'/ws', ZMQWebSocketProxy, {'make_eventhandler_message': self.make_eventhandler_message,
'proxy_filter': self.proxy_filter,
'handle_trigger': self.handle_trigger,
'event_handler_connector': self._event_handler_connector})]
self.application = Application([(
r'/ws', ZMQWebSocketProxy,{
'event_handler_connector': self._event_handler_connector,
'message_handlers': [self.handle_trigger]
})]
)
#self.controller_responder = ControllerResponder(self.fsm) TODO: add this once controller stuff is resolved
@property
def fsm(self):
return self._fsm
@property
def fsm_manager(self):
return self._fsm_manager
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', '')
try:
self._fsm_manager.trigger(trigger, message)
except AttributeError:
LOG.debug('FSM failed to execute nonexistent trigger: "%s"', trigger)
def proxy_filter(self, message):
# pylint: disable=unused-argument,no-self-use
return True
if 'trigger' in message:
LOG.debug('Executing handler for trigger "%s"', message.get('trigger', ''))
self._uplink_connector.send_to_eventhandler({
'key': 'fsm',
'data': {
'command': 'trigger',
'value': message.get('trigger', '')
}
})
def listen(self, port):
self.application.listen(port)
class FSMManager:
def __init__(self, fsm):
self._fsm = fsm
self.trigger_predicates = defaultdict(list)
self.messenge_sender = MessageSender()
@property
def fsm(self):
return self._fsm
def trigger(self, trigger, message):
predicate_results = []
for predicate in self.trigger_predicates[trigger]:
success, message = predicate(message)
predicate_results.append(success)
self.messenge_sender.send('FSM', message)
if all(predicate_results):
try:
self.fsm.trigger(trigger, message=message)
except AttributeError:
LOG.debug('FSM failed to execute nonexistent trigger: "%s"', trigger)
def subscribe_predicate(self, trigger, *predicates):
self.trigger_predicates[trigger].extend(predicates)
def unsubscribe_predicate(self, trigger, *predicates):
self.trigger_predicates[trigger] = [predicate for predicate in self.trigger_predicates[trigger]
not in predicates]
class FSMUpdater:
def __init__(self, fsm):
self.fsm = fsm
self.uplink = ServerUplinkConnector()
def update(self, kwargs_dict):
# pylint: disable=unused-argument
self.uplink.send(self.generate_fsm_update())
def generate_fsm_update(self):
return {'key': 'FSMUpdate',
'data': self.get_fsm_state_and_transitions()}
def get_fsm_state_and_transitions(self):
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}
@@ -1,91 +0,0 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
import json
from abc import ABC, abstractmethod
from tornado.websocket import WebSocketHandler
from tfw.networking import deserialize_tfw_msg, validate_message
from tfw.config.logs import logging
LOG = logging.getLogger(__name__)
class ZMQWebSocketHandler(WebSocketHandler, ABC):
instances = set()
def initialize(self, **kwargs): # pylint: disable=arguments-differ
self._event_handler_connector = kwargs['event_handler_connector']
def prepare(self):
ZMQWebSocketHandler.instances.add(self)
def on_close(self):
ZMQWebSocketHandler.instances.remove(self)
def open(self, *args, **kwargs):
LOG.debug('WebSocket connection initiated')
self._event_handler_connector.register_callback(self.zmq_callback)
def zmq_callback(self, msg_parts):
keyhandlers = {'mirror': self.mirror}
message = deserialize_tfw_msg(*msg_parts)
LOG.debug('Received on pull socket: %s', message)
if not validate_message(message):
return
self.handle_trigger(message)
if message['key'] not in keyhandlers:
for instance in ZMQWebSocketHandler.instances:
instance.write_message(message)
else:
try:
keyhandlers[message['key']](message)
except KeyError:
LOG.error('Invalid mirror message format! Ignoring.')
def mirror(self, message):
message = message['data']
self._event_handler_connector.send_message(message)
def on_message(self, message):
LOG.debug('Received on WebSocket: %s', message)
if validate_message(message):
self.send_message(self.make_eventhandler_message(message))
@abstractmethod
def make_eventhandler_message(self, message):
raise NotImplementedError
def send_message(self, message: dict):
self._event_handler_connector.send_message(message)
@abstractmethod
def handle_trigger(self, message):
raise NotImplementedError
# much secure, very cors, wow
def check_origin(self, origin):
return True
class ZMQWebSocketProxy(ZMQWebSocketHandler):
# pylint: disable=abstract-method
def initialize(self, **kwargs): # pylint: disable=arguments-differ
super(ZMQWebSocketProxy, self).initialize(**kwargs)
self._make_eventhandler_message = kwargs['make_eventhandler_message']
self._proxy_filter = kwargs['proxy_filter']
self._handle_trigger = kwargs['handle_trigger']
def on_message(self, message):
message = json.loads(message)
if self._proxy_filter(message):
super().on_message(message)
def make_eventhandler_message(self, message):
return self._make_eventhandler_message(message)
def handle_trigger(self, message):
self._handle_trigger(message)
@@ -0,0 +1,121 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
import json
from tornado.websocket import WebSocketHandler
from tfw.mixins import CallbackMixin
from tfw.config.logs import logging
LOG = logging.getLogger(__name__)
class ZMQWebSocketProxy(WebSocketHandler):
instances = set()
def initialize(self, **kwargs): # pylint: disable=arguments-differ
self._event_handler_connector = kwargs['event_handler_connector']
self._message_handlers = kwargs.get('message_handlers', [])
self._proxy_filters = kwargs.get('proxy_filters', [])
self.proxy_eventhandler_to_websocket = TFWProxy(
self.send_eventhandler_message,
self.send_websocket_message
)
self.proxy_websocket_to_eventhandler = TFWProxy(
self.send_websocket_message,
self.send_eventhandler_message
)
proxies = (self.proxy_eventhandler_to_websocket, self.proxy_websocket_to_eventhandler)
for proxy in proxies:
proxy.proxy_filters.subscribe_callbacks(*self._proxy_filters)
proxy.proxy_callbacks.subscribe_callbacks(*self._message_handlers)
def prepare(self):
ZMQWebSocketProxy.instances.add(self)
def on_close(self):
ZMQWebSocketProxy.instances.remove(self)
def open(self, *args, **kwargs):
LOG.debug('WebSocket connection initiated')
self._event_handler_connector.register_callback(self.eventhander_callback)
def eventhander_callback(self, message):
"""
Invoked on ZMQ messages from event handlers.
"""
LOG.debug('Received on pull socket: %s', message)
self.proxy_eventhandler_to_websocket(message)
def on_message(self, message):
"""
Invoked on WS messages from frontend.
"""
message = json.loads(message)
LOG.debug('Received on WebSocket: %s', message)
self.proxy_websocket_to_eventhandler(message)
def send_eventhandler_message(self, message):
self._event_handler_connector.send_message(message)
@staticmethod
def send_websocket_message(message):
for instance in ZMQWebSocketProxy.instances:
instance.write_message(message)
# much secure, very cors, wow
def check_origin(self, origin):
return True
class TFWProxy:
def __init__(self, to_source, to_destination):
self.to_source = to_source
self.to_destination = to_destination
self.proxy_filters = CallbackMixin()
self.proxy_callbacks = CallbackMixin()
self.proxy_filters.subscribe_callback(self.validate_message)
self.keyhandlers = {
'mirror': self.mirror,
'broadcast': self.broadcast
}
@staticmethod
def validate_message(message):
if 'key' not in message:
raise ValueError('Invalid TFW message format!')
def __call__(self, message):
try:
self.proxy_filters._execute_callbacks(message)
except ValueError:
LOG.exception('Invalid TFW message received!')
return
self.proxy_callbacks._execute_callbacks(message)
if message['key'] not in self.keyhandlers:
self.to_destination(message)
else:
handler = self.keyhandlers[message['key']]
try:
handler(message)
except KeyError:
LOG.error('Invalid "%s" message format! Ignoring.', handler.__name__)
def mirror(self, message):
message = message['data']
LOG.debug('Mirroring message: %s', message)
self.to_source(message)
def broadcast(self, message):
message = message['data']
LOG.debug('Broadcasting message: %s', message)
self.to_source(message)
self.to_destination(message)
+64
View File
@@ -0,0 +1,64 @@
from subprocess import Popen, run
from functools import partial
from contextlib import suppress
import yaml
from transitions import State
from tfw import FSMBase
class YamlFSM(FSMBase):
def __init__(self, config_file):
self.config = self.parse_config(config_file)
self.setup_states()
super().__init__() # FSMBase.__init__() requires states
self.setup_transitions()
@staticmethod
def parse_config(config_file):
with open(config_file, 'r') as ifile:
return yaml.safe_load(ifile)
def setup_states(self):
self.for_config_states_and_transitions_do(self.wrap_callbacks_with_subprocess_call)
self.states = [State(**state) for state in self.config['states']]
def setup_transitions(self):
self.for_config_states_and_transitions_do(self.subscribe_and_remove_predicates)
for transition in self.config['transitions']:
self.add_transition(**transition)
def for_config_states_and_transitions_do(self, what):
for array in ('states', 'transitions'):
for json_obj in self.config[array]:
what(json_obj)
@staticmethod
def wrap_callbacks_with_subprocess_call(json_obj):
topatch = ('on_enter', 'on_exit', 'prepare', 'before', 'after')
for key in json_obj:
if key in topatch:
json_obj[key] = partial(run_command_async, json_obj[key])
def subscribe_and_remove_predicates(self, json_obj):
if 'predicates' in json_obj:
for predicate in json_obj['predicates']:
self.subscribe_predicate(
json_obj['trigger'],
partial(
command_statuscode_is_zero,
predicate
)
)
with suppress(KeyError):
json_obj.pop('predicates')
def run_command_async(command, event):
Popen(command, shell=True)
def command_statuscode_is_zero(command):
return run(command, shell=True).returncode == 0
+1
View File
@@ -3,3 +3,4 @@ pyzmq==17.0.0
transitions==0.6.4
terminado==0.8.1
watchdog==0.8.3
PyYAML==3.12
+18 -12
View File
@@ -2,7 +2,6 @@ from os.path import dirname, realpath, join
from setuptools import setup, find_packages
here = dirname(realpath(__file__))
with open(join(here, 'VERSION'), 'r') as ifile:
@@ -10,14 +9,21 @@ with open(join(here, 'VERSION'), 'r') as ifile:
with open(join(here, 'requirements.txt'), 'r') as ifile:
requirements = ifile.read().splitlines()
setup(name='tfw',
version=version,
description='Avatao tutorial-framework',
url='https://github.com/avatao-content/baseimage-tutorial-framework',
author='Avatao.com Innovative Learning Kft.',
author_email='support@avatao.com',
license='custom',
packages=find_packages('lib'),
package_dir={'': 'lib'},
install_requires=requirements,
zip_safe=False)
setup(
name = 'tfw',
version = version,
description = 'Avatao tutorial-framework',
url = 'https://github.com/avatao-content/baseimage-tutorial-framework',
author = 'Avatao.com Innovative Learning Kft.',
author_email = 'support@avatao.com',
license = 'custom',
packages = find_packages('lib'),
package_dir = {'': 'lib'},
install_requires = requirements,
extras_require = {
'docs': [
'sphinx >= 1.7.0',
],
},
zip_safe = False,
)
+4
View File
@@ -0,0 +1,4 @@
[program:tfwserver]
user=root
directory=%(ENV_TFW_SERVER_DIR)s
command=python3 tfw_server.py
+3 -1
View File
@@ -1,8 +1,10 @@
[supervisord]
user=root
logfile = /tmp/supervisord.log
logfile=/dev/null
logfile_maxbytes=0
loglevel = debug
pidfile = /tmp/supervisord.pid
strip_ansi=true
[inet_http_server]
port = 127.0.0.1:%(ENV_TFW_SUPERVISOR_HTTP_PORT)s
+9
View File
@@ -0,0 +1,9 @@
from tornado.ioloop import IOLoop
from tfw.networking import TFWServer
from tfw.config import TFWENV
if __name__ == '__main__':
TFWServer().listen(TFWENV.WEB_PORT)
IOLoop.instance().start()