* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Monolog\Util; use Symfony\Component\Process\Process; class LocalSocket { const TCP = 'tcp'; const UDP = 'udp'; private static $sockets = []; private static $shutdownHandler = false; public static function initSocket(int $port = 51984, string $proto = LocalSocket::TCP) { if (!isset(self::$sockets[$proto][$port])) { $file = self::initFile($port, $proto); $process = new Process(escapeshellarg(PHP_BINARY).' '.escapeshellarg($file)); $process->start(function ($type, $out) use ($proto, $port) { if ($type === 'err') { if (substr($out, 0, 4) === 'INIT') { if ($proto === LocalSocket::UDP) { self::$sockets[$proto][$port]['comms'] = null; } else { $sock = socket_create(AF_INET, SOCK_STREAM, getprotobyname($proto)); socket_connect($sock, '127.0.0.1', $port); socket_write($sock, "MONITOR\n"); self::$sockets[$proto][$port]['comms'] = $sock; } } } }); self::$sockets[$proto][$port] = [ 'file' => $file, 'process' => $process, 'busy' => false, ]; // make sure the socket is listening while (true) { if ($process->getErrorOutput() === 'INIT') { break; } usleep(100); } if (!self::$shutdownHandler) { register_shutdown_function(function () { LocalSocket::shutdownSockets(); }); self::$shutdownHandler = true; } } $sock = self::$sockets[$proto][$port]; if (!$sock['process']->isRunning()) { throw new \RuntimeException( 'LocalSocket '.$proto.'://127.0.0.1:'.$port.' appears to have died unexpectedly: ' . "\n\n" . $sock['process']->getOutput() ); } self::clearSocket($port, $proto); return new class($sock['process'], $sock['comms']) { public function __construct(Process $proc, $comms) { $this->process = $proc; $this->comms = $comms; } public function getOutput() { // read out until getting a !DONE! ack and then tell the socket to terminate the connection if ($this->comms) { $out = ''; socket_write($this->comms, "DONE?\n"); while ($data = socket_read($this->comms, 2048)) { $out .= $data; if (substr($out, -6) === '!DONE!') { $out = substr($out, 0, -6); break; } } $out = preg_replace('{.*!BEGIN!}', '', $out); socket_write($this->comms, "TERMINATE\n"); return $out; } // wait 3 seconds max for output for UDP $retries = 3000; while (!$this->process->getOutput() && $retries-- && $this->process->getStatus()) { usleep(100); } return $this->process->getOutput(); } }; } private static function clearSocket(int $port = 51984, string $proto = LocalSocket::TCP) { if (isset(self::$sockets[$proto][$port])) { self::$sockets[$proto][$port]['process']->clearOutput(); } } public static function shutdownSocket(int $port = 51984, string $proto = LocalSocket::TCP) { if (!isset(self::$sockets[$proto][$port])) { return; } if (is_resource(self::$sockets[$proto][$port]['comms'])) { socket_write(self::$sockets[$proto][$port]['comms'], "EXIT\n"); socket_close(self::$sockets[$proto][$port]['comms']); } $sock = self::$sockets[$proto][$port]; $sock['process']->stop(); @unlink($sock['file']); unset(self::$sockets[$proto][$port]); } public static function shutdownSockets() { foreach (self::$sockets as $proto => $ports) { foreach ($ports as $port => $sock) { self::shutdownSocket($port, $proto); } } } private static function initFile(int $port, string $proto): string { $tmpFile = sys_get_temp_dir().'/monolog-test-'.$proto.'-socket-'.$port.'.php'; if ($proto === self::UDP) { file_put_contents($tmpFile, <<