From 222415859af50e583087e24fd4c6a9e7a9a4eba2 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Mon, 23 Jan 2023 20:02:33 +0000 Subject: [PATCH] Require pthreads ^5.1 This version of pthreads has a substantially improved API, improved performance, improved memory usage, and much less magical and broken behaviour. --- .github/workflows/main.yml | 10 +- composer.json | 8 +- composer.lock | 91 ++++++++++--------- src/PocketMine.php | 4 +- src/console/ConsoleReaderChildProcess.php | 11 ++- src/network/mcpe/ChunkRequestTask.php | 8 +- src/network/mcpe/auth/ProcessLoginTask.php | 10 +- .../mcpe/compression/CompressBatchTask.php | 9 +- .../mcpe/raklib/PthreadsChannelReader.php | 5 +- .../mcpe/raklib/PthreadsChannelWriter.php | 5 +- src/network/mcpe/raklib/RakLibInterface.php | 6 +- src/network/mcpe/raklib/RakLibServer.php | 25 +++-- .../SnoozeAwarePthreadsChannelWriter.php | 5 +- src/scheduler/AsyncPool.php | 2 +- src/scheduler/AsyncTask.php | 32 +++---- src/thread/CommonThreadPartsTrait.php | 9 +- src/thread/NonThreadSafeValue.php | 57 ++++++++++++ src/thread/ThreadManager.php | 15 ++- src/utils/MainLogger.php | 3 + src/utils/MainLoggerThread.php | 9 +- tests/phpstan/configs/phpstan-bugs.neon | 8 +- tests/phpstan/stubs/pthreads.stub | 31 ++++++- 22 files changed, 245 insertions(+), 118 deletions(-) create mode 100644 src/thread/NonThreadSafeValue.php diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0481c78c9..85abd45ed 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,7 +21,7 @@ jobs: with: php-version: ${{ matrix.php }} install-path: "./bin" - pm-version-major: "4" + pm-version-major: "5" phpstan: name: PHPStan analysis @@ -42,7 +42,7 @@ jobs: with: php-version: ${{ matrix.php }} install-path: "./bin" - pm-version-major: "4" + pm-version-major: "5" - name: Install Composer run: curl -sS https://getcomposer.org/installer | php @@ -81,7 +81,7 @@ jobs: with: php-version: ${{ matrix.php }} install-path: "./bin" - pm-version-major: "4" + pm-version-major: "5" - name: Install Composer run: curl -sS https://getcomposer.org/installer | php @@ -122,7 +122,7 @@ jobs: with: php-version: ${{ matrix.php }} install-path: "./bin" - pm-version-major: "4" + pm-version-major: "5" - name: Install Composer run: curl -sS https://getcomposer.org/installer | php @@ -161,7 +161,7 @@ jobs: with: php-version: ${{ matrix.php }} install-path: "./bin" - pm-version-major: "4" + pm-version-major: "5" - name: Install Composer run: curl -sS https://getcomposer.org/installer | php diff --git a/composer.json b/composer.json index 41d30efe2..39d0a9e16 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "ext-openssl": "*", "ext-pcre": "*", "ext-phar": "*", - "ext-pthreads": "^4.0", + "ext-pthreads": "^5.1", "ext-reflection": "*", "ext-simplexml": "*", "ext-sockets": "*", @@ -40,17 +40,17 @@ "pocketmine/bedrock-protocol": "~18.0.0+bedrock-1.19.50", "pocketmine/binaryutils": "^0.2.1", "pocketmine/callback-validator": "^1.0.2", - "pocketmine/classloader": "^0.2.0", + "pocketmine/classloader": "dev-pthreads-v5", "pocketmine/color": "^0.3.0", "pocketmine/errorhandler": "^0.6.0", "pocketmine/locale-data": "~2.18.0", "pocketmine/log": "^0.4.0", - "pocketmine/log-pthreads": "^0.4.0", + "pocketmine/log-pthreads": "dev-pthreads-v5", "pocketmine/math": "^0.4.0", "pocketmine/nbt": "^0.3.2", "pocketmine/raklib": "^0.14.2", "pocketmine/raklib-ipc": "^0.1.0", - "pocketmine/snooze": "^0.3.0", + "pocketmine/snooze": "dev-pthreads-v5", "ramsey/uuid": "^4.1", "symfony/filesystem": "^5.4" }, diff --git a/composer.lock b/composer.lock index a48d419ba..d31480efa 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "19f6b653d5b36e04f2d799de5972e567", + "content-hash": "5725b919e65c02842812b1fed8a3d77c", "packages": [ { "name": "adhocore/json-comment", @@ -469,20 +469,20 @@ }, { "name": "pocketmine/classloader", - "version": "0.2.0", + "version": "dev-pthreads-v5", "source": { "type": "git", "url": "https://github.com/pmmp/ClassLoader.git", - "reference": "49ea303993efdfb39cd302e2156d50aa78209e78" + "reference": "dc98186e947d8940b8f6f4dbb2837f7c961a4812" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/ClassLoader/zipball/49ea303993efdfb39cd302e2156d50aa78209e78", - "reference": "49ea303993efdfb39cd302e2156d50aa78209e78", + "url": "https://api.github.com/repos/pmmp/ClassLoader/zipball/dc98186e947d8940b8f6f4dbb2837f7c961a4812", + "reference": "dc98186e947d8940b8f6f4dbb2837f7c961a4812", "shasum": "" }, "require": { - "ext-pthreads": "~3.2.0 || ^4.0", + "ext-pthreads": "^5.0", "ext-reflection": "*", "php": "^8.0" }, @@ -491,8 +491,8 @@ }, "require-dev": { "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "0.12.99", - "phpstan/phpstan-strict-rules": "^0.12.4", + "phpstan/phpstan": "1.9.4", + "phpstan/phpstan-strict-rules": "^1.0", "phpunit/phpunit": "^9.5" }, "type": "library", @@ -508,9 +508,9 @@ "description": "Ad-hoc autoloading components used by PocketMine-MP", "support": { "issues": "https://github.com/pmmp/ClassLoader/issues", - "source": "https://github.com/pmmp/ClassLoader/tree/0.2.0" + "source": "https://github.com/pmmp/ClassLoader/tree/pthreads-v5" }, - "time": "2021-11-01T20:17:27+00:00" + "time": "2023-01-20T18:50:37+00:00" }, { "name": "pocketmine/color", @@ -591,16 +591,16 @@ }, { "name": "pocketmine/locale-data", - "version": "2.18.0", + "version": "2.18.3", "source": { "type": "git", "url": "https://github.com/pmmp/Language.git", - "reference": "0f50afc3d0fec29f769a62e93c71f8a0fb968f76" + "reference": "da25bfe9ee4822a84feb9b7e620c56ad4000aed0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/Language/zipball/0f50afc3d0fec29f769a62e93c71f8a0fb968f76", - "reference": "0f50afc3d0fec29f769a62e93c71f8a0fb968f76", + "url": "https://api.github.com/repos/pmmp/Language/zipball/da25bfe9ee4822a84feb9b7e620c56ad4000aed0", + "reference": "da25bfe9ee4822a84feb9b7e620c56ad4000aed0", "shasum": "" }, "type": "library", @@ -608,9 +608,9 @@ "description": "Language resources used by PocketMine-MP", "support": { "issues": "https://github.com/pmmp/Language/issues", - "source": "https://github.com/pmmp/Language/tree/2.18.0" + "source": "https://github.com/pmmp/Language/tree/2.18.3" }, - "time": "2023-01-14T17:52:46+00:00" + "time": "2023-01-17T21:43:36+00:00" }, { "name": "pocketmine/log", @@ -655,21 +655,21 @@ }, { "name": "pocketmine/log-pthreads", - "version": "0.4.0", + "version": "dev-pthreads-v5", "source": { "type": "git", "url": "https://github.com/pmmp/LogPthreads.git", - "reference": "61f709e8cf36bcc24e4efe02acded680a1ce23cd" + "reference": "83a66c9b8c39531b97a3b08c0ea97db967a3c60a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/LogPthreads/zipball/61f709e8cf36bcc24e4efe02acded680a1ce23cd", - "reference": "61f709e8cf36bcc24e4efe02acded680a1ce23cd", + "url": "https://api.github.com/repos/pmmp/LogPthreads/zipball/83a66c9b8c39531b97a3b08c0ea97db967a3c60a", + "reference": "83a66c9b8c39531b97a3b08c0ea97db967a3c60a", "shasum": "" }, "require": { - "ext-pthreads": "~3.2.0 || ^4.0", - "php": "^7.4 || ^8.0", + "ext-pthreads": "^5.0", + "php": "^8.0", "pocketmine/log": "^0.4.0" }, "conflict": { @@ -677,8 +677,8 @@ }, "require-dev": { "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "0.12.88", - "phpstan/phpstan-strict-rules": "^0.12.4" + "phpstan/phpstan": "1.8.11", + "phpstan/phpstan-strict-rules": "^1.0" }, "type": "library", "autoload": { @@ -693,9 +693,9 @@ "description": "Logging components specialized for pthreads used by PocketMine-MP and related projects", "support": { "issues": "https://github.com/pmmp/LogPthreads/issues", - "source": "https://github.com/pmmp/LogPthreads/tree/0.4.0" + "source": "https://github.com/pmmp/LogPthreads/tree/pthreads-v5" }, - "time": "2021-11-01T21:42:09+00:00" + "time": "2023-01-20T19:45:45+00:00" }, { "name": "pocketmine/math", @@ -866,26 +866,26 @@ }, { "name": "pocketmine/snooze", - "version": "0.3.1", + "version": "dev-pthreads-v5", "source": { "type": "git", "url": "https://github.com/pmmp/Snooze.git", - "reference": "0ac8fc2a781c419a1f64ebca4d5835028f59e29b" + "reference": "8589ddfa1672215dcc78d8edb7acb4cf67d59d5a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/Snooze/zipball/0ac8fc2a781c419a1f64ebca4d5835028f59e29b", - "reference": "0ac8fc2a781c419a1f64ebca4d5835028f59e29b", + "url": "https://api.github.com/repos/pmmp/Snooze/zipball/8589ddfa1672215dcc78d8edb7acb4cf67d59d5a", + "reference": "8589ddfa1672215dcc78d8edb7acb4cf67d59d5a", "shasum": "" }, "require": { - "ext-pthreads": "~3.2.0 || ^4.0", - "php-64bit": "^7.3 || ^8.0" + "ext-pthreads": "^5.0", + "php-64bit": "^8.0" }, "require-dev": { "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "0.12.99", - "phpstan/phpstan-strict-rules": "^0.12.4" + "phpstan/phpstan": "1.9.14", + "phpstan/phpstan-strict-rules": "^1.0" }, "type": "library", "autoload": { @@ -900,9 +900,9 @@ "description": "Thread notification management library for code using the pthreads extension", "support": { "issues": "https://github.com/pmmp/Snooze/issues", - "source": "https://github.com/pmmp/Snooze/tree/0.3.1" + "source": "https://github.com/pmmp/Snooze/tree/pthreads-v5" }, - "time": "2021-11-01T20:50:08+00:00" + "time": "2023-01-20T18:19:39+00:00" }, { "name": "ramsey/collection", @@ -1610,16 +1610,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.15.2", + "version": "v4.15.3", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc" + "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc", - "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/570e980a201d8ed0236b0a62ddf2c9cbb2034039", + "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039", "shasum": "" }, "require": { @@ -1660,9 +1660,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.3" }, - "time": "2022-11-12T15:38:23+00:00" + "time": "2023-01-16T22:05:37+00:00" }, { "name": "phar-io/manifest", @@ -3374,7 +3374,10 @@ "stability-flags": { "pocketmine/bedrock-block-upgrade-schema": 20, "pocketmine/bedrock-data": 20, - "pocketmine/bedrock-item-upgrade-schema": 20 + "pocketmine/bedrock-item-upgrade-schema": 20, + "pocketmine/classloader": 20, + "pocketmine/log-pthreads": 20, + "pocketmine/snooze": 20 }, "prefer-stable": false, "prefer-lowest": false, @@ -3396,7 +3399,7 @@ "ext-openssl": "*", "ext-pcre": "*", "ext-phar": "*", - "ext-pthreads": "^4.0", + "ext-pthreads": "^5.1", "ext-reflection": "*", "ext-simplexml": "*", "ext-sockets": "*", diff --git a/src/PocketMine.php b/src/PocketMine.php index 4b0b644ec..762837102 100644 --- a/src/PocketMine.php +++ b/src/PocketMine.php @@ -122,8 +122,8 @@ namespace pocketmine { if(substr_count($pthreads_version, ".") < 2){ $pthreads_version = "0.$pthreads_version"; } - if(version_compare($pthreads_version, "4.0.0") < 0 || version_compare($pthreads_version, "5.0.0") >= 0){ - $messages[] = "pthreads ^4.0.0 is required, while you have $pthreads_version."; + if(version_compare($pthreads_version, "5.1.0") < 0 || version_compare($pthreads_version, "6.0.0") >= 0){ + $messages[] = "pthreads ^5.0.0 is required, while you have $pthreads_version."; } } diff --git a/src/console/ConsoleReaderChildProcess.php b/src/console/ConsoleReaderChildProcess.php index 2d4e3fc56..3dd2c24c2 100644 --- a/src/console/ConsoleReaderChildProcess.php +++ b/src/console/ConsoleReaderChildProcess.php @@ -46,13 +46,17 @@ if($socket === false){ throw new \RuntimeException("Failed to connect to server process ($errCode): $errMessage"); } -$channel = new \Threaded(); +/** @phpstan-var \ThreadedArray $channel */ +$channel = new \ThreadedArray(); $thread = new class($channel) extends \Thread{ + /** + * @phpstan-param \ThreadedArray $channel + */ public function __construct( - private \Threaded $channel, + private \ThreadedArray $channel, ){} - public function run(){ + public function run() : void{ require dirname(__DIR__, 2) . '/vendor/autoload.php'; $channel = $this->channel; @@ -75,7 +79,6 @@ while(!feof($socket)){ if(count($channel) === 0){ $channel->wait(1_000_000); } - /** @var string|null $line */ $line = $channel->shift(); return $line; }); diff --git a/src/network/mcpe/ChunkRequestTask.php b/src/network/mcpe/ChunkRequestTask.php index 1c9410f22..b466618c9 100644 --- a/src/network/mcpe/ChunkRequestTask.php +++ b/src/network/mcpe/ChunkRequestTask.php @@ -33,6 +33,7 @@ use pocketmine\network\mcpe\protocol\serializer\PacketSerializerContext; use pocketmine\network\mcpe\protocol\types\ChunkPosition; use pocketmine\network\mcpe\serializer\ChunkSerializer; use pocketmine\scheduler\AsyncTask; +use pocketmine\thread\NonThreadSafeValue; use pocketmine\world\format\Chunk; use pocketmine\world\format\io\FastChunkSerializer; @@ -43,14 +44,15 @@ class ChunkRequestTask extends AsyncTask{ protected string $chunk; protected int $chunkX; protected int $chunkZ; - protected Compressor $compressor; + /** @phpstan-var NonThreadSafeValue */ + protected NonThreadSafeValue $compressor; private string $tiles; /** * @phpstan-param (\Closure() : void)|null $onError */ public function __construct(int $chunkX, int $chunkZ, Chunk $chunk, CompressBatchPromise $promise, Compressor $compressor, ?\Closure $onError = null){ - $this->compressor = $compressor; + $this->compressor = new NonThreadSafeValue($compressor); $this->chunk = FastChunkSerializer::serializeTerrain($chunk); $this->chunkX = $chunkX; @@ -66,7 +68,7 @@ class ChunkRequestTask extends AsyncTask{ $subCount = ChunkSerializer::getSubChunkCount($chunk); $encoderContext = new PacketSerializerContext(GlobalItemTypeDictionary::getInstance()->getDictionary()); $payload = ChunkSerializer::serializeFullChunk($chunk, RuntimeBlockMapping::getInstance(), $encoderContext, $this->tiles); - $this->setResult($this->compressor->compress(PacketBatch::fromPackets($encoderContext, LevelChunkPacket::create(new ChunkPosition($this->chunkX, $this->chunkZ), $subCount, false, null, $payload))->getBuffer())); + $this->setResult($this->compressor->deserialize()->compress(PacketBatch::fromPackets($encoderContext, LevelChunkPacket::create(new ChunkPosition($this->chunkX, $this->chunkZ), $subCount, false, null, $payload))->getBuffer())); } public function onError() : void{ diff --git a/src/network/mcpe/auth/ProcessLoginTask.php b/src/network/mcpe/auth/ProcessLoginTask.php index 8df9886d3..1ea36fa37 100644 --- a/src/network/mcpe/auth/ProcessLoginTask.php +++ b/src/network/mcpe/auth/ProcessLoginTask.php @@ -30,6 +30,7 @@ use pocketmine\network\mcpe\JwtUtils; use pocketmine\network\mcpe\protocol\types\login\JwtChainLinkBody; use pocketmine\network\mcpe\protocol\types\login\JwtHeader; use pocketmine\scheduler\AsyncTask; +use pocketmine\thread\NonThreadSafeValue; use function base64_decode; use function igbinary_serialize; use function igbinary_unserialize; @@ -49,8 +50,10 @@ class ProcessLoginTask extends AsyncTask{ * Whether the keychain signatures were validated correctly. This will be set to an error message if any link in the * keychain is invalid for whatever reason (bad signature, not in nbf-exp window, etc). If this is non-null, the * keychain might have been tampered with. The player will always be disconnected if this is non-null. + * + * @phpstan-var NonThreadSafeValue|string|null */ - private Translatable|string|null $error = "Unknown"; + private NonThreadSafeValue|string|null $error = "Unknown"; /** * Whether the player is logged into Xbox Live. This is true if any link in the keychain is signed with the Mojang * root public key. @@ -77,7 +80,8 @@ class ProcessLoginTask extends AsyncTask{ $this->clientPublicKey = $this->validateChain(); $this->error = null; }catch(VerifyLoginException $e){ - $this->error = $e->getDisconnectMessage(); + $disconnectMessage = $e->getDisconnectMessage(); + $this->error = $disconnectMessage instanceof Translatable ? new NonThreadSafeValue($disconnectMessage) : $disconnectMessage; } } @@ -195,6 +199,6 @@ class ProcessLoginTask extends AsyncTask{ * @phpstan-var \Closure(bool, bool, Translatable|string|null, ?string) : void $callback */ $callback = $this->fetchLocal(self::TLS_KEY_ON_COMPLETION); - $callback($this->authenticated, $this->authRequired, $this->error, $this->clientPublicKey); + $callback($this->authenticated, $this->authRequired, $this->error instanceof NonThreadSafeValue ? $this->error->deserialize() : $this->error, $this->clientPublicKey); } } diff --git a/src/network/mcpe/compression/CompressBatchTask.php b/src/network/mcpe/compression/CompressBatchTask.php index b863076fa..96e9051b6 100644 --- a/src/network/mcpe/compression/CompressBatchTask.php +++ b/src/network/mcpe/compression/CompressBatchTask.php @@ -24,21 +24,26 @@ declare(strict_types=1); namespace pocketmine\network\mcpe\compression; use pocketmine\scheduler\AsyncTask; +use pocketmine\thread\NonThreadSafeValue; class CompressBatchTask extends AsyncTask{ private const TLS_KEY_PROMISE = "promise"; + /** @phpstan-var NonThreadSafeValue */ + private NonThreadSafeValue $compressor; + public function __construct( private string $data, CompressBatchPromise $promise, - private Compressor $compressor + Compressor $compressor ){ + $this->compressor = new NonThreadSafeValue($compressor); $this->storeLocal(self::TLS_KEY_PROMISE, $promise); } public function onRun() : void{ - $this->setResult($this->compressor->compress($this->data)); + $this->setResult($this->compressor->deserialize()->compress($this->data)); } public function onCompletion() : void{ diff --git a/src/network/mcpe/raklib/PthreadsChannelReader.php b/src/network/mcpe/raklib/PthreadsChannelReader.php index d5b52c790..68c718b0f 100644 --- a/src/network/mcpe/raklib/PthreadsChannelReader.php +++ b/src/network/mcpe/raklib/PthreadsChannelReader.php @@ -26,7 +26,10 @@ namespace pocketmine\network\mcpe\raklib; use raklib\server\ipc\InterThreadChannelReader; final class PthreadsChannelReader implements InterThreadChannelReader{ - public function __construct(private \Threaded $buffer){} + /** + * @phpstan-param \ThreadedArray $buffer + */ + public function __construct(private \ThreadedArray $buffer){} public function read() : ?string{ return $this->buffer->shift(); diff --git a/src/network/mcpe/raklib/PthreadsChannelWriter.php b/src/network/mcpe/raklib/PthreadsChannelWriter.php index 5462f7776..afbeefdd2 100644 --- a/src/network/mcpe/raklib/PthreadsChannelWriter.php +++ b/src/network/mcpe/raklib/PthreadsChannelWriter.php @@ -26,7 +26,10 @@ namespace pocketmine\network\mcpe\raklib; use raklib\server\ipc\InterThreadChannelWriter; final class PthreadsChannelWriter implements InterThreadChannelWriter{ - public function __construct(private \Threaded $buffer){} + /** + * @phpstan-param \ThreadedArray $buffer + */ + public function __construct(private \ThreadedArray $buffer){} public function write(string $str) : void{ $this->buffer[] = $str; diff --git a/src/network/mcpe/raklib/RakLibInterface.php b/src/network/mcpe/raklib/RakLibInterface.php index 1cb82ef5a..1adb3c844 100644 --- a/src/network/mcpe/raklib/RakLibInterface.php +++ b/src/network/mcpe/raklib/RakLibInterface.php @@ -85,8 +85,10 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{ $this->sleeper = new SleeperNotifier(); - $mainToThreadBuffer = new \Threaded(); - $threadToMainBuffer = new \Threaded(); + /** @phpstan-var \ThreadedArray $mainToThreadBuffer */ + $mainToThreadBuffer = new \ThreadedArray(); + /** @phpstan-var \ThreadedArray $threadToMainBuffer */ + $threadToMainBuffer = new \ThreadedArray(); $this->rakLib = new RakLibServer( $this->server->getLogger(), diff --git a/src/network/mcpe/raklib/RakLibServer.php b/src/network/mcpe/raklib/RakLibServer.php index 13523f3a8..ccdadca55 100644 --- a/src/network/mcpe/raklib/RakLibServer.php +++ b/src/network/mcpe/raklib/RakLibServer.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace pocketmine\network\mcpe\raklib; use pocketmine\snooze\SleeperNotifier; +use pocketmine\thread\NonThreadSafeValue; use pocketmine\thread\Thread; use raklib\generic\Socket; use raklib\generic\SocketException; @@ -43,19 +44,27 @@ class RakLibServer extends Thread{ protected bool $cleanShutdown = false; protected bool $ready = false; protected string $mainPath; - public ?RakLibThreadCrashInfo $crashInfo = null; + /** @phpstan-var NonThreadSafeValue|null */ + public ?NonThreadSafeValue $crashInfo = null; + /** @phpstan-var NonThreadSafeValue */ + protected NonThreadSafeValue $address; + /** + * @phpstan-param \ThreadedArray $mainToThreadBuffer + * @phpstan-param \ThreadedArray $threadToMainBuffer + */ public function __construct( protected \ThreadedLogger $logger, - protected \Threaded $mainToThreadBuffer, - protected \Threaded $threadToMainBuffer, - protected InternetAddress $address, + protected \ThreadedArray $mainToThreadBuffer, + protected \ThreadedArray $threadToMainBuffer, + InternetAddress $address, protected int $serverId, protected int $maxMtuSize, protected int $protocolVersion, protected SleeperNotifier $mainThreadNotifier ){ $this->mainPath = \pocketmine\PATH; + $this->address = new NonThreadSafeValue($address); } /** @@ -75,12 +84,12 @@ class RakLibServer extends Thread{ } public function getCrashInfo() : ?RakLibThreadCrashInfo{ - return $this->crashInfo; + return $this->crashInfo?->deserialize(); } private function setCrashInfo(RakLibThreadCrashInfo $info) : void{ $this->synchronized(function(RakLibThreadCrashInfo $info) : void{ - $this->crashInfo = $info; + $this->crashInfo = new NonThreadSafeValue($info); $this->notify(); }, $info); } @@ -91,7 +100,7 @@ class RakLibServer extends Thread{ while(!$this->ready && $this->crashInfo === null){ $this->wait(); } - $crashInfo = $this->crashInfo; + $crashInfo = $this->crashInfo?->deserialize(); if($crashInfo !== null){ if($crashInfo->getClass() === SocketException::class){ throw new SocketException($crashInfo->getMessage()); @@ -110,7 +119,7 @@ class RakLibServer extends Thread{ register_shutdown_function([$this, "shutdownHandler"]); try{ - $socket = new Socket($this->address); + $socket = new Socket($this->address->deserialize()); }catch(SocketException $e){ $this->setCrashInfo(RakLibThreadCrashInfo::fromThrowable($e)); return; diff --git a/src/network/mcpe/raklib/SnoozeAwarePthreadsChannelWriter.php b/src/network/mcpe/raklib/SnoozeAwarePthreadsChannelWriter.php index 739acf051..723cb3730 100644 --- a/src/network/mcpe/raklib/SnoozeAwarePthreadsChannelWriter.php +++ b/src/network/mcpe/raklib/SnoozeAwarePthreadsChannelWriter.php @@ -27,8 +27,11 @@ use pocketmine\snooze\SleeperNotifier; use raklib\server\ipc\InterThreadChannelWriter; final class SnoozeAwarePthreadsChannelWriter implements InterThreadChannelWriter{ + /** + * @phpstan-param \ThreadedArray $buffer + */ public function __construct( - private \Threaded $buffer, + private \ThreadedArray $buffer, private SleeperNotifier $notifier ){} diff --git a/src/scheduler/AsyncPool.php b/src/scheduler/AsyncPool.php index 76fe6c052..e8097c3df 100644 --- a/src/scheduler/AsyncPool.php +++ b/src/scheduler/AsyncPool.php @@ -158,7 +158,7 @@ class AsyncPool{ throw new \InvalidArgumentException("Cannot submit the same AsyncTask instance more than once"); } - $task->progressUpdates = new \Threaded(); + $task->progressUpdates = new \ThreadedArray(); $task->setSubmitted(); $this->getWorker($worker)->stack($task); diff --git a/src/scheduler/AsyncTask.php b/src/scheduler/AsyncTask.php index 9b8866f43..2194f47db 100644 --- a/src/scheduler/AsyncTask.php +++ b/src/scheduler/AsyncTask.php @@ -23,11 +23,11 @@ declare(strict_types=1); namespace pocketmine\scheduler; -use pocketmine\utils\AssumptionFailedError; +use pocketmine\thread\NonThreadSafeValue; use function igbinary_serialize; use function igbinary_unserialize; +use function is_null; use function is_scalar; -use function is_string; use function spl_object_id; /** @@ -51,15 +51,10 @@ use function spl_object_id; * thread, e.g. during {@link AsyncTask::onCompletion()} or {@link AsyncTask::onProgressUpdate()}. This means that * whatever you do in onRun() must be able to work without the Server instance. * - * WARNING: Any non-Threaded objects WILL BE SERIALIZED when assigned to members of AsyncTasks or other Threaded object. - * If later accessed from said Threaded object, you will be operating on a COPY OF THE OBJECT, NOT THE ORIGINAL OBJECT. - * If you want to store non-serializable objects to access when the task completes, store them using + * If you want to store non-thread-safe objects to access when the task completes, store them using * {@link AsyncTask::storeLocal}. - * - * WARNING: Arrays are converted to Volatile objects when assigned as members of Threaded objects. - * Keep this in mind when using arrays stored as members of your AsyncTask. */ -abstract class AsyncTask extends \Threaded{ +abstract class AsyncTask extends \ThreadedRunnable{ /** * @var \ArrayObject|mixed[]|null object hash => mixed data * @phpstan-var \ArrayObject>|null @@ -71,10 +66,11 @@ abstract class AsyncTask extends \Threaded{ /** @var AsyncWorker|null $worker */ public $worker = null; - public \Threaded $progressUpdates; + /** @phpstan-var \ThreadedArray */ + public \ThreadedArray $progressUpdates; - private string|int|bool|null|float $result = null; - private bool $serialized = false; + /** @phpstan-var NonThreadSafeValue|string|int|bool|float|null */ + private NonThreadSafeValue|string|int|bool|null|float $result = null; private bool $cancelRun = false; private bool $submitted = false; @@ -117,15 +113,14 @@ abstract class AsyncTask extends \Threaded{ * @return mixed */ public function getResult(){ - if($this->serialized){ - if(!is_string($this->result)) throw new AssumptionFailedError("Result expected to be a serialized string"); - return igbinary_unserialize($this->result); + if($this->result instanceof NonThreadSafeValue){ + return $this->result->deserialize(); } return $this->result; } public function setResult(mixed $result) : void{ - $this->result = ($this->serialized = !is_scalar($result)) ? igbinary_serialize($result) : $result; + $this->result = is_scalar($result) || is_null($result) ? $result : new NonThreadSafeValue($result); } public function cancelRun() : void{ @@ -164,15 +159,14 @@ abstract class AsyncTask extends \Threaded{ * @param mixed $progress A value that can be safely serialize()'ed. */ public function publishProgress(mixed $progress) : void{ - $this->progressUpdates[] = igbinary_serialize($progress); + $this->progressUpdates[] = igbinary_serialize($progress) ?? throw new \InvalidArgumentException("Progress must be serializable"); } /** * @internal Only call from AsyncPool.php on the main thread */ public function checkProgressUpdates() : void{ - while($this->progressUpdates->count() !== 0){ - $progress = $this->progressUpdates->shift(); + while(($progress = $this->progressUpdates->shift()) !== null){ $this->onProgressUpdate(igbinary_unserialize($progress)); } } diff --git a/src/thread/CommonThreadPartsTrait.php b/src/thread/CommonThreadPartsTrait.php index 8268d5c3f..777acd009 100644 --- a/src/thread/CommonThreadPartsTrait.php +++ b/src/thread/CommonThreadPartsTrait.php @@ -28,8 +28,11 @@ use pocketmine\Server; use function error_reporting; trait CommonThreadPartsTrait{ - /** @var \Threaded|\ClassLoader[]|null */ - private ?\Threaded $classLoaders = null; + /** + * @var \ThreadedArray|\ClassLoader[]|null + * @phpstan-var \ThreadedArray|null + */ + private ?\ThreadedArray $classLoaders = null; protected ?string $composerAutoloaderPath = null; protected bool $isKilled = false; @@ -52,7 +55,7 @@ trait CommonThreadPartsTrait{ } if($this->classLoaders === null){ - $this->classLoaders = new \Threaded(); + $this->classLoaders = new \ThreadedArray(); }else{ foreach($this->classLoaders as $k => $autoloader){ unset($this->classLoaders[$k]); diff --git a/src/thread/NonThreadSafeValue.php b/src/thread/NonThreadSafeValue.php new file mode 100644 index 000000000..9d443b065 --- /dev/null +++ b/src/thread/NonThreadSafeValue.php @@ -0,0 +1,57 @@ +variable = igbinary_serialize($variable) ?? throw new \InvalidArgumentException("Cannot serialize variable of type " . get_debug_type($variable)); + } + + /** + * Returns a deserialized copy of the original variable. + * + * @phpstan-return TValue + */ + public function deserialize() : mixed{ + return igbinary_unserialize($this->variable); + } +} diff --git a/src/thread/ThreadManager.php b/src/thread/ThreadManager.php index f383b2c49..a60d9bf9e 100644 --- a/src/thread/ThreadManager.php +++ b/src/thread/ThreadManager.php @@ -25,7 +25,7 @@ namespace pocketmine\thread; use function spl_object_id; -class ThreadManager extends \Volatile{ +class ThreadManager extends \ThreadedBase{ private static ?self $instance = null; @@ -40,12 +40,19 @@ class ThreadManager extends \Volatile{ return self::$instance; } + /** @phpstan-var \ThreadedArray */ + private \ThreadedArray $threads; + + private function __construct(){ + $this->threads = new \ThreadedArray(); + } + public function add(Worker|Thread $thread) : void{ - $this[spl_object_id($thread)] = $thread; + $this->threads[spl_object_id($thread)] = $thread; } public function remove(Worker|Thread $thread) : void{ - unset($this[spl_object_id($thread)]); + unset($this->threads[spl_object_id($thread)]); } /** @@ -56,7 +63,7 @@ class ThreadManager extends \Volatile{ /** * @var Worker|Thread $thread */ - foreach($this as $key => $thread){ + foreach($this->threads as $key => $thread){ $array[$key] = $thread; } diff --git a/src/utils/MainLogger.php b/src/utils/MainLogger.php index 6f65303bf..f79615586 100644 --- a/src/utils/MainLogger.php +++ b/src/utils/MainLogger.php @@ -194,6 +194,9 @@ class MainLogger extends \AttachableThreadedLogger implements \BufferedLogger{ Terminal::writeLine($message); $this->logWriterThread->write($time->format("Y-m-d") . " " . TextFormat::clean($message) . PHP_EOL); + /** + * @var \ThreadedLoggerAttachment $attachment + */ foreach($this->attachments as $attachment){ $attachment->log($level, $message); } diff --git a/src/utils/MainLoggerThread.php b/src/utils/MainLoggerThread.php index b56562134..c7287e349 100644 --- a/src/utils/MainLoggerThread.php +++ b/src/utils/MainLoggerThread.php @@ -30,14 +30,15 @@ use function is_resource; use function touch; final class MainLoggerThread extends \Thread{ - private \Threaded $buffer; + /** @phpstan-var \ThreadedArray */ + private \ThreadedArray $buffer; private bool $syncFlush = false; private bool $shutdown = false; public function __construct( private string $logFile ){ - $this->buffer = new \Threaded(); + $this->buffer = new \ThreadedArray(); touch($this->logFile); } @@ -72,9 +73,7 @@ final class MainLoggerThread extends \Thread{ * @param resource $logResource */ private function writeLogStream($logResource) : void{ - while($this->buffer->count() > 0){ - /** @var string $chunk */ - $chunk = $this->buffer->shift(); + while(($chunk = $this->buffer->shift()) !== null){ fwrite($logResource, $chunk); } diff --git a/tests/phpstan/configs/phpstan-bugs.neon b/tests/phpstan/configs/phpstan-bugs.neon index 3ee56e8be..f45f9a643 100644 --- a/tests/phpstan/configs/phpstan-bugs.neon +++ b/tests/phpstan/configs/phpstan-bugs.neon @@ -46,9 +46,9 @@ parameters: path: ../../../src/plugin/PluginManager.php - - message: "#^Offset \\(int\\|string\\) on array\\ in isset\\(\\) always exists and is not nullable\\.$#" + message: "#^Parameter \\#1 \\$work of method Worker\\:\\:stack\\(\\) expects Threaded, pocketmine\\\\scheduler\\\\AsyncTask given\\.$#" count: 1 - path: ../../../src/plugin/PluginManager.php + path: ../../../src/scheduler/AsyncPool.php - message: "#^Static property pocketmine\\\\scheduler\\\\AsyncTask\\:\\:\\$threadLocalStorage \\(ArrayObject\\\\>\\|null\\) does not accept non\\-empty\\-array\\\\>\\|ArrayObject\\\\>\\.$#" @@ -56,12 +56,12 @@ parameters: path: ../../../src/scheduler/AsyncTask.php - - message: "#^Property pocketmine\\\\thread\\\\Thread\\:\\:\\$classLoaders \\(\\(iterable\\&Threaded\\)\\|null\\) does not accept array\\\\|\\(iterable\\&Threaded\\)\\.$#" + message: "#^Property pocketmine\\\\thread\\\\Thread\\:\\:\\$classLoaders \\(ThreadedArray\\\\|null\\) does not accept array\\{ClassLoader\\}\\|ThreadedArray\\\\.$#" count: 1 path: ../../../src/thread/Thread.php - - message: "#^Property pocketmine\\\\thread\\\\Worker\\:\\:\\$classLoaders \\(\\(iterable\\&Threaded\\)\\|null\\) does not accept array\\\\|\\(iterable\\&Threaded\\)\\.$#" + message: "#^Property pocketmine\\\\thread\\\\Worker\\:\\:\\$classLoaders \\(ThreadedArray\\\\|null\\) does not accept array\\{ClassLoader\\}\\|ThreadedArray\\\\.$#" count: 1 path: ../../../src/thread/Worker.php diff --git a/tests/phpstan/stubs/pthreads.stub b/tests/phpstan/stubs/pthreads.stub index 054c8c372..a48fceea0 100644 --- a/tests/phpstan/stubs/pthreads.stub +++ b/tests/phpstan/stubs/pthreads.stub @@ -1,6 +1,33 @@ + * @implements \IteratorAggregate */ -class Threaded implements \Traversable{} +abstract class ThreadedBase implements \IteratorAggregate{ + + /** + * @template TReturn + * @param \Closure() : TReturn $function + * @param mixed ...$args + * @return TReturn + */ + public function synchronized(\Closure $function, mixed ...$args) : mixed{} +} + +/** + * @template TKey of array-key + * @template TValue + * @implements ArrayAccess + */ +final class ThreadedArray extends ThreadedBase implements Countable, ArrayAccess{ + + /** + * @return TValue|null + */ + public function pop() : mixed{} + + /** + * @return TValue|null + */ + public function shift() : mixed{} +} \ No newline at end of file