Skip to content
Snippets Groups Projects
Commit ff6f9a02 authored by Dillenn Terumalai's avatar Dillenn Terumalai :speech_balloon:
Browse files

// WIP

parent 26bea1a1
Branches
Tags
No related merge requests found
Showing with 371 additions and 62 deletions
......@@ -6,5 +6,3 @@ composer.lock
coverage
docs
vendor
\ No newline at end of file
docker/
docker-compose.yml
\ No newline at end of file
......@@ -4,7 +4,7 @@ test:package:
- apt-get update
- apt-get install -qq git curl libmcrypt-dev libjpeg-dev libpng-dev libfreetype6-dev libbz2-dev libzip-dev zip
- apt-get clean
- pecl install mcrypt
- pecl install mcrypt inotify
- docker-php-ext-enable mcrypt
- docker-php-ext-install zip
- curl --silent --show-error "https://getcomposer.org/installer" | php -- --install-dir=/usr/local/bin --filename=composer
......
......@@ -16,6 +16,7 @@ return [
'watchers' => [
'default' => [
'path' => storage_path('app'),
'mask' => 256 //IN_CREATE
],
],
......
# For more information: https://laravel.com/docs/sail
version: '3'
services:
laravel-watcher:
build:
context: ./docker
dockerfile: Dockerfile
image: laravel-watcher-7.4/app
volumes:
- '.:/var/www/html'
networks:
- sail
networks:
sail:
driver: bridge
\ No newline at end of file
FROM php:7.4-alpine
# Install dev dependencies
RUN apk add --no-cache --virtual .build-deps \
$PHPIZE_DEPS \
curl-dev \
imagemagick-dev \
libtool \
libxml2-dev \
postgresql-dev
# Install production dependencies
RUN apk add --no-cache \
bash \
curl \
freetype-dev \
g++ \
gcc \
git \
icu-dev \
icu-libs \
imagemagick \
libc-dev \
libjpeg-turbo-dev \
libpng-dev \
libzip-dev \
make \
oniguruma-dev \
yarn \
openssh-client \
postgresql-libs \
rsync \
zlib-dev
# Install PECL and PEAR extensions
RUN pecl install \
inotify \
redis \
imagick \
xdebug
# Enable PECL and PEAR extensions
RUN docker-php-ext-enable \
inotify \
redis \
imagick \
xdebug
# Configure php extensions
RUN docker-php-ext-configure gd --with-freetype --with-jpeg
# Install php extensions
RUN docker-php-ext-install \
bcmath \
calendar \
curl \
exif \
gd \
iconv \
intl \
mbstring \
pdo \
pdo_pgsql \
pcntl \
tokenizer \
xml \
zip
# Install composer
ENV COMPOSER_HOME /composer
ENV PATH ./vendor/bin:/composer/vendor/bin:$PATH
ENV COMPOSER_ALLOW_SUPERUSER 1
RUN curl -s https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin/ --filename=composer
# Install PHP_CodeSniffer
RUN composer global require "squizlabs/php_codesniffer=*"
# Cleanup dev dependencies
RUN apk del -f .build-deps
# Setup working directory
WORKDIR /var/www
<?php
namespace Dterumal\Watcher\Console;
use Illuminate\Console\Command;
class TestWatchCommand extends Command
{
protected $hidden = true;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'watch:test';
/**
* Execute the console command.
*/
public function handle(): void
{
dispatch(function() {
$this->callSilently('watch:run', [
'--timeout' => 10
]);
});
}
}
......@@ -20,7 +20,7 @@ class WatchCommand extends Command
{--force : Force the worker to run even in maintenance mode}
{--sleep=3 : Number of seconds to sleep when no job is available}
{--rest=0 : Number of seconds to rest between jobs}
{--once : Only process the event on the queue}';
{--timeout=0 : Only process the event on the queue}';
/**
* The console command description.
......@@ -62,7 +62,7 @@ class WatchCommand extends Command
*
* @return int|null
*/
public function handle(): ?int
public function handle()
{
if ($this->downForMaintenance()) {
$this->watcher->sleep($this->option('sleep'));
......@@ -71,6 +71,7 @@ class WatchCommand extends Command
$this->listenForEvents();
return $this->runWatcher();
}
/**
......@@ -98,7 +99,7 @@ class WatchCommand extends Command
$this->option('sleep'),
$this->option('force'),
$this->option('rest'),
$this->option('once')
$this->option('timeout')
);
}
......
<?php
namespace Dterumal\Watcher\Events;
class FileEvent
{
/**
* The file directory
*
* @var string
*/
public string $directory;
/**
* The file mask
*
* @var int
*/
public int $mask;
/**
* The file name
*
* @var string
*/
public string $name;
/**
* The file cookie
*
* @var int
*/
public int $cookie;
/**
* Create a new event instance.
*
* @param string $directory
* @param int $mask
* @param string $name
* @param int $cookie
*/
public function __construct(
string $directory,
string $name,
int $mask,
int $cookie
)
{
$this->directory = $directory;
$this->name = $name;
$this->mask = $mask;
$this->cookie = $cookie;
}
}
......@@ -5,20 +5,19 @@ namespace Dterumal\Watcher\Events;
class WatcherRestarted
{
/**
* The restart status.
* The last restart
*
* @var bool
* @var int
*/
public bool $status;
public int $lastRestart;
/**
* Create a new event instance.
*
* @param bool $status
* @return void
* @param int $lastRestart
*/
public function __construct(bool $status)
public function __construct(int $lastRestart)
{
$this->status = $status;
$this->lastRestart = $lastRestart;
}
}
<?php
namespace Dterumal\Watcher\Events;
class WatcherStopping
{
/**
* The exit status.
*
* @var int
*/
public int $status;
/**
* Create a new event instance.
*
* @param int $status
* @return void
*/
public function __construct(int $status)
{
$this->status = $status;
}
}
......@@ -2,7 +2,10 @@
namespace Dterumal\Watcher\Providers;
use Dterumal\Watcher\Events\FileEvent;
use Dterumal\Watcher\Events\WatcherCreated;
use Dterumal\Watcher\Events\WatcherRestarted;
use Dterumal\Watcher\Events\WatcherStopped;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
......@@ -10,7 +13,16 @@ class EventServiceProvider extends ServiceProvider
protected $listen = [
WatcherCreated::class => [
//
]
],
WatcherStopped::class => [
//
],
WatcherRestarted::class => [
//
],
FileEvent::class => [
//
],
];
/**
......
......@@ -3,8 +3,10 @@
namespace Dterumal\Watcher;
use Dterumal\Watcher\Events\FileEvent;
use Dterumal\Watcher\Events\WatcherCreated;
use Dterumal\Watcher\Events\WatcherStopping;
use Dterumal\Watcher\Events\WatcherRestarted;
use Dterumal\Watcher\Events\WatcherStopped;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Contracts\Cache\Repository as CacheContract;
use Illuminate\Contracts\Debug\ExceptionHandler;
......@@ -15,8 +17,6 @@ class Watcher
{
public const EXIT_SUCCESS = 0;
public const MASKS = 256;
/**
* The event dispatcher instance.
*
......@@ -106,6 +106,8 @@ class Watcher
$this->lastRestart = $this->getTimestampOfLastWatchRestart();
$timeStart = microtime(true);
while (true) {
if (!$this->daemonShouldRun($options)) {
$status = $this->pauseWatcher($options, $this->lastRestart);
......@@ -117,14 +119,24 @@ class Watcher
continue;
}
if ((int) $options->timeout !== 0) {
if ((microtime(true) - $timeStart) > (int) $options->timeout) {
$this->shouldQuit = true;
}
}
$events = inotify_read($this->inotify);
if (!empty($events)) {
foreach ($events as $event) {
ray($event);
if($options->once) {
$this->shouldQuit = true;
}
[$directory, $filename, $mask, $cookie] = [$this->watchers[$event['wd']], $event['name'], $event['mask'], $event['cookie']];
$this->raiseOnFileEvent(
$directory,
$filename,
$mask,
$cookie
);
if ($options->rest > 0) {
$this->sleep($options->rest);
......@@ -235,17 +247,16 @@ class Watcher
{
$paths = [];
foreach ($this->directories() as $directory) {
$path = storage_path($directory);
$paths[] = $path;
foreach ($this->directories() as $identifier => $value) {
$paths[] = $value['path'];
$watcher = inotify_add_watch(
$this->inotify,
$path,
self::MASKS
$value['path'],
$value['mask']
);
$this->watchers[$watcher] = basename($directory);
$this->raiseAfterWatcherCreation($path);
$this->watchers[$watcher] = $identifier;
$this->raiseAfterWatcherCreation($value['path']);
}
$this->registerWatchers($paths);
......@@ -344,6 +355,8 @@ class Watcher
$this->run();
$this->lastRestart = $this->getTimestampOfLastWatchRestart();
$this->raiseAfterWatcherRestart($this->lastRestart );
}
/**
......@@ -374,7 +387,7 @@ class Watcher
*/
protected function directories(): array
{
return ['app'];
return config('watcher.watchers');
}
/**
......@@ -385,11 +398,30 @@ class Watcher
*/
public function stop($status = 0): int
{
$this->events->dispatch(new WatcherStopping($status));
$this->events->dispatch(new WatcherStopped($status));
return $status;
}
/**
* Raise after a watcher creation.
*
* @param string $directory
* @param string $filename
* @param int $mask
* @param int $cookie
* @return void
*/
protected function raiseOnFileEvent(string $directory, string $filename, int $mask, int $cookie): void
{
$this->events->dispatch(new FileEvent(
$directory,
$filename,
$mask,
$cookie
));
}
/**
* Raise after a watcher creation.
*
......@@ -403,6 +435,19 @@ class Watcher
));
}
/**
* Raise after a watcher creation.
*
* @param int $lastRestart
* @return void
*/
protected function raiseAfterWatcherRestart(int $lastRestart): void
{
$this->events->dispatch(new WatcherRestarted(
$lastRestart
));
}
/**
* Set the cache repository implementation.
*
......
......@@ -28,9 +28,9 @@ class WatcherOptions
/**
* Indicates if the watcher should stop after one event.
*
* @var boolean
* @var mixed
*/
public $once;
public $timeout;
/**
* Create a new watcher options instance.
......@@ -40,11 +40,11 @@ class WatcherOptions
* @param mixed $rest
* @return void
*/
public function __construct($sleep = 5, $force = false, $rest = 0, $once = false)
public function __construct($sleep = 5, $force = false, $rest = 0, $timeout = 0)
{
$this->sleep = $sleep;
$this->rest = $rest;
$this->force = $force;
$this->once = $once;
$this->timeout = $timeout;
}
}
......@@ -4,6 +4,7 @@ namespace Dterumal\Watcher;
use Dterumal\Watcher\Console\RestartCommand;
use Dterumal\Watcher\Console\StopCommand;
use Dterumal\Watcher\Console\TestWatchCommand;
use Dterumal\Watcher\Console\WatchCommand;
use Dterumal\Watcher\Console\WatchListCommand;
use Dterumal\Watcher\Providers\EventServiceProvider;
......@@ -27,6 +28,8 @@ class WatcherServiceProvider extends ServiceProvider
});
$this->app->register(EventServiceProvider::class);
$this->mergeConfigFrom(__DIR__.'/../config/config.php', 'watcher');
}
public function boot()
......@@ -35,7 +38,8 @@ class WatcherServiceProvider extends ServiceProvider
RestartCommand::class,
StopCommand::class,
WatchCommand::class,
WatchListCommand::class
WatchListCommand::class,
TestWatchCommand::class
]);
if ($this->app->runningInConsole()) {
......
......@@ -2,60 +2,82 @@
namespace Dterumal\Watcher\Tests\Feature;
use Dterumal\Watcher\Events\FileEvent;
use Dterumal\Watcher\Events\WatcherCreated;
use Dterumal\Watcher\Events\WatcherRestarted;
use Dterumal\Watcher\Events\WatcherStopped;
use Dterumal\Watcher\Tests\TestCase;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Process\Process;
class WatcherRunCommandTest extends TestCase
class WatcherCommandTest extends TestCase
{
/** @test */
function it_starts_the_watcher()
function it_can_start_the_watcher_with_a_timeout()
{
Event::fake();
// destination path of the Foo class
$fooFile = storage_path('app/test.txt');
// Directory watched
$directory = storage_path('app');
// make sure we're starting from a clean state
if (File::exists($fooFile)) {
unlink($fooFile);
}
// Run the artisan command
$exitCode = Artisan::call('watch:run', [
'--timeout' => 1
], new NullOutput);
$this->assertFalse(File::exists($fooFile));
// Check that exit code is zero
$this->assertSame(0, $exitCode);
// Run the make command
$process = new Process(['/usr/local/bin/php /var/www/html/artisan']);
$process->start();
// Check that the watcher was created
Event::assertDispatched(WatcherCreated::class, function ($event) use ($directory) {
return $event->directory === $directory;
});
if (!$process->isSuccessful()) {
throw new ProcessFailedException($process);
// Check that the watcher was stopped
Event::assertDispatched(WatcherStopped::class, function ($event) {
return $event->status === 0;
});
}
dump($process->getOutput());
/*$command = $this->artisan('watch:run', [
'--once' => true
]);*/
/** @test */
function it_detects_a_file()
{
Event::fake();
Event::assertDispatched(WatcherCreated::class, function ($event) use ($directory) {
return $event->directory === $directory;
});
// File created
$fooFile = storage_path('app/test.txt');
//$command->expectsOutput('- Watching: /var/www/html/storage/app');
// make sure we're starting from a clean state
if (File::exists($fooFile)) {
unlink($fooFile);
}
file_put_contents($fooFile, 'Hello world!');
// Start background process
$process = new Process(['/bin/bash', 'sleep-and-create-a-file.sh', $fooFile], $this->getStubDirectory());
$process->start();
//$command->assertExitCode(0);
// Run the artisan command
$exitCode = Artisan::call('watch:run', [
'--timeout' => 3
], new NullOutput);
// Assert a new file is created
$this->assertTrue(File::exists($fooFile));
unlink($fooFile);
// Check that exit code
$this->assertSame(0, $exitCode);
// Check that the watcher was created
Event::assertDispatched(FileEvent::class, function ($event) {
ray($event);
return $event->directory === 'default' &&
$event->mask === 256 &&
$event->cookie === 0 &&
$event->name === 'test.txt';
});
}
}
\ No newline at end of file
......@@ -27,4 +27,9 @@ class TestCase extends \Orchestra\Testbench\TestCase
{
// perform environment setup
}
public function getStubDirectory(): string
{
return __DIR__.'/stubs';
}
}
\ No newline at end of file
#!/bin/bash
sleep 1
touch $1
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment