using System.Linq; using System.Threading.Tasks; using Content.Server.Chat.Managers; using Content.Server.Discord; using Content.Shared.CCVar; using Robust.Server; using Robust.Server.Player; using Robust.Server.ServerStatus; using Robust.Shared.Configuration; using Robust.Shared.Enums; using Robust.Shared.Player; using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Content.Server.ServerUpdates; /// /// Responsible for restarting the server periodically or for update, when not disruptive. /// /// /// This was originally only designed for restarting on *update*, /// but now also handles periodic restarting to keep server uptime via . /// public sealed partial class ServerUpdateManager : IPostInjectInit { [Dependency] private IGameTiming _gameTiming = default!; [Dependency] private IWatchdogApi _watchdog = default!; [Dependency] private IPlayerManager _playerManager = default!; [Dependency] private IChatManager _chatManager = default!; [Dependency] private IBaseServer _server = default!; [Dependency] private IConfigurationManager _cfg = default!; [Dependency] private ILogManager _logManager = default!; //WL-Chages-start [Dependency] private DiscordWebhook _discord = default!; private WebhookIdentifier? _discordWebhook; //WL-Changes-end private ISawmill _sawmill = default!; [ViewVariables] private bool _updateOnRoundEnd; private TimeSpan? _restartTime; private TimeSpan _uptimeRestart; public void Initialize() { _watchdog.UpdateReceived += WatchdogOnUpdateReceived; _playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged; //WL-Changes-start var url = _cfg.GetCVar(CCVars.DiscordRoundUpdateWebhook); if (string.IsNullOrEmpty(url)) return; _discord.GetWebhook(url, webhookData => _discordWebhook = webhookData.ToIdentifier()); //WL-Changes-end _cfg.OnValueChanged( CCVars.ServerUptimeRestartMinutes, minutes => _uptimeRestart = TimeSpan.FromMinutes(minutes), true); } public void Update() { if (_restartTime != null) { if (_restartTime < _gameTiming.RealTime) { DoShutdown(); } } else { if (ShouldShutdownDueToUptime()) { ServerEmptyUpdateRestartCheck("uptime"); } } } /// /// Notify that the round just ended, which is a great time to restart if necessary! /// /// True if the server is going to restart. public bool RoundEnded() { if (_updateOnRoundEnd || ShouldShutdownDueToUptime()) { DoShutdown(); return true; } return false; } private void PlayerManagerOnPlayerStatusChanged(object? sender, SessionStatusEventArgs e) { switch (e.NewStatus) { case SessionStatus.Connected: if (_restartTime != null) _sawmill.Debug("Aborting server restart timer due to player connection"); _restartTime = null; break; case SessionStatus.Disconnected: ServerEmptyUpdateRestartCheck("last player disconnect"); break; } } private async void WatchdogOnUpdateReceived() { _chatManager.DispatchServerAnnouncement(Loc.GetString("server-updates-received")); _updateOnRoundEnd = true; //WL-Changes-start await SendDiscordNotify(); //WL-Changes-end ServerEmptyUpdateRestartCheck("update notification"); } //WL-Changes-start private async Task SendDiscordNotify() { try { if (_discordWebhook == null) return; var payload = new WebhookPayload() { Content = "Сервер получил обновление и будет перезапущен в конце текущего раунда." }; await _discord.CreateMessage(_discordWebhook.Value, payload); } catch (Exception exc) { _sawmill.Error($"Вызвано исключение во время отправки дискорд-оповещения об обновлении сервера: {exc.ToStringBetter()}"); } } //WL-Changes-end /// /// 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(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 || ShouldShutdownDueToUptime())) { // Still somebody online. return; } if (_restartTime != null) { // 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() { _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"); } }