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->call();
if($ev->isCancelled()){
$this->close($this->getLeaveMessage(), $ev->getKickMessage());
if($ev->isCancelled() or !$this->isConnected()){
$this->disconnect($ev->getKickMessage());
return;
}
@ -758,7 +758,7 @@ class Player extends Human implements CommandSender, ChunkLoader, ChunkListener,
* @return 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";
}
}
$this->close($ev->getQuitMessage(), $message);
$this->disconnect($message, $ev->getQuitMessage());
return true;
}
@ -2488,81 +2488,92 @@ class Player extends Human implements CommandSender, ChunkLoader, ChunkListener,
}
/**
* Note for plugin developers: use kick() with the isAdmin
* flag set to kick without the "Kicked by admin" part instead of this method.
* Removes the player from the server. This cannot be cancelled.
* 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
* @param string $reason Reason showed in console
* Note for plugin developers: Prefer kick() with the isAdmin flag set to kick without the "Kicked by admin" part
* 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
*/
final public function close($message = "", string $reason = "generic reason", bool $notify = true) : void{
if($this->isConnected() and !$this->closed){
$ip = $this->networkSession->getIp();
$port = $this->networkSession->getPort();
$this->networkSession->onPlayerDestroyed($reason, $notify);
public function disconnect(string $reason, $quitMessage = null, bool $notify = true) : void{
if(!$this->isConnected()){
return;
}
$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;
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->craftingGrid = null;
parent::close();
$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->perm = null;
if($this->perm !== null){
$this->perm->clearPermissions();
$this->perm = null;
}
parent::close();
}
}

View File

@ -167,7 +167,7 @@ class LevelManager{
$this->server->getLogger()->info($this->server->getLanguage()->translateString("pocketmine.level.unloading", [$level->getDisplayName()]));
foreach($level->getPlayers() as $player){
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){
$player->teleport($this->levelDefault->getSafeSpawn());
}

View File

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