diff --git a/Content.Server/ServerUpdates/ServerUpdateManager.cs b/Content.Server/ServerUpdates/ServerUpdateManager.cs
index f4e54984e9..bf18428e25 100644
--- a/Content.Server/ServerUpdates/ServerUpdateManager.cs
+++ b/Content.Server/ServerUpdates/ServerUpdateManager.cs
@@ -12,9 +12,13 @@ using Robust.Shared.Timing;
namespace Content.Server.ServerUpdates;
///
-/// Responsible for restarting the server for update, when not disruptive.
+/// Responsible for restarting the server periodically or for update, when not disruptive.
///
-public sealed class ServerUpdateManager
+///
+/// This was originally only designed for restarting on *update*,
+/// but now also handles periodic restarting to keep server uptime via .
+///
+public sealed class ServerUpdateManager : IPostInjectInit
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IWatchdogApi _watchdog = default!;
@@ -22,23 +26,43 @@ public sealed class ServerUpdateManager
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IBaseServer _server = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
+ [Dependency] private readonly ILogManager _logManager = default!;
+
+ private ISawmill _sawmill = default!;
[ViewVariables]
private bool _updateOnRoundEnd;
private TimeSpan? _restartTime;
+ private TimeSpan _uptimeRestart;
+
public void Initialize()
{
_watchdog.UpdateReceived += WatchdogOnUpdateReceived;
_playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged;
+
+ _cfg.OnValueChanged(
+ CCVars.ServerUptimeRestartMinutes,
+ minutes => _uptimeRestart = TimeSpan.FromMinutes(minutes),
+ true);
}
public void Update()
{
- if (_restartTime != null && _restartTime < _gameTiming.RealTime)
+ if (_restartTime != null)
{
- DoShutdown();
+ if (_restartTime < _gameTiming.RealTime)
+ {
+ DoShutdown();
+ }
+ }
+ else
+ {
+ if (ShouldShutdownDueToUptime())
+ {
+ ServerEmptyUpdateRestartCheck("uptime");
+ }
}
}
@@ -48,7 +72,7 @@ public sealed class ServerUpdateManager
/// True if the server is going to restart.
public bool RoundEnded()
{
- if (_updateOnRoundEnd)
+ if (_updateOnRoundEnd || ShouldShutdownDueToUptime())
{
DoShutdown();
return true;
@@ -61,11 +85,14 @@ public sealed class ServerUpdateManager
{
switch (e.NewStatus)
{
- case SessionStatus.Connecting:
+ case SessionStatus.Connected:
+ if (_restartTime != null)
+ _sawmill.Debug("Aborting server restart timer due to player connection");
+
_restartTime = null;
break;
case SessionStatus.Disconnected:
- ServerEmptyUpdateRestartCheck();
+ ServerEmptyUpdateRestartCheck("last player disconnect");
break;
}
}
@@ -74,20 +101,20 @@ public sealed class ServerUpdateManager
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("server-updates-received"));
_updateOnRoundEnd = true;
- ServerEmptyUpdateRestartCheck();
+ ServerEmptyUpdateRestartCheck("update notification");
}
///
/// Checks whether there are still players on the server,
/// and if not starts a timer to automatically reboot the server if an update is available.
///
- private void ServerEmptyUpdateRestartCheck()
+ private void ServerEmptyUpdateRestartCheck(string reason)
{
// Can't simple check the current connected player count since that doesn't update
// before PlayerStatusChanged gets fired.
// So in the disconnect handler we'd still see a single player otherwise.
var playersOnline = _playerManager.Sessions.Any(p => p.Status != SessionStatus.Disconnected);
- if (playersOnline || !_updateOnRoundEnd)
+ if (playersOnline || !(_updateOnRoundEnd || ShouldShutdownDueToUptime()))
{
// Still somebody online.
return;
@@ -95,16 +122,30 @@ public sealed class ServerUpdateManager
if (_restartTime != null)
{
- // Do nothing because I guess we already have a timer running..?
+ // Do nothing because we already have a timer running.
return;
}
var restartDelay = TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.UpdateRestartDelay));
_restartTime = restartDelay + _gameTiming.RealTime;
+
+ _sawmill.Debug("Started server-empty restart timer due to {Reason}", reason);
}
private void DoShutdown()
{
- _server.Shutdown(Loc.GetString("server-updates-shutdown"));
+ _sawmill.Debug($"Shutting down via {nameof(ServerUpdateManager)}!");
+ var reason = _updateOnRoundEnd ? "server-updates-shutdown" : "server-updates-shutdown-uptime";
+ _server.Shutdown(Loc.GetString(reason));
+ }
+
+ private bool ShouldShutdownDueToUptime()
+ {
+ return _uptimeRestart != TimeSpan.Zero && _gameTiming.RealTime > _uptimeRestart;
+ }
+
+ void IPostInjectInit.PostInject()
+ {
+ _sawmill = _logManager.GetSawmill("restart");
}
}
diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs
index 2d6aa56390..339c0f895f 100644
--- a/Content.Shared/CCVar/CCVars.cs
+++ b/Content.Shared/CCVar/CCVars.cs
@@ -32,6 +32,21 @@ namespace Content.Shared.CCVar
public static readonly CVarDef DefaultGuide =
CVarDef.Create("server.default_guide", "NewPlayer", CVar.REPLICATED | CVar.SERVER);
+ ///
+ /// If greater than 0, automatically restart the server after this many minutes of uptime.
+ ///
+ ///
+ ///
+ /// This is intended to work around various bugs and performance issues caused by long continuous server uptime.
+ ///
+ ///
+ /// This uses the same non-disruptive logic as update restarts,
+ /// i.e. the game will only restart at round end or when there is nobody connected.
+ ///
+ ///
+ public static readonly CVarDef ServerUptimeRestartMinutes =
+ CVarDef.Create("server.uptime_restart_minutes", 0, CVar.SERVERONLY);
+
/*
* Ambience
*/
diff --git a/Resources/Locale/en-US/server-updates/server-updates.ftl b/Resources/Locale/en-US/server-updates/server-updates.ftl
index 72047432bb..ae775c9931 100644
--- a/Resources/Locale/en-US/server-updates/server-updates.ftl
+++ b/Resources/Locale/en-US/server-updates/server-updates.ftl
@@ -1,2 +1,3 @@
server-updates-received = Update has been received, server will automatically restart for update at the end of this round.
server-updates-shutdown = Server is shutting down for update and will automatically restart.
+server-updates-shutdown-uptime = Server is shutting down for periodic cleanup and will automatically restart.