diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d029a38..ceb18954 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,7 +68,7 @@ All notable changes to `mcp/sdk` will be documented in this file. * Fix a resource whose text is not valid UTF-8 (e.g. a Latin-1 `.txt` file) failing `resources/read` with an internal error or an uncaught `JsonException`: `ResourceResultFormatter` now sends it as a blob with the same MIME type. * [BC Break] Answer a request of a known method whose params do not fit it (e.g. `tools/call` without `name`) with `-32602` (Invalid params) instead of `-32600` (Invalid request), as JSON-RPC requires. `Request::fromArray()` throws the new `InvalidParamsException`, a subclass of `InvalidArgumentException`, and `InvalidInputMessageException::isInvalidParams()` tells the protocols which code to send. A malformed envelope and an unknown method keep their codes. * Fix a modern-era result that cannot be encoded as JSON, e.g. tool or resource text that is not valid UTF-8, escaping as an uncaught `JsonException` or ending a stream without an answer: the client gets `-32603`, with HTTP status `500` or as the last frame of a stream, and the failure is logged and dispatched as an `ErrorEvent`. Outside a stream, an error message that is not valid UTF-8 has the invalid bytes replaced. -* Fix `FileSessionStore` serving a partial session on Windows when a write could not replace the session file: it no longer falls back to a `copy()` that readers can see half done, but on Windows retries the move for a moment, and `write()` returns `false` if the move fails. +* Fix `FileSessionStore` serving a partial session on Windows when a write could not replace the session file: it no longer falls back to a `copy()` that readers can see half done, but on Windows retries the move for up to a second while a reader or another writer holds the file, and `write()` returns `false` if the move fails. * Fix a stdio server on Windows ending by itself after about 13.5 hours without input, when `fgets()` on the idle pipe reported a false EOF: `StdioTransport` now reads only once `stream_select()` reports input, which also keeps the run loop going while idle. Requires PHP 8.5, as `stream_select()` on Windows reports an idle pipe as readable before that. * Fix `StdioTransport` throwing a `ValueError` on an input that cannot be selected, like `php://memory`. * Fix a stdio server never answering a message that reaches it in several reads, such as a tool call with large arguments: `StdioTransport` handled each read as a message and answered the parts with `-32700`. It now waits for the newline. diff --git a/src/Server/Session/FileSessionStore.php b/src/Server/Session/FileSessionStore.php index 8952fe13..02cc3243 100644 --- a/src/Server/Session/FileSessionStore.php +++ b/src/Server/Session/FileSessionStore.php @@ -25,7 +25,7 @@ */ class FileSessionStore implements SessionStoreInterface { - private const WINDOWS_MOVE_ATTEMPTS = 10; + private const WINDOWS_MOVE_BUDGET_NS = 1_000_000_000; public function __construct( private readonly string $directory, @@ -259,18 +259,21 @@ private function deleteExpiredLockFile(string $path, int $now): void private function moveIntoPlace(string $tmp, string $path): bool { - // Overlapping writers of a session can briefly deny each other the replace on Windows. - $attempts = 'Windows' === \PHP_OS_FAMILY ? self::WINDOWS_MOVE_ATTEMPTS : 1; + if ('Windows' !== \PHP_OS_FAMILY) { + return @rename($tmp, $path); + } - for ($attempt = 1; $attempt < $attempts; ++$attempt) { - if (@rename($tmp, $path)) { - return true; + // Windows refuses to replace a file while anyone, even a reader, holds it open. + $deadline = hrtime(true) + self::WINDOWS_MOVE_BUDGET_NS; + while (!@rename($tmp, $path)) { + if (hrtime(true) >= $deadline) { + return false; } - usleep($attempt * 1000); + usleep(1000); } - return @rename($tmp, $path); + return true; } private function isTemporaryFile(string $entry): bool