Watcher plugin fixes

* com.noccy.watcher: Initial inotify support
* ScriptRunner now accepts an array of local vars for expansion
  when evaluating scripts
This commit is contained in:
Chris 2021-12-15 03:47:39 +01:00
parent 30dfd4889b
commit 1125ccb82d
5 changed files with 135 additions and 12 deletions

View File

@ -17,10 +17,12 @@ class FileWatcher {
public function __construct() public function __construct()
{ {
if (extension_loaded('inotify')) { if (extension_loaded('inotify') && !getenv("SPARK_NO_INOTIFY")) {
$this->monitor = new MtimeMonitor(); //$this->monitor = new MtimeMonitor();
//$this->monitor = new InotifyMonitor(); printf("Enabling inotify support, watching directories\n");
$this->monitor = new InotifyMonitor();
} else { } else {
printf("No inotify support, watching file mtimes\n");
$this->monitor = new MtimeMonitor(); $this->monitor = new MtimeMonitor();
} }
$this->scriptRunner = get_environment()->getScriptRunner(); $this->scriptRunner = get_environment()->getScriptRunner();
@ -38,7 +40,11 @@ class FileWatcher {
private function triggerRule(Rule $rule) private function triggerRule(Rule $rule)
{ {
$actions = $rule->getActions(); $actions = $rule->getActions();
$this->scriptRunner->evaluate($actions); $locals = [
'WATCHER_RULE' => $rule->getName(),
'WATCHER_FILES' => join(" ",$rule->getWatchedFiles()),
];
$this->scriptRunner->evaluate($actions, $locals);
} }
public function loop() public function loop()

View File

@ -0,0 +1,97 @@
<?php
namespace SparkPlug\Com\Noccy\Watcher\Monitor;
use SparkPlug\Com\Noccy\Watcher\Rule;
class InotifyMonitor implements MonitorInterface
{
private array $rules = [];
private array $watched = [];
private array $modified = [];
private $fd;
public function __construct()
{
$this->fd = \inotify_init();
}
public function __destruct()
{
if (is_resource($this->fd)) fclose($this->fd);
}
/**
* {@inheritDoc}
*/
public function add(Rule $rule)
{
$this->rules[] = $rule;
$paths = $rule->getWatchedFiles();
$check = [];
foreach ($paths as $path) {
if (str_contains($path, '*')) {
$check = array_merge($check, glob($path));
} else {
$check[] = $path;
}
}
$dirs = [];
foreach ($check as $path) {
$dir = is_dir($path) ? $path : dirname($path);
if (!array_key_exists($dir, $dirs)) {
$dirs[$dir] = $rule;
\inotify_add_watch($this->fd, $dir, \IN_ATTRIB);
}
}
$this->watched = array_merge($this->watched, $dirs);
}
/**
* {@inheritDoc}
*/
public function getModified(): array
{
$mod = $this->modified;
$this->modified = [];
return $mod;
}
/**
* {@inheritDoc}
*/
public function getWatched(): array
{
return [];
}
public function loop()
{
$read = [ $this->fd ];
$write = null;
$except = null;
$changed = [];
while (stream_select($read,$write,$except,0)) {
$events = \inotify_read($this->fd);
foreach ($events as $event) {
$changed[] = $event['name'];
}
}
foreach ($changed as $file) {
foreach ($this->watched as $dir=>$rule) {
if (file_exists($dir."/".$file)) {
if (!in_array($rule, $this->modified)) {
printf("~ modified: %s (%s)\n", $dir."/".$file, $rule->getName());
$this->modified[] = $rule;
}
}
}
}
}
}

View File

@ -60,12 +60,13 @@ class MtimeMonitor implements MonitorInterface
} }
foreach ($check as $path) { foreach ($check as $path) {
if (!file_exists($path)) continue;
if (empty($this->watched[$path])) { if (empty($this->watched[$path])) {
$this->watched[$path] = filemtime($path); $this->watched[$path] = filemtime($path);
} else { } else {
$mtime = filemtime($path); $mtime = filemtime($path);
if ($mtime > $this->watched[$path]) { if ($mtime > $this->watched[$path]) {
printf("* modified: %s (%s)\n", $path, $rule->getName()); printf("~ modified: %s (%s)\n", $path, $rule->getName());
$this->watched[$path] = $mtime; $this->watched[$path] = $mtime;
if (!in_array($rule, $this->modified)) { if (!in_array($rule, $this->modified)) {
$this->modified[] = $rule; $this->modified[] = $rule;

View File

@ -34,4 +34,21 @@ issues.
} }
``` ```
The `initial-trigger` key controls whether the rule is triggered on startup. - `name` contains an optional name of the rule.
- `initial-trigger` controls whether the rule is triggered on startup. If false
or not specified, the actions will be triggered when the file is modified
after startup.
- `watch` is an array of files or wildcards to watch.
- `actions` are script actions to invoke.
The executed action will have access to the `WATCHER_RULE` and `WATCHER_FILES`
variables for expansion.
## Known issues
- The *inotify* monitor will trigger whenever a file is changed in a watched
directory, even if an explicit file is set. For example, changing the file
`./templates/index.html` will trigger the rule for `./templates/style.scss`.
If you can't live with that, export `SPARK_NO_INOTIFY=1` before invoking!
- The *mtime* monitor will poll the mtime of each watched file every check.
This works great if you are explicit in what you are watching.

View File

@ -26,16 +26,16 @@ class ScriptRunner
$this->evaluate($script); $this->evaluate($script);
} }
public function evaluate(string|array $script) public function evaluate(string|array $script, array $locals=[])
{ {
if (is_array($script)) { if (is_array($script)) {
foreach ($script as $step) { foreach ($script as $step) {
$this->evaluate($step); $this->evaluate($step, $locals);
} }
return; return;
} }
$script = $this->expandString($script); $script = $this->expandString($script, $locals);
// Determine what to do // Determine what to do
if (str_starts_with($script, '@')) { if (str_starts_with($script, '@')) {
@ -81,10 +81,12 @@ class ScriptRunner
} }
} }
public function expandString(string $input) public function expandString(string $input, array $locals=[])
{ {
return preg_replace_callback('/(\$\{(.+?)\})/', function ($match) { return preg_replace_callback('/(\$\{(.+?)\})/', function ($match) use ($locals) {
if (array_key_exists($match[2], $locals)) {
return $locals[$match[2]];
}
return ($_ENV[$match[2]]??getenv($match[2]))??null; return ($_ENV[$match[2]]??getenv($match[2]))??null;
}, $input); }, $input);
} }