Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions Source/Client/Networking/HostUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,11 +201,7 @@ private static void StartLocalServer()
{
Multiplayer.LocalServer.running = true;

Multiplayer.localServerThread = new Thread(Multiplayer.LocalServer.Run)
{
Name = "Local server thread"
};
Multiplayer.localServerThread.Start();
Multiplayer.localServerThread = Multiplayer.LocalServer.StartServer("Local server thread");

const string text = "Server started.";
Messages.Message(text, MessageTypeDefOf.SilentInput, false);
Expand Down
11 changes: 11 additions & 0 deletions Source/Common/FreezeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ public FreezeManager(MultiplayerServer server)

public void Tick()
{
// Old fix for #991, disabled — superseded by the loop join in TryStop.
// var hostUsername = Server.hostUsername;
// ServerPlayer hostPlayer = null;
// foreach (var p in Server.PlayingPlayers)
// {
// if (hostUsername != null && p.Username == hostUsername)
// {
// hostPlayer = p;
// break;
// }
// }
var hostPlayer = Server.PlayingPlayers.FirstOrDefault(p => p.IsHost);

if (hostPlayer != null)
Expand Down
23 changes: 23 additions & 0 deletions Source/Common/MultiplayerServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ static MultiplayerServer()
InitDataState.Requested;

public volatile bool running;
public Thread? serverThread;
// Atomic latch: TryStop may be called both from a handler and from Run's
// exit; teardown must run only once.
private int stopFlag;

public bool ArbiterPlaying => PlayingPlayers.Any(p => p.IsArbiter && p.status == PlayerStatus.Playing);
public ServerPlayer HostPlayer => PlayingPlayers.First(p => p.IsHost);
Expand Down Expand Up @@ -200,8 +204,27 @@ private void TickNet()
serverTimePerTick = StandardTimePerTick * 4f;
}

public Thread StartServer(string threadName = "Server thread")
{
serverThread = new Thread(Run) { Name = threadName };
serverThread.Start();
return serverThread;
}

// Waits for the server loop to end before tearing down, so nulling
// instance can't race with Tick (#991). Callers must clear running first.
// Never joins the loop thread itself, which would deadlock Run -> TryStop.
public void TryStop()
{
if (serverThread is { } thread && thread != Thread.CurrentThread)
{
if (!thread.Join(TimeSpan.FromSeconds(5)))
ServerLog.Error("Server loop thread did not stop within 5 seconds, proceeding with shutdown");
}

if (Interlocked.CompareExchange(ref stopFlag, 1, 0) != 0)
return;

ServerLog.Detail("Server shutting down...");

playerManager.OnServerStop();
Expand Down
2 changes: 1 addition & 1 deletion Source/Server/Server.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
server.netManagers.Add(lan);
}

new Thread(server.Run) { Name = "Server thread" }.Start();
server.StartServer();

while (server.running)
{
Expand Down
41 changes: 41 additions & 0 deletions Source/Tests/FreezeManagerTest.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Threading;
using Multiplayer.Common;

namespace Tests;
Expand Down Expand Up @@ -115,6 +116,46 @@ public void HostAbsent_NoPlayers_NotFrozenStaysNotFrozen()
Assert.That(server.freezeManager.Frozen, Is.False);
}

[Test]
public void TryStop_JoinsServerLoopThread()
{
// The #991 shutdown race is closed by having TryStop wait for the server
// loop to finish before nulling the static instance. A fake loop verifies
// the wait really happens: TryStop must not return before the loop did.
var loopFinished = false;
var fakeLoop = new Thread(() =>
{
Thread.Sleep(100);
loopFinished = true;
}) { IsBackground = true };
server.serverThread = fakeLoop;
fakeLoop.Start();

server.TryStop();

Assert.That(loopFinished, Is.True);
Assert.That(fakeLoop.IsAlive, Is.False);
}

[Test]
public void TryStop_FromLoopThreadItself_DoesNotDeadlock()
{
// Run() calls TryStop at its own exit; self-joining would hang forever.
var completed = false;
var loopThread = new Thread(() =>
{
server.serverThread = Thread.CurrentThread;
server.TryStop(); // same thread as the loop we must not join
completed = true;
}) { IsBackground = true };
loopThread.Start();

// If self-join deadlocked, the loop thread would never reach completed=true
// and the join with timeout below would observe an alive thread.
Assert.That(loopThread.Join(TimeSpan.FromSeconds(5)), Is.True);
Assert.That(completed, Is.True);
}

[Test]
public void HostReconnects_ResumesNormalBehavior()
{
Expand Down
2 changes: 2 additions & 0 deletions Source/Tests/ServerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ private MultiplayerServer MakeServer(out int port)
port = liteNet.netManagers[0].manager.LocalPort;

var serverThread = new Thread(server.Run) { IsBackground = true };
// Register the loop thread so TryStop's join contract is exercised here too.
server.serverThread = serverThread;
serverThread.Start();

teardownActions.Add(() =>
Expand Down
Loading