Compare commits

...
Author SHA1 Message Date
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
Kristóf Tóth 0038663bc6 Allow listening to inotify events in additional directories using IdeEH 2018-05-24 16:19:04 +02:00
Kristóf Tóth 6489e4452e Remove ambigous API for log reading (actions always reply with logs included) 2018-05-24 11:22:39 +02:00
Kristóf Tóth bfd1efa9f9 Append deployment logs to every response of ProcessManagingEventHandler 2018-05-24 11:20:00 +02:00
Kristóf Tóth 293d7972f1 Document serialization module 2018-05-11 14:36:56 +02:00
Kristóf Tóth d30226d55b Extend docstrings in server_connector 2018-05-11 14:07:59 +02:00
Kristóf Tóth 9e454a83d0 Make setup.py comply import conventions 2018-05-10 17:09:34 +02:00
Kristóf Tóth 34cadcf9b1 Extend .pylintrc disable bouquet 2018-05-10 17:08:40 +02:00
Kristóf Tóth a9d6794c4d Make setup.py parse requirements.txt 2018-05-10 17:08:31 +02:00
Kristóf Tóth 1fb13fefff Add setup.py to allow local pip installs of tfw 2018-05-10 16:20:58 +02:00
Kristóf Tóth d67d2c6bc9 Fix nginx redirecting to port 8888 on platform on /webservice (no trailing slash) 2018-05-09 17:01:29 +02:00
Kristóf Tóth d1305bc879 Add step_next trigger to LinearFSM allowing stateless stepping 2018-05-04 17:45:16 +02:00
Kristóf Tóth 06e17a6591 Raise comprehendable exception from webide when no files are present 2018-04-26 11:09:31 +02:00
Kristóf Tóth 8396c10a7b Rename TerminadoEventHandler 2018-04-26 10:13:02 +02:00
Kristóf Tóth 17c5ade3ce Fix broken HISTFILE due to bash and sh incompabilities 2018-04-26 10:04:56 +02:00
Kristóf Tóth f4310c855f Review README with Zsuuuuuzsi 2018-04-25 16:10:12 +02:00
Kristóf Tóth c831ba5ca4 Fix permissions on Python libraries besides TFW 2018-04-25 11:18:49 +02:00
Kristóf Tóth 3b30b333e5 Rename webide -> ide 2018-04-20 17:33:06 +02:00
Kristóf Tóth 9b85c19d40 Make messaging format defined as text to avoid JSON handcuffs 2018-04-19 11:15:19 +02:00
Kristóf Tóth 0b651abb7d Nicen some EventHandler API docstrings 2018-04-19 10:47:20 +02:00
Kristóf Tóth b1592e8ebb Use new f-strings where possible 2018-04-19 09:21:41 +02:00
Kristóf Tóth 3f36826597 Document TFW message format in README 2018-04-18 20:41:16 +02:00
Kristóf Tóth db1b008fc9 Fix EventHandlerBase.event_handler_callback broken logic 2018-04-18 20:16:56 +02:00
Kristóf Tóth 9d93d055b8 Refactor EventHandlerBase to be more general 2018-04-18 20:07:41 +02:00
Kristóf Tóth addd517ba7 Add a huge bunch of docstrings 2018-04-18 19:44:26 +02:00
Kristóf Tóth 690f9bb190 Move TriggerlessEventHandler logic to base class EventHandlerBase 2018-04-18 18:47:51 +02:00
Kristóf Tóth c909317ca0 Make TerminalCommands alias nonexistent commands only 2018-04-18 16:22:01 +02:00
Kristóf Tóth 25d039e84e Include LICENSE in image 2018-04-18 13:38:31 +02:00
Kristóf Tóth 117e7ffc18 Force bash history to be flushed before executing commands 2018-04-18 13:19:51 +02:00
Kristóf Tóth 8e8963b0f8 Simplify TFW baseimage contract 2018-04-17 17:44:38 +02:00
Kristóf Tóth b592a1625a Unify nginx configuration style 2018-04-17 15:14:02 +02:00
Kristóf Tóth 13e90dde41 Fix terminado not being included in nginx config 2018-04-17 15:13:27 +02:00
Kristóf Tóth 63c235bb41 Remove nginx component belonging to test repo 2018-04-17 15:00:43 +02:00
Kristóf Tóth 1061220f82 Add 'where to go next' section to README 2018-04-17 13:57:52 +02:00
Kristóf Tóth ca1278e0b4 Improve documentation 2018-04-17 13:46:32 +02:00
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
Bokros Bálint c05f9f6a28 Merge pull request #26 from avatao-content/niceimports
Niceimports
2018-04-06 16:24:37 +02:00
Kristóf Tóth 11743e830a Expose ServerUplinkConnector with a name understandable without TFW knowledge 2018-04-06 16:22:21 +02:00
Kristóf Tóth 0464acbdc4 Make error logs more explicit 2018-04-06 16:09:05 +02:00
Kristóf Tóth 64bd2f1ba0 Refactor handling of shared EventHandlerConnector ownership to nicen imports 2018-04-06 16:04:25 +02:00
Kristóf Tóth 1f2e1f0489 Refactor whole importing hell into a nice and clean API 2018-04-06 15:31:16 +02:00
Bokros Bálint b942c1cf53 Merge pull request #25 from avatao-content/usersep
Fix webide vulnerabilities
2018-04-05 17:22:04 +02:00
Kristóf Tóth bd84e4fe06 Improve code formatting in webide 2018-04-05 17:19:56 +02:00
Kristóf Tóth bc340e2e19 Enforce webide whitelisting 2018-04-05 17:16:41 +02:00
Kristóf Tóth b73b7307bd Improve webide whitelisting by enforcing stricter rules (fix symlink attack) 2018-04-05 17:01:50 +02:00
Kristóf Tóth b74ff39438 Implement directory whitelisting in webide 2018-04-05 14:43:39 +02:00
Kristóf Tóth 35421649c9 Extract spawning bash as user to a variable 2018-04-05 14:43:07 +02:00
Kristóf Tóth b54c91848b Remove tfw server init from baseimage (part of child contract now) 2018-04-04 17:48:49 +02:00
Kristóf Tóth 19f819c142 Implement TFW & challenge solver user separation 2018-04-04 17:43:18 +02:00
Kristóf Tóth 68fc4ca050 Fix nginx on readonly filesystems 2018-04-04 14:18:37 +02:00
Kristóf Tóth ade0936c6f Provide high-level documentation of event handlers and FSM 2018-04-03 17:41:19 +02:00
Kristóf Tóth d072f9ee9c Extend documentation with framework basics 2018-04-03 16:20:12 +02:00
Kristóf Tóth eaa0eee4ca Add LICENSE file and include copyright notice in source files 2018-04-03 14:49:14 +02:00
Kristóf Tóth b8bcda134c Start rewriting README.md 2018-03-30 22:37:36 +02:00
75 changed files with 1838 additions and 551 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/bombay-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 = 120
disable = missing-docstring, too-few-public-methods, invalid-name
+17 -16
View File
@@ -11,7 +11,8 @@ RUN curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
libzmq5 \ libzmq5 \
nginx \ nginx \
gettext-base &&\ gettext-base &&\
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/* &&\
ln -sf /bin/bash /bin/sh
COPY requirements.txt /tmp COPY requirements.txt /tmp
RUN pip3 install -r /tmp/requirements.txt RUN pip3 install -r /tmp/requirements.txt
@@ -26,7 +27,7 @@ ENV TFW_PUBLIC_PORT=8888 \
EXPOSE ${TFW_PUBLIC_PORT} EXPOSE ${TFW_PUBLIC_PORT}
ENV PYTHONPATH="/usr/local/lib/" \ ENV PYTHONPATH="/usr/local/lib" \
TFW_SUPERVISOR_HTTP_URI="http://localhost:${TFW_SUPERVISOR_HTTP_PORT}" \ TFW_SUPERVISOR_HTTP_URI="http://localhost:${TFW_SUPERVISOR_HTTP_PORT}" \
TFW_SUPERVISORD_CONF="/etc/supervisor/supervisord.conf" \ TFW_SUPERVISORD_CONF="/etc/supervisor/supervisord.conf" \
TFW_SUPERVISORD_COMPONENTS="/etc/supervisor/conf" \ TFW_SUPERVISORD_COMPONENTS="/etc/supervisor/conf" \
@@ -39,30 +40,30 @@ ENV PYTHONPATH="/usr/local/lib/" \
TFW_HISTFILE="/home/${AVATAO_USER}/.bash_history" \ TFW_HISTFILE="/home/${AVATAO_USER}/.bash_history" \
PROMPT_COMMAND="history -a" PROMPT_COMMAND="history -a"
RUN echo "shopt -s cmdhist\n" \ COPY bashrc /tmp
"shopt -s histappend\n" \ RUN echo "export HISTFILE=${TFW_HISTFILE}" >> /tmp/bashrc &&\
"unset HISTCONTROL\n" \ cat /tmp/bashrc >> /home/${AVATAO_USER}/.bashrc
"export HISTFILE=$TFW_HISTFILE\n" \
"export HISTFILESIZE=1000\n" \
"export HISTSIZE=1000\n" \
'PROMPT_COMMAND="history -a"\n' \
>> /home/${AVATAO_USER}/.bashrc
COPY supervisor/supervisord.conf ${TFW_SUPERVISORD_CONF} COPY supervisor/supervisord.conf ${TFW_SUPERVISORD_CONF}
COPY nginx/nginx.conf ${TFW_NGINX_CONF} COPY nginx/nginx.conf ${TFW_NGINX_CONF}
COPY nginx/default.conf ${TFW_NGINX_DEFAULT} COPY nginx/default.conf ${TFW_NGINX_DEFAULT}
COPY lib ${TFW_LIB_DIR} COPY nginx/components/ ${TFW_NGINX_COMPONENTS}
COPY lib LICENSE ${TFW_LIB_DIR}
ONBUILD ARG BUILD_CONTEXT="." RUN for dir in "${TFW_LIB_DIR}"/{tfw,tao,envvars} "/etc/nginx" "/etc/supervisor"; do \
chown -R root:root "$dir" && chmod -R 700 "$dir"; \
done
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/ ${TFW_NGINX_COMPONENTS}
ONBUILD COPY ${BUILD_CONTEXT}/supervisor/components/ ${TFW_SUPERVISORD_COMPONENTS} ONBUILD COPY ${BUILD_CONTEXT}/supervisor/ ${TFW_SUPERVISORD_COMPONENTS}
ONBUILD RUN chown -R ${AVATAO_USER} /var/log/nginx /var/lib/nginx &&\ ONBUILD RUN for f in "${TFW_NGINX_DEFAULT}" ${TFW_NGINX_COMPONENTS}/*.conf; do \
for f in "${TFW_NGINX_DEFAULT}" ${TFW_NGINX_COMPONENTS}/*.conf; do \
envsubst "$(printenv | cut -d= -f1 | grep TFW_ | sed -e 's/^/$/g')" < $f > $f~ && mv $f~ $f ;\ envsubst "$(printenv | cut -d= -f1 | grep TFW_ | sed -e 's/^/$/g')" < $f > $f~ && mv $f~ $f ;\
done done
ONBUILD VOLUME ["/etc/nginx", "/var/lib/nginx", "/var/log/nginx"]
ONBUILD COPY ${BUILD_CONTEXT}/frontend /data/ ONBUILD COPY ${BUILD_CONTEXT}/frontend /data/
ONBUILD RUN test -z "${NOFRONTEND}" && cd /data && yarn install --frozen-lockfile || : ONBUILD RUN test -z "${NOFRONTEND}" && cd /data && yarn install --frozen-lockfile || :
+12
View File
@@ -0,0 +1,12 @@
AVATAO CONFIDENTIAL
Copyright (C) 2018 Avatao.com Innovative Learning Kft.
All Rights Reserved.
All source code, configuration files and documentation contained herein
is, and remains the exclusive property of Avatao.com Innovative Learning Kft.
The intellectual and technical concepts contained herein are proprietary
to Avatao.com Innovative Learning Kft. and are protected by trade secret
or copyright law. Dissemination of this information or reproduction of
this material is strictly forbidden unless prior written permission is
obtained from Avatao.com Innovative Learning Kft.
+78 -9
View File
@@ -1,16 +1,85 @@
Tutorial Framework # baseimage-tutorial-framework
==================
## Building and running with magic starter This is the beating heart of TFW the Docker baseimage containing the internals of the framework.
Execute the `tfw_magic_start.sh` script and it will automagically run a backend instance in Docker and serve the Angular frontend locally. Every tutorial-framework based challenge has a `solvable` Docker image based on this one: their `Dockerfile`s begin with `FROM eu.gcr.io/avatao-challengestore/tutorial-framework`.
Note that TFW is not avaliable on Docker Hub due to legal reasons and is only accessible through local builds (don't worry, we've got you covered with build scripts in the [test-tutorial-framework](https://github.com/avatao-content/test-tutorial-framework) repo).
This requires the `test-tutorial-framework` (Docker image `FROM baseimage-tutorial-framework`) and `frontend-tutorial-framework` repositories to have a common parent folder. This document explains the general concepts of TFW and should be the first thing you read before getting started with development.
## Building with Docker For more on building and running you should check the [test-tutorial-framework](https://github.com/avatao-content/test-tutorial-framework) repo.
Simply issue `docker build -t baseimage-tutorial-framework .` in the project root. ## The framework
## Creating child images The goal of the tutorial-framework is to help content developers in creating interactive tutorials for the Avatao platform.
Documentation in progress... To make this possible TFW implements a robust messaging system and provides several pre-written components built upon it, such as a file editor and a terminal (both running in your browser).
The foundation of the whole framework is the messaging system connecting the frontend with the backend.
Frontend components use websockets to connect to the TFW server, to which you can hook several *event handlers* defining how to handle specific messages.
![TFW architecture](docs/tfw_architecture.png)
### 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.
Event handler allow you to define actions triggered on the backend when the user presses a button on the frontend or moves the cursor to a specific area, etc.
Event handlers use ZeroMQ to connect to the framework. Due to this they are as loosely-coupled as possible: usually they are running in separate processes and only communicate with TFW through ZMQ.
Our pre-made event handlers are written in Python3, but you can write event handlers in any language that has ZeroMQ bindings (this means virtually any language).
This makes the framework really flexible: you can demonstrate the concepts you want to in any language while using the same set of tools provided by TFW.
Inside Avatao this means that any of the content teams can use the framework with ease.
### FSM
Another unique feature of the framework is the FSM finite state machine representing the state of your challenge.
This allows you to track users progressing with the tasks you've defined for them to complete.
For instance, you could represent whether the user managed to create a malicious user with a state called `user_registered` and subscribe callbacks to events regarding that state (like entering or leaving).
You could create challenges that can be completed in several different ways: imagine a state called `challenge_complete`, which indicates if the challenge is completed. Several series of actions (triggers) could lead to this state.
This enables you to guide your users through the experience you've envisioned with your tutorial.
We can provide a whole new level of interactivity in our challenges because we know what the user is doing.
This includes context-dependent hints and the automatic typing of commands to a terminal.
### Frontend
Note that our frontend implementation is written in Angular. It is maintained and documented in the [frontend-tutorial-framework](https://github.com/avatao-content/frontend-tutorial-framework) repository.
### Messaging format
The framework uses JSON messages internally and in exposed APIs as well.
These messages must comply with some rules.
Don't worry, we are not too fond of rules around these parts.
The TFW message format:
```text
{
"key: "some identifier used for addressing",
"data":
{
...
JSON object carrying anything, preferably cats
...
},
"trigger": "FSM action"
}
```
- The `key` field is used by TFW for addressing and every message must have one (it can be an empty string though)
- The `data` object can contain anything you might want to send
- The `trigger` key is an optional field that triggers an FSM action with that name from the current state (whatever that might be)
## Where to go next
Most of the components you need have docstrings included (hang on tight, this is work in progress) refer to them for usage info.
In the `docs` folder you can find our Sphinx-based API 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.
+1 -1
View File
@@ -1 +1 @@
egyptianmau bombay
+20
View File
@@ -0,0 +1,20 @@
_postcmd()
{
AT_PROMPT=1
history -a
}
PROMPT_COMMAND="_postcmd"
_precmd()
{
[ -z "$AT_PROMPT" ] && return
unset AT_PROMPT
history -a
}
trap _precmd DEBUG
shopt -s cmdhist
shopt -s histappend
unset HISTCONTROL
export HISTFILESIZE=1000
export HISTSIZE=1000
+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 = 'bombay'
# -- 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.

After

Width:  |  Height:  |  Size: 40 KiB

-9
View File
@@ -1,9 +0,0 @@
from collections import namedtuple
from os import environ
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)
+25
View File
@@ -0,0 +1,25 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from collections import namedtuple
from os import environ
from tfw.decorators import lazy_property
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
View File
@@ -0,0 +1,2 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
+4 -1
View File
@@ -1 +1,4 @@
from .envvars import * # pylint: disable=wildcard-import # Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from .envvars import TAOENV
+5 -2
View File
@@ -1,3 +1,6 @@
from envvars import prefixed_envvars_to_namedtuple # Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
TAOENV = prefixed_envvars_to_namedtuple('AVATAO_', 'taoenvtuple') from envvars import LazyEnvironment
TAOENV = LazyEnvironment('AVATAO_', 'taoenvtuple').environment
+6
View File
@@ -0,0 +1,6 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from .event_handler_base import EventHandlerBase, TriggeredEventHandler
from .fsm_base import FSMBase
from .linear_fsm import LinearFSM
+10
View File
@@ -0,0 +1,10 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from .directory_monitoring_event_handler import DirectoryMonitoringEventHandler
from .process_managing_event_handler import ProcessManagingEventHandler
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
@@ -1 +0,0 @@
from .rate_limiter import RateLimiter
+18 -18
View File
@@ -1,30 +1,27 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
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.server_connector 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, ide_key, directories):
self.observer = Observer() self.eventhandler = IdeReloadWatchdogEventHandler(ide_key)
self.eventhandler = WebideReloadWatchdogEventHandler() for directory in directories:
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
@@ -46,9 +43,10 @@ class DirectoryMonitor:
self.directorymonitor.resume() self.directorymonitor.resume()
class WebideReloadWatchdogEventHandler(FileSystemWatchdogEventHandler): class IdeReloadWatchdogEventHandler(FileSystemWatchdogEventHandler):
def __init__(self): def __init__(self, ide_key):
super().__init__() super().__init__()
self.ide_key = ide_key
self.uplink = ServerUplinkConnector() self.uplink = ServerUplinkConnector()
self._paused = False self._paused = False
self.ignore = 0 self.ignore = 0
@@ -67,8 +65,10 @@ 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({
self.uplink.send(key, {'data': {'command': 'reload'}}) 'key': self.ide_key,
'data': {'command': 'reload'}
})
def with_monitor_paused(fun): def with_monitor_paused(fun):
@@ -1,22 +1,33 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from os.path import isdir, exists from os.path import isdir, exists
from tfw.components.directory_monitor import DirectoryMonitor from tfw import EventHandlerBase
from tfw.event_handler_base import TriggerlessEventHandler
from tfw.config.logs import logging from tfw.config.logs import logging
from tfw.mixins import MonitorManagerMixin
from .directory_monitor import DirectoryMonitor
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class DirectoryMonitoringEventHandler(TriggerlessEventHandler): class DirectoryMonitoringEventHandler(EventHandlerBase, 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.reload_monitor() self,
self.commands = {'pause': self.pause, DirectoryMonitor,
key,
self._directory
)
self.commands = {
'pause': self.pause,
'resume': self.resume, 'resume': self.resume,
'ignore': self.ignore, 'ignore': self.ignore,
'selectdir': self.selectdir} 'selectdir': self.selectdir
}
@property @property
def directory(self): def directory(self):
@@ -28,20 +39,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
+41 -17
View File
@@ -1,13 +1,15 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from os.path import dirname from os.path import dirname
from re import findall 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.callback_mixin import CallbackMixin from tfw.mixins import CallbackMixin, ObserverMixin
from tfw.components.decorators.rate_limiter import RateLimiter from tfw.decorators import RateLimiter
class CallbackEventHandler(PatternMatchingEventHandler, ABC): class CallbackEventHandler(PatternMatchingEventHandler, ABC):
@@ -21,17 +23,30 @@ class CallbackEventHandler(PatternMatchingEventHandler, ABC):
callback() callback()
class HistoryMonitor(CallbackMixin, ABC): class HistoryMonitor(CallbackMixin, ObserverMixin, ABC):
"""
Abstract class capable of monitoring and parsing a history file such as
bash HISTFILEs. Monitoring means detecting when the file was changed and
notifying subscribers about new content in the file.
This is useful for monitoring CLI sessions.
To specify a custom HistoryMonitor inherit from this class and override the
command pattern property and optionally the sanitize_command method.
See examples below.
"""
def __init__(self, histfile): def __init__(self, histfile):
CallbackMixin.__init__(self)
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(
self.observer.schedule(CallbackEventHandler([self.histfile], CallbackEventHandler(
[self.histfile],
self._fetch_history, self._fetch_history,
self._invoke_callbacks), self._invoke_callbacks
dirname(self.histfile)) ),
dirname(self.histfile)
)
@property @property
def history(self): def history(self):
@@ -42,7 +57,10 @@ class HistoryMonitor(CallbackMixin, ABC):
with open(self.histfile, 'r') as ifile: with open(self.histfile, 'r') as ifile:
pattern = compileregex(self.command_pattern) pattern = compileregex(self.command_pattern)
data = ifile.read() 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 @property
@abstractmethod @abstractmethod
@@ -57,15 +75,17 @@ 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):
"""
HistoryMonitor for monitoring bash CLI sessions.
This requires the following to be set in bash
(note that this is done automatically by TFW):
PROMPT_COMMAND="history -a"
shopt -s cmdhist
shopt -s histappend
unset HISTCONTROL
"""
@property @property
def command_pattern(self): def command_pattern(self):
return r'.+' return r'.+'
@@ -75,6 +95,10 @@ class BashMonitor(HistoryMonitor):
class GDBMonitor(HistoryMonitor): class GDBMonitor(HistoryMonitor):
"""
HistoryMonitor to monitor GDB sessions.
For this to work "set trace-commands on" must be set in GDB.
"""
@property @property
def command_pattern(self): def command_pattern(self):
return r'(?<=\n)\+(.+)\n' return r'(?<=\n)\+(.+)\n'
+254
View File
@@ -0,0 +1,254 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from os.path import isfile, join, relpath, exists, isdir, realpath
from glob import glob
from fnmatch import fnmatchcase
from collections import Iterable
from tfw import EventHandlerBase
from tfw.mixins import MonitorManagerMixin
from tfw.config.logs import logging
from .directory_monitor import DirectoryMonitor
LOG = logging.getLogger(__name__)
class FileManager: # pylint: disable=too-many-instance-attributes
def __init__(self, working_directory, allowed_directories, selected_file=None, exclude=None):
self._exclude, self.exclude = None, exclude
self._allowed_directories, self.allowed_directories = None, allowed_directories
self._workdir, self.workdir = None, working_directory
self._filename, self.filename = None, selected_file or self.files[0]
@property
def exclude(self):
return self._exclude
@exclude.setter
def exclude(self, exclude):
if exclude is None:
return
if not isinstance(exclude, Iterable):
raise TypeError('Exclude must be Iterable!')
self._exclude = exclude
@property
def workdir(self):
return self._workdir
@workdir.setter
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_allowed_dir(directory):
raise EnvironmentError(f'Directory "{directory}" is not allowed!')
self._workdir = directory
@property
def allowed_directories(self):
return self._allowed_directories
@allowed_directories.setter
def allowed_directories(self, directories):
self._allowed_directories = directories
@property
def filename(self):
return self._filename
@filename.setter
def filename(self, filename):
if filename not in self.files:
raise EnvironmentError('No such file in workdir!')
self._filename = filename
@property
def files(self):
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):
with open(self._filepath(self.filename), 'r', errors='surrogateescape') as ifile:
return ifile.read()
@file_contents.setter
def file_contents(self, value):
with open(self._filepath(self.filename), 'w', errors='surrogateescape') as ofile:
ofile.write(value)
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
)
def _filepath(self, filename):
return join(self._workdir, filename)
def _relpath(self, filename):
return relpath(self._filepath(filename), start=self._workdir)
class IdeEventHandler(EventHandlerBase, MonitorManagerMixin):
# pylint: disable=too-many-arguments
"""
Event handler implementing the backend of our browser based IDE.
By default all files in the directory specified in __init__ are displayed
on the fontend. Note that this is a stateful component.
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
a command to be executed.
The API of each command is documented in their respective handler.
"""
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 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.)
"""
super().__init__(key)
try:
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}"!'
)
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
}
def read(self, data):
"""
Read the currently selected file.
:return dict: message with the contents of the file in data['content']
"""
try:
data['content'] = self.filemanager.file_contents
except PermissionError:
data['content'] = 'You have no permission to open that file :('
except FileNotFoundError:
data['content'] = 'This file was removed :('
except Exception: # pylint: disable=broad-except
data['content'] = 'Failed to read file :('
return data
def write(self, data):
"""
Overwrites a file with the desired string.
:param data: TFW message data containing key 'content'
(new file content)
"""
self.monitor.ignore = self.monitor.ignore + 1
try:
self.filemanager.file_contents = data['content']
except Exception: # pylint: disable=broad-except
LOG.exception('Error writing file!')
del data['content']
return data
def select(self, data):
"""
Selects a file from 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']
except EnvironmentError:
LOG.exception('Failed to select file "%s"', data['filename'])
return data
def select_dir(self, data):
"""
Select a new working directory to display files from.
: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']
self.reload_monitor()
try:
self.filemanager.filename = self.filemanager.files[0]
self.read(data)
except IndexError:
data['content'] = 'No files in this directory :('
except EnvironmentError as err:
LOG.error('Failed to select directory "%s". Reason: %s', data['directory'], str(err))
return data
def exclude(self, data):
"""
Overwrite list of excluded files
: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'])
except TypeError:
LOG.error('Exclude must be Iterable!')
return data
def attach_fileinfo(self, data):
"""
Basic information included in every response to the frontend.
"""
data['filename'] = self.filemanager.filename
data['files'] = self.filemanager.files
data['directory'] = self.filemanager.workdir
def handle_event(self, message):
try:
data = message['data']
message['data'] = self.commands[data['command']](data)
self.attach_fileinfo(data)
return message
except KeyError:
LOG.error('IGNORING MESSAGE: Invalid message received: %s', message)
def cleanup(self):
self.monitor.stop()
+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()
-1
View File
@@ -1 +0,0 @@
from .supervisor_mixin import SupervisorMixin
@@ -1,17 +0,0 @@
from functools import partial
class CallbackMixin:
def __init__(self):
self._callbacks = []
def subscribe_callback(self, callback, *args, **kwargs):
fun = partial(callback, *args, **kwargs)
self._callbacks.append(fun)
def unsubscribe_callback(self, callback):
self._callbacks.remove(callback)
def _execute_callbacks(self, *args, **kwargs):
for callback in self._callbacks:
callback(*args, **kwargs)
@@ -1,27 +0,0 @@
import xmlrpc.client
from xmlrpc.client import Fault as SupervisorFault
from contextlib import suppress
from os import remove
from tfw.config import TFWENV
class SupervisorMixin:
supervisor = xmlrpc.client.ServerProxy(TFWENV.SUPERVISOR_HTTP_URI).supervisor
def stop_process(self, process_name):
with suppress(SupervisorFault):
self.supervisor.stopProcess(process_name)
def start_process(self, process_name):
self.supervisor.startProcess(process_name)
def read_log(self, process_name):
logs = self.supervisor.readProcessStderrLog(process_name, 0, 0)
remove(self.supervisor.getProcessInfo(process_name)['stderr_logfile'])
self.supervisor.clearProcessLogs(process_name)
return logs
def restart_process(self, process_name):
self.stop_process(process_name)
self.start_process(process_name)
@@ -1,40 +1,65 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from xmlrpc.client import Fault as SupervisorFault from xmlrpc.client import Fault as SupervisorFault
from tfw.event_handler_base import TriggerlessEventHandler from tfw import EventHandlerBase
from tfw.components.mixins import SupervisorMixin from tfw.mixins import SupervisorMixin, SupervisorLogMixin
from tfw.components.directory_monitor import with_monitor_paused
from tfw.config.logs import logging from tfw.config.logs import logging
from .directory_monitor import with_monitor_paused
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class ProcessManager(SupervisorMixin): class ProcessManager(SupervisorMixin, SupervisorLogMixin):
def __init__(self): def __init__(self):
self.commands = {'start': self.start_process, self.commands = {
'start': self.start_process,
'stop': self.stop_process, 'stop': self.stop_process,
'restart': self.restart_process, 'restart': self.restart_process
'readlog': self.read_log} }
def __call__(self, command, process_name): def __call__(self, command, process_name):
return self.commands[command](process_name) return self.commands[command](process_name)
class ProcessManagingEventHandler(TriggerlessEventHandler): class ProcessManagingEventHandler(EventHandlerBase):
def __init__(self, key, dirmonitor=None): """
Event handler that can manage processes managed by supervisor.
This EventHandler accepts messages that have a data['command'] key specifying
a command to be executed.
Every message must contain a data['process_name'] field with the name of the
process to manage. This is the name specified in supervisor config files like so:
[program:someprogram]
Commands available: start, stop, restart, readlog
(the names are as self-documenting as it gets)
"""
def __init__(self, key, dirmonitor=None, log_tail=0):
super().__init__(key) super().__init__(key)
self.key = key self.key = key
self.monitor = dirmonitor self.monitor = dirmonitor
self.processmanager = ProcessManager() self.processmanager = ProcessManager()
self.log_tail = log_tail
@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']
try:
self.processmanager(data['command'], data['process_name']) 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 return message
except KeyError: except KeyError:
LOG.error('IGNORING MESSAGE: Invalid message received: %s', message) 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(message['data']['process_name'])
return message
@@ -1,163 +0,0 @@
from os.path import isfile, join, relpath, exists, isdir
from glob import glob
from fnmatch import fnmatchcase
from collections import Iterable
from tfw.event_handler_base import TriggerlessEventHandler
from tfw.components.directory_monitor import DirectoryMonitor
from tfw.config.logs import logging
LOG = logging.getLogger(__name__)
class FileManager:
def __init__(self, working_directory, selected_file=None, exclude=None):
self._exclude, self.exclude = None, exclude
self._workdir, self.workdir = None, working_directory
self._filename, self.filename = None, selected_file or self.files[0]
@property
def exclude(self):
return self._exclude
@exclude.setter
def exclude(self, exclude):
if exclude is None:
return
if not isinstance(exclude, Iterable):
raise TypeError('Exclude must be Iterable!')
self._exclude = exclude
@property
def workdir(self):
return self._workdir
@workdir.setter
def workdir(self, directory):
if not exists(directory) or not isdir(directory):
raise EnvironmentError('"{}" is not a directory!'.format(directory))
self._workdir = directory
@property
def filename(self):
return self._filename
@filename.setter
def filename(self, filename):
if not filename in self.files:
raise EnvironmentError('No such file in workdir!')
self._filename = filename
@property
def files(self):
return [self._relpath(file) for file in glob(join(self._workdir, '**/*'), recursive=True)
if isfile(file) and
not any(fnmatchcase(file, blacklisted) for blacklisted in self.exclude)]
@property
def file_contents(self):
with open(self._filepath(self.filename), 'r', errors='surrogateescape') as ifile:
return ifile.read()
@file_contents.setter
def file_contents(self, value):
with open(self._filepath(self.filename), 'w', errors='surrogateescape') as ofile:
ofile.write(value)
def _filepath(self, filename):
return join(self._workdir, filename)
def _relpath(self, filename):
return relpath(self._filepath(filename), start=self._workdir)
class SourceCodeEventHandler(TriggerlessEventHandler):
def __init__(self, key, directory, selected_file=None, exclude=None):
super().__init__(key)
self.filemanager = FileManager(directory, selected_file=selected_file, exclude=exclude)
self.commands = {'read': self.read,
'write': self.write,
'select': self.select,
'selectdir': self.select_dir,
'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):
try:
data['content'] = self.filemanager.file_contents
except PermissionError:
data['content'] = 'You have no permission to open that file :('
except FileNotFoundError:
data['content'] = 'This file was removed :('
except Exception: # pylint: disable=broad-except
data['content'] = 'Failed to read file :('
return data
def write(self, data):
self.monitor.ignore = self.monitor.ignore + 1
try:
self.filemanager.file_contents = data['content']
except Exception: # pylint: disable=broad-except
LOG.exception('Error writing file!')
del data['content']
return data
def select(self, data):
try:
self.filemanager.filename = data['filename']
except EnvironmentError:
LOG.exception('Failed to select file "%s"', data['filename'])
return data
def select_dir(self, data):
try:
self.filemanager.workdir = data['directory']
self.reload_monitor()
try:
self.filemanager.filename = self.filemanager.files[0]
self.read(data)
except IndexError:
data['content'] = 'No files in this directory :('
except EnvironmentError:
LOG.error('Failed to select directory "%s"', data['directory'])
return data
def exclude(self, data):
try:
self.filemanager.exclude = list(data['exclude'])
except TypeError:
LOG.error('Exclude must be Iterable!')
return data
def attach_fileinfo(self, data):
data['filename'] = self.filemanager.filename
data['files'] = self.filemanager.files
data['directory'] = self.filemanager.workdir
def handle_event(self, key, message):
try:
data = message['data']
message['data'] = self.commands[data['command']](data)
self.attach_fileinfo(data)
return message
except KeyError:
LOG.error('IGNORING MESSAGE: Invalid message received: %s', message)
def cleanup(self):
self.monitor.stop()
@@ -1,44 +0,0 @@
from tfw.components.terminado_mini_server import TerminadoMiniServer
from tfw.event_handler_base import TriggerlessEventHandler
from tfw.config import TFWENV
from tfw.config.logs import logging
LOG = logging.getLogger(__name__)
class TerminadoEventHandler(TriggerlessEventHandler):
def __init__(self, key, monitor):
super().__init__(key)
self.working_directory = TFWENV.TERMINADO_DIR
self._historymonitor = monitor
self.terminado_server = TerminadoMiniServer('/terminal', TFWENV.TERMINADO_PORT, TFWENV.TERMINADO_WD, ['bash'])
self.commands = {'write': self.write,
'read': self.read}
if self._historymonitor:
self._historymonitor.watch()
self.terminado_server.listen()
@property
def historymonitor(self):
return self._historymonitor
def handle_event(self, key, message):
LOG.debug('TerminadoEventHandler received event: %s', message)
try:
message['data'] = self.commands[message['data']['command']](message['data'])
return message
except KeyError:
LOG.error('IGNORING MESSAGE: Invalid message received: %s', message)
def write(self, data):
self.terminado_server.pty.write(data['shellcmd'])
def read(self, data):
data['count'] = int(data.get('count', 1))
if self.historymonitor:
data['history'] = self.historymonitor.history[-data['count']:]
return data
def cleanup(self):
if self.historymonitor:
self.historymonitor.stop()
+15 -7
View File
@@ -1,3 +1,6 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
from tornado.web import Application from tornado.web import Application
from terminado import TermSocket, SingleTermManager from terminado import TermSocket, SingleTermManager
@@ -11,15 +14,15 @@ LOG = logging.getLogger(__name__)
class TerminadoMiniServer: class TerminadoMiniServer:
def __init__(self, url, port, workdir, shellcmd): def __init__(self, url, port, workdir, shellcmd):
self.port = port self.port = port
self._term_manager = SingleTermManager(shell_command=shellcmd, self._term_manager = SingleTermManager(
term_settings={'cwd': workdir}) shell_command=shellcmd,
self.application = Application( term_settings={'cwd': workdir}
[( )
self.application = Application([(
url, url,
TerminadoMiniServer.ResetterTermSocket, TerminadoMiniServer.ResetterTermSocket,
{'term_manager': self._term_manager} {'term_manager': self._term_manager}
)] )])
)
@property @property
def term_manager(self): def term_manager(self):
@@ -43,5 +46,10 @@ class TerminadoMiniServer:
if __name__ == '__main__': if __name__ == '__main__':
LOG.info('Terminado Mini Server listening on %s', TFWENV.TERMINADO_PORT) 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() IOLoop.instance().start()
+71
View File
@@ -0,0 +1,71 @@
# 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 shlex import split
from tfw.config.logs import logging
LOG = logging.getLogger(__name__)
class TerminalCommands(ABC):
"""
A class you can use to define hooks for terminal commands. This means that you can
have python code executed when the user enters a specific command to the terminal on
our frontend.
To receive events you need to subscribe TerminalCommand.callback to a HistoryMonitor
instance.
Inherit from this class and define methods which start with "command\_". When the user
executes the command specified after the underscore, your method will be invoked. All
such commands must expect the parameter \*args which will contain the arguments of the
command.
For example to define a method that runs when someone starts vim in the terminal
you have to define a method like: "def command_vim(self, \*args)"
You can also use this class to create new commands similarly.
"""
def __init__(self, bashrc=None):
self._command_method_regex = r'^command_(.+)$'
self.command_implemetations = self._build_command_to_implementation_dict()
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 = 'type {0} &> /dev/null || alias {0}="{0} &> /dev/null"\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 = split(history[-1])
command = parts[0]
if command in self.command_implemetations.keys():
try:
self.command_implemetations[command](*parts[1:])
except Exception: # pylint: disable=broad-except
LOG.exception('Command "%s" failed:', command)
@@ -0,0 +1,87 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from tfw import EventHandlerBase
from tfw.config import TFWENV
from tfw.config.logs import logging
from tao.config import TAOENV
from .terminado_mini_server import TerminadoMiniServer
LOG = logging.getLogger(__name__)
class TerminalEventHandler(EventHandlerBase):
"""
Event handler responsible for managing terminal sessions for frontend xterm
sessions to connect to. You need to instanciate this in order for frontend
terminals to work.
This EventHandler accepts messages that have a data['command'] key specifying
a command to be executed.
The API of each command is documented in their respective handler.
"""
def __init__(self, key, monitor):
"""
:param key: key this EventHandler listens to
:param monitor: tfw.components.HistoryMonitor instance to read command history from
"""
super().__init__(key)
self.working_directory = TFWENV.TERMINADO_DIR
self._historymonitor = monitor
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
}
if self._historymonitor:
self._historymonitor.watch()
self.terminado_server.listen()
@property
def historymonitor(self):
return self._historymonitor
def handle_event(self, message):
LOG.debug('TerminadoEventHandler received event: %s', message)
try:
data = message['data']
message['data'] = self.commands[data['command']](data)
return message
except KeyError:
LOG.error('IGNORING MESSAGE: Invalid message received: %s', message)
def write(self, data):
"""
Writes a string to the terminal session (on the pty level).
Useful for pre-typing and executing commands for the user.
:param data: TFW message data containing 'value'
(command to be written to the pty)
"""
self.terminado_server.pty.write(data['value'])
def read(self, data):
"""
Reads the history of commands executed.
: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:
data['history'] = self.historymonitor.history[-data['count']:]
return data
def cleanup(self):
if self.historymonitor:
self.historymonitor.stop()
+4 -1
View File
@@ -1 +1,4 @@
from .envvars import * # Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from .envvars import TFWENV
+5 -2
View File
@@ -1,3 +1,6 @@
from envvars import prefixed_envvars_to_namedtuple # Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
TFWENV = prefixed_envvars_to_namedtuple('TFW_', 'tfwenvtuple') from envvars import LazyEnvironment
TFWENV = LazyEnvironment('TFW_', 'tfwenvtuple').environment
+3
View File
@@ -1,3 +1,6 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
import logging import logging
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
+5
View File
@@ -0,0 +1,5 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# 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
@@ -1,3 +1,6 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from functools import wraps from functools import wraps
from time import time, sleep from time import time, sleep
+68 -49
View File
@@ -1,85 +1,104 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from tfw.networking.serialization import deserialize_all from tfw.networking import deserialize_tfw_msg
from tfw.networking.event_handlers.server_connector import ServerConnector from tfw.networking.event_handlers import ServerConnector
class EventHandlerBase(ABC): class EventHandlerBase(ABC):
"""
Abstract base class for all Python based EventHandlers. Useful implementation template
for other languages.
Derived classes must implement the handle_event() method
"""
def __init__(self, key): def __init__(self, key):
self.server_connector = ServerConnector() self.server_connector = ServerConnector()
self.key = key self.key = key
self.subscriptions = set() self.subscribe(self.key, 'reset')
self.subscribe(self.key)
self.subscribe('reset')
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) """
response = self.dispatch_handling(key, message) Callback that is invoked when receiving a message.
if response is None: Dispatches messages to handler methods and sends
return a response back in case the handler returned something.
self.server_connector.send(key, response) 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)
@abstractmethod def dispatch_handling(self, message):
def dispatch_handling(self, key, message): """
raise NotImplementedError Used to dispatch messages to their specific handlers.
"""
def _dispatch_handling(self, key, message): if message['key'] != 'reset':
# pylint: disable=no-else-return return self.handle_event(message)
if key != 'reset':
return self.handle_event(key, message)
else:
return self.handle_reset(message) return self.handle_reset(message)
@abstractmethod @abstractmethod
def handle_event(self, key, message): def handle_event(self, message):
"""
Abstract method that implements the handling of messages.
:param message: the message received
"""
raise NotImplementedError raise NotImplementedError
def handle_reset(self, message): def handle_reset(self, message):
# pylint: disable=unused-argument,no-self-use # pylint: disable=unused-argument,no-self-use
"""
Usually 'reset' events receive some sort of special treatment.
:param message: the message received
"""
return None return None
def cleanup(self): def subscribe(self, *keys):
pass """
Subscribe this EventHandler to receive events for given keys.
Note that you can subscribe to the same key several times in which
case you will need to unsubscribe multiple times in order to stop
receiving events.
def message_other(self, key, data): :param keys: list of keys to subscribe to
message = { """
'key': key, for key in keys:
'data': data
}
self.server_connector.send(key, message)
def subscribe(self, key):
if key not in self.subscriptions:
self.subscriptions.add(key)
self.server_connector.subscribe(key) self.server_connector.subscribe(key)
def unsubscribe(self, key): def unsubscribe(self, *keys):
try: """
self.subscriptions.remove(key) Unsubscribe this eventhandler from the given keys.
:param keys: list of keys to unsubscribe from
"""
for key in keys:
self.server_connector.unsubscribe(key) self.server_connector.unsubscribe(key)
except KeyError:
def cleanup(self):
"""
Perform cleanup actions such as releasing database
connections and stuff like that.
"""
pass pass
def unsubscribe_all(self):
for sub in self.subscriptions:
self.server_connector.unsubscribe(key=sub)
self.subscriptions.clear()
class TriggerlessEventHandler(EventHandlerBase, ABC):
# pylint: disable=abstract-method
def dispatch_handling(self, key, message):
return self._dispatch_handling(key, message)
class TriggeredEventHandler(EventHandlerBase, ABC): class TriggeredEventHandler(EventHandlerBase, ABC):
# pylint: disable=abstract-method # pylint: disable=abstract-method
"""
Abstract base class for EventHandlers which are only triggered in case
TFWServer has successfully triggered an FSM step defined in __init__.
"""
def __init__(self, key, trigger): def __init__(self, key, trigger):
super().__init__(key) super().__init__(key)
self.trigger = trigger self.trigger = trigger
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 super().dispatch_handling(message)
return None return None
+15 -4
View File
@@ -1,23 +1,34 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from typing import List from typing import List
from transitions import Machine from transitions import Machine
from tfw.components.mixins.callback_mixin import CallbackMixin from tfw.mixins import CallbackMixin
class FSMBase(CallbackMixin): class FSMBase(CallbackMixin):
"""
A general FSM base class you can inherit from to track user progress.
See linear_fsm.py for an example use-case.
TFW the transitions library for state machines, please refer to their
documentation for more information on creating your own machines:
https://github.com/pytransitions/transitions
"""
states, transitions = [], [] states, transitions = [], []
def __init__(self, initial: str = None, accepted_states: List[str] = None): def __init__(self, initial: str = None, accepted_states: List[str] = None):
CallbackMixin.__init__(self)
self.accepted_states = accepted_states or [self.states[-1]] self.accepted_states = accepted_states or [self.states[-1]]
self.machine = Machine(model=self, self.machine = Machine(
model=self,
states=self.states, states=self.states,
transitions=self.transitions, transitions=self.transitions,
initial=initial or self.states[0], initial=initial or self.states[0],
send_event=True, send_event=True,
ignore_invalid_triggers=True, ignore_invalid_triggers=True,
after_state_change='execute_callbacks') after_state_change='execute_callbacks'
)
def execute_callbacks(self, event_data): def execute_callbacks(self, event_data):
self._execute_callbacks(event_data.kwargs) self._execute_callbacks(event_data.kwargs)
+29
View File
@@ -0,0 +1,29 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from .fsm_base import FSMBase
class LinearFSM(FSMBase):
# pylint: disable=anomalous-backslash-in-string
"""
This is a state machine for challenges with linear progression, consisting of
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
"""
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)
})
super(LinearFSM, self).__init__()
-18
View File
@@ -1,18 +0,0 @@
from datetime import datetime
from tfw.networking.event_handlers.server_connector import ServerUplinkConnector
class MessageSender:
def __init__(self, custom_key: str = None):
self.server_connector = ServerUplinkConnector()
self.key = custom_key or 'message'
def send(self, originator, message):
data = {
'originator': originator,
'timestamp': datetime.now().isoformat(),
'message': message
}
response = {'data': data}
self.server_connector.send(self.key, response)
+7
View File
@@ -0,0 +1,7 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from .supervisor_mixin import SupervisorMixin, SupervisorLogMixin
from .callback_mixin import CallbackMixin
from .observer_mixin import ObserverMixin
from .monitor_manager_mixin import MonitorManagerMixin
+29
View File
@@ -0,0 +1,29 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from functools import partial
from tfw.decorators import lazy_property
class CallbackMixin:
@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
"""
fun = partial(callback, *args, **kwargs)
self._callbacks.append(fun)
def unsubscribe_callback(self, callback):
self._callbacks.remove(callback)
def _execute_callbacks(self, *args, **kwargs):
for callback in self._callbacks:
callback(*args, **kwargs)
+30
View File
@@ -0,0 +1,30 @@
# 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, *monitor_args):
self._monitor_type = monitor_type
self._monitor = None
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.monitor_args)
self._monitor.watch() # This runs on a separate thread
+19
View File
@@ -0,0 +1,19 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from watchdog.observers import Observer
from tfw.decorators import lazy_property
class ObserverMixin:
@lazy_property
def observer(self):
return Observer()
def watch(self):
self.observer.start()
def stop(self):
self.observer.stop()
self.observer.join()
+43
View File
@@ -0,0 +1,43 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
import xmlrpc.client
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 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)
def start_process(self, process_name):
self.supervisor.startProcess(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)
+9
View File
@@ -0,0 +1,9 @@
# 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 .zmq_connector_base import ZMQConnectorBase
# from .controller_connector import ControllerConnector # TODO: readd once controller stuff is resolved
from .message_sender import MessageSender
from .event_handlers.server_connector import ServerUplinkConnector as TFWServerConnector
from .server.tfw_server import TFWServer
+5 -2
View File
@@ -1,15 +1,18 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
import zmq import zmq
from zmq.eventloop.zmqstream import ZMQStream from zmq.eventloop.zmqstream import ZMQStream
from tfw.config import TFWENV from tfw.config import TFWENV
from tfw.networking.zmq_connector_base import ZMQConnectorBase from tfw.networking import ZMQConnectorBase
class ControllerConnector(ZMQConnectorBase): class ControllerConnector(ZMQConnectorBase):
def __init__(self, zmq_context=None): def __init__(self, zmq_context=None):
super(ControllerConnector, self).__init__(zmq_context) super(ControllerConnector, self).__init__(zmq_context)
self._zmq_rep_socket = self._zmq_context.socket(zmq.REP) self._zmq_rep_socket = self._zmq_context.socket(zmq.REP)
self._zmq_rep_socket.connect('tcp://localhost:{}'.format(TFWENV.CONTROLLER_PORT)) self._zmq_rep_socket.connect(f'tcp://localhost:{TFWENV.CONTROLLER_PORT}')
self._zmq_rep_stream = ZMQStream(self._zmq_rep_socket) self._zmq_rep_stream = ZMQStream(self._zmq_rep_socket)
self.register_callback = self._zmq_rep_stream.on_recv_stream self.register_callback = self._zmq_rep_stream.on_recv_stream
@@ -0,0 +1,4 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from .server_connector import ServerConnector, ServerUplinkConnector, ServerDownlinkConnector
@@ -1,18 +1,21 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from functools import partial from functools import partial
import zmq import zmq
from zmq.eventloop.zmqstream import ZMQStream from zmq.eventloop.zmqstream import ZMQStream
from tfw.networking.serialization import serialize_all from tfw.networking import serialize_tfw_msg
from tfw.networking import ZMQConnectorBase
from tfw.config import TFWENV from tfw.config import TFWENV
from tfw.networking.zmq_connector_base import ZMQConnectorBase
class ServerDownlinkConnector(ZMQConnectorBase): class ServerDownlinkConnector(ZMQConnectorBase):
def __init__(self, zmq_context=None): def __init__(self, zmq_context=None):
super(ServerDownlinkConnector, self).__init__(zmq_context) super(ServerDownlinkConnector, self).__init__(zmq_context)
self._zmq_sub_socket = self._zmq_context.socket(zmq.SUB) self._zmq_sub_socket = self._zmq_context.socket(zmq.SUB)
self._zmq_sub_socket.connect('tcp://localhost:{}'.format(TFWENV.PUBLISHER_PORT)) self._zmq_sub_socket.connect(f'tcp://localhost:{TFWENV.PUBLISHER_PORT}')
self._zmq_sub_stream = ZMQStream(self._zmq_sub_socket) self._zmq_sub_stream = ZMQStream(self._zmq_sub_socket)
self.subscribe = partial(self._zmq_sub_socket.setsockopt_string, zmq.SUBSCRIBE) self.subscribe = partial(self._zmq_sub_socket.setsockopt_string, zmq.SUBSCRIBE)
@@ -21,18 +24,35 @@ class ServerDownlinkConnector(ZMQConnectorBase):
class ServerUplinkConnector(ZMQConnectorBase): class ServerUplinkConnector(ZMQConnectorBase):
"""
Class capable of sending messages to the TFW server and event handlers.
"""
def __init__(self, zmq_context=None): def __init__(self, zmq_context=None):
super(ServerUplinkConnector, self).__init__(zmq_context) super(ServerUplinkConnector, self).__init__(zmq_context)
self._zmq_push_socket = self._zmq_context.socket(zmq.PUSH) self._zmq_push_socket = self._zmq_context.socket(zmq.PUSH)
self._zmq_push_socket.connect('tcp://localhost:{}'.format(TFWENV.RECEIVER_PORT)) self._zmq_push_socket.connect(f'tcp://localhost:{TFWENV.RECEIVER_PORT}')
def send_to_eventhandler(self, key, message): def send_to_eventhandler(self, message):
message['data']['key'] = key """
self.send('mirror', message) Send a message to an event handler.
This envelopes the desired message in the 'data' field of the message to
TFWServer, which will mirror it to event handlers.
def send(self, key, message): :param message: JSON message you want to send
message['key'] = key :param message['key']: key of event handler you want to address
self._zmq_push_socket.send_multipart(serialize_all(key, message)) """
nested_message = {'key': message['key'], 'data': message.pop('data')}
message['key'] = 'mirror'
message['data'] = nested_message
self.send(message)
def send(self, message):
"""
Send a message to the TFW server
:param message: JSON message you want to send
"""
self._zmq_push_socket.send_multipart(serialize_tfw_msg(message))
class ServerConnector(ServerUplinkConnector, ServerDownlinkConnector): class ServerConnector(ServerUplinkConnector, ServerDownlinkConnector):
+46
View File
@@ -0,0 +1,46 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from tfw.networking.event_handlers import ServerUplinkConnector
class MessageSender:
"""
Provides mechanisms to send messages to our frontend messaging component.
"""
def __init__(self):
self.server_connector = ServerUplinkConnector()
self.key = 'message'
self.queue_key = 'queueMessages'
def send(self, originator, message):
"""
Sends a message.
:param originator: name of sender to be displayed on the frontend
:param message: message to send
"""
data = {
'originator': originator,
'message': message
}
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
})
+86 -10
View File
@@ -1,21 +1,97 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# 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):
"""
Create TFW multipart data from message dict
"""
return _serialize_all(message['key'], message)
def deserialize_tfw_msg(*args):
"""
Return message from TFW multipart data
"""
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):
"""
Return input as bytes
(serialize input if it is JSON)
"""
if not isinstance(data, str):
data = json.dumps(data)
return _encode_if_needed(data)
def _deserialize_single(data):
"""
Try parsing input as JSON, return it as
string if parsing fails.
"""
try:
return json.loads(data)
except ValueError:
return _decode_if_needed(data)
def _encode_if_needed(value):
"""
Return input as bytes
(encode if input is string)
"""
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):
"""
Return input as string
(decode if input is bytes)
"""
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)
+7
View File
@@ -0,0 +1,7 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
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
@@ -1,5 +1,7 @@
from tfw.networking.controller_connector import ControllerConnector # Copyright (C) 2018 Avatao.com Innovative Learning Kft.
from tfw.networking.serialization import deserialize_all, serialize_all # All Rights Reserved. See LICENSE file for details.
from tfw.networking import deserialize_all, serialize_all, ControllerConnector
class ControllerResponder: class ControllerResponder:
@@ -1,8 +1,10 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
import zmq import zmq
from zmq.eventloop.zmqstream import ZMQStream from zmq.eventloop.zmqstream import ZMQStream
from tfw.networking.zmq_connector_base import ZMQConnectorBase from tfw.networking import ZMQConnectorBase, serialize_tfw_msg
from tfw.networking.serialization import serialize_all
from tfw.config import TFWENV from tfw.config import TFWENV
from tfw.config.logs import logging from tfw.config.logs import logging
@@ -14,7 +16,7 @@ class EventHandlerDownlinkConnector(ZMQConnectorBase):
super(EventHandlerDownlinkConnector, self).__init__(zmq_context) super(EventHandlerDownlinkConnector, self).__init__(zmq_context)
self._zmq_pull_socket = self._zmq_context.socket(zmq.PULL) self._zmq_pull_socket = self._zmq_context.socket(zmq.PULL)
self._zmq_pull_stream = ZMQStream(self._zmq_pull_socket) self._zmq_pull_stream = ZMQStream(self._zmq_pull_socket)
address = 'tcp://*:{}'.format(TFWENV.RECEIVER_PORT) address = f'tcp://*:{TFWENV.RECEIVER_PORT}'
self._zmq_pull_socket.bind(address) self._zmq_pull_socket.bind(address)
LOG.debug('Pull socket bound to %s', address) LOG.debug('Pull socket bound to %s', address)
@@ -23,7 +25,7 @@ class EventHandlerUplinkConnector(ZMQConnectorBase):
def __init__(self, zmq_context=None): def __init__(self, zmq_context=None):
super(EventHandlerUplinkConnector, self).__init__(zmq_context) super(EventHandlerUplinkConnector, self).__init__(zmq_context)
self._zmq_pub_socket = self._zmq_context.socket(zmq.PUB) self._zmq_pub_socket = self._zmq_context.socket(zmq.PUB)
address = 'tcp://*:{}'.format(TFWENV.PUBLISHER_PORT) address = f'tcp://*:{TFWENV.PUBLISHER_PORT}'
self._zmq_pub_socket.bind(address) self._zmq_pub_socket.bind(address)
LOG.debug('Pub socket bound to %s', address) LOG.debug('Pub socket bound to %s', address)
@@ -32,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))
+57 -18
View File
@@ -1,27 +1,45 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from collections import defaultdict from collections import defaultdict
from tornado.web import Application from tornado.web import Application
from tfw.networking.server.zmq_websocket_handler import ZMQWebSocketProxy from tfw.networking import MessageSender
from tfw.networking.event_handlers.server_connector import ServerUplinkConnector from tfw.networking.event_handlers import ServerUplinkConnector
from tfw.message_sender import MessageSender from tfw.networking.server import EventHandlerConnector
from tfw.config.logs import logging from tfw.config.logs import logging
from .zmq_websocket_handler import ZMQWebSocketProxy
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class TFWServer: class TFWServer:
"""
This class handles the proxying of messages between the frontend and event handers.
It proxies messages from the "/ws" route to all event handlers subscribed to a ZMQ
SUB socket. It also manages an FSM you can define as a constructor argument.
"""
def __init__(self, fsm_type): def __init__(self, fsm_type):
"""
:param fsm_type: the type of FSM you want TFW to use
"""
self._fsm = fsm_type() self._fsm = fsm_type()
self._fsm_updater = FSMUpdater(self._fsm) self._fsm_updater = FSMUpdater(self._fsm)
self._fsm_manager = FSMManager(self._fsm) self._fsm_manager = FSMManager(self._fsm)
self._fsm.subscribe_callback(self._fsm_updater.update) self._fsm.subscribe_callback(self._fsm_updater.update)
self._event_handler_connector = EventHandlerConnector()
self.application = Application( self.application = Application([(
[(r'/ws', ZMQWebSocketProxy, {'make_response': self.make_response, r'/ws', ZMQWebSocketProxy,{
'proxy_filter': self.proxy_filter})] '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.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
@property @property
def fsm(self): def fsm(self):
@@ -31,13 +49,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
@@ -60,7 +86,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)
@@ -74,8 +100,11 @@ class FSMManager:
self.trigger_predicates[trigger].extend(predicates) self.trigger_predicates[trigger].extend(predicates)
def unsubscribe_predicate(self, trigger, *predicates): def unsubscribe_predicate(self, trigger, *predicates):
self.trigger_predicates[trigger] = [predicate for predicate in self.trigger_predicates[trigger] self.trigger_predicates[trigger] = [
not in predicates] predicate
for predicate in self.trigger_predicates[trigger]
not in predicates
]
class FSMUpdater: class FSMUpdater:
@@ -85,11 +114,21 @@ 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 {
response = {'data': {'current_state': self.fsm.state, 'key': 'FSMUpdate',
'valid_transitions': 'data': self.get_fsm_state_and_transitions()
[{'trigger': trigger} for trigger in self.fsm.machine.get_triggers(self.fsm.state)]}} }
return key, response
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,19 +1,23 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
import json import json
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from tornado.websocket import WebSocketHandler from tornado.websocket import WebSocketHandler
from tfw.networking.serialization import deserialize_all from tfw.networking import deserialize_tfw_msg, validate_message
from tfw.networking.server.event_handler_connector import EventHandlerConnector
from tfw.config.logs import logging from tfw.config.logs import logging
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class ZMQWebSocketHandler(WebSocketHandler, ABC): class ZMQWebSocketHandler(WebSocketHandler, ABC):
_event_handler_connector = EventHandlerConnector()
instances = set() instances = set()
def initialize(self, **kwargs): # pylint: disable=arguments-differ
self._event_handler_connector = kwargs['event_handler_connector']
def prepare(self): def prepare(self):
ZMQWebSocketHandler.instances.add(self) ZMQWebSocketHandler.instances.add(self)
@@ -24,36 +28,43 @@ class ZMQWebSocketHandler(WebSocketHandler, ABC):
LOG.debug('WebSocket connection initiated') LOG.debug('WebSocket connection initiated')
self._event_handler_connector.register_callback(self.zmq_callback) self._event_handler_connector.register_callback(self.zmq_callback)
@classmethod def zmq_callback(self, msg_parts):
def zmq_callback(cls, msg_parts): keyhandlers = {'mirror': self.mirror}
keyhandlers = {'mirror': cls.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):
for instance in cls.instances: return
instance.write_message(data)
self.handle_trigger(message)
if message['key'] not in keyhandlers:
for instance in ZMQWebSocketHandler.instances:
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.')
@classmethod def mirror(self, message):
def mirror(cls, data): message = message['data']
key = data['key'] self._event_handler_connector.send_message(message)
cls._event_handler_connector.send_message({'data': data}, key)
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):
@@ -62,15 +73,19 @@ class ZMQWebSocketHandler(WebSocketHandler, ABC):
class ZMQWebSocketProxy(ZMQWebSocketHandler): class ZMQWebSocketProxy(ZMQWebSocketHandler):
# pylint: disable=abstract-method # pylint: disable=abstract-method
def initialize(self, make_response, proxy_filter): def initialize(self, **kwargs): # pylint: disable=arguments-differ
# pylint: disable=arguments-differ super(ZMQWebSocketProxy, self).initialize(**kwargs)
self._make_response = make_response self._make_eventhandler_message = kwargs['make_eventhandler_message']
self._proxy_filter = 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)
+3
View File
@@ -1,3 +1,6 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
import zmq import zmq
-3
View File
@@ -1,3 +0,0 @@
location = /login {
proxy_pass http://127.0.0.1:${TFW_LOGIN_APP_PORT};
}
+1 -1
View File
@@ -3,4 +3,4 @@ location = /terminal {
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
} }
+1
View File
@@ -4,6 +4,7 @@ server {
proxy_connect_timeout 7d; proxy_connect_timeout 7d;
proxy_send_timeout 7d; proxy_send_timeout 7d;
proxy_read_timeout 7d; proxy_read_timeout 7d;
absolute_redirect off;
location = /ws { location = /ws {
proxy_pass http://127.0.0.1:${TFW_WEB_PORT}; proxy_pass http://127.0.0.1:${TFW_WEB_PORT};
+2 -4
View File
@@ -1,13 +1,11 @@
worker_processes auto; worker_processes auto;
pid /tmp/nginx.pid; pid /tmp/nginx.pid;
events events {
{
worker_connections 1024; worker_connections 1024;
} }
http http {
{
sendfile on; sendfile on;
tcp_nopush on; tcp_nopush on;
tcp_nodelay on; tcp_nodelay on;
+29
View File
@@ -0,0 +1,29 @@
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:
version = ifile.read().strip('\n')
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,
extras_require = {
'docs': [
'sphinx >= 1.7.0',
],
},
zip_safe = False,
)
+4 -6
View File
@@ -1,8 +1,10 @@
[supervisord] [supervisord]
user=user user=root
logfile = /tmp/supervisord.log logfile=/dev/null
logfile_maxbytes=0
loglevel = debug loglevel = debug
pidfile = /tmp/supervisord.pid pidfile = /tmp/supervisord.pid
strip_ansi=true
[inet_http_server] [inet_http_server]
port = 127.0.0.1:%(ENV_TFW_SUPERVISOR_HTTP_PORT)s port = 127.0.0.1:%(ENV_TFW_SUPERVISOR_HTTP_PORT)s
@@ -18,9 +20,5 @@ command=/usr/sbin/nginx -g 'daemon off;'
autostart=true autostart=true
autorestart=true autorestart=true
[program:app]
directory=%(ENV_TFW_APP_DIR)s
command=python3 app.py
[include] [include]
files=%(ENV_TFW_SUPERVISORD_COMPONENTS)s/*.conf files=%(ENV_TFW_SUPERVISORD_COMPONENTS)s/*.conf