mojira.dev
MC-92255

Singleplayer freezes instead of kicking a player

This is a issue which happened before 1.9 snapshots but not every time. Now in the 1.9 snapshots it seems to happen every time.

When the player gets kicked in a singleplayer world for example for invalid movement, the client freezes instead of leaving the world properly:

[20:27:39] [Server thread/WARN]: Marcono1234 was kicked for floating a vehicle too long!
[20:27:39] [Server thread/INFO]: Marcono1234 lost connection: TextComponent{text='Disconnected', siblings=[], style=Style{hasParent=false, color=null, bold=null, italic=null, underlined=null, obfuscated=null, clickEvent=null, hoverEvent=null, insertion=null}}
[20:27:39] [Server thread/INFO]: Marcono1234 left the game
[20:27:39] [Server thread/INFO]: Stopping singleplayer server as player logged out
[20:27:40] [Server thread/INFO]: Stopping server
[20:27:40] [Server thread/INFO]: Saving players
[20:27:40] [Server thread/INFO]: Saving worlds
[20:27:40] [Server thread/INFO]: Saving chunks for level 'End Test'/Overworld
[20:27:40] [Server thread/INFO]: Saving chunks for level 'End Test'/Nether
[20:27:40] [Server thread/INFO]: Saving chunks for level 'End Test'/The End

How to reproduce

Use the following command (see MC-79818):

/tellraw @p {"text":"Click me","clickEvent":{"action":"run_command","value":"\u00A7"}}

The reason

In Minecraft 1.8 (decompiled using MCP) the reason for this to happen seems to be that the initiateShutdown() method of the /Client/src/net/minecraft/server/integrated/IntegratedServer.java class is called twice. One time by the server because the hosting player left the game and the second time by the client when the main menu is loaded (or the other way around). Only the server should probably call this method.

The following displays in which order the methods are called. However this is not correct with the timing as the server calls the initiateShutdown() method after the client called it.

Server

Client

/Client/src/net/minecraft/network/NetHandlerPlayServer.java
-> kickPlayerFromServer(String reason)

/**
 * Kick a player from the server with a reason
 */
public void kickPlayerFromServer(String reason)
{
	// Client side: Packets and Channel closing
	//...
	
	// Server side
	this.netManager.disableAutoRead();
	Futures.getUnchecked(this.serverController.addScheduledTask(new Runnable()
	{
		private static final String __OBFID = "CL_00001454";
		public void run()
		{
			NetHandlerPlayServer.this.netManager.checkDisconnected();
		}
	}));
}

/Client/src/net/minecraft/client/Minecraft.java
-> runTick()

/**
 * Runs the current tick.
 */
public void runTick() throws IOException
{
	//...
	
	if (!this.isGamePaused && this.theWorld != null)
	{
		this.playerController.updateController();
	}
	
	//...
}

/Client/src/net/minecraft/network/NetworkManager.java
-> checkDisconnected()

public void checkDisconnected()
{
	if (!this.hasNoChannel() && !this.isChannelOpen() && !this.disconnected)
	{
		this.disconnected = true;

		if (this.getExitMessage() != null)
		{
			this.getNetHandler().onDisconnect(this.getExitMessage());
		}
		else if (this.getNetHandler() != null)
		{
			this.getNetHandler().onDisconnect(new ChatComponentText("Disconnected"));
		}
	}
}

/Client/src/net/minecraft/client/multiplayer/PlayerControllerMP.java
-> updateController()

public void updateController()
{
	//...

	if (this.netClientHandler.getNetworkManager().isChannelOpen())
	{
		this.netClientHandler.getNetworkManager().processReceivedPackets();
	}
	else
	{
		this.netClientHandler.getNetworkManager().checkDisconnected();
	}
}

/Client/src/net/minecraft/network/NetHandlerPlayServer.java
-> onDisconnect(IChatComponent reason)

/**
 * Invoked when disconnecting, the parameter is a ChatComponent describing the reason for termination
 */
public void onDisconnect(IChatComponent reason)
{
	//...
	
	if (this.serverController.isSinglePlayer() && this.playerEntity.getName().equals(this.serverController.getServerOwner()))
	{
		logger.info("Stopping singleplayer server as player logged out");
		this.serverController.initiateShutdown();
	}
}

/Client/src/net/minecraft/network/NetworkManager.java
-> checkDisconnected()

public void checkDisconnected()
{
	if (!this.hasNoChannel() && !this.isChannelOpen() && !this.disconnected)
	{
		this.disconnected = true;

		if (this.getExitMessage() != null)
		{
			this.getNetHandler().onDisconnect(this.getExitMessage());
		}
		else if (this.getNetHandler() != null)
		{
			this.getNetHandler().onDisconnect(new ChatComponentText("Disconnected"));
		}
	}
}

-

/Client/src/net/minecraft/client/network/NetHandlerPlayClient.java
-> public void onDisconnect(IChatComponent reason)

/**
 * Invoked when disconnecting, the parameter is a ChatComponent describing the reason for termination
 */
public void onDisconnect(IChatComponent reason)
{
	this.gameController.loadWorld((WorldClient)null);

	//...
}

-

/Client/src/net/minecraft/client/Minecraft.java
-> loadWorld(WorldClient worldClient)

/**
 * unloads the current world first
 */
public void loadWorld(WorldClient worldClientIn)
{
	this.loadWorld(worldClientIn, "");
}

-> loadWorld(WorldClient worldClientIn, String loadingMessage)

/**
 * par2Str is displayed on the loading screen to the user unloads the current world first
 */
public void loadWorld(WorldClient worldClientIn, String loadingMessage)
{
	if (worldClientIn == null)
	{
		NetHandlerPlayClient var3 = this.getNetHandler();

		if (var3 != null)
		{
			var3.cleanup();
		}

		if (this.theIntegratedServer != null && this.theIntegratedServer.func_175578_N())
		{
			// This should probably be only handled by the server
			this.theIntegratedServer.initiateShutdown();
			this.theIntegratedServer.func_175592_a();
		}

		this.theIntegratedServer = null;
		//...
	}
	//...
}

Related issues

Comments

SunCat

Can't reproduce

marcono1234

What can't you reproduce? The cases in which the player is kicked or doesn't the client freeze for you?

I added a command to reproduce as well

SunCat

The first one, thanks =)

bemoty

Can confirm for MC 1.12.1.

pokechu22

Appears to have been fixed in 18w01a. IntegratedServer.initiateShutdown is only called once (from the client thread) in 18w01a, while it was called on both the server and client threads in prior versions. Checked with JDB, by setting an appropriate breakpoint; in 1.12.2 it is chd.x; in 17w50a it is (per forge's MCPInfo) clm.q; and in 18w01a it is cmd.q. Unfortunately I'm not entirely sure why it's not getting called twice anymore, as I don't see what changed.

pokechu22

Happening again as per MC-72943 (this issue is effectively a duplicate of that, but for a different means of reproduction). I haven't personally reproduced this variant, but I can confirm that the methods are again called twice in 18w20a, but not in 18w19b.

marcono1234

coschevi

Confirmed

freeze, kick, player, singleplayer

Minecraft 15w45a, Minecraft 15w46a, Minecraft 15w50a, Minecraft 1.10.2, Minecraft 16w32b, ..., Minecraft 1.12, Minecraft 1.12.1, Minecraft 1.12.2, Minecraft 1.13-pre1, Minecraft 1.13-pre3

Minecraft 18w01a, Minecraft 1.13-pre6

Retrieved