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