1
0
mirror of https://github.com/avatao-content/test-tutorial-framework synced 2024-11-14 22:07:17 +00:00

Merge branch 'ocicat', the unrealized dream. Ocicat will return...

This commit is contained in:
Kristóf Tóth 2019-05-15 17:26:02 +02:00
commit d33ba34454
13 changed files with 86 additions and 50 deletions

View File

@ -30,6 +30,9 @@ Dependencies:
- yarn - yarn
- Angular CLI - Angular CLI
- GNU coreutils - GNU coreutils
- GNU findutils
- GNU sed
- GNU grep
Just copy and paste the following command in a terminal: Just copy and paste the following command in a terminal:
@ -180,7 +183,7 @@ Refer to the example in this repo.
### src ### 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. 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: There are two ways to define a state machine:
- Using a YAML configuration file - 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). 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. 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. 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). 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 ## Baby steps
When creating your own challenge the process should be the following: When creating your own challenge the process should be the following:

View File

@ -7,7 +7,6 @@ ENV PYTHONPATH="/usr/local/lib" \
TFW_AUTH_KEY="/tmp/tfw-auth.key" \ TFW_AUTH_KEY="/tmp/tfw-auth.key" \
CONTROLLER_PORT=5555 CONTROLLER_PORT=5555
RUN pip3 install watchdog transitions
COPY ./controller/ / COPY ./controller/ /
CMD ["python3", "/opt/server.py"] CMD ["python3", "/opt/server.py"]

View File

@ -4,7 +4,7 @@ import json
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
from tornado.web import RequestHandler, Application from tornado.web import RequestHandler, Application
from tfw import FSMAwareEventHandler from tfw.event_handler_base import FSMAwareEventHandler
class ControllerPostHandler(RequestHandler): class ControllerPostHandler(RequestHandler):
@ -15,7 +15,7 @@ class ControllerPostHandler(RequestHandler):
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
self.set_header('Content-Type', 'application/json') self.set_header('Content-Type', 'application/json')
self.write(json.dumps({ self.write(json.dumps({
'solved': self.controller.in_accepted_state 'solved': self.controller.fsm_in_accepted_state
})) }))

View File

@ -3,7 +3,7 @@ set -eu
set -o pipefail set -o pipefail
set -o errtrace set -o errtrace
shopt -s expand_aliases shopt -s expand_aliases
[ "$(uname)" == "Darwin" ] && alias sed="gsed" || : [ "$(uname)" == "Darwin" ] && alias sed="gsed" && alias grep="ggrep" || :
HERE="$(pwd)" HERE="$(pwd)"
CHALLENGE=${CHALLENGE:-test-tutorial-framework} CHALLENGE=${CHALLENGE:-test-tutorial-framework}

View File

@ -40,7 +40,7 @@ challenge::run() {
if [[ -d "${BASEIMAGE_PATH}" ]]; then if [[ -d "${BASEIMAGE_PATH}" ]]; then
mount_baseimage="-v ${BASEIMAGE_PATH}/lib/tfw:/usr/local/lib/tfw" mount_baseimage="-v ${BASEIMAGE_PATH}/lib/tfw:/usr/local/lib/tfw"
fi 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}" mount_volumes="${mount_baseimage:-} ${mount_challenge}"
fi fi
popd popd

View File

@ -3,7 +3,7 @@ set -eu
set -o pipefail set -o pipefail
set -o errtrace set -o errtrace
shopt -s expand_aliases 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]}")")" SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
TFW_PATH="${TFW_PATH:-$SCRIPT_DIR/../..}" TFW_PATH="${TFW_PATH:-$SCRIPT_DIR/../..}"

View File

@ -7,7 +7,7 @@ RUN pip3 install Flask==1.0 \
git+https://github.com/avatao-content/tfwconnector.git#subdirectory=python3 git+https://github.com/avatao-content/tfwconnector.git#subdirectory=python3
# Define variables to use later # Define variables to use later
ENV TFW_EHMAIN_DIR="/srv/.tfw_builtin_ehs" \ ENV TFW_EHMAIN_DIR="${TFW_DIR}/builtin_event_handlers" \
TFW_WEBSERVICE_DIR="/srv/webservice" \ TFW_WEBSERVICE_DIR="/srv/webservice" \
TFW_IDE_WD="/home/${AVATAO_USER}/workdir" \ TFW_IDE_WD="/home/${AVATAO_USER}/workdir" \
TFW_TERMINADO_WD="/home/${AVATAO_USER}/workdir" TFW_TERMINADO_WD="/home/${AVATAO_USER}/workdir"
@ -26,7 +26,8 @@ RUN mkdir -p ${TFW_IDE_WD} &&\
chmod -R 755 "${TFW_IDE_WD}" "${TFW_WEBSERVICE_DIR}" chmod -R 755 "${TFW_IDE_WD}" "${TFW_WEBSERVICE_DIR}"
# Hide TFW related code from user # 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 AVATAO_USER's home writeable and set it as WORKDIR
# Make webservice directory writable # Make webservice directory writable

View File

