24 Commits
Author SHA1 Message Date
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
12 changed files with 165 additions and 118 deletions
-3
View File
@@ -117,6 +117,3 @@ dmypy.json
# Temporary Vim files
.*.swp
# config file
/config.py
+18 -10
View File
@@ -1,6 +1,9 @@
# 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.
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.
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.
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
* [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.
### 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*.
All arguments except for *job* are optional.
```python
# 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)
# Good: await asyncio.sleep(10)
import asyncio
import logging
async def custom_job(event, task_id):
await asyncio.sleep(10)
logging.info(f"{task_id}: execute example task: {event}")
@@ -64,13 +70,15 @@ task_sched = TaskScheduler(
files=True,
dirs=False,
delay=0,
logname="sched")
logname="sched",
global_vars=globals())
```
### 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.
```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.
shell_sched = ShellScheduler(
cmd="/usr/local/bin/task.sh {maskname} {pathname} {src_pathname}")
@@ -126,7 +134,7 @@ event_map = {
"IN_Q_OVERFLOW": None,
"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)
```
The following events are available:
@@ -186,16 +194,16 @@ The following loglevels are available:
```python
# Configure global loglevel
setLoglevel(INFO)
```
Configure loglevel per *logname*.
```python
# Configure loglevel per logname.
setLoglevel(INFO, logname="daemon")
```
### Syslog
Send log messages to the local syslog server.
```python
# Enable logging to local syslog server (/dev/log). Use *address* to specify a different target.
# Enable logging to local syslog server (/dev/log).
# Use *address* to specify a different target.
enableSyslog(loglevel=INFO, address="/dev/log")
# Enable syslog per logname
@@ -2,7 +2,7 @@
# Distributed under the terms of the GNU General Public License v2
EAPI=7
PYTHON_COMPAT=( python3_{7,8,9} )
PYTHON_COMPAT=( python3_{8..10} )
DISTUTILS_USE_SETUPTOOLS=rdepend
SCM=""
+1 -1
View File
@@ -1,6 +1,6 @@
#!/bin/bash
set -e
set -x
PYTHON=$(which python)
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}/../..")
cd "${pkg_dir}/dist"
ls -la
msg="Select version to distribute (cancel with CTRL+C):"
echo "${msg}"
select version in $(find . -maxdepth 1 -type f -name "pyinotifyd-*.*.*.tar.gz" -printf "%f\n" | sed "s#\.tar\.gz##g"); do
+15 -14
View File
@@ -15,7 +15,9 @@
#
__all__ = [
"EventMap"
"setLoglevel",
"enableSyslog",
"EventMap",
"Watch",
"Pyinotifyd",
"DaemonInstance",
@@ -34,7 +36,7 @@ from pyinotify import ProcessEvent
from pyinotifyd._install import install, uninstall
from pyinotifyd.scheduler import TaskScheduler, Cancel
__version__ = "0.0.3"
__version__ = "0.0.6"
def setLoglevel(loglevel, logname=None):
@@ -201,12 +203,12 @@ class Pyinotifyd:
config = {}
name = Pyinotifyd.name
exec("from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL",
{}, config)
exec(f"from {name} import Pyinotifyd, Watch", {}, config)
exec(f"from {name} import setLoglevel, enableSyslog", {}, config)
exec(f"from {name}.scheduler import *", {}, config)
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:
exec(fh.read(), {}, config)
exec(fh.read(), config)
instance = config[f"{name}"]
assert isinstance(instance, Pyinotifyd), \
f"{name}: expected {type(Pyinotifyd)}, " \
@@ -434,15 +436,14 @@ def main():
ch.setFormatter(formatter)
loop = asyncio.get_event_loop()
for signame in ["SIGINT", "SIGTERM"]:
loop.add_signal_handler(
getattr(signal, signame),
lambda: loop.create_task(
daemon.shutdown(signame)))
signal.SIGTERM, lambda: loop.create_task(
daemon.shutdown("SIGTERM")))
loop.add_signal_handler(
getattr(signal, "SIGHUP"),
lambda: loop.create_task(
signal.SIGINT, 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.start()
+102 -61
View File
@@ -14,15 +14,101 @@
# along with pyinotifyd. If not, see <http://www.gnu.org/licenses/>.
#
import filecmp
import logging
import os
import shutil
import sys
SYSTEMD_PATH = "/lib/systemd/system"
SYSTEMD_PATHS = ["/lib/systemd/system", "/usr/lib/systemd/system"]
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():
if os.getuid() != 0:
logging.error("you need to have root privileges, please try again")
@@ -32,7 +118,11 @@ def _check_root():
def _check_systemd():
systemd = os.path.isdir(SYSTEMD_PATH)
for path in SYSTEMD_PATHS:
systemd = os.path.isdir(path)
if systemd:
break
if systemd:
logging.info("systemd detected")
@@ -47,37 +137,6 @@ def _check_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):
if not _check_root():
sys.exit(2)
@@ -85,33 +144,16 @@ def install(name):
pkg_dir = os.path.dirname(__file__)
if _check_systemd():
dst = f"{SYSTEMD_PATH}/{name}.service"
src = f"{pkg_dir}/misc/systemd/{name}.service"
_copy_missing_file(src, dst)
_install_files(_systemd_files(pkg_dir, name))
if _check_openrc():
files = [
(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)
_install_files(_openrc_files(pkg_dir, name))
logging.info("install configuration file")
config_dir = f"/etc/{name}"
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}")
if not _create_dir(f"/etc/{name}"):
logging.error(" => unable to create config dir, giving up ...")
sys.exit(3)
files = [
(f"{pkg_dir}/misc/config.py.default", f"{config_dir}/config.py")]
for src, dst in files:
_copy_missing_file(src, dst)
_install_files(_config_files(pkg_dir, name))
logging.info(f"{name} successfully installed")
@@ -120,13 +162,12 @@ def uninstall(name):
if not _check_root():
sys.exit(2)
if _check_systemd():
_delete_present_file(f"{SYSTEMD_PATH}/{name}.service")
pkg_dir = os.path.dirname(__file__)
if _check_openrc():
_delete_present_file(f"/etc/init.d/{name}")
_warn_exists(f"/etc/conf.d/{name}")
_uninstall_files(_systemd_files(pkg_dir, name))
_uninstall_files(_openrc_files(pkg_dir, name))
_uninstall_files(_config_files(pkg_dir, name))
_warn_exists(f"/etc/{name}")
_delete_dir(f"/etc/{name}")
logging.info(f"{name} successfully uninstalled")
+6 -1
View File
@@ -2,14 +2,19 @@
# TaskScheduler config #
##########################
#import asyncio
#import logging
#
#async def custom_job(event, task_id):
# asyncio.sleep(1)
# logging.info(f"{task_id}: execute example task: {event}")
#
#task_sched = TaskScheduler(
# job=custom_job,
# files=True,
# dirs=False,
# delay=10)
# delay=10
# global_vars=globals())
###########################
-5
View File
@@ -13,11 +13,6 @@ retry="SIGTERM/${shutdown_timeout}"
extra_commands="configtest reload"
depend() {
need net
before mta
}
checkconfig() {
output=$(${command} ${command_args} -t 2>&1)
ret=$?
+16 -8
View File
@@ -25,7 +25,6 @@ import os
import re
import shutil
from dataclasses import dataclass
from inspect import iscoroutinefunction
from shlex import quote as shell_quote
from uuid import uuid4
@@ -46,14 +45,14 @@ class SchedulerLogger(logging.LoggerAdapter):
class TaskScheduler:
@dataclass
class TaskState:
id: str = str(uuid4())
task: asyncio.Task = None
cancelable: bool = True
def __init__(self, task_id=None, task=None, cancelable=True):
self.id = task_id or str(uuid4())
self.task = task
self.cancelable = cancelable
def __init__(self, job, files=True, dirs=False, delay=0, logname="sched",
loop=None):
loop=None, global_vars={}):
assert iscoroutinefunction(job), \
f"job: expected coroutine, got {type(job)}"
assert isinstance(files, bool), \
@@ -62,6 +61,8 @@ class TaskScheduler:
f"dirs: expected {type(bool)}, got {type(dirs)}"
assert isinstance(delay, int), \
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._files = files
@@ -69,7 +70,7 @@ class TaskScheduler:
self._delay = delay
self._log = logging.getLogger((logname or __name__))
self._loop = (loop or asyncio.get_event_loop())
self._globals = global_vars
self._tasks = {}
self._pause = False
@@ -125,7 +126,14 @@ class TaskScheduler:
return
logger.info("start task")
if self._globals:
local_vars = {"self": self,
"event": event,
"task_id": task_state.id}
task_state.task = self._loop.create_task(
eval("self._job(event, task_id)", self._globals, local_vars))
else:
task_state.task = self._loop.create_task(
self._job(event, task_state.id))
@@ -135,7 +143,7 @@ class TaskScheduler:
except asyncio.CancelledError:
logger.warning("ongoing task cancelled")
else:
self._log.info("task finished")
logger.info("task finished")
finally:
del self._tasks[event.pathname]
+1 -1
View File
@@ -18,7 +18,7 @@ setup(name = "pyinotifyd",
# 3 - Alpha
# 4 - Beta
# 5 - Production/Stable
"Development Status :: 3 - Alpha",
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
"Operating System :: OS Independent",
"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()
)