Compare commits

..
11 changed files with 90 additions and 18 deletions
+1 -1
View File
@@ -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
View File
@@ -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
+3 -1
View File
@@ -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()
for directory in directories:
self.observer.schedule(self.eventhandler, directory, recursive=True) 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
+13 -2
View File
@@ -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,
@@ -15,8 +15,7 @@ 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
+3 -3
View File
@@ -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
+14 -4
View File
@@ -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))
+22
View File
@@ -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
+23
View File
@@ -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)