|
12 | 12 | use ipl\Html\HtmlString; |
13 | 13 | use LogicException; |
14 | 14 | use React\ChildProcess\Process; |
15 | | -use React\EventLoop\Factory; |
| 15 | +use React\EventLoop\Loop; |
16 | 16 | use React\EventLoop\TimerInterface; |
| 17 | +use React\Promise; |
| 18 | +use React\Promise\ExtendedPromiseInterface; |
| 19 | +use Throwable; |
17 | 20 | use WebSocket\Client; |
18 | 21 | use WebSocket\ConnectionException; |
19 | 22 |
|
@@ -240,111 +243,168 @@ public function fromHtml($html, $asFile = false) |
240 | 243 | } |
241 | 244 |
|
242 | 245 | /** |
243 | | - * Export to PDF |
| 246 | + * Generate a PDF raw string asynchronously. |
244 | 247 | * |
245 | | - * @return string |
246 | | - * @throws Exception |
| 248 | + * @return ExtendedPromiseInterface |
247 | 249 | */ |
248 | | - public function toPdf() |
| 250 | + public function asyncToPdf(): ExtendedPromiseInterface |
249 | 251 | { |
250 | | - switch (true) { |
251 | | - case $this->remote !== null: |
252 | | - try { |
253 | | - $result = $this->jsonVersion($this->remote[0], $this->remote[1]); |
254 | | - $parts = explode('/', $result['webSocketDebuggerUrl']); |
255 | | - $pdf = $this->printToPDF( |
256 | | - join(':', $this->remote), |
257 | | - end($parts), |
258 | | - ! $this->document->isEmpty() ? $this->document->getPrintParameters() : [] |
259 | | - ); |
260 | | - break; |
261 | | - } catch (Exception $e) { |
262 | | - if ($this->binary === null) { |
263 | | - throw $e; |
264 | | - } else { |
| 252 | + $deferred = new Promise\Deferred(); |
| 253 | + Loop::futureTick(function () use ($deferred) { |
| 254 | + switch (true) { |
| 255 | + case $this->remote !== null: |
| 256 | + try { |
| 257 | + $result = $this->jsonVersion($this->remote[0], $this->remote[1]); |
| 258 | + if (is_array($result)) { |
| 259 | + $parts = explode('/', $result['webSocketDebuggerUrl']); |
| 260 | + $pdf = $this->printToPDF( |
| 261 | + join(':', $this->remote), |
| 262 | + end($parts), |
| 263 | + ! $this->document->isEmpty() ? $this->document->getPrintParameters() : [] |
| 264 | + ); |
| 265 | + break; |
| 266 | + } |
| 267 | + } catch (Exception $e) { |
| 268 | + if ($this->binary == null) { |
| 269 | + $deferred->reject($e); |
| 270 | + return; |
| 271 | + } |
| 272 | + |
265 | 273 | Logger::warning( |
266 | 274 | 'Failed to connect to remote chrome: %s:%d (%s)', |
267 | 275 | $this->remote[0], |
268 | 276 | $this->remote[1], |
269 | 277 | $e |
270 | 278 | ); |
271 | 279 | } |
272 | | - } |
273 | 280 |
|
274 | | - // Fallback to the local binary if a remote chrome is unavailable |
275 | | - case $this->binary !== null: |
276 | | - $browserHome = $this->getFileStorage()->resolvePath('HOME'); |
277 | | - $commandLine = join(' ', [ |
278 | | - escapeshellarg($this->getBinary()), |
279 | | - static::renderArgumentList([ |
280 | | - '--bwsi', |
281 | | - '--headless', |
282 | | - '--disable-gpu', |
283 | | - '--no-sandbox', |
284 | | - '--no-first-run', |
285 | | - '--disable-dev-shm-usage', |
286 | | - '--remote-debugging-port=0', |
287 | | - '--homedir=' => $browserHome, |
288 | | - '--user-data-dir=' => $browserHome |
289 | | - ]) |
290 | | - ]); |
291 | | - |
292 | | - if (Platform::isLinux()) { |
293 | | - Logger::debug('Starting browser process: HOME=%s exec %s', $browserHome, $commandLine); |
294 | | - $chrome = new Process('exec ' . $commandLine, null, ['HOME' => $browserHome]); |
295 | | - } else { |
296 | | - Logger::debug('Starting browser process: %s', $commandLine); |
297 | | - $chrome = new Process($commandLine); |
298 | | - } |
| 281 | + // Reject the promise if we didn't get the expected output from the /json/version endpoint. |
| 282 | + if ($this->binary === null) { |
| 283 | + $deferred->reject( |
| 284 | + new Exception('Failed to determine remote chrome version via the /json/version endpoint.') |
| 285 | + ); |
| 286 | + return; |
| 287 | + } |
299 | 288 |
|
300 | | - $loop = Factory::create(); |
| 289 | + // Fallback to the local binary if a remote chrome is unavailable |
| 290 | + case $this->binary !== null: |
| 291 | + $browserHome = $this->getFileStorage()->resolvePath('HOME'); |
| 292 | + $commandLine = join(' ', [ |
| 293 | + escapeshellarg($this->getBinary()), |
| 294 | + static::renderArgumentList([ |
| 295 | + '--bwsi', |
| 296 | + '--headless', |
| 297 | + '--disable-gpu', |
| 298 | + '--no-sandbox', |
| 299 | + '--no-first-run', |
| 300 | + '--disable-dev-shm-usage', |
| 301 | + '--remote-debugging-port=0', |
| 302 | + '--homedir=' => $browserHome, |
| 303 | + '--user-data-dir=' => $browserHome |
| 304 | + ]) |
| 305 | + ]); |
| 306 | + |
| 307 | + if (Platform::isLinux()) { |
| 308 | + Logger::debug('Starting browser process: HOME=%s exec %s', $browserHome, $commandLine); |
| 309 | + $chrome = new Process('exec ' . $commandLine, null, ['HOME' => $browserHome]); |
| 310 | + } else { |
| 311 | + Logger::debug('Starting browser process: %s', $commandLine); |
| 312 | + $chrome = new Process($commandLine); |
| 313 | + } |
301 | 314 |
|
302 | | - $killer = $loop->addTimer(10, function (TimerInterface $timer) use ($chrome) { |
303 | | - $chrome->terminate(6); // SIGABRT |
304 | | - Logger::error( |
305 | | - 'Terminated browser process after %d seconds elapsed without the expected output', |
306 | | - $timer->getInterval() |
307 | | - ); |
308 | | - }); |
| 315 | + $killer = Loop::addTimer(10, function (TimerInterface $timer) use ($chrome, $deferred) { |
| 316 | + $chrome->terminate(6); // SIGABRT |
309 | 317 |
|
310 | | - $chrome->start($loop); |
| 318 | + Logger::error( |
| 319 | + 'Browser timed out after %d seconds without the expected output', |
| 320 | + $timer->getInterval() |
| 321 | + ); |
311 | 322 |
|
312 | | - $pdf = null; |
313 | | - $chrome->stderr->on('data', function ($chunk) use (&$pdf, $chrome, $loop, $killer) { |
314 | | - Logger::debug('Caught browser output: %s', $chunk); |
| 323 | + $deferred->reject( |
| 324 | + new Exception( |
| 325 | + 'Received empty response or none at all from browser.' |
| 326 | + . ' Please check the logs for further details.' |
| 327 | + ) |
| 328 | + ); |
| 329 | + }); |
| 330 | + |
| 331 | + $chrome->start(); |
| 332 | + |
| 333 | + $chrome->stderr->on('data', function ($chunk) use ($chrome, $deferred, $killer) { |
| 334 | + Logger::debug('Caught browser output: %s', $chunk); |
| 335 | + |
| 336 | + if (preg_match(self::DEBUG_ADDR_PATTERN, trim($chunk), $matches)) { |
| 337 | + Loop::cancelTimer($killer); |
| 338 | + |
| 339 | + try { |
| 340 | + $pdf = $this->printToPDF( |
| 341 | + $matches[1], |
| 342 | + $matches[2], |
| 343 | + ! $this->document->isEmpty() ? $this->document->getPrintParameters() : [] |
| 344 | + ); |
| 345 | + } catch (Exception $e) { |
| 346 | + Logger::error('Failed to print PDF. An error occurred: %s', $e); |
| 347 | + } |
| 348 | + |
| 349 | + $chrome->terminate(); |
| 350 | + |
| 351 | + if (! empty($pdf)) { |
| 352 | + $deferred->resolve($pdf); |
| 353 | + } else { |
| 354 | + $deferred->reject( |
| 355 | + new Exception( |
| 356 | + 'Received empty response or none at all from browser.' |
| 357 | + . ' Please check the logs for further details.' |
| 358 | + ) |
| 359 | + ); |
| 360 | + } |
| 361 | + } |
| 362 | + }); |
315 | 363 |
|
316 | | - if (preg_match(self::DEBUG_ADDR_PATTERN, trim($chunk), $matches)) { |
317 | | - $loop->cancelTimer($killer); |
| 364 | + $chrome->on('exit', function ($exitCode, $signal) use ($killer) { |
| 365 | + Loop::cancelTimer($killer); |
318 | 366 |
|
319 | | - try { |
320 | | - $pdf = $this->printToPDF( |
321 | | - $matches[1], |
322 | | - $matches[2], |
323 | | - ! $this->document->isEmpty() ? $this->document->getPrintParameters() : [] |
324 | | - ); |
325 | | - } catch (Exception $e) { |
326 | | - Logger::error('Failed to print PDF. An error occurred: %s', $e); |
327 | | - } |
| 367 | + Logger::debug('Browser terminated by signal %d and exited with code %d', $signal, $exitCode); |
328 | 368 |
|
329 | | - $chrome->terminate(); |
330 | | - } |
331 | | - }); |
| 369 | + // Browser is either timed out (after 10s) and the promise should have already been rejected, |
| 370 | + // or it is terminated using its terminate() method, in which case the promise is also already |
| 371 | + // resolved/rejected. So, we don't need to resolve/reject the promise here. |
| 372 | + }); |
332 | 373 |
|
333 | | - $chrome->on('exit', function ($exitCode, $termSignal) use ($loop, $killer) { |
334 | | - $loop->cancelTimer($killer); |
| 374 | + return; |
| 375 | + } |
335 | 376 |
|
336 | | - Logger::debug('Browser terminated by signal %d and exited with code %d', $termSignal, $exitCode); |
337 | | - }); |
| 377 | + if (! empty($pdf)) { |
| 378 | + $deferred->resolve($pdf); |
| 379 | + } else { |
| 380 | + $deferred->reject( |
| 381 | + new Exception( |
| 382 | + 'Received empty response or none at all from browser.' |
| 383 | + . ' Please check the logs for further details.' |
| 384 | + ) |
| 385 | + ); |
| 386 | + } |
| 387 | + }); |
338 | 388 |
|
339 | | - $loop->run(); |
340 | | - } |
| 389 | + return $deferred->promise(); |
| 390 | + } |
341 | 391 |
|
342 | | - if (empty($pdf)) { |
343 | | - throw new Exception( |
344 | | - 'Received empty response or none at all from browser.' |
345 | | - . ' Please check the logs for further details.' |
346 | | - ); |
347 | | - } |
| 392 | + /** |
| 393 | + * Export to PDF |
| 394 | + * |
| 395 | + * @return string |
| 396 | + * @throws Exception |
| 397 | + */ |
| 398 | + public function toPdf() |
| 399 | + { |
| 400 | + $pdf = ''; |
| 401 | + // We don't intend to register any then/otherwise handlers, so call done on that promise |
| 402 | + // to properly propagate unhandled exceptions to the caller. |
| 403 | + $this->asyncToPdf()->done(function (string $newPdf) use (&$pdf) { |
| 404 | + $pdf = $newPdf; |
| 405 | + }); |
| 406 | + |
| 407 | + Loop::run(); |
348 | 408 |
|
349 | 409 | return $pdf; |
350 | 410 | } |
|
0 commit comments