From cd99a8821b0292b9a4c78cd076b6b747116720cd Mon Sep 17 00:00:00 2001 From: Demin Yin Date: Sun, 27 Sep 2026 00:53:01 -0700 Subject: [PATCH 1/2] Update the embedded library to swoole/library 6.2.x (5aeb8b3) Co-Authored-By: Claude Fable 5.1 --- ext-src/php_swoole_library.h | 1665 ++++++++++++++++++++++++++-------- 1 file changed, 1270 insertions(+), 395 deletions(-) diff --git a/ext-src/php_swoole_library.h b/ext-src/php_swoole_library.h index a08398da77..4742578e3c 100644 --- a/ext-src/php_swoole_library.h +++ b/ext-src/php_swoole_library.h @@ -14,7 +14,7 @@ +----------------------------------------------------------------------+ */ -/* $Id: 26e2e98b4bdaf3da1497910c21c44cd96f7a8dc8 */ +/* $Id: 5aeb8b373fa093e6f901211be3ee11ff52d73e8d */ #ifndef SWOOLE_LIBRARY_H #define SWOOLE_LIBRARY_H @@ -83,7 +83,11 @@ static const char* swoole_library_source_constants = "!defined('CURLOPT_HEADEROPT') && define('CURLOPT_HEADEROPT', 229);\n" "!defined('CURLOPT_PROXYHEADER') && define('CURLOPT_PROXYHEADER', 10228);\n" "!defined('CURLOPT_RESOLVE') && define('CURLOPT_RESOLVE', 10203);\n" - "!defined('CURLOPT_UNIX_SOCKET_PATH') && define('CURLOPT_UNIX_SOCKET_PATH', 10231);\n"; + "!defined('CURLOPT_UNIX_SOCKET_PATH') && define('CURLOPT_UNIX_SOCKET_PATH', 10231);\n" + "// Do not define these in the global namespace: native curl clients use their presence for feature detection.\n" + "!defined('CURLOPT_PREREQFUNCTION') && define('Swoole\\Curl\\CURLOPT_PREREQFUNCTION', 20312);\n" + "!defined('CURL_PREREQFUNC_OK') && define('Swoole\\Curl\\CURL_PREREQFUNC_OK', 0);\n" + "!defined('CURL_PREREQFUNC_ABORT') && define('Swoole\\Curl\\CURL_PREREQFUNC_ABORT', 1);\n"; static const char* swoole_library_source_std_exec = "\n" @@ -299,6 +303,8 @@ static const char* swoole_library_source_core_constant = "\n" " public const OPTION_HTTP2_MAX_HEADER_LIST_SIZE = 'http2_max_header_list_size';\n" "\n" + " public const OPTION_HTTP2_MAX_HEADERS = 'http2_max_headers';\n" + "\n" " public const OPTION_AIO_CORE_WORKER_NUM = 'aio_core_worker_num';\n" "\n" " public const OPTION_AIO_WORKER_NUM = 'aio_worker_num';\n" @@ -826,7 +832,7 @@ static const char* swoole_library_source_core_string_object = "\n" " public function endsWith(string $needle): bool\n" " {\n" - " return strrpos($this->string, $needle) === (strlen($this->string) - strlen($needle));\n" + " return str_ends_with($this->string, $needle);\n" " }\n" "\n" " public function equals($str, bool $strict = false): bool\n" @@ -1086,6 +1092,8 @@ static const char* swoole_library_source_core_array_object = " */\n" " public function get(mixed $key)\n" " {\n" + " // A null key is the empty string for PHP, which deprecates passing it as null as of PHP 8.5.\n" + " $key = $key ?? '';\n" " if (!$this->exists($key)) {\n" " throw new ArrayKeyNotExists($key);\n" " }\n" @@ -1097,6 +1105,7 @@ static const char* swoole_library_source_core_array_object = " */\n" " public function getOr(mixed $key, mixed $default = null)\n" " {\n" + " $key = $key ?? '';\n" " if (!$this->exists($key)) {\n" " return $default;\n" " }\n" @@ -1148,6 +1157,8 @@ static const char* swoole_library_source_core_array_object = " */\n" " public function set(mixed $key, mixed $value): self\n" " {\n" + " $key = $key ?? '';\n" + "\n" " $this->array[$key] = $value;\n" " return $this;\n" " }\n" @@ -1166,13 +1177,17 @@ static const char* swoole_library_source_core_array_object = " */\n" " public function remove(mixed $value, bool $strict = true, bool $loop = false): self\n" " {\n" - " do {\n" + " if (!$loop) {\n" " $key = $this->search($value, $strict);\n" - " if ($key === false) {\n" - " break;\n" + " if ($key !== false) {\n" + " unset($this->array[$key]);\n" " }\n" + " return $this;\n" + " }\n" + " // All the matching keys in one pass, instead of an array_search() from the start for every removal.\n" + " foreach (array_keys($this->array, $value, $strict) as $key) {\n" " unset($this->array[$key]);\n" - " } while ($loop);\n" + " }\n" "\n" " return $this;\n" " }\n" @@ -1192,6 +1207,7 @@ static const char* swoole_library_source_core_array_object = " #[\\ReturnTypeWillChange]\n" " public function offsetGet(mixed $key)\n" " {\n" + " $key = $key ?? '';\n" " if (!array_key_exists($key, $this->array)) {\n" " return null;\n" " }\n" @@ -1200,6 +1216,8 @@ static const char* swoole_library_source_core_array_object = "\n" " public function offsetSet(mixed $key, mixed $value): void\n" " {\n" + " $key = $key ?? '';\n" + "\n" " $this->array[$key] = $value;\n" " }\n" "\n" @@ -1214,11 +1232,13 @@ static const char* swoole_library_source_core_array_object = " #[\\ReturnTypeWillChange]\n" " public function offsetExists(mixed $key)\n" " {\n" + " $key = $key ?? '';\n" " return isset($this->array[$key]);\n" " }\n" "\n" " public function exists(mixed $key): bool\n" " {\n" + " $key = $key ?? '';\n" " return array_key_exists($key, $this->array);\n" " }\n" "\n" @@ -1240,17 +1260,10 @@ static const char* swoole_library_source_core_array_object = " */\n" " public function lastIndexOf(mixed $value, bool $strict = true)\n" " {\n" - " $array = $this->array;\n" - " for (end($array); ($currentKey = key($array)) !== null; prev($array)) {\n" - " $currentValue = current($array);\n" - " if ($currentValue == $value) {\n" - " if ($strict && $currentValue !== $value) {\n" - " continue;\n" - " }\n" - " break;\n" - " }\n" - " }\n" - " return $currentKey;\n" + " // array_keys() compares the way array_search() does (== or ===), without the copy of the array that\n" + " // walking it backwards with end()/prev() forced.\n" + " $keys = array_keys($this->array, $value, $strict);\n" + " return $keys ? $keys[array_key_last($keys)] : null;\n" " }\n" "\n" " /**\n" @@ -1496,9 +1509,8 @@ static const char* swoole_library_source_core_array_object = " */\n" " public function natcasesort(): self\n" " {\n" - " if (natcasesort($this->array) !== true) { // @phpstan-ignore notIdentical.alwaysFalse\n" - " throw new \\RuntimeException('natcasesort() failed');\n" - " }\n" + " natcasesort($this->array);\n" + "\n" " return $this;\n" " }\n" "\n" @@ -1507,9 +1519,8 @@ static const char* swoole_library_source_core_array_object = " */\n" " public function natsort(): self\n" " {\n" - " if (natsort($this->array) !== true) { // @phpstan-ignore notIdentical.alwaysFalse\n" - " throw new \\RuntimeException('natsort() failed');\n" - " }\n" + " natsort($this->array);\n" + "\n" " return $this;\n" " }\n" "\n" @@ -1518,9 +1529,8 @@ static const char* swoole_library_source_core_array_object = " */\n" " public function rsort(int $sort_flags = SORT_REGULAR): self\n" " {\n" - " if (rsort($this->array, $sort_flags) !== true) { // @phpstan-ignore notIdentical.alwaysFalse\n" - " throw new \\RuntimeException('rsort() failed');\n" - " }\n" + " rsort($this->array, $sort_flags);\n" + "\n" " return $this;\n" " }\n" "\n" @@ -1830,11 +1840,9 @@ static const char* swoole_library_source_core_coroutine_server = " return false;\n" " }\n" "\n" - " while ($this->running) { // @phpstan-ignore while.alwaysTrue\n" - " $conn = null;\n" - " /** @var Socket $conn */\n" + " while ($this->running) {\n" " $conn = $socket->accept();\n" - " if ($conn) { // @phpstan-ignore if.alwaysTrue\n" + " if ($conn) {\n" " $conn->setProtocol($this->setting);\n" " if (!empty($this->setting[Constant::OPTION_OPEN_SSL])) {\n" " $fn = static function ($fn, $connection) {\n" @@ -1849,7 +1857,7 @@ static const char* swoole_library_source_core_coroutine_server = " $fn = $this->fn;\n" " $arguments = [new Connection($conn)];\n" " }\n" - " if (Coroutine::create($fn, ...$arguments) < 0) {\n" + " if (Coroutine::create($fn, ...$arguments) === false) {\n" " goto _wait;\n" " }\n" " } else {\n" @@ -1864,12 +1872,13 @@ static const char* swoole_library_source_core_coroutine_server = " if ($socket->errCode == SOCKET_ECANCELED) {\n" " break;\n" " }\n" + " $this->errCode = $socket->errCode;\n" " trigger_error(\"accept failed, Error: {$socket->errMsg}[{$socket->errCode}]\", E_USER_WARNING);\n" - " break;\n" + " return false;\n" " }\n" " }\n" "\n" - " return true; // @phpstan-ignore deadCode.unreachable\n" + " return true;\n" " }\n" "}\n"; @@ -2251,6 +2260,506 @@ static const char* swoole_library_source_core_coroutine_http_functions = " return request($url, 'GET', null, $options, $headers, $cookies);\n" "}\n"; +static const char* swoole_library_source_core_coroutine_http2_multiplex_client = + "\n" + "/**\n" + " * This file is part of Swoole.\n" + " *\n" + " * @link https://www.swoole.com\n" + " * @contact team@swoole.com\n" + " * @license https://github.com/swoole/library/blob/master/LICENSE\n" + " */\n" + "\n" + "declare(strict_types=1);\n" + "\n" + "namespace Swoole\\Coroutine\\Http2;\n" + "\n" + "use Swoole\\Constant;\n" + "use Swoole\\Coroutine\\Channel;\n" + "use Swoole\\Http2\\Request;\n" + "use Swoole\\Http2\\Response;\n" + "\n" + "use function Swoole\\Coroutine\\go;\n" + "\n" + "// This file is packed into the Swoole extension and evaluated at startup, where declaring a class\n" + "// whose parent is missing is a fatal error; the parent class only exists when the extension is\n" + "// compiled with HTTP/2 support (--enable-http2).\n" + "if (!class_exists(Client::class, false)) {\n" + " return;\n" + "}\n" + "\n" + "/**\n" + " * An HTTP/2 client that multiplexes requests from many coroutines over one shared connection.\n" + " *\n" + " * A single recv-loop coroutine reads every response off the connection and routes it to the\n" + " * requesting coroutine by stream ID. The connection is established on demand and re-established\n" + " * transparently after a teardown (the pre-send health check only verifies the local socket state,\n" + " * not peer liveness); a heartbeat checker closes it after a period without requests.\n" + " *\n" + " * The connection is owned by that machinery: the frame-level API inherited from the base class\n" + " * (recv(), read(), write(), goaway()) is disabled and throws, and send() must not be called\n" + " * directly -- the response to a stream that request() did not register would be parked forever,\n" + " * and the connection could then never idle-close. Use request().\n" + " *\n" + " * Besides the settings understood by the underlying client, set() accepts two options that borrow\n" + " * Swoole\\Server's heartbeat vocabulary -- here they govern the client's own outbound connection:\n" + " * - heartbeat_check_interval: seconds between two idle checks (default:\n" + " * DEFAULT_HEARTBEAT_CHECK_INTERVAL; non-positive values fall back to the default)\n" + " * - heartbeat_idle_time: seconds without a new request after which a connection with no in-flight\n" + " * requests is closed automatically (default: DEFAULT_HEARTBEAT_IDLE_TIME); a non-positive value\n" + " * disables idle closing, in which case the connection stays open until close() is called or the\n" + " * peer closes it\n" + " * Both are read when the heartbeat checker starts; changing them on a client whose checker is\n" + " * already running takes effect only after the next close().\n" + " *\n" + " * Call close() once the client is no longer needed. The recv loop and the heartbeat checker are\n" + " * coroutines of their own, and Swoole\\Coroutine\\run() returns only after every coroutine has ended:\n" + " * a client left open holds it back until the connection is closed for being idle, which takes up to\n" + " * heartbeat_idle_time plus heartbeat_check_interval seconds, and for as long as the peer keeps the\n" + " * connection open when idle closing is disabled.\n" + " */\n" + "class MultiplexClient extends Client\n" + "{\n" + " protected const DEFAULT_HEARTBEAT_CHECK_INTERVAL = 3;\n" + "\n" + " protected const DEFAULT_HEARTBEAT_IDLE_TIME = 10;\n" + "\n" + " /**\n" + " * @var object|null identity token of the currently running recv loop; null when none is running.\n" + " * A recv loop whose token no longer matches is stale and must exit without\n" + " * closing the client: a newer loop may own the (reconnected) connection already.\n" + " */\n" + " protected ?object $recvLoopToken = null;\n" + "\n" + " /**\n" + " * @var Channel|null used by the heartbeat checker for an interruptible sleep; closed by close()\n" + " * to wake the checker up immediately\n" + " */\n" + " protected ?Channel $sleepChannel = null;\n" + "\n" + " /**\n" + " * @var Channel|null present while a coroutine health-checks the connection and starts the shared\n" + " * recv loop; other coroutines block on it (until it is closed) before sending\n" + " */\n" + " protected ?Channel $startupBarrier = null;\n" + "\n" + " /**\n" + " * @var array per-stream channels carrying the responses of in-flight requests\n" + " */\n" + " protected array $streamChannels = [];\n" + "\n" + " /**\n" + " * @var array IDs of streams whose requester timed out; the recv loop drops their late\n" + " * responses instead of parking them\n" + " */\n" + " protected array $abandonedStreams = [];\n" + "\n" + " /**\n" + " * @var bool guards against spawning more than one heartbeat-checker coroutine\n" + " */\n" + " protected bool $heartbeatCheckerRunning = false;\n" + "\n" + " /**\n" + " * @var int number of request() calls currently in progress -- including connection setup and\n" + " * sending, where no stream channel exists yet; the heartbeat checker must not close\n" + " * the connection while one is pending\n" + " */\n" + " protected int $pendingRequests = 0;\n" + "\n" + " /**\n" + " * @var float Unix timestamp of the completion of the most recent request() call, successful or\n" + " * not; the heartbeat checker measures idleness against it\n" + " */\n" + " protected float $lastActiveTime = 0;\n" + "\n" + " /**\n" + " * Sends a request over the shared connection and waits for its response.\n" + " *\n" + " * Safe to call from any number of coroutines concurrently; the connection is established (or\n" + " * re-established) on demand.\n" + " *\n" + " * @param float $timeout seconds to wait for the response. Non-positive values fall back to the\n" + " * client's `timeout` setting; without that setting the wait is unbounded.\n" + " * @return false|Response the response, or false on failure: the connection could not be\n" + " * established, sending failed, the wait timed out or was canceled, or\n" + " * the connection was closed while waiting. errCode/errMsg describe the\n" + " * cause -- best-effort only: they live on the shared client, so read\n" + " * them immediately after the false return, before yielding; a\n" + " * concurrently failing request may overwrite them.\n" + " */\n" + " public function request(Request $request, float $timeout = -1): false|Response\n" + " {\n" + " if ($timeout <= 0) {\n" + " // The ping check in ensureRecvLoop() only confirms the request could be written out, not\n" + " // that the peer is alive, so an unbounded default wait could hang on a stale connection\n" + " // forever.\n" + " $timeout = (float) ($this->setting[Constant::OPTION_TIMEOUT] ?? -1);\n" + " }\n" + " // The counter -- not the stream-channel registry, which only has an entry once send() has\n" + " // returned -- is what keeps the heartbeat checker from closing the connection while a\n" + " // request is still connecting, sending, or waiting.\n" + " $this->pendingRequests++;\n" + " try {\n" + " if (!$this->ensureRecvLoop()) {\n" + " return false;\n" + " }\n" + " $streamId = $this->send($request);\n" + " if ($streamId === false) {\n" + " return false;\n" + " }\n" + " // The response can arrive before send() returns when send() yields on socket I/O; the\n" + " // recv loop then has registered the stream channel already, with the response buffered\n" + " // in it.\n" + " $chan = $this->streamChannels[$streamId] ?? null;\n" + " if ($chan === null) {\n" + " if ($this->recvLoopToken === null) {\n" + " // The client was closed while send() yielded, and the flush has already dropped\n" + " // any response parked for this stream: fail fast instead of waiting out the\n" + " // timeout on a channel nothing will ever push to.\n" + " $this->setError(SWOOLE_ERROR_CLIENT_NO_CONNECTION, 'the connection was closed while the request was being sent');\n" + " return false;\n" + " }\n" + " $chan = $this->openStreamChannel($streamId);\n" + " }\n" + " $data = false;\n" + " try {\n" + " $data = $chan->pop($timeout);\n" + " } finally {\n" + " $this->closeStreamChannel($streamId);\n" + " if ($data === false) {\n" + " if ($chan->errCode === SWOOLE_CHANNEL_CLOSED) {\n" + " // Connection teardown: no abandoned-stream marker may be left behind --\n" + " // stream IDs restart after a reconnect, and a stale marker would drop a\n" + " // response of the next connection.\n" + " $this->setError(SWOOLE_ERROR_CLIENT_NO_CONNECTION, 'the connection was closed while waiting for the response');\n" + " } else {\n" + " // Timed out or canceled: nobody waits for this stream anymore, but the\n" + " // connection lives on -- the recv loop must drop the late response instead\n" + " // of parking it, which would leak the parked channel (and block idle\n" + " // closing) until the connection closes.\n" + " $this->abandonedStreams[$streamId] = true;\n" + " if ($chan->errCode === SWOOLE_CHANNEL_TIMEOUT) {\n" + " $this->setError(SWOOLE_ERROR_CO_TIMEDOUT, 'the response timed out');\n" + " } else {\n" + " $this->setError(SWOOLE_ERROR_CO_CANCELED, 'the wait for the response was canceled');\n" + " }\n" + " }\n" + " }\n" + " }\n" + "\n" + " return $data;\n" + " } finally {\n" + " $this->pendingRequests--;\n" + " // Idleness is measured from the completion of the last request, however it ended.\n" + " $this->lastActiveTime = microtime(true);\n" + " }\n" + " }\n" + "\n" + " /**\n" + " * Closes the connection, aborting every in-flight request (their request() calls return false)\n" + " * and stopping the recv loop and the heartbeat checker.\n" + " *\n" + " * The client remains usable: the next request() reconnects.\n" + " *\n" + " * @return bool whether there was a connection (or a running recv loop) to tear down; false\n" + " * when there was nothing left to close, e.g. a repeated close() or a connection\n" + " * already closed by the heartbeat checker or the recv loop. Unlike in the base\n" + " * class, the value does not reflect the raw socket close: that reports failure\n" + " * whenever a coroutine is still bound to the socket, which on this class is the\n" + " * normal case -- the recv loop reads the connection permanently.\n" + " */\n" + " public function close(): bool\n" + " {\n" + " $wasUp = $this->connected || $this->recvLoopToken !== null;\n" + " // Invalidate the connection before waking anyone: every requester woken during the\n" + " // teardown may retry immediately, and must find the token cleared and reconnect instead\n" + " // of sending into the socket being torn down. parent::close() itself resumes coroutines\n" + " // bound to the socket (the recv loop, senders mid-send()) and reports\n" + " // SW_ERROR_CO_SOCKET_CLOSE_WAIT on errCode while doing so -- mechanics, not a failure --\n" + " // so the error slot is restored afterwards.\n" + " $this->recvLoopToken = null;\n" + " $errCode = $this->errCode;\n" + " $errMsg = $this->errMsg;\n" + " parent::close();\n" + " $this->errCode = $errCode;\n" + " $this->errMsg = $errMsg;\n" + " $this->flushStreamChannels();\n" + " $this->abandonedStreams = [];\n" + " // Clear the property before closing, as with the startup barrier: the close resumes the\n" + " // heartbeat checker immediately, and when a requester woken above has already started a\n" + " // new connection generation, the checker does not exit -- it must find the property null\n" + " // and create a fresh channel, not re-enter pop() on the closed one, which returns without\n" + " // yielding: a busy spin that would starve the event loop.\n" + " $sleepChannel = $this->sleepChannel;\n" + " $this->sleepChannel = null;\n" + " $sleepChannel?->close();\n" + " return $wasUp;\n" + " }\n" + "\n" + " /**\n" + " * Disabled: the shared recv loop owns all reads on a multiplexed connection; reading from user\n" + " * code would steal its frames and corrupt the routing of responses. Use request().\n" + " *\n" + " * @throws \\BadMethodCallException always\n" + " */\n" + " public function recv(float $timeout = 0): Response|false\n" + " {\n" + " throw new \\BadMethodCallException('the shared recv loop owns all reads on a multiplexed connection; use request()');\n" + " }\n" + "\n" + " /**\n" + " * Disabled: the shared recv loop owns all reads on a multiplexed connection. Use request().\n" + " *\n" + " * @throws \\BadMethodCallException always\n" + " */\n" + " public function read(float $timeout = 0): Response|false\n" + " {\n" + " throw new \\BadMethodCallException('the shared recv loop owns all reads on a multiplexed connection; use request()');\n" + " }\n" + "\n" + " /**\n" + " * Disabled: writing to a stream that request() did not register conflicts with the shared recv\n" + " * loop, and its response could never be routed. Use request().\n" + " *\n" + " * @throws \\BadMethodCallException always\n" + " */\n" + " public function write(int $stream_id, mixed $data, bool $end_stream = false): bool\n" + " {\n" + " throw new \\BadMethodCallException('streams cannot be written to directly on a multiplexed connection; use request()');\n" + " }\n" + "\n" + " /**\n" + " * Disabled: shutting the connection down beneath the recv loop and the in-flight requests must\n" + " * go through close().\n" + " *\n" + " * @throws \\BadMethodCallException always\n" + " */\n" + " public function goaway(int $error_code = SWOOLE_HTTP2_ERROR_NO_ERROR, string $debug_data = ''): bool\n" + " {\n" + " throw new \\BadMethodCallException('a multiplexed connection is shut down through close()');\n" + " }\n" + "\n" + " /**\n" + " * Registers the channel over which the response for the given stream will be delivered.\n" + " */\n" + " protected function openStreamChannel(int $streamId): Channel\n" + " {\n" + " return $this->streamChannels[$streamId] = new Channel(1);\n" + " }\n" + "\n" + " /**\n" + " * Unregisters a stream's channel, waking its requester (pop() returns false) if one still waits.\n" + " */\n" + " protected function closeStreamChannel(int $streamId): void\n" + " {\n" + " if ($channel = $this->streamChannels[$streamId] ?? null) {\n" + " $channel->close();\n" + " }\n" + "\n" + " unset($this->streamChannels[$streamId]);\n" + " }\n" + "\n" + " /**\n" + " * Aborts all in-flight requests by closing and unregistering their stream channels.\n" + " */\n" + " protected function flushStreamChannels(): void\n" + " {\n" + " foreach (array_keys($this->streamChannels) as $streamId) {\n" + " $this->closeStreamChannel($streamId);\n" + " }\n" + " }\n" + "\n" + " /**\n" + " * Re-establishes the connection, discarding the current one.\n" + " */\n" + " protected function reconnect(): bool\n" + " {\n" + " parent::close();\n" + " return parent::connect();\n" + " }\n" + "\n" + " /**\n" + " * Makes sure the connection is healthy and the shared recv loop is running, (re)connecting and\n" + " * starting the loop when needed. Serializes concurrent callers so that none of them sends while\n" + " * the connection is still being set up.\n" + " *\n" + " * @return bool false when no recv loop is running: the connection could not be established\n" + " * (errCode/errMsg are connect()'s), or a concurrent close() aborted the startup\n" + " */\n" + " protected function ensureRecvLoop(): bool\n" + " {\n" + " $this->ensureHeartbeatChecker();\n" + "\n" + " // The health check and the reconnect below both yield; sending during either would fail\n" + " // spuriously. Coroutines arriving in the meantime must wait for the starter to finish\n" + " // (closing the channel wakes all waiters) and then re-check, as the state has changed.\n" + " while ($this->startupBarrier !== null) {\n" + " $this->startupBarrier->pop();\n" + " }\n" + " if ($this->recvLoopToken !== null) {\n" + " return true;\n" + " }\n" + "\n" + " $this->startupBarrier = new Channel(1);\n" + " $spawned = false;\n" + " $token = null;\n" + " try {\n" + " $token = $this->recvLoopToken = new \\stdClass();\n" + " if (!$this->ping() && !$this->reconnect()) {\n" + " // The connection cannot be established; connect() has recorded a precise errCode\n" + " // (such as a refused connection). Returning here keeps the caller from send()ing,\n" + " // which would overwrite it with a generic no-connection error.\n" + " return false;\n" + " }\n" + " // close() may have cleared the token while ping()/reconnect() yielded. A loop spawned\n" + " // with that stale token would park in recv() -- binding the socket -- while looking\n" + " // stopped to everyone else, and the next request would spawn a second reader onto the\n" + " // same socket.\n" + " if ($this->recvLoopToken !== $token) {\n" + " $this->setError(SWOOLE_ERROR_CLIENT_NO_CONNECTION, 'the connection was closed during startup');\n" + " return false;\n" + " }\n" + " go(fn () => $this->recvLoop($token));\n" + " $spawned = true;\n" + " // The loop runs immediately until its first yield: on a connection that died between\n" + " // connect() and the first read, it exits -- and runs close() -- before go() returns, so\n" + " // an unconditional true here would let the caller send() on the closed client and\n" + " // overwrite the precise errCode with a generic one.\n" + " return $this->recvLoopToken === $token;\n" + " } finally {\n" + " if (!$spawned && $this->recvLoopToken === $token) {\n" + " // Startup died before the recv loop was spawned (an exception from the health\n" + " // check or reconnect): leave no token behind, or every later request would skip\n" + " // reconnection and send with no coroutine reading the connection.\n" + " $this->recvLoopToken = null;\n" + " }\n" + " // Clear the property before closing: close() resumes the waiters immediately, and a\n" + " // waiter that still saw the barrier set would re-enter pop() on the closed channel,\n" + " // which returns synchronously -- a busy spin that never yields back to this coroutine.\n" + " $barrier = $this->startupBarrier;\n" + " $this->startupBarrier = null;\n" + " $barrier->close();\n" + " }\n" + " }\n" + "\n" + " /**\n" + " * Reads responses off the connection and routes each one to its requester's stream channel;\n" + " * runs in a coroutine of its own until the connection breaks or the client is closed.\n" + " *\n" + " * @param object $token the loop's identity: once it no longer matches $recvLoopToken, this loop\n" + " * is stale and exits without closing the client\n" + " */\n" + " protected function recvLoop(object $token): void\n" + " {\n" + " $reason = 'unknown';\n" + " try {\n" + " while (true) {\n" + " // Wait without a read timeout: recv() called without one falls back to the socket's\n" + " // read timeout (the `timeout` setting, 60 seconds when unset), which would tear down\n" + " // a merely quiet connection as broken -- aborting every in-flight request with it --\n" + " // and cap how long any single response may take regardless of the timeout passed to\n" + " // request(). Idle teardown belongs to the heartbeat checker alone. (parent:: because\n" + " // the public recv() is disabled -- this loop is the connection's only reader.)\n" + " $response = parent::recv(-1);\n" + "\n" + " if ($this->recvLoopToken !== $token) {\n" + " $reason = 'the client was closed or the loop was superseded';\n" + " break;\n" + " }\n" + "\n" + " if ($response === false) {\n" + " $reason = 'the connection was broken';\n" + " break;\n" + " }\n" + "\n" + " $streamId = $response->streamId;\n" + " if ($channel = $this->streamChannels[$streamId] ?? null) {\n" + " $channel->push($response);\n" + " } elseif (isset($this->abandonedStreams[$streamId])) {\n" + " unset($this->abandonedStreams[$streamId]);\n" + " } elseif ($streamId % 2 === 1) {\n" + " // The response won the race against its requester registering the stream\n" + " // channel (client-initiated streams have odd IDs): register the channel\n" + " // here and buffer the response for the requester to pick up.\n" + " $this->openStreamChannel($streamId)->push($response);\n" + " }\n" + " }\n" + " } catch (\\Throwable $exception) {\n" + " $reason = 'an exception was thrown: ' . $exception->getMessage();\n" + " swoole_error_log(SWOOLE_LOG_ERROR, (string) $exception);\n" + " } finally {\n" + " swoole_error_log(SWOOLE_LOG_DEBUG, 'Recv loop stopped; the next request will restart it. Reason: ' . $reason);\n" + " if ($this->recvLoopToken === $token) {\n" + " $this->close();\n" + " }\n" + " }\n" + " }\n" + "\n" + " /**\n" + " * Makes sure the heartbeat checker is running, unless disabled: a coroutine that closes the\n" + " * connection once no requests are in flight and none was sent for `heartbeat_idle_time` seconds.\n" + " */\n" + " protected function ensureHeartbeatChecker(): void\n" + " {\n" + " if ($this->heartbeatCheckerRunning) {\n" + " return;\n" + " }\n" + " $maxIdleTime = (float) ($this->setting[Constant::OPTION_HEARTBEAT_IDLE_TIME] ?? self::DEFAULT_HEARTBEAT_IDLE_TIME);\n" + " if ($maxIdleTime <= 0) {\n" + " return;\n" + " }\n" + " $checkInterval = (float) ($this->setting[Constant::OPTION_HEARTBEAT_CHECK_INTERVAL] ?? self::DEFAULT_HEARTBEAT_CHECK_INTERVAL);\n" + " if ($checkInterval <= 0) {\n" + " // A non-positive interval would make interruptibleSleep() wait forever -- silently\n" + " // disabling idle closing through the wrong option while pinning the checker coroutine.\n" + " $checkInterval = self::DEFAULT_HEARTBEAT_CHECK_INTERVAL;\n" + " }\n" + "\n" + " $this->heartbeatCheckerRunning = true;\n" + " go(\n" + " function () use ($checkInterval, $maxIdleTime) {\n" + " try {\n" + " while (true) {\n" + " $this->interruptibleSleep($checkInterval);\n" + " if ($this->recvLoopToken === null) {\n" + " break;\n" + " }\n" + " // The pending-requests counter covers requests still connecting or sending;\n" + " // the stream-channel registry covers responses parked for pickup.\n" + " if ($this->pendingRequests === 0 && $this->streamChannels === [] && microtime(true) - $this->lastActiveTime > $maxIdleTime) {\n" + " $this->close();\n" + " break;\n" + " }\n" + " }\n" + " } finally {\n" + " $this->heartbeatCheckerRunning = false;\n" + " }\n" + " }\n" + " );\n" + " }\n" + "\n" + " /**\n" + " * Records the failure of the current operation on the client's errCode/errMsg properties.\n" + " */\n" + " protected function setError(int $errCode, string $errMsg): void\n" + " {\n" + " $this->errCode = $errCode;\n" + " $this->errMsg = $errMsg;\n" + " }\n" + "\n" + " /**\n" + " * Sleeps for the given number of seconds, or until close() wakes the sleeper up early by\n" + " * closing the sleep channel.\n" + " */\n" + " protected function interruptibleSleep(float $seconds): void\n" + " {\n" + " // Deliberately not the null-coalescing assignment operator: written adjacently it contains\n" + " // a C trigraph sequence that breaks this file when packed into the extension header.\n" + " $this->sleepChannel = $this->sleepChannel ?? new Channel(1);\n" + " $this->sleepChannel->pop($seconds);\n" + " }\n" + "}\n"; + static const char* swoole_library_source_core_connection_pool = "\n" "/**\n" @@ -2288,6 +2797,10 @@ static const char* swoole_library_source_core_connection_pool = "\n" " public function fill(): void\n" " {\n" + " if ($this->pool === null) {\n" + " // A closed pool has nowhere to keep the connections: they would be made, counted and dropped.\n" + " return;\n" + " }\n" " while ($this->size > $this->num) {\n" " $this->make();\n" " }\n" @@ -2326,6 +2839,9 @@ static const char* swoole_library_source_core_connection_pool = "\n" " public function close(): void\n" " {\n" + " if ($this->pool === null) {\n" + " return;\n" + " }\n" " $this->pool->close();\n" " $this->pool = null;\n" " $this->num = 0;\n" @@ -2555,6 +3071,27 @@ static const char* swoole_library_source_core_database_mysqli_pool = " return $mysqli;\n" " }, $size, MysqliProxy::class);\n" " }\n" + "\n" + " /**\n" + " * Get a mysqli connection from the pool, wrapped in a MysqliProxy. The proxy's transaction tracking is reset,\n" + " * as PDOPool does, so that a connection put back in the middle of a transaction does not keep the next\n" + " * borrower from reconnecting when the connection is lost.\n" + " *\n" + " * @param float $timeout > 0 means waiting for the specified number of seconds. other means no waiting.\n" + " * @return MysqliProxy|false Returns a MysqliProxy object from the pool, or false if the pool is full and the timeout is reached.\n" + " */\n" + " public function get(float $timeout = -1)\n" + " {\n" + " /* @var MysqliProxy|false $mysqli */\n" + " $mysqli = parent::get($timeout);\n" + " if ($mysqli === false) {\n" + " return false;\n" + " }\n" + "\n" + " $mysqli->reset();\n" + "\n" + " return $mysqli;\n" + " }\n" "}\n"; static const char* swoole_library_source_core_database_mysqli_proxy = @@ -2576,7 +3113,7 @@ static const char* swoole_library_source_core_database_mysqli_proxy = " */\n" "class MysqliProxy extends ObjectProxy\n" "{\n" - " public const IO_METHOD_REGEX = '/^autocommit|begin_transaction|change_user|close|commit|kill|multi_query|ping|prepare|query|real_connect|real_query|reap_async_query|refresh|release_savepoint|rollback|savepoint|select_db|send_query|set_charset|ssl_set$/i';\n" + " public const IO_METHOD_REGEX = '/^(autocommit|begin_transaction|change_user|close|commit|execute_query|kill|multi_query|ping|prepare|query|real_connect|real_query|reap_async_query|refresh|release_savepoint|rollback|savepoint|select_db|send_query|set_charset|ssl_set)$/i';\n" "\n" " public const IO_ERRORS = [\n" " 2002, // MYSQLND_CR_CONNECTION_ERROR\n" @@ -2598,6 +3135,18 @@ static const char* swoole_library_source_core_database_mysqli_proxy = "\n" " protected int $round = 0;\n" "\n" + " /**\n" + " * Whether a transaction was started with begin_transaction() and not yet ended with commit() or rollback().\n" + " */\n" + " protected bool $inTransaction = false;\n" + "\n" + " /**\n" + " * Whether autocommit was turned off with autocommit(false): every statement then runs inside an implicit\n" + " * transaction, and commit() or rollback() only ends the current one, so the connection counts as in a\n" + " * transaction until autocommit(true).\n" + " */\n" + " protected bool $autocommitDisabled = false;\n" + "\n" " public function __construct(callable $constructor)\n" " {\n" " parent::__construct($constructor());\n" @@ -2607,23 +3156,63 @@ static const char* swoole_library_source_core_database_mysqli_proxy = " public function __call(string $name, array $arguments)\n" " {\n" " for ($n = 3; $n--;) {\n" - " $ret = @$this->__object->{$name}(...$arguments);\n" + " // Under the default report mode (MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT since PHP 8.1) a failure\n" + " // throws a mysqli_sql_exception instead of returning false; both forms are handled the same way below.\n" + " $exception = null;\n" + " try {\n" + " $ret = @$this->__object->{$name}(...$arguments);\n" + " } catch (\\mysqli_sql_exception $exception) {\n" + " $ret = false;\n" + " }\n" " if ($ret === false) {\n" + " $errno = $exception ? $exception->getCode() : $this->__object->errno;\n" " /* non-IO method */\n" " if (!preg_match(static::IO_METHOD_REGEX, $name)) {\n" + " if ($exception) {\n" + " throw $exception;\n" + " }\n" + " if (in_array($errno, static::IO_ERRORS, true)) {\n" + " // A method outside the retry list, e.g. store_result(), next_result() or stat(), can return\n" + " // false with no error at all, but false with a lost-connection errno is the connection going\n" + " // away, which is reported rather than passed through as if there were nothing to return.\n" + " // Only the retry is limited to methods that run the statement from the start.\n" + " throw new MysqliException($this->__object->error, $errno);\n" + " }\n" " break;\n" " }\n" " /* no more chances or non-IO failures */\n" - " if (!in_array($this->__object->errno, static::IO_ERRORS, true) || ($n === 0)) {\n" - " throw new MysqliException($this->__object->error, $this->__object->errno);\n" + " if (!in_array($errno, static::IO_ERRORS, true) || ($n === 0)) {\n" + " if ($exception) {\n" + " throw $exception;\n" + " }\n" + " throw new MysqliException($this->__object->error, $errno);\n" + " }\n" + " if ($this->inTransaction()) {\n" + " // The transaction died with the connection. Reconnecting and re-running this one call would\n" + " // silently drop what ran before it in the transaction, and run this call, and the commit(), on\n" + " // a fresh connection outside of any transaction. The caller has to see the lost connection and\n" + " // redo the whole unit of work; the next call, outside the transaction, reconnects as usual.\n" + " $this->reset();\n" + " if ($exception) {\n" + " throw $exception;\n" + " }\n" + " throw new MysqliException($this->__object->error, $errno);\n" " }\n" " $this->reconnect();\n" " continue;\n" " }\n" " if (strcasecmp($name, 'prepare') === 0) {\n" - " $ret = new MysqliStatementProxy($ret, $arguments[0], $this);\n" + " // By position or by name: mysqli::prepare(string $query).\n" + " $ret = new MysqliStatementProxy($ret, $arguments[0] ?? $arguments['query'] ?? null, $this);\n" " } elseif (strcasecmp($name, 'stmt_init') === 0) {\n" " $ret = new MysqliStatementProxy($ret, null, $this);\n" + " } elseif (strcasecmp($name, 'begin_transaction') === 0) {\n" + " $this->inTransaction = true;\n" + " } elseif (strcasecmp($name, 'commit') === 0 || strcasecmp($name, 'rollback') === 0) {\n" + " $this->inTransaction = false;\n" + " } elseif (strcasecmp($name, 'autocommit') === 0) {\n" + " // By position or by name: mysqli::autocommit(bool $enable).\n" + " $this->autocommitDisabled = !($arguments[0] ?? $arguments['enable'] ?? true);\n" " }\n" " break;\n" " }\n" @@ -2641,6 +3230,7 @@ static const char* swoole_library_source_core_database_mysqli_proxy = " $constructor = $this->constructor;\n" " parent::__construct($constructor());\n" " $this->round++;\n" + " $this->reset();\n" " /* restore context */\n" " if (!empty($this->charsetContext)) {\n" " $this->__object->set_charset($this->charsetContext);\n" @@ -2653,6 +3243,27 @@ static const char* swoole_library_source_core_database_mysqli_proxy = " }\n" " }\n" "\n" + " /**\n" + " * Whether the connection is inside a transaction: one started with begin_transaction(), or the implicit one\n" + " * that runs while autocommit is turned off with autocommit(false). A transaction started by hand, with\n" + " * query('START TRANSACTION') or query('SET autocommit=0'), is not seen here, the same limitation PDO's\n" + " * inTransaction() has. A lost connection inside a transaction is reported instead of reconnected.\n" + " */\n" + " public function inTransaction(): bool\n" + " {\n" + " return $this->inTransaction || $this->autocommitDisabled;\n" + " }\n" + "\n" + " /**\n" + " * Forgets the transaction state tracked by inTransaction(): after a reconnect, when the transaction died with\n" + " * the connection, and when the pool hands the connection out.\n" + " */\n" + " public function reset(): void\n" + " {\n" + " $this->inTransaction = false;\n" + " $this->autocommitDisabled = false;\n" + " }\n" + "\n" " public function options(int $option, $value): bool\n" " {\n" " $this->setOptContext[$option] = $value;\n" @@ -2693,7 +3304,12 @@ static const char* swoole_library_source_core_database_mysqli_statement_proxy = "\n" "class MysqliStatementProxy extends ObjectProxy\n" "{\n" - " public const IO_METHOD_REGEX = '/^close|execute|fetch|prepare$/i';\n" + " /**\n" + " * The methods that are run again on a fresh connection after a lost one. fetch() is not one of them: it reads\n" + " * the result of an execute() that went down with the connection, and running it on a statement that was\n" + " * prepared again but never executed fails with \"Commands out of sync\" in place of the lost connection.\n" + " */\n" + " public const IO_METHOD_REGEX = '/^(close|execute|prepare)$/i';\n" "\n" " /** @var \\mysqli_stmt */\n" " protected $__object;\n" @@ -2721,30 +3337,59 @@ static const char* swoole_library_source_core_database_mysqli_statement_proxy = " public function __call(string $name, array $arguments)\n" " {\n" " for ($n = 3; $n--;) {\n" - " $ret = @$this->__object->{$name}(...$arguments);\n" + " // Under the default report mode (MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT since PHP 8.1) a failure\n" + " // throws a mysqli_sql_exception instead of returning false; both forms are handled the same way below.\n" + " $exception = null;\n" + " try {\n" + " $ret = @$this->__object->{$name}(...$arguments);\n" + " } catch (\\mysqli_sql_exception $exception) {\n" + " $ret = false;\n" + " }\n" " if ($ret === false) {\n" + " $errno = $exception ? $exception->getCode() : $this->__object->errno;\n" " /* non-IO method */\n" " if (!preg_match(static::IO_METHOD_REGEX, $name)) {\n" + " if ($exception) {\n" + " throw $exception;\n" + " }\n" + " if (in_array($errno, $this->parent::IO_ERRORS, true)) {\n" + " // False from fetch(), get_result(), store_result() and the like can mean there is nothing to\n" + " // return; with a lost-connection errno it means the connection is gone, and that is reported.\n" + " throw new MysqliException($this->__object->error, $errno);\n" + " }\n" " break;\n" " }\n" - " /* no more chances or non-IO failures or in transaction */\n" - " if (!in_array($this->__object->errno, $this->parent::IO_ERRORS, true) || ($n === 0)) {\n" - " throw new MysqliException($this->__object->error, $this->__object->errno);\n" + " /* no more chances or non-IO failures */\n" + " if (!in_array($errno, $this->parent::IO_ERRORS, true) || ($n === 0)) {\n" + " if ($exception) {\n" + " throw $exception;\n" + " }\n" + " throw new MysqliException($this->__object->error, $errno);\n" + " }\n" + " if ($this->parent->inTransaction()) {\n" + " // The transaction died with the connection; see MysqliProxy::__call() for why this is not retried.\n" + " $this->parent->reset();\n" + " if ($exception) {\n" + " throw $exception;\n" + " }\n" + " throw new MysqliException($this->__object->error, $errno);\n" " }\n" " if ($this->parent->getRound() === $this->parentRound) {\n" " /* if not equal, parent has reconnected */\n" " $this->parent->reconnect();\n" " }\n" - " $parent = $this->parent->__getObject();\n" - " $this->__object = $this->queryString ? @$parent->prepare($this->queryString) : @$parent->stmt_init();\n" - " if ($this->__object === false) {\n" + " $this->parentRound = $this->parent->getRound();\n" + " $parent = $this->parent->__getObject();\n" + " $statement = $this->queryString ? @$parent->prepare($this->queryString) : @$parent->stmt_init();\n" + " if ($statement === false) {\n" " throw new MysqliException($parent->error, $parent->errno);\n" " }\n" + " $this->__object = $statement;\n" " if (!empty($this->bindParamContext)) {\n" " $this->__object->bind_param($this->bindParamContext[0], ...$this->bindParamContext[1]);\n" " }\n" " if (!empty($this->bindResultContext)) {\n" - " $this->__object->bind_result($this->bindResultContext);\n" + " $this->__object->bind_result(...$this->bindResultContext);\n" " }\n" " foreach ($this->attrSetContext as $attr => $value) {\n" " $this->__object->attr_set($attr, $value);\n" @@ -2752,7 +3397,8 @@ static const char* swoole_library_source_core_database_mysqli_statement_proxy = " continue;\n" " }\n" " if (strcasecmp($name, 'prepare') === 0) {\n" - " $this->queryString = $arguments[0];\n" + " // By position or by name: mysqli_stmt::prepare(string $query).\n" + " $this->queryString = $arguments[0] ?? $arguments['query'] ?? null;\n" " }\n" " break;\n" " }\n" @@ -2796,6 +3442,9 @@ static const char* swoole_library_source_core_database_detects_lost_connections "class DetectsLostConnections\n" "{\n" " /**\n" + " * The list mirrors Laravel's `Illuminate\\Database\\LostConnectionDetector` (laravel/framework 12.x), in its order,\n" + " * so that a diff against it stays mechanical; the entries this library adds on its own are marked as such.\n" + " *\n" " * @var array\n" " */\n" " private const ERROR_MESSAGES = [\n" @@ -2816,7 +3465,7 @@ static const char* swoole_library_source_core_database_detects_lost_connections " 'reset by peer',\n" " 'Physical connection is not usable',\n" " 'TCP Provider: Error code 0x68',\n" - " 'ORA-03113',\n" + " 'ORA-03113', // this library's own\n" " 'ORA-03114',\n" " 'Packets out of order. Expected',\n" " 'Adaptive Server connection failed',\n" @@ -2846,8 +3495,9 @@ static const char* swoole_library_source_core_database_detects_lost_connections " 'Reason: Server is in script upgrade mode. Only administrator can connect at this time.',\n" " 'Unknown $curl_error_code: 77',\n" " 'SSL: Handshake timed out',\n" - " 'SQLSTATE[08006] [7] SSL error: sslv3 alert unexpected message',\n" - " 'SQLSTATE[08006] [7] unrecognized SSL error code:',\n" + " 'SSL error: sslv3 alert unexpected message',\n" + " 'SSL error: ssl/tls alert unexpected message',\n" + " 'unrecognized SSL error code:',\n" " 'SQLSTATE[HY000] [1045] Access denied for user',\n" " 'SQLSTATE[HY000] [2002] No connection could be made because the target machine actively refused it',\n" " 'SQLSTATE[HY000] [2002] A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond',\n" @@ -2865,12 +3515,22 @@ static const char* swoole_library_source_core_database_detects_lost_connections " 'Connection lost',\n" " 'Broken pipe',\n" " 'SQLSTATE[25006]: Read only sql transaction: 7',\n" + " // PlanetScale MySQL / Vitess\n" " 'vtgate connection error: no healthy endpoints',\n" " 'primary is not serving, there may be a reparent operation in progress',\n" " 'current keyspace is being resharded',\n" " 'no healthy tablet available',\n" " 'transaction pool connection limit exceeded',\n" " 'SSL operation failed with code 5',\n" + " // PlanetScale PostgreSQL / pg_bouncer\n" + " 'no primary available for branch',\n" + " 'no replica available for branch',\n" + " 'no running members available for branch',\n" + " 'failed to connect to upstream',\n" + " 'failed to send startup message',\n" + " 'failed to read startup message',\n" + " 'canceling statement due to conflict with recovery',\n" + " // This library's own, from here on:\n" " // PDO::prepare(): Send of 77 bytes failed with errno=110 Operation timed out\n" " // SSL: Handshake timed out\n" " // SSL: Operation timed out\n" @@ -3169,6 +3829,13 @@ static const char* swoole_library_source_core_database_pdo_proxy = "\n" " protected int $round = 0;\n" "\n" + " /**\n" + " * The number of transactions started through beginTransaction() and not ended through commit() or rollBack().\n" + " *\n" + " * It is what inTransaction() reports, and what decides whether a statement may reconnect. The state PDO\n" + " * reports cannot: a connection that was lost is inside a transaction for PDO, always with pdo_pgsql, and with\n" + " * pdo_mysql when it was lost inside one, so that it would never be replaced.\n" + " */\n" " protected int $inTransaction = 0;\n" "\n" " public function __construct(callable $constructor)\n" @@ -3199,7 +3866,8 @@ static const char* swoole_library_source_core_database_pdo_proxy = " $this->inTransaction--;\n" " }\n" "\n" - " if ((strcasecmp($name, 'prepare') === 0) || (strcasecmp($name, 'query') === 0)) {\n" + " // Not a statement but false when the call failed and the error mode is not the exception one.\n" + " if ($ret instanceof \\PDOStatement && (strcasecmp($name, 'prepare') === 0 || strcasecmp($name, 'query') === 0)) {\n" " $ret = new PDOStatementProxy($ret, $this);\n" " }\n" "\n" @@ -3291,13 +3959,24 @@ static const char* swoole_library_source_core_database_pdo_statement_proxy = " try {\n" " $ret = $this->__object->{$name}(...$arguments);\n" " } catch (\\PDOException $e) {\n" - " if (!$this->parent->inTransaction() && DetectsLostConnections::causedByLostConnection($e)) {\n" + " // Only execute() is retried on a fresh connection, since it runs the statement from the start. The\n" + " // other methods (fetch*(), rowCount(), ...) read the result of an execute() that went down with the\n" + " // connection; re-preparing the statement and calling one of them on it without executing it first\n" + " // returns no rows and no error, so a lost connection there surfaces as the exception it is.\n" + " if (strcasecmp($name, 'execute') === 0 && !$this->parent->inTransaction() && DetectsLostConnections::causedByLostConnection($e)) {\n" " if ($this->parent->getRound() === $this->parentRound) {\n" " /* if not equal, parent has reconnected */\n" " $this->parent->reconnect();\n" " }\n" - " $parent = $this->parent->__getObject();\n" - " $this->__object = $parent->prepare($this->__object->queryString);\n" + " // Record the parent's round, or the next lost connection on this statement looks like one the parent\n" + " // has already recovered from, and the statement is prepared again on the dead connection instead.\n" + " $this->parentRound = $this->parent->getRound();\n" + " $parent = $this->parent->__getObject();\n" + " $statement = $parent->prepare($this->__object->queryString);\n" + " if ($statement === false) {\n" + " throw $e;\n" + " }\n" + " $this->__object = $statement;\n" "\n" " foreach ($this->setAttributeContext as $attribute => $value) {\n" " $this->__object->setAttribute($attribute, $value);\n" @@ -3387,7 +4066,8 @@ static const char* swoole_library_source_core_database_redis_config = "\n" " protected float $read_timeout = 0.0;\n" "\n" - " protected string $auth = '';\n" + " /** @var string|string[] */\n" + " protected string|array $auth = '';\n" "\n" " protected int $dbIndex = 0;\n" "\n" @@ -3462,12 +4142,18 @@ static const char* swoole_library_source_core_database_redis_config = " return $this;\n" " }\n" "\n" - " public function getAuth(): string\n" + " /**\n" + " * @return string|string[]\n" + " */\n" + " public function getAuth(): string|array\n" " {\n" " return $this->auth;\n" " }\n" "\n" - " public function withAuth(string $auth): self\n" + " /**\n" + " * @param string|string[] $auth string for password only, array for [username, password]\n" + " */\n" + " public function withAuth(string|array $auth): self\n" " {\n" " $this->auth = $auth;\n" " return $this;\n" @@ -3542,23 +4228,17 @@ static const char* swoole_library_source_core_database_redis_pool = " {\n" " parent::__construct(function () {\n" " $redis = new \\Redis();\n" - " /* Compatible with different versions of Redis extension as much as possible */\n" - " $arguments = [\n" + " // Every argument is passed, in the position phpredis defines for it: leaving one out shifts the ones\n" + " // after it into the wrong parameters (a read timeout became the connect timeout, a retry interval\n" + " // landed in the persistent id). The zero defaults mean \"unset\" to phpredis, as they do to the config.\n" + " $redis->connect(\n" " $this->config->getHost(),\n" " $this->config->getPort(),\n" - " ];\n" - " if ($this->config->getTimeout() !== 0.0) {\n" - " $arguments[] = $this->config->getTimeout();\n" - " }\n" - " if ($this->config->getRetryInterval() !== 0) {\n" - " /* reserved should always be NULL */\n" - " $arguments[] = null;\n" - " $arguments[] = $this->config->getRetryInterval();\n" - " }\n" - " if ($this->config->getReadTimeout() !== 0.0) {\n" - " $arguments[] = $this->config->getReadTimeout();\n" - " }\n" - " $redis->connect(...$arguments);\n" + " $this->config->getTimeout(),\n" + " null, // persistent_id: a pooled connection is never persistent\n" + " $this->config->getRetryInterval(),\n" + " $this->config->getReadTimeout()\n" + " );\n" " if ($this->config->getAuth()) {\n" " $redis->auth($this->config->getAuth());\n" " }\n" @@ -3914,14 +4594,14 @@ static const char* swoole_library_source_core_curl_handler = " /** @var callable|null */\n" " private $readFunction;\n" "\n" - " /** @var callable|null */\n" - " private $writeFunction;\n" - "\n" " private $noProgress = true;\n" "\n" - " /** @var callable */\n" + " /** @var callable|null */\n" " private $progressFunction;\n" "\n" + " /** @var callable|null */\n" + " private $prereqFunction;\n" + "\n" " private $returnTransfer = false;\n" "\n" " private $method = '';\n" @@ -4442,13 +5122,7 @@ static const char* swoole_library_source_core_curl_handler = " $this->readFunction = $value;\n" " break;\n" " case CURLOPT_WRITEFUNCTION:\n" - " if (SWOOLE_VERSION_ID >= 50100) {\n" - " $this->clientOptions[Constant::OPTION_WRITE_FUNC] = function ($client, $data) use ($value) {\n" - " return $value($this, $data);\n" - " };\n" - " } else {\n" - " $this->writeFunction = $value;\n" - " }\n" + " $this->clientOptions[Constant::OPTION_WRITE_FUNC] = fn ($client, $data) => $value($this, $data);\n" " break;\n" " case CURLOPT_NOPROGRESS:\n" " $this->noProgress = $value;\n" @@ -4456,6 +5130,9 @@ static const char* swoole_library_source_core_curl_handler = " case CURLOPT_PROGRESSFUNCTION:\n" " $this->progressFunction = $value;\n" " break;\n" + " case CURLOPT_PREREQFUNCTION:\n" + " $this->prereqFunction = $value;\n" + " break;\n" " case CURLOPT_HTTPAUTH:\n" " if (!($value & CURLAUTH_BASIC)) {\n" " trigger_error(\"swoole_curl_setopt(): CURLOPT_HTTPAUTH[{$value}] is not supported\", E_USER_WARNING);\n" @@ -4613,6 +5290,13 @@ static const char* swoole_library_source_core_curl_handler = " // As much as possible to ensure that Host is the first header.\n" " // See: http://tools.ietf.org/html/rfc7230#section-5.4\n" " $client->setHeaders($this->headers);\n" + " /*\n" + " * Pre-request Callback\n" + " */\n" + " if ($this->prereqFunction && !$this->invokePrereqFunction($proxy ?? null, $proxyPort ?? null)) {\n" + " $this->info['total_time'] = microtime(true) - $timeBegin;\n" + " return false;\n" + " }\n" " /**\n" " * Execute.\n" " */\n" @@ -4740,16 +5424,6 @@ static const char* swoole_library_source_core_curl_handler = " }\n" " }\n" "\n" - " if ($this->writeFunction) {\n" - " if (!is_callable($this->writeFunction)) { // @phpstan-ignore booleanNot.alwaysFalse\n" - " trigger_error('curl_exec(): Could not call the CURLOPT_WRITEFUNCTION', E_USER_WARNING);\n" - " $this->setError(CURLE_WRITE_ERROR, 'Failure writing output to destination');\n" - " return false;\n" - " }\n" - " call_user_func($this->writeFunction, $this, $transfer);\n" - " return true;\n" - " }\n" - "\n" " if ($this->returnTransfer) {\n" " return $this->transfer = $transfer;\n" " }\n" @@ -4761,6 +5435,69 @@ static const char* swoole_library_source_core_curl_handler = " return true;\n" " }\n" "\n" + " /**\n" + " * Invokes the CURLOPT_PREREQFUNCTION callback right before the request is sent out.\n" + " *\n" + " * Native cURL fires this callback once the connection is fully established. The coroutine HTTP client\n" + " * connects and sends within a single call, so on a fresh connection the callback fires before the\n" + " * connection exists: the local address is reported as an empty string with port 0, and the primary\n" + " * address is the resolved address the client is about to connect to. When an established connection\n" + " * is reused, the real socket addresses are reported.\n" + " *\n" + " * @return bool false when the transfer must be aborted; the error has been recorded already\n" + " */\n" + " private function invokePrereqFunction(?string $proxyIp, ?int $proxyPort): bool\n" + " {\n" + " $primaryIp = '';\n" + " $primaryPort = 0;\n" + " $localIp = '';\n" + " $localPort = 0;\n" + " if ($this->client->connected) {\n" + " $peer = $this->client->getpeername();\n" + " if (is_array($peer)) {\n" + " $primaryIp = $peer['address'];\n" + " $primaryPort = $peer['port'];\n" + " }\n" + " $sock = $this->client->getsockname();\n" + " if (is_array($sock)) {\n" + " $localIp = $sock['address'];\n" + " $localPort = $sock['port'];\n" + " }\n" + " } elseif ($this->unix_socket_path) {\n" + " $primaryIp = $this->unix_socket_path;\n" + " $primaryPort = $this->urlInfo['port'];\n" + " } elseif ($proxyIp !== null) {\n" + " // The connection is made to the proxy; native cURL reports the proxy as the connection peer.\n" + " $primaryIp = $proxyIp;\n" + " $primaryPort = (int) $proxyPort;\n" + " } else {\n" + " $host = $this->urlInfo['host'];\n" + " if (filter_var($host, FILTER_VALIDATE_IP)) {\n" + " $primaryIp = $host;\n" + " } else {\n" + " $ip = System::gethostbyname($host, AF_INET, $this->clientOptions[Constant::OPTION_CONNECT_TIMEOUT] ?? -1);\n" + " if (!$ip) {\n" + " // Native cURL never invokes the callback when the connection cannot be established;\n" + " // let Client::execute() fail with the canonical DNS error.\n" + " return true;\n" + " }\n" + " $primaryIp = $ip;\n" + " }\n" + " $primaryPort = $this->urlInfo['port'];\n" + " }\n" + "\n" + " $retval = ($this->prereqFunction)($this, $primaryIp, $localIp, $primaryPort, $localPort);\n" + " if ($retval === CURL_PREREQFUNC_OK) {\n" + " return true;\n" + " }\n" + " $this->setError(CURLE_ABORTED_BY_CALLBACK, 'operation aborted by pre-request callback');\n" + " if ($retval === CURL_PREREQFUNC_ABORT) {\n" + " return false;\n" + " }\n" + " $message = 'The CURLOPT_PREREQFUNCTION callback must return either CURL_PREREQFUNC_OK or CURL_PREREQFUNC_ABORT';\n" + " throw is_int($retval) ? new \\ValueError($message) : new \\TypeError($message);\n" + " }\n" + "\n" " /* ====== Redirect helper ====== */\n" "\n" " private static function unparseUrl(array $parsedUrl): string\n" @@ -5188,59 +5925,20 @@ static const char* swoole_library_source_core_fast_cgi_record_params = " protected static function unpackPayload(Record $self, string $binaryData): void\n" " {\n" " assert($self instanceof self); // @phpstan-ignore function.alreadyNarrowedType,instanceof.alwaysTrue\n" - " $currentOffset = 0;\n" - " do {\n" - " /** @phpstan-var false|array{nameLengthHigh: int} */\n" - " $payload = unpack('CnameLengthHigh', $binaryData);\n" - " if ($payload === false) {\n" + " // Record::unpack() has already checked that the buffer holds the whole content (and the padding), so every\n" + " // read below is bounded by the content length alone. Decoding by offset, instead of cutting the rest of the\n" + " // buffer off for every pair, keeps the cost linear in the payload size.\n" + " $contentLength = $self->getContentLength();\n" + " $offset = 0;\n" + " while ($offset < $contentLength) {\n" + " $nameLength = self::unpackLength($binaryData, $contentLength, $offset);\n" + " $valueLength = self::unpackLength($binaryData, $contentLength, $offset);\n" + " if ($offset + $nameLength + $valueLength > $contentLength) {\n" " throw new \\RuntimeException('Can not unpack data from the binary buffer');\n" " }\n" - " [$nameLengthHigh] = array_values($payload);\n" - " $isLongName = ($nameLengthHigh >> 7 == 1);\n" - " $valueOffset = $isLongName ? 4 : 1;\n" - "\n" - " /** @phpstan-var false|array{valueLengthHigh: int} */\n" - " $payload = unpack('CvalueLengthHigh', substr($binaryData, $valueOffset));\n" - " if ($payload === false) {\n" - " throw new \\RuntimeException('Can not unpack data from the binary buffer');\n" - " }\n" - " [$valueLengthHigh] = array_values($payload);\n" - " $isLongValue = ($valueLengthHigh >> 7 == 1);\n" - " $dataOffset = $valueOffset + ($isLongValue ? 4 : 1);\n" - "\n" - " $formatParts = [\n" - " $isLongName ? 'NnameLength' : 'CnameLength',\n" - " $isLongValue ? 'NvalueLength' : 'CvalueLength',\n" - " ];\n" - " $format = join('/', $formatParts);\n" - "\n" - " /** @phpstan-var false|array{nameLength: int, valueLength: int} */\n" - " $payload = unpack($format, $binaryData);\n" - " if ($payload === false) {\n" - " throw new \\RuntimeException('Can not unpack data from the binary buffer');\n" - " }\n" - " [$nameLength, $valueLength] = array_values($payload);\n" - "\n" - " // Clear top bit for long record\n" - " $nameLength &= ($isLongName ? 0x7FFFFFFF : 0x7F);\n" - " $valueLength &= ($isLongValue ? 0x7FFFFFFF : 0x7F);\n" - "\n" - " /** @phpstan-var false|array{nameData: string, valueData: string} */\n" - " $payload = unpack(\n" - " \"a{$nameLength}nameData/a{$valueLength}valueData\",\n" - " substr($binaryData, $dataOffset)\n" - " );\n" - " if ($payload === false) {\n" - " throw new \\RuntimeException('Can not unpack data from the binary buffer');\n" - " }\n" - " [$nameData, $valueData] = array_values($payload);\n" - "\n" - " $self->values[$nameData] = $valueData;\n" - "\n" - " $keyValueLength = $dataOffset + $nameLength + $valueLength;\n" - " $binaryData = substr($binaryData, $keyValueLength);\n" - " $currentOffset += $keyValueLength;\n" - " } while ($currentOffset < $self->getContentLength());\n" + " $self->values[substr($binaryData, $offset, $nameLength)] = substr($binaryData, $offset + $nameLength, $valueLength);\n" + " $offset += $nameLength + $valueLength;\n" + " }\n" " }\n" "\n" " /**\n" @@ -5253,30 +5951,45 @@ static const char* swoole_library_source_core_fast_cgi_record_params = " if ($valueData === null) { // @phpstan-ignore identical.alwaysFalse\n" " continue;\n" " }\n" + " $valueData = (string) $valueData;\n" " $nameLength = strlen($nameData);\n" - " $valueLength = strlen((string) $valueData);\n" - " $isLongName = $nameLength > 127;\n" - " $isLongValue = $valueLength > 127;\n" - " $formatParts = [\n" - " $isLongName ? 'N' : 'C',\n" - " $isLongValue ? 'N' : 'C',\n" - " \"a{$nameLength}\",\n" - " \"a{$valueLength}\",\n" - " ];\n" - "\n" - " $format = join('', $formatParts);\n" - "\n" - " $payload .= pack(\n" - " $format,\n" - " $isLongName ? ($nameLength | 0x80000000) : $nameLength,\n" - " $isLongValue ? ($valueLength | 0x80000000) : $valueLength,\n" - " $nameData,\n" - " $valueData\n" - " );\n" + " $valueLength = strlen($valueData);\n" + " // Each length is one byte up to 127, and four bytes with the top bit set above that. The name and the\n" + " // value follow as they are, so they are appended directly rather than copied through pack(\"a{n}\").\n" + " $payload .= ($nameLength > 127 ? pack('N', $nameLength | 0x80000000) : chr($nameLength))\n" + " . ($valueLength > 127 ? pack('N', $valueLength | 0x80000000) : chr($valueLength))\n" + " . $nameData\n" + " . $valueData;\n" " }\n" "\n" " return $payload;\n" " }\n" + "\n" + " /**\n" + " * Reads the length at $offset, one byte up to 127 or four bytes with the top bit set above that, and moves\n" + " * $offset past it.\n" + " */\n" + " private static function unpackLength(string $binaryData, int $contentLength, int &$offset): int\n" + " {\n" + " if ($offset >= $contentLength) {\n" + " throw new \\RuntimeException('Can not unpack data from the binary buffer');\n" + " }\n" + " $length = ord($binaryData[$offset]);\n" + " if ($length >> 7 === 0) {\n" + " $offset++;\n" + " return $length;\n" + " }\n" + " if ($offset + 4 > $contentLength) {\n" + " throw new \\RuntimeException('Can not unpack data from the binary buffer');\n" + " }\n" + " /** @phpstan-var false|array{1: int} */\n" + " $payload = unpack('N', $binaryData, $offset);\n" + " if ($payload === false) {\n" + " throw new \\RuntimeException('Can not unpack data from the binary buffer');\n" + " }\n" + " $offset += 4;\n" + " return $payload[1] & 0x7FFFFFFF;\n" + " }\n" "}\n"; static const char* swoole_library_source_core_fast_cgi_record_abort_request = @@ -6062,7 +6775,7 @@ static const char* swoole_library_source_core_fast_cgi_request = " $beginRequestFrame = new BeginRequest(FastCGI::RESPONDER, $this->keepConn ? FastCGI::KEEP_CONN : 0);\n" " $paramsFrame = new Params($this->getParams());\n" " $paramsEofFrame = new Params([]);\n" - " if (empty($body)) {\n" + " if ($body === '') {\n" " $message = \"{$beginRequestFrame}{$paramsFrame}{$paramsEofFrame}\";\n" " } else {\n" " $stdinList = [];\n" @@ -6621,6 +7334,9 @@ static const char* swoole_library_source_core_fast_cgi_http_response = " parent::__construct($records);\n" " $body = $this->getBody();\n" " if (strlen($body) === 0) {\n" + " /* An empty FastCGI response carries no HTTP payload at all; report it the same way as a malformed one\n" + " * so that the accessors are always safe to call. */\n" + " $this->withStatusCode(Status::BAD_GATEWAY)->withReasonPhrase('Invalid FastCGI Response');\n" " return;\n" " }\n" " $array = explode(\"\\r\\n\\r\\n\", $body, 2); // An array that contains the HTTP headers and the body.\n" @@ -7525,7 +8241,7 @@ static const char* swoole_library_source_core_remote_object_server = " public function onRequest(Request $request, Response $response): void\n" " {\n" " $ctx = new Context($request, $response);\n" - " if ($this->apiKey and $this->apiKey !== $request->header['x-api-key']) {\n" + " if ($this->apiKey and $this->apiKey !== ($request->header['x-api-key'] ?? '')) {\n" " $response->status(403);\n" " $ctx->end(['code' => -3, 'msg' => 'invalid api key']);\n" " return;\n" @@ -7846,16 +8562,43 @@ static const char* swoole_library_source_core_remote_object_client = "namespace Swoole\\RemoteObject;\n" "\n" "use Swoole\\Coroutine;\n" + "use Swoole\\Coroutine\\Channel;\n" "use Swoole\\Coroutine\\Http\\Client as HttpClient;\n" "use Swoole\\RemoteObject;\n" "\n" "class Client\n" "{\n" + " /**\n" + " * The live clients, keyed by client id.\n" + " *\n" + " * The references are weak on purpose. A RemoteObject holds a strong reference to the client it came\n" + " * from, so a client stays reachable here for exactly as long as one of its remote objects is alive,\n" + " * which is the lifetime RemoteObject::__destruct() needs to be able to send /destroy. A client nobody\n" + " * else holds on to is freed right away instead of being pinned for the lifetime of the process.\n" + " *\n" + " * @var array\n" + " */\n" " private static array $clients = [];\n" "\n" " private HttpClient $client;\n" "\n" - " private string $id;\n" + " /**\n" + " * Serializes the remote calls made through this client.\n" + " *\n" + " * The HTTP connection underneath can only be driven by one coroutine at a time: a second coroutine using\n" + " * it while a request is in flight is a fatal \"Socket has already been bound to another coroutine\". A client\n" + " * is routinely shared, though: by a service object handling concurrent requests, and by every remote object\n" + " * it created, whose destructor sends a /destroy from whichever coroutine drops the last reference.\n" + " */\n" + " private Channel $lock;\n" + "\n" + " /**\n" + " * The coroutine currently holding the lock, if any.\n" + " */\n" + " private int $lockOwner = -1;\n" + "\n" + " // Has a default value: __destruct() also runs on an instance built without the constructor.\n" + " private string $id = '';\n" "\n" " private int $ownerCoroutineId;\n" "\n" @@ -7863,6 +8606,7 @@ static const char* swoole_library_source_core_remote_object_client = " {\n" " $this->id = $this->genUuid();\n" " $this->client = new HttpClient($host, $port);\n" + " $this->lock = new Channel(1);\n" " $this->ownerCoroutineId = Coroutine::getCid();\n" "\n" " $headers = [\n" @@ -7928,14 +8672,36 @@ static const char* swoole_library_source_core_remote_object_client = "\n" " public function execute(string $path, array $array)\n" " {\n" - " $rs = $this->client->post($path, $array);\n" - " if (!$rs) {\n" - " throw new Exception($this->client->errMsg);\n" + " $cid = Coroutine::getCid();\n" + " // Outside a coroutine both sides are -1. The call goes on to the lock below, where Channel::push() ends it\n" + " // with the fatal error \"API must be called in the coroutine\".\n" + " if ($cid !== -1 && $this->lockOwner === $cid) {\n" + " // Only reachable from a destructor the garbage collector runs in the middle of a call of this same\n" + " // coroutine. Waiting would be waiting for itself.\n" + " throw new Exception('The remote object client is already in use by the current coroutine');\n" + " }\n" + " if (!$this->lock->push(true)) {\n" + " // The wait was cancelled (Coroutine::cancel()) or the channel closed; the lock was never acquired, so\n" + " // this coroutine must not run its call alongside the holder, nor release the holder's lock in finally.\n" + " throw new Exception('Cancelled while waiting for the remote object client', $this->lock->errCode);\n" + " }\n" + " $this->lockOwner = $cid;\n" + " try {\n" + " $rs = $this->client->post($path, $array);\n" + " if (!$rs) {\n" + " throw new Exception($this->client->errMsg);\n" + " }\n" + " // A body that does not unserialize is reported below, as an exception; the warning would only come first.\n" + " $result = @unserialize($this->client->body);\n" + " } finally {\n" + " $this->lockOwner = -1;\n" + " $this->lock->pop();\n" + " }\n" + " if (!is_array($result) || !array_key_exists('code', $result)) {\n" + " throw new Exception('Malformed response from the remote object server');\n" " }\n" - " $result = unserialize($this->client->body);\n" " if ($result['code'] != 0) {\n" - " $ex = $result['exception'];\n" - " throw new Exception('Server Error: ' . $ex['message'], $ex['code']);\n" + " throw Exception::fromResponse($result);\n" " }\n" " return $result;\n" " }\n" @@ -7975,6 +8741,47 @@ static const char* swoole_library_source_core_remote_object_exception = "\n" "class Exception extends \\RuntimeException\n" "{\n" + " private ?string $remoteClass = null;\n" + "\n" + " private int|string|null $remoteCode = null;\n" + "\n" + " /**\n" + " * Builds the exception for an error response of the remote object server, i.e. one whose code is not 0.\n" + " *\n" + " * The server answers its own errors with a message under 'msg' (-1 invalid request, -3 invalid API key), and\n" + " * an exception it caught (-2) with that exception's message, code and class under 'exception'. The relayed\n" + " * code is not always an integer (a PDOException carries its SQLSTATE, e.g. \"42S02\"), and a PHP exception code\n" + " * has to be one, so it is kept as it is in getRemoteCode() and used as getCode() only when it is an integer.\n" + " */\n" + " public static function fromResponse(array $response): self\n" + " {\n" + " $ex = $response['exception'] ?? null;\n" + " if (!is_array($ex)) {\n" + " return new self('Server Error: ' . ($response['msg'] ?? 'unknown error'), (int) ($response['code'] ?? 0));\n" + " }\n" + "\n" + " $code = $ex['code'] ?? 0;\n" + " $e = new self('Server Error: ' . ($ex['message'] ?? ''), is_int($code) ? $code : 0);\n" + " $e->remoteClass = isset($ex['class']) ? (string) $ex['class'] : null;\n" + " $e->remoteCode = is_int($code) || is_string($code) ? $code : null;\n" + " return $e;\n" + " }\n" + "\n" + " /**\n" + " * The class of the exception the server caught, or null when the error is the server's own.\n" + " */\n" + " public function getRemoteClass(): ?string\n" + " {\n" + " return $this->remoteClass;\n" + " }\n" + "\n" + " /**\n" + " * The code of the exception the server caught, as it was, or null when the error is the server's own.\n" + " */\n" + " public function getRemoteCode(): int|string|null\n" + " {\n" + " return $this->remoteCode;\n" + " }\n" "}\n"; static const char* swoole_library_source_core_remote_object_proxy_trait = @@ -8944,7 +9751,7 @@ static const char* swoole_library_source_core_server_admin = " return self::json(['error' => 'Object destroyed!'], 4004);\n" " }\n" "\n" - " $object_hash = spl_object_hash($object); // @phpstan-ignore argument.type\n" + " $object_hash = spl_object_hash($object);\n" " if ($object_hash != $json['object_hash']) {\n" " return self::json(['error' => 'Object destroyed!'], 4004);\n" " }\n" @@ -9403,191 +10210,192 @@ static const char* swoole_library_source_core_server_helper = " public const STATS_TIMER_INTERVAL_TIME = 1000;\n" "\n" " public const GLOBAL_OPTIONS = [\n" - " 'debug_mode' => true,\n" - " 'trace_flags' => true,\n" - " 'log_file' => true,\n" - " 'log_level' => true,\n" - " 'log_date_format' => true,\n" - " 'log_date_with_microseconds' => true,\n" - " 'log_rotation' => true,\n" - " 'display_errors' => true,\n" - " 'dns_server' => true,\n" - " 'socket_dns_timeout' => true,\n" - " 'socket_connect_timeout' => true,\n" - " 'socket_write_timeout' => true,\n" - " 'socket_send_timeout' => true,\n" - " 'socket_read_timeout' => true,\n" - " 'socket_recv_timeout' => true,\n" - " 'socket_buffer_size' => true,\n" - " 'socket_timeout' => true,\n" - " 'http2_header_table_size' => true,\n" - " 'http2_enable_push' => true,\n" - " 'http2_max_concurrent_streams' => true,\n" - " 'http2_init_window_size' => true,\n" - " 'http2_max_frame_size' => true,\n" - " 'http2_max_header_list_size' => true,\n" + " Constant::OPTION_DEBUG_MODE => true,\n" + " Constant::OPTION_TRACE_FLAGS => true,\n" + " Constant::OPTION_LOG_FILE => true,\n" + " Constant::OPTION_LOG_LEVEL => true,\n" + " Constant::OPTION_LOG_DATE_FORMAT => true,\n" + " Constant::OPTION_LOG_DATE_WITH_MICROSECONDS => true,\n" + " Constant::OPTION_LOG_ROTATION => true,\n" + " Constant::OPTION_DISPLAY_ERRORS => true,\n" + " Constant::OPTION_DNS_SERVER => true,\n" + " Constant::OPTION_SOCKET_DNS_TIMEOUT => true,\n" + " Constant::OPTION_SOCKET_CONNECT_TIMEOUT => true,\n" + " Constant::OPTION_SOCKET_WRITE_TIMEOUT => true,\n" + " Constant::OPTION_SOCKET_SEND_TIMEOUT => true,\n" + " Constant::OPTION_SOCKET_READ_TIMEOUT => true,\n" + " Constant::OPTION_SOCKET_RECV_TIMEOUT => true,\n" + " Constant::OPTION_SOCKET_BUFFER_SIZE => true,\n" + " Constant::OPTION_SOCKET_TIMEOUT => true,\n" + " Constant::OPTION_HTTP2_HEADER_TABLE_SIZE => true,\n" + " Constant::OPTION_HTTP2_ENABLE_PUSH => true,\n" + " Constant::OPTION_HTTP2_MAX_CONCURRENT_STREAMS => true,\n" + " Constant::OPTION_HTTP2_INIT_WINDOW_SIZE => true,\n" + " Constant::OPTION_HTTP2_MAX_FRAME_SIZE => true,\n" + " Constant::OPTION_HTTP2_MAX_HEADER_LIST_SIZE => true,\n" + " Constant::OPTION_HTTP2_MAX_HEADERS => true,\n" " ];\n" "\n" " public const SERVER_OPTIONS = [\n" - " 'chroot' => true,\n" - " 'user' => true,\n" - " 'group' => true,\n" - " 'daemonize' => true,\n" - " 'pid_file' => true,\n" - " 'reactor_num' => true,\n" - " 'single_thread' => true,\n" - " 'worker_num' => true,\n" - " 'max_wait_time' => true,\n" - " 'max_queued_bytes' => true,\n" - " 'max_concurrency' => true,\n" - " 'worker_max_concurrency' => true,\n" - " 'enable_coroutine' => true,\n" - " 'send_timeout' => true,\n" - " 'dispatch_mode' => true,\n" - " 'send_yield' => true,\n" - " 'dispatch_func' => true,\n" - " 'discard_timeout_request' => true,\n" - " 'enable_unsafe_event' => true,\n" - " 'enable_delay_receive' => true,\n" - " 'enable_reuse_port' => true,\n" - " 'task_use_object' => true,\n" - " 'task_object' => true,\n" - " 'event_object' => true,\n" - " 'task_enable_coroutine' => true,\n" - " 'task_worker_num' => true,\n" - " 'task_ipc_mode' => true,\n" - " 'task_tmpdir' => true,\n" - " 'task_max_request' => true,\n" - " 'task_max_request_grace' => true,\n" - " 'max_connection' => true,\n" - " 'max_conn' => true,\n" - " 'start_session_id' => true,\n" - " 'heartbeat_check_interval' => true,\n" - " 'heartbeat_idle_time' => true,\n" - " 'max_request' => true,\n" - " 'max_request_grace' => true,\n" - " 'reload_async' => true,\n" - " 'open_cpu_affinity' => true,\n" - " 'cpu_affinity_ignore' => true,\n" - " 'http_parse_cookie' => true,\n" - " 'http_parse_post' => true,\n" - " 'http_parse_files' => true,\n" - " 'http_compression' => true,\n" - " 'http_compression_level' => true,\n" - " 'compression_level' => true,\n" - " 'http_gzip_level' => true,\n" - " 'http_compression_min_length' => true,\n" - " 'compression_min_length' => true,\n" - " 'websocket_compression' => true,\n" - " 'upload_tmp_dir' => true,\n" - " 'upload_max_filesize' => true,\n" - " 'enable_static_handler' => true,\n" - " 'document_root' => true,\n" - " 'http_autoindex' => true,\n" - " 'http_index_files' => true,\n" - " 'http_compression_types' => true,\n" - " 'compression_types' => true,\n" - " 'static_handler_locations' => true,\n" - " 'input_buffer_size' => true,\n" - " 'buffer_input_size' => true,\n" - " 'output_buffer_size' => true,\n" - " 'buffer_output_size' => true,\n" - " 'message_queue_key' => true,\n" - " 'bootstrap' => true,\n" - " 'init_arguments' => true,\n" - " 'url_rewrite_rules' => true,\n" + " Constant::OPTION_CHROOT => true,\n" + " Constant::OPTION_USER => true,\n" + " Constant::OPTION_GROUP => true,\n" + " Constant::OPTION_DAEMONIZE => true,\n" + " Constant::OPTION_PID_FILE => true,\n" + " Constant::OPTION_REACTOR_NUM => true,\n" + " Constant::OPTION_SINGLE_THREAD => true,\n" + " Constant::OPTION_WORKER_NUM => true,\n" + " Constant::OPTION_MAX_WAIT_TIME => true,\n" + " Constant::OPTION_MAX_QUEUED_BYTES => true,\n" + " Constant::OPTION_MAX_CONCURRENCY => true,\n" + " Constant::OPTION_WORKER_MAX_CONCURRENCY => true,\n" + " Constant::OPTION_ENABLE_COROUTINE => true,\n" + " Constant::OPTION_SEND_TIMEOUT => true,\n" + " Constant::OPTION_DISPATCH_MODE => true,\n" + " Constant::OPTION_SEND_YIELD => true,\n" + " Constant::OPTION_DISPATCH_FUNC => true,\n" + " Constant::OPTION_DISCARD_TIMEOUT_REQUEST => true,\n" + " Constant::OPTION_ENABLE_UNSAFE_EVENT => true,\n" + " Constant::OPTION_ENABLE_DELAY_RECEIVE => true,\n" + " Constant::OPTION_ENABLE_REUSE_PORT => true,\n" + " Constant::OPTION_TASK_USE_OBJECT => true,\n" + " Constant::OPTION_TASK_OBJECT => true,\n" + " Constant::OPTION_EVENT_OBJECT => true,\n" + " Constant::OPTION_TASK_ENABLE_COROUTINE => true,\n" + " Constant::OPTION_TASK_WORKER_NUM => true,\n" + " Constant::OPTION_TASK_IPC_MODE => true,\n" + " Constant::OPTION_TASK_TMPDIR => true,\n" + " Constant::OPTION_TASK_MAX_REQUEST => true,\n" + " Constant::OPTION_TASK_MAX_REQUEST_GRACE => true,\n" + " Constant::OPTION_MAX_CONNECTION => true,\n" + " Constant::OPTION_MAX_CONN => true,\n" + " Constant::OPTION_START_SESSION_ID => true,\n" + " Constant::OPTION_HEARTBEAT_CHECK_INTERVAL => true,\n" + " Constant::OPTION_HEARTBEAT_IDLE_TIME => true,\n" + " Constant::OPTION_MAX_REQUEST => true,\n" + " Constant::OPTION_MAX_REQUEST_GRACE => true,\n" + " Constant::OPTION_RELOAD_ASYNC => true,\n" + " Constant::OPTION_OPEN_CPU_AFFINITY => true,\n" + " Constant::OPTION_CPU_AFFINITY_IGNORE => true,\n" + " Constant::OPTION_HTTP_PARSE_COOKIE => true,\n" + " Constant::OPTION_HTTP_PARSE_POST => true,\n" + " Constant::OPTION_HTTP_PARSE_FILES => true,\n" + " Constant::OPTION_HTTP_COMPRESSION => true,\n" + " Constant::OPTION_HTTP_COMPRESSION_LEVEL => true,\n" + " Constant::OPTION_COMPRESSION_LEVEL => true,\n" + " Constant::OPTION_HTTP_GZIP_LEVEL => true,\n" + " Constant::OPTION_HTTP_COMPRESSION_MIN_LENGTH => true,\n" + " Constant::OPTION_COMPRESSION_MIN_LENGTH => true,\n" + " Constant::OPTION_WEBSOCKET_COMPRESSION => true,\n" + " Constant::OPTION_UPLOAD_TMP_DIR => true,\n" + " Constant::OPTION_UPLOAD_MAX_FILESIZE => true,\n" + " Constant::OPTION_ENABLE_STATIC_HANDLER => true,\n" + " Constant::OPTION_DOCUMENT_ROOT => true,\n" + " Constant::OPTION_HTTP_AUTOINDEX => true,\n" + " Constant::OPTION_HTTP_INDEX_FILES => true,\n" + " Constant::OPTION_HTTP_COMPRESSION_TYPES => true,\n" + " Constant::OPTION_COMPRESSION_TYPES => true,\n" + " Constant::OPTION_STATIC_HANDLER_LOCATIONS => true,\n" + " Constant::OPTION_INPUT_BUFFER_SIZE => true,\n" + " Constant::OPTION_BUFFER_INPUT_SIZE => true,\n" + " Constant::OPTION_OUTPUT_BUFFER_SIZE => true,\n" + " Constant::OPTION_BUFFER_OUTPUT_SIZE => true,\n" + " Constant::OPTION_MESSAGE_QUEUE_KEY => true,\n" + " Constant::OPTION_BOOTSTRAP => true,\n" + " Constant::OPTION_INIT_ARGUMENTS => true,\n" + " Constant::OPTION_URL_REWRITE_RULES => true,\n" " ];\n" "\n" " public const PORT_OPTIONS = [\n" - " 'ssl_cert_file' => true,\n" - " 'ssl_key_file' => true,\n" - " 'backlog' => true,\n" - " 'socket_buffer_size' => true,\n" - " 'kernel_socket_recv_buffer_size' => true,\n" - " 'kernel_socket_send_buffer_size' => true,\n" - " 'heartbeat_idle_time' => true,\n" - " 'buffer_high_watermark' => true,\n" - " 'buffer_low_watermark' => true,\n" - " 'open_tcp_nodelay' => true,\n" - " 'tcp_defer_accept' => true,\n" - " 'open_tcp_keepalive' => true,\n" - " 'open_eof_check' => true,\n" - " 'open_eof_split' => true,\n" - " 'package_eof' => true,\n" - " 'open_http_protocol' => true,\n" - " 'open_websocket_protocol' => true,\n" - " 'websocket_subprotocol' => true,\n" - " 'open_websocket_close_frame' => true,\n" - " 'open_websocket_ping_frame' => true,\n" - " 'open_websocket_pong_frame' => true,\n" - " 'open_http2_protocol' => true,\n" - " 'open_mqtt_protocol' => true,\n" - " 'open_redis_protocol' => true,\n" - " 'max_idle_time' => true,\n" - " 'tcp_keepidle' => true,\n" - " 'tcp_keepinterval' => true,\n" - " 'tcp_keepcount' => true,\n" - " 'tcp_user_timeout' => true,\n" - " 'tcp_fastopen' => true,\n" - " 'open_length_check' => true,\n" - " 'package_length_type' => true,\n" - " 'package_length_offset' => true,\n" - " 'package_body_offset' => true,\n" - " 'package_body_start' => true,\n" - " 'package_length_func' => true,\n" - " 'package_max_length' => true,\n" - " 'ssl_compress' => true,\n" - " 'ssl_protocols' => true,\n" - " 'ssl_verify_peer' => true,\n" - " 'ssl_allow_self_signed' => true,\n" - " 'ssl_client_cert_file' => true,\n" - " 'ssl_cafile' => true,\n" - " 'ssl_capath' => true,\n" - " 'ssl_verify_depth' => true,\n" - " 'ssl_prefer_server_ciphers' => true,\n" - " 'ssl_ciphers' => true,\n" - " 'ssl_ecdh_curve' => true,\n" - " 'ssl_dhparam' => true,\n" - " 'ssl_sni_certs' => true,\n" + " Constant::OPTION_SSL_CERT_FILE => true,\n" + " Constant::OPTION_SSL_KEY_FILE => true,\n" + " Constant::OPTION_BACKLOG => true,\n" + " Constant::OPTION_SOCKET_BUFFER_SIZE => true,\n" + " Constant::OPTION_KERNEL_SOCKET_RECV_BUFFER_SIZE => true,\n" + " Constant::OPTION_KERNEL_SOCKET_SEND_BUFFER_SIZE => true,\n" + " Constant::OPTION_HEARTBEAT_IDLE_TIME => true,\n" + " Constant::OPTION_BUFFER_HIGH_WATERMARK => true,\n" + " Constant::OPTION_BUFFER_LOW_WATERMARK => true,\n" + " Constant::OPTION_OPEN_TCP_NODELAY => true,\n" + " Constant::OPTION_TCP_DEFER_ACCEPT => true,\n" + " Constant::OPTION_OPEN_TCP_KEEPALIVE => true,\n" + " Constant::OPTION_OPEN_EOF_CHECK => true,\n" + " Constant::OPTION_OPEN_EOF_SPLIT => true,\n" + " Constant::OPTION_PACKAGE_EOF => true,\n" + " Constant::OPTION_OPEN_HTTP_PROTOCOL => true,\n" + " Constant::OPTION_OPEN_WEBSOCKET_PROTOCOL => true,\n" + " Constant::OPTION_WEBSOCKET_SUBPROTOCOL => true,\n" + " Constant::OPTION_OPEN_WEBSOCKET_CLOSE_FRAME => true,\n" + " Constant::OPTION_OPEN_WEBSOCKET_PING_FRAME => true,\n" + " Constant::OPTION_OPEN_WEBSOCKET_PONG_FRAME => true,\n" + " Constant::OPTION_OPEN_HTTP2_PROTOCOL => true,\n" + " Constant::OPTION_OPEN_MQTT_PROTOCOL => true,\n" + " Constant::OPTION_OPEN_REDIS_PROTOCOL => true,\n" + " Constant::OPTION_MAX_IDLE_TIME => true,\n" + " Constant::OPTION_TCP_KEEPIDLE => true,\n" + " Constant::OPTION_TCP_KEEPINTERVAL => true,\n" + " Constant::OPTION_TCP_KEEPCOUNT => true,\n" + " Constant::OPTION_TCP_USER_TIMEOUT => true,\n" + " Constant::OPTION_TCP_FASTOPEN => true,\n" + " Constant::OPTION_OPEN_LENGTH_CHECK => true,\n" + " Constant::OPTION_PACKAGE_LENGTH_TYPE => true,\n" + " Constant::OPTION_PACKAGE_LENGTH_OFFSET => true,\n" + " Constant::OPTION_PACKAGE_BODY_OFFSET => true,\n" + " Constant::OPTION_PACKAGE_BODY_START => true,\n" + " Constant::OPTION_PACKAGE_LENGTH_FUNC => true,\n" + " Constant::OPTION_PACKAGE_MAX_LENGTH => true,\n" + " Constant::OPTION_SSL_COMPRESS => true,\n" + " Constant::OPTION_SSL_PROTOCOLS => true,\n" + " Constant::OPTION_SSL_VERIFY_PEER => true,\n" + " Constant::OPTION_SSL_ALLOW_SELF_SIGNED => true,\n" + " Constant::OPTION_SSL_CLIENT_CERT_FILE => true,\n" + " Constant::OPTION_SSL_CAFILE => true,\n" + " Constant::OPTION_SSL_CAPATH => true,\n" + " Constant::OPTION_SSL_VERIFY_DEPTH => true,\n" + " Constant::OPTION_SSL_PREFER_SERVER_CIPHERS => true,\n" + " Constant::OPTION_SSL_CIPHERS => true,\n" + " Constant::OPTION_SSL_ECDH_CURVE => true,\n" + " Constant::OPTION_SSL_DHPARAM => true,\n" + " Constant::OPTION_SSL_SNI_CERTS => true,\n" " ];\n" "\n" " public const AIO_OPTIONS = [\n" - " 'aio_core_worker_num' => true,\n" - " 'aio_worker_num' => true,\n" - " 'aio_max_wait_time' => true,\n" - " 'aio_max_idle_time' => true,\n" - " 'iouring_entries' => true,\n" - " 'iouring_workers' => true,\n" - " 'iouring_flag' => true,\n" - " 'enable_signalfd' => true,\n" - " 'wait_signal' => true,\n" - " 'dns_cache_refresh_time' => true,\n" - " 'thread_num' => true,\n" - " 'min_thread_num' => true,\n" - " 'max_thread_num' => true,\n" - " 'socket_dontwait' => true,\n" - " 'dns_lookup_random' => true,\n" - " 'use_async_resolver' => true,\n" - " 'enable_coroutine' => true,\n" + " Constant::OPTION_AIO_CORE_WORKER_NUM => true,\n" + " Constant::OPTION_AIO_WORKER_NUM => true,\n" + " Constant::OPTION_AIO_MAX_WAIT_TIME => true,\n" + " Constant::OPTION_AIO_MAX_IDLE_TIME => true,\n" + " Constant::OPTION_IOURING_ENTRIES => true,\n" + " Constant::OPTION_IOURING_WORKERS => true,\n" + " Constant::OPTION_IOURING_FLAG => true,\n" + " Constant::OPTION_ENABLE_SIGNALFD => true,\n" + " Constant::OPTION_WAIT_SIGNAL => true,\n" + " Constant::OPTION_DNS_CACHE_REFRESH_TIME => true,\n" + " Constant::OPTION_THREAD_NUM => true,\n" + " Constant::OPTION_MIN_THREAD_NUM => true,\n" + " Constant::OPTION_MAX_THREAD_NUM => true,\n" + " Constant::OPTION_SOCKET_DONTWAIT => true,\n" + " Constant::OPTION_DNS_LOOKUP_RANDOM => true,\n" + " Constant::OPTION_USE_ASYNC_RESOLVER => true,\n" + " Constant::OPTION_ENABLE_COROUTINE => true,\n" " ];\n" "\n" " public const COROUTINE_OPTIONS = [\n" - " 'max_coro_num' => true,\n" - " 'max_coroutine' => true,\n" - " 'enable_deadlock_check' => true,\n" - " 'hook_flags' => true,\n" - " 'enable_preemptive_scheduler' => true,\n" - " 'c_stack_size' => true,\n" - " 'stack_size' => true,\n" - " 'name_resolver' => true,\n" - " 'dns_cache_expire' => true,\n" - " 'dns_cache_capacity' => true,\n" + " Constant::OPTION_MAX_CORO_NUM => true,\n" + " Constant::OPTION_MAX_COROUTINE => true,\n" + " Constant::OPTION_ENABLE_DEADLOCK_CHECK => true,\n" + " Constant::OPTION_HOOK_FLAGS => true,\n" + " Constant::OPTION_ENABLE_PREEMPTIVE_SCHEDULER => true,\n" + " Constant::OPTION_C_STACK_SIZE => true,\n" + " Constant::OPTION_STACK_SIZE => true,\n" + " Constant::OPTION_NAME_RESOLVER => true,\n" + " Constant::OPTION_DNS_CACHE_EXPIRE => true,\n" + " Constant::OPTION_DNS_CACHE_CAPACITY => true,\n" " ];\n" "\n" " public const HELPER_OPTIONS = [\n" - " 'stats_file' => true,\n" - " 'stats_timer_interval' => true,\n" - " 'admin_server' => true,\n" + " Constant::OPTION_STATS_FILE => true,\n" + " Constant::OPTION_STATS_TIMER_INTERVAL => true,\n" + " Constant::OPTION_ADMIN_SERVER => true,\n" " ];\n" "\n" " public static function checkOptions(array $input_options): void\n" @@ -9606,27 +10414,27 @@ static const char* swoole_library_source_core_server_helper = "\n" " public static function onBeforeStart(Server $server): void\n" " {\n" - " if (!empty($server->setting['admin_server'])) {\n" + " if (!empty($server->setting[Constant::OPTION_ADMIN_SERVER])) {\n" " Admin::init($server);\n" " }\n" " }\n" "\n" " public static function onBeforeShutdown(Server $server): void\n" " {\n" - " if (isset($server->admin_server)) { // @phpstan-ignore isset.property\n" + " if (isset($server->admin_server)) {\n" " $server->admin_server->shutdown();\n" - " $server->admin_server = null; // @phpstan-ignore assign.propertyType\n" + " $server->admin_server = null;\n" " }\n" " }\n" "\n" " public static function onWorkerStart(Server $server, int $workerId): void\n" " {\n" - " if (!empty($server->setting['stats_file']) and $workerId == 0) {\n" - " $interval_ms = empty($server->setting['stats_timer_interval']) ? self::STATS_TIMER_INTERVAL_TIME : intval($server->setting['stats_timer_interval']);\n" + " if (!empty($server->setting[Constant::OPTION_STATS_FILE]) and $workerId == 0) {\n" + " $interval_ms = empty($server->setting[Constant::OPTION_STATS_TIMER_INTERVAL]) ? self::STATS_TIMER_INTERVAL_TIME : intval($server->setting[Constant::OPTION_STATS_TIMER_INTERVAL]);\n" "\n" " $server->stats_timer = Timer::tick($interval_ms, function () use ($server) {\n" " $stats = $server->stats();\n" - " $stats_file = swoole_string($server->setting['stats_file']);\n" + " $stats_file = swoole_string($server->setting[Constant::OPTION_STATS_FILE]);\n" " if ($stats_file->endsWith('.json')) {\n" " $out = json_encode($stats, JSON_THROW_ON_ERROR);\n" " } elseif ($stats_file->endsWith('.php')) {\n" @@ -9638,7 +10446,7 @@ static const char* swoole_library_source_core_server_helper = " }\n" " $out = implode(\"\\n\", $lines);\n" " }\n" - " file_put_contents($server->setting['stats_file'], $out);\n" + " file_put_contents($server->setting[Constant::OPTION_STATS_FILE], $out);\n" " });\n" " }\n" " }\n" @@ -10049,7 +10857,8 @@ static const char* swoole_library_source_core_name_resolver_nacos = " }\n" " $cluster = new Cluster();\n" " foreach ($result->hosts as $node) {\n" - " $cluster->add($node->ip, $node->port, $node->weight);\n" + " // Nacos reports the weight as a float (100.0).\n" + " $cluster->add($node->ip, $node->port, (int) $node->weight);\n" " }\n" " return $cluster;\n" " }\n" @@ -10492,7 +11301,7 @@ static const char* swoole_library_source_core_coroutine_functions = "\n" "function deadlock_check()\n" "{\n" - " $all_coroutines = Coroutine::listCoroutines();\n" + " $all_coroutines = Coroutine::list();\n" " $count = Coroutine::stats()['coroutine_num'];\n" "\n" " // coroutine deadlock detected, header\n" @@ -10514,7 +11323,7 @@ static const char* swoole_library_source_core_coroutine_functions = " foreach ($all_coroutines as $cid) {\n" " echo \"\\n [Coroutine-{$cid}]\";\n" " echo \"\\n {$hr2}\\n\";\n" - " echo Coroutine::printBackTrace($cid, DEBUG_BACKTRACE_IGNORE_ARGS, $depth);\n" + " Coroutine::printBackTrace($cid, DEBUG_BACKTRACE_IGNORE_ARGS, $depth);\n" " $index++;\n" " // limit the number of maximum outputs\n" " if ($index >= $limit) {\n" @@ -10829,10 +11638,10 @@ static const char* swoole_library_source_ext_sockets = " if ($socket->isClosed()) {\n" " return false;\n" " }\n" - " if (isset($socket->__ext_sockets_nonblock) and $socket->__ext_sockets_nonblock) {\n" - " $socket->setOption(SOL_SOCKET, SO_RCVTIMEO, $socket->__ext_sockets_timeout); // @phpstan-ignore property.notFound\n" + " if ($socket->__ext_sockets_nonblock) {\n" + " $socket->setOption(SOL_SOCKET, SO_RCVTIMEO, $socket->__ext_sockets_timeout);\n" " }\n" - " $socket->__ext_sockets_nonblock = false; // @phpstan-ignore property.notFound\n" + " $socket->__ext_sockets_nonblock = false;\n" " return true;\n" "}\n" "\n" @@ -10841,11 +11650,11 @@ static const char* swoole_library_source_ext_sockets = " if ($socket->isClosed()) {\n" " return false;\n" " }\n" - " if (isset($socket->__ext_sockets_nonblock) and $socket->__ext_sockets_nonblock) {\n" + " if ($socket->__ext_sockets_nonblock) {\n" " return true;\n" " }\n" - " $socket->__ext_sockets_nonblock = true; // @phpstan-ignore property.notFound\n" - " $socket->__ext_sockets_timeout = $socket->getOption(SOL_SOCKET, SO_RCVTIMEO); // @phpstan-ignore property.notFound\n" + " $socket->__ext_sockets_nonblock = true;\n" + " $socket->__ext_sockets_timeout = $socket->getOption(SOL_SOCKET, SO_RCVTIMEO);\n" " $socket->setOption(SOL_SOCKET, SO_RCVTIMEO, ['sec' => 0, 'usec' => 1000]);\n" " return true;\n" "}\n" @@ -10869,7 +11678,7 @@ static const char* swoole_library_source_ext_sockets = " */\n" "function swoole_socket_import_stream(mixed $stream): Socket|false\n" "{\n" - " return Socket::import($stream); // @phpstan-ignore staticMethod.notFound\n" + " return Socket::import($stream);\n" "}\n"; static const char* swoole_library_source_ext_standard = @@ -11024,11 +11833,11 @@ static const char* swoole_library_source_functions = "\n" "declare(strict_types=1);\n" "\n" - "if (PHP_VERSION_ID < 80100) { // @phpstan-ignore smaller.alwaysFalse\n" - " throw new RuntimeException('require PHP version 8.1 or later');\n" + "if (PHP_VERSION_ID < 80200) { // @phpstan-ignore smaller.alwaysFalse\n" + " throw new RuntimeException('require PHP version 8.2 or later');\n" "}\n" "\n" - "if (SWOOLE_USE_SHORTNAME) { // @phpstan-ignore if.alwaysTrue\n" + "if (SWOOLE_USE_SHORTNAME) {\n" " function _string(string $string = ''): Swoole\\StringObject\n" " {\n" " return new Swoole\\StringObject($string);\n" @@ -11145,6 +11954,9 @@ static const char* swoole_library_source_functions = "\n" "function swoole_array_default_value(array $array, $key, $default_value = null)\n" "{\n" + " // A null key is the empty string for PHP, which deprecates passing it as null as of PHP 8.5.\n" + " $key = $key ?? '';\n" + "\n" " return array_key_exists($key, $array) ? $array[$key] : $default_value;\n" "}\n" "\n" @@ -11166,10 +11978,10 @@ static const char* swoole_library_source_functions = " // cgroup v2\n" " $cpu_max = '/sys/fs/cgroup/cpu.max';\n" " if (file_exists($cpu_max)) {\n" - " $cpu_max = file_get_contents($cpu_max);\n" - " $fields = explode($cpu_max, ' ');\n" + " // The file holds \"$MAX $PERIOD\", e.g. \"150000 100000\", or \"max 100000\" when no quota is set.\n" + " $fields = explode(' ', trim((string) file_get_contents($cpu_max)));\n" " $quota_us = $fields[0];\n" - " if ($quota_us === 'max') { // @phpstan-ignore identical.alwaysFalse\n" + " if ($quota_us === 'max') {\n" " return swoole_cpu_num();\n" " }\n" " $period_us = $fields[1] ?? 100000;\n" @@ -11186,44 +11998,52 @@ static const char* swoole_library_source_functions = "\n" "function swoole_init_default_remote_object_server(): void\n" "{\n" - " $dir = swoole_library_get_option('default_remote_object_server_dir');\n" - " if (empty($dir)) {\n" - " $home = getenv('HOME') ?: sys_get_temp_dir();\n" - " $dir = $home . '/.swoole';\n" - " swoole_library_set_option('default_remote_object_server_dir', $dir);\n" - " }\n" - "\n" + " $verbose = swoole_library_get_option('default_remote_object_server_verbose') ?: false;\n" + " $dir = swoole_get_default_remote_object_server_dir();\n" " $pid_file = $dir . '/remote-object-server.pid';\n" "\n" + " $print_log = function ($msg) use ($verbose) {\n" + " if ($verbose) {\n" + " echo $msg . PHP_EOL;\n" + " }\n" + " };\n" + "\n" " if (!is_dir($dir)) {\n" " mkdir($dir, 0755, true);\n" + " $print_log(\"create dir[{$dir}]\");\n" " } else {\n" " if (is_file($pid_file)\n" " and posix_kill(intval(file_get_contents($pid_file)), 0)) {\n" + " $print_log('remote object server already running');\n" " return;\n" " }\n" " }\n" "\n" " $options = swoole_library_get_option('default_remote_object_server_options');\n" " if (!$options) {\n" - " $worker_num = swoole_library_get_option('default_remote_object_server_worker_num') ?: 128;\n" - " $options = [\n" + " $default_worker_num = defined('SWOOLE_THREAD') ? 128 : 8;\n" + " $worker_num = swoole_library_get_option('default_remote_object_server_worker_num') ?: $default_worker_num;\n" + " $options = [\n" " 'worker_num' => $worker_num,\n" " 'server_mode' => defined('SWOOLE_THREAD') ? SWOOLE_THREAD : SWOOLE_BASE,\n" " ];\n" " }\n" "\n" + " $print_log('remote object server options: ' . var_export($options, true));\n" + "\n" " $php_file = $dir . '/remote-object-server.php';\n" " $socket_file = $dir . '/remote-object-server.sock';\n" " $log_file = $dir . '/remote-object-server.log';\n" " $lock_file = $dir . '/remote-object-server.lock';\n" "\n" - " $wait_ready_fn = function () use ($socket_file) {\n" + " $wait_ready_fn = function () use ($socket_file, $print_log) {\n" " // wait for remote object server ready\n" " while (true) {\n" " if (posix_access($socket_file, POSIX_R_OK)) {\n" + " $print_log('remote object server ready');\n" " break;\n" " }\n" + " $print_log('wait for remote object server ready');\n" " usleep(500000);\n" " }\n" " };\n" @@ -11235,6 +12055,7 @@ static const char* swoole_library_source_functions = " // If the lock was not acquired, it indicates that another process is trying to start the remote object server.\n" " // In this case, the service should be skipped from starting and proceed to the ready wait detection branch.\n" " if (!flock($lock_handle, LOCK_EX | LOCK_NB)) {\n" + " $print_log('remote object server is already running');\n" " fclose($lock_handle);\n" " $wait_ready_fn();\n" " return;\n" @@ -11247,27 +12068,70 @@ static const char* swoole_library_source_functions = " $options['daemonize'] = true;\n" " $options['socket_type'] = SWOOLE_SOCK_UNIX_STREAM;\n" "\n" - " $rv = file_put_contents($php_file, 'start();\\n\");\n" + " // The generated file lives inside the project tree, so PHP-CS-Fixer checks it along with everything else\n" + " // and it has to be written in the project's coding style: standard file header, strict types, braces on\n" + " // their own lines, short array syntax, a trailing comma on every multiline array element and `=>` operators\n" + " // aligned within each array block. var_export() is not used for the options because it emits long array\n" + " // syntax and does not align anything; the array is written out below instead, one element per line.\n" + " $export_array = static function (array $data, int $depth = 1) use (&$export_array): string {\n" + " if (!$data) {\n" + " return '[]';\n" + " }\n" + " $indent = str_repeat(' ', $depth);\n" + " $keys = array_map(static fn ($key) => var_export($key, true), array_keys($data));\n" + " $width = max(array_map('strlen', $keys));\n" + " $lines = [];\n" + " foreach (array_values($data) as $i => $value) {\n" + " if (is_array($value)) {\n" + " $exported = $export_array($value, $depth + 1);\n" + " } else {\n" + " $exported = $value === null ? 'null' : var_export($value, true);\n" + " }\n" + " $lines[] = $indent . str_pad($keys[$i], $width) . ' => ' . $exported . ',';\n" + " }\n" + " return \"[\\n\" . implode(\"\\n\", $lines) . \"\\n\" . str_repeat(' ', $depth - 1) . ']';\n" + " };\n" + "\n" + " $exported_options = $export_array($options);\n" + "\n" + " $rv = file_put_contents($php_file, <<start();\n" + "\n" + " PHP);\n" " if (!$rv) {\n" " throw new RuntimeException(\"failed to write php file[{$php_file}]\");\n" " }\n" + " $print_log(\"write php file[{$php_file}]\");\n" "\n" " $php_bin = PHP_BINARY;\n" " if (posix_access($socket_file, POSIX_R_OK)) {\n" " unlink($socket_file);\n" + " $print_log(\"remove socket file[{$socket_file}]\");\n" " }\n" "\n" " $hook_flags = Swoole\\Runtime::getHookFlags();\n" " // Having enabled the MongoDB hook, you need to install the MongoDB PHP library through Composer.\n" " if (defined('SWOOLE_HOOK_MONGODB') and $hook_flags & SWOOLE_HOOK_MONGODB and !is_dir($dir . '/vendor/mongodb/mongodb')) {\n" " system(\"cd {$dir} && composer require mongodb/mongodb\");\n" + " $print_log('install mongodb library');\n" " }\n" "\n" " // start server\n" @@ -11279,13 +12143,16 @@ static const char* swoole_library_source_functions = " if ($proc === false) {\n" " throw new RuntimeException('failed to start remote object server');\n" " }\n" + " $print_log('start remote object server');\n" + "\n" " $status = proc_get_status($proc);\n" " if (!$status['running'] or !$status['pid']) {\n" " throw new RuntimeException('failed to start remote object server');\n" " }\n" + " $print_log(\"remote object server pid: {$status['pid']}\");\n" "\n" " if (Swoole\\Coroutine::getCid() > 0) {\n" - " $exitStatus = Swoole\\Coroutine\\System::waitPid($status['pid']);\n" + " $exitStatus = Swoole\\Coroutine\\System::waitpid($status['pid']);\n" " } else {\n" " pcntl_waitpid($status['pid'], $status);\n" " $exitStatus['code'] = $status;\n" @@ -11295,24 +12162,31 @@ static const char* swoole_library_source_functions = " throw new RuntimeException(\"failed to start remote object server: exit code {$exitStatus['code']}, output: \" . $output);\n" " }\n" " proc_close($proc);\n" + " $print_log('remote object server frontend exited, the process turns to the backend for running');\n" "\n" " $wait_ready_fn();\n" " flock($lock_handle, LOCK_UN);\n" " fclose($lock_handle);\n" "}\n" "\n" + "function swoole_get_default_remote_object_server_dir(): string\n" + "{\n" + " $dir = swoole_library_get_option('default_remote_object_server_dir');\n" + " if (empty($dir)) {\n" + " $home = getenv('HOME') ?: sys_get_temp_dir();\n" + " $dir = $home . '/.swoole';\n" + " }\n" + " return $dir;\n" + "}\n" + "\n" "function swoole_get_default_remote_object_client(): Swoole\\RemoteObject\\Client\n" "{\n" " if (!SwooleLibrary::$remote_object_server_initiated) {\n" - " SwooleLibrary::$remote_object_server_initiated = true;\n" " swoole_init_default_remote_object_server();\n" + " SwooleLibrary::$remote_object_server_initiated = true;\n" " }\n" " if (!SwooleLibrary::$remote_object_server_socket_file) {\n" - " $dir = swoole_library_get_option('default_remote_object_server_dir');\n" - " if (empty($dir)) {\n" - " $home = getenv('HOME') ?: sys_get_temp_dir();\n" - " $dir = $home . '/.swoole';\n" - " }\n" + " $dir = swoole_get_default_remote_object_server_dir();\n" " SwooleLibrary::$remote_object_server_socket_file = 'unix://' . $dir . '/remote-object-server.sock';\n" " }\n" " return new Swoole\\RemoteObject\\Client(SwooleLibrary::$remote_object_server_socket_file);\n" @@ -11330,7 +12204,7 @@ static const char* swoole_library_source_alias = "\n" "declare(strict_types=1);\n" "\n" - "if (SWOOLE_USE_SHORTNAME) { // @phpstan-ignore if.alwaysTrue\n" + "if (SWOOLE_USE_SHORTNAME) {\n" " class_alias(Swoole\\Coroutine\\WaitGroup::class, Co\\WaitGroup::class, true);\n" " class_alias(Swoole\\Coroutine\\Server::class, Co\\Server::class, true);\n" " class_alias(Swoole\\Coroutine\\Server\\Connection::class, Co\\Server\\Connection::class, true);\n" @@ -11357,7 +12231,7 @@ static const char* swoole_library_source_alias_ns = "\n" "use Swoole\\Coroutine;\n" "\n" - "if (SWOOLE_USE_SHORTNAME) { // @phpstan-ignore if.alwaysTrue\n" + "if (SWOOLE_USE_SHORTNAME) {\n" " function run(callable $fn, ...$args)\n" " {\n" " return \\Swoole\\Coroutine\\run($fn, ...$args);\n" @@ -11390,6 +12264,7 @@ void php_swoole_load_library() _eval(swoole_library_source_core_coroutine_barrier, "@swoole/library/core/Coroutine/Barrier.php"); _eval(swoole_library_source_core_coroutine_http_client_proxy, "@swoole/library/core/Coroutine/Http/ClientProxy.php"); _eval(swoole_library_source_core_coroutine_http_functions, "@swoole/library/core/Coroutine/Http/functions.php"); + _eval(swoole_library_source_core_coroutine_http2_multiplex_client, "@swoole/library/core/Coroutine/Http2/MultiplexClient.php"); _eval(swoole_library_source_core_connection_pool, "@swoole/library/core/ConnectionPool.php"); _eval(swoole_library_source_core_database_object_proxy, "@swoole/library/core/Database/ObjectProxy.php"); _eval(swoole_library_source_core_database_mysqli_config, "@swoole/library/core/Database/MysqliConfig.php"); From 4efbbb6c532466a69682353b5e319931118bd85b Mon Sep 17 00:00:00 2001 From: Demin Yin Date: Mon, 28 Sep 2026 20:35:41 -0700 Subject: [PATCH 2/2] Update the embedded library to swoole/library 6.2.x (3892f45) Co-Authored-By: Claude Fable 5.1 --- ext-src/php_swoole_library.h | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/ext-src/php_swoole_library.h b/ext-src/php_swoole_library.h index 4742578e3c..ec79e856fc 100644 --- a/ext-src/php_swoole_library.h +++ b/ext-src/php_swoole_library.h @@ -14,7 +14,7 @@ +----------------------------------------------------------------------+ */ -/* $Id: 5aeb8b373fa093e6f901211be3ee11ff52d73e8d */ +/* $Id: 3892f45776259e692bbf8d7c49dd6dd0b88ebf38 */ #ifndef SWOOLE_LIBRARY_H #define SWOOLE_LIBRARY_H @@ -3990,8 +3990,8 @@ static const char* swoole_library_source_core_database_pdo_statement_proxy = " foreach ($this->bindColumnContext as $column => $item) {\n" " $this->__object->bindColumn($column, ...$item);\n" " }\n" - " foreach ($this->bindValueContext as $value => $item) {\n" - " $this->__object->bindParam($value, ...$item);\n" + " foreach ($this->bindValueContext as $parameter => $item) {\n" + " $this->__object->bindValue($parameter, ...$item);\n" " }\n" " $ret = $this->__object->{$name}(...$arguments);\n" " } else {\n" @@ -4021,13 +4021,15 @@ static const char* swoole_library_source_core_database_pdo_statement_proxy = "\n" " public function bindParam($parameter, &$variable, $data_type = \\PDO::PARAM_STR, $length = 0, $driver_options = null): bool\n" " {\n" - " $this->bindParamContext[$parameter] = [$variable, $data_type, $length, $driver_options];\n" + " // The variable is kept by reference: a statement prepared again after a reconnect is bound to the variable of\n" + " // the caller, as the first one was, and not to the value the variable had at the time of this call.\n" + " $this->bindParamContext[$parameter] = [&$variable, $data_type, $length, $driver_options];\n" " return $this->__object->bindParam($parameter, $variable, $data_type, $length, $driver_options);\n" " }\n" "\n" - " public function bindColumn($column, &$param, $type = null, $maxlen = null, $driverdata = null): bool\n" + " public function bindColumn($column, &$param, $type = \\PDO::PARAM_STR, $maxlen = 0, $driverdata = null): bool\n" " {\n" - " $this->bindColumnContext[$column] = [$param, $type, $maxlen, $driverdata];\n" + " $this->bindColumnContext[$column] = [&$param, $type, $maxlen, $driverdata];\n" " return $this->__object->bindColumn($column, $param, $type, $maxlen, $driverdata);\n" " }\n" "\n"