mirror of
https://github.com/chylex/Minecraft-Phantom-Panel.git
synced 2025-09-16 00:32:12 +02:00
Compare commits
8 Commits
25d0e2b0d1
...
main
Author | SHA1 | Date | |
---|---|---|---|
591a6a62ab
|
|||
ae32537d8c
|
|||
8149d31d51
|
|||
9873f8779f
|
|||
31e101b21e
|
|||
398bb14742
|
|||
4e89d7b12f
|
|||
3b313bf0a8
|
@@ -3,28 +3,12 @@ using System.Buffers.Binary;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using Phantom.Utils.Logging;
|
||||
using Serilog;
|
||||
using Phantom.Common.Data.Instance;
|
||||
|
||||
namespace Phantom.Agent.Minecraft.Server;
|
||||
|
||||
public sealed class ServerStatusProtocol {
|
||||
private readonly ILogger logger;
|
||||
|
||||
public ServerStatusProtocol(string loggerName) {
|
||||
this.logger = PhantomLogger.Create<ServerStatusProtocol>(loggerName);
|
||||
}
|
||||
|
||||
public async Task<int?> GetOnlinePlayerCount(int serverPort, CancellationToken cancellationToken) {
|
||||
try {
|
||||
return await GetOnlinePlayerCountOrThrow(serverPort, cancellationToken);
|
||||
} catch (Exception e) {
|
||||
logger.Error(e, "Caught exception checking online player count.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<int?> GetOnlinePlayerCountOrThrow(int serverPort, CancellationToken cancellationToken) {
|
||||
public static class ServerStatusProtocol {
|
||||
public static async Task<InstancePlayerCounts> GetPlayerCounts(ushort serverPort, CancellationToken cancellationToken) {
|
||||
using var tcpClient = new TcpClient();
|
||||
await tcpClient.ConnectAsync(IPAddress.Loopback, serverPort, cancellationToken);
|
||||
var tcpStream = tcpClient.GetStream();
|
||||
@@ -33,24 +17,22 @@ public sealed class ServerStatusProtocol {
|
||||
tcpStream.WriteByte(0xFE);
|
||||
await tcpStream.FlushAsync(cancellationToken);
|
||||
|
||||
short? messageLength = await ReadStreamHeader(tcpStream, cancellationToken);
|
||||
return messageLength == null ? null : await ReadOnlinePlayerCount(tcpStream, messageLength.Value * 2, cancellationToken);
|
||||
short messageLength = await ReadStreamHeader(tcpStream, cancellationToken);
|
||||
return await ReadPlayerCounts(tcpStream, messageLength * 2, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<short?> ReadStreamHeader(NetworkStream tcpStream, CancellationToken cancellationToken) {
|
||||
private static async Task<short> ReadStreamHeader(NetworkStream tcpStream, CancellationToken cancellationToken) {
|
||||
var headerBuffer = ArrayPool<byte>.Shared.Rent(3);
|
||||
try {
|
||||
await tcpStream.ReadExactlyAsync(headerBuffer, 0, 3, cancellationToken);
|
||||
|
||||
if (headerBuffer[0] != 0xFF) {
|
||||
logger.Error("Unexpected first byte in response from server: {FirstByte}.", headerBuffer[0]);
|
||||
return null;
|
||||
throw new ProtocolException("Unexpected first byte in response from server: " + headerBuffer[0]);
|
||||
}
|
||||
|
||||
short messageLength = BinaryPrimitives.ReadInt16BigEndian(headerBuffer.AsSpan(1));
|
||||
if (messageLength <= 0) {
|
||||
logger.Error("Unexpected message length in response from server: {MessageLength}.", messageLength);
|
||||
return null;
|
||||
throw new ProtocolException("Unexpected message length in response from server: " + messageLength);
|
||||
}
|
||||
|
||||
return messageLength;
|
||||
@@ -59,35 +41,54 @@ public sealed class ServerStatusProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<int?> ReadOnlinePlayerCount(NetworkStream tcpStream, int messageLength, CancellationToken cancellationToken) {
|
||||
private static async Task<InstancePlayerCounts> ReadPlayerCounts(NetworkStream tcpStream, int messageLength, CancellationToken cancellationToken) {
|
||||
var messageBuffer = ArrayPool<byte>.Shared.Rent(messageLength);
|
||||
try {
|
||||
await tcpStream.ReadExactlyAsync(messageBuffer, 0, messageLength, cancellationToken);
|
||||
|
||||
// Valid response separator encoded in UTF-16BE is 0x00 0xA7 (§).
|
||||
const byte SeparatorSecondByte = 0xA7;
|
||||
|
||||
static bool IsValidSeparator(ReadOnlySpan<byte> buffer, int index) {
|
||||
return index > 0 && buffer[index - 1] == 0x00;
|
||||
}
|
||||
|
||||
int separator2 = Array.LastIndexOf(messageBuffer, SeparatorSecondByte);
|
||||
int separator1 = separator2 == -1 ? -1 : Array.LastIndexOf(messageBuffer, SeparatorSecondByte, separator2 - 1);
|
||||
if (!IsValidSeparator(messageBuffer, separator1) || !IsValidSeparator(messageBuffer, separator2)) {
|
||||
logger.Error("Could not find message separators in response from server.");
|
||||
return null;
|
||||
}
|
||||
|
||||
string onlinePlayerCountStr = Encoding.BigEndianUnicode.GetString(messageBuffer.AsSpan((separator1 + 1)..(separator2 - 1)));
|
||||
if (!int.TryParse(onlinePlayerCountStr, out int onlinePlayerCount)) {
|
||||
logger.Error("Could not parse online player count in response from server: {OnlinePlayerCount}.", onlinePlayerCountStr);
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.Debug("Detected {OnlinePlayerCount} online player(s).", onlinePlayerCount);
|
||||
return onlinePlayerCount;
|
||||
return ReadPlayerCountsFromResponse(messageBuffer.AsSpan(0, messageLength));
|
||||
} finally {
|
||||
ArrayPool<byte>.Shared.Return(messageBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Legacy query protocol uses the paragraph symbol (§) as separator encoded in UTF-16BE.
|
||||
/// </summary>
|
||||
private static readonly byte[] Separator = { 0x00, 0xA7 };
|
||||
|
||||
private static InstancePlayerCounts ReadPlayerCountsFromResponse(ReadOnlySpan<byte> messageBuffer) {
|
||||
int lastSeparator = messageBuffer.LastIndexOf(Separator);
|
||||
int middleSeparator = messageBuffer[..lastSeparator].LastIndexOf(Separator);
|
||||
|
||||
if (lastSeparator == -1 || middleSeparator == -1) {
|
||||
throw new ProtocolException("Could not find message separators in response from server.");
|
||||
}
|
||||
|
||||
var onlinePlayerCountBuffer = messageBuffer[(middleSeparator + Separator.Length)..lastSeparator];
|
||||
var maximumPlayerCountBuffer = messageBuffer[(lastSeparator + Separator.Length)..];
|
||||
|
||||
// Player counts are integers, whose maximum string length is 10 characters.
|
||||
Span<char> integerStringBuffer = stackalloc char[10];
|
||||
|
||||
return new InstancePlayerCounts(
|
||||
DecodeAndParsePlayerCount(onlinePlayerCountBuffer, integerStringBuffer, "online"),
|
||||
DecodeAndParsePlayerCount(maximumPlayerCountBuffer, integerStringBuffer, "maximum")
|
||||
);
|
||||
}
|
||||
|
||||
private static int DecodeAndParsePlayerCount(ReadOnlySpan<byte> inputBuffer, Span<char> tempCharBuffer, string countType) {
|
||||
if (!Encoding.BigEndianUnicode.TryGetChars(inputBuffer, tempCharBuffer, out int charCount)) {
|
||||
throw new ProtocolException("Could not decode " + countType + " player count in response from server.");
|
||||
}
|
||||
|
||||
if (!int.TryParse(tempCharBuffer, out int playerCount)) {
|
||||
throw new ProtocolException("Could not parse " + countType + " player count in response from server: " + tempCharBuffer[..charCount].ToString());
|
||||
}
|
||||
|
||||
return playerCount;
|
||||
}
|
||||
|
||||
public sealed class ProtocolException : Exception {
|
||||
internal ProtocolException(string message) : base(message) {}
|
||||
}
|
||||
}
|
||||
|
@@ -5,7 +5,6 @@ using Phantom.Common.Data.Instance;
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Messages.Agent.ToController;
|
||||
using Phantom.Utils.Logging;
|
||||
using Phantom.Utils.Tasks;
|
||||
using Serilog;
|
||||
|
||||
namespace Phantom.Agent.Services.Instances;
|
||||
|
@@ -1,5 +1,8 @@
|
||||
using Phantom.Agent.Minecraft.Instance;
|
||||
using Phantom.Agent.Minecraft.Server;
|
||||
using Phantom.Agent.Rpc;
|
||||
using Phantom.Common.Data.Instance;
|
||||
using Phantom.Common.Messages.Agent.ToController;
|
||||
using Phantom.Utils.Logging;
|
||||
using Phantom.Utils.Tasks;
|
||||
using Phantom.Utils.Threading;
|
||||
@@ -7,34 +10,35 @@ using Phantom.Utils.Threading;
|
||||
namespace Phantom.Agent.Services.Instances.State;
|
||||
|
||||
sealed class InstancePlayerCountTracker : CancellableBackgroundTask {
|
||||
private readonly InstanceProcess process;
|
||||
private readonly ControllerConnection controllerConnection;
|
||||
private readonly Guid instanceGuid;
|
||||
private readonly ushort serverPort;
|
||||
|
||||
private readonly ServerStatusProtocol serverStatusProtocol;
|
||||
private readonly InstanceProcess process;
|
||||
|
||||
private readonly TaskCompletionSource firstDetection = AsyncTasks.CreateCompletionSource();
|
||||
private readonly ManualResetEventSlim serverOutputEvent = new ();
|
||||
|
||||
private int? onlinePlayerCount;
|
||||
private InstancePlayerCounts? playerCounts;
|
||||
|
||||
public int? OnlinePlayerCount {
|
||||
public InstancePlayerCounts? PlayerCounts {
|
||||
get {
|
||||
lock (this) {
|
||||
return onlinePlayerCount;
|
||||
return playerCounts;
|
||||
}
|
||||
}
|
||||
private set {
|
||||
EventHandler<int?>? onlinePlayerCountChanged;
|
||||
lock (this) {
|
||||
if (onlinePlayerCount == value) {
|
||||
if (playerCounts == value) {
|
||||
return;
|
||||
}
|
||||
|
||||
onlinePlayerCount = value;
|
||||
playerCounts = value;
|
||||
onlinePlayerCountChanged = OnlinePlayerCountChanged;
|
||||
}
|
||||
|
||||
onlinePlayerCountChanged?.Invoke(this, value);
|
||||
onlinePlayerCountChanged?.Invoke(this, value?.Online);
|
||||
controllerConnection.Send(new ReportInstancePlayerCountsMessage(instanceGuid, value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,9 +47,10 @@ sealed class InstancePlayerCountTracker : CancellableBackgroundTask {
|
||||
private bool isDisposed = false;
|
||||
|
||||
public InstancePlayerCountTracker(InstanceContext context, InstanceProcess process, ushort serverPort) : base(PhantomLogger.Create<InstancePlayerCountTracker>(context.ShortName)) {
|
||||
this.controllerConnection = context.Services.ControllerConnection;
|
||||
this.instanceGuid = context.InstanceGuid;
|
||||
this.process = process;
|
||||
this.serverPort = serverPort;
|
||||
this.serverStatusProtocol = new ServerStatusProtocol(context.ShortName);
|
||||
Start();
|
||||
}
|
||||
|
||||
@@ -59,7 +64,7 @@ sealed class InstancePlayerCountTracker : CancellableBackgroundTask {
|
||||
while (!CancellationToken.IsCancellationRequested) {
|
||||
serverOutputEvent.Reset();
|
||||
|
||||
OnlinePlayerCount = await serverStatusProtocol.GetOnlinePlayerCount(serverPort, CancellationToken);
|
||||
PlayerCounts = await TryGetPlayerCounts();
|
||||
|
||||
if (!firstDetection.Task.IsCompleted) {
|
||||
firstDetection.SetResult();
|
||||
@@ -67,7 +72,21 @@ sealed class InstancePlayerCountTracker : CancellableBackgroundTask {
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), CancellationToken);
|
||||
await serverOutputEvent.WaitHandle.WaitOneAsync(CancellationToken);
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), CancellationToken);
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), CancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<InstancePlayerCounts?> TryGetPlayerCounts() {
|
||||
try {
|
||||
var result = await ServerStatusProtocol.GetPlayerCounts(serverPort, CancellationToken);
|
||||
Logger.Debug("Detected {OnlinePlayerCount} / {MaximumPlayerCount} online player(s).", result.Online, result.Maximum);
|
||||
return result;
|
||||
} catch (ServerStatusProtocol.ProtocolException e) {
|
||||
Logger.Error(e.Message);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "Caught exception while checking online player count.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,12 +96,12 @@ sealed class InstancePlayerCountTracker : CancellableBackgroundTask {
|
||||
var onlinePlayersDetected = AsyncTasks.CreateCompletionSource();
|
||||
|
||||
lock (this) {
|
||||
if (onlinePlayerCount == null) {
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
else if (onlinePlayerCount > 0) {
|
||||
if (playerCounts is { Online: > 0 }) {
|
||||
return;
|
||||
}
|
||||
else if (playerCounts == null) {
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
OnlinePlayerCountChanged += OnOnlinePlayerCountChanged;
|
||||
|
||||
@@ -112,7 +131,7 @@ sealed class InstancePlayerCountTracker : CancellableBackgroundTask {
|
||||
protected override void Dispose() {
|
||||
lock (this) {
|
||||
isDisposed = true;
|
||||
onlinePlayerCount = null;
|
||||
playerCounts = null;
|
||||
}
|
||||
|
||||
process.RemoveOutputListener(OnOutput);
|
||||
|
@@ -8,9 +8,10 @@ public sealed partial record Instance(
|
||||
[property: MemoryPackOrder(0)] Guid InstanceGuid,
|
||||
[property: MemoryPackOrder(1)] InstanceConfiguration Configuration,
|
||||
[property: MemoryPackOrder(2)] IInstanceStatus Status,
|
||||
[property: MemoryPackOrder(3)] bool LaunchAutomatically
|
||||
[property: MemoryPackOrder(3)] InstancePlayerCounts? PlayerCounts,
|
||||
[property: MemoryPackOrder(4)] bool LaunchAutomatically
|
||||
) {
|
||||
public static Instance Offline(Guid instanceGuid, InstanceConfiguration configuration, bool launchAutomatically = false) {
|
||||
return new Instance(instanceGuid, configuration, InstanceStatus.Offline, launchAutomatically);
|
||||
return new Instance(instanceGuid, configuration, InstanceStatus.Offline, PlayerCounts: null, launchAutomatically);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,9 @@
|
||||
using MemoryPack;
|
||||
|
||||
namespace Phantom.Common.Data.Instance;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public readonly partial record struct InstancePlayerCounts(
|
||||
[property: MemoryPackOrder(0)] int Online,
|
||||
[property: MemoryPackOrder(1)] int Maximum
|
||||
);
|
@@ -31,6 +31,7 @@ public static class AgentMessageRegistries {
|
||||
ToController.Add<InstanceOutputMessage>(5);
|
||||
ToController.Add<ReportAgentStatusMessage>(6);
|
||||
ToController.Add<ReportInstanceEventMessage>(7);
|
||||
ToController.Add<ReportInstancePlayerCountsMessage>(8);
|
||||
ToController.Add<ReplyMessage>(127);
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,10 @@
|
||||
using MemoryPack;
|
||||
using Phantom.Common.Data.Instance;
|
||||
|
||||
namespace Phantom.Common.Messages.Agent.ToController;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record ReportInstancePlayerCountsMessage(
|
||||
[property: MemoryPackOrder(0)] Guid InstanceGuid,
|
||||
[property: MemoryPackOrder(1)] InstancePlayerCounts? PlayerCounts
|
||||
) : IMessageToController;
|
@@ -96,6 +96,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
||||
Receive<UpdateJavaRuntimesCommand>(UpdateJavaRuntimes);
|
||||
ReceiveAndReplyLater<CreateOrUpdateInstanceCommand, Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstance);
|
||||
Receive<UpdateInstanceStatusCommand>(UpdateInstanceStatus);
|
||||
Receive<UpdateInstancePlayerCountsCommand>(UpdateInstancePlayerCounts);
|
||||
ReceiveAndReplyLater<LaunchInstanceCommand, Result<LaunchInstanceResult, InstanceActionFailure>>(LaunchInstance);
|
||||
ReceiveAndReplyLater<StopInstanceCommand, Result<StopInstanceResult, InstanceActionFailure>>(StopInstance);
|
||||
ReceiveAndReplyLater<SendCommandToInstanceCommand, Result<SendCommandToInstanceResult, InstanceActionFailure>>(SendMinecraftCommand);
|
||||
@@ -159,7 +160,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
||||
private async Task<ImmutableArray<ConfigureInstanceMessage>> PrepareInitialConfigurationMessages() {
|
||||
var configurationMessages = ImmutableArray.CreateBuilder<ConfigureInstanceMessage>();
|
||||
|
||||
foreach (var (instanceGuid, instanceConfiguration, _, launchAutomatically) in instanceDataByGuid.Values.ToImmutableArray()) {
|
||||
foreach (var (instanceGuid, instanceConfiguration, _, _, launchAutomatically) in instanceDataByGuid.Values.ToImmutableArray()) {
|
||||
var serverExecutableInfo = await minecraftVersions.GetServerExecutableInfo(instanceConfiguration.MinecraftVersion, cancellationToken);
|
||||
configurationMessages.Add(new ConfigureInstanceMessage(instanceGuid, instanceConfiguration, new InstanceLaunchProperties(serverExecutableInfo), launchAutomatically));
|
||||
}
|
||||
@@ -187,6 +188,8 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
||||
|
||||
public sealed record UpdateInstanceStatusCommand(Guid InstanceGuid, IInstanceStatus Status) : ICommand;
|
||||
|
||||
public sealed record UpdateInstancePlayerCountsCommand(Guid InstanceGuid, InstancePlayerCounts? PlayerCounts) : ICommand;
|
||||
|
||||
public sealed record LaunchInstanceCommand(Guid LoggedInUserGuid, Guid InstanceGuid) : ICommand, ICanReply<Result<LaunchInstanceResult, InstanceActionFailure>>;
|
||||
|
||||
public sealed record StopInstanceCommand(Guid LoggedInUserGuid, Guid InstanceGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<Result<StopInstanceResult, InstanceActionFailure>>;
|
||||
@@ -342,6 +345,10 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
||||
TellInstance(command.InstanceGuid, new InstanceActor.SetStatusCommand(command.Status));
|
||||
}
|
||||
|
||||
private void UpdateInstancePlayerCounts(UpdateInstancePlayerCountsCommand command) {
|
||||
TellInstance(command.InstanceGuid, new InstanceActor.SetPlayerCountsCommand(command.PlayerCounts));
|
||||
}
|
||||
|
||||
private Task<Result<LaunchInstanceResult, InstanceActionFailure>> LaunchInstance(LaunchInstanceCommand command) {
|
||||
return RequestInstance<InstanceActor.LaunchInstanceCommand, LaunchInstanceResult>(command.InstanceGuid, new InstanceActor.LaunchInstanceCommand(command.LoggedInUserGuid));
|
||||
}
|
||||
|
@@ -26,6 +26,7 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
|
||||
|
||||
private InstanceConfiguration configuration;
|
||||
private IInstanceStatus status;
|
||||
private InstancePlayerCounts? playerCounts;
|
||||
private bool launchAutomatically;
|
||||
|
||||
private readonly ActorRef<InstanceDatabaseStorageActor.ICommand> databaseStorageActor;
|
||||
@@ -35,11 +36,12 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
|
||||
this.agentConnection = init.AgentConnection;
|
||||
this.cancellationToken = init.CancellationToken;
|
||||
|
||||
(this.instanceGuid, this.configuration, this.status, this.launchAutomatically) = init.Instance;
|
||||
(this.instanceGuid, this.configuration, this.status, this.playerCounts, this.launchAutomatically) = init.Instance;
|
||||
|
||||
this.databaseStorageActor = Context.ActorOf(InstanceDatabaseStorageActor.Factory(new InstanceDatabaseStorageActor.Init(instanceGuid, init.DbProvider, init.CancellationToken)), "DatabaseStorage");
|
||||
|
||||
Receive<SetStatusCommand>(SetStatus);
|
||||
Receive<SetPlayerCountsCommand>(SetPlayerCounts);
|
||||
ReceiveAsyncAndReply<ConfigureInstanceCommand, Result<ConfigureInstanceResult, InstanceActionFailure>>(ConfigureInstance);
|
||||
ReceiveAsyncAndReply<LaunchInstanceCommand, Result<LaunchInstanceResult, InstanceActionFailure>>(LaunchInstance);
|
||||
ReceiveAsyncAndReply<StopInstanceCommand, Result<StopInstanceResult, InstanceActionFailure>>(StopInstance);
|
||||
@@ -47,7 +49,7 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
|
||||
}
|
||||
|
||||
private void NotifyInstanceUpdated() {
|
||||
agentActor.Tell(new AgentActor.ReceiveInstanceDataCommand(new Instance(instanceGuid, configuration, status, launchAutomatically)));
|
||||
agentActor.Tell(new AgentActor.ReceiveInstanceDataCommand(new Instance(instanceGuid, configuration, status, playerCounts, launchAutomatically)));
|
||||
}
|
||||
|
||||
private void SetLaunchAutomatically(bool newValue) {
|
||||
@@ -66,6 +68,8 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
|
||||
|
||||
public sealed record SetStatusCommand(IInstanceStatus Status) : ICommand;
|
||||
|
||||
public sealed record SetPlayerCountsCommand(InstancePlayerCounts? PlayerCounts) : ICommand;
|
||||
|
||||
public sealed record ConfigureInstanceCommand(Guid AuditLogUserGuid, Guid InstanceGuid, InstanceConfiguration Configuration, InstanceLaunchProperties LaunchProperties, bool IsCreatingInstance) : ICommand, ICanReply<Result<ConfigureInstanceResult, InstanceActionFailure>>;
|
||||
|
||||
public sealed record LaunchInstanceCommand(Guid AuditLogUserGuid) : ICommand, ICanReply<Result<LaunchInstanceResult, InstanceActionFailure>>;
|
||||
@@ -76,6 +80,16 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
|
||||
|
||||
private void SetStatus(SetStatusCommand command) {
|
||||
status = command.Status;
|
||||
|
||||
if (!status.IsRunning() && status != InstanceStatus.Offline /* Guard against temporary disconnects */) {
|
||||
playerCounts = null;
|
||||
}
|
||||
|
||||
NotifyInstanceUpdated();
|
||||
}
|
||||
|
||||
private void SetPlayerCounts(SetPlayerCountsCommand command) {
|
||||
playerCounts = command.PlayerCounts;
|
||||
NotifyInstanceUpdated();
|
||||
}
|
||||
|
||||
|
@@ -39,6 +39,7 @@ sealed class AgentMessageHandlerActor : ReceiveActor<IMessageToController> {
|
||||
Receive<AdvertiseJavaRuntimesMessage>(HandleAdvertiseJavaRuntimes);
|
||||
Receive<ReportAgentStatusMessage>(HandleReportAgentStatus);
|
||||
Receive<ReportInstanceStatusMessage>(HandleReportInstanceStatus);
|
||||
Receive<ReportInstancePlayerCountsMessage>(HandleReportInstancePlayerCounts);
|
||||
Receive<ReportInstanceEventMessage>(HandleReportInstanceEvent);
|
||||
Receive<InstanceOutputMessage>(HandleInstanceOutput);
|
||||
Receive<ReplyMessage>(HandleReply);
|
||||
@@ -74,6 +75,10 @@ sealed class AgentMessageHandlerActor : ReceiveActor<IMessageToController> {
|
||||
agentManager.TellAgent(agentGuid, new AgentActor.UpdateInstanceStatusCommand(message.InstanceGuid, message.InstanceStatus));
|
||||
}
|
||||
|
||||
private void HandleReportInstancePlayerCounts(ReportInstancePlayerCountsMessage message) {
|
||||
agentManager.TellAgent(agentGuid, new AgentActor.UpdateInstancePlayerCountsCommand(message.InstanceGuid, message.PlayerCounts));
|
||||
}
|
||||
|
||||
private void HandleReportInstanceEvent(ReportInstanceEventMessage message) {
|
||||
message.Event.Accept(eventLogManager.CreateInstanceEventVisitor(message.EventGuid, message.UtcTime, agentGuid, message.InstanceGuid));
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Immutable;
|
||||
using Phantom.Common.Data;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults;
|
||||
using Phantom.Controller.Database;
|
||||
using Phantom.Controller.Database.Entities;
|
||||
using Phantom.Controller.Database.Repositories;
|
||||
@@ -56,12 +57,12 @@ sealed class UserManager {
|
||||
wasCreated = true;
|
||||
}
|
||||
else {
|
||||
return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.CreationFailed(result.Error);
|
||||
return new CreationFailed(result.Error);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (userRepository.SetUserPassword(user, password).TryGetError(out var error)) {
|
||||
return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.UpdatingFailed(error);
|
||||
return new UpdatingFailed(error);
|
||||
}
|
||||
|
||||
auditLogWriter.AdministratorUserModified(user);
|
||||
@@ -70,7 +71,7 @@ sealed class UserManager {
|
||||
|
||||
var role = await new RoleRepository(db).GetByGuid(Role.Administrator.Guid);
|
||||
if (role == null) {
|
||||
return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.AddingToRoleFailed();
|
||||
return new AddingToRoleFailed();
|
||||
}
|
||||
|
||||
await new UserRoleRepository(db).Add(user, role);
|
||||
@@ -84,10 +85,10 @@ sealed class UserManager {
|
||||
Logger.Information("Updated administrator user \"{Username}\" (GUID {Guid}).", username, user.UserGuid);
|
||||
}
|
||||
|
||||
return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.Success(user.ToUserInfo());
|
||||
return new Success(user.ToUserInfo());
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "Could not create or update administrator user \"{Username}\".", username);
|
||||
return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.UnknownError();
|
||||
return new UnknownError();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>11</LangVersion>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>13</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
10
Dockerfile
10
Dockerfile
@@ -1,7 +1,7 @@
|
||||
# +---------------+
|
||||
# | Prepare build |
|
||||
# +---------------+
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/nightly/sdk:8.0 AS phantom-builder
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/nightly/sdk:9.0 AS phantom-builder
|
||||
ARG TARGETARCH
|
||||
|
||||
ADD . /app
|
||||
@@ -19,7 +19,7 @@ RUN find .artifacts/publish/*/* -maxdepth 0 -execdir mv '{}' 'release' \;
|
||||
# +---------------------+
|
||||
# | Phantom Agent image |
|
||||
# +---------------------+
|
||||
FROM mcr.microsoft.com/dotnet/nightly/runtime:8.0 AS phantom-agent
|
||||
FROM mcr.microsoft.com/dotnet/nightly/runtime:9.0 AS phantom-agent
|
||||
|
||||
RUN mkdir /data && chmod 777 /data
|
||||
WORKDIR /data
|
||||
@@ -27,7 +27,7 @@ WORKDIR /data
|
||||
COPY --from=eclipse-temurin:8-jre /opt/java/openjdk /opt/java/8
|
||||
COPY --from=eclipse-temurin:16-jdk /opt/java/openjdk /opt/java/16
|
||||
COPY --from=eclipse-temurin:17-jre /opt/java/openjdk /opt/java/17
|
||||
COPY --from=eclipse-temurin:20-jre /opt/java/openjdk /opt/java/20
|
||||
COPY --from=eclipse-temurin:21-jre /opt/java/openjdk /opt/java/21
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
@@ -46,7 +46,7 @@ ENTRYPOINT ["dotnet", "/app/Phantom.Agent.dll"]
|
||||
# +--------------------------+
|
||||
# | Phantom Controller image |
|
||||
# +--------------------------+
|
||||
FROM mcr.microsoft.com/dotnet/nightly/runtime:8.0 AS phantom-controller
|
||||
FROM mcr.microsoft.com/dotnet/nightly/runtime:9.0 AS phantom-controller
|
||||
|
||||
RUN mkdir /data && chmod 777 /data
|
||||
WORKDIR /data
|
||||
@@ -59,7 +59,7 @@ ENTRYPOINT ["dotnet", "/app/Phantom.Controller.dll"]
|
||||
# +-------------------+
|
||||
# | Phantom Web image |
|
||||
# +-------------------+
|
||||
FROM mcr.microsoft.com/dotnet/nightly/aspnet:8.0 AS phantom-web
|
||||
FROM mcr.microsoft.com/dotnet/nightly/aspnet:9.0 AS phantom-web
|
||||
|
||||
RUN mkdir /data && chmod 777 /data
|
||||
WORKDIR /data
|
||||
|
@@ -1,6 +1,6 @@
|
||||
@using Phantom.Web.Services.Authentication
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@using Phantom.Web.Services
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@using Phantom.Web.Services.Authentication
|
||||
@inject ApplicationProperties ApplicationProperties
|
||||
|
||||
<div class="navbar navbar-dark">
|
||||
|
@@ -3,8 +3,8 @@
|
||||
@using System.Collections.Immutable
|
||||
@using Phantom.Common.Data.Web.AuditLog
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@using Phantom.Web.Services.Users
|
||||
@using Phantom.Web.Services.Instances
|
||||
@using Phantom.Web.Services.Users
|
||||
@inherits PhantomComponent
|
||||
@inject AuditLogManager AuditLogManager
|
||||
@inject InstanceManager InstanceManager
|
||||
|
@@ -6,7 +6,7 @@
|
||||
@using Phantom.Web.Services.Agents
|
||||
@using Phantom.Web.Services.Events
|
||||
@using Phantom.Web.Services.Instances
|
||||
@inherits Phantom.Web.Components.PhantomComponent
|
||||
@inherits PhantomComponent
|
||||
@inject AgentManager AgentManager
|
||||
@inject EventLogManager EventLogManager
|
||||
@inject InstanceManager InstanceManager
|
||||
|
@@ -1,5 +1,5 @@
|
||||
@page "/"
|
||||
@inherits Phantom.Web.Components.PhantomComponent
|
||||
@inherits PhantomComponent
|
||||
|
||||
<h1>Home</h1>
|
||||
|
||||
|
@@ -21,6 +21,7 @@
|
||||
<Column Width="40%">Agent</Column>
|
||||
<Column Width="40%">Name</Column>
|
||||
<Column MinWidth="215px">Status</Column>
|
||||
<Column Class="text-center" MinWidth="120px">Players</Column>
|
||||
<Column Width="20%">Version</Column>
|
||||
<Column Class="text-center" MinWidth="110px">Server Port</Column>
|
||||
<Column Class="text-center" MinWidth="110px">Rcon Port</Column>
|
||||
@@ -40,6 +41,14 @@
|
||||
<Cell>
|
||||
<InstanceStatusText Status="instance.Status" />
|
||||
</Cell>
|
||||
<Cell class="text-center">
|
||||
@if (instance.PlayerCounts is var (online, maximum)) {
|
||||
<p class="font-monospace">@online.ToString() / @maximum.ToString()</p>
|
||||
}
|
||||
else {
|
||||
<p class="font-monospace">-</p>
|
||||
}
|
||||
</Cell>
|
||||
<Cell>@configuration.MinecraftServerKind @configuration.MinecraftVersion</Cell>
|
||||
<Cell class="text-center">
|
||||
<p class="font-monospace">@configuration.ServerPort.ToString()</p>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
@page "/login"
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Phantom.Web.Services
|
||||
@using Phantom.Web.Services.Authentication
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@attribute [AllowAnonymous]
|
||||
@inject Navigation Navigation
|
||||
@inject UserLoginManager LoginManager
|
||||
|
@@ -1,4 +1,6 @@
|
||||
@page "/setup"
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using System.Security.Cryptography
|
||||
@using Phantom.Common.Data
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@using Phantom.Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults
|
||||
@@ -7,8 +9,6 @@
|
||||
@using Phantom.Web.Services
|
||||
@using Phantom.Web.Services.Authentication
|
||||
@using Phantom.Web.Services.Rpc
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using System.Security.Cryptography
|
||||
@attribute [AllowAnonymous]
|
||||
@inject ApplicationProperties ApplicationProperties
|
||||
@inject UserLoginManager LoginManager
|
||||
|
@@ -1,11 +1,11 @@
|
||||
@page "/users"
|
||||
@attribute [Authorize(Permission.ViewUsersPolicy)]
|
||||
@using System.Collections.Immutable
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@using Phantom.Web.Services.Authentication
|
||||
@using Phantom.Web.Services.Authorization
|
||||
@using Phantom.Web.Services.Users
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@inherits Phantom.Web.Components.PhantomComponent
|
||||
@inherits PhantomComponent
|
||||
@inject UserManager UserManager
|
||||
@inject RoleManager RoleManager
|
||||
@inject UserRoleManager UserRoleManager
|
||||
|
@@ -2,22 +2,27 @@
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Data.Web.Minecraft;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Common.Data.Web.Users.AddUserErrors;
|
||||
using Phantom.Common.Data.Web.Users.PasswordRequirementViolations;
|
||||
using Phantom.Common.Data.Web.Users.SetUserPasswordErrors;
|
||||
using Phantom.Common.Data.Web.Users.UsernameRequirementViolations;
|
||||
using PasswordIsInvalid = Phantom.Common.Data.Web.Users.AddUserErrors.PasswordIsInvalid;
|
||||
|
||||
namespace Phantom.Web.Utils;
|
||||
|
||||
static class Messages {
|
||||
public static string ToSentences(this AddUserError error, string delimiter) {
|
||||
return error switch {
|
||||
Common.Data.Web.Users.AddUserErrors.NameIsInvalid e => e.Violation.ToSentence(),
|
||||
Common.Data.Web.Users.AddUserErrors.PasswordIsInvalid e => string.Join(delimiter, e.Violations.Select(static v => v.ToSentence())),
|
||||
Common.Data.Web.Users.AddUserErrors.NameAlreadyExists => "Username is already occupied.",
|
||||
NameIsInvalid e => e.Violation.ToSentence(),
|
||||
PasswordIsInvalid e => string.Join(delimiter, e.Violations.Select(static v => v.ToSentence())),
|
||||
NameAlreadyExists => "Username is already occupied.",
|
||||
_ => "Unknown error."
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToSentences(this SetUserPasswordError error, string delimiter) {
|
||||
return error switch {
|
||||
Common.Data.Web.Users.SetUserPasswordErrors.UserNotFound => "User not found.",
|
||||
UserNotFound => "User not found.",
|
||||
Common.Data.Web.Users.SetUserPasswordErrors.PasswordIsInvalid e => string.Join(delimiter, e.Violations.Select(static v => v.ToSentence())),
|
||||
_ => "Unknown error."
|
||||
};
|
||||
@@ -25,18 +30,18 @@ static class Messages {
|
||||
|
||||
public static string ToSentence(this UsernameRequirementViolation violation) {
|
||||
return violation switch {
|
||||
Common.Data.Web.Users.UsernameRequirementViolations.IsEmpty => "Username must not be empty.",
|
||||
Common.Data.Web.Users.UsernameRequirementViolations.TooLong v => "Username must not be longer than " + v.MaxLength + " character(s).",
|
||||
IsEmpty => "Username must not be empty.",
|
||||
TooLong v => "Username must not be longer than " + v.MaxLength + " character(s).",
|
||||
_ => "Unknown error."
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToSentence(this PasswordRequirementViolation violation) {
|
||||
return violation switch {
|
||||
Common.Data.Web.Users.PasswordRequirementViolations.TooShort v => "Password must be at least " + v.MinimumLength + " character(s) long.",
|
||||
Common.Data.Web.Users.PasswordRequirementViolations.MustContainLowercaseLetter => "Password must contain a lowercase letter.",
|
||||
Common.Data.Web.Users.PasswordRequirementViolations.MustContainUppercaseLetter => "Password must contain an uppercase letter.",
|
||||
Common.Data.Web.Users.PasswordRequirementViolations.MustContainDigit => "Password must contain a digit.",
|
||||
TooShort v => "Password must be at least " + v.MinimumLength + " character(s) long.",
|
||||
MustContainLowercaseLetter => "Password must contain a lowercase letter.",
|
||||
MustContainUppercaseLetter => "Password must contain an uppercase letter.",
|
||||
MustContainDigit => "Password must contain a digit.",
|
||||
_ => "Unknown error."
|
||||
};
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "8.0.0",
|
||||
"version": "9.0.0",
|
||||
"rollForward": "latestMinor",
|
||||
"allowPrerelease": true
|
||||
}
|
||||
|
Reference in New Issue
Block a user