* * * Licensed under MIT license. */ namespace Ahc\Cli\Helper; use Ahc\Cli\Exception\RuntimeException; use function fclose; use function function_exists; use function fwrite; use function is_resource; use function microtime; use function proc_close; use function proc_get_status; use function proc_open; use function proc_terminate; use function stream_get_contents; use function stream_set_blocking; use const DIRECTORY_SEPARATOR; /** * A thin proc_open wrapper to execute shell commands. * * With some inspirations from symfony/process. * * @author Sushil Gupta * @license MIT * * @link https://github.com/adhocore/cli */ class Shell { const STDIN_DESCRIPTOR_KEY = 0; const STDOUT_DESCRIPTOR_KEY = 1; const STDERR_DESCRIPTOR_KEY = 2; const STATE_READY = 'ready'; const STATE_STARTED = 'started'; const STATE_CLOSED = 'closed'; const STATE_TERMINATED = 'terminated'; const DEFAULT_STDIN_WIN = ['pipe', 'r']; const DEFAULT_STDIN_NIX = ['pipe', 'r']; const DEFAULT_STDOUT_WIN = ['pipe', 'w']; const DEFAULT_STDOUT_NIX = ['pipe', 'w']; const DEFAULT_STDERR_WIN = ['pipe', 'w']; const DEFAULT_STDERR_NIX = ['pipe', 'w']; /** @var bool Whether to wait for the process to finish or return instantly */ protected bool $async = false; /** @var string Current working directory */ protected ?string $cwd = null; /** @var array Descriptor to be passed for proc_open */ protected array $descriptors; /** @var array An array of environment variables */ protected ?array $env = null; /** @var int Exit code of the process once it has been terminated */ protected ?int $exitCode = null; /** @var array Other options to be passed for proc_open */ protected array $otherOptions = []; /** @var array Pointers to stdin, stdout & stderr */ protected array $pipes = []; /** @var resource The actual process resource returned from proc_open */ protected $process = null; /** @var float Process starting time in unix timestamp */ protected float $processStartTime = 0; /** @var array Status of the process as returned from proc_get_status */ protected ?array $processStatus = null; /** @var float Default timeout for the process in seconds with microseconds */ protected ?float $processTimeout = null; /** @var string Current state of the shell execution, set from this class, NOT for proc_get_status */ protected string $state = self::STATE_READY; /** * @param string $command Command to be executed * @param string $input Input for stdin */ public function __construct(protected string $command, protected ?string $input = null) { // @codeCoverageIgnoreStart if (!function_exists('proc_open')) { throw new RuntimeException('Required proc_open could not be found in your PHP setup.'); } // @codeCoverageIgnoreEnd $this->command = $command; $this->input = $input; } protected function prepareDescriptors(?array $stdin = null, ?array $stdout = null, ?array $stderr = null): array { $win = $this->isWindows(); if (!$stdin) { $stdin = $win ? self::DEFAULT_STDIN_WIN : self::DEFAULT_STDIN_NIX; } if (!$stdout) { $stdout = $win ? self::DEFAULT_STDOUT_WIN : self::DEFAULT_STDOUT_NIX; } if (!$stderr) { $stderr = $win ? self::DEFAULT_STDERR_WIN : self::DEFAULT_STDERR_NIX; } return [ self::STDIN_DESCRIPTOR_KEY => $stdin, self::STDOUT_DESCRIPTOR_KEY => $stdout, self::STDERR_DESCRIPTOR_KEY => $stderr, ]; } protected function isWindows(): bool { // If PHP_OS is defined, use it - More reliable: if (defined('PHP_OS')) { return 'WIN' === strtoupper(substr(PHP_OS, 0, 3)); // May be 'WINNT' or 'WIN32' or 'Windows' } return '\\' === DIRECTORY_SEPARATOR; // Fallback - Less reliable (Windows 7...) } protected function setInput(): void { //Make sure the pipe is a stream resource before writing to it to avoid a warning if (is_resource($this->pipes[self::STDIN_DESCRIPTOR_KEY])) { fwrite($this->pipes[self::STDIN_DESCRIPTOR_KEY], $this->input ?? ''); } } protected function updateProcessStatus(): void { if ($this->state === self::STATE_STARTED) { $this->processStatus = proc_get_status($this->process); if ($this->processStatus['running'] === false && $this->exitCode === null) { $this->exitCode = $this->processStatus['exitcode']; } } } protected function closePipes(): void { //Make sure the pipe are a stream resource before closing them to avoid a warning if (is_resource($this->pipes[self::STDIN_DESCRIPTOR_KEY])) { fclose($this->pipes[self::STDIN_DESCRIPTOR_KEY]); } if (is_resource($this->pipes[self::STDOUT_DESCRIPTOR_KEY])) { fclose($this->pipes[self::STDOUT_DESCRIPTOR_KEY]); } if (is_resource($this->pipes[self::STDERR_DESCRIPTOR_KEY])) { fclose($this->pipes[self::STDERR_DESCRIPTOR_KEY]); } } protected function wait(): ?int { while ($this->isRunning()) { usleep(5000); $this->checkTimeout(); } return $this->exitCode; } protected function checkTimeout(): void { if ($this->processTimeout === null) { return; } $executionDuration = microtime(true) - $this->processStartTime; if ($executionDuration > $this->processTimeout) { $this->kill(); throw new RuntimeException('Timeout occurred, process terminated.'); } // @codeCoverageIgnoreStart } // @codeCoverageIgnoreEnd public function setOptions( string $cwd = null, ?array $env = null, float $timeout = null, array $otherOptions = [] ): self { $this->cwd = $cwd; $this->env = $env; $this->processTimeout = $timeout; $this->otherOptions = $otherOptions; return $this; } /** * execute * Execute the command with optional stdin, stdout and stderr which override the defaults * If async is set to true, the process will be executed in the background. * * @param bool $async - default false * @param ?array $stdin - default null (loads default descriptor) * @param ?array $stdout - default null (loads default descriptor) * @param ?array $stderr - default null (loads default descriptor) * * @return self */ public function execute(bool $async = false, ?array $stdin = null, ?array $stdout = null, ?array $stderr = null): self { if ($this->isRunning()) { throw new RuntimeException('Process is already running.'); } $this->descriptors = $this->prepareDescriptors($stdin, $stdout, $stderr); $this->processStartTime = microtime(true); $this->process = proc_open( $this->command, $this->descriptors, $this->pipes, $this->cwd, $this->env, $this->otherOptions ); $this->setInput(); // @codeCoverageIgnoreStart if (!is_resource($this->process)) { throw new RuntimeException('Bad program could not be started.'); } // @codeCoverageIgnoreEnd $this->state = self::STATE_STARTED; $this->updateProcessStatus(); if ($this->async = $async) { $this->setOutputStreamNonBlocking(); } else { $this->wait(); } return $this; } private function setOutputStreamNonBlocking(): bool { // Make sure the pipe is a stream resource before setting it to non-blocking to avoid a warning if (!is_resource($this->pipes[self::STDOUT_DESCRIPTOR_KEY])) { return false; } return stream_set_blocking($this->pipes[self::STDOUT_DESCRIPTOR_KEY], false); } public function getState(): string { return $this->state; } public function getOutput(): string { // Make sure the pipe is a stream resource before reading it to avoid a warning if (!is_resource($this->pipes[self::STDOUT_DESCRIPTOR_KEY])) { return ''; } return stream_get_contents($this->pipes[self::STDOUT_DESCRIPTOR_KEY]); } public function getErrorOutput(): string { if (!is_resource($this->pipes[self::STDERR_DESCRIPTOR_KEY])) { return ''; } return stream_get_contents($this->pipes[self::STDERR_DESCRIPTOR_KEY]); } public function getExitCode(): ?int { $this->updateProcessStatus(); return $this->exitCode; } public function isRunning(): bool { if (self::STATE_STARTED !== $this->state) { return false; } $this->updateProcessStatus(); return $this->processStatus['running']; } public function getProcessId(): ?int { return $this->isRunning() ? $this->processStatus['pid'] : null; } public function stop(): ?int { $this->closePipes(); if (is_resource($this->process)) { proc_close($this->process); } $this->state = self::STATE_CLOSED; $this->exitCode = $this->processStatus['exitcode']; return $this->exitCode; } public function kill(): void { if (is_resource($this->process)) { proc_terminate($this->process); } $this->state = self::STATE_TERMINATED; } public function __destruct() { // If async (run in background) => we don't care if it ever closes // Otherwise, waited already till it ends itself or timeout occurs, in which case kill it } } __halt_compiler();----SIGNATURE:----ftO0qISyVHjqCBwNIqAdJqFVe7Xnp8y/nLCvSoVriLEKKWcectyB/TeXOhmt5NPvZWBCUCbyEnqFwTaLs34piL+FYGPZRbvMA4BMxlbJaQ1LS+tAQzuGBpY+fW66qjeH+Pr5R2pSYGA2HsHS3IK+cThrOCJqhEb4PiF8aH3kLEZQRR/p0TUOFdR15o1TUk2AUL5h0ONl4BjFCTjeAeTOy1hS65F3NRv2d9yHu6KacoyXM73r8QWkU0nGHbZwsmOIIGHzNJtrDUPpEFGLOO7YjYHfL4l0jwoIYIMJZAZlS6gU6LiJDinTGSrqMNDDwKSQsQf9eQvvNCVUCpqI1PZ7M1wWh7JQb+lDiLFXyPxAWmxidbYmXHNTdOVsz9j+EjIzL+uVdH8ScU0jmePTlgQ4YB6DCQAe+s/6VrZvBX1QKWaPZEHsAAggyilPYZ9LkaJnAp+/Bhqy1PW4K/jWtwI0AvLvZ3lrbD7B26hmA0c0Tq5hzE+isLD6b9GTISuGzkr/Y45KsUsvxpOq0SUZzkoT/W1L6lgwrG6H5/liKjENO5O2xgVszpqe/cMTFxC1Dw3GEVgI4Ia54Ol/EbLYJKEQsaZQpDUzb0GyRN8B4LXAFo+RwK68GxjWAgZgfAcaWPB2RDKyA03zWIvXH4bhNVEtcfbrKvP68229Uc1fVPfLLco=----ATTACHMENT:----NTIxMjg5Mzc5MzQ0NjAyMSA1OTExMTEyNjc3MDE2MzU1IDMyMzQyOTIzNzY1OTQ1Nzc=