A library monitor file system changes utilizing React PHP Event Loop and React Inotify
It was created to monitor 3 basic events: Created, Modified, and Deleted. And to reduce the many events need to be used with React Inotify to just simple 3.
- PHP >= 7.1
- inotify php extension
- Linux like system where inotify extension is available
- Monitor files and directories
- Configured patterns to monitor using shell patterns
- Configured nesting level for monitoring, supported at global level and at pattern level
- Base Directory needs not to exist prior to running the loop, it will be automatically (and optionally) waited to be created.
- Optimized for speed
$ composer require idimsh/php-inotify-monitor
or in composer.json
{
"require": {
"idimsh/php-inotify-monitor": "dev-master"
}
}
require_once 'vendor/autoload.php';
use Dimsh\React\Filesystem\Monitor\Monitor;
use Dimsh\React\Filesystem\Monitor\MonitorConfigurator;
$monitor = new Monitor(MonitorConfigurator::factory()
->setBaseDirectory('/tmp')
->setLevel(2)
->setFilesToMonitor([
'*.yaml',
]));
$monitor
->on(Monitor::EV_CREATE, function ($path, $monitor) {
echo "created: $path\n";
})
->on(Monitor::EV_MODIFY, function ($path, $monitor) {
echo "modified: $path\n";
})
->on(Monitor::EV_DELETE, function ($path, $monitor) {
echo "deleted: $path\n";
})
->run();
That code will monitor *.yaml
files up to two levels inside /tmp/
so the following patterns match: /tmp/*.yaml
and /tmp/*/*.yaml
but not /tmp/*/*/*.yaml
Two classes defined:
MonitorConfigurator
: used to configure the monitorMonitor
: Define the event handlers and perform actions when events are caught.
The constructor accepts aMonitorConfigurator
object as dependency.
Inherits\Evenement\EventEmitter
.
-
setBaseDirectory()
: Defines the directory the Monitor will watch changes in, it must be in absolute path and if not: the current working directory is used to construct an absolute path, so an empty Base Directory is valid to be passed.
The general rule is: DO NOT set it to the root'/'
directory, and do not operate on a directory where lot of files are being modified like:/var/log/
.
If the directory specified represents a file, an\Exception
is thrown. -
setLevel()
: Specify the level inside Base Directory to recurse to, default to 1 which is directly inside Base Dir.
If Base Dir is/var/www/
then:
/var/www/file1.html
is at level 1
/var/www/html/file2.html
is at level 2
Setting level to 0 (Zero) will recurse to all sub directories. -
setFilesToMonitor([])
: Accepts an array of shell patterns which represent the files and/or directories the Monitor will watch changes for and trigger the events.
When a file (or directory) that matches a pattern defined here has been: created, modified, or deleted the defined handler function for that event is called ang get passed the absolute path to the file or directory along with the Monitor object instance.
You can target directories by specifying shell patterns that ends with a slash'/'
, file patterns does not end with slash.
Patterns which do not contain a slash'/'
in the middle are recursive to the level defined bysetLevel()
, so an entry like:'*.yaml'
will match any file with extensionyaml
up to the level defined.
But patterns which include a slash in the middle or at the beginning are considered in absolute form and are not recursive, so an entry like:'*/*.yaml'
(is exactly equivalent to the entry'/*/*.yaml'
) will match any file with extensionyaml
which are under base directory at level 2 and it will not match them at any other level (unless other entry in the list instructs so).
Patterns must not include the Base Directory patterns are checked for files/directories inside the Base Dir after removing Base Dir from their absolute path. -
setFireModifiedOnDirectories()
: Default to false. Set to true to fire the modified event on directories, this is fired when a directory attributes (mtime, permissions) are changed. The modified event will be fired only if the directory matches one of the patterns specified bysetFilesToMonitor([])
(again patterns which matches directories are those ending with slash'/'
).
In order to monitor the base directory itself for changes when this flag is on, pattern entry would be the empty string''
, it has the special meaning of: match the Base Dir iffire_modified_on_directories
is on. -
setMonitorCreatedOnly()
: Default to false. Set to true to only watch for the "CREATED" event, used by Monitor itself for quick waiting for non existent Base Dir. -
setAutoCreateNotFoundMonitor()
: Default to false. Set to true and Monitor instance will create another inner Monitor automatically for non existent Base Dir. Usually when the Monitor is run and base directory does not exist on the file system, the run call will return immediately and nothing will be monitored. With this flag set totrue
an internal Monitor object instance will be created which is optimized to monitor the root directory'/'
(as Base Dir) for the creation of the Base Dir of the parent Monitor instance.
examples
MonitorConfigurator::factory()
->setBaseDirectory('/tmp')
->setLevel(2)
->setFilesToMonitor([
'*.yaml', # Will monitor and matches files: '/tmp/*.yaml', '/tmp/*/*.yaml' only
'/*.xml', # Will monitor and matches files: '/tmp/*.xml' only, '/tmp/*/*.xml' are not
# monitored because this pattern starts with a slash and is not recursive.
'config*/', # Will monitor and matches directories: '/tmp/config-yaml/', '/tmp/config*/', '/tmp/*/config*/'
# (This only makes scense if setFireModifiedOnDirectories() is set to true)
]));
MonitorConfigurator::factory()
->setBaseDirectory('/etc')
->setLevel(5)
->setFilesToMonitor([
'nginx/*.conf', # Will monitor and matches files: '/etc/nginx/*.conf', this pattern constains
# a slash so not recursive and is at level 2
# In fact with this config level is set internally to 2, since there is no
# pattern which will match at levels 3 to 5.
]));
The constructor of Monitor
accepts a MonitorConfigurator
instance and optionally an instance of an external
React PHP LoopInterface (or React PHP EventLoop). If no external EventLoop is passed, an internal one is created.
The loop must be ran and the run()
method of Monitor will execute the run()
call on the EventLoop (The
external or the internal).
Use an external EventLoop if for example two Monitors need to watch two different Base Directories (with different
patterns for each), then create an external Loop, pass it to both instances of Monitor
and fire the run()
externally after initializing both.
The Monitor extends the EventEmitter
class and the instance of it can be setup to
listen to events using on()
method, recognized events are:
Monitor::EV_CREATE
Monitor::EV_MODIFY
Monitor::EV_DELETE
The callback function to each event is passed the file/directory path which the event occurs on and the instance of
the Monitor
which fires the event.
Directories are passed to the callback with a trailing slash.
Calling run()
on EventLoop will block, so the worker process which utilize this class has to handle it.
-
run()
: will run the event loop (whether the external one passed to the constructor or the internally created one). This call will block.
Ifrun()
is called and an external event loop is passed to the constructor a warning of typeuser
is generated to inform the caller that external event loops are meant to be called not by the monitor itself. -
stop()
: stop the Monitor by removing all watches registered. -
stopAll()
: stop the Monitor by removing all watches registered and callstop()
on the event loop, causing it to stop. -
stopQuick()
: stop the Monitor by a quick call to inotify close. Not recommended unless in special cases (like inside POSIX signal handler). -
stopQuickAll()
: likestopQuick()
and will call stop on the event loop also.
example
/**
* This example will monitor the file upload directory of a web application
* for PHP files and automatically delete any PHP file created or uploaded.
*/
require_once 'vendor/autoload.php';
use Dimsh\React\Filesystem\Monitor\Monitor;
use Dimsh\React\Filesystem\Monitor\MonitorConfigurator;
$base_dir = '/tmp/uploads';
$monitor_config = MonitorConfigurator::factory()
->setLevel(2)
->setAutoCreateNotFoundMonitor(true)// wait for $base_dir to be created if not exists.
->setFireModifiedOnDirectories(true)
->setFilesToMonitor([
'*.php', // monitor any php file in the uploads directory.
'', // since ->setFireModifiedOnDirectories(true) is set,
// this entry means that we want to be notified if
// the $base_dir has been modified also (permissions).
'*/*.swf', // monitor swf files at level 2 only
'/stop-if-created/', // monitor this exact directory at level 1, and in our
// event handler we will stop the monitor if this
// directory is created.
]);
try {
$monitor_config->setBaseDirectory($base_dir);
} catch (\Exception $e) {
die("exception thrown: [{$e->getMessage()}]\n");
}
$monitor = new Monitor($monitor_config);
$monitor
->on(Monitor::EV_CREATE, function ($path, $monitor) use ($base_dir) {
/** @var Monitor $monitor */
if ($path === "$base_dir/stop-if-created/") {
echo "stopping ... \n";
$monitor->stop();
}
if ($monitor->hasTrailingSlash($path)) {
// this will just print once for:
// "directory created: $base_dir/stop-if-created/"
// since we are not monitoring any directory with our set patterns
// except one, the stop will occur after all event handlers are fired.
echo "directory created: $path\n";
} else {
echo "file created: $path\n";
if (strtolower(substr($path, -4)) === '.php') {
echo "php files are not allowed to be created in upload directory\n";
@unlink($path);
}
}
})
->on(Monitor::EV_MODIFY, function ($path, $monitor) use ($base_dir) {
if ($base_dir === $path) {
echo "base directory modified: $path\n";
} else {
echo "modified: $path\n";
}
})
->on(Monitor::EV_DELETE, function ($path, $monitor) {
// this will be printed for PHP files which we are deleting on our created handler also.
echo "deleted: $path\n";
});
$monitor->run();
In case of PHP warnings like this are encountered:
PHP Warning: inotify_add_watch(): The user limit on the total number of inotify watches was reached or the kernel failed to allocate a needed resource in 1vendor/mkraemer/react-inotify/src/MKraemer/ReactInotify/Inotify.php
The following command can solve the issue:
$ echo 999999 | sudo tee -a /proc/sys/fs/inotify/max_user_watches && \
echo 999999 | sudo tee -a /proc/sys/fs/inotify/max_queued_events && \
echo 999999 | sudo tee -a /proc/sys/fs/inotify/max_user_instances && \
sudo sysctl -p
But this warning may indicate that you are setting up the watcher with too many resources which can mean bad configuration.
MIT
are welcome