Skip to content

Commit 535cfed

Browse files
committed
热更新支持fswatch驱动
1 parent 788f50f commit 535cfed

File tree

10 files changed

+194
-37
lines changed

10 files changed

+194
-37
lines changed

composer.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@
1212
"php": "^8.0",
1313
"ext-json": "*",
1414
"ext-swoole": "^4.0|^5.0",
15+
"topthink/framework": "^6.0|^8.0",
1516
"nette/php-generator": "^4.0",
1617
"open-smf/connection-pool": ">=1.0",
1718
"stechstudio/backoff": "^1.2",
1819
"symfony/finder": ">=4.3",
19-
"topthink/framework": "^6.0|^8.0",
20+
"symfony/process": ">=4.2",
2021
"swoole/ide-helper": "^5.0"
2122
},
2223
"require-dev": {

phpstan.neon

+3
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ parameters:
22
level: 5
33
paths:
44
- src
5+
- tests
56
scanFiles:
67
- vendor/topthink/framework/src/helper.php
78
scanDirectories:
89
- vendor/swoole/ide-helper/src/swoole_library/src
910
treatPhpDocTypesAsCertain: false
11+
universalObjectCratesClasses:
12+
- PHPUnit\Framework\TestCase
1013
ignoreErrors:
1114
-
1215
identifier: while.alwaysTrue

src/Watcher.php

+18-9
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,37 @@
22

33
namespace think\swoole;
44

5-
use think\swoole\contract\WatcherInterface;
6-
75
/**
8-
* @mixin WatcherInterface
6+
* @mixin \think\swoole\watcher\Driver
97
*/
108
class Watcher extends \think\Manager
119
{
12-
protected $namespace = '\\think\\swoole\\watcher\\';
10+
protected $namespace = '\\think\\swoole\\watcher\\driver\\';
1311

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

17+
/**
18+
* @param $name
19+
* @return \think\swoole\watcher\Driver
20+
*/
21+
public function monitor($name = null)
22+
{
23+
return $this->driver($name);
24+
}
25+
1926
protected function resolveParams($name): array
2027
{
2128
return [
22-
array_filter($this->getConfig('include', []), function ($dir) {
23-
return is_dir($dir);
24-
}),
25-
$this->getConfig('exclude', []),
26-
$this->getConfig('name', []),
29+
[
30+
'directory' => array_filter($this->getConfig('include', []), function ($dir) {
31+
return is_dir($dir);
32+
}),
33+
'exclude' => $this->getConfig('exclude', []),
34+
'name' => $this->getConfig('name', []),
35+
],
2736
];
2837
}
2938

src/contract/WatcherInterface.php

-8
This file was deleted.

src/watcher/Driver.php

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace think\swoole\watcher;
4+
5+
abstract class Driver
6+
{
7+
abstract public function watch(callable $callback);
8+
9+
abstract public function stop();
10+
}

src/watcher/Find.php src/watcher/driver/Find.php

+18-9
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
<?php
22

3-
namespace think\swoole\watcher;
3+
namespace think\swoole\watcher\driver;
44

55
use InvalidArgumentException;
66
use Swoole\Coroutine\System;
77
use Swoole\Timer;
88
use think\helper\Str;
9-
use think\swoole\contract\WatcherInterface;
9+
use think\swoole\watcher\Driver;
1010

11-
class Find implements WatcherInterface
11+
class Find extends Driver
1212
{
1313
protected $name;
1414
protected $directory;
1515
protected $exclude;
16+
protected $timer = null;
1617

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

28-
$this->directory = $directory;
29-
$this->exclude = $exclude;
30-
$this->name = $name;
29+
$this->directory = $config['directory'];
30+
$this->exclude = $config['exclude'];
31+
$this->name = $config['name'];
3132
}
3233

3334
public function watch(callable $callback)
@@ -63,15 +64,23 @@ public function watch(callable $callback)
6364

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

66-
Timer::tick($ms, function () use ($callback, $command) {
67+
$this->timer = Timer::tick($ms, function () use ($callback, $command) {
6768
$ret = System::exec($command);
6869
if ($ret['code'] === 0 && strlen($ret['output'])) {
6970
$stdout = trim($ret['output']);
7071
if (!empty($stdout)) {
71-
call_user_func($callback);
72+
$files = array_filter(explode("\n", $stdout));
73+
call_user_func($callback, $files);
7274
}
7375
}
7476
});
7577
}
7678

79+
public function stop()
80+
{
81+
if ($this->timer) {
82+
Timer::clear($this->timer);
83+
}
84+
}
85+
7786
}

src/watcher/driver/Fswatch.php

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
namespace think\swoole\watcher\driver;
4+
5+
use InvalidArgumentException;
6+
use Swoole\Coroutine\System;
7+
use Symfony\Component\Finder\Glob;
8+
use Symfony\Component\Process\Process;
9+
use think\swoole\watcher\Driver;
10+
use Throwable;
11+
12+
class Fswatch extends Driver
13+
{
14+
protected $directory;
15+
protected $matchRegexps = [];
16+
/** @var Process */
17+
protected $process;
18+
19+
public function __construct($config)
20+
{
21+
$ret = System::exec('which fswatch');
22+
if (empty($ret['output'])) {
23+
throw new InvalidArgumentException('which not exists.');
24+
}
25+
26+
$this->directory = $config['directory'];
27+
28+
if (!empty($config['name'])) {
29+
foreach ($config['name'] as $value) {
30+
$this->matchRegexps[] = Glob::toRegex($value);
31+
}
32+
}
33+
}
34+
35+
public function watch(callable $callback)
36+
{
37+
$command = $this->getCommand();
38+
$this->process = new Process($command, timeout: 0);
39+
try {
40+
$this->process->run(function ($type, $data) use ($callback) {
41+
$files = array_unique(array_filter(explode("\n", $data)));
42+
if (!empty($this->matchRegexps)) {
43+
$files = array_filter($files, function ($file) {
44+
$filename = basename($file);
45+
foreach ($this->matchRegexps as $regex) {
46+
if (preg_match($regex, $filename)) {
47+
return true;
48+
}
49+
}
50+
return false;
51+
});
52+
}
53+
if (!empty($files)) {
54+
$callback($files);
55+
}
56+
});
57+
} catch (Throwable) {
58+
59+
}
60+
}
61+
62+
protected function getCommand()
63+
{
64+
$command = ["fswatch", "--format=%p", '-r', '--event=Created', '--event=Updated', '--event=Removed', '--event=Renamed'];
65+
66+
return [...$command, ...$this->directory];
67+
}
68+
69+
public function stop()
70+
{
71+
if ($this->process) {
72+
$this->process->stop();
73+
}
74+
}
75+
}
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
11
<?php
22

3-
namespace think\swoole\watcher;
3+
namespace think\swoole\watcher\driver;
44

55
use Swoole\Timer;
66
use Symfony\Component\Finder\Finder;
77
use Symfony\Component\Finder\SplFileInfo;
8-
use think\swoole\contract\WatcherInterface;
8+
use think\swoole\watcher\Driver;
99

10-
class Scan implements WatcherInterface
10+
class Scan extends Driver
1111
{
1212
protected $finder;
13-
1413
protected $files = [];
14+
protected $timer = null;
1515

16-
public function __construct($directory, $exclude, $name)
16+
public function __construct($config)
1717
{
1818
$this->finder = new Finder();
1919
$this->finder
2020
->files()
21-
->name($name)
22-
->in($directory)
23-
->exclude($exclude);
21+
->name($config['name'])
22+
->in($config['directory'])
23+
->exclude($config['exclude']);
2424
}
2525

2626
protected function findFiles()
@@ -37,18 +37,25 @@ public function watch(callable $callback)
3737
{
3838
$this->files = $this->findFiles();
3939

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

4242
$files = $this->findFiles();
4343

4444
foreach ($files as $path => $time) {
4545
if (empty($this->files[$path]) || $this->files[$path] != $time) {
46-
call_user_func($callback);
46+
call_user_func($callback, [$path]);
4747
break;
4848
}
4949
}
5050

5151
$this->files = $files;
5252
});
5353
}
54+
55+
public function stop()
56+
{
57+
if ($this->timer) {
58+
Timer::clear($this->timer);
59+
}
60+
}
5461
}

