Player: Disconnects no longer nuke player internals, (mostly) fixes #1239

there are some problems that haven't been accounted for yet, but this fixes all the direct crashes.
This commit is contained in:
Dylan K. Taylor 2019-04-03 17:44:34 +01:00
parent a0b8d9a64e
commit 0e3e984db9
3 changed files with 107 additions and 85 deletions

View File

@ -339,8 +339,8 @@ class Player extends Human implements CommandSender, ChunkLoader, ChunkListener,
$ev = new PlayerLoginEvent($this, "Plugin reason"); $ev = new PlayerLoginEvent($this, "Plugin reason");
$ev->call(); $ev->call();
if($ev->isCancelled()){ if($ev->isCancelled() or !$this->isConnected()){
$this->close($this->getLeaveMessage(), $ev->getKickMessage()); $this->disconnect($ev->getKickMessage());
return; return;
} }
@ -758,7 +758,7 @@ class Player extends Human implements CommandSender, ChunkLoader, ChunkListener,
* @return bool * @return bool
*/ */
public function isConnected() : bool{ public function isConnected() : bool{
return $this->networkSession !== null; return $this->networkSession !== null and $this->networkSession->isConnected();
} }
/** /**
@ -2479,7 +2479,7 @@ class Player extends Human implements CommandSender, ChunkLoader, ChunkListener,
$message = "disconnectionScreen.noReason"; $message = "disconnectionScreen.noReason";
} }
} }
$this->close($ev->getQuitMessage(), $message); $this->disconnect($message, $ev->getQuitMessage());
return true; return true;
} }
@ -2488,81 +2488,92 @@ class Player extends Human implements CommandSender, ChunkLoader, ChunkListener,
} }
/** /**
* Note for plugin developers: use kick() with the isAdmin * Removes the player from the server. This cannot be cancelled.
* flag set to kick without the "Kicked by admin" part instead of this method. * This is used for remote disconnects and for uninterruptible disconnects (for example, when the server shuts down).
* *
* @param TextContainer|string $message Message to be broadcasted * Note for plugin developers: Prefer kick() with the isAdmin flag set to kick without the "Kicked by admin" part
* @param string $reason Reason showed in console * instead of this method. This way other plugins can have a say in whether the player is removed or not.
*
* @param string $reason Shown to the player, usually this will appear on their disconnect screen.
* @param TextContainer|string $quitMessage Message to broadcast to online players (null will use default)
* @param bool $notify * @param bool $notify
*/ */
final public function close($message = "", string $reason = "generic reason", bool $notify = true) : void{ public function disconnect(string $reason, $quitMessage = null, bool $notify = true) : void{
if($this->isConnected() and !$this->closed){ if(!$this->isConnected()){
$ip = $this->networkSession->getIp(); return;
$port = $this->networkSession->getPort(); }
$this->networkSession->onPlayerDestroyed($reason, $notify);
$this->networkSession->onPlayerDestroyed($reason, $notify);
//prevent the player receiving their own disconnect message
PermissionManager::getInstance()->unsubscribeFromPermission(Server::BROADCAST_CHANNEL_USERS, $this);
PermissionManager::getInstance()->unsubscribeFromPermission(Server::BROADCAST_CHANNEL_ADMINISTRATIVE, $this);
$ev = new PlayerQuitEvent($this, $quitMessage ?? $this->getLeaveMessage(), $reason);
$ev->call();
if(!empty($ev->getQuitMessage())){
$this->server->broadcastMessage($ev->getQuitMessage());
}
$this->save();
$this->server->getLogger()->info($this->getServer()->getLanguage()->translateString("pocketmine.player.logOut", [
TextFormat::AQUA . $this->getName() . TextFormat::WHITE,
$this->networkSession->getIp(),
$this->networkSession->getPort(),
$this->getServer()->getLanguage()->translateString($reason)
]));
$this->spawned = false;
$this->stopSleep();
$this->despawnFromAll();
$this->server->removeOnlinePlayer($this);
foreach($this->server->getOnlinePlayers() as $player){
if(!$player->canSee($this)){
$player->showPlayer($this);
}
}
$this->hiddenPlayers = [];
if($this->isValid()){
foreach($this->usedChunks as $index => $d){
Level::getXZ($index, $chunkX, $chunkZ);
$this->level->unregisterChunkLoader($this, $chunkX, $chunkZ);
$this->level->unregisterChunkListener($this, $chunkX, $chunkZ);
foreach($this->level->getChunkEntities($chunkX, $chunkZ) as $entity){
$entity->despawnFrom($this);
}
unset($this->usedChunks[$index]);
}
}
$this->usedChunks = [];
$this->loadQueue = [];
$this->removeAllWindows(true);
$this->windows = [];
$this->windowIndex = [];
$this->perm->clearPermissions();
$this->flagForDespawn();
}
final public function close() : void{
if(!$this->closed){
$this->disconnect("Player destroyed");
$this->networkSession = null; $this->networkSession = null;
PermissionManager::getInstance()->unsubscribeFromPermission(Server::BROADCAST_CHANNEL_USERS, $this);
PermissionManager::getInstance()->unsubscribeFromPermission(Server::BROADCAST_CHANNEL_ADMINISTRATIVE, $this);
$this->stopSleep();
if($this->spawned){
$ev = new PlayerQuitEvent($this, $message, $reason);
$ev->call();
if($ev->getQuitMessage() != ""){
$this->server->broadcastMessage($ev->getQuitMessage());
}
$this->save();
}
if($this->isValid()){
foreach($this->usedChunks as $index => $d){
Level::getXZ($index, $chunkX, $chunkZ);
$this->level->unregisterChunkLoader($this, $chunkX, $chunkZ);
$this->level->unregisterChunkListener($this, $chunkX, $chunkZ);
foreach($this->level->getChunkEntities($chunkX, $chunkZ) as $entity){
$entity->despawnFrom($this);
}
unset($this->usedChunks[$index]);
}
}
$this->usedChunks = [];
$this->loadQueue = [];
foreach($this->server->getOnlinePlayers() as $player){
if(!$player->canSee($this)){
$player->showPlayer($this);
}
}
$this->hiddenPlayers = [];
$this->removeAllWindows(true);
$this->windows = [];
$this->windowIndex = [];
$this->cursorInventory = null; $this->cursorInventory = null;
$this->craftingGrid = null; $this->craftingGrid = null;
parent::close();
$this->spawned = false; $this->spawned = false;
$this->server->removeOnlinePlayer($this);
$this->server->getLogger()->info($this->getServer()->getLanguage()->translateString("pocketmine.player.logOut", [
TextFormat::AQUA . $this->getName() . TextFormat::WHITE,
$ip,
$port,
$this->getServer()->getLanguage()->translateString($reason)
]));
$this->spawnPosition = null; $this->spawnPosition = null;
$this->perm = null;
if($this->perm !== null){ parent::close();
$this->perm->clearPermissions();
$this->perm = null;
}
} }
} }