@ -4,11 +4,12 @@ from signal import signal, SIGTERM, SIGINT
from tornado.ioloop import IOLoop 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 IdeEventHandler, TerminalEventHandler
from tfw.components import ProcessManagingEventHandler, BashMonitor from tfw.components import ProcessManagingEventHandler, BashMonitor
from tfw.components import TerminalCommands, LogMonitoringEventHandler 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.networking import MessageSender, TFWServerConnector
from tfw.config import TFWENV from tfw.config import TFWENV
from tfw.config.logs import logging from tfw.config.logs import logging
@ -87,22 +88,29 @@ class MessageFSMStepsEventHandler(FSMAwareEventHandler):
def handle_event(self, message): def handle_event(self, message):
pass 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. When the FSM steps this method is invoked.
Receives a 'data' field from an fsm_update message as kwargs.
""" """
MessageSender().send( MessageSender().send(
'FSM info', 'FSM info',
f'FSM has stepped from state "{from_state}" ' f'FSM has stepped from state "{kwargs["last_event"]["from_state"]}" '
f'to state "{to_state}" in response to trigger "{trigger}"' 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) # TFW component EventHandlers (builtins, required for their respective functionalities)
fsm = FSMManagingEventHandler( # TFW FSM fsm = FSMManagingEventHandler( # TFW FSM
key='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 ide = IdeEventHandler( # Web IDE backend
key='ide', key='ide',
@ -124,17 +132,22 @@ if __name__ == '__main__':
process_name='webservice', process_name='webservice',
log_tail=2000 log_tail=2000
) )
snapshot = DirectorySnapshottingEventHandler( # Manages filesystem snapshots of directories
key='snapshot',
directories=[
TFWENV.IDE_WD,
TFWENV.WEBSERVICE_DIR
]
)
# Your custom event handlers # Your custom event handlers
message_fsm_steps = MessageFSMStepsEventHandler( message_fsm_steps_eh = MessageFSMStepsEventHandler(
key='test' key='test'
) )
# Terminal command handlers # Terminal command handlers
commands = TestCommands(bashrc=f'/home/{TAOENV.USER}/.bashrc') commands = TestCommands(bashrc=f'/home/{TAOENV.USER}/.bashrc')
terminal.historymonitor.subscribe_callback(commands.callback) terminal.historymonitor.subscribe_callback(commands.callback)
# Example terminal command callback
terminal.historymonitor.subscribe_callback(cenator) terminal.historymonitor.subscribe_callback(cenator)
event_handlers = EventHandlerBase.get_local_instances() event_handlers = EventHandlerBase.get_local_instances()
@ -146,3 +159,7 @@ if __name__ == '__main__':
signal(SIGINT, cleanup) signal(SIGINT, cleanup)
IOLoop.instance().start() IOLoop.instance().start()
if __name__ == '__main__':
main()

View File

@ -2,7 +2,7 @@ from signal import signal, SIGTERM, SIGINT
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
from tfw import EventHandlerBase from tfw.event_handler_base import EventHandlerBase
from tfw.components import PipeIOEventHandler from tfw.components import PipeIOEventHandler

View File

@ -2,7 +2,7 @@
from os.path import exists from os.path import exists
from tfw import LinearFSM from tfw.fsm import LinearFSM
from tfw.networking import MessageSender from tfw.networking import MessageSender

View File

@ -41,3 +41,8 @@ transitions:
- trigger: step_5 - trigger: step_5
source: '4' source: '4'
dest: '5' 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 %}

View File

@ -13,18 +13,29 @@ session_factory = sessionmaker(
) )
@contextmanager class SessionWrapper:
def Session(factory=session_factory): def __init__(self):
session = factory() self._session_factory = session_factory
self._session_handle = None
@contextmanager
def session(self):
try: try:
yield session yield self._session
session.commit() self._session.commit()
except: except:
session.rollback() self._session.rollback()
raise raise
# session is closed by flask
# finally: @property
# session.close() 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() Base = declarative_base()

View File

@ -1,9 +1,8 @@
from os import urandom, getenv from os import urandom, getenv
from functools import partial
from flask import Flask, render_template, request, session, url_for, g 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 user_ops import UserOps
from errors import InvalidCredentialsError, UserExistsError from errors import InvalidCredentialsError, UserExistsError
@ -16,25 +15,24 @@ app.jinja_env.globals.update( # pylint: disable=no-member
) )
def get_db_session(): @app.before_request
if not hasattr(g, 'db_session'): def setup_db():
g.db_session = session_factory() # pylint: disable=protected-access
return g.db_session g._db_session_wrapper = SessionWrapper()
g.db_session = g._db_session_wrapper.session
Session = partial(Session, get_db_session)
@app.teardown_appcontext @app.teardown_appcontext
def close_db_session(err): # pylint: disable=unused-argument def close_db_session(_):
if hasattr(g, 'db_session'): # pylint: disable=protected-access
g.db_session.close() g._db_session_wrapper.teardown()
@app.route('/', methods=['GET', 'POST']) @app.route('/', methods=['GET', 'POST'])
def index(): def index():
if request.method == 'POST': if request.method == 'POST':
try: try:
with Session() as db_session: with g.db_session() as db_session:
UserOps( UserOps(
request.form.get('username'), request.form.get('username'),
request.form.get('password'), request.form.get('password'),
@ -67,7 +65,7 @@ def register():
return render_template('register.html', alert='Passwords do not match! Please try again.') return render_template('register.html', alert='Passwords do not match! Please try again.')
try: try:
with Session() as db_session: with g.db_session() as db_session:
UserOps( UserOps(
request.form.get('username'), request.form.get('username'),
request.form.get('password'), request.form.get('password'),