tests/Pest.php

+4
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
<?php
22
define('STUB_DIR', realpath(__DIR__ . '/stub'));
3+
4+
$app = new \think\App(STUB_DIR);
5+
6+
$app->initialize();

tests/unit/watcher/FswatchTest.php

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
use Swoole\Coroutine;
4+
use Swoole\Timer;
5+
use think\swoole\Watcher;
6+
use think\swoole\watcher\Driver;
7+
use function Swoole\Coroutine\run;
8+
9+
beforeEach(function () {
10+
app()->config->set([
11+
'hot_update' => [
12+
'name' => ['*.txt'],
13+
'include' => [runtime_path()],
14+
'exclude' => [],
15+
],
16+
], 'swoole');
17+
});
18+
19+
it('test fswatch watcher', function ($type) {
20+
run(function () use ($type) {
21+
$monitor = app(Watcher::class)->monitor($type);
22+
expect($monitor)->toBeInstanceOf(Driver::class);
23+
24+
$changes = [];
25+
Coroutine::create(function () use (&$changes, $monitor) {
26+
$monitor->watch(function ($data) use (&$changes) {
27+
$changes = array_merge($changes, $data);
28+
});
29+
});
30+
Timer::after(500, function () {
31+
file_put_contents(runtime_path() . 'some.css', 'test');
32+
file_put_contents(runtime_path() . 'test.txt', 'test');
33+
});
34+
35+
sleep(3);
36+
37+
expect($changes)->toBe([runtime_path() . 'test.txt']);
38+
$monitor->stop();
39+
});
40+
})->with([
41+
'find',
42+
'fswatch',
43+
'scan',
44+
])->after(function () {
45+
@unlink(runtime_path() . 'test.css');
46+
@unlink(runtime_path() . 'test.txt');
47+
});

0 commit comments

Comments
 (0)