Make files observable and refactor event handlers

This commit is contained in:
R. Richard 2019-06-27 14:12:01 +02:00 committed by Kristóf Tóth
parent 778f155580
commit ffe512776a
6 changed files with 123 additions and 81 deletions

View File

@ -10,18 +10,28 @@ from tfw.components.inotify import InotifyObserver
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
BUILD_ARTIFACTS = [
class IdeInotifyObserver(InotifyObserver): "*.a",
def __init__(self, paths): "*.class",
self.uplink = TFWServerUplinkConnector() "*.dll",
super().__init__(paths) "*.dylib",
"*.elf",
def on_modified(self, event): "*.exe",
LOG.debug(event) "*.jar",
self.uplink.send_message({ "*.ko",
'key': 'ide', "*.la",
'data': {'command': 'reload'} "*.lib",
}, Scope.WEBSOCKET) "*.lo",
"*.o",
"*.obj",
"*.out",
"*.py[cod]",
"*.so",
"*.so.*",
"*.tar.gz",
"*.zip",
"*__pycache__*"
]
class IdeEventHandler(FrontendEventHandlerBase): class IdeEventHandler(FrontendEventHandlerBase):
@ -60,7 +70,14 @@ class IdeEventHandler(FrontendEventHandlerBase):
f'No file(s) in IdeEventHandler working_directory "{directory}"!' f'No file(s) in IdeEventHandler working_directory "{directory}"!'
) )
self.monitor = IdeInotifyObserver(self.filemanager.allowed_directories) self.monitor = InotifyObserver(self.filemanager.allowed_directories, exclude=BUILD_ARTIFACTS)
def on_modified(event):
LOG.debug(event)
self.server_connector.send_message({
'key': 'ide',
'data': {'command': 'reload'}
}, Scope.WEBSOCKET)
self.monitor.on_modified = on_modified
self.monitor.start() self.monitor.start()
self.commands = { self.commands = {

View File

@ -13,32 +13,26 @@ LOG = logging.getLogger(__name__)
class LogInotifyObserver(InotifyObserver, SupervisorLogMixin): class LogInotifyObserver(InotifyObserver, SupervisorLogMixin):
def __init__(self, process_name, log_tail=0): def __init__(self, server_connector, process_name, log_tail=0):
self.prevent_log_recursion() self.prevent_log_recursion()
self.uplink = TFWServerUplinkConnector() self.server_connector = server_connector
self.process_name = process_name self._process_name = process_name
self.log_tail = log_tail self.log_tail = log_tail
self.procinfo = self.supervisor.getProcessInfo(self.process_name) self.procinfo = self.supervisor.getProcessInfo(self.process_name)
super().__init__( InotifyObserver.__init__(self, [self.procinfo['stdout_logfile'], self.procinfo['stderr_logfile']])
[dirname(self.procinfo['stdout_logfile']), dirname(self.procinfo['stderr_logfile'])],
[self.procinfo['stdout_logfile'], self.procinfo['stderr_logfile']]
)
def reset(self, process_name, log_tail): @property
self.process_name = process_name def process_name(self):
self.log_tail = log_tail return self._process_name
self.procinfo = self.supervisor.getProcessInfo(self.process_name)
self.paths = [ @process_name.setter
dirname(self.procinfo['stdout_logfile']), def process_name(self, process_name):
dirname(self.procinfo['stderr_logfile']) self._process_name = process_name
] self.procinfo = self.supervisor.getProcessInfo(process_name)
self.patterns = [ self.paths = [self.procinfo['stdout_logfile'], self.procinfo['stderr_logfile']]
self.procinfo['stdout_logfile'],
self.procinfo['stderr_logfile']
]
def on_modified(self, event): def on_modified(self, event):
self.uplink.send_message({ self.server_connector.send_message({
'key': 'processlog', 'key': 'processlog',
'data': { 'data': {
'command': 'new_log', 'command': 'new_log',
@ -67,7 +61,7 @@ class LogMonitoringEventHandler(FrontendEventHandlerBase):
super().__init__(key) super().__init__(key)
self.process_name = process_name self.process_name = process_name
self.log_tail = log_tail self.log_tail = log_tail
self.monitor = LogInotifyObserver(process_name, log_tail) self.monitor = LogInotifyObserver(self.server_connector, process_name, log_tail)
self.monitor.start() self.monitor.start()
self.command_handlers = { self.command_handlers = {
@ -89,7 +83,7 @@ class LogMonitoringEventHandler(FrontendEventHandlerBase):
:param data: TFW message data containing 'value' :param data: TFW message data containing 'value'
(name of the process to monitor) (name of the process to monitor)
""" """
self.monitor.reset(data['value'], self.log_tail) self.monitor.process_name = data['value']
def handle_log_tail(self, data): def handle_log_tail(self, data):
""" """
@ -100,7 +94,7 @@ class LogMonitoringEventHandler(FrontendEventHandlerBase):
:param data: TFW message data containing 'value' :param data: TFW message data containing 'value'
(new tail length) (new tail length)
""" """
self.monitor.reset(self.process_name, data['value']) self.monitor.log_tail = data['value']
def cleanup(self): def cleanup(self):
self.monitor.stop() self.monitor.stop()

View File

@ -4,6 +4,7 @@
import logging import logging
from tfw.event_handlers import FrontendEventHandlerBase from tfw.event_handlers import FrontendEventHandlerBase
from tfw.components import BashMonitor
from tfw.components.terminado_mini_server import TerminadoMiniServer from tfw.components.terminado_mini_server import TerminadoMiniServer
from tfw.config import TFWENV from tfw.config import TFWENV
from tao.config import TAOENV from tao.config import TAOENV
@ -21,13 +22,13 @@ class TerminalEventHandler(FrontendEventHandlerBase):
a command to be executed. a command to be executed.
The API of each command is documented in their respective handler. The API of each command is documented in their respective handler.
""" """
def __init__(self, key, monitor): def __init__(self, key):
""" """
:param key: key this EventHandler listens to :param key: key this EventHandler listens to
:param monitor: tfw.components.HistoryMonitor instance to read command history from :param monitor: tfw.components.HistoryMonitor instance to read command history from
""" """
super().__init__(key) super().__init__(key)
self._historymonitor = monitor self._historymonitor = BashMonitor(self.server_connector, TFWENV.HISTFILE)
bash_as_user_cmd = ['sudo', '-u', TAOENV.USER, 'bash'] bash_as_user_cmd = ['sudo', '-u', TAOENV.USER, 'bash']
self.terminado_server = TerminadoMiniServer( self.terminado_server = TerminadoMiniServer(
@ -42,7 +43,6 @@ class TerminalEventHandler(FrontendEventHandlerBase):
'read': self.read 'read': self.read
} }
if self._historymonitor:
self._historymonitor.start() self._historymonitor.start()
self.terminado_server.listen() self.terminado_server.listen()
@ -84,5 +84,4 @@ class TerminalEventHandler(FrontendEventHandlerBase):
def cleanup(self): def cleanup(self):
self.terminado_server.stop() self.terminado_server.stop()
if self.historymonitor:
self.historymonitor.stop() self.historymonitor.stop()

View File

@ -1,6 +1,10 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft. # Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details. # All Rights Reserved. See LICENSE file for details.
from pwd import getpwnam
from grp import getgrnam
from pathlib import Path
from os import chown
from os.path import dirname from os.path import dirname
from re import findall from re import findall
from re import compile as compileregex from re import compile as compileregex
@ -22,30 +26,39 @@ class HistoryMonitor(ABC, InotifyObserver):
command pattern property and optionally the sanitize_command method. command pattern property and optionally the sanitize_command method.
See examples below. See examples below.
""" """
def __init__(self, domain, histfile): def __init__(self, uplink, histfile):
self.domain = domain self._domain = ''
self.histfile = histfile self.histfile = histfile
self._history = [] self.history = []
self._last_length = len(self._history) self._last_length = len(self.history)
self.uplink = TFWServerUplinkConnector() self.uplink = uplink
super().__init__(dirname(self.histfile), [self.histfile]) uid = getpwnam('user').pw_uid
gid = getgrnam('users').gr_gid
path = Path(self.histfile)
path.touch()
chown(self.histfile, uid, gid)
super().__init__(self.histfile)
@property
def domain(self):
return self._domain
@domain.setter
def domain(self, domain):
self._domain = domain
def on_modified(self, event): def on_modified(self, event):
self._fetch_history() self._fetch_history()
if self._last_length < len(self._history): if self._last_length < len(self.history):
for command in self._history[self._last_length:]: for command in self.history[self._last_length:]:
self.send_message(command) self.send_message(command)
@property
def history(self):
return self._history
def _fetch_history(self): def _fetch_history(self):
self._last_length = len(self._history) self._last_length = len(self.history)
with open(self.histfile, 'r') as ifile: with open(self.histfile, 'r') as ifile:
pattern = compileregex(self.command_pattern) pattern = compileregex(self.command_pattern)
data = ifile.read() data = ifile.read()
self._history = [ self.history = [
self.sanitize_command(command) self.sanitize_command(command)
for command in findall(pattern, data) for command in findall(pattern, data)
] ]
@ -76,8 +89,9 @@ class BashMonitor(HistoryMonitor):
shopt -s histappend shopt -s histappend
unset HISTCONTROL unset HISTCONTROL
""" """
def __init__(self, histfile): def __init__(self, uplink, histfile):
super().__init__('bash', histfile) super().__init__(uplink, histfile)
self.domain = 'bash'
@property @property
def command_pattern(self): def command_pattern(self):
@ -93,7 +107,8 @@ class GDBMonitor(HistoryMonitor):
For this to work "set trace-commands on" must be set in GDB. For this to work "set trace-commands on" must be set in GDB.
""" """
def __init__(self, histfile): def __init__(self, histfile):
super().__init__('gdb', histfile) super().__init__(histfile)
self.domain = 'gdb'
@property @property
def command_pattern(self): def command_pattern(self):

View File

@ -1,7 +1,8 @@
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
from time import time
from typing import Iterable from typing import Iterable
from time import time
from os.path import abspath, dirname, isfile
from watchdog.observers import Observer from watchdog.observers import Observer
from watchdog.events import FileSystemMovedEvent, PatternMatchingEventHandler from watchdog.events import FileSystemMovedEvent, PatternMatchingEventHandler
@ -65,28 +66,44 @@ class InotifyDirDeletedEvent(InotifyEvent):
class InotifyObserver: class InotifyObserver:
def __init__(self, paths, patterns=None, exclude=None, recursive=False): def __init__(self, path, patterns=[], exclude=None, recursive=False):
self._paths = paths self._files = []
self._paths = path
self._patterns = patterns self._patterns = patterns
self._exclude = exclude self._exclude = exclude
self._recursive = recursive self._recursive = recursive
self._observer = Observer() self._observer = Observer()
self._reset(paths, patterns, exclude) self._reset()
def _reset(self, paths, patterns, exclude): def _reset(self):
_dispatch_event = self._dispatch_event dispatch_event = self._dispatch_event
class TransformerEventHandler(PatternMatchingEventHandler): class TransformerEventHandler(PatternMatchingEventHandler):
def on_any_event(self, event): def on_any_event(self, event):
_dispatch_event(event) dispatch_event(event)
self.handler = TransformerEventHandler(patterns, exclude)
self._observer.unschedule_all() if isinstance(self._paths, str):
if isinstance(paths, str): self._paths = [self._paths]
self._observer.schedule(self.handler, paths, self._recursive) if isinstance(self._paths, Iterable):
elif isinstance(paths, Iterable): self._extract_files_from_paths()
for path in paths:
self._observer.schedule(self.handler, path, self._recursive)
else: else:
raise ValueError('Expected one or more strings representing paths.') raise ValueError('Expected one or more string paths.')
patterns = self._files+self.patterns
self.handler = TransformerEventHandler(patterns if patterns else None, self.exclude)
self._observer.unschedule_all()
for path in self.paths:
self._observer.schedule(self.handler, path, self._recursive)
def _extract_files_from_paths(self):
files, paths = [], []
for path in self._paths:
if isfile(path):
new_file = abspath(path)
files.append(new_file)
paths.append(dirname(new_file))
else:
paths.append(path)
self._files, self._paths = files, paths
@property @property
def paths(self): def paths(self):
@ -95,7 +112,7 @@ class InotifyObserver:
@paths.setter @paths.setter
def paths(self, paths): def paths(self, paths):
self._paths = paths self._paths = paths
self._reset(paths, self._patterns, self._exclude) self._reset()
@property @property
def patterns(self): def patterns(self):
@ -104,7 +121,7 @@ class InotifyObserver:
@patterns.setter @patterns.setter
def patterns(self, patterns): def patterns(self, patterns):
self._patterns = patterns self._patterns = patterns
self._reset(self._paths, patterns, self._exclude) self._reset()
@property @property
def exclude(self): def exclude(self):
@ -113,7 +130,7 @@ class InotifyObserver:
@exclude.setter @exclude.setter
def exclude(self, exclude): def exclude(self, exclude):
self._exclude = exclude self._exclude = exclude
self._reset(self._paths, self._patterns, exclude) self._reset()
def start(self): def start(self):
self._observer.start() self._observer.start()

View File

@ -11,8 +11,8 @@ from tempfile import TemporaryDirectory
import watchdog import watchdog
import pytest import pytest
from inotify import InotifyObserver from .inotify import InotifyObserver
from inotify import ( from .inotify import (
InotifyFileCreatedEvent, InotifyFileModifiedEvent, InotifyFileMovedEvent, InotifyFileCreatedEvent, InotifyFileModifiedEvent, InotifyFileMovedEvent,
InotifyFileDeletedEvent, InotifyDirCreatedEvent, InotifyDirModifiedEvent, InotifyFileDeletedEvent, InotifyDirCreatedEvent, InotifyDirModifiedEvent,
InotifyDirMovedEvent, InotifyDirDeletedEvent InotifyDirMovedEvent, InotifyDirDeletedEvent
@ -113,8 +113,8 @@ def test_create(context):
assert context.check_any() assert context.check_any()
def test_modify(context): def test_modify(context):
with open(context.subfile, 'w') as ofile: with open(context.subfile, 'wb', buffering=0) as ofile:
ofile.write('text') ofile.write(b'text')
context.check_event(InotifyFileModifiedEvent, context.subfile) context.check_event(InotifyFileModifiedEvent, context.subfile)
while True: while True:
try: try: