diff --git a/build/Version.props b/build/Version.props
index af7c24e51a..c6573e2f14 100644
--- a/build/Version.props
+++ b/build/Version.props
@@ -3,15 +3,15 @@
- 6.9.2
+ 6.10.0
5.2.0
- 10.7.0
+ 10.8.0
7.0.0
13.7.0
16.0.0
- 7.2.1
- 5.9.0
- 1.4.1
+ 7.3.0
+ 5.10.0
+ 1.5.0
1.2.1
2.0.0
netstandard2.0
diff --git a/build/tgstation-server.service b/build/tgstation-server.service
index 98c03fee57..44ce74fe2e 100644
--- a/build/tgstation-server.service
+++ b/build/tgstation-server.service
@@ -11,7 +11,7 @@ User=tgstation-server
Type=notify-reload
NotifyAccess=all
WorkingDirectory=/opt/tgstation-server
-ExecStart=/usr/bin/dotnet Tgstation.Server.Host.Console.dll --appsettings-base-path=/etc/tgstation-server --General:SetupWizardMode=Never --Internal:UsingSystemD=true
+ExecStart=/usr/bin/dotnet Tgstation.Server.Host.Console.dll --appsettings-base-path=/etc/tgstation-server --General:SetupWizardMode=Never
TimeoutStartSec=600
Restart=always
KillMode=process
diff --git a/src/DMAPI/tgs.dm b/src/DMAPI/tgs.dm
index 4766b3dfe6..42f2d5fc31 100644
--- a/src/DMAPI/tgs.dm
+++ b/src/DMAPI/tgs.dm
@@ -1,7 +1,7 @@
// tgstation-server DMAPI
// The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in IETF RFC 2119.
-#define TGS_DMAPI_VERSION "7.2.1"
+#define TGS_DMAPI_VERSION "7.3.0"
// All functions and datums outside this document are subject to change with any version and should not be relied on.
diff --git a/src/DMAPI/tgs/v5/__interop_version.dm b/src/DMAPI/tgs/v5/__interop_version.dm
index f4806f7adb..29ea239ad8 100644
--- a/src/DMAPI/tgs/v5/__interop_version.dm
+++ b/src/DMAPI/tgs/v5/__interop_version.dm
@@ -1 +1 @@
-"5.9.0"
+"5.10.0"
diff --git a/src/DMAPI/tgs/v5/_defines.dm b/src/DMAPI/tgs/v5/_defines.dm
index 92c7a8388a..a47bfd7800 100644
--- a/src/DMAPI/tgs/v5/_defines.dm
+++ b/src/DMAPI/tgs/v5/_defines.dm
@@ -95,6 +95,7 @@
#define DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION "newServerVersion"
#define DMAPI5_TOPIC_PARAMETER_BROADCAST_MESSAGE "broadcastMessage"
+#define DMAPI5_TOPIC_RESPONSE_CLIENT_COUNT "clientCount"
#define DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE "commandResponse"
#define DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE_MESSAGE "commandResponseMessage"
#define DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES "chatResponses"
diff --git a/src/DMAPI/tgs/v5/topic.dm b/src/DMAPI/tgs/v5/topic.dm
index e1f2cb6385..59e5e63e5c 100644
--- a/src/DMAPI/tgs/v5/topic.dm
+++ b/src/DMAPI/tgs/v5/topic.dm
@@ -149,7 +149,9 @@
if(DMAPI5_TOPIC_COMMAND_HEALTHCHECK)
if(event_handler && event_handler.receive_health_checks)
event_handler.HandleEvent(TGS_EVENT_HEALTH_CHECK)
- return TopicResponse()
+ var/list/health_check_response = TopicResponse()
+ health_check_response[DMAPI5_TOPIC_RESPONSE_CLIENT_COUNT] = TGS_CLIENT_COUNT
+ return health_check_response;
if(DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH)
detached = FALSE
diff --git a/src/DMAPI/tgs/v5/undefs.dm b/src/DMAPI/tgs/v5/undefs.dm
index 237207fdfd..d8dbf7a781 100644
--- a/src/DMAPI/tgs/v5/undefs.dm
+++ b/src/DMAPI/tgs/v5/undefs.dm
@@ -17,8 +17,8 @@
#undef DMAPI5_BRIDGE_COMMAND_EVENT
#undef DMAPI5_PARAMETER_ACCESS_IDENTIFIER
+#undef DMAPI5_PARAMETER_CLIENT_COUNT
#undef DMAPI5_PARAMETER_CUSTOM_COMMANDS
-#undef DMAPI5_PARAMETER_TOPIC_PORT
#undef DMAPI5_CHUNK
#undef DMAPI5_CHUNK_PAYLOAD
@@ -95,6 +95,7 @@
#undef DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION
#undef DMAPI5_TOPIC_PARAMETER_BROADCAST_MESSAGE
+#undef DMAPI5_TOPIC_RESPONSE_CLIENT_COUNT
#undef DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE
#undef DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE_MESSAGE
#undef DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES
diff --git a/src/Tgstation.Server.Api/Models/Internal/DreamDaemonApiBase.cs b/src/Tgstation.Server.Api/Models/Internal/DreamDaemonApiBase.cs
index 6bde7948d0..e3411b8115 100644
--- a/src/Tgstation.Server.Api/Models/Internal/DreamDaemonApiBase.cs
+++ b/src/Tgstation.Server.Api/Models/Internal/DreamDaemonApiBase.cs
@@ -1,4 +1,6 @@
-namespace Tgstation.Server.Api.Models.Internal
+using System;
+
+namespace Tgstation.Server.Api.Models.Internal
{
///
/// Base class for DreamDaemon API models.
@@ -11,6 +13,18 @@
[ResponseOptions]
public long? SessionId { get; set; }
+ ///
+ /// When the current server execution was started.
+ ///
+ [ResponseOptions]
+ public DateTimeOffset? LaunchTime { get; set; }
+
+ ///
+ /// The last known count of connected players. Requires to not be 0 and a game server interop version >= 5.10.0 to populate.
+ ///
+ [ResponseOptions]
+ public int? ClientCount { get; set; }
+
///
/// If the server is undergoing a soft reset. This may be automatically set by changes to other fields.
///
diff --git a/src/Tgstation.Server.Host.Console/Program.cs b/src/Tgstation.Server.Host.Console/Program.cs
index c8383865af..26f5b8f5b5 100644
--- a/src/Tgstation.Server.Host.Console/Program.cs
+++ b/src/Tgstation.Server.Host.Console/Program.cs
@@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.Extensions.Hosting.Systemd;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Common;
@@ -44,6 +46,11 @@ namespace Tgstation.Server.Host.Console
var trace = arguments.Remove("--trace-host-watchdog");
var debug = arguments.Remove("--debug-host-watchdog");
+ const string SystemDArg = "--Internal:UsingSystemD=true";
+ if (!arguments.Any(arg => arg.Equals(SystemDArg, StringComparison.OrdinalIgnoreCase))
+ && SystemdHelpers.IsSystemdService())
+ arguments.Add(SystemDArg);
+
using var loggerFactory = LoggerFactory.Create(builder =>
{
if (trace)
diff --git a/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj b/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj
index 3cbf1c1a16..a405553d55 100644
--- a/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj
+++ b/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj
@@ -11,6 +11,8 @@
+
+
diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicResponse.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicResponse.cs
index 12db6f3ef1..e5b30150de 100644
--- a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicResponse.cs
+++ b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicResponse.cs
@@ -36,5 +36,10 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
///
public IReadOnlyCollection? MissingChunks { get; set; }
+
+ ///
+ /// The number of connected clients to the game. Added in Interop 5.10.0.
+ ///
+ public int? ClientCount { get; }
}
}
diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs
index 754b672b7d..77a25fd979 100644
--- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs
+++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs
@@ -124,6 +124,9 @@ namespace Tgstation.Server.Host.Components.Session
///
public long? MemoryUsage => process.MemoryUsage;
+ ///
+ public DateTimeOffset? LaunchTime => process.LaunchTime;
+
///
/// The for the .
///
diff --git a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs
index 2220c56932..35da1504f8 100644
--- a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs
+++ b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs
@@ -19,6 +19,16 @@ namespace Tgstation.Server.Host.Components.Watchdog
///
long? SessionId { get; }
+ ///
+ /// When the current server executions was started.
+ ///
+ DateTimeOffset? LaunchTime { get; }
+
+ ///
+ /// Last known client count queried from the DMAPI. Requires health checks to be enabled to populate.
+ ///
+ int? ClientCount { get; }
+
///
/// The current .
///
diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs
index 5d9326ce7d..3c67ca495c 100644
--- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs
+++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs
@@ -38,6 +38,12 @@ namespace Tgstation.Server.Host.Components.Watchdog
///
public long? SessionId => GetActiveController()?.ReattachInformation.Id;
+ ///
+ public int? ClientCount { get; private set; }
+
+ ///
+ public DateTimeOffset? LaunchTime => GetActiveController()?.LaunchTime;
+
///
public WatchdogStatus Status
{
@@ -567,6 +573,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
// since neither server is running, this is safe to do
LastLaunchParameters = ActiveLaunchParameters;
healthChecksMissed = 0;
+ ClientCount = null;
try
{
@@ -664,6 +671,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
// we lost the server, just restart entirely
// DCT: Operation must always run
await DisposeAndNullControllers(CancellationToken.None);
+ ClientCount = null;
const string FailReattachMessage = "Unable to properly reattach to server! Restarting watchdog...";
Logger.LogWarning(FailReattachMessage);
@@ -1180,7 +1188,10 @@ namespace Tgstation.Server.Host.Components.Watchdog
}
}
else
+ {
healthChecksMissed = 0;
+ ClientCount = response.ClientCount;
+ }
return MonitorAction.Continue;
}
diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
index 79e03ebb84..e6760f0f17 100644
--- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
+++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
@@ -366,6 +366,8 @@ namespace Tgstation.Server.Host.Controllers
firstIteration = false;
result.Status = dd.Status;
result.SessionId = dd.SessionId;
+ result.LaunchTime = dd.LaunchTime;
+ result.ClientCount = dd.ClientCount;
}
while (result.Status == WatchdogStatus.Online && !result.SessionId.HasValue); // this is the one invalid combo, it's not that racy
diff --git a/src/Tgstation.Server.Host/System/IProcessBase.cs b/src/Tgstation.Server.Host/System/IProcessBase.cs
index 6ae88dce50..b95b0bf928 100644
--- a/src/Tgstation.Server.Host/System/IProcessBase.cs
+++ b/src/Tgstation.Server.Host/System/IProcessBase.cs
@@ -1,4 +1,5 @@
-using System.Threading;
+using System;
+using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.System
@@ -8,6 +9,11 @@ namespace Tgstation.Server.Host.System
///
public interface IProcessBase
{
+ ///
+ /// When the process was started.
+ ///
+ DateTimeOffset? LaunchTime { get; }
+
///
/// The resulting in the exit code of the process or if the process was detached.
///
diff --git a/src/Tgstation.Server.Host/System/Process.cs b/src/Tgstation.Server.Host/System/Process.cs
index e180cfc3cc..76f9ef56f3 100644
--- a/src/Tgstation.Server.Host/System/Process.cs
+++ b/src/Tgstation.Server.Host/System/Process.cs
@@ -16,6 +16,23 @@ namespace Tgstation.Server.Host.System
///
public int Id { get; }
+ ///
+ public DateTimeOffset? LaunchTime
+ {
+ get
+ {
+ try
+ {
+ return handle.StartTime;
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(ex, "Failed to get PID {pid}'s memory usage!", Id);
+ return null;
+ }
+ }
+ }
+
///
public Task Startup { get; }
diff --git a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs
index 08f436b26d..d7930e0cc6 100644
--- a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs
+++ b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs
@@ -682,6 +682,7 @@ namespace Tgstation.Server.Tests.Live.Instance
Assert.AreEqual(WatchdogStatus.Offline, daemonStatus.Status.Value);
Assert.IsNotNull(daemonStatus.ActiveCompileJob);
Assert.IsFalse(daemonStatus.SessionId.HasValue);
+ Assert.IsFalse(daemonStatus.LaunchTime.HasValue);
Assert.IsNull(daemonStatus.StagedCompileJob);
Assert.AreEqual(DMApiConstants.InteropVersion, daemonStatus.ActiveCompileJob.DMApiVersion);
Assert.AreEqual(DreamDaemonSecurity.Trusted, daemonStatus.ActiveCompileJob.MinimumSecurityLevel);
@@ -730,6 +731,7 @@ namespace Tgstation.Server.Tests.Live.Instance
daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken);
Assert.AreEqual(WatchdogStatus.Offline, daemonStatus.Status.Value);
Assert.IsFalse(daemonStatus.SessionId.HasValue);
+ Assert.IsFalse(daemonStatus.LaunchTime.HasValue);
await ExpectGameDirectoryCount(1, cancellationToken);
await CheckDMApiFail(daemonStatus.ActiveCompileJob, cancellationToken, false);
@@ -741,12 +743,18 @@ namespace Tgstation.Server.Tests.Live.Instance
}, cancellationToken);
Assert.AreEqual(string.Empty, daemonStatus.AdditionalParameters);
Assert.IsFalse(daemonStatus.SessionId.HasValue);
+ Assert.IsFalse(daemonStatus.LaunchTime.HasValue);
}
long? sessionIdTracker;
void ValidateSessionId(DreamDaemonResponse daemonStatus, bool? knownIncrease)
{
Assert.IsTrue(daemonStatus.SessionId.HasValue, $"Expected a session ID in the DreamDaemonResponse");
+ Assert.IsTrue(daemonStatus.LaunchTime.HasValue);
+ Assert.IsTrue(daemonStatus.LaunchTime.Value >= DateTimeOffset.UtcNow.AddHours(-1));
+
+ if (daemonStatus.ClientCount.HasValue)
+ Assert.AreEqual(0, daemonStatus.ClientCount.Value);
if (sessionIdTracker.HasValue)
if (knownIncrease.HasValue)
@@ -856,6 +864,8 @@ namespace Tgstation.Server.Tests.Live.Instance
// Ensure it's responding to health checks
await Task.WhenAny(Task.Delay(7000, cancellationToken), ourProcessHandler.Lifetime);
Assert.IsFalse(ddProc.HasExited);
+ var daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken);
+ Assert.AreEqual(0, daemonStatus.ClientCount);
// check DD agrees
var topicRequestResult = await SendTestTopic(