RCON: lots of cleanup, now notification-based instead of poll-based

This now utilizes Snooze in order to have the server wake up to process RCON commands ondemand, similar to how the CommandReader thread operates. This is better for performance and response times.

This also makes a few other changes:
- RCON thread will now waste less CPU since it uses a blocking select() with timeout to read
- Following from that, IPC sockets are used to allow interrupting select() from the RCON thread.
- Multiple threads for RCON has been removed (this is entirely unnecessary, reading data from sockets is not CPU-intensive, and a single thread is easier to work with)
This commit is contained in:
Dylan K. Taylor
2018-05-10 12:33:05 +01:00
parent 1e4a97f921
commit 9c5f7128a4
3 changed files with 133 additions and 132 deletions

View File

@ -30,6 +30,7 @@ namespace pocketmine\network\rcon;
use pocketmine\command\RemoteConsoleCommandSender;
use pocketmine\event\server\RemoteServerCommandEvent;
use pocketmine\Server;
use pocketmine\snooze\SleeperNotifier;
use pocketmine\utils\TextFormat;
class RCON{
@ -37,25 +38,22 @@ class RCON{
private $server;
/** @var resource */
private $socket;
/** @var string */
private $password;
/** @var int */
private $threads;
/** @var RCONInstance[] */
private $workers = [];
/** @var int */
private $clientsPerThread;
public function __construct(Server $server, string $password, int $port = 19132, string $interface = "0.0.0.0", int $threads = 1, int $clientsPerThread = 50){
/** @var RCONInstance */
private $instance;
/** @var resource */
private $ipcMainSocket;
/** @var resource */
private $ipcThreadSocket;
public function __construct(Server $server, string $password, int $port = 19132, string $interface = "0.0.0.0", int $maxClients = 50){
$this->server = $server;
$this->password = $password;
$this->server->getLogger()->info("Starting remote control listener");
if($this->password === ""){
if($password === ""){
throw new \InvalidArgumentException("Empty password");
}
$this->threads = (int) max(1, $threads);
$this->clientsPerThread = (int) max(1, $clientsPerThread);
$this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if($this->socket === false or !@socket_bind($this->socket, $interface, $port) or !@socket_listen($this->socket)){
@ -64,52 +62,51 @@ class RCON{
socket_set_block($this->socket);
for($n = 0; $n < $this->threads; ++$n){
$this->workers[$n] = new RCONInstance($this->socket, $this->password, $this->clientsPerThread);
$ret = @socket_create_pair(AF_UNIX, SOCK_STREAM, 0, $ipc);
if(!$ret){
$err = socket_last_error();
if(($err !== SOCKET_EPROTONOSUPPORT and $err !== SOCKET_ENOPROTOOPT) or !@socket_create_pair(AF_INET, SOCK_STREAM, 0, $ipc)){
throw new \RuntimeException(trim(socket_strerror(socket_last_error())));
}
}
[$this->ipcMainSocket, $this->ipcThreadSocket] = $ipc;
$notifier = new SleeperNotifier();
$this->server->getTickSleeper()->addNotifier($notifier, function() : void{
$this->check();
});
$this->instance = new RCONInstance($this->socket, $password, (int) max(1, $maxClients), $this->server->getLogger(), $this->ipcThreadSocket, $notifier);
socket_getsockname($this->socket, $addr, $port);
$this->server->getLogger()->info("RCON running on $addr:$port");
}
public function stop(){
for($n = 0; $n < $this->threads; ++$n){
$this->workers[$n]->close();
Server::microSleep(50000);
$this->workers[$n]->quit();
}
$this->instance->close();
socket_write($this->ipcMainSocket, "\x00"); //make select() return
Server::microSleep(50000);
$this->instance->quit();
@socket_close($this->socket);
$this->threads = 0;
@socket_close($this->ipcMainSocket);
@socket_close($this->ipcThreadSocket);
}
public function check(){
for($n = 0; $n < $this->threads; ++$n){
if($this->workers[$n]->isTerminated()){
$this->workers[$n] = new RCONInstance($this->socket, $this->password, $this->clientsPerThread);
}elseif($this->workers[$n]->isWaiting()){
if($this->workers[$n]->response !== ""){
$this->server->getLogger()->info($this->workers[$n]->response);
$this->workers[$n]->synchronized(function(RCONInstance $thread){
$thread->notify();
}, $this->workers[$n]);
}else{
$response = new RemoteConsoleCommandSender();
$command = $this->instance->cmd;
$response = new RemoteConsoleCommandSender();
$command = $this->workers[$n]->cmd;
$this->server->getPluginManager()->callEvent($ev = new RemoteServerCommandEvent($response, $command));
$this->server->getPluginManager()->callEvent($ev = new RemoteServerCommandEvent($response, $command));
if(!$ev->isCancelled()){
$this->server->dispatchCommand($ev->getSender(), $ev->getCommand());
}
$this->workers[$n]->response = TextFormat::clean($response->getMessage());
$this->workers[$n]->synchronized(function(RCONInstance $thread){
$thread->notify();
}, $this->workers[$n]);
}
}
if(!$ev->isCancelled()){
$this->server->dispatchCommand($ev->getSender(), $ev->getCommand());
}
$this->instance->response = TextFormat::clean($response->getMessage());
$this->instance->synchronized(function(RCONInstance $thread){
$thread->notify();
}, $this->instance);
}
}