/** * @coroutine * * @param string $connectionString * @param float|int $timeout * * @return \Generator * * @resolve \Icicle\Postgres\Connection * * @throws \Icicle\Postgres\Exception\FailureException */ function connect(string $connectionString, float $timeout = 0) : \Generator { if (!($connection = @\pg_connect($connectionString, \PGSQL_CONNECT_ASYNC | \PGSQL_CONNECT_FORCE_NEW))) { throw new FailureException('Failed to create connection resource'); } if (\pg_connection_status($connection) === \PGSQL_CONNECTION_BAD) { throw new FailureException(\pg_last_error($connection)); } if (!($socket = \pg_socket($connection))) { throw new FailureException('Failed to access connection socket'); } $delayed = new Delayed(); $callback = function ($resource, bool $expired) use(&$poll, &$await, $connection, $delayed, $timeout) { try { if ($expired) { throw new TimeoutException('Connection attempt timed out.'); } switch (\pg_connect_poll($connection)) { case \PGSQL_POLLING_READING: return; // Connection not ready, poll again. // Connection not ready, poll again. case \PGSQL_POLLING_WRITING: $await->listen($timeout); return; // Still writing... // Still writing... case \PGSQL_POLLING_FAILED: throw new FailureException('Could not connect to PostgreSQL server'); case \PGSQL_POLLING_OK: $poll->free(); $await->free(); $delayed->resolve(new BasicConnection($connection, $resource)); return; } } catch (\Throwable $exception) { $poll->free(); $await->free(); \pg_close($connection); $delayed->reject($exception); } }; $poll = Loop\poll($socket, $callback, true); $await = Loop\await($socket, $callback); $poll->listen($timeout); $await->listen($timeout); return (yield $delayed); }
/** * {@inheritdoc} */ public function connect(string $ip, int $port = null, array $options = []) : \Generator { $protocol = (string) ($options['protocol'] ?? (null === $port ? 'unix' : 'tcp')); $autoClose = (bool) ($options['auto_close'] ?? true); $allowSelfSigned = (bool) ($options['allow_self_signed'] ?? self::DEFAULT_ALLOW_SELF_SIGNED); $timeout = (double) ($options['timeout'] ?? self::DEFAULT_CONNECT_TIMEOUT); $verifyDepth = (int) ($options['verify_depth'] ?? self::DEFAULT_VERIFY_DEPTH); $cafile = (string) ($options['cafile'] ?? ''); $name = (string) ($options['name'] ?? ''); $cn = (string) ($options['cn'] ?? $name); $context = []; $context['socket'] = ['connect' => Socket\makeName($ip, $port)]; $context['ssl'] = ['capture_peer_cert' => true, 'capture_peer_chain' => true, 'capture_peer_cert_chain' => true, 'verify_peer' => true, 'verify_peer_name' => true, 'allow_self_signed' => $allowSelfSigned, 'verify_depth' => $verifyDepth, 'CN_match' => $cn, 'SNI_enabled' => true, 'SNI_server_name' => $name, 'peer_name' => $name, 'disable_compression' => true, 'honor_cipher_order' => true]; if ('' !== $cafile) { if (!file_exists($cafile)) { throw new InvalidArgumentError('No file exists at path given for cafile.'); } $context['ssl']['cafile'] = $cafile; } $context = stream_context_create($context); $uri = Socket\makeUri($protocol, $ip, $port); // Error reporting suppressed since stream_socket_client() emits an E_WARNING on failure (checked below). $socket = @stream_socket_client($uri, $errno, $errstr, null, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT, $context); if (!$socket || $errno) { throw new FailureException(sprintf('Could not connect to %s; Errno: %d; %s', $uri, $errno, $errstr)); } $delayed = new Delayed(); $await = Loop\await($socket, function ($resource, bool $expired, Io $await) use($delayed, $autoClose) { $await->free(); if ($expired) { $delayed->reject(new TimeoutException('Connection attempt timed out.')); return; } $delayed->resolve(new NetworkSocket($resource, $autoClose)); }); $await->listen($timeout); try { return (yield $delayed); } finally { $await->free(); } }
/** * Connection constructor. * * @param resource $handle PostgreSQL connection handle. * @param resource $socket PostgreSQL connection stream socket. */ public function __construct($handle, $socket) { $this->handle = $handle; $this->poll = Loop\poll($socket, static function ($resource, bool $expired, Io $poll) use($handle) { /** @var \Icicle\Awaitable\Delayed $delayed */ $delayed = $poll->getData(); if (!\pg_consume_input($handle)) { $delayed->reject(new FailureException(\pg_last_error($handle))); return; } if (!\pg_connection_busy($handle)) { $delayed->resolve(\pg_get_result($handle)); return; } $poll->listen(); // Reading not done, listen again. }); $this->await = Loop\await($socket, static function ($resource, bool $expired, Io $await) use($handle) { $flush = \pg_flush($handle); if (0 === $flush) { $await->listen(); // Not finished sending data, listen again. return; } if (false === $flush) { /** @var \Icicle\Awaitable\Delayed $delayed */ $delayed = $await->getData(); $delayed->reject(new FailureException(\pg_last_error($handle))); } }); $this->onCancelled = static function () use($handle) { \pg_cancel_query($handle); }; $this->executeCallback = function (string $name, array $params) : \Generator { return $this->createResult(yield from $this->send('pg_send_execute', $name, $params)); }; }
/** * @param resource $resource * @param \SplQueue $writeQueue * * @return \Icicle\Loop\Watcher\Io */ private function createAwait($resource, \SplQueue $writeQueue) : Io { return Loop\await($resource, static function ($resource, bool $expired, Io $await) use($writeQueue) { /** @var \Icicle\Awaitable\Delayed $delayed */ list($data, $previous, $peer, $delayed) = $writeQueue->shift(); $length = strlen($data); if (0 === $length) { $delayed->resolve($previous); } else { $written = stream_socket_sendto($resource, substr($data, 0, self::MAX_PACKET_SIZE), 0, $peer); // Having difficulty finding a test to cover this scenario, but the check seems appropriate. if (false === $written || -1 === $written || 0 === $written) { $message = 'Failed to write to datagram.'; if ($error = error_get_last()) { $message .= sprintf(' Errno: %d; %s', $error['type'], $error['message']); } $delayed->reject(new FailureException($message)); return; } if ($length <= $written) { $delayed->resolve($written + $previous); } else { $data = substr($data, $written); $written += $previous; $writeQueue->unshift([$data, $written, $peer, $delayed]); } } if (!$writeQueue->isEmpty()) { $await->listen(); } }); }
/** * @param resource $resource * @param \SplQueue $writeQueue * * @return \Icicle\Loop\Watcher\Io */ private function createAwait($resource, \SplQueue $writeQueue) : Io { return Loop\await($resource, static function ($resource, bool $expired, Io $await) use($writeQueue) { /** @var \Icicle\Awaitable\Delayed $delayed */ list($data, $previous, $timeout, $delayed) = $writeQueue->shift(); if ($expired) { $delayed->reject(new TimeoutException('Writing to the socket timed out.')); return; } $length = strlen($data); if (0 === $length) { $delayed->resolve($previous); } else { // Error reporting suppressed since fwrite() emits E_WARNING if the pipe is broken or the buffer is full. $written = @fwrite($resource, $data, self::CHUNK_SIZE); if (false === $written || 0 === $written) { $message = 'Failed to write to stream.'; if ($error = error_get_last()) { $message .= sprintf(' Errno: %d; %s', $error['type'], $error['message']); } $delayed->reject(new FailureException($message)); return; } if ($length <= $written) { $delayed->resolve($written + $previous); } else { $data = substr($data, $written); $written += $previous; $writeQueue->unshift([$data, $written, $timeout, $delayed]); } } if (!$writeQueue->isEmpty()) { list(, , $timeout) = $writeQueue->bottom(); $await->listen($timeout); } }); }