View File

@ -167,7 +167,7 @@ class LevelManager{
$this->server->getLogger()->info($this->server->getLanguage()->translateString("pocketmine.level.unloading", [$level->getDisplayName()])); $this->server->getLogger()->info($this->server->getLanguage()->translateString("pocketmine.level.unloading", [$level->getDisplayName()]));
foreach($level->getPlayers() as $player){ foreach($level->getPlayers() as $player){
if($level === $this->levelDefault or $this->levelDefault === null){ if($level === $this->levelDefault or $this->levelDefault === null){
$player->close($player->getLeaveMessage(), "Forced default world unload"); $player->disconnect("Forced default world unload");
}elseif($this->levelDefault instanceof Level){ }elseif($this->levelDefault instanceof Level){
$player->teleport($this->levelDefault->getSafeSpawn()); $player->teleport($this->levelDefault->getSafeSpawn());
} }

View File

@ -106,6 +106,8 @@ class NetworkSession{
/** @var bool */ /** @var bool */
private $connected = true; private $connected = true;
/** @var bool */ /** @var bool */
private $disconnectGuard = false;
/** @var bool */
private $loggedIn = false; private $loggedIn = false;
/** @var bool */ /** @var bool */
private $authenticated = false; private $authenticated = false;
@ -395,13 +397,14 @@ class NetworkSession{
$this->interface->putPacket($this, $payload, $immediate); $this->interface->putPacket($this, $payload, $immediate);
} }
private function checkDisconnect() : bool{ private function tryDisconnect(\Closure $func) : void{
if($this->connected){ if($this->connected and !$this->disconnectGuard){
$this->disconnectGuard = true;
$func();
$this->disconnectGuard = false;
$this->connected = false; $this->connected = false;
$this->manager->remove($this); $this->manager->remove($this);
return true;
} }
return false;
} }
/** /**
@ -411,12 +414,12 @@ class NetworkSession{
* @param bool $notify * @param bool $notify
*/ */
public function disconnect(string $reason, bool $notify = true) : void{ public function disconnect(string $reason, bool $notify = true) : void{
if($this->checkDisconnect()){ $this->tryDisconnect(function() use($reason, $notify){
if($this->player !== null){ if($this->player !== null){
$this->player->close($this->player->getLeaveMessage(), $reason); $this->player->disconnect($reason, null, $notify);
} }
$this->doServerDisconnect($reason, $notify); $this->doServerDisconnect($reason, $notify);
} });
} }
/** /**
@ -429,11 +432,17 @@ class NetworkSession{
* @throws \UnsupportedOperationException * @throws \UnsupportedOperationException
*/ */
public function transfer(string $ip, int $port, string $reason = "transfer") : void{ public function transfer(string $ip, int $port, string $reason = "transfer") : void{
$pk = new TransferPacket(); $this->tryDisconnect(function() use($ip, $port, $reason){
$pk->address = $ip; $pk = new TransferPacket();
$pk->port = $port; $pk->address = $ip;
$this->sendDataPacket($pk, true); $pk->port = $port;
$this->disconnect($reason); $this->sendDataPacket($pk, true);
$this->disconnect($reason, false);
if($this->player !== null){
$this->player->disconnect($reason, null, false);
}
$this->doServerDisconnect($reason, false);
});
} }
/** /**
@ -443,9 +452,9 @@ class NetworkSession{
* @param bool $notify * @param bool $notify
*/ */
public function onPlayerDestroyed(string $reason, bool $notify = true) : void{ public function onPlayerDestroyed(string $reason, bool $notify = true) : void{
if($this->checkDisconnect()){ $this->tryDisconnect(function() use($reason, $notify){
$this->doServerDisconnect($reason, $notify); $this->doServerDisconnect($reason, $notify);
} });
} }
/** /**
@ -472,9 +481,11 @@ class NetworkSession{
* @param string $reason * @param string $reason
*/ */
public function onClientDisconnect(string $reason) : void{ public function onClientDisconnect(string $reason) : void{
if($this->checkDisconnect() and $this->player !== null){ $this->tryDisconnect(function() use($reason){
$this->player->close($this->player->getLeaveMessage(), $reason); if($this->player !== null){
} $this->player->disconnect($reason, null, false);
}
});
} }
public function setAuthenticationStatus(bool $authenticated, bool $authRequired, ?string $error) : bool{ public function setAuthenticationStatus(bool $authenticated, bool $authRequired, ?string $error) : bool{