Use a subprocess for reading lines from STDIN (#4332)

this FINALLY provides us with a way to deal with Windows without needing to forcibly terminate the entire server on shutdown.
This commit is contained in:
Dylan T 2021-07-24 22:10:50 +01:00 committed by GitHub
parent 772935cd7e
commit 1246d1b7ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 121 additions and 22 deletions

View File

@ -0,0 +1,49 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\console;
use function cli_set_process_title;
use function count;
use function dirname;
use function fwrite;
use function stream_socket_client;
require dirname(__DIR__, 2) . '/vendor/autoload.php';
if(count($argv) !== 2){
die("Please provide a server to connect to");
}
@cli_set_process_title('PocketMine-MP Console Reader');
$socket = stream_socket_client($argv[1]);
if($socket === false){
throw new \RuntimeException("Failed to connect to server process");
}
$consoleReader = new ConsoleReader();
while(true){
$line = $consoleReader->readLine();
if($line !== null){
fwrite($socket, $line . "\n");
}
}

View File

@ -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{

View File

@ -10,6 +10,11 @@ parameters:
count: 2
path: ../../../src/console/ConsoleReader.php
-
message: "#^Parameter \\#1 \\$command of function proc_open expects string, array\\<int, string\\> given\\.$#"
count: 1
path: ../../../src/console/ConsoleReaderThread.php
-
message: "#^Method pocketmine\\\\crafting\\\\CraftingManager\\:\\:getDestructorCallbacks\\(\\) should return pocketmine\\\\utils\\\\ObjectSet\\<Closure\\(\\)\\: void\\> but returns pocketmine\\\\utils\\\\ObjectSet\\<Closure\\(\\)\\: void\\>\\|pocketmine\\\\utils\\\\ObjectSet\\<object\\>\\.$#"
count: 1