Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0038663bc6 | ||
|
|
6489e4452e | ||
|
|
bfd1efa9f9 | ||
|
|
293d7972f1 | ||
|
|
d30226d55b | ||
|
|
9e454a83d0 | ||
|
|
34cadcf9b1 | ||
|
|
a9d6794c4d | ||
|
|
1fb13fefff | ||
|
|
d67d2c6bc9 | ||
|
|
d1305bc879 |
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
ignored-modules = zmq
|
ignored-modules = zmq
|
||||||
max-line-length = 150
|
max-line-length = 150
|
||||||
disable = missing-docstring, too-few-public-methods
|
disable = missing-docstring, too-few-public-methods, invalid-name
|
||||||
|
|||||||
+1
-1
@@ -50,7 +50,7 @@ COPY nginx/default.conf ${TFW_NGINX_DEFAULT}
|
|||||||
COPY nginx/components/ ${TFW_NGINX_COMPONENTS}
|
COPY nginx/components/ ${TFW_NGINX_COMPONENTS}
|
||||||
COPY lib LICENSE ${TFW_LIB_DIR}
|
COPY lib LICENSE ${TFW_LIB_DIR}
|
||||||
|
|
||||||
RUN for dir in "${TFW_LIB_DIR}"/{tfw,tao,envvars.py} "/etc/nginx" "/etc/supervisor"; do \
|
RUN for dir in "${TFW_LIB_DIR}"/{tfw,tao,envvars} "/etc/nginx" "/etc/supervisor"; do \
|
||||||
chown -R root:root "$dir" && chmod -R 700 "$dir"; \
|
chown -R root:root "$dir" && chmod -R 700 "$dir"; \
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,12 @@ LOG = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class DirectoryMonitor(ObserverMixin):
|
class DirectoryMonitor(ObserverMixin):
|
||||||
def __init__(self, directory):
|
def __init__(self, directories):
|
||||||
ObserverMixin.__init__(self)
|
ObserverMixin.__init__(self)
|
||||||
self.eventhandler = IdeReloadWatchdogEventHandler()
|
self.eventhandler = IdeReloadWatchdogEventHandler()
|
||||||
self.observer.schedule(self.eventhandler, directory, recursive=True)
|
for directory in directories:
|
||||||
|
self.observer.schedule(self.eventhandler, directory, recursive=True)
|
||||||
|
|
||||||
self.pause, self.resume = self.eventhandler.pause, self.eventhandler.resume
|
self.pause, self.resume = self.eventhandler.pause, self.eventhandler.resume
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -98,17 +98,24 @@ class IdeEventHandler(EventHandlerBase, MonitorManagerMixin):
|
|||||||
By default all files in the directory specified in __init__ are displayed
|
By default all files in the directory specified in __init__ are displayed
|
||||||
on the fontend. Note that this is a stateful component.
|
on the fontend. Note that this is a stateful component.
|
||||||
|
|
||||||
|
When any file in the selected directory changes they are automatically refreshed
|
||||||
|
on the frontend (this is done by listening to inotify events).
|
||||||
|
|
||||||
This EventHandler accepts messages that have a data["command"] key specifying
|
This EventHandler accepts messages that have a data["command"] key specifying
|
||||||
a command to be executed.
|
a command to be executed.
|
||||||
The API of each command is documented in their respective handlers.
|
The API of each command is documented in their respective handlers.
|
||||||
"""
|
"""
|
||||||
def __init__(self, key, directory, allowed_directories, selected_file=None, exclude=None):
|
def __init__(self, key, directory, allowed_directories, selected_file=None, exclude=None,
|
||||||
|
additional_watched_directories=None):
|
||||||
"""
|
"""
|
||||||
:param key: the key this instance should listen to
|
:param key: the key this instance should listen to
|
||||||
:param directory: working directory which the EventHandler should serve files from
|
:param directory: working directory which the EventHandler should serve files from
|
||||||
:param allowed_directories: list of directories that can be switched to using the selectdir command
|
:param allowed_directories: list of directories that can be switched to using the selectdir command
|
||||||
:param selected_file: file that is selected by default
|
:param selected_file: file that is selected by default
|
||||||
:param exclude: list of filenames that should not appear between files (for *.o, *.pyc, etc.)
|
:param exclude: list of filenames that should not appear between files (for *.o, *.pyc, etc.)
|
||||||
|
:param additional_watched_directories: refresh the selected file when files change in these directories
|
||||||
|
(the working directory is watched by default, this is useful for
|
||||||
|
symlinks and such)
|
||||||
"""
|
"""
|
||||||
super().__init__(key)
|
super().__init__(key)
|
||||||
try:
|
try:
|
||||||
@@ -116,7 +123,11 @@ class IdeEventHandler(EventHandlerBase, MonitorManagerMixin):
|
|||||||
selected_file=selected_file, exclude=exclude)
|
selected_file=selected_file, exclude=exclude)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
raise EnvironmentError(f'No file(s) in IdeEventHandler working_directory "{directory}"!')
|
raise EnvironmentError(f'No file(s) in IdeEventHandler working_directory "{directory}"!')
|
||||||
MonitorManagerMixin.__init__(self, DirectoryMonitor, self.filemanager.workdir)
|
|
||||||
|
self.watched_directories = [self.filemanager.workdir]
|
||||||
|
if additional_watched_directories:
|
||||||
|
self.watched_directories.extend(additional_watched_directories)
|
||||||
|
MonitorManagerMixin.__init__(self, DirectoryMonitor, self.watched_directories)
|
||||||
|
|
||||||
self.commands = {'read': self.read,
|
self.commands = {'read': self.read,
|
||||||
'write': self.write,
|
'write': self.write,
|
||||||
|
|||||||
@@ -13,10 +13,9 @@ LOG = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class ProcessManager(SupervisorMixin):
|
class ProcessManager(SupervisorMixin):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.commands = {'start': self.start_process,
|
self.commands = {'start': self.start_process,
|
||||||
'stop': self.stop_process,
|
'stop': self.stop_process,
|
||||||
'restart': self.restart_process,
|
'restart': self.restart_process}
|
||||||
'readlog': self.read_log}
|
|
||||||
|
|
||||||
def __call__(self, command, process_name):
|
def __call__(self, command, process_name):
|
||||||
return self.commands[command](process_name)
|
return self.commands[command](process_name)
|
||||||
@@ -46,10 +45,11 @@ class ProcessManagingEventHandler(EventHandlerBase):
|
|||||||
try:
|
try:
|
||||||
data = message['data']
|
data = message['data']
|
||||||
self.processmanager(data['command'], data['process_name'])
|
self.processmanager(data['command'], data['process_name'])
|
||||||
|
message['data']['log'] = self.processmanager.read_log_stdout(message['data']['process_name'])
|
||||||
return message
|
return message
|
||||||
except KeyError:
|
except KeyError:
|
||||||
LOG.error('IGNORING MESSAGE: Invalid message received: %s', message)
|
LOG.error('IGNORING MESSAGE: Invalid message received: %s', message)
|
||||||
except SupervisorFault as fault:
|
except SupervisorFault as fault:
|
||||||
message['data']['error'] = fault.faultString
|
message['data']['error'] = fault.faultString
|
||||||
message['data']['log'] = self.processmanager.read_log(message['data']['process_name'])
|
message['data']['log'] = self.processmanager.read_log_stderr(message['data']['process_name'])
|
||||||
return message
|
return message
|
||||||
|
|||||||
@@ -5,14 +5,18 @@ from .fsm_base import FSMBase
|
|||||||
|
|
||||||
|
|
||||||
class LinearFSM(FSMBase):
|
class LinearFSM(FSMBase):
|
||||||
|
# pylint: disable=anomalous-backslash-in-string
|
||||||
"""
|
"""
|
||||||
This is a state machine for challenges with linear progression, consisting of
|
This is a state machine for challenges with linear progression, consisting of
|
||||||
a number of steps specified in the constructor. It automatically sets up a single
|
a number of steps specified in the constructor. It automatically sets up 2
|
||||||
action between states as such:
|
actions (triggers) between states as such:
|
||||||
0 ==step_1==> 1 ==step_2==> 2 ==step_3==> 3 ... and so on
|
(0) -- step_1 --> (1) -- step_2 --> (2) -- step_3 --> (3) ... and so on
|
||||||
|
\-step_next-/ \-step_next-/ \-step_next-/
|
||||||
"""
|
"""
|
||||||
def __init__(self, number_of_steps):
|
def __init__(self, number_of_steps):
|
||||||
self.states = list(map(str, range(number_of_steps)))
|
self.states = list(map(str, range(number_of_steps)))
|
||||||
self.transitions = [{'trigger': f'step_{int(index)+1}', 'source': index, 'dest': str(int(index)+1)}
|
self.transitions = []
|
||||||
for index in self.states[:-1]]
|
for index in self.states[:-1]:
|
||||||
|
self.transitions.append({'trigger': f'step_{int(index)+1}', 'source': index, 'dest': str(int(index)+1)})
|
||||||
|
self.transitions.append({'trigger': 'step_next', 'source': index, 'dest': str(int(index)+1)})
|
||||||
super(LinearFSM, self).__init__()
|
super(LinearFSM, self).__init__()
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ LOG = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class MonitorManagerMixin:
|
class MonitorManagerMixin:
|
||||||
def __init__(self, monitor_type, directory):
|
def __init__(self, monitor_type, directories):
|
||||||
self._monitor_type = monitor_type
|
self._monitor_type = monitor_type
|
||||||
self._monitor = None
|
self._monitor = None
|
||||||
self._monitored_directory = directory
|
self._monitored_directories = directories
|
||||||
self.reload_monitor()
|
self.reload_monitor()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -23,5 +23,5 @@ class MonitorManagerMixin:
|
|||||||
self._monitor.stop()
|
self._monitor.stop()
|
||||||
except KeyError:
|
except KeyError:
|
||||||
LOG.debug('Working directory was removed – ignoring...')
|
LOG.debug('Working directory was removed – ignoring...')
|
||||||
self._monitor = self._monitor_type(self._monitored_directory)
|
self._monitor = self._monitor_type(self._monitored_directories)
|
||||||
self._monitor.watch() # This runs on a separate thread
|
self._monitor.watch() # This runs on a separate thread
|
||||||
|
|||||||
@@ -19,11 +19,21 @@ class SupervisorMixin:
|
|||||||
def start_process(self, process_name):
|
def start_process(self, process_name):
|
||||||
self.supervisor.startProcess(process_name)
|
self.supervisor.startProcess(process_name)
|
||||||
|
|
||||||
def read_log(self, process_name):
|
def read_log_stdout(self, process_name):
|
||||||
logs = self.supervisor.readProcessStderrLog(process_name, 0, 0)
|
return self._read_log_internal(self.supervisor.readProcessStdoutLog, process_name)
|
||||||
remove(self.supervisor.getProcessInfo(process_name)['stderr_logfile'])
|
|
||||||
|
def read_log_stderr(self, process_name):
|
||||||
|
return self._read_log_internal(self.supervisor.readProcessStderrLog, process_name)
|
||||||
|
|
||||||
|
def _read_log_internal(self, read_method, process_name):
|
||||||
|
log = read_method(process_name, 0, 0)
|
||||||
|
self.clear_logs(process_name)
|
||||||
|
return log
|
||||||
|
|
||||||
|
def clear_logs(self, process_name):
|
||||||
|
for logfile in ('stdout_logfile', 'stderr_logfile'):
|
||||||
|
remove(self.supervisor.getProcessInfo(process_name)[logfile])
|
||||||
self.supervisor.clearProcessLogs(process_name)
|
self.supervisor.clearProcessLogs(process_name)
|
||||||
return logs
|
|
||||||
|
|
||||||
def restart_process(self, process_name):
|
def restart_process(self, process_name):
|
||||||
self.stop_process(process_name)
|
self.stop_process(process_name)
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ class ServerUplinkConnector(ZMQConnectorBase):
|
|||||||
def send_to_eventhandler(self, message):
|
def send_to_eventhandler(self, message):
|
||||||
"""
|
"""
|
||||||
Send a message to an event handler.
|
Send a message to an event handler.
|
||||||
|
This envelopes the desired message in the 'data' field of the message to
|
||||||
|
TFWServer, which will mirror it to event handlers.
|
||||||
|
|
||||||
:param message: JSON message you want to send
|
:param message: JSON message you want to send
|
||||||
:param message['key']: key of event handler you want to address
|
:param message['key']: key of event handler you want to address
|
||||||
"""
|
"""
|
||||||
@@ -46,6 +49,7 @@ class ServerUplinkConnector(ZMQConnectorBase):
|
|||||||
def send(self, message):
|
def send(self, message):
|
||||||
"""
|
"""
|
||||||
Send a message to the TFW server
|
Send a message to the TFW server
|
||||||
|
|
||||||
:param message: JSON message you want to send
|
:param message: JSON message you want to send
|
||||||
"""
|
"""
|
||||||
self._zmq_push_socket.send_multipart(serialize_tfw_msg(message))
|
self._zmq_push_socket.send_multipart(serialize_tfw_msg(message))
|
||||||
|
|||||||
@@ -29,10 +29,16 @@ def validate_message(message):
|
|||||||
|
|
||||||
|
|
||||||
def serialize_tfw_msg(message):
|
def serialize_tfw_msg(message):
|
||||||
|
"""
|
||||||
|
Create TFW multipart data from message dict
|
||||||
|
"""
|
||||||
return _serialize_all(message['key'], message)
|
return _serialize_all(message['key'], message)
|
||||||
|
|
||||||
|
|
||||||
def deserialize_tfw_msg(*args):
|
def deserialize_tfw_msg(*args):
|
||||||
|
"""
|
||||||
|
Return message from TFW multipart data
|
||||||
|
"""
|
||||||
return _deserialize_all(*args)[1]
|
return _deserialize_all(*args)[1]
|
||||||
|
|
||||||
|
|
||||||
@@ -45,12 +51,20 @@ def _deserialize_all(*args):
|
|||||||
|
|
||||||
|
|
||||||
def _serialize_single(data):
|
def _serialize_single(data):
|
||||||
|
"""
|
||||||
|
Return input as bytes
|
||||||
|
(serialize input if it is JSON)
|
||||||
|
"""
|
||||||
if not isinstance(data, str):
|
if not isinstance(data, str):
|
||||||
data = json.dumps(data)
|
data = json.dumps(data)
|
||||||
return _encode_if_needed(data)
|
return _encode_if_needed(data)
|
||||||
|
|
||||||
|
|
||||||
def _deserialize_single(data):
|
def _deserialize_single(data):
|
||||||
|
"""
|
||||||
|
Try parsing input as JSON, return it as
|
||||||
|
string if parsing fails.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return json.loads(data)
|
return json.loads(data)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -58,12 +72,20 @@ def _deserialize_single(data):
|
|||||||
|
|
||||||
|
|
||||||
def _encode_if_needed(value):
|
def _encode_if_needed(value):
|
||||||
|
"""
|
||||||
|
Return input as bytes
|
||||||
|
(encode if input is string)
|
||||||
|
"""
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
value = value.encode('utf-8')
|
value = value.encode('utf-8')
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def _decode_if_needed(value):
|
def _decode_if_needed(value):
|
||||||
|
"""
|
||||||
|
Return input as string
|
||||||
|
(decode if input is bytes)
|
||||||
|
"""
|
||||||
if isinstance(value, (bytes, bytearray)):
|
if isinstance(value, (bytes, bytearray)):
|
||||||
value = value.decode('utf-8')
|
value = value.decode('utf-8')
|
||||||
return value
|
return value
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ server {
|
|||||||
proxy_connect_timeout 7d;
|
proxy_connect_timeout 7d;
|
||||||
proxy_send_timeout 7d;
|
proxy_send_timeout 7d;
|
||||||
proxy_read_timeout 7d;
|
proxy_read_timeout 7d;
|
||||||
|
absolute_redirect off;
|
||||||
|
|
||||||
location = /ws {
|
location = /ws {
|
||||||
proxy_pass http://127.0.0.1:${TFW_WEB_PORT};
|
proxy_pass http://127.0.0.1:${TFW_WEB_PORT};
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from os.path import dirname, realpath, join
|
||||||
|
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
|
||||||
|
here = dirname(realpath(__file__))
|
||||||
|
|
||||||
|
with open(join(here, 'VERSION'), 'r') as ifile:
|
||||||
|
version = ifile.read().strip('\n')
|
||||||
|
with open(join(here, 'requirements.txt'), 'r') as ifile:
|
||||||
|
requirements = ifile.read().splitlines()
|
||||||
|
|
||||||
|
setup(name='tfw',
|
||||||
|
version=version,
|
||||||
|
description='Avatao tutorial-framework',
|
||||||
|
url='https://github.com/avatao-content/baseimage-tutorial-framework',
|
||||||
|
author='Avatao.com Innovative Learning Kft.',
|
||||||
|
author_email='support@avatao.com',
|
||||||
|
license='custom',
|
||||||
|
packages=find_packages('lib'),
|
||||||
|
package_dir={'': 'lib'},
|
||||||
|
install_requires=requirements,
|
||||||
|
zip_safe=False)
|
||||||
Reference in New Issue
Block a user