diff --git a/src/console/ConsoleReaderChildProcess.php b/src/console/ConsoleReaderChildProcess.php new file mode 100644 index 000000000..ea827ecc8 --- /dev/null +++ b/src/console/ConsoleReaderChildProcess.php @@ -0,0 +1,49 @@ +readLine(); + if($line !== null){ + fwrite($socket, $line . "\n"); + } +} diff --git a/src/console/ConsoleReaderThread.php b/src/console/ConsoleReaderThread.php index accc6a839..2fc139b3e 100644 --- a/src/console/ConsoleReaderThread.php +++ b/src/console/ConsoleReaderThread.php @@ -25,10 +25,21 @@ namespace pocketmine\console; use pocketmine\snooze\SleeperNotifier; use pocketmine\thread\Thread; -use pocketmine\thread\ThreadException; -use function microtime; +use pocketmine\utils\AssumptionFailedError; +use Webmozart\PathUtil\Path; +use function fgets; +use function fopen; use function preg_replace; -use function usleep; +use function proc_open; +use function proc_terminate; +use function sprintf; +use function stream_select; +use function stream_socket_accept; +use function stream_socket_get_name; +use function stream_socket_server; +use function stream_socket_shutdown; +use const PHP_BINARY; +use const STREAM_SHUT_RDWR; final class ConsoleReaderThread extends Thread{ private \Threaded $buffer; @@ -45,35 +56,69 @@ final class ConsoleReaderThread extends Thread{ $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 ConsoleReader(); - while(!$this->shutdown){ - $line = $reader->readLine(); + /* + * This pile of shit exists because PHP on Windows is broken, and can't handle stream_select() on stdin or pipes + * properly - stdin native triggers stream_select() when a key is pressed, causing it to get stuck in fgets() + * waiting for a line that might never come (and Windows doesn't support character-based reading either), and + * pipes just constantly trigger stream_select() instead of only when data is returned, rendering it useless. + * + * This results in whichever process reads stdin getting stuck on shutdown, which previously forced us to kill + * the entire server process to make it go away. + * + * To get around this problem, we delegate the responsibility of reading stdin to a subprocess, which we can + * then brutally murder when the server shuts down, without killing the entire server process. + * Thankfully, stream_select() actually works properly on sockets, so we can use them for inter-process + * communication. + */ - if($line !== null){ - $buffer[] = preg_replace("#\\x1b\\x5b([^\\x1b]*\\x7e|[\\x40-\\x50])#", "", $line); + $server = stream_socket_server("tcp://127.0.0.1:0"); + if($server === false){ + throw new \RuntimeException("Failed to open console reader socket server"); + } + $address = stream_socket_get_name($server, false); + if($address === false) throw new AssumptionFailedError("stream_socket_get_name() shouldn't return false here"); + + $sub = proc_open( + [PHP_BINARY, '-r', sprintf('require "%s";', Path::join(__DIR__, 'ConsoleReaderChildProcess.php')), $address], + [ + 2 => fopen("php://stderr", "w"), + ], + $pipes + ); + if($sub === false){ + throw new AssumptionFailedError("Something has gone horribly wrong"); + } + $client = stream_socket_accept($server); + if($client === false){ + throw new AssumptionFailedError("stream_socket_accept() returned false"); + } + stream_socket_shutdown($server, STREAM_SHUT_RDWR); + while(!$this->shutdown){ + $r = [$client]; + $w = null; + $e = null; + if(stream_select($r, $w, $e, 0, 200000) === 1){ + $command = fgets($client); + if($command === false){ + throw new AssumptionFailedError("Something has gone horribly wrong"); + } + + $buffer[] = preg_replace("#\\x1b\\x5b([^\\x1b]*\\x7e|[\\x40-\\x50])#", "", $command); if($notifier !== null){ $notifier->wakeupSleeper(); } } } + + //we have no way to signal to the subprocess to shut down gracefully; besides, Windows sucks, and the subprocess + //gets stuck in a blocking fgets() read because stream_select() is a hunk of junk (hence the separate process in + //the first place). + proc_terminate($sub); + stream_socket_shutdown($client, STREAM_SHUT_RDWR); } public function getThreadName() : string{ diff --git a/tests/phpstan/configs/phpstan-bugs.neon b/tests/phpstan/configs/phpstan-bugs.neon index 48a613645..b484b3e8b 100644 --- a/tests/phpstan/configs/phpstan-bugs.neon +++ b/tests/phpstan/configs/phpstan-bugs.neon @@ -10,6 +10,11 @@ parameters: count: 2 path: ../../../src/console/ConsoleReader.php + - + message: "#^Parameter \\#1 \\$command of function proc_open expects string, array\\ given\\.$#" + count: 1 + path: ../../../src/console/ConsoleReaderThread.php + - message: "#^Method pocketmine\\\\crafting\\\\CraftingManager\\:\\:getDestructorCallbacks\\(\\) should return pocketmine\\\\utils\\\\ObjectSet\\ but returns pocketmine\\\\utils\\\\ObjectSet\\\\|pocketmine\\\\utils\\\\ObjectSet\\\\.$#" count: 1