diff --git a/plugins/com.noccy.watcher/FileWatcher.php b/plugins/com.noccy.watcher/FileWatcher.php index af6fd01..074d8b8 100644 --- a/plugins/com.noccy.watcher/FileWatcher.php +++ b/plugins/com.noccy.watcher/FileWatcher.php @@ -17,10 +17,12 @@ class FileWatcher { public function __construct() { - if (extension_loaded('inotify')) { - $this->monitor = new MtimeMonitor(); - //$this->monitor = new InotifyMonitor(); + if (extension_loaded('inotify') && !getenv("SPARK_NO_INOTIFY")) { + //$this->monitor = new MtimeMonitor(); + printf("Enabling inotify support, watching directories\n"); + $this->monitor = new InotifyMonitor(); } else { + printf("No inotify support, watching file mtimes\n"); $this->monitor = new MtimeMonitor(); } $this->scriptRunner = get_environment()->getScriptRunner(); @@ -38,7 +40,11 @@ class FileWatcher { private function triggerRule(Rule $rule) { $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() diff --git a/plugins/com.noccy.watcher/Monitor/InotifyMonitor.php b/plugins/com.noccy.watcher/Monitor/InotifyMonitor.php new file mode 100644 index 0000000..21bcb00 --- /dev/null +++ b/plugins/com.noccy.watcher/Monitor/InotifyMonitor.php @@ -0,0 +1,97 @@ +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; + } + } + } + } + } +} diff --git a/plugins/com.noccy.watcher/Monitor/MtimeMonitor.php b/plugins/com.noccy.watcher/Monitor/MtimeMonitor.php index 384b8b0..14cb00c 100644 --- a/plugins/com.noccy.watcher/Monitor/MtimeMonitor.php +++ b/plugins/com.noccy.watcher/Monitor/MtimeMonitor.php @@ -60,12 +60,13 @@ class MtimeMonitor implements MonitorInterface } foreach ($check as $path) { + if (!file_exists($path)) continue; if (empty($this->watched[$path])) { $this->watched[$path] = filemtime($path); } else { $mtime = filemtime($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; if (!in_array($rule, $this->modified)) { $this->modified[] = $rule; diff --git a/plugins/com.noccy.watcher/README.md b/plugins/com.noccy.watcher/README.md index 5f86351..6bb2472 100644 --- a/plugins/com.noccy.watcher/README.md +++ b/plugins/com.noccy.watcher/README.md @@ -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. diff --git a/src/Environment/ScriptRunner.php b/src/Environment/ScriptRunner.php index 1d7ce82..0b6ce2e 100644 --- a/src/Environment/ScriptRunner.php +++ b/src/Environment/ScriptRunner.php @@ -26,16 +26,16 @@ class ScriptRunner $this->evaluate($script); } - public function evaluate(string|array $script) + public function evaluate(string|array $script, array $locals=[]) { if (is_array($script)) { foreach ($script as $step) { - $this->evaluate($step); + $this->evaluate($step, $locals); } return; } - $script = $this->expandString($script); + $script = $this->expandString($script, $locals); // Determine what to do 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; }, $input); }