diff --git a/README.md b/README.md index 5b80f06..f5fd5f3 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,9 @@ Dependencies: - yarn - Angular CLI - GNU coreutils +- GNU findutils +- GNU sed +- GNU grep Just copy and paste the following command in a terminal: @@ -180,7 +183,7 @@ Refer to the example in this repo. ### src -This folder contains the source code of a server running TFW and an other server running our event handlers. +This folder contains the source code of our pre-written event handlers and example FSMs. Note that this is not a part of the framework by any means, these are just simple examples. ``` @@ -207,7 +210,7 @@ A good state machine is the backbone of a good TFW challenge. There are two ways to define a state machine: - Using a YAML configuration file - - Implementing it in Python by hand + - Implementing it in Python The first option allows you to handle FSM callbacks and custom logic in any programming language (not just Python) and is generally really easy to work with (you can execute arbitrary shell commands on events). You should choose this method unless you have good reason not to. @@ -230,6 +233,8 @@ It is also possible to add preconditions to transitions. This is done by adding a `predicates` key with a list of shell commands to run. If you do this, the transition will only succeed if the return code of all predicates was `0` (as per unix convention for success). +Our `YamlFSM` implementation also supports jinja2 templates inside the `YAML` config file (examples in `test_fsm.yml`). + ## Baby steps When creating your own challenge the process should be the following: diff --git a/controller/Dockerfile b/controller/Dockerfile index ef96d41..d09b2fd 100644 --- a/controller/Dockerfile +++ b/controller/Dockerfile @@ -7,7 +7,6 @@ ENV PYTHONPATH="/usr/local/lib" \ TFW_AUTH_KEY="/tmp/tfw-auth.key" \ CONTROLLER_PORT=5555 -RUN pip3 install watchdog transitions COPY ./controller/ / CMD ["python3", "/opt/server.py"] diff --git a/controller/opt/server.py b/controller/opt/server.py index 82a3811..175d738 100644 --- a/controller/opt/server.py +++ b/controller/opt/server.py @@ -4,7 +4,7 @@ import json from tornado.ioloop import IOLoop from tornado.web import RequestHandler, Application -from tfw import FSMAwareEventHandler +from tfw.event_handler_base import FSMAwareEventHandler class ControllerPostHandler(RequestHandler): @@ -15,7 +15,7 @@ class ControllerPostHandler(RequestHandler): def post(self, *args, **kwargs): self.set_header('Content-Type', 'application/json') self.write(json.dumps({ - 'solved': self.controller.in_accepted_state + 'solved': self.controller.fsm_in_accepted_state })) diff --git a/hack/bootstrap.sh b/hack/bootstrap.sh index 643a9cc..a6aedfa 100755 --- a/hack/bootstrap.sh +++ b/hack/bootstrap.sh @@ -3,7 +3,7 @@ set -eu set -o pipefail set -o errtrace shopt -s expand_aliases -[ "$(uname)" == "Darwin" ] && alias sed="gsed" || : +[ "$(uname)" == "Darwin" ] && alias sed="gsed" && alias grep="ggrep" || : HERE="$(pwd)" CHALLENGE=${CHALLENGE:-test-tutorial-framework} diff --git a/hack/libhack/challenge.sh b/hack/libhack/challenge.sh index 8a7e558..4ed1377 100644 --- a/hack/libhack/challenge.sh +++ b/hack/libhack/challenge.sh @@ -40,7 +40,7 @@ challenge::run() { if [[ -d "${BASEIMAGE_PATH}" ]]; then mount_baseimage="-v ${BASEIMAGE_PATH}/lib/tfw:/usr/local/lib/tfw" fi - mount_challenge="-v ${CHALLENGE_PATH}/solvable/src:/srv/.tfw_builtin_ehs" + mount_challenge="-v ${CHALLENGE_PATH}/solvable/src:/.tfw/builtin_event_handlers" mount_volumes="${mount_baseimage:-} ${mount_challenge}" fi popd diff --git a/hack/tfw.sh b/hack/tfw.sh index 46a843b..f05f2b7 100755 --- a/hack/tfw.sh +++ b/hack/tfw.sh @@ -3,7 +3,7 @@ set -eu set -o pipefail set -o errtrace shopt -s expand_aliases -[ "$(uname)" == "Darwin" ] && alias readlink="greadlink" || : +[ "$(uname)" == "Darwin" ] && alias readlink="greadlink" && alias grep="ggrep" || : SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" TFW_PATH="${TFW_PATH:-$SCRIPT_DIR/../..}" diff --git a/solvable/Dockerfile b/solvable/Dockerfile index 7dd51b0..aa6382f 100644 --- a/solvable/Dockerfile +++ b/solvable/Dockerfile @@ -7,9 +7,9 @@ RUN pip3 install Flask==1.0 \ git+https://github.com/avatao-content/tfwconnector.git#subdirectory=python3 # Define variables to use later -ENV TFW_EHMAIN_DIR="/srv/.tfw_builtin_ehs" \ - TFW_WEBSERVICE_DIR="/srv/webservice" \ - TFW_IDE_WD="/home/${AVATAO_USER}/workdir" \ +ENV TFW_EHMAIN_DIR="${TFW_DIR}/builtin_event_handlers" \ + TFW_WEBSERVICE_DIR="/srv/webservice" \ + TFW_IDE_WD="/home/${AVATAO_USER}/workdir" \ TFW_TERMINADO_WD="/home/${AVATAO_USER}/workdir" # Copy TFW related stuff to a dedicated directory @@ -26,7 +26,8 @@ RUN mkdir -p ${TFW_IDE_WD} &&\ chmod -R 755 "${TFW_IDE_WD}" "${TFW_WEBSERVICE_DIR}" # Hide TFW related code from user -RUN chown -R root:root ${TFW_SERVER_DIR} && chmod -R 700 ${TFW_SERVER_DIR} +RUN chown -R root:root ${TFW_SERVER_DIR} ${TFW_DIR} &&\ + chmod -R 700 ${TFW_SERVER_DIR} ${TFW_DIR} # Make AVATAO_USER's home writeable and set it as WORKDIR # Make webservice directory writable diff --git a/solvable/src/event_handler_main.py b/solvable/src/event_handler_main.py index a4f231a..5be6423 100644 --- a/solvable/src/event_handler_main.py +++ b/solvable/src/event_handler_main.py @@ -4,11 +4,12 @@ from signal import signal, SIGTERM, SIGINT from tornado.ioloop import IOLoop -from tfw import YamlFSM, FSMAwareEventHandler, EventHandlerBase +from tfw.fsm import YamlFSM +from tfw.event_handler_base import EventHandlerBase, FSMAwareEventHandler from tfw.components import IdeEventHandler, TerminalEventHandler from tfw.components import ProcessManagingEventHandler, BashMonitor from tfw.components import TerminalCommands, LogMonitoringEventHandler -from tfw.components import FSMManagingEventHandler +from tfw.components import FSMManagingEventHandler, DirectorySnapshottingEventHandler from tfw.networking import MessageSender, TFWServerConnector from tfw.config import TFWENV from tfw.config.logs import logging @@ -87,22 +88,29 @@ class MessageFSMStepsEventHandler(FSMAwareEventHandler): def handle_event(self, message): pass - def handle_fsm_step(self, from_state, to_state, trigger): + def handle_fsm_step(self, **kwargs): """ When the FSM steps this method is invoked. + Receives a 'data' field from an fsm_update message as kwargs. """ MessageSender().send( 'FSM info', - f'FSM has stepped from state "{from_state}" ' - f'to state "{to_state}" in response to trigger "{trigger}"' + f'FSM has stepped from state "{kwargs["last_event"]["from_state"]}" ' + f'to state "{kwargs["current_state"]}" in response to trigger "{kwargs["last_event"]["trigger"]}"' ) -if __name__ == '__main__': +def main(): + # pylint: disable=unused-variable + # # TFW component EventHandlers (builtins, required for their respective functionalities) - fsm = FSMManagingEventHandler( # TFW FSM + fsm = FSMManagingEventHandler( # TFW FSM key='fsm', - fsm_type=partial(YamlFSM, 'test_fsm.yml') + fsm_type=partial( + YamlFSM, + 'test_fsm.yml', + {} # jinja2 variables, use empty dict to enable jinja2 parsing without any variables + ) ) ide = IdeEventHandler( # Web IDE backend key='ide', @@ -124,17 +132,22 @@ if __name__ == '__main__': process_name='webservice', log_tail=2000 ) + snapshot = DirectorySnapshottingEventHandler( # Manages filesystem snapshots of directories + key='snapshot', + directories=[ + TFWENV.IDE_WD, + TFWENV.WEBSERVICE_DIR + ] + ) # Your custom event handlers - message_fsm_steps = MessageFSMStepsEventHandler( + message_fsm_steps_eh = MessageFSMStepsEventHandler( key='test' ) # Terminal command handlers commands = TestCommands(bashrc=f'/home/{TAOENV.USER}/.bashrc') terminal.historymonitor.subscribe_callback(commands.callback) - - # Example terminal command callback terminal.historymonitor.subscribe_callback(cenator) event_handlers = EventHandlerBase.get_local_instances() @@ -146,3 +159,7 @@ if __name__ == '__main__': signal(SIGINT, cleanup) IOLoop.instance().start() + + +if __name__ == '__main__': + main() diff --git a/solvable/src/pipe_io_main.py b/solvable/src/pipe_io_main.py index 26e7349..1edd1ab 100644 --- a/solvable/src/pipe_io_main.py +++ b/solvable/src/pipe_io_main.py @@ -2,7 +2,7 @@ from signal import signal, SIGTERM, SIGINT from tornado.ioloop import IOLoop -from tfw import EventHandlerBase +from tfw.event_handler_base import EventHandlerBase from tfw.components import PipeIOEventHandler diff --git a/solvable/src/test_fsm.py b/solvable/src/test_fsm.py index 3dcf7c3..2db9b97 100644 --- a/solvable/src/test_fsm.py +++ b/solvable/src/test_fsm.py @@ -2,7 +2,7 @@ from os.path import exists -from tfw import LinearFSM +from tfw.fsm import LinearFSM from tfw.networking import MessageSender diff --git a/solvable/src/test_fsm.yml b/solvable/src/test_fsm.yml index b195479..d34ebcb 100644 --- a/solvable/src/test_fsm.yml +++ b/solvable/src/test_fsm.yml @@ -41,3 +41,8 @@ transitions: - trigger: step_5 source: '4' dest: '5' + {% for i in range(5) %} # you can also use jinja2 in this config file + - trigger: 'step_next' + source: '{{i}}' + dest: '{{i+1}}' + {% endfor %} diff --git a/solvable/src/webservice/model.py b/solvable/src/webservice/model.py index d722cdc..b084b1e 100644 --- a/solvable/src/webservice/model.py +++ b/solvable/src/webservice/model.py @@ -13,18 +13,29 @@ session_factory = sessionmaker( ) -@contextmanager -def Session(factory=session_factory): - session = factory() - try: - yield session - session.commit() - except: - session.rollback() - raise - # session is closed by flask - # finally: - # session.close() +class SessionWrapper: + def __init__(self): + self._session_factory = session_factory + self._session_handle = None + + @contextmanager + def session(self): + try: + yield self._session + self._session.commit() + except: + self._session.rollback() + raise + + @property + def _session(self): + if self._session_handle is None: + self._session_handle = self._session_factory() + return self._session_handle + + def teardown(self): + if self._session_handle is not None: + self._session_handle.close() Base = declarative_base() diff --git a/solvable/src/webservice/server.py b/solvable/src/webservice/server.py index c359c61..1b36762 100644 --- a/solvable/src/webservice/server.py +++ b/solvable/src/webservice/server.py @@ -1,9 +1,8 @@ from os import urandom, getenv -from functools import partial from flask import Flask, render_template, request, session, url_for, g -from model import init_db, session_factory, Session +from model import init_db, SessionWrapper from user_ops import UserOps from errors import InvalidCredentialsError, UserExistsError @@ -16,25 +15,24 @@ app.jinja_env.globals.update( # pylint: disable=no-member ) -def get_db_session(): - if not hasattr(g, 'db_session'): - g.db_session = session_factory() - return g.db_session - -Session = partial(Session, get_db_session) +@app.before_request +def setup_db(): + # pylint: disable=protected-access + g._db_session_wrapper = SessionWrapper() + g.db_session = g._db_session_wrapper.session @app.teardown_appcontext -def close_db_session(err): # pylint: disable=unused-argument - if hasattr(g, 'db_session'): - g.db_session.close() +def close_db_session(_): + # pylint: disable=protected-access + g._db_session_wrapper.teardown() @app.route('/', methods=['GET', 'POST']) def index(): if request.method == 'POST': try: - with Session() as db_session: + with g.db_session() as db_session: UserOps( request.form.get('username'), request.form.get('password'), @@ -67,7 +65,7 @@ def register(): return render_template('register.html', alert='Passwords do not match! Please try again.') try: - with Session() as db_session: + with g.db_session() as db_session: UserOps( request.form.get('username'), request.form.get('password'),