Skip to content

Commit

Permalink
热更新支持fswatch驱动
Browse files Browse the repository at this point in the history
  • Loading branch information
yunwuxin committed Jan 2, 2025
1 parent 788f50f commit 535cfed
Show file tree
Hide file tree
Showing 10 changed files with 194 additions and 37 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@
"php": "^8.0",
"ext-json": "*",
"ext-swoole": "^4.0|^5.0",
"topthink/framework": "^6.0|^8.0",
"nette/php-generator": "^4.0",
"open-smf/connection-pool": ">=1.0",
"stechstudio/backoff": "^1.2",
"symfony/finder": ">=4.3",
"topthink/framework": "^6.0|^8.0",
"symfony/process": ">=4.2",
"swoole/ide-helper": "^5.0"
},
"require-dev": {
Expand Down
3 changes: 3 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ parameters:
level: 5
paths:
- src
- tests
scanFiles:
- vendor/topthink/framework/src/helper.php
scanDirectories:
- vendor/swoole/ide-helper/src/swoole_library/src
treatPhpDocTypesAsCertain: false
universalObjectCratesClasses:
- PHPUnit\Framework\TestCase
ignoreErrors:
-
identifier: while.alwaysTrue
Expand Down
27 changes: 18 additions & 9 deletions src/Watcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,37 @@

namespace think\swoole;

use think\swoole\contract\WatcherInterface;

/**
* @mixin WatcherInterface
* @mixin \think\swoole\watcher\Driver
*/
class Watcher extends \think\Manager
{
protected $namespace = '\\think\\swoole\\watcher\\';
protected $namespace = '\\think\\swoole\\watcher\\driver\\';

protected function getConfig(string $name, $default = null)
{
return $this->app->config->get('swoole.hot_update.' . $name, $default);
}

/**
* @param $name
* @return \think\swoole\watcher\Driver
*/
public function monitor($name = null)
{
return $this->driver($name);
}

protected function resolveParams($name): array
{
return [
array_filter($this->getConfig('include', []), function ($dir) {
return is_dir($dir);
}),
$this->getConfig('exclude', []),
$this->getConfig('name', []),
[
'directory' => array_filter($this->getConfig('include', []), function ($dir) {
return is_dir($dir);
}),
'exclude' => $this->getConfig('exclude', []),
'name' => $this->getConfig('name', []),
],
];
}

Expand Down
8 changes: 0 additions & 8 deletions src/contract/WatcherInterface.php

This file was deleted.

10 changes: 10 additions & 0 deletions src/watcher/Driver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace think\swoole\watcher;

abstract class Driver
{
abstract public function watch(callable $callback);

abstract public function stop();
}
27 changes: 18 additions & 9 deletions src/watcher/Find.php → src/watcher/driver/Find.php
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
<?php

namespace think\swoole\watcher;
namespace think\swoole\watcher\driver;

use InvalidArgumentException;
use Swoole\Coroutine\System;
use Swoole\Timer;
use think\helper\Str;
use think\swoole\contract\WatcherInterface;
use think\swoole\watcher\Driver;

class Find implements WatcherInterface
class Find extends Driver
{
protected $name;
protected $directory;
protected $exclude;
protected $timer = null;

public function __construct($directory, $exclude, $name)
public function __construct($config)
{
$ret = System::exec('which find');
if (empty($ret['output'])) {
Expand All @@ -25,9 +26,9 @@ public function __construct($directory, $exclude, $name)
throw new InvalidArgumentException('find version not support.');
}

$this->directory = $directory;
$this->exclude = $exclude;
$this->name = $name;
$this->directory = $config['directory'];
$this->exclude = $config['exclude'];
$this->name = $config['name'];
}

public function watch(callable $callback)
Expand Down Expand Up @@ -63,15 +64,23 @@ public function watch(callable $callback)

$command = "find {$dest}{$name}{$notName}{$notPath} -mmin {$minutes} -type f -print";

Timer::tick($ms, function () use ($callback, $command) {
$this->timer = Timer::tick($ms, function () use ($callback, $command) {
$ret = System::exec($command);
if ($ret['code'] === 0 && strlen($ret['output'])) {
$stdout = trim($ret['output']);
if (!empty($stdout)) {
call_user_func($callback);
$files = array_filter(explode("\n", $stdout));
call_user_func($callback, $files);
}
}
});
}

public function stop()
{
if ($this->timer) {
Timer::clear($this->timer);
}
}

}
75 changes: 75 additions & 0 deletions src/watcher/driver/Fswatch.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

namespace think\swoole\watcher\driver;

use InvalidArgumentException;
use Swoole\Coroutine\System;
use Symfony\Component\Finder\Glob;
use Symfony\Component\Process\Process;
use think\swoole\watcher\Driver;
use Throwable;

class Fswatch extends Driver
{
protected $directory;
protected $matchRegexps = [];
/** @var Process */
protected $process;

public function __construct($config)
{
$ret = System::exec('which fswatch');
if (empty($ret['output'])) {
throw new InvalidArgumentException('which not exists.');
}

$this->directory = $config['directory'];

if (!empty($config['name'])) {
foreach ($config['name'] as $value) {
$this->matchRegexps[] = Glob::toRegex($value);
}
}
}

public function watch(callable $callback)
{
$command = $this->getCommand();
$this->process = new Process($command, timeout: 0);
try {
$this->process->run(function ($type, $data) use ($callback) {
$files = array_unique(array_filter(explode("\n", $data)));
if (!empty($this->matchRegexps)) {
$files = array_filter($files, function ($file) {
$filename = basename($file);
foreach ($this->matchRegexps as $regex) {
if (preg_match($regex, $filename)) {
return true;
}
}
return false;
});
}
if (!empty($files)) {
$callback($files);
}
});
} catch (Throwable) {

}
}

protected function getCommand()
{
$command = ["fswatch", "--format=%p", '-r', '--event=Created', '--event=Updated', '--event=Removed', '--event=Renamed'];

return [...$command, ...$this->directory];
}

public function stop()
{
if ($this->process) {
$this->process->stop();
}
}
}
27 changes: 17 additions & 10 deletions src/watcher/Scan.php → src/watcher/driver/Scan.php
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
<?php

namespace think\swoole\watcher;
namespace think\swoole\watcher\driver;

use Swoole\Timer;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
use think\swoole\contract\WatcherInterface;
use think\swoole\watcher\Driver;

class Scan implements WatcherInterface
class Scan extends Driver
{
protected $finder;

protected $files = [];
protected $timer = null;

public function __construct($directory, $exclude, $name)
public function __construct($config)
{
$this->finder = new Finder();
$this->finder
->files()
->name($name)
->in($directory)
->exclude($exclude);
->name($config['name'])
->in($config['directory'])
->exclude($config['exclude']);
}

protected function findFiles()
Expand All @@ -37,18 +37,25 @@ public function watch(callable $callback)
{
$this->files = $this->findFiles();

Timer::tick(2000, function () use ($callback) {
$this->timer = Timer::tick(2000, function () use ($callback) {

$files = $this->findFiles();

foreach ($files as $path => $time) {
if (empty($this->files[$path]) || $this->files[$path] != $time) {
call_user_func($callback);
call_user_func($callback, [$path]);
break;
}
}

$this->files = $files;
});
}

public function stop()
{
if ($this->timer) {
Timer::clear($this->timer);
}
}
}
4 changes: 4 additions & 0 deletions tests/Pest.php
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
<?php
define('STUB_DIR', realpath(__DIR__ . '/stub'));

$app = new \think\App(STUB_DIR);

$app->initialize();
47 changes: 47 additions & 0 deletions tests/unit/watcher/FswatchTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

use Swoole\Coroutine;
use Swoole\Timer;
use think\swoole\Watcher;
use think\swoole\watcher\Driver;
use function Swoole\Coroutine\run;

beforeEach(function () {
app()->config->set([
'hot_update' => [
'name' => ['*.txt'],
'include' => [runtime_path()],
'exclude' => [],
],
], 'swoole');
});

it('test fswatch watcher', function ($type) {
run(function () use ($type) {
$monitor = app(Watcher::class)->monitor($type);
expect($monitor)->toBeInstanceOf(Driver::class);

$changes = [];
Coroutine::create(function () use (&$changes, $monitor) {
$monitor->watch(function ($data) use (&$changes) {
$changes = array_merge($changes, $data);
});
});
Timer::after(500, function () {
file_put_contents(runtime_path() . 'some.css', 'test');
file_put_contents(runtime_path() . 'test.txt', 'test');
});

sleep(3);

expect($changes)->toBe([runtime_path() . 'test.txt']);
$monitor->stop();
});
})->with([
'find',
'fswatch',
'scan',
])->after(function () {
@unlink(runtime_path() . 'test.css');
@unlink(runtime_path() . 'test.txt');
});

0 comments on commit 535cfed

Please sign in to comment.