ProcessPipes.php 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  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. Process::STDERR => tmpfile(),
  40. );
  41. if (false === $this->fileHandles[Process::STDOUT]) {
  42. throw new RuntimeException('A temporary file could not be opened to write the process output to, verify that your TEMP environment variable is writable');
  43. }
  44. if (false === $this->fileHandles[Process::STDERR]) {
  45. throw new RuntimeException('A temporary file could not be opened to write the process output to, verify that your TEMP environment variable is writable');
  46. }
  47. $this->readBytes = array(
  48. Process::STDOUT => 0,
  49. Process::STDERR => 0,
  50. );
  51. }
  52. }
  53. public function __destruct()
  54. {
  55. $this->close();
  56. }
  57. /**
  58. * Sets non-blocking mode on pipes.
  59. */
  60. public function unblock()
  61. {
  62. foreach ($this->pipes as $pipe) {
  63. stream_set_blocking($pipe, 0);
  64. }
  65. }
  66. /**
  67. * Closes file handles and pipes.
  68. */
  69. public function close()
  70. {
  71. $this->closeUnixPipes();
  72. foreach ($this->fileHandles as $offset => $handle) {
  73. fclose($handle);
  74. }
  75. $this->fileHandles = array();
  76. }
  77. /**
  78. * Closes unix pipes.
  79. *
  80. * Nothing happens in case file handles are used.
  81. */
  82. public function closeUnixPipes()
  83. {
  84. foreach ($this->pipes as $pipe) {
  85. fclose($pipe);
  86. }
  87. $this->pipes = array();
  88. }
  89. /**
  90. * Returns an array of descriptors for the use of proc_open.
  91. *
  92. * @return array
  93. */
  94. public function getDescriptors()
  95. {
  96. if ($this->useFiles) {
  97. return array(
  98. array('pipe', 'r'),
  99. $this->fileHandles[Process::STDOUT],
  100. $this->fileHandles[Process::STDERR],
  101. );
  102. }
  103. return array(
  104. array('pipe', 'r'), // stdin
  105. array('pipe', 'w'), // stdout
  106. array('pipe', 'w'), // stderr
  107. );
  108. }
  109. /**
  110. * Reads data in file handles and pipes.
  111. *
  112. * @param Boolean $blocking Whether to use blocking calls or not.
  113. *
  114. * @return array An array of read data indexed by their fd.
  115. */
  116. public function read($blocking)
  117. {
  118. return array_replace($this->readStreams($blocking), $this->readFileHandles());
  119. }
  120. /**
  121. * Reads data in file handles and pipes, closes them if EOF is reached.
  122. *
  123. * @param Boolean $blocking Whether to use blocking calls or not.
  124. *
  125. * @return array An array of read data indexed by their fd.
  126. */
  127. public function readAndCloseHandles($blocking)
  128. {
  129. return array_replace($this->readStreams($blocking, true), $this->readFileHandles(true));
  130. }
  131. /**
  132. * Returns if the current state has open file handles or pipes.
  133. *
  134. * @return Boolean
  135. */
  136. public function hasOpenHandles()
  137. {
  138. if ($this->useFiles) {
  139. return (Boolean) $this->fileHandles;
  140. }
  141. return (Boolean) $this->pipes;
  142. }
  143. /**
  144. * Writes stdin data.
  145. *
  146. * @param Boolean $blocking Whether to use blocking calls or not.
  147. * @param string $stdin The data to write.
  148. */
  149. public function write($blocking, $stdin)
  150. {
  151. if (null === $stdin) {
  152. fclose($this->pipes[0]);
  153. unset($this->pipes[0]);
  154. return;
  155. }
  156. $writePipes = array($this->pipes[0]);
  157. unset($this->pipes[0]);
  158. $stdinLen = strlen($stdin);
  159. $stdinOffset = 0;
  160. while ($writePipes) {
  161. $r = null;
  162. $w = $writePipes;
  163. $e = null;
  164. if (false === $n = @stream_select($r, $w, $e, 0, $blocking ? ceil(Process::TIMEOUT_PRECISION * 1E6) : 0)) {
  165. // if a system call has been interrupted, forget about it, let's try again
  166. if ($this->hasSystemCallBeenInterrupted()) {
  167. continue;
  168. }
  169. break;
  170. }
  171. // nothing has changed, let's wait until the process is ready
  172. if (0 === $n) {
  173. continue;
  174. }
  175. if ($w) {
  176. $written = fwrite($writePipes[0], (binary) substr($stdin, $stdinOffset), 8192);
  177. if (false !== $written) {
  178. $stdinOffset += $written;
  179. }
  180. if ($stdinOffset >= $stdinLen) {
  181. fclose($writePipes[0]);
  182. $writePipes = null;
  183. }
  184. }
  185. }
  186. }
  187. /**
  188. * Reads data in file handles.
  189. *
  190. * @return array An array of read data indexed by their fd.
  191. */
  192. private function readFileHandles($close = false)
  193. {
  194. $read = array();
  195. $fh = $this->fileHandles;
  196. foreach ($fh as $type => $fileHandle) {
  197. if (0 !== fseek($fileHandle, $this->readBytes[$type])) {
  198. continue;
  199. }
  200. $data = '';
  201. while (!feof($fileHandle)) {
  202. if (false !== $dataread = fread($fileHandle, 16392)) {
  203. $data .= $dataread;
  204. }
  205. }
  206. if (0 < $length = strlen($data)) {
  207. $this->readBytes[$type] += $length;
  208. $read[$type] = $data;
  209. }
  210. if (true === $close && feof($fileHandle)) {
  211. fclose($this->fileHandles[$type]);
  212. unset($this->fileHandles[$type]);
  213. }
  214. }
  215. return $read;
  216. }
  217. /**
  218. * Reads data in file pipes streams.
  219. *
  220. * @param Boolean $blocking Whether to use blocking calls or not.
  221. *
  222. * @return array An array of read data indexed by their fd.
  223. */
  224. private function readStreams($blocking, $close = false)
  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 occured, 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 (true === $close && feof($pipe)) {
  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. }