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:
commit
d33ba34454
@ -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:
|
||||
|
@ -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"]
|
||||
|
@ -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
|
||||
}))
|
||||
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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/../..}"
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from os.path import exists
|
||||
|
||||
from tfw import LinearFSM
|
||||
from tfw.fsm import LinearFSM
|
||||
from tfw.networking import MessageSender
|
||||
|
||||
|
||||
|
@ -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 %}
|
||||
|
@ -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()
|
||||
|
@ -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'),
|
||||
|
Loading…
Reference in New Issue
Block a user