Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use different fs watchers #68

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .scrutinizer.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
build:
environment:
php: 7.2
php: 7.4

nodes:
analysis:
Expand Down
16 changes: 0 additions & 16 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,6 @@ language: php

jobs:
include:
- stage: "PHP7.2 - lowest"
php: 7.2
script:
- composer update -n --prefer-dist --prefer-lowest --no-suggest
- composer dump-autoload
- composer ci:tests
- composer ci:php:psalm

- stage: "PHP7.3 - highest"
php: 7.3
script:
- composer update -n --prefer-dist --no-suggest
- composer dump-autoload
- composer ci:tests
- composer ci:php:psalm

- stage: "PHP7.4 - highest"
php: 7.4
script:
Expand Down
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## 0.6.0 (2020-05-11)
## 0.6.0 (2020-06-14)
* Fix: don't use child process for resource watching
* Feature: add fswatch support
* Fix: min required PHP version is set to 7.4

## 0.5.2 (2019-12-07)
* Fix: use predefined const for PHP binary [#59](https://github.com/seregazhuk/php-watcher/pull/59)
Expand Down
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ PHP-watcher does not require any additional changes to your code or method of
* [Default executable](#default-executable)
* [Gracefully reloading down your script](#gracefully-reloading-down-your-script)
* [Automatic restart](#automatic-restart)
* [Performance](#performance)
* [Spinner](#spinner)

## Installation
Expand Down Expand Up @@ -235,9 +236,28 @@ script crashes PHP-watcher will notify you about that.

![app exit](images/exit.svg)

## Performance

The watcher can use different strategies to monitor your file system changes. Under the hood it
detects the environment and chooses the best suitable strategy.

### Resource-Watcher

By default, it uses [yosymfony/resource-watcher](https://github.com/yosymfony/resource-watcher
) which is the slowest, and most resource intensive option, but it should work on all environments.
Under the hood it is constantly asking the filesystem whether there are new changes or not.

### Fswatch

[FsWatch](https://github.com/emcrisostomo/fswatch) is a cross-platform (Linux,Mac,Windows) file change monitor which will automatically
use the platforms native functionality when possible. Under the hood the filesystem notifies us
when any changes occur. If your system has fswatch installed this strategy will be used.

**Has not been extensively tested.**

## Spinner

By default the watcher outputs a nice spinner which indicates that the process is running
By default, the watcher outputs a nice spinner which indicates that the process is running
and watching your files. But if your system doesn't support ansi coded the watcher
will try to detect it and disable the spinner. Or you can always disable the spinner
manually with option '--no-spinner':
Expand Down
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
}
],
"require": {
"php": "^7.2",
"php": "^7.4",
"ext-json": "*",
"ext-pcntl": "*",
"yosymfony/resource-watcher": "^2.0",
Expand All @@ -34,7 +34,8 @@
"react/child-process": "^0.6.1",
"react/stream": "^1.0.0",
"symfony/finder": "^4.3 || ^5.0",
"alecrabbit/php-cli-snake": "^0.5"
"alecrabbit/php-cli-snake": "^0.5",
"seregazhuk/reactphp-fswatch": "^0.1.0"
},
"autoload": {
"psr-4": {
Expand Down
3 changes: 0 additions & 3 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src</directory>
<exclude>
<file>src/Filesystem/watcher.php</file>
</exclude>
</whitelist>
</filter>
</phpunit>
7 changes: 4 additions & 3 deletions src/Filesystem/ChangesListener.php
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
<?php

declare(strict_types=1);
namespace seregazhuk\PhpWatcher\Filesystem;

use seregazhuk\PhpWatcher\Config\WatchList;
namespace seregazhuk\PhpWatcher\Filesystem;

interface ChangesListener
{
public function start(WatchList $watchList): void;
public function start(): void;

public function onChange(callable $callback): void;

public function stop(): void;
}
22 changes: 22 additions & 0 deletions src/Filesystem/Factory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace seregazhuk\PhpWatcher\Filesystem;

use React\EventLoop\LoopInterface;
use seregazhuk\PhpWatcher\Config\WatchList;
use seregazhuk\PhpWatcher\Filesystem\FsWatchBased\ChangesListener as FsWatchBased;
use seregazhuk\PhpWatcher\Filesystem\ResourceWatcherBased\ChangesListener as ResourceBased;

final class Factory
{
public static function create(WatchList $watchList, LoopInterface $loop): ChangesListener
{
if (FsWatchBased::isAvailable()) {
return new FsWatchBased($watchList, $loop);
}

return new ResourceBased($watchList, $loop);
}
}
89 changes: 89 additions & 0 deletions src/Filesystem/FsWatchBased/ChangesListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

declare(strict_types=1);

namespace seregazhuk\PhpWatcher\Filesystem\FsWatchBased;

use Evenement\EventEmitter;
use React\EventLoop\LoopInterface;
use seregazhuk\PhpWatcher\Config\WatchList;
use seregazhuk\PhpWatcher\Filesystem\ChangesListener as ChangesListenerInterface;
use Seregazhuk\ReactFsWatch\FsWatch;

final class ChangesListener extends EventEmitter implements ChangesListenerInterface
{
private FsWatch $fsWatch;

public function __construct(WatchList $watchList, LoopInterface $loop)
{
$this->fsWatch = new FsWatch($this->makeOptions($watchList), $loop);
}

public static function isAvailable(): bool
{
return FsWatch::isAvailable();
}

public function start(): void
{
$this->fsWatch->run();
$this->fsWatch->on(
'change',
function () {
$this->emit('change');
}
);
}

public function onChange(callable $callback): void
{
$this->on('change', $callback);
}

public function stop(): void
{
$this->fsWatch->stop();
}

private function makeOptions(WatchList $watchList): string
{
$options = [];

// first come paths
if ($watchList->paths()) {
$options[] = implode(' ', $watchList->paths());
}

// then we ignore
if ($watchList->ignore()) {
$options[] = '-e ' . implode(' ', $watchList->ignore());
}

// then include
if ($watchList->fileExtensions()) {
$options = array_merge($options, $this->makeIncludeOptions($watchList));
}

$options[] = '-I'; // Case-insensitive

return implode(' ', $options);
}

private function makeIncludeOptions(WatchList $watchList): array
{
$options = [];
// Before including we need to ignore everything
if (empty($watchList->ignore())) {
$options[] = '-e ".*"';
}

$regexpWithExtensions = array_map(
static function ($extension) {
return str_replace('*.', '.', $extension) . '$';
},
$watchList->fileExtensions()
);
$options[] = '-i ' . implode(' ', $regexpWithExtensions);
return $options;
}
}
28 changes: 18 additions & 10 deletions src/Filesystem/ResourceWatcherBased/ChangesListener.php
Original file line number Diff line number Diff line change
@@ -1,31 +1,35 @@
<?php declare(strict_types=1);
<?php

declare(strict_types=1);

namespace seregazhuk\PhpWatcher\Filesystem\ResourceWatcherBased;

use Evenement\EventEmitter;
use React\EventLoop\LoopInterface;
use seregazhuk\PhpWatcher\Config\WatchList;
use seregazhuk\PhpWatcher\Filesystem\ChangesListener as ChangesListenerInterface;
use Yosymfony\ResourceWatcher\ResourceWatcher;

final class ChangesListener extends EventEmitter implements
\seregazhuk\PhpWatcher\Filesystem\ChangesListener
final class ChangesListener extends EventEmitter implements ChangesListenerInterface
{
private const INTERVAL = 0.15;

private $loop;
private LoopInterface $loop;

private ResourceWatcher $watcher;

public function __construct(LoopInterface $loop)
public function __construct(WatchList $watchList, LoopInterface $loop)
{
$this->loop = $loop;
$this->watcher = ResourceWatcherBuilder::create($watchList);
}

public function start(WatchList $watchList): void
public function start(): void
{
$watcher = ResourceWatcherBuilder::create($watchList);

$this->loop->addPeriodicTimer(
self::INTERVAL,
function () use ($watcher) {
if ($watcher->findChanges()->hasChanges()) {
function () {
if ($this->watcher->findChanges()->hasChanges()) {
$this->emit('change');
}
}
Expand All @@ -36,4 +40,8 @@ public function onChange(callable $callback): void
{
$this->on('change', $callback);
}

public function stop(): void
{
}
}
10 changes: 4 additions & 6 deletions src/Watcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@
namespace seregazhuk\PhpWatcher;

use React\EventLoop\LoopInterface;
use seregazhuk\PhpWatcher\Config\WatchList;
use seregazhuk\PhpWatcher\Filesystem\ResourceWatcherBased\ChangesListener;
use seregazhuk\PhpWatcher\Filesystem\ChangesListener;

final class Watcher
{
private $loop;
private LoopInterface $loop;

private $filesystemListener;
private ChangesListener $filesystemListener;

public function __construct(LoopInterface $loop, ChangesListener $filesystemListener)
{
Expand All @@ -22,13 +21,12 @@ public function __construct(LoopInterface $loop, ChangesListener $filesystemList

public function startWatching(
ProcessRunner $processRunner,
WatchList $watchList,
int $signal,
float $delayToRestart
): void {
$processRunner->start();

$this->filesystemListener->start($watchList);
$this->filesystemListener->start();
$this->filesystemListener->onChange(
static function () use ($processRunner, $signal, $delayToRestart) {
$processRunner->stop($signal);
Expand Down
20 changes: 12 additions & 8 deletions src/WatcherCommand.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php

declare(strict_types=1);

namespace seregazhuk\PhpWatcher;
Expand All @@ -9,7 +10,8 @@
use seregazhuk\PhpWatcher\Config\Builder;
use seregazhuk\PhpWatcher\Config\Config;
use seregazhuk\PhpWatcher\Config\InputExtractor;
use seregazhuk\PhpWatcher\Filesystem\ResourceWatcherBased\ChangesListener;
use seregazhuk\PhpWatcher\Filesystem\Factory as ChangesListenerFactory;
use seregazhuk\PhpWatcher\Filesystem\ChangesListener;
use seregazhuk\PhpWatcher\Screen\Screen;
use seregazhuk\PhpWatcher\Screen\SpinnerFactory;
use Symfony\Component\Console\Command\Command as BaseCommand;
Expand Down Expand Up @@ -60,18 +62,16 @@ protected function execute(InputInterface $input, OutputInterface $output)
$config = $this->buildConfig(new InputExtractor($input));
$spinner = SpinnerFactory::create($output, $config->spinnerDisabled());

$this->addTerminationListeners($loop, $spinner);

$screen = new Screen(new SymfonyStyle($input, $output), $spinner);
$filesystem = new ChangesListener($loop);
$filesystem = ChangesListenerFactory::create($config->watchList(), $loop);

$screen->showOptions($config->watchList());
$processRunner = new ProcessRunner($loop, $screen, $config->command());
$this->addTerminationListeners($loop, $spinner, $filesystem);

$watcher = new Watcher($loop, $filesystem);
$watcher->startWatching(
$processRunner,
$config->watchList(),
$config->signalToReload(),
$config->delay()
);
Expand All @@ -82,10 +82,14 @@ protected function execute(InputInterface $input, OutputInterface $output)
/**
* When terminating the watcher we need to manually restore the cursor after the spinner.
*/
private function addTerminationListeners(LoopInterface $loop, SpinnerInterface $spinner): void
{
$func = static function (int $signal) use ($spinner): void {
private function addTerminationListeners(
LoopInterface $loop,
SpinnerInterface $spinner,
ChangesListener $changesListener
): void {
$func = static function (int $signal) use ($spinner, $changesListener): void {
$spinner->end();
$changesListener->stop();
exit($signal);
};

Expand Down
4 changes: 2 additions & 2 deletions tests/Feature/ChangesListenerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ final class ChangesListenerTest extends TestCase
public function it_emits_change_event_on_changes(): void
{
$loop = Factory::create();
$listener = new ChangesListener($loop);
$listener->start(new WatchList([Filesystem::fixturesDir()]));
$listener = new ChangesListener(new WatchList([Filesystem::fixturesDir()]), $loop);
$listener->start();

$loop->addTimer(1, [Filesystem::class, 'createHelloWorldPHPFile']);
$eventWasEmitted = false;
Expand Down
Loading