commands-1.x-dev/src/Commands/DrushCommands.php
src/Commands/DrushCommands.php
<?php
namespace Drupal\commands\Commands;
use Consolidation\OutputFormatters\StructuredData\RowsOfFields;
use Drupal\Component\Datetime\Time;
use Drupal\Core\Cache\Cache;
use Drupal\node\Entity\Node;
use Drupal\commands\Entity\Command;
use Drush\Commands\DrushCommands as DrushCommandsBase;
use Drush\Drush;
use Drush\Exceptions\CommandFailedException;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
/**
* A Drush commandfile.
*
* In addition to this file, you need a drush.services.yml
* in root of your module, and a composer.json file that provides the name
* of the services file to use.
*
* See these files for an example of injecting Drupal services:
* - http://cgit.drupalcode.org/devel/tree/src/Commands/DevelCommands.php
* - http://cgit.drupalcode.org/devel/tree/drush.services.yml
*/
class DrushCommands extends DrushCommandsBase {
/**
* Run Command
*
* @param $id
* The ID of the command to run.
* @param array $options
* An associative array of options whose values come from cli, aliases, config, etc.
* @option force
* Force running the command again, regardless of exit state. (Not recommended. Will be replaced with "run-again".)
* @usage command:run 123
* Enter the node ID of the command to run.
*
* @command command:run
* @aliases t
*/
public function commandRun($id, $options = [
'force' => false,
]) {
$command = Command::load($id);
if (empty($command)) {
throw new CommandFailedException(dt('No command found with that ID.'));
}
if (empty($command->state->value) || $command->state->value == Command::COMMAND_QUEUED || $options['force']) {
$this->logger()->success(dt('Found Command in state:!state [!summary](!link) !command', [
'!summary' => $command->label(),
'!link' => $command->toUrl()->setAbsolute(true)->toString(),
'!command' => $command->command->value,
'!state' => $command->state->value,
]));
if ($options['force']) {
$this->logger()->warning(dt('The command is being run again because the "force" option was used.'));
}
$this->commandRunExecute($command);
}
elseif ($command->state->value == Command::COMMAND_PROCESSING) {
$this->logger()->warning(dt('Command already running: !summary [!link].', [
'!summary' => $command->label(),
'!link' => $command->toUrl()->setAbsolute(true)->toString(),
]));
}
elseif ($command->state->value == Command::COMMAND_ERROR) {
$this->logger()->warning(dt('Command already failed: !summary [!link].', [
'!summary' => $command->label(),
'!link' => $command->toUrl()->setAbsolute(true)->toString(),
]));
}
elseif ($command->state->value === Command::COMMAND_OK) {
$this->logger()->warning(dt('Command already succeeded: !summary [!link].', [
'!summary' => $command->label(),
'!link' => $command->toUrl()->setAbsolute(true)->toString(),
]));
}
else {
# @TODO: (maybe?) Print constant names too.
throw new CommandFailedException(dt('Command state value is not found in Command. Possible states are: 0,1,2,3'));
}
}
private function commandRunExecute(Command $command) {
$this->logger()->info(dt('Command starting. Updated state. Command: @command', [
'@command' => $command->command->value,
]));
$command->set('state', Command::COMMAND_PROCESSING);
$command->set('output', null);
$command->set('executed', \Drupal::time()->getCurrentTime());
// Unset values in case this command is being retried
$command->set('finished', NULL);
$command->set('duration', NULL);
$command->setNewRevision();
$command->save();
// Turn off revisions for process run.
$command->setNewRevision(FALSE);
foreach ($command->command as $command_value) {
# "exec" does not work for system commands like "cd". So instead, we call "sh".
$args = ["/bin/sh", "-c", $command_value->value];
$process = Drush::process($args);
$process->setWorkingDirectory($command->working_directory->value ?? getcwd());
try {
$process->mustRun(function ($type, $buffer) use ($command){
// Echo output to the same stream the process returned.
if (Process::ERR === $type) {
$this->stderr()->write($buffer);
} else {
$this->output()->write($buffer);
}
// @TODO: Use insert queries directly? Test performance.
$command->get('output')->appendItem([
'output' => $buffer,
'stream' => $type == Process::OUT ? Process::STDOUT: Process::STDERR,
]);
$command->save();
});
$command->set('state', Command::COMMAND_OK);
$command->setNewRevision();
$command->set('finished', \Drupal::time()->getCurrentTime());
$command->save();
$this->logger()->info(dt('Command ended in Success. Updated state.'));
}
catch (ProcessFailedException $exception) {
$command->set('state', Command::COMMAND_ERROR);
$command->setNewRevision();
$command->set('finished', \Drupal::time()->getCurrentTime());
$command->save();
$this->logger()->info(dt('Command ended in Failure. Updated state.'));
throw $exception;
}
}
}
/**
* Run Commands as a daemon.
*
* @param array $options
* An associative array of options whose values come from cli, aliases, config, etc.
* @option force
* Force running the command again, regardless of exit state. (Not recommended. Will be replaced with "run-again".)
*
* @command command:daemon
* @aliases c:d
*/
public function commandDaemon($options = [
'sleep' => 1,
'force' => false,
])
{
while (TRUE) {
if (\Drupal::config('commands.settings')->get('daemon_enabled')) {
$this->logger()->notice(dt('Checking for commands...'));
$query = \Drupal::entityQuery('command');
$query->condition('state', Command::COMMAND_QUEUED);
$query->accessCheck(FALSE);
$result = $query->execute();
foreach ($result as $id) {
$this->commandRun($id);
}
sleep($options['sleep']);
}
else {
$this->logger()->notice(dt('Daemon is currently disabled.'));
sleep($options['sleep']);
continue;
}
}
}
}
