ProcOutputPager.php 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. <?php
  2. /*
  3. * This file is part of Psy Shell.
  4. *
  5. * (c) 2012-2023 Justin Hileman
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Psy\Output;
  11. use Symfony\Component\Console\Output\StreamOutput;
  12. /**
  13. * ProcOutputPager class.
  14. *
  15. * A ProcOutputPager instance wraps a regular StreamOutput's stream. Rather
  16. * than writing directly to the stream, it shells out to a pager process and
  17. * gives that process the stream as stdout. This means regular *nix commands
  18. * like `less` and `more` can be used to page large amounts of output.
  19. */
  20. class ProcOutputPager extends StreamOutput implements OutputPager
  21. {
  22. private $proc;
  23. private $pipe;
  24. private $stream;
  25. private $cmd;
  26. /**
  27. * Constructor.
  28. *
  29. * @param StreamOutput $output
  30. * @param string $cmd Pager process command (default: 'less -R -F -X')
  31. */
  32. public function __construct(StreamOutput $output, string $cmd = 'less -R -F -X')
  33. {
  34. $this->stream = $output->getStream();
  35. $this->cmd = $cmd;
  36. }
  37. /**
  38. * Writes a message to the output.
  39. *
  40. * @param string $message A message to write to the output
  41. * @param bool $newline Whether to add a newline or not
  42. *
  43. * @throws \RuntimeException When unable to write output (should never happen)
  44. */
  45. public function doWrite($message, $newline)
  46. {
  47. $pipe = $this->getPipe();
  48. if (false === @\fwrite($pipe, $message.($newline ? \PHP_EOL : ''))) {
  49. // @codeCoverageIgnoreStart
  50. // should never happen
  51. $this->close();
  52. throw new \RuntimeException('Unable to write output');
  53. // @codeCoverageIgnoreEnd
  54. }
  55. \fflush($pipe);
  56. }
  57. /**
  58. * Close the current pager process.
  59. */
  60. public function close()
  61. {
  62. if (isset($this->pipe)) {
  63. \fclose($this->pipe);
  64. }
  65. if (isset($this->proc)) {
  66. $exit = \proc_close($this->proc);
  67. if ($exit !== 0) {
  68. throw new \RuntimeException('Error closing output stream');
  69. }
  70. }
  71. $this->pipe = null;
  72. $this->proc = null;
  73. }
  74. /**
  75. * Get a pipe for paging output.
  76. *
  77. * If no active pager process exists, fork one and return its input pipe.
  78. */
  79. private function getPipe()
  80. {
  81. if (!isset($this->pipe) || !isset($this->proc)) {
  82. $desc = [['pipe', 'r'], $this->stream, \fopen('php://stderr', 'w')];
  83. $this->proc = \proc_open($this->cmd, $desc, $pipes);
  84. if (!\is_resource($this->proc)) {
  85. throw new \RuntimeException('Error opening output stream');
  86. }
  87. $this->pipe = $pipes[0];
  88. }
  89. return $this->pipe;
  90. }
  91. }