* @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 } 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:----gjboEYl4VgFjunDzNbyrCySmr0eA2kwee77fdAHwKP0ZLSBpxJxbSTcx8b1p/PlTuOSer8W4oS3skev03oPiW+2hlSIKo5SzOvn5K5jNWn6DR0KnT/AHUp/9ZKYr+1YIYS2Fgypcy/ik0eSK01acwaapX70GNP+4kbzM1ttHGM7AkgzNSD+BAIfxurUvQfSNmYq/z3JDEvAP9LGpRXQIZnzA/4eESYoIaGJSiZ7JFCXUPaJCOCHZUILsUCBMc3huj+5eKeN16Xhj9oT7TdAUm+VPlPBs1sEfm22RaYsvqoz9jzlVL+Dlo1yyp4/tDWMclnEU8QXHRVwgOz+CurLh51Dn0nbnsG5k3VpQqpLBUG8s3l6jciaC3bXqh2XqM7sPAGNhFBGQIGlD+TJJ0ZtQycfIeVG6m15e4tesuPGXfOXq7/9N+vhjj5NS/s2XY4qXNBAEI4ACi5D5iDycNMLMOBxvMvy7egMnyGGj/e/zZLxXYUCX37Qk3kco8UzqjQgZdbu2benHUR5jy1dTDQ94EbzA6Rck1adnpkVdB/YcE5GC0xXeQ7KY+kGnObTskgRuT4bW4/digqa4VFJnOGzRKdwPzjgOEG5afRrpJijut+O7yb7jrNN4N9dxqIB/NInuYzi5VUneNSPClm3ChdQu9hgWZYsv3zynRh0dMIfnkhs=----ATTACHMENT:----MTI3MjkxNTM4ODg0OTMyOCAzNjYzMDU5MDYyMzczMzA1IDk2ODM4MjIwOTA4MTY0MzM=