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:
commit
d33ba34454
@ -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:
|
||||||
|
@ -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"]
|
||||||
|
@ -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
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
@ -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}
|
||||||
|
@ -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
|
||||||
|
@ -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/../..}"
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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 %}
|
||||||
|
@ -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()
|
||||||
|
@ -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'),
|
||||||
|
Loading…
Reference in New Issue
Block a user