Optimize the code for watcher. (#4999)

This commit is contained in:
她和她的猫 2022-08-10 09:56:28 +08:00 committed by GitHub
parent cb142acca0
commit 15155bce0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 430 additions and 60 deletions

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Watcher\Driver;
use Hyperf\Coordinator\Timer;
use Hyperf\Watcher\Option;
abstract class AbstractDriver implements DriverInterface
{
protected Timer $timer;
protected ?int $timerId = null;
public function __construct(protected Option $option)
{
$this->timer = new Timer();
}
public function __destruct()
{
$this->stop();
}
public function isDarwin(): bool
{
return PHP_OS === 'Darwin';
}
public function stop()
{
if ($this->timerId) {
$this->timer->clear($this->timerId);
$this->timerId = null;
}
}
}

View File

@ -15,24 +15,18 @@ use Hyperf\Engine\Channel;
use Hyperf\Utils\Str;
use Hyperf\Watcher\Option;
use Swoole\Coroutine\System;
use Swoole\Timer;
class FindDriver implements DriverInterface
class FindDriver extends AbstractDriver
{
protected bool $isDarwin = false;
protected bool $isSupportFloatMinutes = true;
protected int $startTime = 0;
public function __construct(protected Option $option)
{
if (PHP_OS === 'Darwin') {
$this->isDarwin = true;
} else {
$this->isDarwin = false;
}
if ($this->isDarwin) {
parent::__construct($option);
if ($this->isDarwin()) {
$ret = System::exec('which gfind');
if (empty($ret['output'])) {
throw new \InvalidArgumentException('gfind not exists. You can `brew install findutils` to install it.');
@ -50,20 +44,15 @@ class FindDriver implements DriverInterface
public function watch(Channel $channel): void
{
$this->startTime = time();
$ms = $this->option->getScanInterval();
$seconds = ceil(($ms + 1000) / 1000);
if ($this->isSupportFloatMinutes) {
$minutes = sprintf('-%.2f', $seconds / 60);
} else {
$minutes = sprintf('-%d', ceil($seconds / 60));
}
Timer::tick($ms, function () use ($channel, $minutes) {
$seconds = $this->option->getScanIntervalSeconds();
$this->timerId = $this->timer->tick($seconds, function () use ($channel) {
global $fileModifyTimes;
if (is_null($fileModifyTimes)) {
$fileModifyTimes = [];
}
[$fileModifyTimes, $changedFiles] = $this->scan($fileModifyTimes, $minutes);
[$fileModifyTimes, $changedFiles] = $this->scan($fileModifyTimes, $this->getScanIntervalMinutes());
foreach ($changedFiles as $file) {
$channel->push($file);
@ -71,6 +60,15 @@ class FindDriver implements DriverInterface
});
}
protected function getScanIntervalMinutes(): string
{
$minutes = $this->option->getScanIntervalSeconds() / 60;
if ($this->isSupportFloatMinutes) {
return sprintf('-%.2f', $minutes);
}
return sprintf('-%d', ceil($minutes));
}
protected function find(array $fileModifyTimes, array $targets, string $minutes, array $ext = []): array
{
$changedFiles = [];
@ -104,7 +102,7 @@ class FindDriver implements DriverInterface
protected function getBin(): string
{
return $this->isDarwin ? 'gfind' : 'find';
return $this->isDarwin() ? 'gfind' : 'find';
}
protected function scan(array $fileModifyTimes, string $minutes): array

View File

@ -15,18 +15,18 @@ use Hyperf\Engine\Channel;
use Hyperf\Utils\Str;
use Hyperf\Watcher\Option;
use Swoole\Coroutine\System;
use Swoole\Timer;
class FindNewerDriver implements DriverInterface
class FindNewerDriver extends AbstractDriver
{
protected string $tmpFile = '/tmp/hyperf_find.php';
protected bool $scaning = false;
protected bool $scanning = false;
protected int $count = 0;
public function __construct(protected Option $option)
{
parent::__construct($option);
$ret = System::exec('which find');
if (empty($ret['output'])) {
throw new \InvalidArgumentException('find not exists.');
@ -38,26 +38,26 @@ class FindNewerDriver implements DriverInterface
public function watch(Channel $channel): void
{
$ms = $this->option->getScanInterval();
Timer::tick($ms, function () use ($channel) {
if ($this->scaning == false) {
$this->scaning = true;
$changedFiles = $this->scan();
++$this->count;
// update mtime
if ($changedFiles) {
System::exec('echo 1 > ' . $this->getToModifyFile());
System::exec('echo 1 > ' . $this->getToScanFile());
}
foreach ($changedFiles as $file) {
$channel->push($file);
$this->scaning = false;
return;
}
$this->scaning = false;
$seconds = $this->option->getScanIntervalSeconds();
$this->timerId = $this->timer->tick($seconds, function () use ($channel) {
if ($this->scanning) {
return;
}
$this->scanning = true;
$changedFiles = $this->scan();
++$this->count;
// update mtime
if ($changedFiles) {
System::exec('echo 1 > ' . $this->getToModifyFile());
System::exec('echo 1 > ' . $this->getToScanFile());
}
foreach ($changedFiles as $file) {
$channel->push($file);
$this->scanning = false;
return;
}
$this->scanning = false;
});
}
@ -97,9 +97,7 @@ class FindNewerDriver implements DriverInterface
protected function scan(): array
{
$ext = $this->option->getExt();
$dirs = array_map(fn ($dir) => BASE_PATH . '/' . $dir, $this->option->getWatchDir());
$files = array_map(fn ($file) => BASE_PATH . '/' . $file, $this->option->getWatchFile());
if ($files) {
@ -109,13 +107,13 @@ class FindNewerDriver implements DriverInterface
return $this->find($dirs, $ext);
}
protected function getToModifyFile()
protected function getToModifyFile(): string
{
return $this->tmpFile . strval($this->count % 2);
return $this->tmpFile . ($this->count % 2);
}
protected function getToScanFile()
protected function getToScanFile(): string
{
return $this->tmpFile . strval(($this->count + 1) % 2);
return $this->tmpFile . (($this->count + 1) % 2);
}
}

View File

@ -12,18 +12,18 @@ declare(strict_types=1);
namespace Hyperf\Watcher\Driver;
use Hyperf\Engine\Channel;
use Hyperf\Utils\Coroutine;
use Hyperf\Engine\Coroutine;
use Hyperf\Utils\Str;
use Hyperf\Watcher\Option;
use Swoole\Coroutine\System;
class FswatchDriver implements DriverInterface
class FswatchDriver extends AbstractDriver
{
protected bool $isDarwin;
protected mixed $process = null;
public function __construct(protected Option $option)
{
$this->isDarwin = PHP_OS === 'Darwin';
parent::__construct($option);
$ret = System::exec('which fswatch');
if (empty($ret['output'])) {
throw new \InvalidArgumentException('fswatch not exists. You can `brew install fswatch` to install it.');
@ -33,12 +33,12 @@ class FswatchDriver implements DriverInterface
public function watch(Channel $channel): void
{
$cmd = $this->getCmd();
$process = proc_open($cmd, [['pipe', 'r'], ['pipe', 'w']], $pipes);
if (! is_resource($process)) {
$this->process = proc_open($cmd, [['pipe', 'r'], ['pipe', 'w']], $pipes);
if (! is_resource($this->process)) {
throw new \RuntimeException('fswatch failed.');
}
while (true) {
while (! $channel->isClosing()) {
$ret = fread($pipes[1], 8192);
Coroutine::create(function () use ($ret, $channel) {
if (is_string($ret)) {
@ -53,13 +53,24 @@ class FswatchDriver implements DriverInterface
}
}
public function stop()
{
parent::stop();
if (is_resource($this->process)) {
$running = proc_get_status($this->process)['running'];
// Kill the child process to exit.
$running && proc_terminate($this->process, SIGKILL);
}
}
protected function getCmd(): string
{
$dir = $this->option->getWatchDir();
$file = $this->option->getWatchFile();
$cmd = 'fswatch ';
if (! $this->isDarwin) {
if (! $this->isDarwin()) {
$cmd .= ' -m inotify_monitor';
$cmd .= " -E --format '%p' -r ";
$cmd .= ' --event Created --event Updated --event Removed --event Renamed ';

View File

@ -16,22 +16,22 @@ use Hyperf\Engine\Channel;
use Hyperf\Utils\Filesystem\Filesystem;
use Hyperf\Utils\Str;
use Hyperf\Watcher\Option;
use Swoole\Timer;
use Symfony\Component\Finder\SplFileInfo;
class ScanFileDriver implements DriverInterface
class ScanFileDriver extends AbstractDriver
{
protected Filesystem $filesystem;
public function __construct(protected Option $option, private StdoutLoggerInterface $logger)
{
parent::__construct($option);
$this->filesystem = new Filesystem();
}
public function watch(Channel $channel): void
{
$ms = $this->option->getScanInterval();
Timer::tick($ms, function () use ($channel) {
$seconds = $this->option->getScanIntervalSeconds();
$this->timerId = $this->timer->tick($seconds, function () use ($channel) {
global $lastMD5;
$files = [];
$currentMD5 = $this->getWatchMD5($files);

View File

@ -90,6 +90,11 @@ class Option
return $this->scanInterval > 0 ? $this->scanInterval : 2000;
}
public function getScanIntervalSeconds(): float
{
return $this->getScanInterval() / 1000;
}
public function isRestart(): bool
{
return $this->restart;

View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Watcher\Driver;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Engine\Channel;
use Hyperf\Watcher\Driver\FindDriver;
use Hyperf\Watcher\Option;
use HyperfTest\Watcher\Stub\ContainerStub;
use HyperfTest\Watcher\Stub\FindDriverStub;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
class FindDriverTest extends TestCase
{
public function testWatch()
{
$container = ContainerStub::getContainer(FindDriver::class);
$option = new Option($container->get(ConfigInterface::class), [], []);
$channel = new Channel(10);
try {
$driver = new FindDriverStub($option);
$driver->watch($channel);
$this->assertSame('.env', $channel->pop($option->getScanIntervalSeconds() + 0.1));
} catch (\InvalidArgumentException $e) {
if (str_contains($e->getMessage(), 'find not exists')) {
$this->markTestSkipped();
}
throw $e;
} finally {
$channel->close();
}
}
}

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Watcher\Driver;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Engine\Channel;
use Hyperf\Watcher\Driver\FindNewerDriver;
use Hyperf\Watcher\Option;
use HyperfTest\Watcher\Stub\ContainerStub;
use HyperfTest\Watcher\Stub\FindNewerDriverStub;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
class FindNewerDriverTest extends TestCase
{
public function testWatch()
{
$container = ContainerStub::getContainer(FindNewerDriver::class);
$option = new Option($container->get(ConfigInterface::class), [], []);
$channel = new Channel(10);
try {
$driver = new FindNewerDriverStub($option);
$driver->watch($channel);
$this->assertSame('.env', $channel->pop($option->getScanIntervalSeconds() + 0.1));
} catch (\InvalidArgumentException $e) {
if (str_contains($e->getMessage(), 'find not exists')) {
$this->markTestSkipped();
}
throw $e;
} finally {
$channel->close();
}
}
}

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Watcher\Driver;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Engine\Channel;
use Hyperf\Engine\Coroutine;
use Hyperf\Watcher\Driver\FswatchDriver;
use Hyperf\Watcher\Option;
use HyperfTest\Watcher\Stub\ContainerStub;
use PHPUnit\Framework\TestCase;
use Swoole\Coroutine\System;
/**
* @internal
* @coversNothing
*/
class FswatchDriverTest extends TestCase
{
public function testWatch()
{
$container = ContainerStub::getContainer(FswatchDriver::class);
$option = new Option($container->get(ConfigInterface::class), [], []);
$channel = new Channel(10);
try {
$driver = new FswatchDriver($option);
Coroutine::create(fn () => $driver->watch($channel));
System::exec('echo 1 > /tmp/.env');
$this->assertStringEndsWith('.env', $channel->pop($option->getScanIntervalSeconds() + 0.1));
} catch (\InvalidArgumentException $e) {
if (str_contains($e->getMessage(), 'fswatch not exists')) {
$this->markTestSkipped();
}
throw $e;
} finally {
if (! empty($driver)) {
// Need to close the fswatch child process manually.
$driver->stop();
}
$channel->close();
}
}
}

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Watcher\Driver;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Engine\Channel;
use Hyperf\Watcher\Driver\ScanFileDriver;
use Hyperf\Watcher\Option;
use HyperfTest\Watcher\Stub\ContainerStub;
use HyperfTest\Watcher\Stub\ScanFileDriverStub;
use PHPUnit\Framework\TestCase;
use Swoole\Coroutine\System;
/**
* @internal
* @coversNothing
*/
class ScanFileDriverTest extends TestCase
{
public function testWatch()
{
$container = ContainerStub::getContainer(ScanFileDriver::class);
$option = new Option($container->get(ConfigInterface::class), [], []);
$channel = new Channel(10);
$driver = new ScanFileDriverStub($option, $container->get(StdoutLoggerInterface::class));
$driver->watch($channel);
System::exec('echo 1 > /tmp/.env');
$this->assertStringEndsWith('.env', $channel->pop($option->getScanIntervalSeconds() + 0.1));
$channel->close();
}
}

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Watcher\Stub;
use Hyperf\Config\Config;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Utils\ApplicationContext;
use Mockery;
use Mockery\MockInterface;
use Psr\Container\ContainerInterface;
class ContainerStub
{
public static function getContainer(string $driver): MockInterface|ContainerInterface
{
$container = Mockery::mock(ContainerInterface::class);
ApplicationContext::setContainer($container);
$container->shouldReceive('get')->with(ConfigInterface::class)->andReturnUsing(function () use ($driver) {
return new Config([
'watcher' => [
'driver' => $driver,
'bin' => 'php',
'watch' => [
'dir' => ['/tmp'],
'file' => ['.env'],
'scan_interval' => 1,
],
],
]);
});
$container->shouldReceive('get')->with(StdoutLoggerInterface::class)->andReturnUsing(function () {
$logger = Mockery::mock(StdoutLoggerInterface::class);
$logger->shouldReceive('debug')->andReturn(null);
$logger->shouldReceive('log')->andReturn(null);
return $logger;
});
return $container;
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Watcher\Stub;
use Hyperf\Watcher\Driver\FindDriver;
class FindDriverStub extends FindDriver
{
protected function scan(array $fileModifyTimes, string $minutes): array
{
return [[], ['.env']];
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Watcher\Stub;
use Hyperf\Watcher\Driver\FindNewerDriver;
class FindNewerDriverStub extends FindNewerDriver
{
protected function scan(): array
{
return ['.env'];
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Watcher\Stub;
use Hyperf\Watcher\Driver\ScanFileDriver;
class ScanFileDriverStub extends ScanFileDriver
{
protected function getWatchMD5(&$files): array
{
$files[] = '.env';
return ['.env' => md5(strval(microtime()))];
}
}

View File

@ -26,6 +26,9 @@ class WatcherTest extends TestCase
$config = new Config([
'watcher' => [
'driver' => 'xxx',
'watch' => [
'scan_interval' => 1500,
],
],
]);
@ -34,6 +37,8 @@ class WatcherTest extends TestCase
$this->assertSame('xxx', $option->getDriver());
$this->assertSame(['app', 'config', 'src'], $option->getWatchDir());
$this->assertSame(['.env'], $option->getWatchFile());
$this->assertSame(1500, $option->getScanInterval());
$this->assertSame(1.5, $option->getScanIntervalSeconds());
}
protected function getContainer()