You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

115 lines
3.2 KiB

2 years ago
  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\VarDumper\Server;
  11. use Psr\Log\LoggerInterface;
  12. use Symfony\Component\VarDumper\Cloner\Data;
  13. use Symfony\Component\VarDumper\Cloner\Stub;
  14. /**
  15. * A server collecting Data clones sent by a ServerDumper.
  16. *
  17. * @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
  18. *
  19. * @final
  20. */
  21. class DumpServer
  22. {
  23. private $host;
  24. private $logger;
  25. /**
  26. * @var resource|null
  27. */
  28. private $socket;
  29. public function __construct(string $host, LoggerInterface $logger = null)
  30. {
  31. if (!str_contains($host, '://')) {
  32. $host = 'tcp://'.$host;
  33. }
  34. $this->host = $host;
  35. $this->logger = $logger;
  36. }
  37. public function start(): void
  38. {
  39. if (!$this->socket = stream_socket_server($this->host, $errno, $errstr)) {
  40. throw new \RuntimeException(sprintf('Server start failed on "%s": ', $this->host).$errstr.' '.$errno);
  41. }
  42. }
  43. public function listen(callable $callback): void
  44. {
  45. if (null === $this->socket) {
  46. $this->start();
  47. }
  48. foreach ($this->getMessages() as $clientId => $message) {
  49. if ($this->logger) {
  50. $this->logger->info('Received a payload from client {clientId}', ['clientId' => $clientId]);
  51. }
  52. $payload = @unserialize(base64_decode($message), ['allowed_classes' => [Data::class, Stub::class]]);
  53. // Impossible to decode the message, give up.
  54. if (false === $payload) {
  55. if ($this->logger) {
  56. $this->logger->warning('Unable to decode a message from {clientId} client.', ['clientId' => $clientId]);
  57. }
  58. continue;
  59. }
  60. if (!\is_array($payload) || \count($payload) < 2 || !$payload[0] instanceof Data || !\is_array($payload[1])) {
  61. if ($this->logger) {
  62. $this->logger->warning('Invalid payload from {clientId} client. Expected an array of two elements (Data $data, array $context)', ['clientId' => $clientId]);
  63. }
  64. continue;
  65. }
  66. [$data, $context] = $payload;
  67. $callback($data, $context, $clientId);
  68. }
  69. }
  70. public function getHost(): string
  71. {
  72. return $this->host;
  73. }
  74. private function getMessages(): iterable
  75. {
  76. $sockets = [(int) $this->socket => $this->socket];
  77. $write = [];
  78. while (true) {
  79. $read = $sockets;
  80. stream_select($read, $write, $write, null);
  81. foreach ($read as $stream) {
  82. if ($this->socket === $stream) {
  83. $stream = stream_socket_accept($this->socket);
  84. $sockets[(int) $stream] = $stream;
  85. } elseif (feof($stream)) {
  86. unset($sockets[(int) $stream]);
  87. fclose($stream);
  88. } else {
  89. yield (int) $stream => fgets($stream);
  90. }
  91. }
  92. }
  93. }
  94. }