1
0
mirror of https://github.com/avatao-content/test-tutorial-framework synced 2024-12-05 01:51:31 +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
- 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:

View File

@ -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"]

View File

@ -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
}))

View File

@ -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}

View File

@ -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

View File

@ -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/../..}"

View File

@ -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

View File

@ -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()

View File

@ -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

View File

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

View File

@ -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 %}

View File

@ -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()

View File

@ -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'),