ProcessPipes.php 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  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 Symfony\Component\Process;
  11. use Symfony\Component\Process\Exception\RuntimeException;
  12. /**
  13. * ProcessPipes manages descriptors and pipes for the use of proc_open.
  14. */
  15. class ProcessPipes
  16. {
  17. /** @var array */
  18. public $pipes = array();
  19. /** @var array */
  20. private $fileHandles = array();
  21. /** @var array */
  22. private $readBytes = array();
  23. /** @var Boolean */
  24. private $useFiles;
  25. public function __construct($useFiles = false)
  26. {
  27. $this->useFiles = (Boolean) $useFiles;
  28. // Fix for PHP bug #51800: reading from STDOUT pipe hangs forever on Windows if the output is too big.
  29. // Workaround for this problem is to use temporary files instead of pipes on Windows platform.
  30. //
  31. // Please note that this work around prevents hanging but
  32. // another issue occurs : In some race conditions, some data may be
  33. // lost or corrupted.
  34. //
  35. // @see https://bugs.php.net/bug.php?id=51800
  36. if ($this->useFiles) {
  37. $this->fileHandles = array(
  38. Process::STDOUT => tmpfile(),
  39. );
  40. if (false === $this->fileHandles[Process::STDOUT]) {
  41. throw new RuntimeException('A temporary file could not be opened to write the process output to, verify that your TEMP environment variable is writable');
  42. }
  43. $this->readBytes = array(
  44. Process::STDOUT => 0,
  45. );
  46. }
  47. }
  48. public function __destruct()
  49. {
  50. $this->close();
  51. }
  52. /**
  53. * Sets non-blocking mode on pipes.
  54. */
  55. public function unblock()
  56. {
  57. foreach ($this->pipes as $pipe) {
  58. stream_set_blocking($pipe, 0);
  59. }
  60. }
  61. /**
  62. * Closes file handles and pipes.
  63. */
  64. public function close()
  65. {
  66. $this->closeUnixPipes();
  67. foreach ($this->fileHandles as $offset => $handle) {
  68. fclose($handle);
  69. }
  70. $this->fileHandles = array();
  71. }
  72. /**
  73. * Closes unix pipes.
  74. *
  75. * Nothing happens in case file handles are used.
  76. */
  77. public function closeUnixPipes()
  78. {
  79. foreach ($this->pipes as $pipe) {
  80. fclose($pipe);
  81. }
  82. $this->pipes = array();
  83. }
  84. /**
  85. * Returns an array of descriptors for the use of proc_open.
  86. *
  87. * @return array
  88. */
  89. public function getDescriptors()
  90. {
  91. if ($this->useFiles) {
  92. return array(
  93. array('pipe', 'r'),
  94. $this->fileHandles[Process::STDOUT],
  95. // Use a file handle only for STDOUT. Using for both STDOUT and STDERR would trigger https://bugs.php.net/bug.php?id=65650
  96. array('pipe', 'w'),
  97. );
  98. }
  99. return array(
  100. array('pipe', 'r'), // stdin
  101. array('pipe', 'w'), // stdout
  102. array('pipe', 'w'), // stderr
  103. );
  104. }
  105. /**
  106. * Reads data in file handles and pipes.
  107. *
  108. * @param Boolean $blocking Whether to use blocking calls or not.
  109. *
  110. * @return array An array of read data indexed by their fd.
  111. */
  112. public function read($blocking)
  113. {
  114. return array_replace($this->readStreams($blocking), $this->readFileHandles());
  115. }
  116. /**
  117. * Reads data in file handles and pipes, closes them if EOF is reached.
  118. *
  119. * @param Boolean $blocking Whether to use blocking calls or not.
  120. *
  121. * @return array An array of read data indexed by their fd.
  122. */
  123. public function readAndCloseHandles($blocking)
  124. {
  125. return array_replace($this->readStreams($blocking, true), $this->readFileHandles(true));
  126. }
  127. /**
  128. * Returns if the current state has open file handles or pipes.
  129. *
  130. * @return Boolean
  131. */
  132. public function hasOpenHandles()
  133. {
  134. if (!$this->useFiles) {
  135. return (Boolean) $this->pipes;
  136. }
  137. return (Boolean) $this->pipes && (Boolean) $this->fileHandles;
  138. }
  139. /**
  140. * Writes stdin data.
  141. *
  142. * @param Boolean $blocking Whether to use blocking calls or not.
  143. * @param string|null $stdin The data to write.
  144. */
  145. public function write($blocking, $stdin)
  146. {
  147. if (null === $stdin) {
  148. fclose($this->pipes[0]);
  149. unset($this->pipes[0]);
  150. return;
  151. }
  152. $writePipes = array($this->pipes[0]);
  153. unset($this->pipes[0]);
  154. $stdinLen = strlen($stdin);
  155. $stdinOffset = 0;
  156. while ($writePipes) {
  157. $r = null;
  158. $w = $writePipes;
  159. $e = null;
  160. if (false === $n = @stream_select($r, $w, $e, 0, $blocking ? ceil(Process::TIMEOUT_PRECISION * 1E6) : 0)) {
  161. // if a system call has been interrupted, forget about it, let's try again
  162. if ($this->hasSystemCallBeenInterrupted()) {
  163. continue;
  164. }
  165. break;
  166. }
  167. // nothing has changed, let's wait until the process is ready
  168. if (0 === $n) {
  169. continue;
  170. }
  171. if ($w) {
  172. $written = fwrite($writePipes[0], (binary) substr($stdin, $stdinOffset), 8192);
  173. if (false !== $written) {
  174. $stdinOffset += $written;
  175. }
  176. if ($stdinOffset >= $stdinLen) {
  177. fclose($writePipes[0]);
  178. $writePipes = null;
  179. }
  180. }
  181. }
  182. }
  183. /**
  184. * Reads data in file handles.
  185. *
  186. * @return array An array of read data indexed by their fd.
  187. */
  188. private function readFileHandles($close = false)
  189. {
  190. $read = array();
  191. $fh = $this->fileHandles;
  192. foreach ($fh as $type => $fileHandle) {
  193. if (0 !== fseek($fileHandle, $this->readBytes[$type])) {
  194. continue;
  195. }
  196. $data = '';
  197. $dataread = null;
  198. while (!feof($fileHandle)) {
  199. if (false !== $dataread = fread($fileHandle, 16392)) {
  200. $data .= $dataread;
  201. }
  202. }
  203. if (0 < $length = strlen($data)) {
  204. $this->readBytes[$type] += $length;
  205. $read[$type] = $data;
  206. }
  207. if (false === $dataread || (true === $close && feof($fileHandle) && '' === $data)) {
  208. fclose($this->fileHandles[$type]);
  209. unset($this->fileHandles[$type]);
  210. }
  211. }
  212. return $read;
  213. }
  214. /**
  215. * Reads data in file pipes streams.
  216. *
  217. * @param Boolean $blocking Whether to use blocking calls or not.
  218. *
  219. * @return array An array of read data indexed by their fd.
  220. */
  221. private function readStreams($blocking, $close = false)
  222. {
  223. if (empty($this->pipes)) {
  224. return array();
  225. }
  226. $read = array();
  227. $r = $this->pipes;
  228. $w = null;
  229. $e = null;
  230. // let's have a look if something changed in streams
  231. if (false === $n = @stream_select($r, $w, $e, 0, $blocking ? ceil(Process::TIMEOUT_PRECISION * 1E6) : 0)) {
  232. // if a system call has been interrupted, forget about it, let's try again
  233. // otherwise, an error occurred, let's reset pipes
  234. if (!$this->hasSystemCallBeenInterrupted()) {
  235. $this->pipes = array();
  236. }
  237. return $read;
  238. }
  239. // nothing has changed
  240. if (0 === $n) {
  241. return $read;
  242. }
  243. foreach ($r as $pipe) {
  244. $type = array_search($pipe, $this->pipes);
  245. $data = fread($pipe, 8192);
  246. if (strlen($data) > 0) {
  247. $read[$type] = $data;
  248. }
  249. if (false === $data || (true === $close && feof($pipe) && '' === $data)) {
  250. fclose($this->pipes[$type]);
  251. unset($this->pipes[$type]);
  252. }
  253. }
  254. return $read;
  255. }
  256. /**
  257. * Returns true if a system call has been interrupted.
  258. *
  259. * @return Boolean
  260. */
  261. private function hasSystemCallBeenInterrupted()
  262. {
  263. $lastError = error_get_last();
  264. // stream_select returns false when the `select` system call is interrupted by an incoming signal
  265. return isset($lastError['message']) && false !== stripos($lastError['message'], 'interrupted system call');
  266. }
  267. }