command = $command; } /** * Set an environment variable to pass to the child process. * * @param string $key * @param string|null $value * @return Process */ public function setEnv($key, $value) { if (isset($value)) { $this->env[$key] = $value; } else { unset($this->env[$key]); } return $this; } /** * Set multiple environment variables from an associative array. * * @param array $values * @return Process */ public function setEnvRange(array $values) { foreach ($values as $k => $v) { $this->setEnv($k, $v); } return $this; } /** * Whether to raise an exception if the process returns a non-zero exit * code. * * @param bool $raiseException true to raise an exception * @return void */ public function setRaiseExceptionOnFailure($raiseException) { $this->raiseExceptionOnFailure = $raiseException; return $this; } /** * Add an output callback to receive process stdout and stderr output. * * Signature is function(Process $process, string $line, array $matches = null) * * @param callable $callback the callback to call * @param string $filter regex to use to filter lines before calling callback * @param mixed $streams * @return void */ public function addOutputCallback(callable $callback, $filter = null, $streams = Process::STREAM_STDOUT) { $this->outputCallbacks[] = [$callback, $filter, $streams]; return $this; } /** * Add a callback to be called when the process transitions into stopping. * * @param callable $callback * @return void */ public function addExitCallback(callable $callback) { $this->exitCallback[] = $callback; return $this; } public function setStartModeSync() { $this->startMode = static::START_MODE_SYNC; return $this; } public function setStartModeAsync() { $this->startMode = static::START_MODE_ASYNC; return $this; } public function setStartModeReplace() { $this->startMode = static::START_MODE_SYNC | static::START_MODE_REPLACE; return $this; } public function getStdout() { return $this->stdout; } public function getStdoutString() { return isset($this->stdout) ? implode(PHP_EOL, $this->stdout) : null; } public function getStderr() { return $this->stderr; } public function getStderrString() { return isset($this->stderr) ? implode(PHP_EOL, $this->stderr) : null; } public function getExitStatus() { return $this->exitStatus; } /** * Check whether the process is still running. This will return false * during the times it is transitioning to starting or into exiting. * * @return bool true if running */ public function isRunning() { return $this->status == static::STATUS_RUNNING; } public function fork() { if (PHP_OS == 'WINNT') { $command = sprintf( 'start /B %s', $this->getCommand() ); } else { $command = sprintf( 'nohup %s > /dev/null 2>&1 &', $this->getCommand() ); } shell_exec($command); } public function start() { $this->status = static::STATUS_STARTING; if ($this->startMode & static::START_MODE_REPLACE) { $descriptors = [ 0 => STDIN, 1 => STDOUT, 2 => STDERR ]; } else { $descriptors = [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w'] ]; } $pipes = null; if (PHP_OS == 'WINNT') { $commandWrapper = '"'; } else { $commandWrapper = ''; } $this->handle = $handle = proc_open( $commandWrapper.$this->getCommand().$commandWrapper, $descriptors, $pipes, $this->cwd, array_merge(getenv(), $this->env) ); if (!is_resource($handle)) { throw new \Exception('Failed to start process'); } if (sizeof($pipes)) { $this->stream_stdin = $pipes[0]; $this->stream_stdout = $pipes[1]; $this->stream_stderr = $pipes[2]; stream_set_blocking($pipes[0], false); stream_set_blocking($pipes[1], false); stream_set_blocking($pipes[2], false); } $this->status = static::STATUS_RUNNING; if ($this->startMode & static::START_MODE_ASYNC) { declare(ticks=1); register_tick_function([&$this, 'tick']); } elseif ($this->startMode & static::START_MODE_SYNC) { while ($this->status != static::STATUS_EXITED) { if ($this->startMode & static::START_MODE_REPLACE) { sleep(1); } else { $read_streams = [$pipes[1], $pipes[2]]; $write_streams = null; $except_streams = null; stream_select($read_streams, $write_streams, $except_streams, 1); } $this->tick(); } } return $this; } public function stop() { proc_terminate($this->handle); return $this; } protected function tick() { if (!($this->startMode & static::START_MODE_REPLACE)) { $this->tickStreams(); } $this->tickProcessStatus(); } protected function tickStreams() { // Pump streams if necessary if (!isset($this->stdout) || !is_array($this->stdout)) { $this->stdout = []; } while ($line = fgets($this->stream_stdout)) { // This array is initialized a few lines above and not set anywhere else. // @phan-suppress-next-line PhanTypeMismatchDimEmpty $this->stdout[] = trim($line); foreach ($this->outputCallbacks as $callback) { if (($callback[2] & static::STREAM_STDOUT) == 0) { continue; } if (isset($callback[1])) { if (preg_match($callback[1], $line, $matches)) { call_user_func($callback[0], $this, $line, $matches); } } else { call_user_func($callback[0], $this, $line); } } } if (!isset($this->stderr) || !is_array($this->stderr)) { $this->stderr = []; } while ($line = fgets($this->stream_stderr)) { // This array is initialized a few lines above and not set anywhere else. // @phan-suppress-next-line PhanTypeMismatchDimEmpty $this->stderr[] = trim($line); foreach ($this->outputCallbacks as $callback) { if (($callback[2] & static::STREAM_STDERR) == 0) { continue; } if (isset($callback[1])) { if (preg_match($callback[1], $line, $matches)) { call_user_func($callback[0], $this, $line, $matches); } } else { call_user_func($callback[0], $this, $line); } } } } protected function tickProcessStatus() { // Check current status $status = proc_get_status($this->handle); if (isset($status['exitcode']) && $status['exitcode'] !== -1) { $this->exitStatus = $status['exitcode']; } if ($status['running'] === false && $this->status == static::STATUS_RUNNING) { $this->stopping(); } elseif ($this->status == static::STATUS_STOPPED) { $this->status = static::STATUS_EXITED; if ($this->startMode & static::START_MODE_ASYNC) { unregister_tick_function([&$this, 'tick']); } $this->exited(); } } protected function stopping() { $this->status = static::STATUS_STOPPING; foreach ($this->exitCallback as $exitCallback) { call_user_func($exitCallback, $this); } $this->status = static::STATUS_STOPPED; } protected function exited() { if (!($this->startMode & static::START_MODE_REPLACE)) { fclose($this->stream_stdin); fclose($this->stream_stdout); fclose($this->stream_stderr); } proc_close($this->handle); if ($this->raiseExceptionOnFailure && $this->exitStatus != 0) { throw new \Exception('Command execution failed ('.$this->exitStatus.'): '.implode(' ', $this->command).PHP_EOL. 'Stdout:'.PHP_EOL. $this->getStdoutString().PHP_EOL. 'Stderr:'.PHP_EOL. $this->getStderrString().PHP_EOL); } } public function write($data) { fwrite($this->stream_stdin, $data); return $this; } public function close() { fclose($this->stream_stdin); return $this; } protected function getCommand() { $processed = []; $command = $this->command; $commandName = array_shift($command); $processed[] = strpos($commandName, ' ') === false ? $commandName : '"'.$commandName.'"'; foreach ($command as $part) { if (is_object($part) && $part instanceof CommandArgument) { $processed[] = $part->get(); } else { $processed[] = $part; } } return implode(' ', $processed); } }