50 Commits
Author SHA1 Message Date
spacefreak f02e1948b7 change README.md 2026-08-18 11:57:16 +02:00
spacefreak a32638860e raise version to 1.0.2 2026-08-18 10:48:42 +02:00
spacefreak 3afcddaeb6 fix default config and raise version to 1.0.1 2026-08-18 10:36:52 +02:00
spacefreak 05eb46a0bd fix Gentoo ebuild 2026-08-18 10:17:38 +02:00
spacefreak 53eb8f48ed fixes for Python 3.14 and change version to 1.0.0 2026-08-18 10:14:19 +02:00
spacefreak 5556902e54 change version to 0.0.10 2023-04-03 14:49:06 +02:00
spacefreak 5a019f1352 change python dependencies in gentoo ebuild 2023-03-30 14:27:58 +02:00
spacefreak 56b19120f4 fix version 2023-03-30 14:10:56 +02:00
spacefreak 546b8c4a2d remove loop parameter (fix for python 3.10) 2023-03-30 14:09:54 +02:00
spacefreak 8d8e08d94f change version to 0.0.9 2022-10-20 17:53:05 +02:00
spacefreak 0ea2dadea4 fix version 2022-10-20 17:51:24 +02:00
spacefreak 006d631682 add exclude_filter to ignore events to paths by regex 2022-10-20 17:48:48 +02:00
spacefreak 669129d919 change version to 0.0.8 2022-09-08 13:49:29 +02:00
spacefreak 961c64e422 remove version specific Gentoo ebuild 2022-09-08 13:46:19 +02:00
spacefreak c22bd73759 add argument singlejob to TaskScheduler 2022-09-08 13:44:42 +02:00
spacefreak 0ac52b23d4 change README.md 2022-09-04 11:28:38 +02:00
spacefreak 88ce35930c change README.md 2022-09-04 11:23:17 +02:00
spacefreak 01ccd1817d fix pyinotifyd-9999.ebuild 2022-08-12 10:02:37 +02:00
spacefreak b80d71e95e change version to 0.0.7 2022-08-12 09:59:44 +02:00
spacefreak d07316990e fix setup.py 2022-08-12 09:57:00 +02:00
spacefreak 327bd919b9 cleanup 2022-08-12 09:52:52 +02:00
spacefreak a32725504d change README.md 2022-08-12 09:50:29 +02:00
spacefreak f0b578bdb8 change PyPI distribution scripts 2022-08-12 09:43:36 +02:00
spacefreak 641d467069 change README.md and default config file 2022-08-12 09:43:18 +02:00
spacefreak f71af57288 fix installation of systemd service file 2022-08-12 09:30:52 +02:00
spacefreak 085f8f1134 change ebuild to 0.0.6 and switch to stable 2022-08-12 09:22:06 +02:00
spacefreak 50d0dbef79 fix global variable handling in config 2022-08-12 09:16:27 +02:00
spacefreak 914ec8cfb9 fix dependencies in init script 2020-11-27 11:04:34 +01:00
spacefreak 769687bca6 fix log message 2020-11-26 13:39:06 +01:00
spacefreak f9ee0c92cb provide ebuild for version 0.0.5 2020-11-24 14:13:49 +01:00
spacefreak 1a9d695374 change version to 0.0.6 2020-11-24 14:13:04 +01:00
spacefreak 9794b89b5a fix TaskState class 2020-11-24 13:25:42 +01:00
spacefreak 8445ca7cb4 fix signal handling 2020-11-24 13:20:38 +01:00
spacefreak 75241f60f4 introduce beta status 2020-11-10 01:58:08 +01:00
spacefreak 99724905be change README.md 2020-11-10 01:22:13 +01:00
spacefreak 34ac9cd596 fix typo in README.md 2020-11-10 00:57:30 +01:00
spacefreak 5b589cc999 change version to 0.0.5 2020-11-10 00:55:42 +01:00
spacefreak d1b1fc9a4e change README.md and provide gentoo ebuild for version 0.0.4 2020-11-10 00:53:39 +01:00
spacefreak 13dbfeb8ee always uninstall everything we might have installed 2020-11-09 23:58:44 +01:00
spacefreak d6a91d6a5f improve install/uninstall routines 2020-11-09 23:51:13 +01:00
spacefreak fe3ecc0fa6 fix typo in README.md 2020-11-09 22:45:35 +01:00
spacefreak b1dffffb5c change README.md 2020-11-09 22:27:06 +01:00
spacefreak 6504662cb8 fix __all__ in __init__.py and change version to 0.0.4 2020-11-09 22:23:28 +01:00
spacefreak e8fe7915c1 change gentoo ebuild version to 0.0.3 2020-11-09 22:18:57 +01:00
spacefreak 4726c17366 change README.md 2020-11-09 22:18:00 +01:00
spacefreak 1a8e7ccb92 improve logging config 2020-11-09 22:15:19 +01:00
spacefreak dbe7ba83b6 change README.md 2020-11-09 21:23:10 +01:00
spacefreak 56a8586168 change README.md 2020-11-09 20:39:18 +01:00
spacefreak 977f06436d fix gentoo ebuilds 2020-11-09 20:16:04 +01:00
spacefreak 2441ba2048 change version to 0.0.3 2020-11-09 20:12:18 +01:00
13 changed files with 325 additions and 253 deletions
-3
View File
@@ -117,6 +117,3 @@ dmypy.json
# Temporary Vim files # Temporary Vim files
.*.swp .*.swp
# config file
/config.py
+48 -43
View File
@@ -1,6 +1,9 @@
# pyinotifyd # pyinotifyd
A daemon to monitor filesystems events with inotify on Linux and run tasks like filesystem operations (copy, move or delete), a shell commands 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, delayed tasks can be cancelled again in case a certain event occurs. A useful example would be 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.
pyinotifyd offers great flexibility through its dev-op configuration approach, which enables you to do almost anything you want.
# Requirements # Requirements
* [pyinotify](https://github.com/seb-m/pyinotify) * [pyinotify](https://github.com/seb-m/pyinotify)
@@ -46,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. 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.
@@ -55,6 +58,9 @@ All arguments except for *job* are optional.
# Bad: time.sleep(10) # Bad: time.sleep(10)
# Good: await asyncio.sleep(10) # Good: await asyncio.sleep(10)
import asyncio
import logging
async def custom_job(event, task_id): async def custom_job(event, task_id):
await asyncio.sleep(10) await asyncio.sleep(10)
logging.info(f"{task_id}: execute example task: {event}") logging.info(f"{task_id}: execute example task: {event}")
@@ -64,13 +70,16 @@ task_sched = TaskScheduler(
files=True, files=True,
dirs=False, dirs=False,
delay=0, delay=0,
logname="sched") logname="sched",
global_vars=globals(),
singlejob=False)
``` ```
### ShellScheduler ### ShellScheduler
Schedule a shell command *cmd*. Replace **{maskname}**, **{pathname}** and **{src_pathname}** in *cmd* with the actual values of occuring events. This scheduler is based on TaskScheduler and has the same optional arguments. Schedule a shell command *cmd*. Replace **{maskname}**, **{pathname}** and **{src_pathname}** in *cmd* with the actual values of occuring events. This scheduler is based on TaskScheduler and has the same optional arguments.
```python ```python
# Please note that **{src_pathname}** is only present for IN_MOVED_TO events and only in the case where the IN_MOVED_FROM events are watched too. # Please note that **{src_pathname}** is only present for IN_MOVED_TO events and only
# in the case where the IN_MOVED_FROM events are watched too.
# If it is not present, the command line argument will be an empty string. # If it is not present, the command line argument will be an empty string.
shell_sched = ShellScheduler( shell_sched = ShellScheduler(
cmd="/usr/local/bin/task.sh {maskname} {pathname} {src_pathname}") cmd="/usr/local/bin/task.sh {maskname} {pathname} {src_pathname}")
@@ -86,8 +95,8 @@ If *action* is delete, delete non-empty directories if *rec* is set to True.
```python ```python
move_rule = FileManagerRule( move_rule = FileManagerRule(
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>", dst_re=r"/dst_path/\g<path>",
auto_create=False, auto_create=False,
rec=False, rec=False,
filemode=None, filemode=None,
@@ -98,7 +107,7 @@ move_rule = FileManagerRule(
delete_rule = FileManagerRule( delete_rule = FileManagerRule(
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(
@@ -126,7 +135,7 @@ event_map = {
"IN_Q_OVERFLOW": None, "IN_Q_OVERFLOW": None,
"IN_UNMOUNT": Cancel(task_sched)} "IN_UNMOUNT": Cancel(task_sched)}
# It is possible to instantiate an event map with a default scheduler set for every event, # It is possible to instantiate an event map with a default scheduler set for every event
event_map = EventMap(default_sched=task_sched) event_map = EventMap(default_sched=task_sched)
``` ```
The following events are available: The following events are available:
@@ -156,64 +165,60 @@ 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( pyinotifyd.add_watch(watch=w)
watch=w)
``` ```
## Logging ## Logging
Pythons [logging](https://docs.python.org/3/howto/logging.html) framework is used to log messages (see https://docs.python.org/3/howto/logging.html). Pythons [logging](https://docs.python.org/3/howto/logging.html) framework is used to log messages (see https://docs.python.org/3/howto/logging.html).
The following loglevels are available:
* DEBUG
* INFO
* WARNING
* ERROR
* CRITICAL
```python
# Configure global loglevel
setLoglevel(INFO)
Configure the global loglevel. This is the default: # Configure loglevel per logname.
```python setLoglevel(INFO, logname="daemon")
logging.getLogger().setLevel(logging.WARNING)
```
It is possible to configure the loglevel per *logname*. This is an example for logname **sched**:
```python
logging.getLogger("sched").setLevel(logging.INFO)
``` ```
### Syslog ### Syslog
Add this to your config file to send log messages to a local syslog server. Send log messages to the local syslog server.
```python ```python
# send log messages to the Unix socket of the syslog server. # Enable logging to local syslog server (/dev/log).
syslog = logging.handlers.SysLogHandler( # Use *address* to specify a different target.
address="/dev/log") enableSyslog(loglevel=INFO, address="/dev/log")
# set the log format of syslog messages # Enable syslog per logname
log_format = "pyinotifyd/%(name)s: %(message)s" enableSyslog(lglevel=INFO, name="daemon")
syslog.setFormatter(
logging.Formatter(formatter)
# set the log level for syslog messages
syslog.setLevel(logging.INFO)
# enable syslog for pyinotifyd
logging.getLogger().addHandler(syslog)
# or enable syslog just for the daemon
logging.getLogger("daemon").addHandler(syslog)
``` ```
# Examples # Examples
## 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}")
@@ -255,23 +260,23 @@ pyinotifyd.add_watch(
```python ```python
move_rule = FileManagerRule( move_rule = FileManagerRule(
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>", dst_re=r"/dst_path/\g<path>",
auto_create=True, auto_create=True,
filemode=0o644, filemode=0o644,
dirmode=0o755) dirmode=0o755)
copy_rule = FileManagerRule( copy_rule = FileManagerRule(
action="copy", action="copy",
src_re="^/src_path/(?P<path>.*)\.to_copy$", src_re=r"^/src_path/(?P<path>.*)\.to_copy$",
dst_re="/dst_path/\g<path>", dst_re=r"/dst_path/\g<path>",
auto_create=True, auto_create=True,
filemode=0o644, filemode=0o644,
dirmode=0o755) dirmode=0o755)
delete_rule = FileManagerRule( delete_rule = FileManagerRule(
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(
@@ -1,47 +0,0 @@
# Copyright 2020 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2
EAPI=7
PYTHON_COMPAT=( python3_{7,8,9} )
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.example 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
@@ -38,7 +38,7 @@ python_install_all() {
dodir /etc/${PN} dodir /etc/${PN}
insinto /etc/${PN} insinto /etc/${PN}
newins ${PN}/misc/config.py.example config.py newins ${PN}/misc/config.py.default config.py
use systemd && systemd_dounit ${PN}/misc/${PN}.service use systemd && systemd_dounit ${PN}/misc/${PN}.service
+1 -1
View File
@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
set -e set -e
set -x
PYTHON=$(which python) PYTHON=$(which python)
script_dir=$(dirname "$(readlink -f -- "$BASH_SOURCE")") script_dir=$(dirname "$(readlink -f -- "$BASH_SOURCE")")
+1
View File
@@ -7,6 +7,7 @@ script_dir=$(dirname "$(readlink -f -- "$BASH_SOURCE")")
pkg_dir=$(realpath "${script_dir}/../..") pkg_dir=$(realpath "${script_dir}/../..")
cd "${pkg_dir}/dist" cd "${pkg_dir}/dist"
ls -la
msg="Select version to distribute (cancel with CTRL+C):" msg="Select version to distribute (cancel with CTRL+C):"
echo "${msg}" echo "${msg}"
select version in $(find . -maxdepth 1 -type f -name "pyinotifyd-*.*.*.tar.gz" -printf "%f\n" | sed "s#\.tar\.gz##g"); do select version in $(find . -maxdepth 1 -type f -name "pyinotifyd-*.*.*.tar.gz" -printf "%f\n" | sed "s#\.tar\.gz##g"); do
Executable → Regular
+86 -43
View File
@@ -15,7 +15,9 @@
# #
__all__ = [ __all__ = [
"EventMap" "setLoglevel",
"enableSyslog",
"EventMap",
"Watch", "Watch",
"Pyinotifyd", "Pyinotifyd",
"DaemonInstance", "DaemonInstance",
@@ -29,25 +31,40 @@ 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.2" __version__ = "1.0.2"
def setLoglevel(loglevel, logname=None):
logger = logging.getLogger(logname)
logger.setLevel(loglevel)
def enableSyslog(loglevel=None, address="/dev/log", logname=None):
logger = logging.getLogger(logname)
syslog = logging.handlers.SysLogHandler(address=address)
syslog.setFormatter(
logging.Formatter(f"{Pyinotifyd.name}/%(name)s: %(message)s"))
if loglevel:
syslog.setLevel(loglevel)
logger.addHandler(syslog)
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
@@ -58,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:
@@ -73,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):
@@ -88,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):
@@ -121,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
@@ -153,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()
@@ -171,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)
@@ -184,11 +227,13 @@ class Pyinotifyd:
def from_cfg_file(config_file): def from_cfg_file(config_file):
config = {} config = {}
name = Pyinotifyd.name name = Pyinotifyd.name
exec("import logging", {}, config) exec("from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL",
exec(f"from {name} import Pyinotifyd, Watch", {}, config) config)
exec(f"from {name}.scheduler import *", {}, config) exec(f"from {name} import Pyinotifyd, Watch", config)
exec(f"from {name} import setLoglevel, enableSyslog", config)
exec(f"from {name}.scheduler import *", config)
with open(config_file, "r") as fh: with open(config_file, "r") as fh:
exec(fh.read(), {}, config) exec(fh.read(), config)
instance = config[f"{name}"] instance = config[f"{name}"]
assert isinstance(instance, Pyinotifyd), \ assert isinstance(instance, Pyinotifyd), \
f"{name}: expected {type(Pyinotifyd)}, " \ f"{name}: expected {type(Pyinotifyd)}, " \
@@ -226,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")
@@ -236,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():
@@ -415,17 +458,17 @@ 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()
for signame in ["SIGINT", "SIGTERM"]:
loop.add_signal_handler( loop.add_signal_handler(
getattr(signal, signame), signal.SIGTERM, lambda: loop.create_task(
lambda: loop.create_task( daemon.shutdown("SIGTERM")))
daemon.shutdown(signame)))
loop.add_signal_handler( loop.add_signal_handler(
getattr(signal, "SIGHUP"), signal.SIGINT, lambda: loop.create_task(
lambda: loop.create_task( daemon.shutdown("SIGINT")))
loop.add_signal_handler(
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()
+102 -61
View File
@@ -14,15 +14,101 @@
# along with pyinotifyd. If not, see <http://www.gnu.org/licenses/>. # along with pyinotifyd. If not, see <http://www.gnu.org/licenses/>.
# #
import filecmp
import logging import logging
import os import os
import shutil import shutil
import sys import sys
SYSTEMD_PATH = "/lib/systemd/system"
SYSTEMD_PATHS = ["/lib/systemd/system", "/usr/lib/systemd/system"]
OPENRC = "/sbin/openrc" OPENRC = "/sbin/openrc"
def _systemd_files(pkg_dir, name):
for path in SYSTEMD_PATHS:
if os.path.isdir(path):
break
return [
(f"{pkg_dir}/misc/systemd/{name}.service",
f"{path}/{name}.service", True)]
def _openrc_files(pkg_dir, name):
return [
(f"{pkg_dir}/misc/openrc/{name}.initd", f"/etc/init.d/{name}", True),
(f"{pkg_dir}/misc/openrc/{name}.confd", f"/etc/conf.d/{name}", False)]
def _config_files(pkg_dir, name):
return [
(f"{pkg_dir}/misc/config.py.default", f"/etc/{name}/config.py", False)]
def _install_files(files):
for src, dst, force in files:
if os.path.exists(dst):
if os.path.isdir(dst):
logging.error(
" => unable to copy file, destination path is a directory")
continue
elif not force:
logging.info(f" => file {dst} already exists")
continue
try:
logging.info(f" => install file {dst}")
shutil.copy2(src, dst)
except Exception as e:
logging.error(f" => unable to install file {dst}: {e}")
def _uninstall_files(files):
for src, dst, force in files:
if not os.path.isfile(dst):
continue
if not force and not filecmp.cmp(src, dst, shallow=True):
logging.warning(
f" => keep modified file {dst}, "
f"you have to remove it manually")
continue
try:
logging.info(f" => uninstall file {dst}")
os.remove(dst)
except Exception as e:
logging.error(f" => unable to uninstall file {dst}: {e}")
def _create_dir(path):
if os.path.isdir(path):
logging.info(f" => directory {path} already exists")
else:
try:
logging.info(f" => create directory {path}")
os.mkdir(path)
except Exception as e:
logging.error(f" => unable to create directory {path}: {e}")
return False
return True
def _delete_dir(path):
if os.path.isdir(path):
if not os.listdir(path):
try:
logging.info(f" => delete directory {path}")
os.rmdir(path)
except Exception as e:
logging.error(f" => unable to delete directory {path}: {e}")
else:
logging.warning(f" => keep non-empty directory {path}")
def _check_root(): def _check_root():
if os.getuid() != 0: if os.getuid() != 0:
logging.error("you need to have root privileges, please try again") logging.error("you need to have root privileges, please try again")
@@ -32,7 +118,11 @@ def _check_root():
def _check_systemd(): def _check_systemd():
systemd = os.path.isdir(SYSTEMD_PATH) for path in SYSTEMD_PATHS:
systemd = os.path.isdir(path)
if systemd:
break
if systemd: if systemd:
logging.info("systemd detected") logging.info("systemd detected")
@@ -47,37 +137,6 @@ def _check_openrc():
return openrc return openrc
def _copy_missing_file(src, dst):
if os.path.exists(dst):
logging.info(f" => file {dst} already installed")
else:
try:
logging.info(f" => install file {dst}")
shutil.copy2(src, dst)
except Exception as e:
logging.error(f" => unable to install file {dst}: {e}")
def _delete_present_file(f):
if os.path.isfile(f):
try:
logging.info(f" => uninstall file {f}")
os.remove(f)
except Exception as e:
logging.error(f" => unable to uninstall file {f}: {e}")
def _warn_exists(path):
if os.path.isdir(path):
logging.warning(
f" => directory {path} is still present, "
f"you have to remove it manually")
else:
logging.warning(
f" => file {path} is still present, "
f"you have to remove it manually")
def install(name): def install(name):
if not _check_root(): if not _check_root():
sys.exit(2) sys.exit(2)
@@ -85,33 +144,16 @@ def install(name):
pkg_dir = os.path.dirname(__file__) pkg_dir = os.path.dirname(__file__)
if _check_systemd(): if _check_systemd():
dst = f"{SYSTEMD_PATH}/{name}.service" _install_files(_systemd_files(pkg_dir, name))
src = f"{pkg_dir}/misc/systemd/{name}.service"
_copy_missing_file(src, dst)
if _check_openrc(): if _check_openrc():
files = [ _install_files(_openrc_files(pkg_dir, name))
(f"{pkg_dir}/misc/openrc/{name}.initd", f"/etc/init.d/{name}"),
(f"{pkg_dir}/misc/openrc/{name}.confd", f"/etc/conf.d/{name}")]
for src, dst in files:
_copy_missing_file(src, dst)
logging.info("install configuration file") if not _create_dir(f"/etc/{name}"):
config_dir = f"/etc/{name}" logging.error(" => unable to create config dir, giving up ...")
if os.path.isdir(config_dir):
logging.info(f" => directory {config_dir} already exists")
else:
try:
logging.info(f" => create directory {config_dir}")
os.mkdir(config_dir)
except Exception as e:
logging.error(f" => unable to create directory {config_dir}: {e}")
sys.exit(3) sys.exit(3)
files = [ _install_files(_config_files(pkg_dir, name))
(f"{pkg_dir}/misc/config.py.default", f"{config_dir}/config.py")]
for src, dst in files:
_copy_missing_file(src, dst)
logging.info(f"{name} successfully installed") logging.info(f"{name} successfully installed")
@@ -120,13 +162,12 @@ def uninstall(name):
if not _check_root(): if not _check_root():
sys.exit(2) sys.exit(2)
if _check_systemd(): pkg_dir = os.path.dirname(__file__)
_delete_present_file(f"{SYSTEMD_PATH}/{name}.service")
if _check_openrc(): _uninstall_files(_systemd_files(pkg_dir, name))
_delete_present_file(f"/etc/init.d/{name}") _uninstall_files(_openrc_files(pkg_dir, name))
_warn_exists(f"/etc/conf.d/{name}") _uninstall_files(_config_files(pkg_dir, name))
_warn_exists(f"/etc/{name}") _delete_dir(f"/etc/{name}")
logging.info(f"{name} successfully uninstalled") logging.info(f"{name} successfully uninstalled")
+38 -6
View File
@@ -2,14 +2,20 @@
# TaskScheduler config # # TaskScheduler config #
########################## ##########################
#import asyncio
#import logging
#
#async def custom_job(event, task_id): #async def custom_job(event, task_id):
# 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(),
# singlejob=False)
########################### ###########################
@@ -20,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)
################################# #################################
@@ -29,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,
@@ -40,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(
@@ -81,4 +88,29 @@
# 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$"])
################
# Log config #
################
# set global loglevel
#setLoglevel(DEBUG)
# set loglevel per logname
#setLoglevel(
# DEBUG,
# logname="daemon")
# enable syslog
#enableSyslog(
# loglevel=DEBUG,
# address="/dev/log")
# enable syslog per logname
#enableSyslog(
# loglevel=DEBUG,
# address="/dev/log",
# logname="sched")
-5
View File
@@ -13,11 +13,6 @@ retry="SIGTERM/${shutdown_timeout}"
extra_commands="configtest reload" extra_commands="configtest reload"
depend() {
need net
before mta
}
checkconfig() { checkconfig() {
output=$(${command} ${command_args} -t 2>&1) output=$(${command} ${command_args} -t 2>&1)
ret=$? ret=$?
Executable → Regular
+40 -26
View File
@@ -25,7 +25,6 @@ import os
import re import re
import shutil import shutil
from dataclasses import dataclass
from inspect import iscoroutinefunction from inspect import iscoroutinefunction
from shlex import quote as shell_quote from shlex import quote as shell_quote
from uuid import uuid4 from uuid import uuid4
@@ -46,14 +45,14 @@ class SchedulerLogger(logging.LoggerAdapter):
class TaskScheduler: class TaskScheduler:
@dataclass
class TaskState: class TaskState:
id: str = str(uuid4()) def __init__(self, task_id=None, task=None, cancelable=True):
task: asyncio.Task = None self.id = task_id or str(uuid4())
cancelable: bool = True self.task = task
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={}, 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), \
@@ -62,14 +61,16 @@ class TaskScheduler:
f"dirs: expected {type(bool)}, got {type(dirs)}" f"dirs: expected {type(bool)}, got {type(dirs)}"
assert isinstance(delay, int), \ assert isinstance(delay, int), \
f"delay: expected {type(int)}, got {type(delay)}" f"delay: expected {type(int)}, got {type(delay)}"
assert isinstance(global_vars, dict), \
f"global_vars: expected {type(dict)}, got {type(global_vars)}"
self._job = job self._job = job
self._files = files self._files = files
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._singlejob = singlejob
self._tasks = {} self._tasks = {}
self._pause = False self._pause = False
@@ -89,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, "
@@ -98,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-"
@@ -125,8 +128,15 @@ class TaskScheduler:
return return
logger.info("start task") logger.info("start task")
if self._globals:
local_vars = {"self": self,
"event": event,
"task_id": task_state.id}
task_state.task = asyncio.create_task(
eval("self._job(event, task_id)", self._globals, local_vars))
task_state.task = self._loop.create_task( else:
task_state.task = asyncio.create_task(
self._job(event, task_state.id)) self._job(event, task_state.id))
try: try:
@@ -135,9 +145,10 @@ class TaskScheduler:
except asyncio.CancelledError: except asyncio.CancelledError:
logger.warning("ongoing task cancelled") logger.warning("ongoing task cancelled")
else: else:
self._log.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
@@ -145,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})
@@ -163,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
@@ -184,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")
@@ -229,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)
@@ -277,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]
+1 -1
View File
@@ -18,7 +18,7 @@ setup(name = "pyinotifyd",
# 3 - Alpha # 3 - Alpha
# 4 - Beta # 4 - Beta
# 5 - Production/Stable # 5 - Production/Stable
"Development Status :: 3 - Alpha", "Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Programming Language :: Python", "Programming Language :: Python",
-9
View File
@@ -1,9 +0,0 @@
#!/usr/bin/env python
import sys
import pyinotifyd
if __name__ == '__main__':
sys.exit(
pyinotifyd.main()
)