Compare commits

..
12 changed files with 94 additions and 21 deletions
+1 -1
View File
@@ -2,4 +2,4 @@
ignored-modules = zmq
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 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"; \
done
+4 -2
View File
@@ -15,10 +15,12 @@ LOG = logging.getLogger(__name__)
class DirectoryMonitor(ObserverMixin):
def __init__(self, directory):
def __init__(self, directories):
ObserverMixin.__init__(self)
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
@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
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
a command to be executed.
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 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 selected_file: file that is selected by default
: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)
try:
@@ -116,7 +123,11 @@ class IdeEventHandler(EventHandlerBase, MonitorManagerMixin):
selected_file=selected_file, exclude=exclude)
except IndexError:
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,
'write': self.write,
@@ -13,10 +13,9 @@ LOG = logging.getLogger(__name__)
class ProcessManager(SupervisorMixin):
def __init__(self):
self.commands = {'start': self.start_process,
'stop': self.stop_process,
'restart': self.restart_process,
'readlog': self.read_log}
self.commands = {'start': self.start_process,
'stop': self.stop_process,
'restart': self.restart_process}
def __call__(self, command, process_name):
return self.commands[command](process_name)
@@ -46,10 +45,11 @@ class ProcessManagingEventHandler(EventHandlerBase):
try:
data = message['data']
self.processmanager(data['command'], data['process_name'])
message['data']['log'] = self.processmanager.read_log_stdout(message['data']['process_name'])
return message
except KeyError:
LOG.error('IGNORING MESSAGE: Invalid message received: %s', message)
except SupervisorFault as fault:
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
+4 -3
View File
@@ -3,6 +3,7 @@
from abc import ABC
from re import match
from shlex import split
from tfw.config.logs import logging
@@ -57,10 +58,10 @@ class TerminalCommands(ABC):
return match(self._command_method_regex, string)
def callback(self, history):
parts = history[-1].split()
parts = split(history[-1])
command = parts[0]
if command in self.command_implemetations.keys():
try:
self.command_implemetations[command](*parts[1:])
except IndexError:
LOG.debug('Command "%s" failed!', command)
except Exception: # pylint: disable=broad-except
LOG.exception('Command "%s" failed:', command)
+3 -3
View File
@@ -7,10 +7,10 @@ LOG = logging.getLogger(__name__)
class MonitorManagerMixin:
def __init__(self, monitor_type, directory):
def __init__(self, monitor_type, directories):
self._monitor_type = monitor_type
self._monitor = None
self._monitored_directory = directory
self._monitored_directories = directories
self.reload_monitor()
@property
@@ -23,5 +23,5 @@ class MonitorManagerMixin:
self._monitor.stop()
except KeyError:
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
+14 -4
View File
@@ -19,11 +19,21 @@ class SupervisorMixin:
def start_process(self, process_name):
self.supervisor.startProcess(process_name)
def read_log(self, process_name):
logs = self.supervisor.readProcessStderrLog(process_name, 0, 0)
remove(self.supervisor.getProcessInfo(process_name)['stderr_logfile'])
def read_log_stdout(self, process_name):
return self._read_log_internal(self.supervisor.readProcessStdoutLog, process_name)
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)
return logs
def restart_process(self, process_name):
self.stop_process(process_name)
@@ -35,6 +35,9 @@ class ServerUplinkConnector(ZMQConnectorBase):
def send_to_eventhandler(self, message):
"""
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['key']: key of event handler you want to address
"""
@@ -46,6 +49,7 @@ class ServerUplinkConnector(ZMQConnectorBase):
def send(self, message):
"""
Send a message to the TFW server
:param message: JSON message you want to send
"""
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):
"""
Create TFW multipart data from message dict
"""
return _serialize_all(message['key'], message)
def deserialize_tfw_msg(*args):
"""
Return message from TFW multipart data
"""
return _deserialize_all(*args)[1]
@@ -45,12 +51,20 @@ def _deserialize_all(*args):
def _serialize_single(data):
"""
Return input as bytes
(serialize input if it is JSON)
"""
if not isinstance(data, str):
data = json.dumps(data)
return _encode_if_needed(data)
def _deserialize_single(data):
"""
Try parsing input as JSON, return it as
string if parsing fails.
"""
try:
return json.loads(data)
except ValueError:
@@ -58,12 +72,20 @@ def _deserialize_single(data):
def _encode_if_needed(value):
"""
Return input as bytes
(encode if input is string)
"""
if isinstance(value, str):
value = value.encode('utf-8')
return value
def _decode_if_needed(value):
"""
Return input as string
(decode if input is bytes)
"""
if isinstance(value, (bytes, bytearray)):
value = value.decode('utf-8')
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)