1<?php 2namespace GuzzleHttp\Handler; 3 4use GuzzleHttp\Promise as P; 5use GuzzleHttp\Promise\Promise; 6use GuzzleHttp\Utils; 7use Psr\Http\Message\RequestInterface; 8 9/** 10 * Returns an asynchronous response using curl_multi_* functions. 11 * 12 * When using the CurlMultiHandler, custom curl options can be specified as an 13 * associative array of curl option constants mapping to values in the 14 * **curl** key of the provided request options. 15 * 16 * @property resource $_mh Internal use only. Lazy loaded multi-handle. 17 */ 18class CurlMultiHandler 19{ 20 /** @var CurlFactoryInterface */ 21 private $factory; 22 private $selectTimeout; 23 private $active; 24 private $handles = []; 25 private $delays = []; 26 private $options = []; 27 28 /** 29 * This handler accepts the following options: 30 * 31 * - handle_factory: An optional factory used to create curl handles 32 * - select_timeout: Optional timeout (in seconds) to block before timing 33 * out while selecting curl handles. Defaults to 1 second. 34 * - options: An associative array of CURLMOPT_* options and 35 * corresponding values for curl_multi_setopt() 36 * 37 * @param array $options 38 */ 39 public function __construct(array $options = []) 40 { 41 $this->factory = isset($options['handle_factory']) 42 ? $options['handle_factory'] : new CurlFactory(50); 43 44 if (isset($options['select_timeout'])) { 45 $this->selectTimeout = $options['select_timeout']; 46 } elseif ($selectTimeout = getenv('GUZZLE_CURL_SELECT_TIMEOUT')) { 47 $this->selectTimeout = $selectTimeout; 48 } else { 49 $this->selectTimeout = 1; 50 } 51 52 $this->options = isset($options['options']) ? $options['options'] : []; 53 } 54 55 public function __get($name) 56 { 57 if ($name === '_mh') { 58 $this->_mh = curl_multi_init(); 59 60 foreach ($this->options as $option => $value) { 61 // A warning is raised in case of a wrong option. 62 curl_multi_setopt($this->_mh, $option, $value); 63 } 64 65 // Further calls to _mh will return the value directly, without entering the 66 // __get() method at all. 67 return $this->_mh; 68 } 69 70 throw new \BadMethodCallException(); 71 } 72 73 public function __destruct() 74 { 75 if (isset($this->_mh)) { 76 curl_multi_close($this->_mh); 77 unset($this->_mh); 78 } 79 } 80 81 public function __invoke(RequestInterface $request, array $options) 82 { 83 $easy = $this->factory->create($request, $options); 84 $id = (int) $easy->handle; 85 86 $promise = new Promise( 87 [$this, 'execute'], 88 function () use ($id) { 89 return $this->cancel($id); 90 } 91 ); 92 93 $this->addRequest(['easy' => $easy, 'deferred' => $promise]); 94 95 return $promise; 96 } 97 98 /** 99 * Ticks the curl event loop. 100 */ 101 public function tick() 102 { 103 // Add any delayed handles if needed. 104 if ($this->delays) { 105 $currentTime = Utils::currentTime(); 106 foreach ($this->delays as $id => $delay) { 107 if ($currentTime >= $delay) { 108 unset($this->delays[$id]); 109 curl_multi_add_handle( 110 $this->_mh, 111 $this->handles[$id]['easy']->handle 112 ); 113 } 114 } 115 } 116 117 // Step through the task queue which may add additional requests. 118 P\queue()->run(); 119 120 if ($this->active && 121 curl_multi_select($this->_mh, $this->selectTimeout) === -1 122 ) { 123 // Perform a usleep if a select returns -1. 124 // See: https://bugs.php.net/bug.php?id=61141 125 usleep(250); 126 } 127 128 while (curl_multi_exec($this->_mh, $this->active) === CURLM_CALL_MULTI_PERFORM); 129 130 $this->processMessages(); 131 } 132 133 /** 134 * Runs until all outstanding connections have completed. 135 */ 136 public function execute() 137 { 138 $queue = P\queue(); 139 140 while ($this->handles || !$queue->isEmpty()) { 141 // If there are no transfers, then sleep for the next delay 142 if (!$this->active && $this->delays) { 143 usleep($this->timeToNext()); 144 } 145 $this->tick(); 146 } 147 } 148 149 private function addRequest(array $entry) 150 { 151 $easy = $entry['easy']; 152 $id = (int) $easy->handle; 153 $this->handles[$id] = $entry; 154 if (empty($easy->options['delay'])) { 155 curl_multi_add_handle($this->_mh, $easy->handle); 156 } else { 157 $this->delays[$id] = Utils::currentTime() + ($easy->options['delay'] / 1000); 158 } 159 } 160 161 /** 162 * Cancels a handle from sending and removes references to it. 163 * 164 * @param int $id Handle ID to cancel and remove. 165 * 166 * @return bool True on success, false on failure. 167 */ 168 private function cancel($id) 169 { 170 // Cannot cancel if it has been processed. 171 if (!isset($this->handles[$id])) { 172 return false; 173 } 174 175 $handle = $this->handles[$id]['easy']->handle; 176 unset($this->delays[$id], $this->handles[$id]); 177 curl_multi_remove_handle($this->_mh, $handle); 178 curl_close($handle); 179 180 return true; 181 } 182 183 private function processMessages() 184 { 185 while ($done = curl_multi_info_read($this->_mh)) { 186 $id = (int) $done['handle']; 187 curl_multi_remove_handle($this->_mh, $done['handle']); 188 189 if (!isset($this->handles[$id])) { 190 // Probably was cancelled. 191 continue; 192 } 193 194 $entry = $this->handles[$id]; 195 unset($this->handles[$id], $this->delays[$id]); 196 $entry['easy']->errno = $done['result']; 197 $entry['deferred']->resolve( 198 CurlFactory::finish( 199 $this, 200 $entry['easy'], 201 $this->factory 202 ) 203 ); 204 } 205 } 206 207 private function timeToNext() 208 { 209 $currentTime = Utils::currentTime(); 210 $nextTime = PHP_INT_MAX; 211 foreach ($this->delays as $time) { 212 if ($time < $nextTime) { 213 $nextTime = $time; 214 } 215 } 216 217 return max(0, $nextTime - $currentTime) * 1000000; 218 } 219} 220