Files
DeltaZern/paper-server/src/main/java/org/spigotmc/RestartCommand.java
T
Zach Brown 2f74bdb56b Properly handle async calls to restart the server
The watchdog thread calls the server restart function asynchronously. Prior to
this change, it attempted to do several non-safe operations from the watchdog
thread, rather than the main. Specifically, because of a separate upstream change,
it causes player entities to be ticked asynchronously, among other things.

This is dangerous.

This patch moves the old handling into a synchronous variant, for calls from the
restart command, and adds separate handling for async calls, such as those from
the watchdog thread.

When calling from the watchdog thread, we cannot assume the main thread is in a
tickable state; it may be completely deadlocked. In order to handle this, we mark
the server as stopping, in order to account for situations where the server should
complete a tick reasonbly soon, i.e. 99% of cases.

Should the server not enter a state where it is stopping within 10 seconds, We
will assume that the server has in fact deadlocked and will proceed to force
kill the server.

This modification does not force restart the server should we actually enter a
deadlocked state where the server is stopping, whereas this will in most cases
exit within a reasonable amount of time, to put a fixed limit on a process that
will have plugins and worlds saving to the disk has a high potential to result
in corruption/dataloss.
2017-05-12 23:34:11 -05:00

180 lines
5.3 KiB
Java

package org.spigotmc;
import java.io.File;
import java.util.List;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.craftbukkit.util.CraftChatMessage;
public class RestartCommand extends Command
{
public RestartCommand(String name)
{
super( name );
this.description = "Restarts the server";
this.usageMessage = "/restart";
this.setPermission( "bukkit.command.restart" );
}
@Override
public boolean execute(CommandSender sender, String currentAlias, String[] args)
{
if ( this.testPermission( sender ) )
{
MinecraftServer.getServer().processQueue.add( new Runnable()
{
@Override
public void run()
{
RestartCommand.restart();
}
} );
}
return true;
}
public static void restart()
{
RestartCommand.restart( SpigotConfig.restartScript );
}
private static void restart(final String restartScript)
{
AsyncCatcher.enabled = false; // Disable async catcher incase it interferes with us
try
{
// Paper - extract method and cleanup
boolean isRestarting = addShutdownHook( restartScript );
if ( isRestarting )
{
System.out.println( "Attempting to restart with " + SpigotConfig.restartScript );
} else
{
System.out.println( "Startup script '" + SpigotConfig.restartScript + "' does not exist! Stopping server." );
}
// Stop the watchdog
WatchdogThread.doStop();
shutdownServer( isRestarting );
// Paper end
} catch ( Exception ex )
{
ex.printStackTrace();
}
}
// Paper start - sync copied from above with minor changes, async added
private static void shutdownServer(boolean isRestarting)
{
if ( MinecraftServer.getServer().isSameThread() )
{
// Kick all players
for ( ServerPlayer p : com.google.common.collect.ImmutableList.copyOf( MinecraftServer.getServer().getPlayerList().players ) )
{
p.connection.disconnect( CraftChatMessage.fromStringOrEmpty( SpigotConfig.restartMessage, true ) );
}
// Give the socket a chance to send the packets
try
{
Thread.sleep( 100 );
} catch ( InterruptedException ex )
{
}
closeSocket();
// Actually shutdown
try
{
MinecraftServer.getServer().close(); // calls stop()
} catch ( Throwable t )
{
}
// Actually stop the JVM
System.exit( 0 );
} else
{
// Mark the server to shutdown at the end of the tick
MinecraftServer.getServer().safeShutdown( false, isRestarting );
// wait 10 seconds to see if we're actually going to try shutdown
try
{
Thread.sleep( 10000 );
}
catch (InterruptedException ignored)
{
}
// Check if we've actually hit a state where the server is going to safely shutdown
// if we have, let the server stop as usual
if (MinecraftServer.getServer().isStopped()) return;
// If the server hasn't stopped by now, assume worse case and kill
closeSocket();
System.exit( 0 );
}
}
// Paper end
// Paper - Split from moved code
private static void closeSocket()
{
// Close the socket so we can rebind with the new process
MinecraftServer.getServer().getConnection().stop();
// Give time for it to kick in
try
{
Thread.sleep( 100 );
} catch ( InterruptedException ex )
{
}
}
// Paper end
// Paper start - copied from above and modified to return if the hook registered
private static boolean addShutdownHook(String restartScript)
{
String[] split = restartScript.split( " " );
if ( split.length > 0 && new File( split[0] ).isFile() )
{
Thread shutdownHook = new Thread()
{
@Override
public void run()
{
try
{
String os = System.getProperty( "os.name" ).toLowerCase(java.util.Locale.ENGLISH);
if ( os.contains( "win" ) )
{
Runtime.getRuntime().exec( "cmd /c start " + restartScript );
} else
{
Runtime.getRuntime().exec( "sh " + restartScript );
}
} catch ( Exception e )
{
e.printStackTrace();
}
}
};
shutdownHook.setDaemon( true );
Runtime.getRuntime().addShutdownHook( shutdownHook );
return true;
} else
{
return false;
}
}
// Paper end
}