diff --git a/src/Server.php b/src/Server.php index f3f1e4e6a..702739dc5 100644 --- a/src/Server.php +++ b/src/Server.php @@ -28,7 +28,7 @@ declare(strict_types=1); namespace pocketmine; use pocketmine\command\Command; -use pocketmine\command\CommandReader; +use pocketmine\command\CommandReaderThread; use pocketmine\command\CommandSender; use pocketmine\command\ConsoleCommandSender; use pocketmine\command\SimpleCommandMap; @@ -227,7 +227,7 @@ class Server{ /** @var MemoryManager */ private $memoryManager; - /** @var CommandReader */ + /** @var CommandReaderThread */ private $console; /** @var SimpleCommandMap */ @@ -1133,11 +1133,12 @@ class Server{ $this->subscribeToBroadcastChannel(self::BROADCAST_CHANNEL_USERS, $consoleSender); $consoleNotifier = new SleeperNotifier(); - $this->console = new CommandReader($consoleNotifier); - $this->tickSleeper->addNotifier($consoleNotifier, function() use ($consoleSender) : void{ + $commandBuffer = new \Threaded(); + $this->console = new CommandReaderThread($commandBuffer, $consoleNotifier); + $this->tickSleeper->addNotifier($consoleNotifier, function() use ($commandBuffer, $consoleSender) : void{ Timings::$serverCommand->startTiming(); - while(($line = $this->console->getLine()) !== null){ - $this->dispatchCommand($consoleSender, $line); + while(($line = $commandBuffer->shift()) !== null){ + $this->dispatchCommand($consoleSender, (string) $line); } Timings::$serverCommand->stopTiming(); }); @@ -1422,7 +1423,7 @@ class Server{ $this->configGroup->save(); } - if($this->console instanceof CommandReader){ + if($this->console instanceof CommandReaderThread){ $this->getLogger()->debug("Closing console"); $this->console->shutdown(); $this->console->notify(); diff --git a/src/command/CommandReader.php b/src/command/CommandReader.php index ac74145ec..0aeefdf2b 100644 --- a/src/command/CommandReader.php +++ b/src/command/CommandReader.php @@ -23,149 +23,58 @@ declare(strict_types=1); namespace pocketmine\command; -use pocketmine\snooze\SleeperNotifier; -use pocketmine\thread\Thread; -use pocketmine\thread\ThreadException; use function fclose; use function fgets; use function fopen; -use function fstat; use function is_resource; -use function microtime; -use function preg_replace; -use function readline; -use function stream_isatty; use function stream_select; use function trim; use function usleep; -class CommandReader extends Thread{ - - public const TYPE_READLINE = 0; - public const TYPE_STREAM = 1; - public const TYPE_PIPED = 2; - +final class CommandReader{ /** @var resource */ - private static $stdin; + private $stdin; - /** @var \Threaded */ - protected $buffer; - /** @var bool */ - private $shutdown = false; - /** @var int */ - private $type = self::TYPE_STREAM; - - /** @var SleeperNotifier|null */ - private $notifier; - - public function __construct(?SleeperNotifier $notifier = null){ - $this->buffer = new \Threaded; - $this->notifier = $notifier; - } - - public function shutdown() : void{ - $this->shutdown = true; - } - - public function quit() : void{ - $wait = microtime(true) + 0.5; - while(microtime(true) < $wait){ - if($this->isRunning()){ - usleep(100000); - }else{ - parent::quit(); - return; - } - } - - $message = "Thread blocked for unknown reason"; - if($this->type === self::TYPE_PIPED){ - $message = "STDIN is being piped from another location and the pipe is blocked, cannot stop safely"; - } - - throw new ThreadException($message); + public function __construct(){ + $this->initStdin(); } private function initStdin() : void{ - if(is_resource(self::$stdin)){ - fclose(self::$stdin); + if(is_resource($this->stdin)){ + fclose($this->stdin); } - self::$stdin = fopen("php://stdin", "r"); - if($this->isPipe(self::$stdin)){ - $this->type = self::TYPE_PIPED; - }else{ - $this->type = self::TYPE_STREAM; - } - } - - /** - * Checks if the specified stream is a FIFO pipe. - * - * @param resource $stream - */ - private function isPipe($stream) : bool{ - return is_resource($stream) and (!stream_isatty($stream) or ((fstat($stream)["mode"] & 0170000) === 0010000)); + $this->stdin = fopen("php://stdin", "r"); } /** * Reads a line from the console and adds it to the buffer. This method may block the thread. - * - * @return bool if the main execution should continue reading lines */ - private function readLine() : bool{ - if(!is_resource(self::$stdin)){ + public function readLine() : ?string{ + if(!is_resource($this->stdin)){ $this->initStdin(); } - $r = [self::$stdin]; + $r = [$this->stdin]; $w = $e = null; if(($count = stream_select($r, $w, $e, 0, 200000)) === 0){ //nothing changed in 200000 microseconds - return true; + return null; }elseif($count === false){ //stream error $this->initStdin(); } - if(($raw = fgets(self::$stdin)) === false){ //broken pipe or EOF + if(($raw = fgets($this->stdin)) === false){ //broken pipe or EOF $this->initStdin(); - $this->synchronized(function() : void{ - $this->wait(200000); - }); //prevent CPU waste if it's end of pipe - return true; //loop back round + usleep(200000); //prevent CPU waste if it's end of pipe + return null; //loop back round } $line = trim($raw); - if($line !== ""){ - $this->buffer[] = preg_replace("#\\x1b\\x5b([^\\x1b]*\\x7e|[\\x40-\\x50])#", "", $line); - if($this->notifier !== null){ - $this->notifier->wakeupSleeper(); - } - } - - return true; + return $line !== "" ? $line : null; } - /** - * Reads a line from console, if available. Returns null if not available - */ - public function getLine() : ?string{ - if($this->buffer->count() !== 0){ - return (string) $this->buffer->shift(); - } - - return null; - } - - protected function onRun() : void{ - $this->initStdin(); - - while(!$this->shutdown and $this->readLine()); - - fclose(self::$stdin); - } - - public function getThreadName() : string{ - return "Console"; + public function __destruct(){ + fclose($this->stdin); } } diff --git a/src/command/CommandReaderThread.php b/src/command/CommandReaderThread.php new file mode 100644 index 000000000..5ae04a1c5 --- /dev/null +++ b/src/command/CommandReaderThread.php @@ -0,0 +1,82 @@ +buffer = $buffer; + $this->notifier = $notifier; + } + + public function shutdown() : void{ + $this->shutdown = true; + } + + public function quit() : void{ + $wait = microtime(true) + 0.5; + while(microtime(true) < $wait){ + if($this->isRunning()){ + usleep(100000); + }else{ + parent::quit(); + return; + } + } + + throw new ThreadException("CommandReader is stuck in a blocking STDIN read"); + } + + protected function onRun() : void{ + $buffer = $this->buffer; + $notifier = $this->notifier; + + $reader = new CommandReader(); + while(!$this->shutdown){ + $line = $reader->readLine(); + + if($line !== null){ + $buffer[] = preg_replace("#\\x1b\\x5b([^\\x1b]*\\x7e|[\\x40-\\x50])#", "", $line); + if($notifier !== null){ + $notifier->wakeupSleeper(); + } + } + } + } + + public function getThreadName() : string{ + return "Console"; + } +} diff --git a/tests/phpstan/configs/actual-problems.neon b/tests/phpstan/configs/actual-problems.neon index 21c05e8af..49b2c5f07 100644 --- a/tests/phpstan/configs/actual-problems.neon +++ b/tests/phpstan/configs/actual-problems.neon @@ -6,7 +6,7 @@ parameters: path: ../../../src/CrashDump.php - - message: "#^Instanceof between pocketmine\\\\command\\\\CommandReader and pocketmine\\\\command\\\\CommandReader will always evaluate to true\\.$#" + message: "#^Instanceof between pocketmine\\\\command\\\\CommandReaderThread and pocketmine\\\\command\\\\CommandReaderThread will always evaluate to true\\.$#" count: 1 path: ../../../src/Server.php diff --git a/tests/phpstan/configs/check-explicit-mixed-baseline.neon b/tests/phpstan/configs/check-explicit-mixed-baseline.neon index d746d6d60..a26af9506 100644 --- a/tests/phpstan/configs/check-explicit-mixed-baseline.neon +++ b/tests/phpstan/configs/check-explicit-mixed-baseline.neon @@ -37,7 +37,7 @@ parameters: - message: "#^Cannot cast mixed to string\\.$#" - count: 1 + count: 2 path: ../../../src/Server.php - @@ -80,11 +80,6 @@ parameters: count: 2 path: ../../../src/VersionInfo.php - - - message: "#^Cannot cast mixed to string\\.$#" - count: 1 - path: ../../../src/command/CommandReader.php - - message: "#^Part \\$host \\(mixed\\) of encapsed string cannot be cast to string\\.$#" count: 1 diff --git a/tests/phpstan/configs/l7-baseline.neon b/tests/phpstan/configs/l7-baseline.neon index 496df8311..f3d116666 100644 --- a/tests/phpstan/configs/l7-baseline.neon +++ b/tests/phpstan/configs/l7-baseline.neon @@ -436,17 +436,7 @@ parameters: path: ../../../src/block/tile/TileFactory.php - - message: "#^Cannot access offset 'mode' on array\\(0 \\=\\> int, 1 \\=\\> int, 2 \\=\\> int, 3 \\=\\> int, 4 \\=\\> int, 5 \\=\\> int, 6 \\=\\> int, 7 \\=\\> int, \\.\\.\\.\\)\\|false\\.$#" - count: 1 - path: ../../../src/command/CommandReader.php - - - - message: "#^Parameter \\#1 \\$stream of method pocketmine\\\\command\\\\CommandReader\\:\\:isPipe\\(\\) expects resource, resource\\|false given\\.$#" - count: 1 - path: ../../../src/command/CommandReader.php - - - - message: "#^Static property pocketmine\\\\command\\\\CommandReader\\:\\:\\$stdin \\(resource\\) does not accept resource\\|false\\.$#" + message: "#^Property pocketmine\\\\command\\\\CommandReader\\:\\:\\$stdin \\(resource\\) does not accept resource\\|false\\.$#" count: 1 path: ../../../src/command/CommandReader.php