Skip to content
Merged
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
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,31 @@ or using CLI:
php-watcher server.php --exec php7
```

## Gracefully reloading down your script

It is possible to have PHP-watcher send any signal that you specify to your
application.

```bash
php-watcher --signal SIGTERM server.php
```

Your application can handle the signal as follows:

```php
<?php

declare(ticks = 1);
pcntl_signal(SIGTERM, 'terminationHandler');

function terminationHandler()
{
// ...
}
```

By default PHP-watcher sends SIGINT signal.

# License

MIT [http://rem.mit-license.org](http://rem.mit-license.org)
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"react/event-loop": "^1.1",
"symfony/yaml": "^4.3",
"react/child-process": "^0.6.1",
"ext-json": "*"
"ext-json": "*",
"ext-pcntl": "*"
},
"autoload": {
"psr-4": {
Expand Down
2 changes: 2 additions & 0 deletions src/Config/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public function build(InputInterface $input): Config
return new Config(
$configValues['script'],
$configValues['executable'],
$configValues['signal'],
$configValues['delay'],
$configValues['arguments'],
new WatchList(
Expand Down Expand Up @@ -64,6 +65,7 @@ private function valuesFromCommandLineArgs(InputInterface $input): array
'watch' => $input->getOption('watch'),
'extensions' => empty($input->getOption('ext')) ? [] : explode(',', $input->getOption('ext')),
'ignore' => $input->getOption('ignore'),
'signal' => $input->getOption('signal') ? constant($input->getOption('signal')) : null,
'delay' => (float)$input->getOption('delay'),
'arguments' => $input->getOption('arguments'),
];
Expand Down
25 changes: 20 additions & 5 deletions src/Config/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,29 @@ final class Config
{
private const DEFAULT_PHP_EXECUTABLE = 'php';
private const DEFAULT_DELAY_IN_SECONDS = 0.25;

private $delay;
private const DEFAULT_SIGNAL = SIGINT;

private $script;

private $phpExecutable;

private $signal;

private $delay;

/**
* @var string[]
*/
private $arguments;

private $watchList;

public function __construct(string $script, ?string $phpExecutable, ?float $delay, array $arguments, WatchList $watchList)
public function __construct(string $script, ?string $phpExecutable, ?int $signal, ?float $delay, array $arguments, WatchList $watchList)
{
$this->script = $script;
$this->delay = $delay ?: self::DEFAULT_DELAY_IN_SECONDS;
$this->phpExecutable = $phpExecutable ?: self::DEFAULT_PHP_EXECUTABLE;
$this->signal = $signal ?: self::DEFAULT_SIGNAL;
$this->delay = $delay ?: self::DEFAULT_DELAY_IN_SECONDS;
$this->arguments = $arguments;
$this->watchList = $watchList;
}
Expand All @@ -36,11 +40,22 @@ public function watchList(): WatchList

public function command(): string
{
return implode(' ', [$this->phpExecutable, $this->script, implode(' ', $this->arguments)]);
$commandline = implode(' ', [$this->phpExecutable, $this->script, implode(' ', $this->arguments)]);
if ('\\' !== DIRECTORY_SEPARATOR) {
// exec is mandatory to deal with sending a signal to the process
$commandline = 'exec '.$commandline;
}

return $commandline;
}

public function delay(): float
{
return $this->delay;
}

public function signal(): int
{
return $this->signal;
}
}
4 changes: 3 additions & 1 deletion src/Screen/Screen.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,13 @@ private function info(string $text): void

public function start(string $command): void
{
$this->info(sprintf('starting `%s`', str_replace("'", '', trim($command))));
$command = str_replace('exec', '', $command);
$this->info(sprintf('starting `%s`', trim($command)));
}

public function restarting(string $command): void
{
$this->output->writeln('');
$this->spinner->erase();
$this->output->writeln('');
$this->info('restarting due to changes...');
Expand Down
6 changes: 3 additions & 3 deletions src/Watcher/Watcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ public function __construct(LoopInterface $loop, Screen $screen, ChangesListener
$this->filesystemListener = $filesystemListener;
}

public function startWatching(Process $process, float $delayToRestart): void
public function startWatching(Process $process, int $signal, float $delayToRestart): void
{
$this->screen->start($process->getCommand());
$this->screen->showSpinner($this->loop);
$this->startProcess($process);

$this->filesystemListener->start(function () use ($process, $delayToRestart) {
$process->terminate();
$this->filesystemListener->start(function () use ($process, $signal, $delayToRestart) {
$process->terminate($signal);
$this->screen->restarting($process->getCommand());

$this->loop->addTimer($delayToRestart, function () use ($process) {
Expand Down
3 changes: 2 additions & 1 deletion src/WatcherCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ protected function configure(): void
->addOption('ignore', '-i', InputOption::VALUE_IS_ARRAY + InputOption::VALUE_OPTIONAL, 'Paths to ignore', [])
->addOption('exec', null, InputOption::VALUE_OPTIONAL, 'PHP executable')
->addOption('delay', null, InputOption::VALUE_OPTIONAL, 'Delaying restart')
->addOption('signal', null, InputOption::VALUE_OPTIONAL, 'Signal to reload the app')
->addOption('arguments', null, InputOption::VALUE_IS_ARRAY + InputOption::VALUE_OPTIONAL, 'Arguments for the script', [])
->addOption('config', null, InputOption::VALUE_OPTIONAL, 'Path to config file');
}
Expand All @@ -42,6 +43,6 @@ protected function execute(InputInterface $input, OutputInterface $output)

$screen->showOptions($config->watchList());
$process = new Process($config->command());
$watcher->startWatching($process, $config->delay());
$watcher->startWatching($process, $config->signal(), $config->delay());
}
}
23 changes: 23 additions & 0 deletions tests/Helper/Filesystem.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,29 @@ public static function createHelloWorldPHPFile(): string
return $name;
}

public static function createHelloWorldPHPFileWithSignalsHandling(): string
{
$name = self::FIXTURES_DIR . 'test.php';
$code = <<<CODE
<?php declare(ticks = 1);

pcntl_signal(SIGTERM, "handler");
pcntl_signal(SIGINT, "handler");

while (true) {
echo "Hello, world";
sleep(1);
}
function handler(\$signal) {
echo "\$signal signal was received" . PHP_EOL;
exit;
}
CODE;
self::createFile($name, $code);

return $name;
}

public static function createConfigFile(array $options): string
{
$name = self::FIXTURES_DIR . '.php-watcher.yml';
Expand Down
29 changes: 29 additions & 0 deletions tests/SignalTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php declare(strict_types=1);

namespace tests;

use tests\Helper\Filesystem;
use tests\Helper\WatcherRunner;
use tests\Helper\WatcherTestCase;

final class SignalTest extends WatcherTestCase
{
/** @test */
public function it_sends_a_specified_signal_to_restart_the_app(): void
{
if (!defined('SIGTERM')) {
$this->markTestSkipped('SIGTERM is not defined');
}

$scriptToRun = Filesystem::createHelloWorldPHPFileWithSignalsHandling();
$watcher = (new WatcherRunner)->run($scriptToRun, ['--signal', 'SIGTERM', '--watch', __DIR__]);
$this->wait();

Filesystem::createHelloWorldPHPFile();
$this->wait();

$output = $watcher->getOutput();

$this->assertStringContainsString(SIGTERM . ' signal was received', $output);
}
}