17 Commits
6 changed files with 108 additions and 117 deletions
+11 -6
View File
@@ -1,5 +1,5 @@
# pyinotifyd # pyinotifyd
A daemon to monitor filesystems events with inotify on Linux and run tasks like filesystem operations (copy, move or delete), a shell command or custom async python methods. A daemon for monitoring filesystem events with inotify on Linux and run tasks like filesystem operations (copy, move or delete), a shell command or custom async python methods.
It is possible to schedule tasks with a delay, which can then be canceled again in case a canceling event occurs. A useful example for this is to run tasks only if a file has not changed within a certain amount of time. It is possible to schedule tasks with a delay, which can then be canceled again in case a canceling event occurs. A useful example for this is to run tasks only if a file has not changed within a certain amount of time.
@@ -49,7 +49,7 @@ The basic idea is to instantiate one or multiple schedulers and map specific ino
pyinotifyd has different schedulers to schedule tasks with an optional delay. The advantages of using a scheduler are consistent logging and the possibility to cancel delayed tasks. Furthermore, schedulers have the ability to differentiate between files and directories. pyinotifyd has different schedulers to schedule tasks with an optional delay. The advantages of using a scheduler are consistent logging and the possibility to cancel delayed tasks. Furthermore, schedulers have the ability to differentiate between files and directories.
### TaskScheduler ### TaskScheduler
Schedule a custom python method *job* with an optional *delay* in seconds. Skip scheduling of tasks for files and/or directories according to *files* and *dirs* arguments. If there already is a scheduled task, re-schedule it with *delay*. Use *logname* in log messages. All additional modules, functions and variables that are defined in the config file and are needed within the *job*, need to be passed as dictionary to the TaskManager through *global_vars*. Schedule a custom python method *job* with an optional *delay* in seconds. Skip scheduling of tasks for files and/or directories according to *files* and *dirs* arguments. If there already is a scheduled task, re-schedule it with *delay*. Use *logname* in log messages. All additional modules, functions and variables that are defined in the config file and are needed within the *job*, need to be passed as dictionary to the TaskManager through *global_vars*. If you want to limit the scheduler to run only one job at a time, set *singlejob* to True.
All arguments except for *job* are optional. All arguments except for *job* are optional.
```python ```python
# Please note that pyinotifyd uses pythons asyncio for asynchronous task execution. # Please note that pyinotifyd uses pythons asyncio for asynchronous task execution.
@@ -71,7 +71,8 @@ task_sched = TaskScheduler(
dirs=False, dirs=False,
delay=0, delay=0,
logname="sched", logname="sched",
global_vars=globals()) global_vars=globals(),
singlejob=False)
``` ```
### ShellScheduler ### ShellScheduler
@@ -164,21 +165,23 @@ pyinotifyd = Pyinotifyd(
``` ```
### Watches ### Watches
A watch connects the *path* to an *event_map*. Automatically add a watch on each sub-directories in *path* if *rec* is set to True. If *auto_add* is True, a watch will be added automatically on newly created sub-directories in *path*. A watch connects the *path* to an *event_map*. Automatically add a watch on each sub-directories in *path* if *rec* is set to True. If *auto_add* is True, a watch will be added automatically on newly created sub-directories in *path*. All events for paths matching one of the regular expressions in *exclude_filter* are ignored. If the value of *exclude_filter* is a string, it is assumed to be a path to a file from which the list of regular expressions will be read.
```python ```python
# Add a watch directly to Pyinotifyd. # Add a watch directly to Pyinotifyd.
pyinotifyd.add_watch( pyinotifyd.add_watch(
path="/src_path", path="/src_path",
event_map=event_map, event_map=event_map,
rec=False, rec=False,
auto_add=False) auto_add=False,
exclude_filter=["^/src_path/subpath$"])
# Or instantiate and add it # Or instantiate and add it
w = Watch( w = Watch(
path="/src_path", path="/src_path",
event_map=event_map, event_map=event_map,
rec=False, rec=False,
auto_add=False) auto_add=False,
exclude_filter=["^/src_path/subpath$"])
pyinotifyd.add_watch(watch=w) pyinotifyd.add_watch(watch=w)
``` ```
@@ -214,6 +217,8 @@ enableSyslog(lglevel=INFO, name="daemon")
## Schedule python method for all events on files and directories ## Schedule python method for all events on files and directories
```python ```python
import logging
async def custom_job(event, task_id): async def custom_job(event, task_id):
logging.info(f"{task_id}: execute example task: {event}") logging.info(f"{task_id}: execute example task: {event}")
@@ -1,47 +0,0 @@
# Copyright 2020 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2
EAPI=7
PYTHON_COMPAT=( python3_{8..10} )
DISTUTILS_USE_SETUPTOOLS=rdepend
SCM=""
if [ "${PV#9999}" != "${PV}" ] ; then
SCM="git-r3"
EGIT_REPO_URI="https://github.com/spacefreak86/${PN}"
fi
inherit ${SCM} distutils-r1 systemd
DESCRIPTION="Monitore filesystems events and execute Python methods or Shell commands."
HOMEPAGE="https://github.com/spacefreak86/pymodmilter"
if [ "${PV#9999}" != "${PV}" ] ; then
SRC_URI=""
KEYWORDS=""
# Needed for tests
S="${WORKDIR}/${PN}"
EGIT_CHECKOUT_DIR="${S}"
else
SRC_URI="https://github.com/spacefreak86/${PN}/archive/${PV}.tar.gz -> ${P}.tar.gz"
KEYWORDS="amd64 x86"
fi
LICENSE="GPL-3"
SLOT="0"
IUSE="systemd"
RDEPEND="dev-python/pyinotify[${PYTHON_USEDEP}]"
python_install_all() {
distutils-r1_python_install_all
dodir /etc/${PN}
insinto /etc/${PN}
newins ${PN}/misc/config.py.default config.py
use systemd && systemd_dounit ${PN}/misc/${PN}.service
newinitd ${PN}/misc/openrc/${PN}.initd ${PN}
newconfd ${PN}/misc/openrc/${PN}.confd ${PN}
}
@@ -1,9 +1,9 @@
# Copyright 2020 Gentoo Authors # Copyright 2020 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2 # Distributed under the terms of the GNU General Public License v2
EAPI=7 EAPI=8
PYTHON_COMPAT=( python3_{7,8,9} ) PYTHON_COMPAT=( python3_{13..14} )
DISTUTILS_USE_SETUPTOOLS=rdepend DISTUTILS_USE_PEP517=setuptools
SCM="" SCM=""
if [ "${PV#9999}" != "${PV}" ] ; then if [ "${PV#9999}" != "${PV}" ] ; then
Executable → Regular
+55 -31
View File
@@ -31,12 +31,12 @@ import pyinotify
import signal import signal
import sys import sys
from pyinotify import ProcessEvent from pyinotify import ProcessEvent, ExcludeFilter
from pyinotifyd._install import install, uninstall from pyinotifyd._install import install, uninstall
from pyinotifyd.scheduler import TaskScheduler, Cancel from pyinotifyd.scheduler import TaskScheduler, Cancel
__version__ = "0.0.6" __version__ = "1.0.1"
def setLoglevel(loglevel, logname=None): def setLoglevel(loglevel, logname=None):
@@ -56,16 +56,15 @@ def enableSyslog(loglevel=None, address="/dev/log", logname=None):
class _SchedulerList: class _SchedulerList:
def __init__(self, schedulers=[], loop=None): def __init__(self, schedulers=[]):
if not isinstance(schedulers, list): if not isinstance(schedulers, list):
schedulers = [schedulers] schedulers = [schedulers]
self._schedulers = schedulers self._schedulers = schedulers
self._loop = (loop or asyncio.get_event_loop())
def process_event(self, event): def process_event(self, event):
for scheduler in self._schedulers: for scheduler in self._schedulers:
self._loop.create_task(scheduler.process_event(event)) asyncio.create_task(scheduler.process_event(event))
def schedulers(self): def schedulers(self):
return self._schedulers return self._schedulers
@@ -76,10 +75,10 @@ class EventMap(ProcessEvent):
**pyinotify.EventsCodes.OP_FLAGS, **pyinotify.EventsCodes.OP_FLAGS,
**pyinotify.EventsCodes.EVENT_FLAGS} **pyinotify.EventsCodes.EVENT_FLAGS}
def my_init(self, event_map=None, default_sched=None, loop=None, def my_init(self, event_map=None, default_sched=None, exclude_filter=None,
logname="eventmap"): logname="eventmap"):
self._map = {} self._map = {}
self._loop = (loop or asyncio.get_event_loop()) self._exclude_filter = None
if default_sched is not None: if default_sched is not None:
for flag in EventMap.flags: for flag in EventMap.flags:
@@ -91,6 +90,7 @@ class EventMap(ProcessEvent):
for flag, schedulers in event_map.items(): for flag, schedulers in event_map.items():
self.set_scheduler(flag, schedulers) self.set_scheduler(flag, schedulers)
self.set_exclude_filter(exclude_filter)
self._log = logging.getLogger((logname or __name__)) self._log = logging.getLogger((logname or __name__))
def set_scheduler(self, flag, schedulers): def set_scheduler(self, flag, schedulers):
@@ -106,27 +106,43 @@ class EventMap(ProcessEvent):
isinstance(scheduler, Cancel): isinstance(scheduler, Cancel):
instances.append(scheduler) instances.append(scheduler)
else: else:
instances.append( instances.append(TaskScheduler(scheduler))
TaskScheduler(scheduler, loop=self._loop))
self._map[flag] = _SchedulerList(instances, loop=self._loop) self._map[flag] = _SchedulerList(instances)
elif flag in self._map: elif flag in self._map:
del self._map[flag] del self._map[flag]
def set_exclude_filter(self, exclude_filter):
if exclude_filter is None:
self._exclude_filter = None
return
if not isinstance(exclude_filter, ExcludeFilter):
self._exclude_filter = ExcludeFilter(exclude_filter)
else:
self._exclude_filter = exclude_filter
def process_default(self, event): def process_default(self, event):
msg = "received event" attrs = ""
for attr in [ for attr in [
"dir", "mask", "maskname", "pathname", "src_pathname", "wd"]: "dir", "mask", "maskname", "pathname", "src_pathname", "wd"]:
value = getattr(event, attr, None) value = getattr(event, attr, None)
if attr == "mask": if attr == "mask":
value = hex(value) value = hex(value)
if value: if value:
msg += f", {attr}={value}" attrs += f", {attr}={value}"
self._log.debug(msg) self._log.debug(f"received event{attrs}")
maskname = event.maskname.split("|")[0] maskname = event.maskname.split("|")[0]
if maskname in self._map:
if maskname not in self._map:
return
if self._exclude_filter and self._exclude_filter(event.pathname):
self._log.debug(f"pathname {event.pathname} is excluded")
return
self._map[maskname].process_event(event) self._map[maskname].process_event(event)
def schedulers(self): def schedulers(self):
@@ -139,23 +155,32 @@ class EventMap(ProcessEvent):
class Watch: class Watch:
def __init__(self, path, event_map=None, default_sched=None, rec=False, def __init__(self, path, event_map=None, default_sched=None,
auto_add=False, logname="watch", loop=None): rec=False, auto_add=False, exclude_filter=None,
assert isinstance(path, str), \ logname="watch"):
f"path: expected {type('')}, got {type(path)}" assert (isinstance(path, str) or isinstance(path, list)), \
f"path: expected {type('')} or {type([])}, got {type(path)}"
if isinstance(event_map, EventMap): if isinstance(event_map, EventMap):
self._event_map = event_map self._event_map = event_map
else: else:
self._event_map = EventMap( self._event_map = EventMap(
event_map=event_map, default_sched=default_sched) event_map=event_map, default_sched=default_sched,
exclude_filter=exclude_filter)
assert isinstance(rec, bool), \ assert isinstance(rec, bool), \
f"rec: expected {type(bool)}, got {type(rec)}" f"rec: expected {type(bool)}, got {type(rec)}"
assert isinstance(auto_add, bool), \ assert isinstance(auto_add, bool), \
f"auto_add: expected {type(bool)}, got {type(auto_add)}" f"auto_add: expected {type(bool)}, got {type(auto_add)}"
self._exclude_filter = None
if exclude_filter:
if not isinstance(exclude_filter, ExcludeFilter):
self._exclude_filter = ExcludeFilter(exclude_filter)
else:
self._exclude_filter = exclude_filter
logname = (logname or __name__) logname = (logname or __name__)
self._loop = loop
self._path = path self._path = path
self._rec = rec self._rec = rec
@@ -171,14 +196,16 @@ class Watch:
def event_map(self): def event_map(self):
return self._event_map return self._event_map
def start(self, loop=None): def start(self):
loop = (loop or self._loop)
self._watch_manager.add_watch(self._path, pyinotify.ALL_EVENTS, self._watch_manager.add_watch(self._path, pyinotify.ALL_EVENTS,
rec=self._rec, auto_add=self._auto_add, rec=self._rec, auto_add=self._auto_add,
exclude_filter=self._exclude_filter,
do_glob=True) do_glob=True)
self._notifier = pyinotify.AsyncioNotifier( self._notifier = pyinotify.AsyncioNotifier(
self._watch_manager, loop, default_proc_fun=self._event_map) self._watch_manager,
asyncio.get_event_loop(),
default_proc_fun=self._event_map)
def stop(self): def stop(self):
self._notifier.stop() self._notifier.stop()
@@ -189,12 +216,10 @@ class Watch:
class Pyinotifyd: class Pyinotifyd:
name = "pyinotifyd" name = "pyinotifyd"
def __init__(self, watches=[], shutdown_timeout=30, logname="daemon", def __init__(self, watches=[], shutdown_timeout=30, logname="daemon"):
loop=None):
self.set_watches(watches) self.set_watches(watches)
self.set_shutdown_timeout(shutdown_timeout) self.set_shutdown_timeout(shutdown_timeout)
logname = (logname or __name__) logname = (logname or __name__)
self._loop = (loop or asyncio.get_event_loop())
self._log = logging.getLogger(logname) self._log = logging.getLogger(logname)
@@ -246,9 +271,7 @@ class Pyinotifyd:
schedulers.extend(w.event_map().schedulers()) schedulers.extend(w.event_map().schedulers())
return list(set(schedulers)) return list(set(schedulers))
def start(self, loop=None): def start(self):
loop = (loop or self._loop)
if len(self._watches) == 0: if len(self._watches) == 0:
self._log.warning( self._log.warning(
"no watches configured, the daemon will not do anything") "no watches configured, the daemon will not do anything")
@@ -256,7 +279,7 @@ class Pyinotifyd:
for watch in self._watches: for watch in self._watches:
self._log.info( self._log.info(
f"start listening for inotify events on '{watch.path()}'") f"start listening for inotify events on '{watch.path()}'")
watch.start(loop) watch.start()
def pause(self): def pause(self):
for scheduler in self.schedulers(): for scheduler in self.schedulers():
@@ -435,7 +458,7 @@ def main():
f"%(asctime)s - {name}/%(name)s - %(levelname)s - %(message)s") f"%(asctime)s - {name}/%(name)s - %(levelname)s - %(message)s")
ch.setFormatter(formatter) ch.setFormatter(formatter)
loop = asyncio.get_event_loop() loop = asyncio.new_event_loop()
loop.add_signal_handler( loop.add_signal_handler(
signal.SIGTERM, lambda: loop.create_task( signal.SIGTERM, lambda: loop.create_task(
daemon.shutdown("SIGTERM"))) daemon.shutdown("SIGTERM")))
@@ -445,6 +468,7 @@ def main():
loop.add_signal_handler( loop.add_signal_handler(
signal.SIGHUP, lambda: loop.create_task( signal.SIGHUP, lambda: loop.create_task(
daemon.reload("SIGHUP", args.config, args.debug))) daemon.reload("SIGHUP", args.config, args.debug)))
asyncio.set_event_loop(loop)
daemon.start() daemon.start()
loop.run_forever() loop.run_forever()
+11 -8
View File
@@ -6,15 +6,16 @@
#import logging #import logging
# #
#async def custom_job(event, task_id): #async def custom_job(event, task_id):
# asyncio.sleep(1) # await asyncio.sleep(1)
# logging.info(f"{task_id}: execute example task: {event}") # logging.info(f"{task_id}: execute example task: {event}")
# #
#task_sched = TaskScheduler( #task_sched = TaskScheduler(
# job=custom_job, # job=custom_job,
# files=True, # files=True,
# dirs=False, # dirs=False,
# delay=10 # delay=10,
# global_vars=globals()) # global_vars=globals(),
# singlejob=False)
########################### ###########################
@@ -25,7 +26,8 @@
# cmd="/usr/local/bin/task.sh {maskname} {pathname} {src_pathname}", # cmd="/usr/local/bin/task.sh {maskname} {pathname} {src_pathname}",
# files=True, # files=True,
# dirs=False, # dirs=False,
# delay=10) # delay=10,
# singlejob=False)
################################# #################################
@@ -34,8 +36,8 @@
#move_rule = Rule( #move_rule = Rule(
# action="move", # action="move",
# src_re="^/src_path/(?P<path>.*).to_move", # src_re=r"^/src_path/(?P<path>.*).to_move",
# dst_re="/dst_path/\g<path>.moved", # dst_re=r"/dst_path/\g<path>.moved",
# auto_create=True, # auto_create=True,
# filemode=0o755, # filemode=0o755,
# dirmode=0o644, # dirmode=0o644,
@@ -45,7 +47,7 @@
#delete_rule = Rule( #delete_rule = Rule(
# action="delete", # action="delete",
# src_re="^/src_path/(?P<path>.*).to_delete", # src_re=r"^/src_path/(?P<path>.*).to_delete",
# rec=False) # rec=False)
#file_sched = FileManagerScheduler( #file_sched = FileManagerScheduler(
@@ -86,7 +88,8 @@
# path="/watched/directory", # path="/watched/directory",
# event_map = event_map, # event_map = event_map,
# rec=True, # rec=True,
# auto_add=True) # auto_add=True,
# exclude_filter=["^/watched/directory/subpath$"])
################ ################
Executable → Regular
+26 -20
View File
@@ -52,7 +52,7 @@ class TaskScheduler:
self.cancelable = cancelable self.cancelable = cancelable
def __init__(self, job, files=True, dirs=False, delay=0, logname="sched", def __init__(self, job, files=True, dirs=False, delay=0, logname="sched",
loop=None, global_vars={}): global_vars={}, singlejob=False):
assert iscoroutinefunction(job), \ assert iscoroutinefunction(job), \
f"job: expected coroutine, got {type(job)}" f"job: expected coroutine, got {type(job)}"
assert isinstance(files, bool), \ assert isinstance(files, bool), \
@@ -69,8 +69,8 @@ class TaskScheduler:
self._dirs = dirs self._dirs = dirs
self._delay = delay self._delay = delay
self._log = logging.getLogger((logname or __name__)) self._log = logging.getLogger((logname or __name__))
self._loop = (loop or asyncio.get_event_loop())
self._globals = global_vars self._globals = global_vars
self._singlejob = singlejob
self._tasks = {} self._tasks = {}
self._pause = False self._pause = False
@@ -90,8 +90,7 @@ class TaskScheduler:
self._log.info( self._log.info(
f"wait {timeout} seconds for {len(pending)} " f"wait {timeout} seconds for {len(pending)} "
f"remaining task(s) to complete") f"remaining task(s) to complete")
done, pending = await asyncio.wait([*pending], timeout=timeout, done, pending = await asyncio.wait([*pending], timeout=timeout)
loop=self._loop)
if pending: if pending:
self._log.warning( self._log.warning(
f"shutdown timeout exceeded, " f"shutdown timeout exceeded, "
@@ -99,20 +98,23 @@ class TaskScheduler:
for task in pending: for task in pending:
task.cancel() task.cancel()
try: try:
await asyncio.gather(*pending, loop=self._loop) await asyncio.gather(*pending)
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
else: else:
self._log.info("all remainig tasks completed") self._log.info("all remainig tasks completed")
def taskindex(self, event):
return "singlejob" if self._singlejob else event.pathname
async def _run_job(self, event, task_state, restart=False): async def _run_job(self, event, task_state, restart=False):
logger = SchedulerLogger(self._log, { logger = SchedulerLogger(self._log, {
"event": event, "event": event,
"id": task_state.id}) "id": task_state.id})
if self._delay > 0: if self._delay > 0:
task_state.task = self._loop.create_task( task_state.task = asyncio.create_task(
asyncio.sleep(self._delay, loop=self._loop)) asyncio.sleep(self._delay))
try: try:
if restart: if restart:
prefix = "re-" prefix = "re-"
@@ -130,11 +132,11 @@ class TaskScheduler:
local_vars = {"self": self, local_vars = {"self": self,
"event": event, "event": event,
"task_id": task_state.id} "task_id": task_state.id}
task_state.task = self._loop.create_task( task_state.task = asyncio.create_task(
eval("self._job(event, task_id)", self._globals, local_vars)) eval("self._job(event, task_id)", self._globals, local_vars))
else: else:
task_state.task = self._loop.create_task( task_state.task = asyncio.create_task(
self._job(event, task_state.id)) self._job(event, task_state.id))
try: try:
@@ -145,7 +147,8 @@ class TaskScheduler:
else: else:
logger.info("task finished") logger.info("task finished")
finally: finally:
del self._tasks[event.pathname] task_index = self.taskindex(event)
del self._tasks[task_index]
async def process_event(self, event): async def process_event(self, event):
if not ((not event.dir and self._files) or if not ((not event.dir and self._files) or
@@ -153,9 +156,13 @@ class TaskScheduler:
return return
restart = False restart = False
task_index = self.taskindex(event)
try: try:
task_state = self._tasks[event.pathname] task_state = self._tasks[task_index]
except KeyError:
task_state = TaskScheduler.TaskState()
self._tasks[task_index] = task_state
else:
logger = SchedulerLogger(self._log, { logger = SchedulerLogger(self._log, {
"event": event, "event": event,
"id": task_state.id}) "id": task_state.id})
@@ -171,16 +178,13 @@ class TaskScheduler:
logger.warning("skip event due to ongoing task") logger.warning("skip event due to ongoing task")
return return
except KeyError:
task_state = TaskScheduler.TaskState()
self._tasks[event.pathname] = task_state
if not self._pause: if not self._pause:
await self._run_job(event, task_state, restart) await self._run_job(event, task_state, restart)
async def process_cancel_event(self, event): async def process_cancel_event(self, event):
try: try:
task_state = self._tasks[event.pathname] task_index = self.taskindex(event)
task_state = self._tasks[task_index]
except KeyError: except KeyError:
return return
@@ -192,7 +196,8 @@ class TaskScheduler:
task_state.task.cancel() task_state.task.cancel()
logger.info("scheduled task cancelled") logger.info("scheduled task cancelled")
task_state.task = None task_state.task = None
del self._tasks[event.pathname] logger.info(f"{task_index}")
del self._tasks[task_index]
else: else:
logger.warning("skip event due to ongoing task") logger.warning("skip event due to ongoing task")
@@ -237,7 +242,7 @@ class ShellScheduler(TaskScheduler):
logger.info(f"execute shell command, cmd={cmd}") logger.info(f"execute shell command, cmd={cmd}")
try: try:
proc = await asyncio.create_subprocess_shell(cmd, loop=self._loop) proc = await asyncio.create_subprocess_shell(cmd)
await proc.communicate() await proc.communicate()
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
@@ -285,7 +290,8 @@ class FileManagerRule:
class FileManagerScheduler(TaskScheduler): class FileManagerScheduler(TaskScheduler):
def __init__(self, rules, job=None, *args, **kwargs): def __init__(self, rules, job=None, *args, **kwargs):
super().__init__(*args, **kwargs, job=self._manager_job) super().__init__(
*args, **kwargs, job=self._manager_job, singlejob=False)
if not isinstance(rules, list): if not isinstance(rules, list):
rules = [rules] rules = [rules]