-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
285 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Luzrain\PhpRunnerBundle\FileMonitorWatcher; | ||
|
||
use Psr\Log\LoggerInterface; | ||
use Revolt\EventLoop\Driver; | ||
|
||
/** | ||
* @psalm-suppress UndefinedPropertyAssignment | ||
* @psalm-suppress InaccessibleProperty | ||
*/ | ||
abstract class FileMonitorWatcher | ||
{ | ||
protected readonly LoggerInterface $logger; | ||
protected readonly array $sourceDir; | ||
private readonly array $filePattern; | ||
private readonly \Closure $reloadCallback; | ||
|
||
public static function create( | ||
LoggerInterface $logger, | ||
/** @var list<string> $sourceDir */ | ||
array $sourceDir, | ||
/** @var list<string> $filePattern */ | ||
array $filePattern, | ||
float $pollingInterval, | ||
\Closure $reloadCallback, | ||
): self { | ||
$watcher = \extension_loaded('inotify') ? new InotifyMonitorWatcher() : new PollingMonitorWatcher($pollingInterval); | ||
$watcher->logger = $logger; | ||
$watcher->sourceDir = \array_filter($sourceDir, \is_dir(...)); | ||
$watcher->filePattern = $filePattern; | ||
$watcher->reloadCallback = $reloadCallback; | ||
|
||
return $watcher; | ||
} | ||
|
||
final protected function isPatternMatch(string $filename): bool | ||
{ | ||
foreach ($this->filePattern as $pattern) { | ||
if (\fnmatch($pattern, $filename)) { | ||
return true; | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
|
||
final protected function reload(): void | ||
{ | ||
($this->reloadCallback)(); | ||
} | ||
|
||
abstract public function start(Driver $eventLoop): void; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Luzrain\PhpRunnerBundle\FileMonitorWatcher; | ||
|
||
use Revolt\EventLoop\Driver; | ||
|
||
/** | ||
* @psalm-suppress UndefinedConstant | ||
* @psalm-suppress PropertyNotSetInConstructor | ||
*/ | ||
final class InotifyMonitorWatcher extends FileMonitorWatcher | ||
{ | ||
private const REBOOT_DELAY = 0.3; | ||
|
||
/** @var resource */ | ||
private mixed $fd; | ||
/** @var array<int, string> */ | ||
private array $pathByWd = []; | ||
private \Closure|null $delayedRebootCallback = null; | ||
|
||
protected function __construct() | ||
{ | ||
} | ||
|
||
public function start(Driver $eventLoop): void | ||
{ | ||
$this->fd = \inotify_init(); | ||
\stream_set_blocking($this->fd, false); | ||
|
||
foreach ($this->sourceDir as $dir) { | ||
$dirIterator = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS); | ||
$iterator = new \RecursiveIteratorIterator($dirIterator, \RecursiveIteratorIterator::SELF_FIRST); | ||
|
||
$this->watchDir($dir); | ||
foreach ($iterator as $file) { | ||
/** @var \SplFileInfo $file */ | ||
if ($file->isDir()) { | ||
$this->watchDir($file->getPathname()); | ||
} | ||
} | ||
} | ||
|
||
$eventLoop->onReadable($this->fd, fn(string $id, mixed $fd) => $this->onNotify($eventLoop, $fd)); | ||
} | ||
|
||
/** | ||
* @param resource $inotifyFd | ||
*/ | ||
private function onNotify(Driver $eventLoop, mixed $inotifyFd): void | ||
{ | ||
/** @psalm-suppress RiskyTruthyFalsyComparison */ | ||
$events = \inotify_read($inotifyFd) ?: []; | ||
|
||
if ($this->delayedRebootCallback !== null) { | ||
return; | ||
} | ||
|
||
foreach($events as $event) { | ||
if ($this->isFlagSet($event['mask'], IN_IGNORED)) { | ||
unset($this->pathByWd[$event['wd']]); | ||
continue; | ||
} | ||
|
||
if ($this->isFlagSet($event['mask'], IN_CREATE | IN_ISDIR)) { | ||
$this->watchDir($this->pathByWd[$event['wd']] . '/' . $event['name']); | ||
continue; | ||
} | ||
|
||
if (!$this->isPatternMatch($event['name'])) { | ||
continue; | ||
} | ||
|
||
$this->delayedRebootCallback = function (): void { | ||
$this->delayedRebootCallback = null; | ||
$this->reload(); | ||
}; | ||
|
||
$eventLoop->delay(self::REBOOT_DELAY, $this->delayedRebootCallback); | ||
|
||
return; | ||
} | ||
} | ||
|
||
private function watchDir(string $path): void | ||
{ | ||
$wd = \inotify_add_watch($this->fd, $path, IN_MODIFY | IN_CREATE | IN_DELETE | IN_MOVED_TO); | ||
$this->pathByWd[$wd] = $path; | ||
} | ||
|
||
private function isFlagSet(int $check, int $flag): bool | ||
{ | ||
return ($check & $flag) === $flag; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Luzrain\PhpRunnerBundle\FileMonitorWatcher; | ||
|
||
use Revolt\EventLoop\Driver; | ||
|
||
/** | ||
* @psalm-suppress PropertyNotSetInConstructor | ||
*/ | ||
final class PollingMonitorWatcher extends FileMonitorWatcher | ||
{ | ||
private const TO_MANY_FILES_WARNING_LIMIT = 1000; | ||
|
||
private int $lastMTime = 0; | ||
private bool $toManyFiles = false; | ||
|
||
protected function __construct(private readonly float $pollingInterval) | ||
{ | ||
} | ||
|
||
public function start(Driver $eventLoop): void | ||
{ | ||
$this->lastMTime = \time(); | ||
$eventLoop->repeat($this->pollingInterval, $this->checkFileSystemChanges(...)); | ||
$this->logger->notice('Polling file monitoring can be inefficient if the project has many files. Install the php-inotify extension to increase performance.'); | ||
} | ||
|
||
private function checkFileSystemChanges(): void | ||
{ | ||
$filesCout = 0; | ||
|
||
foreach ($this->sourceDir as $dir) { | ||
$dirIterator = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS); | ||
$iterator = new \RecursiveIteratorIterator($dirIterator, \RecursiveIteratorIterator::SELF_FIRST); | ||
|
||
foreach ($iterator as $file) { | ||
/** @var \SplFileInfo $file */ | ||
if ($file->isDir()) { | ||
continue; | ||
} | ||
|
||
if (!$this->toManyFiles && ++$filesCout > self::TO_MANY_FILES_WARNING_LIMIT) { | ||
$this->toManyFiles = true; | ||
$this->logger->warning('There are too many files. This makes file monitoring very slow. Install php-inotify extension to increase performance.'); | ||
} | ||
|
||
if (!$this->isPatternMatch($file->getFilename())) { | ||
continue; | ||
} | ||
|
||
if ($file->getFileInfo()->getMTime() > $this->lastMTime) { | ||
$this->lastMTime = $file->getFileInfo()->getMTime(); | ||
$this->reload(); | ||
return; | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Luzrain\PhpRunnerBundle\Worker; | ||
|
||
use Luzrain\PhpRunner\WorkerProcess; | ||
use Luzrain\PhpRunnerBundle\FileMonitorWatcher\FileMonitorWatcher; | ||
|
||
final class FileMonitorWorker extends WorkerProcess | ||
{ | ||
public function __construct( | ||
private array $sourceDir, | ||
private array $filePattern, | ||
private float $pollingInterval, | ||
string|null $user, | ||
string|null $group, | ||
private \Closure $reloadCallback, | ||
) { | ||
parent::__construct( | ||
name: 'File monitor', | ||
user: $user, | ||
group: $group, | ||
reloadable: false, | ||
onStart: $this->onStart(...), | ||
); | ||
} | ||
|
||
private function onStart(): void | ||
{ | ||
$fileMonitor = FileMonitorWatcher::create( | ||
$this->getLogger(), | ||
$this->sourceDir, | ||
$this->filePattern, | ||
$this->pollingInterval, | ||
$this->doReload(...), | ||
); | ||
$fileMonitor->start($this->getEventLoop()); | ||
} | ||
|
||
/** | ||
* @psalm-suppress NoValue | ||
* @psalm-suppress RiskyTruthyFalsyComparison | ||
*/ | ||
private function doReload(): void | ||
{ | ||
($this->reloadCallback)(); | ||
|
||
if (\function_exists('opcache_get_status') && $status = \opcache_get_status()) { | ||
foreach (\array_keys($status['scripts'] ?? []) as $file) { | ||
\opcache_invalidate($file, true); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters