Skip to content

Commit

Permalink
Merge pull request #86 from jigarius/feature/handle-sigint
Browse files Browse the repository at this point in the history
Handle SIGINT gracefully
  • Loading branch information
jigarius authored Jan 3, 2024
2 parents 2c12c0a + 7fbf7f7 commit 7e3260d
Show file tree
Hide file tree
Showing 12 changed files with 219 additions and 88 deletions.
3 changes: 3 additions & 0 deletions .docker/main/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ RUN cp "$PHP_INI_DIR/php.ini-development" "$PHP_INI_PATH" \
&& pear config-set php_ini "$PHP_INI_PATH" \
&& pecl install --force xdebug

RUN docker-php-ext-configure pcntl --enable-pcntl \
&& docker-php-ext-install pcntl

# Provision Drall.
COPY . /opt/drall

Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ command just like version 1.x.
- `drall exec:drush ...` is now `drall exec drush ...`
- `drall exec:shell ...` is now `drall exec ...`

#### Interrupting a command

When `drall exec` receives a signal to interrupt (usually `ctrl + c`), Drall
stops after processing the site that is currently being processed. This
prevents the current command from terminating abruptly. However, if a second
interrupt signal is received, then Drall stops immediately.

#### Drush with @@dir

In this method, the `--uri` option is sent to `drush`.
Expand Down
7 changes: 7 additions & 0 deletions src/Command/BaseCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Drall\Command;

use Drall\Service\SiteDetector;
use Drall\Trait\SiteDetectorAwareTrait;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\Console\Command\Command;
Expand Down Expand Up @@ -68,6 +69,12 @@ protected function preExecute(InputInterface $input, OutputInterface $output) {
$this->logger = new ConsoleLogger($output);
}

if (!$this->hasSiteDetector()) {
$root = $input->getParameterOption('--root') ?: getcwd();
$siteDetector = SiteDetector::create($root);
$this->setSiteDetector($siteDetector);
}

if ($group = $this->getDrallGroup($input)) {
$this->logger->debug('Detected group: {group}', ['group' => $group]);
}
Expand Down
82 changes: 68 additions & 14 deletions src/Command/ExecCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Drall\Model\EnvironmentId;
use Drall\Model\Placeholder;
use Drall\Model\RawCommand;
use Drall\Trait\SignalAwareTrait;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
Expand All @@ -24,6 +25,8 @@
*/
class ExecCommand extends BaseCommand {

use SignalAwareTrait;

/**
* Maximum number of Drall workers.
*
Expand Down Expand Up @@ -144,17 +147,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
return 0;
}

// Determine number of workers.
$workers = $input->getOption('drall-workers');

if ($workers > self::WORKER_LIMIT) {
$this->logger->warning('Limiting workers to {count}, which is the maximum.', ['count' => self::WORKER_LIMIT]);
$workers = self::WORKER_LIMIT;
}

if ($workers > 1) {
$this->logger->notice("Using {count} workers.", ['count' => $workers]);
}
$workers = $this->getWorkerCount($input);

// Display commands without executing them.
if ($input->getOption('drall-no-execute')) {
Expand All @@ -173,11 +166,40 @@ protected function execute(InputInterface $input, OutputInterface $output): int
);
$exitCode = 0;

Loop::run(function () use ($values, $command, $placeholder, $output, $progressBar, $workers, &$exitCode) {
// Handle interruption signals to stop Drall gracefully.
$isStopping = FALSE;
$this->registerInterruptionListener(function () use (&$isStopping, $output) {
$output->writeln('');

// If a previous SIGINT was received, then stop immediately.
if ($isStopping) {
$this->logger->error('Interrupted by user.');
exit(1);
}

// Prepare to stop after the current item is processed.
$this->logger->warning('Stopping after current item.');
$isStopping = TRUE;
});

Loop::run(function () use (
$values,
$command,
$placeholder,
$output,
$progressBar,
$workers,
&$exitCode,
&$isStopping
) {
yield ConcurrentIterator\each(
Iterator\fromIterable($values),
new LocalSemaphore($workers),
function ($value) use ($command, $placeholder, $output, $progressBar, &$exitCode) {
function ($value) use ($command, $placeholder, $output, $progressBar, &$exitCode, &$isStopping) {
if ($isStopping) {
return;
}

$sCommand = Placeholder::replace([$placeholder->value => $value], $command);
$process = new Process("($sCommand) 2>&1", getcwd());

Expand All @@ -199,9 +221,17 @@ function ($value) use ($command, $placeholder, $output, $progressBar, &$exitCode
);
});

$progressBar->finish();
if (!$isStopping) {
$progressBar->finish();
}

$output->writeln('');

if ($isStopping) {
$this->logger->error('Interrupted by user.');
return 1;
}

return $exitCode;
}

Expand All @@ -227,6 +257,30 @@ protected function getCommand(): RawCommand {
return $command;
}

/**
* Gets the number of workers that should be used.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* The input.
*
* @return int
* Number of workers to be used.
*/
protected function getWorkerCount(InputInterface $input): int {
$result = $input->getOption('drall-workers');

if ($result > self::WORKER_LIMIT) {
$this->logger->warning('Limiting workers to {count}, which is the maximum.', ['count' => self::WORKER_LIMIT]);
$result = self::WORKER_LIMIT;
}

if ($result > 1) {
$this->logger->notice("Using {count} workers.", ['count' => $result]);
}

return $result;
}

/**
* Get unique placeholder from a command.
*/
Expand Down
48 changes: 5 additions & 43 deletions src/Drall.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,14 @@

namespace Drall;

use Consolidation\SiteAlias\SiteAliasManager;
use Drall\Command\ExecCommand;
use Drall\Command\SiteAliasesCommand;
use Drall\Command\SiteDirectoriesCommand;
use Drall\Command\SiteKeysCommand;
use Drall\Model\EnvironmentId;
use Drall\Service\SiteDetector;
use Drall\Trait\SiteDetectorAwareTrait;
use DrupalFinder\DrupalFinder;
use Drush\SiteAlias\SiteAliasFileLoader;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
Expand All @@ -31,39 +26,16 @@ final class Drall extends Application {
/**
* Creates a Phpake Application instance.
*/
public function __construct(
SiteDetector $siteDetector = NULL,
?InputInterface $input = NULL
) {
public function __construct() {
parent::__construct();
$this->setName(self::NAME);
$this->setVersion(self::VERSION);
$this->setAutoExit(FALSE);

// @todo Instead of using $input to create a SiteDetector here, we can
// create the SiteDetector in BaseCommand::preExecute(). That way,
// we won't need this extra dependency injection, thereby simplifying
// the code and the tests.
$input = $input ?? new ArgvInput();
$root = $input->getParameterOption('--root') ?: getcwd();
$siteDetector ??= $this->createDefaultSiteDetector($root);
$this->setSiteDetector($siteDetector);

$cmd = new SiteDirectoriesCommand();
$cmd->setSiteDetector($siteDetector);
$this->add($cmd);

$cmd = new SiteKeysCommand();
$cmd->setSiteDetector($siteDetector);
$this->add($cmd);

$cmd = new SiteAliasesCommand();
$cmd->setSiteDetector($siteDetector);
$this->add($cmd);

$cmd = new ExecCommand();
$cmd->setSiteDetector($siteDetector);
$this->add($cmd);
$this->add(new SiteDirectoriesCommand());
$this->add(new SiteKeysCommand());
$this->add(new SiteAliasesCommand());
$this->add(new ExecCommand());
}

protected function configureIO(InputInterface $input, OutputInterface $output) {
Expand Down Expand Up @@ -110,16 +82,6 @@ protected function getDefaultInputDefinition(): InputDefinition {
return $definition;
}

private function createDefaultSiteDetector(string $root): SiteDetector {
$drupalFinder = new DrupalFinder();
$drupalFinder->locateRoot($root);

$siteAliasManager = new SiteAliasManager(new SiteAliasFileLoader());
$siteAliasManager->addSearchLocation($drupalFinder->getComposerRoot() . '/drush/sites');

return new SiteDetector($drupalFinder, $siteAliasManager);
}

public function find($name) {
try {
return parent::find($name);
Expand Down
21 changes: 21 additions & 0 deletions src/Service/SiteDetector.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@

use Consolidation\Filter\FilterOutputData;
use Consolidation\Filter\LogicalOpFactory;
use Consolidation\SiteAlias\SiteAliasManager;
use Consolidation\SiteAlias\SiteAliasManagerAwareTrait;
use Consolidation\SiteAlias\SiteAliasManagerInterface;
use Drall\Model\SitesFile;
use Drall\Trait\DrupalFinderAwareTrait;
use DrupalFinder\DrupalFinder;
use Drush\SiteAlias\SiteAliasFileLoader;

class SiteDetector {

Expand Down Expand Up @@ -211,6 +213,25 @@ private function filter(
return $result;
}

/**
* Create a SiteDetector given a Drupal project root.
*
* @param string $root
* Composer project root directory.
*
* @return static
* A SiteDetector.
*/
public static function create(string $root): static {
$drupalFinder = new DrupalFinder();
$drupalFinder->locateRoot($root);

$siteAliasManager = new SiteAliasManager(new SiteAliasFileLoader());
$siteAliasManager->addSearchLocation($drupalFinder->getComposerRoot() . '/drush/sites');

return new SiteDetector($drupalFinder, $siteAliasManager);
}

/**
* Detects sites in a "sites" directory.
*
Expand Down
46 changes: 46 additions & 0 deletions src/Trait/SignalAwareTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace Drall\Trait;

trait SignalAwareTrait {

/**
* Register a signal listener.
*
* @param int $signo
* Signal Number.
* @param callable $handler
* A callable event listener.
*
* @return bool
* True if the listener was registered.
*/
protected function registerSignalListener(int $signo, callable $handler): bool {
if (!extension_loaded('pcntl')) {
return FALSE;
}

declare(ticks = 1);
\pcntl_signal($signo, $handler);

return TRUE;
}

/**
* Register a listener for SIGINT.
*
* @param callable $handler
* A callable event listener.
*
* @return bool
* True if the listener was registered.
*/
protected function registerInterruptionListener(callable $handler): bool {
if (!defined('SIGINT')) {
return FALSE;
}

return self::registerSignalListener(SIGINT, $handler);
}

}
2 changes: 1 addition & 1 deletion src/Trait/SiteDetectorAwareTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public function setSiteDetector(SiteDetector $siteDetector) {
public function siteDetector(): SiteDetector {
if (!$this->hasSiteDetector()) {
throw new \BadMethodCallException(
'A site detecetor instance must first be assigned'
'A site detector instance must first be assigned'
);
}

Expand Down
19 changes: 11 additions & 8 deletions test/Unit/Command/ExecCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ public function testNoSitesFound() {
->method('getSiteDirNames')
->willReturn([]);

$app = new Drall($siteDetectorMock);
$app = new Drall();
$input = ['cmd' => 'cat @@dir'];
$command = $app->find('exec')
->setArgv(self::arrayInputAsArgv($input));
/** @var \Drall\Command\ExecCommand $command */
$command = $app->find('exec');
$command->setSiteDetector($siteDetectorMock);
$command->setArgv(self::arrayInputAsArgv($input));
$tester = new CommandTester($command);
$tester->execute($input);

Expand All @@ -54,11 +56,12 @@ public function testNonZeroExitCode() {
->method('getSiteAliasNames')
->willReturn(['@splinter', '@shredder']);

$app = new Drall($siteDetectorMock);
$app = new Drall();
$input = ['cmd' => 'drush @@site.dev core:rebuild'];
/** @var ExecCommand $command */
$command = $app->find('exec')
->setArgv(self::arrayInputAsArgv($input));
/** @var \Drall\Command\ExecCommand $command */
$command = $app->find('exec');
$command->setSiteDetector($siteDetectorMock);
$command->setArgv(self::arrayInputAsArgv($input));
$tester = new CommandTester($command);

$this->assertEquals(1, $tester->execute($input));
Expand Down Expand Up @@ -95,7 +98,7 @@ public function testWithMixedPlaceholders() {
}

/**
* Drall caps the maximum number of workers.
* Drall caps the maximum number of workers to pre-determined limit.
*/
public function testWorkerLimit() {
$input = [
Expand Down
Loading

0 comments on commit 7e3260d

Please sign in to comment.