diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e7823f..bb01b9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.15.0] - 2026-08-26 + ### Added - **A TCP listener can ask the kernel for a port, and say which one it got.** `addListener()`, `addHttp1Listener()` and `addHttp2Listener()` take port 0, which binds to whatever the kernel assigns, and `HttpServer::getBoundListeners()` reports what the server actually holds — one entry per configured listener, in configuration order, empty while the server is not running. Naming a free port before binding it leaves a window in which the port belongs to nobody: the phpt suite picks ports that way and loses the race under `-j4`, where a second server's `start()` answers `Failed to acquire TCP listener for 127.0.0.1:58864 (bind)`. Port 0 removes the window rather than narrowing it. Across several threads such a listener is bound once into the shared set and every thread adopts a duplicate, SO_REUSEPORT or not — binding per thread would hand each a different port. HTTP/3 still requires an explicit port: it binds through the UDP path, which reports no local address, so 0 is refused there rather than answered with a guess. @@ -1446,7 +1448,9 @@ on the [TrueAsync](https://github.com/true-async) event loop. and Windows, quick start), `docs/` (coding standards, contributor recommendations, llhttp upstream notes), Apache 2.0 `LICENSE`. -[Unreleased]: https://github.com/true-async/server/compare/v0.13.0...HEAD +[Unreleased]: https://github.com/true-async/server/compare/v0.15.0...HEAD +[0.15.0]: https://github.com/true-async/server/compare/v0.14.0...v0.15.0 +[0.14.0]: https://github.com/true-async/server/compare/v0.13.0...v0.14.0 [0.13.0]: https://github.com/true-async/server/compare/v0.12.0...v0.13.0 [0.12.0]: https://github.com/true-async/server/compare/v0.11.3...v0.12.0 [0.11.3]: https://github.com/true-async/server/compare/v0.11.2...v0.11.3 diff --git a/ide-stubs/true-async-server.php b/ide-stubs/true-async-server.php index 661760b..34ada4d 100644 --- a/ide-stubs/true-async-server.php +++ b/ide-stubs/true-async-server.php @@ -431,12 +431,21 @@ public function setSymlinkPolicy(StaticSymlinks $policy): static {} * * Patterns are matched against the path *relative to the root * directory*, with `/` as the separator, by gitignore's rule: - * a pattern naming no directory names a file, and covers a file - * of that name at any depth, so `*.php` hides `index.php` and - * `admin/tools.php` alike. A pattern naming a directory is - * anchored at the root and `*` stops at each separator, so - * `cache/*` hides the mount's own `cache/` and not `var/cache/`. * + * `*.php` a file of that name at any depth + * `/index.php` that file at the root directory only + * `cache/x` anchored at the root directory, `*` stopping at each `/` + * `cache/` a directory of that name at any depth, and all it holds + * `cache/**` anchored, `*` crossing `/` + * + * A pattern opening with a double star names every directory, the root + * directory among them. A pattern without `/` reads the file name, so a + * directory named like it keeps serving what it holds — `cache/` is how to + * cover a directory. Case follows the platform's own filesystem: + * case-insensitive on Windows, case-sensitive elsewhere. + * + * @throws HttpServerInvalidArgumentException when a pattern is empty or + * longer than 512 bytes. * @return static */ public function hide(string ...$globs): static {} @@ -558,7 +567,8 @@ public function __construct(?string $host = null, int $port = 8080) {} * {@see addHttp3Listener()}. * * @param string $host Host to bind (e.g., "127.0.0.1", "0.0.0.0") - * @param int $port Port to listen on + * @param int $port Port to listen on, or 0 to take whatever the kernel + * gives — {@see HttpServer::getBoundListeners()} reports it * @param bool $tls Enable TLS for this listener * @return static */ @@ -1795,6 +1805,23 @@ public function reload(): bool {} */ public function isRunning(): bool {} + /** + * The listeners the server actually holds, one entry per configured + * listener, in configuration order. + * + * A listener configured with port 0 is bound to a port the kernel picks, + * and the entry carries that port: there is no gap between choosing an + * address and owning it, which a caller picking a free port beforehand + * cannot avoid. HTTP/3 listeners still require an explicit port and + * report the configured one. + * + * Empty while the server is not running: before start(), after stop(). + * + * @return array + */ + public function getBoundListeners(): array {} + /** * Whether the extension was built with HTTP/2 support (--enable-http2). */ diff --git a/include/php_http_server.h b/include/php_http_server.h index 71ce7ae..8fe24a7 100644 --- a/include/php_http_server.h +++ b/include/php_http_server.h @@ -69,7 +69,7 @@ static zend_always_inline bool http_path_is_cwd_independent(const char *path, si * static-build codegen (genif.sh) can discover it; see the comment there. */ #include "php_true_async_server.h" -#define PHP_HTTP_SERVER_VERSION "0.14.0" +#define PHP_HTTP_SERVER_VERSION "0.15.0" /* * ==========================================================================