1
0
mirror of https://github.com/chylex/Minecraft-Phantom-Panel.git synced 2026-01-14 05:50:30 +01:00

2 Commits

Author SHA1 Message Date
93fde594a4 WIP 2025-12-27 00:01:25 +01:00
e4dbb18584 WIP 2025-12-26 12:55:12 +01:00
66 changed files with 312 additions and 1641 deletions

View File

@@ -3,7 +3,7 @@
"isRoot": true, "isRoot": true,
"tools": { "tools": {
"dotnet-ef": { "dotnet-ef": {
"version": "10.0.1", "version": "9.0.9",
"commands": [ "commands": [
"dotnet-ef" "dotnet-ef"
], ],

View File

@@ -43,7 +43,7 @@ sealed record Variables(
try { try {
return LoadOrThrow(); return LoadOrThrow();
} catch (Exception e) { } catch (Exception e) {
PhantomLogger.Root.Fatal("{}", e.Message); PhantomLogger.Root.Fatal(e.Message);
throw StopProcedureException.Instance; throw StopProcedureException.Instance;
} }
} }

View File

@@ -7,38 +7,12 @@ namespace Phantom.Common.Data.Web.Agent;
[MemoryPackable(GenerateType.VersionTolerant)] [MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record Agent( public sealed partial record Agent(
[property: MemoryPackOrder(0)] Guid AgentGuid, [property: MemoryPackOrder(0)] Guid AgentGuid,
[property: MemoryPackOrder(1)] AgentConfiguration Configuration, [property: MemoryPackOrder(1)] string Name,
[property: MemoryPackOrder(2)] ImmutableArray<byte> ConnectionKey, [property: MemoryPackOrder(2)] ImmutableArray<byte> ConnectionKey,
[property: MemoryPackOrder(3)] AgentRuntimeInfo RuntimeInfo, [property: MemoryPackOrder(3)] AgentConfiguration Configuration,
[property: MemoryPackOrder(4)] AgentStats? Stats, [property: MemoryPackOrder(4)] AgentStats? Stats,
[property: MemoryPackOrder(5)] IAgentConnectionStatus ConnectionStatus [property: MemoryPackOrder(5)] IAgentConnectionStatus ConnectionStatus
) { ) {
[MemoryPackIgnore] [MemoryPackIgnore]
public RamAllocationUnits? AvailableMemory => RuntimeInfo.MaxMemory - Stats?.RunningInstanceMemory; public RamAllocationUnits? AvailableMemory => Configuration.MaxMemory - Stats?.RunningInstanceMemory;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record Update(
[property: MemoryPackOrder(0)] Optional<AgentConfiguration> Configuration,
[property: MemoryPackOrder(1)] Optional<ImmutableArray<byte>> ConnectionKey,
[property: MemoryPackOrder(2)] Optional<AgentRuntimeInfo> RuntimeInfo,
[property: MemoryPackOrder(3)] OptionalNullable<AgentStats> Stats,
[property: MemoryPackOrder(4)] Optional<IAgentConnectionStatus> ConnectionStatus
) {
public Update Merge(Update newer) => new (
newer.Configuration.Or(Configuration),
newer.ConnectionKey.Or(ConnectionKey),
newer.RuntimeInfo.Or(RuntimeInfo),
newer.Stats.Or(Stats),
newer.ConnectionStatus.Or(ConnectionStatus)
);
public Agent Apply(Agent target) => new (
target.AgentGuid,
Configuration.Or(target.Configuration),
ConnectionKey.Or(target.ConnectionKey),
RuntimeInfo.Or(target.RuntimeInfo),
Stats.Or(target.Stats),
ConnectionStatus.Or(target.ConnectionStatus)
);
}
} }

View File

@@ -1,8 +1,18 @@
using MemoryPack; using MemoryPack;
using Phantom.Common.Data.Agent;
namespace Phantom.Common.Data.Web.Agent; namespace Phantom.Common.Data.Web.Agent;
[MemoryPackable(GenerateType.VersionTolerant)] [MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record AgentConfiguration( public sealed partial record AgentConfiguration(
[property: MemoryPackOrder(0)] string AgentName [property: MemoryPackOrder(0)] ushort ProtocolVersion,
); [property: MemoryPackOrder(1)] string BuildVersion,
[property: MemoryPackOrder(2)] ushort MaxInstances,
[property: MemoryPackOrder(3)] RamAllocationUnits MaxMemory,
[property: MemoryPackOrder(4)] AllowedPorts? AllowedServerPorts = null,
[property: MemoryPackOrder(5)] AllowedPorts? AllowedRconPorts = null
) {
public static AgentConfiguration From(AgentInfo agentInfo) {
return new AgentConfiguration(agentInfo.ProtocolVersion, agentInfo.BuildVersion, agentInfo.MaxInstances, agentInfo.MaxMemory, agentInfo.AllowedServerPorts, agentInfo.AllowedRconPorts);
}
}

View File

@@ -1,17 +0,0 @@
using MemoryPack;
using Phantom.Common.Data.Agent;
namespace Phantom.Common.Data.Web.Agent;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record AgentRuntimeInfo(
[property: MemoryPackOrder(0)] AgentVersionInfo? VersionInfo = null,
[property: MemoryPackOrder(1)] ushort? MaxInstances = null,
[property: MemoryPackOrder(2)] RamAllocationUnits? MaxMemory = null,
[property: MemoryPackOrder(3)] AllowedPorts? AllowedServerPorts = null,
[property: MemoryPackOrder(4)] AllowedPorts? AllowedRconPorts = null
) {
public static AgentRuntimeInfo From(AgentInfo agentInfo) {
return new AgentRuntimeInfo(new AgentVersionInfo(agentInfo.ProtocolVersion, agentInfo.BuildVersion), agentInfo.MaxInstances, agentInfo.MaxMemory, agentInfo.AllowedServerPorts, agentInfo.AllowedRconPorts);
}
}

View File

@@ -1,9 +0,0 @@
using MemoryPack;
namespace Phantom.Common.Data.Web.Agent;
[MemoryPackable(GenerateType.VersionTolerant)]
public readonly partial record struct AgentVersionInfo(
[property: MemoryPackOrder(0)] ushort ProtocolVersion,
[property: MemoryPackOrder(1)] string BuildVersion
);

View File

@@ -1,17 +0,0 @@
namespace Phantom.Common.Data.Web.Agent;
public enum CreateOrUpdateAgentResult : byte {
UnknownError,
Success,
AgentNameMustNotBeEmpty,
}
public static class CreateOrUpdateAgentResultExtensions {
public static string ToSentence(this CreateOrUpdateAgentResult reason) {
return reason switch {
CreateOrUpdateAgentResult.Success => "Success.",
CreateOrUpdateAgentResult.AgentNameMustNotBeEmpty => "Agent name must not be empty.",
_ => "Unknown error.",
};
}
}

View File

@@ -9,8 +9,6 @@ public enum AuditLogEventType {
UserPasswordChanged, UserPasswordChanged,
UserRolesChanged, UserRolesChanged,
UserDeleted, UserDeleted,
AgentCreated,
AgentEdited,
InstanceCreated, InstanceCreated,
InstanceEdited, InstanceEdited,
InstanceLaunched, InstanceLaunched,
@@ -28,8 +26,6 @@ public static class AuditLogEventTypeExtensions {
{ AuditLogEventType.UserPasswordChanged, AuditLogSubjectType.User }, { AuditLogEventType.UserPasswordChanged, AuditLogSubjectType.User },
{ AuditLogEventType.UserRolesChanged, AuditLogSubjectType.User }, { AuditLogEventType.UserRolesChanged, AuditLogSubjectType.User },
{ AuditLogEventType.UserDeleted, AuditLogSubjectType.User }, { AuditLogEventType.UserDeleted, AuditLogSubjectType.User },
{ AuditLogEventType.AgentCreated, AuditLogSubjectType.Agent },
{ AuditLogEventType.AgentEdited, AuditLogSubjectType.Agent },
{ AuditLogEventType.InstanceCreated, AuditLogSubjectType.Instance }, { AuditLogEventType.InstanceCreated, AuditLogSubjectType.Instance },
{ AuditLogEventType.InstanceEdited, AuditLogSubjectType.Instance }, { AuditLogEventType.InstanceEdited, AuditLogSubjectType.Instance },
{ AuditLogEventType.InstanceLaunched, AuditLogSubjectType.Instance }, { AuditLogEventType.InstanceLaunched, AuditLogSubjectType.Instance },

View File

@@ -2,6 +2,5 @@
public enum AuditLogSubjectType { public enum AuditLogSubjectType {
User, User,
Agent,
Instance, Instance,
} }

View File

@@ -1,5 +0,0 @@
namespace Phantom.Common.Data.Web;
public class RemoteDictionary<K, V> {
}

View File

@@ -3,47 +3,8 @@
namespace Phantom.Common.Data; namespace Phantom.Common.Data;
[MemoryPackable] [MemoryPackable]
public readonly partial struct Optional<T> { public readonly partial record struct Optional<T>(T? Value) {
[MemoryPackOrder(0)] public static implicit operator Optional<T>(T? value) {
public bool HasValue { get; }
[MemoryPackOrder(1)]
public T Value {
get {
if (HasValue) {
return field!;
}
else {
throw new InvalidOperationException();
}
}
}
[MemoryPackIgnore]
public T? ValueOrDefault => HasValue ? Value : default;
public Optional() : this(hasValue: false, value: default) {}
public Optional(T value) : this(hasValue: true, value) {}
[MemoryPackConstructor]
private Optional(bool hasValue, T? value) {
this.HasValue = hasValue;
this.Value = value;
}
public Optional<R> Map<R>(Func<T, R> func) {
return HasValue ? new Optional<R>(func(Value)) : new Optional<R>();
}
public T Or(T fallbackValue) {
return HasValue ? Value : fallbackValue;
}
public Optional<T> Or(Optional<T> fallbackOptional) {
return HasValue ? Value : fallbackOptional;
}
public static implicit operator Optional<T>(T value) {
return new Optional<T>(value); return new Optional<T>(value);
} }
} }

View File

@@ -1,42 +0,0 @@
using MemoryPack;
namespace Phantom.Common.Data;
[MemoryPackable]
public readonly partial struct OptionalNullable<T> {
[MemoryPackOrder(0)]
public bool HasValue { get; }
[MemoryPackOrder(1)]
public T? Value {
get {
if (HasValue) {
return field;
}
else {
throw new InvalidOperationException();
}
}
}
public OptionalNullable() : this(hasValue: false, value: default) {}
public OptionalNullable(T? value) : this(hasValue: true, value) {}
[MemoryPackConstructor]
private OptionalNullable(bool hasValue, T? value) {
this.HasValue = hasValue;
this.Value = value;
}
public T? Or(T? fallbackValue) {
return HasValue ? Value : fallbackValue;
}
public OptionalNullable<T> Or(OptionalNullable<T> fallbackOptional) {
return HasValue ? Value : fallbackOptional;
}
public static implicit operator OptionalNullable<T>(T? value) {
return new OptionalNullable<T>(value);
}
}

View File

@@ -18,10 +18,10 @@ public static class AgentMessageRegistries {
ToAgent.Add<StopInstanceMessage, Result<StopInstanceResult, InstanceActionFailure>>(); ToAgent.Add<StopInstanceMessage, Result<StopInstanceResult, InstanceActionFailure>>();
ToAgent.Add<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, InstanceActionFailure>>(); ToAgent.Add<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, InstanceActionFailure>>();
ToController.Add<ReportAgentStatusMessage>();
ToController.Add<ReportInstanceStatusMessage>(); ToController.Add<ReportInstanceStatusMessage>();
ToController.Add<ReportInstancePlayerCountsMessage>();
ToController.Add<ReportInstanceEventMessage>();
ToController.Add<InstanceOutputMessage>(); ToController.Add<InstanceOutputMessage>();
ToController.Add<ReportAgentStatusMessage>();
ToController.Add<ReportInstanceEventMessage>();
ToController.Add<ReportInstancePlayerCountsMessage>();
} }
} }

View File

@@ -1,15 +0,0 @@
using System.Collections.Immutable;
using MemoryPack;
using Phantom.Common.Data;
using Phantom.Common.Data.Web.Agent;
using Phantom.Common.Data.Web.Users;
using Phantom.Utils.Actor;
namespace Phantom.Common.Messages.Web.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record CreateOrUpdateAgentMessage(
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
[property: MemoryPackOrder(1)] Guid AgentGuid,
[property: MemoryPackOrder(2)] AgentConfiguration Configuration
) : IMessageToController, ICanReply<Result<CreateOrUpdateAgentResult, UserActionFailure>>;

View File

@@ -1,25 +0,0 @@
using System.Collections.Immutable;
using MemoryPack;
using Phantom.Common.Data.Web.Agent;
namespace Phantom.Common.Messages.Web.ToWeb;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record RefreshAgentsMessage2(
[property: MemoryPackOrder(0)] ImmutableArray<RefreshAgentsMessage2.IItemAction> Actions
) : IMessageToWeb {
[MemoryPackable]
[MemoryPackUnion(tag: 0, typeof(RemoveItem))]
[MemoryPackUnion(tag: 1, typeof(SetItem))]
[MemoryPackUnion(tag: 2, typeof(UpdateItem))]
public partial interface IItemAction;
[MemoryPackable]
public sealed partial record RemoveItem(Guid AgentGuid) : IItemAction;
[MemoryPackable]
public sealed partial record SetItem(Agent Agent) : IItemAction;
[MemoryPackable]
public sealed partial record UpdateItem(Guid AgentGuid, Agent.Update Update) : IItemAction;
}

View File

@@ -3,7 +3,6 @@ using Phantom.Common.Data;
using Phantom.Common.Data.Java; using Phantom.Common.Data.Java;
using Phantom.Common.Data.Minecraft; using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies; using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.Agent;
using Phantom.Common.Data.Web.AuditLog; using Phantom.Common.Data.Web.AuditLog;
using Phantom.Common.Data.Web.EventLog; using Phantom.Common.Data.Web.EventLog;
using Phantom.Common.Data.Web.Instance; using Phantom.Common.Data.Web.Instance;
@@ -26,23 +25,21 @@ public static class WebMessageRegistries {
ToController.Add<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(); ToController.Add<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>();
ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(); ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>();
ToController.Add<CreateUserMessage, Result<CreateUserResult, UserActionFailure>>(); ToController.Add<CreateUserMessage, Result<CreateUserResult, UserActionFailure>>();
ToController.Add<DeleteUserMessage, Result<DeleteUserResult, UserActionFailure>>();
ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(); ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>();
ToController.Add<GetRolesMessage, ImmutableArray<RoleInfo>>(); ToController.Add<GetRolesMessage, ImmutableArray<RoleInfo>>();
ToController.Add<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(); ToController.Add<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>();
ToController.Add<ChangeUserRolesMessage, Result<ChangeUserRolesResult, UserActionFailure>>(); ToController.Add<ChangeUserRolesMessage, Result<ChangeUserRolesResult, UserActionFailure>>();
ToController.Add<DeleteUserMessage, Result<DeleteUserResult, UserActionFailure>>();
ToController.Add<CreateOrUpdateAgentMessage, Result<CreateOrUpdateAgentResult, UserActionFailure>>();
ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>();
ToController.Add<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(); ToController.Add<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>();
ToController.Add<LaunchInstanceMessage, Result<LaunchInstanceResult, UserInstanceActionFailure>>(); ToController.Add<LaunchInstanceMessage, Result<LaunchInstanceResult, UserInstanceActionFailure>>();
ToController.Add<StopInstanceMessage, Result<StopInstanceResult, UserInstanceActionFailure>>(); ToController.Add<StopInstanceMessage, Result<StopInstanceResult, UserInstanceActionFailure>>();
ToController.Add<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, UserInstanceActionFailure>>(); ToController.Add<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, UserInstanceActionFailure>>();
ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(); ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>();
ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>();
ToController.Add<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>(); ToController.Add<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>();
ToController.Add<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>(); ToController.Add<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>();
ToWeb.Add<RefreshAgentsMessage>(); ToWeb.Add<RefreshAgentsMessage>();
ToWeb.Add<RefreshAgentsMessage2>();
ToWeb.Add<RefreshInstancesMessage>(); ToWeb.Add<RefreshInstancesMessage>();
ToWeb.Add<InstanceOutputMessage>(); ToWeb.Add<InstanceOutputMessage>();
ToWeb.Add<RefreshUserSessionMessage>(); ToWeb.Add<RefreshUserSessionMessage>();

View File

@@ -1,356 +0,0 @@
// <auto-generated />
using System;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Phantom.Controller.Database;
#nullable disable
namespace Phantom.Controller.Database.Postgres.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20251228053557_AgentFieldNullability")]
partial class AgentFieldNullability
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Phantom.Controller.Database.Entities.AgentEntity", b =>
{
b.Property<Guid>("AgentGuid")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<byte[]>("AuthSecret")
.HasMaxLength(12)
.HasColumnType("bytea");
b.Property<string>("BuildVersion")
.HasColumnType("text");
b.Property<int?>("MaxInstances")
.HasColumnType("integer");
b.Property<int?>("MaxMemory")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int?>("ProtocolVersion")
.HasColumnType("integer");
b.HasKey("AgentGuid");
b.ToTable("Agents", "agents");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.AuditLogEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<JsonDocument>("Data")
.HasColumnType("jsonb");
b.Property<string>("EventType")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SubjectId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SubjectType")
.IsRequired()
.HasColumnType("text");
b.Property<Guid?>("UserGuid")
.HasColumnType("uuid");
b.Property<DateTime>("UtcTime")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("UserGuid");
b.ToTable("AuditLog", "system");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.EventLogEntity", b =>
{
b.Property<Guid>("EventGuid")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid?>("AgentGuid")
.HasColumnType("uuid");
b.Property<JsonDocument>("Data")
.HasColumnType("jsonb");
b.Property<string>("EventType")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SubjectId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SubjectType")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("UtcTime")
.HasColumnType("timestamp with time zone");
b.HasKey("EventGuid");
b.ToTable("EventLog", "system");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.InstanceEntity", b =>
{
b.Property<Guid>("InstanceGuid")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("AgentGuid")
.HasColumnType("uuid");
b.Property<string>("InstanceName")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("JavaRuntimeGuid")
.HasColumnType("uuid");
b.Property<string>("JvmArguments")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("LaunchAutomatically")
.HasColumnType("boolean");
b.Property<int>("MemoryAllocation")
.HasColumnType("integer");
b.Property<string>("MinecraftServerKind")
.IsRequired()
.HasColumnType("text");
b.Property<string>("MinecraftVersion")
.IsRequired()
.HasColumnType("text");
b.Property<int>("RconPort")
.HasColumnType("integer");
b.Property<int>("ServerPort")
.HasColumnType("integer");
b.HasKey("InstanceGuid");
b.ToTable("Instances", "agents");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.PermissionEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Permissions", "identity");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.RoleEntity", b =>
{
b.Property<Guid>("RoleGuid")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("RoleGuid");
b.ToTable("Roles", "identity");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.RolePermissionEntity", b =>
{
b.Property<Guid>("RoleGuid")
.HasColumnType("uuid");
b.Property<string>("PermissionId")
.HasColumnType("text");
b.HasKey("RoleGuid", "PermissionId");
b.HasIndex("PermissionId");
b.ToTable("RolePermissions", "identity");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserAgentAccessEntity", b =>
{
b.Property<Guid>("UserGuid")
.HasColumnType("uuid");
b.Property<Guid>("AgentGuid")
.HasColumnType("uuid");
b.HasKey("UserGuid", "AgentGuid");
b.HasIndex("AgentGuid");
b.ToTable("UserAgentAccess", "identity");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserEntity", b =>
{
b.Property<Guid>("UserGuid")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("text");
b.HasKey("UserGuid");
b.HasIndex("Name")
.IsUnique();
b.ToTable("Users", "identity");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserPermissionEntity", b =>
{
b.Property<Guid>("UserGuid")
.HasColumnType("uuid");
b.Property<string>("PermissionId")
.HasColumnType("text");
b.HasKey("UserGuid", "PermissionId");
b.HasIndex("PermissionId");
b.ToTable("UserPermissions", "identity");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserRoleEntity", b =>
{
b.Property<Guid>("UserGuid")
.HasColumnType("uuid");
b.Property<Guid>("RoleGuid")
.HasColumnType("uuid");
b.HasKey("UserGuid", "RoleGuid");
b.HasIndex("RoleGuid");
b.ToTable("UserRoles", "identity");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.AuditLogEntity", b =>
{
b.HasOne("Phantom.Controller.Database.Entities.UserEntity", "User")
.WithMany()
.HasForeignKey("UserGuid")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("User");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.RolePermissionEntity", b =>
{
b.HasOne("Phantom.Controller.Database.Entities.PermissionEntity", null)
.WithMany()
.HasForeignKey("PermissionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Phantom.Controller.Database.Entities.RoleEntity", null)
.WithMany()
.HasForeignKey("RoleGuid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserAgentAccessEntity", b =>
{
b.HasOne("Phantom.Controller.Database.Entities.AgentEntity", null)
.WithMany()
.HasForeignKey("AgentGuid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Phantom.Controller.Database.Entities.UserEntity", null)
.WithMany()
.HasForeignKey("UserGuid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserPermissionEntity", b =>
{
b.HasOne("Phantom.Controller.Database.Entities.PermissionEntity", null)
.WithMany()
.HasForeignKey("PermissionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Phantom.Controller.Database.Entities.UserEntity", null)
.WithMany()
.HasForeignKey("UserGuid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserRoleEntity", b =>
{
b.HasOne("Phantom.Controller.Database.Entities.RoleEntity", "Role")
.WithMany()
.HasForeignKey("RoleGuid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Phantom.Controller.Database.Entities.UserEntity", "User")
.WithMany()
.HasForeignKey("UserGuid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
b.Navigation("User");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,98 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Phantom.Controller.Database.Postgres.Migrations
{
/// <inheritdoc />
public partial class AgentFieldNullability : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<int>(
name: "ProtocolVersion",
schema: "agents",
table: "Agents",
type: "integer",
nullable: true,
oldClrType: typeof(int),
oldType: "integer");
migrationBuilder.AlterColumn<int>(
name: "MaxMemory",
schema: "agents",
table: "Agents",
type: "integer",
nullable: true,
oldClrType: typeof(int),
oldType: "integer");
migrationBuilder.AlterColumn<int>(
name: "MaxInstances",
schema: "agents",
table: "Agents",
type: "integer",
nullable: true,
oldClrType: typeof(int),
oldType: "integer");
migrationBuilder.AlterColumn<string>(
name: "BuildVersion",
schema: "agents",
table: "Agents",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<int>(
name: "ProtocolVersion",
schema: "agents",
table: "Agents",
type: "integer",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "integer",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "MaxMemory",
schema: "agents",
table: "Agents",
type: "integer",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "integer",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "MaxInstances",
schema: "agents",
table: "Agents",
type: "integer",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "integer",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "BuildVersion",
schema: "agents",
table: "Agents",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
}
}
}

View File

@@ -1,357 +0,0 @@
// <auto-generated />
using System;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Phantom.Controller.Database;
#nullable disable
namespace Phantom.Controller.Database.Postgres.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20251228211902_AgentAuthSecretNullability")]
partial class AgentAuthSecretNullability
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Phantom.Controller.Database.Entities.AgentEntity", b =>
{
b.Property<Guid>("AgentGuid")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<byte[]>("AuthSecret")
.IsRequired()
.HasMaxLength(12)
.HasColumnType("bytea");
b.Property<string>("BuildVersion")
.HasColumnType("text");
b.Property<int?>("MaxInstances")
.HasColumnType("integer");
b.Property<int?>("MaxMemory")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int?>("ProtocolVersion")
.HasColumnType("integer");
b.HasKey("AgentGuid");
b.ToTable("Agents", "agents");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.AuditLogEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<JsonDocument>("Data")
.HasColumnType("jsonb");
b.Property<string>("EventType")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SubjectId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SubjectType")
.IsRequired()
.HasColumnType("text");
b.Property<Guid?>("UserGuid")
.HasColumnType("uuid");
b.Property<DateTime>("UtcTime")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("UserGuid");
b.ToTable("AuditLog", "system");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.EventLogEntity", b =>
{
b.Property<Guid>("EventGuid")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid?>("AgentGuid")
.HasColumnType("uuid");
b.Property<JsonDocument>("Data")
.HasColumnType("jsonb");
b.Property<string>("EventType")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SubjectId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SubjectType")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("UtcTime")
.HasColumnType("timestamp with time zone");
b.HasKey("EventGuid");
b.ToTable("EventLog", "system");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.InstanceEntity", b =>
{
b.Property<Guid>("InstanceGuid")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("AgentGuid")
.HasColumnType("uuid");
b.Property<string>("InstanceName")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("JavaRuntimeGuid")
.HasColumnType("uuid");
b.Property<string>("JvmArguments")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("LaunchAutomatically")
.HasColumnType("boolean");
b.Property<int>("MemoryAllocation")
.HasColumnType("integer");
b.Property<string>("MinecraftServerKind")
.IsRequired()
.HasColumnType("text");
b.Property<string>("MinecraftVersion")
.IsRequired()
.HasColumnType("text");
b.Property<int>("RconPort")
.HasColumnType("integer");
b.Property<int>("ServerPort")
.HasColumnType("integer");
b.HasKey("InstanceGuid");
b.ToTable("Instances", "agents");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.PermissionEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Permissions", "identity");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.RoleEntity", b =>
{
b.Property<Guid>("RoleGuid")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("RoleGuid");
b.ToTable("Roles", "identity");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.RolePermissionEntity", b =>
{
b.Property<Guid>("RoleGuid")
.HasColumnType("uuid");
b.Property<string>("PermissionId")
.HasColumnType("text");
b.HasKey("RoleGuid", "PermissionId");
b.HasIndex("PermissionId");
b.ToTable("RolePermissions", "identity");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserAgentAccessEntity", b =>
{
b.Property<Guid>("UserGuid")
.HasColumnType("uuid");
b.Property<Guid>("AgentGuid")
.HasColumnType("uuid");
b.HasKey("UserGuid", "AgentGuid");
b.HasIndex("AgentGuid");
b.ToTable("UserAgentAccess", "identity");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserEntity", b =>
{
b.Property<Guid>("UserGuid")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("text");
b.HasKey("UserGuid");
b.HasIndex("Name")
.IsUnique();
b.ToTable("Users", "identity");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserPermissionEntity", b =>
{
b.Property<Guid>("UserGuid")
.HasColumnType("uuid");
b.Property<string>("PermissionId")
.HasColumnType("text");
b.HasKey("UserGuid", "PermissionId");
b.HasIndex("PermissionId");
b.ToTable("UserPermissions", "identity");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserRoleEntity", b =>
{
b.Property<Guid>("UserGuid")
.HasColumnType("uuid");
b.Property<Guid>("RoleGuid")
.HasColumnType("uuid");
b.HasKey("UserGuid", "RoleGuid");
b.HasIndex("RoleGuid");
b.ToTable("UserRoles", "identity");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.AuditLogEntity", b =>
{
b.HasOne("Phantom.Controller.Database.Entities.UserEntity", "User")
.WithMany()
.HasForeignKey("UserGuid")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("User");
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.RolePermissionEntity", b =>
{
b.HasOne("Phantom.Controller.Database.Entities.PermissionEntity", null)
.WithMany()
.HasForeignKey("PermissionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Phantom.Controller.Database.Entities.RoleEntity", null)
.WithMany()
.HasForeignKey("RoleGuid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserAgentAccessEntity", b =>
{
b.HasOne("Phantom.Controller.Database.Entities.AgentEntity", null)
.WithMany()
.HasForeignKey("AgentGuid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Phantom.Controller.Database.Entities.UserEntity", null)
.WithMany()
.HasForeignKey("UserGuid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserPermissionEntity", b =>
{
b.HasOne("Phantom.Controller.Database.Entities.PermissionEntity", null)
.WithMany()
.HasForeignKey("PermissionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Phantom.Controller.Database.Entities.UserEntity", null)
.WithMany()
.HasForeignKey("UserGuid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserRoleEntity", b =>
{
b.HasOne("Phantom.Controller.Database.Entities.RoleEntity", "Role")
.WithMany()
.HasForeignKey("RoleGuid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Phantom.Controller.Database.Entities.UserEntity", "User")
.WithMany()
.HasForeignKey("UserGuid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
b.Navigation("User");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,42 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Phantom.Controller.Database.Postgres.Migrations
{
/// <inheritdoc />
public partial class AgentAuthSecretNullability : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<byte[]>(
name: "AuthSecret",
schema: "agents",
table: "Agents",
type: "bytea",
maxLength: 12,
nullable: false,
defaultValue: new byte[0],
oldClrType: typeof(byte[]),
oldType: "bytea",
oldMaxLength: 12,
oldNullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<byte[]>(
name: "AuthSecret",
schema: "agents",
table: "Agents",
type: "bytea",
maxLength: 12,
nullable: true,
oldClrType: typeof(byte[]),
oldType: "bytea",
oldMaxLength: 12);
}
}
}

View File

@@ -30,24 +30,24 @@ namespace Phantom.Controller.Database.Postgres.Migrations
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<byte[]>("AuthSecret") b.Property<byte[]>("AuthSecret")
.IsRequired()
.HasMaxLength(12) .HasMaxLength(12)
.HasColumnType("bytea"); .HasColumnType("bytea");
b.Property<string>("BuildVersion") b.Property<string>("BuildVersion")
.IsRequired()
.HasColumnType("text"); .HasColumnType("text");
b.Property<int?>("MaxInstances") b.Property<int>("MaxInstances")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<int?>("MaxMemory") b.Property<int>("MaxMemory")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
b.Property<int?>("ProtocolVersion") b.Property<int>("ProtocolVersion")
.HasColumnType("integer"); .HasColumnType("integer");
b.HasKey("AgentGuid"); b.HasKey("AgentGuid");

View File

@@ -2,7 +2,6 @@
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Phantom.Common.Data; using Phantom.Common.Data;
using Phantom.Common.Data.Web.Agent;
using Phantom.Utils.Rpc; using Phantom.Utils.Rpc;
namespace Phantom.Controller.Database.Entities; namespace Phantom.Controller.Database.Entities;
@@ -14,22 +13,17 @@ public sealed class AgentEntity {
public Guid AgentGuid { get; init; } public Guid AgentGuid { get; init; }
public string Name { get; set; } public string Name { get; set; }
public ushort? ProtocolVersion { get; set; } public ushort ProtocolVersion { get; set; }
public string? BuildVersion { get; set; } public string BuildVersion { get; set; }
public ushort? MaxInstances { get; set; } public ushort MaxInstances { get; set; }
public RamAllocationUnits? MaxMemory { get; set; } public RamAllocationUnits MaxMemory { get; set; }
[MaxLength(AuthSecret.Length)] [MaxLength(AuthSecret.Length)]
public AuthSecret AuthSecret { get; set; } public AuthSecret? AuthSecret { get; set; }
public AgentConfiguration Configuration => new (Name);
public AgentVersionInfo? VersionInfo => ProtocolVersion is {} protocolVersion && BuildVersion is {} buildVersion ? new AgentVersionInfo(protocolVersion, buildVersion) : null;
public AgentRuntimeInfo RuntimeInfo => new (VersionInfo, MaxInstances, MaxMemory);
internal AgentEntity(Guid agentGuid) { internal AgentEntity(Guid agentGuid) {
AgentGuid = agentGuid; AgentGuid = agentGuid;
Name = null!; Name = null!;
BuildVersion = null!; BuildVersion = null!;
AuthSecret = null!;
} }
} }

View File

@@ -14,21 +14,15 @@ public abstract class AbstractUpsertHelper<T> where T : class {
private protected abstract T Construct(Guid guid); private protected abstract T Construct(Guid guid);
public T Fetch(Guid guid) { public T Fetch(Guid guid) {
return Fetch(guid, out _);
}
public T Fetch(Guid guid, out bool wasCreated) {
DbSet<T> set = Set; DbSet<T> set = Set;
T? entity = set.Find(guid); T? entity = set.Find(guid);
if (entity == null) { if (entity == null) {
entity = Construct(guid); entity = Construct(guid);
set.Add(entity); set.Add(entity);
wasCreated = true;
} }
else { else {
set.Update(entity); set.Update(entity);
wasCreated = false;
} }
return entity; return entity;

View File

@@ -11,6 +11,7 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="System.Linq.Async" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -61,14 +61,6 @@ sealed partial class AuditLogRepository {
}); });
} }
public void AgentCreated(Guid agentGuid) {
AddItem(AuditLogEventType.AgentCreated, agentGuid.ToString());
}
public void AgentEdited(Guid agentGuid) {
AddItem(AuditLogEventType.AgentEdited, agentGuid.ToString());
}
public void InstanceCreated(Guid instanceGuid) { public void InstanceCreated(Guid instanceGuid) {
AddItem(AuditLogEventType.InstanceCreated, instanceGuid.ToString()); AddItem(AuditLogEventType.InstanceCreated, instanceGuid.ToString());
} }

View File

@@ -150,12 +150,12 @@ sealed class MinecraftVersionApi : IDisposable {
private static JsonElement GetJsonPropertyOrThrow(JsonElement parentElement, string propertyKey, JsonValueKind expectedKind, string location) { private static JsonElement GetJsonPropertyOrThrow(JsonElement parentElement, string propertyKey, JsonValueKind expectedKind, string location) {
if (!parentElement.TryGetProperty(propertyKey, out var valueElement)) { if (!parentElement.TryGetProperty(propertyKey, out var valueElement)) {
Logger.Error("Missing \"{Property}\" key in {Location}.", propertyKey, location); Logger.Error("Missing \"{Property}\" key in " + location + ".", propertyKey);
throw StopProcedureException.Instance; throw StopProcedureException.Instance;
} }
if (valueElement.ValueKind != expectedKind) { if (valueElement.ValueKind != expectedKind) {
Logger.Error("The \"{Property}\" key in {Location} does not contain a JSON {ExpectedType}. Actual type: {ActualType}", propertyKey, location, expectedKind, valueElement.ValueKind); Logger.Error("The \"{Property}\" key in " + location + " does not contain a JSON {ExpectedType}. Actual type: {ActualType}", propertyKey, expectedKind, valueElement.ValueKind);
throw StopProcedureException.Instance; throw StopProcedureException.Instance;
} }

View File

@@ -34,11 +34,10 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand>, IWithTimers {
private static readonly TimeSpan DisconnectionThreshold = TimeSpan.FromSeconds(12); private static readonly TimeSpan DisconnectionThreshold = TimeSpan.FromSeconds(12);
public readonly record struct Init( public readonly record struct Init(
Guid? LoggedInUserGuid,
Guid AgentGuid, Guid AgentGuid,
AgentConfiguration AgentConfiguration, string AgentName,
AuthSecret AuthSecret, AuthSecret AuthSecret,
AgentRuntimeInfo AgentRuntimeInfo, AgentConfiguration AgentConfiguration,
AgentConnectionKeys AgentConnectionKeys, AgentConnectionKeys AgentConnectionKeys,
ControllerState ControllerState, ControllerState ControllerState,
MinecraftVersions MinecraftVersions, MinecraftVersions MinecraftVersions,
@@ -59,15 +58,13 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand>, IWithTimers {
private readonly CancellationToken cancellationToken; private readonly CancellationToken cancellationToken;
private readonly Guid agentGuid; private readonly Guid agentGuid;
private readonly string agentName;
private readonly AuthInfo authInfo; private readonly AuthInfo authInfo;
private AgentConfiguration configuration; private AgentConfiguration configuration;
private AgentRuntimeInfo runtimeInfo;
private AgentStats? stats; private AgentStats? stats;
private ImmutableArray<TaggedJavaRuntime> javaRuntimes = ImmutableArray<TaggedJavaRuntime>.Empty; private ImmutableArray<TaggedJavaRuntime> javaRuntimes = ImmutableArray<TaggedJavaRuntime>.Empty;
private string AgentName => configuration.AgentName;
private readonly AgentConnection connection; private readonly AgentConnection connection;
private DateTimeOffset? lastPingTime; private DateTimeOffset? lastPingTime;
@@ -100,22 +97,17 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand>, IWithTimers {
this.cancellationToken = init.CancellationToken; this.cancellationToken = init.CancellationToken;
this.agentGuid = init.AgentGuid; this.agentGuid = init.AgentGuid;
this.agentName = init.AgentName;
this.authInfo = new AuthInfo(this, init.AuthSecret); this.authInfo = new AuthInfo(this, init.AuthSecret);
this.configuration = init.AgentConfiguration; this.configuration = init.AgentConfiguration;
this.runtimeInfo = init.AgentRuntimeInfo; this.connection = new AgentConnection(agentGuid, agentName);
this.connection = new AgentConnection(agentGuid, configuration.AgentName);
this.databaseStorageActor = Context.ActorOf(AgentDatabaseStorageActor.Factory(new AgentDatabaseStorageActor.Init(agentGuid, init.DbProvider, init.CancellationToken)), "DatabaseStorage"); this.databaseStorageActor = Context.ActorOf(AgentDatabaseStorageActor.Factory(new AgentDatabaseStorageActor.Init(agentGuid, init.DbProvider, init.CancellationToken)), "DatabaseStorage");
if (init.LoggedInUserGuid is {} loggedInUserGuid) {
databaseStorageActor.Tell(new AgentDatabaseStorageActor.StoreAgentConfigurationCommand(loggedInUserGuid, configuration));
}
NotifyAgentUpdated(); NotifyAgentUpdated();
ReceiveAsync<InitializeCommand>(Initialize); ReceiveAsync<InitializeCommand>(Initialize);
Receive<ConfigureAgentCommand>(ConfigureAgent);
ReceiveAsyncAndReply<RegisterCommand, ImmutableArray<ConfigureInstanceMessage>>(Register); ReceiveAsyncAndReply<RegisterCommand, ImmutableArray<ConfigureInstanceMessage>>(Register);
Receive<SetConnectionCommand>(SetConnection); Receive<SetConnectionCommand>(SetConnection);
Receive<UnregisterCommand>(Unregister); Receive<UnregisterCommand>(Unregister);
@@ -133,7 +125,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand>, IWithTimers {
} }
private void NotifyAgentUpdated() { private void NotifyAgentUpdated() {
controllerState.UpdateAgent(new Agent(agentGuid, configuration, authInfo.ConnectionKey, runtimeInfo, stats, ConnectionStatus)); controllerState.UpdateAgent(new Agent(agentGuid, agentName, authInfo.ConnectionKey, configuration, stats, ConnectionStatus));
} }
protected override void PreStart() { protected override void PreStart() {
@@ -201,9 +193,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand>, IWithTimers {
private sealed record InitializeCommand : ICommand; private sealed record InitializeCommand : ICommand;
public sealed record ConfigureAgentCommand(Guid LoggedInUserGuid, AgentConfiguration Configuration) : ICommand; public sealed record RegisterCommand(AgentConfiguration Configuration, ImmutableArray<TaggedJavaRuntime> JavaRuntimes) : ICommand, ICanReply<ImmutableArray<ConfigureInstanceMessage>>;
public sealed record RegisterCommand(AgentRuntimeInfo RuntimeInfo, ImmutableArray<TaggedJavaRuntime> JavaRuntimes) : ICommand, ICanReply<ImmutableArray<ConfigureInstanceMessage>>;
public sealed record SetConnectionCommand(RpcServerToClientConnection<IMessageToController, IMessageToAgent> Connection) : ICommand; public sealed record SetConnectionCommand(RpcServerToClientConnection<IMessageToController, IMessageToAgent> Connection) : ICommand;
@@ -258,24 +248,19 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand>, IWithTimers {
} }
} }
private void ConfigureAgent(ConfigureAgentCommand message) {
configuration = message.Configuration;
NotifyAgentUpdated();
databaseStorageActor.Tell(new AgentDatabaseStorageActor.StoreAgentConfigurationCommand(message.LoggedInUserGuid, configuration));
}
private async Task<ImmutableArray<ConfigureInstanceMessage>> Register(RegisterCommand command) { private async Task<ImmutableArray<ConfigureInstanceMessage>> Register(RegisterCommand command) {
var configurationMessages = await PrepareInitialConfigurationMessages(); var configurationMessages = await PrepareInitialConfigurationMessages();
runtimeInfo = command.RuntimeInfo; configuration = command.Configuration;
connection.SetAgentName(agentName);
lastPingTime = DateTimeOffset.Now; lastPingTime = DateTimeOffset.Now;
isOnline = true; isOnline = true;
NotifyAgentUpdated(); NotifyAgentUpdated();
Logger.Information("Registered agent \"{AgentName}\" (GUID {AgentGuid}).", AgentName, agentGuid); Logger.Information("Registered agent \"{Name}\" (GUID {Guid}).", agentName, agentGuid);
databaseStorageActor.Tell(new AgentDatabaseStorageActor.StoreAgentRuntimeInfoCommand(runtimeInfo)); databaseStorageActor.Tell(new AgentDatabaseStorageActor.StoreAgentConfigurationCommand(agentName, authInfo.Secret, configuration));
javaRuntimes = command.JavaRuntimes; javaRuntimes = command.JavaRuntimes;
controllerState.UpdateAgentJavaRuntimes(agentGuid, javaRuntimes); controllerState.UpdateAgentJavaRuntimes(agentGuid, javaRuntimes);
@@ -297,7 +282,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand>, IWithTimers {
TellAllInstances(new InstanceActor.SetStatusCommand(InstanceStatus.Offline)); TellAllInstances(new InstanceActor.SetStatusCommand(InstanceStatus.Offline));
Logger.Information("Unregistered agent \"{AgentName}\" (GUID {AgentGuid}).", AgentName, agentGuid); Logger.Information("Unregistered agent \"{Name}\" (GUID {Guid}).", agentName, agentGuid);
} }
private AuthSecret GetAuthSecret(GetAuthSecretCommand command) { private AuthSecret GetAuthSecret(GetAuthSecretCommand command) {
@@ -309,7 +294,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand>, IWithTimers {
isOnline = false; isOnline = false;
NotifyAgentUpdated(); NotifyAgentUpdated();
Logger.Warning("Lost connection to agent \"{AgentName}\" (GUID {AgentGuid}).", AgentName, agentGuid); Logger.Warning("Lost connection to agent \"{Name}\" (GUID {Guid}).", agentName, agentGuid);
} }
} }
@@ -320,7 +305,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand>, IWithTimers {
isOnline = true; isOnline = true;
NotifyAgentUpdated(); NotifyAgentUpdated();
Logger.Warning("Restored connection to agent \"{AgentName}\" (GUID {AgentGuid}).", AgentName, agentGuid); Logger.Warning("Restored connection to agent \"{Name}\" (GUID {Guid}).", agentName, agentGuid);
} }
} }
@@ -369,15 +354,19 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand>, IWithTimers {
var isCreating = command.IsCreatingInstance; var isCreating = command.IsCreatingInstance;
if (result.Is(ConfigureInstanceResult.Success)) { if (result.Is(ConfigureInstanceResult.Success)) {
string action = isCreating ? "Created" : "Edited"; string action = isCreating ? "Added" : "Edited";
Logger.Information(action + " instance \"{InstanceName}\" (GUID {InstanceGuid}) in agent \"{AgentName}\".", instanceName, instanceGuid, AgentName); string relation = isCreating ? "to agent" : "in agent";
Logger.Information(action + " instance \"{InstanceName}\" (GUID {InstanceGuid}) " + relation + " \"{AgentName}\".", instanceName, instanceGuid, agentName);
return CreateOrUpdateInstanceResult.Success; return CreateOrUpdateInstanceResult.Success;
} }
else { else {
string action = isCreating ? "creating" : "editing"; string action = isCreating ? "adding" : "editing";
string relation = isCreating ? "to agent" : "in agent";
string reason = result.Into(ConfigureInstanceResultExtensions.ToSentence, InstanceActionFailureExtensions.ToSentence); string reason = result.Into(ConfigureInstanceResultExtensions.ToSentence, InstanceActionFailureExtensions.ToSentence);
Logger.Information("Failed " + action + " instance \"{InstanceName}\" (GUID {InstanceGuid}) in agent \"{AgentName}\". {ErrorMessage}", instanceName, instanceGuid, AgentName, reason);
Logger.Information("Failed " + action + " instance \"{InstanceName}\" (GUID {InstanceGuid}) " + relation + " \"{AgentName}\". {ErrorMessage}", instanceName, instanceGuid, agentName, reason);
return CreateOrUpdateInstanceResult.UnknownError; return CreateOrUpdateInstanceResult.UnknownError;
} }

View File

@@ -1,8 +1,6 @@
using Akka.Actor; using Akka.Actor;
using Phantom.Common.Data.Web.Agent; using Phantom.Common.Data.Web.Agent;
using Phantom.Controller.Database; using Phantom.Controller.Database;
using Phantom.Controller.Database.Entities;
using Phantom.Controller.Database.Repositories;
using Phantom.Utils.Actor; using Phantom.Utils.Actor;
using Phantom.Utils.Logging; using Phantom.Utils.Logging;
using Phantom.Utils.Rpc; using Phantom.Utils.Rpc;
@@ -25,105 +23,63 @@ sealed class AgentDatabaseStorageActor : ReceiveActor<AgentDatabaseStorageActor.
private readonly IDbContextProvider dbProvider; private readonly IDbContextProvider dbProvider;
private readonly CancellationToken cancellationToken; private readonly CancellationToken cancellationToken;
private StoreAgentRuntimeInfoCommand? storeRuntimeInfoCommand; private StoreAgentConfigurationCommand? storeCommand;
private bool hasScheduledFlush;
private AgentDatabaseStorageActor(Init init) { private AgentDatabaseStorageActor(Init init) {
this.agentGuid = init.AgentGuid; this.agentGuid = init.AgentGuid;
this.dbProvider = init.DbProvider; this.dbProvider = init.DbProvider;
this.cancellationToken = init.CancellationToken; this.cancellationToken = init.CancellationToken;
Receive<StoreAgentRuntimeInfoCommand>(StoreAgentRuntimeInfo); Receive<StoreAgentConfigurationCommand>(StoreAgentConfiguration);
ReceiveAsync<StoreAgentConfigurationCommand>(StoreAgentConfiguration); ReceiveAsync<FlushChangesCommand>(FlushChanges);
ReceiveAsync<FlushAgentRuntimeInfoCommand>(FlushAgentRuntimeInfo);
}
private ValueTask<AgentEntity?> FindAgentEntity(ILazyDbContext db) {
return db.Ctx.Agents.FindAsync([agentGuid], cancellationToken);
} }
public interface ICommand; public interface ICommand;
public sealed record StoreAgentConfigurationCommand(Guid AuditLogUserGuid, AgentConfiguration Configuration) : ICommand; public sealed record StoreAgentConfigurationCommand(string Name, AuthSecret AuthSecret, AgentConfiguration Configuration) : ICommand;
public sealed record StoreAgentRuntimeInfoCommand(AgentRuntimeInfo RuntimeInfo) : ICommand; private sealed record FlushChangesCommand : ICommand;
private sealed record FlushAgentRuntimeInfoCommand : ICommand; private void StoreAgentConfiguration(StoreAgentConfigurationCommand command) {
storeCommand = command;
private async Task StoreAgentConfiguration(StoreAgentConfigurationCommand command) {
await FlushAgentRuntimeInfo();
bool wasCreated;
await using (var db = dbProvider.Lazy()) {
var entity = db.Ctx.AgentUpsert.Fetch(agentGuid, out wasCreated);
if (wasCreated) {
entity.AuthSecret = AuthSecret.Generate();
}
entity.Name = command.Configuration.AgentName;
var auditLogWriter = new AuditLogRepository(db).Writer(command.AuditLogUserGuid);
if (wasCreated) {
auditLogWriter.AgentCreated(agentGuid);
}
else {
auditLogWriter.AgentEdited(agentGuid);
}
await db.Ctx.SaveChangesAsync(cancellationToken);
}
string action = wasCreated ? "Created" : "Edited";
Logger.Information(action + " agent \"{AgentName}\" (GUID {AgentGuid}) in database.", command.Configuration.AgentName, agentGuid);
}
private void StoreAgentRuntimeInfo(StoreAgentRuntimeInfoCommand command) {
storeRuntimeInfoCommand = command;
ScheduleFlush(TimeSpan.FromSeconds(2)); ScheduleFlush(TimeSpan.FromSeconds(2));
} }
private void ScheduleFlush(TimeSpan delay) { private async Task FlushChanges(FlushChangesCommand command) {
if (storeRuntimeInfoCommand != null) { hasScheduledFlush = false;
Timers.StartSingleTimer("FlushChanges", new FlushAgentRuntimeInfoCommand(), delay, Self);
}
}
private Task FlushAgentRuntimeInfo(FlushAgentRuntimeInfoCommand command) { if (storeCommand == null) {
return FlushAgentRuntimeInfo();
}
private async Task FlushAgentRuntimeInfo() {
if (storeRuntimeInfoCommand == null) {
return; return;
} }
string agentName;
await using (var db = dbProvider.Lazy()) {
var entity = await FindAgentEntity(db);
if (entity == null) {
return;
}
agentName = entity.Name;
try { try {
entity.ProtocolVersion = storeRuntimeInfoCommand.RuntimeInfo.VersionInfo?.ProtocolVersion; await using var ctx = dbProvider.Eager();
entity.BuildVersion = storeRuntimeInfoCommand.RuntimeInfo.VersionInfo?.BuildVersion; var entity = ctx.AgentUpsert.Fetch(agentGuid);
entity.MaxInstances = storeRuntimeInfoCommand.RuntimeInfo.MaxInstances;
entity.MaxMemory = storeRuntimeInfoCommand.RuntimeInfo.MaxMemory;
await db.Ctx.SaveChangesAsync(cancellationToken); entity.Name = storeCommand.Name;
entity.AuthSecret = storeCommand.AuthSecret;
entity.ProtocolVersion = storeCommand.Configuration.ProtocolVersion;
entity.BuildVersion = storeCommand.Configuration.BuildVersion;
entity.MaxInstances = storeCommand.Configuration.MaxInstances;
entity.MaxMemory = storeCommand.Configuration.MaxMemory;
await ctx.SaveChangesAsync(cancellationToken);
} catch (Exception e) { } catch (Exception e) {
ScheduleFlush(TimeSpan.FromSeconds(10)); ScheduleFlush(TimeSpan.FromSeconds(10));
Logger.Error(e, "Could not update agent \"{AgentName}\" (GUID {AgentGuid}) in database.", entity.Name, agentGuid); Logger.Error(e, "Could not store agent \"{AgentName}\" (GUID {AgentGuid}) in database.", storeCommand.Name, agentGuid);
return; return;
} }
Logger.Information("Stored agent \"{AgentName}\" (GUID {AgentGuid}) in database.", storeCommand.Name, agentGuid);
storeCommand = null;
} }
Logger.Information("Updated agent \"{AgentName}\" (GUID {AgentGuid}) in database.", agentName, agentGuid); private void ScheduleFlush(TimeSpan delay) {
if (!hasScheduledFlush) {
storeRuntimeInfoCommand = null; hasScheduledFlush = true;
Timers.StartSingleTimer("FlushChanges", new FlushChangesCommand(), delay, Self);
}
} }
} }

View File

@@ -1,6 +1,7 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Immutable; using System.Collections.Immutable;
using Akka.Actor; using Akka.Actor;
using Microsoft.EntityFrameworkCore;
using Phantom.Common.Data; using Phantom.Common.Data;
using Phantom.Common.Data.Replies; using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.Agent; using Phantom.Common.Data.Web.Agent;
@@ -8,6 +9,7 @@ using Phantom.Common.Data.Web.Users;
using Phantom.Common.Messages.Agent.Handshake; using Phantom.Common.Messages.Agent.Handshake;
using Phantom.Common.Messages.Agent.ToAgent; using Phantom.Common.Messages.Agent.ToAgent;
using Phantom.Controller.Database; using Phantom.Controller.Database;
using Phantom.Controller.Database.Entities;
using Phantom.Controller.Minecraft; using Phantom.Controller.Minecraft;
using Phantom.Controller.Services.Users.Sessions; using Phantom.Controller.Services.Users.Sessions;
using Phantom.Utils.Actor; using Phantom.Utils.Actor;
@@ -22,6 +24,7 @@ sealed class AgentManager(
AgentConnectionKeys agentConnectionKeys, AgentConnectionKeys agentConnectionKeys,
ControllerState controllerState, ControllerState controllerState,
MinecraftVersions minecraftVersions, MinecraftVersions minecraftVersions,
UserLoginManager userLoginManager,
IDbContextProvider dbProvider, IDbContextProvider dbProvider,
CancellationToken cancellationToken CancellationToken cancellationToken
) { ) {
@@ -29,31 +32,41 @@ sealed class AgentManager(
private readonly ConcurrentDictionary<Guid, ActorRef<AgentActor.ICommand>> agentsByAgentGuid = new (); private readonly ConcurrentDictionary<Guid, ActorRef<AgentActor.ICommand>> agentsByAgentGuid = new ();
private ActorRef<AgentActor.ICommand> CreateAgentActor(Guid agentGuid, string agentName, AuthSecret authSecret, AgentConfiguration agentConfiguration) {
var init = new AgentActor.Init(agentGuid, agentName, authSecret, agentConfiguration, agentConnectionKeys, controllerState, minecraftVersions, dbProvider, cancellationToken);
var name = "Agent:" + agentGuid;
return actorSystem.ActorOf(AgentActor.Factory(init), name);
}
public async Task Initialize() { public async Task Initialize() {
await using var ctx = dbProvider.Eager(); await using var ctx = dbProvider.Eager();
List<AgentEntity> agentsWithoutSecrets = await ctx.Agents.Where(static entity => entity.AuthSecret == null).ToListAsync(cancellationToken);
if (agentsWithoutSecrets.Count > 0) {
foreach (var entity in agentsWithoutSecrets) {
entity.AuthSecret = AuthSecret.Generate();
}
await ctx.SaveChangesAsync(cancellationToken);
}
await foreach (var entity in ctx.Agents.AsAsyncEnumerable().WithCancellation(cancellationToken)) { await foreach (var entity in ctx.Agents.AsAsyncEnumerable().WithCancellation(cancellationToken)) {
var agentGuid = entity.AgentGuid; var agentGuid = entity.AgentGuid;
var agentConfiguration = new AgentConfiguration(entity.ProtocolVersion, entity.BuildVersion, entity.MaxInstances, entity.MaxMemory);
if (AddAgent(loggedInUserGuid: null, agentGuid, entity.Configuration, entity.AuthSecret, entity.RuntimeInfo)) { if (agentsByAgentGuid.TryAdd(agentGuid, CreateAgentActor(agentGuid, entity.Name, entity.AuthSecret!, agentConfiguration))) {
Logger.Information("Loaded agent \"{AgentName}\" (GUID {AgentGuid}) from database.", entity.Name, agentGuid); Logger.Information("Loaded agent \"{AgentName}\" (GUID {AgentGuid}) from database.", entity.Name, agentGuid);
} }
} }
} }
private bool AddAgent(Guid? loggedInUserGuid, Guid agentGuid, AgentConfiguration configuration, AuthSecret authSecret, AgentRuntimeInfo runtimeInfo) {
var init = new AgentActor.Init(loggedInUserGuid, agentGuid, configuration, authSecret, runtimeInfo, agentConnectionKeys, controllerState, minecraftVersions, dbProvider, cancellationToken);
var name = "Agent:" + agentGuid;
return agentsByAgentGuid.TryAdd(agentGuid, actorSystem.ActorOf(AgentActor.Factory(init), name));
}
public async Task<ImmutableArray<ConfigureInstanceMessage>?> RegisterAgent(Guid agentGuid, AgentRegistration registration) { public async Task<ImmutableArray<ConfigureInstanceMessage>?> RegisterAgent(Guid agentGuid, AgentRegistration registration) {
if (!agentsByAgentGuid.TryGetValue(agentGuid, out var agentActor)) { if (!agentsByAgentGuid.TryGetValue(agentGuid, out var agentActor)) {
return null; return null;
} }
var runtimeInfo = AgentRuntimeInfo.From(registration.AgentInfo); var agentConfiguration = AgentConfiguration.From(registration.AgentInfo);
return await agentActor.Request(new AgentActor.RegisterCommand(runtimeInfo, registration.JavaRuntimes), cancellationToken); return await agentActor.Request(new AgentActor.RegisterCommand(agentConfiguration, registration.JavaRuntimes), cancellationToken);
} }
public async Task<AuthSecret?> GetAgentAuthSecret(Guid agentGuid) { public async Task<AuthSecret?> GetAgentAuthSecret(Guid agentGuid) {
@@ -71,31 +84,13 @@ sealed class AgentManager(
return true; return true;
} }
else { else {
Logger.Warning("Could not deliver command {CommandType} to unknown agent {AgentGuid}.", command.GetType().Name, agentGuid); Logger.Warning("Could not deliver command {CommandType} to agent {AgentGuid}, agent not registered.", command.GetType().Name, agentGuid);
return false; return false;
} }
} }
public Result<CreateOrUpdateAgentResult, UserActionFailure> CreateOrUpdateAgent(LoggedInUser loggedInUser, Guid agentGuid, AgentConfiguration configuration) { public async Task<Result<TReply, UserInstanceActionFailure>> DoInstanceAction<TCommand, TReply>(Permission requiredPermission, ImmutableArray<byte> authToken, Guid agentGuid, Func<Guid, TCommand> commandFactoryFromLoggedInUserGuid) where TCommand : class, AgentActor.ICommand, ICanReply<Result<TReply, InstanceActionFailure>> {
if (!loggedInUser.CheckPermission(Permission.ManageAllAgents)) { var loggedInUser = userLoginManager.GetLoggedInUser(authToken);
return UserActionFailure.NotAuthorized;
}
if (configuration.AgentName.Length == 0) {
return CreateOrUpdateAgentResult.AgentNameMustNotBeEmpty;
}
if (agentsByAgentGuid.TryGetValue(agentGuid, out var agent)) {
agent.Tell(new AgentActor.ConfigureAgentCommand(loggedInUser.Guid!.Value, configuration));
}
else {
AddAgent(loggedInUser.Guid!.Value, agentGuid, configuration, AuthSecret.Generate(), new AgentRuntimeInfo());
}
return CreateOrUpdateAgentResult.Success;
}
public async Task<Result<TReply, UserInstanceActionFailure>> DoInstanceAction<TCommand, TReply>(LoggedInUser loggedInUser, Permission requiredPermission, Guid agentGuid, Func<Guid, TCommand> commandFactoryFromLoggedInUserGuid) where TCommand : class, AgentActor.ICommand, ICanReply<Result<TReply, InstanceActionFailure>> {
if (!loggedInUser.HasAccessToAgent(agentGuid) || !loggedInUser.CheckPermission(requiredPermission)) { if (!loggedInUser.HasAccessToAgent(agentGuid) || !loggedInUser.CheckPermission(requiredPermission)) {
return (UserInstanceActionFailure) UserActionFailure.NotAuthorized; return (UserInstanceActionFailure) UserActionFailure.NotAuthorized;
} }

View File

@@ -58,7 +58,7 @@ public sealed class ControllerServices : IDisposable {
this.UserLoginManager = new UserLoginManager(AuthenticatedUserCache, dbProvider); this.UserLoginManager = new UserLoginManager(AuthenticatedUserCache, dbProvider);
this.PermissionManager = new PermissionManager(dbProvider); this.PermissionManager = new PermissionManager(dbProvider);
this.AgentManager = new AgentManager(ActorSystem, new AgentConnectionKeys(agentCertificateThumbprint), ControllerState, MinecraftVersions, dbProvider, cancellationToken); this.AgentManager = new AgentManager(ActorSystem, new AgentConnectionKeys(agentCertificateThumbprint), ControllerState, MinecraftVersions, UserLoginManager, dbProvider, cancellationToken);
this.InstanceLogManager = new InstanceLogManager(); this.InstanceLogManager = new InstanceLogManager();
this.AuditLogManager = new AuditLogManager(dbProvider); this.AuditLogManager = new AuditLogManager(dbProvider);

View File

@@ -1,4 +1,5 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using Akka.Actor; using Akka.Actor;
using Phantom.Common.Messages.Agent; using Phantom.Common.Messages.Agent;
using Phantom.Controller.Services.Agents; using Phantom.Controller.Services.Agents;
@@ -10,29 +11,19 @@ using Phantom.Utils.Rpc.Runtime.Server;
namespace Phantom.Controller.Services.Rpc; namespace Phantom.Controller.Services.Rpc;
sealed class AgentClientRegistrar : IRpcServerClientRegistrar<IMessageToController, IMessageToAgent> { sealed class AgentClientRegistrar(
private readonly IActorRefFactory actorSystem; IActorRefFactory actorSystem,
private readonly AgentManager agentManager; AgentManager agentManager,
private readonly InstanceLogManager instanceLogManager; InstanceLogManager instanceLogManager,
private readonly EventLogManager eventLogManager; EventLogManager eventLogManager
) : IRpcServerClientRegistrar<IMessageToController, IMessageToAgent> {
private readonly Func<Guid, Guid, Receiver> receiverFactory;
private readonly ConcurrentDictionary<Guid, Receiver> receiversBySessionGuid = new (); private readonly ConcurrentDictionary<Guid, Receiver> receiversBySessionGuid = new ();
public AgentClientRegistrar(IActorRefFactory actorSystem, AgentManager agentManager, InstanceLogManager instanceLogManager, EventLogManager eventLogManager) { [SuppressMessage("ReSharper", "LambdaShouldNotCaptureContext")]
this.actorSystem = actorSystem; public IMessageReceiver<IMessageToController> Register(RpcServerToClientConnection<IMessageToController, IMessageToAgent> connection, Guid agentGuid) {
this.agentManager = agentManager;
this.instanceLogManager = instanceLogManager;
this.eventLogManager = eventLogManager;
this.receiverFactory = CreateReceiver;
}
public IMessageReceiver<IMessageToController> Register(RpcServerToClientConnection<IMessageToController, IMessageToAgent> connection) {
Guid agentGuid = connection.ClientGuid;
agentManager.TellAgent(agentGuid, new AgentActor.SetConnectionCommand(connection)); agentManager.TellAgent(agentGuid, new AgentActor.SetConnectionCommand(connection));
var receiver = receiversBySessionGuid.GetOrAdd(connection.SessionGuid, receiverFactory, agentGuid); var receiver = receiversBySessionGuid.GetOrAdd(connection.SessionGuid, CreateReceiver, agentGuid);
if (receiver.AgentGuid != agentGuid) { if (receiver.AgentGuid != agentGuid) {
throw new InvalidOperationException("Cannot register two agents to the same session!"); throw new InvalidOperationException("Cannot register two agents to the same session!");
} }

View File

@@ -25,30 +25,30 @@ sealed class AgentMessageHandlerActor : ReceiveActor<IMessageToController> {
this.instanceLogManager = init.InstanceLogManager; this.instanceLogManager = init.InstanceLogManager;
this.eventLogManager = init.EventLogManager; this.eventLogManager = init.EventLogManager;
Receive<ReportAgentStatusMessage>(ReportAgentStatus); Receive<ReportAgentStatusMessage>(HandleReportAgentStatus);
Receive<ReportInstanceStatusMessage>(ReportInstanceStatus); Receive<ReportInstanceStatusMessage>(HandleReportInstanceStatus);
Receive<ReportInstancePlayerCountsMessage>(ReportInstancePlayerCounts); Receive<ReportInstancePlayerCountsMessage>(HandleReportInstancePlayerCounts);
Receive<ReportInstanceEventMessage>(ReportInstanceEvent); Receive<ReportInstanceEventMessage>(HandleReportInstanceEvent);
Receive<InstanceOutputMessage>(InstanceOutput); Receive<InstanceOutputMessage>(HandleInstanceOutput);
} }
private void ReportAgentStatus(ReportAgentStatusMessage message) { private void HandleReportAgentStatus(ReportAgentStatusMessage message) {
agentManager.TellAgent(agentGuid, new AgentActor.UpdateStatsCommand(message.RunningInstanceCount, message.RunningInstanceMemory)); agentManager.TellAgent(agentGuid, new AgentActor.UpdateStatsCommand(message.RunningInstanceCount, message.RunningInstanceMemory));
} }
private void ReportInstanceStatus(ReportInstanceStatusMessage message) { private void HandleReportInstanceStatus(ReportInstanceStatusMessage message) {
agentManager.TellAgent(agentGuid, new AgentActor.UpdateInstanceStatusCommand(message.InstanceGuid, message.InstanceStatus)); agentManager.TellAgent(agentGuid, new AgentActor.UpdateInstanceStatusCommand(message.InstanceGuid, message.InstanceStatus));
} }
private void ReportInstancePlayerCounts(ReportInstancePlayerCountsMessage message) { private void HandleReportInstancePlayerCounts(ReportInstancePlayerCountsMessage message) {
agentManager.TellAgent(agentGuid, new AgentActor.UpdateInstancePlayerCountsCommand(message.InstanceGuid, message.PlayerCounts)); agentManager.TellAgent(agentGuid, new AgentActor.UpdateInstancePlayerCountsCommand(message.InstanceGuid, message.PlayerCounts));
} }
private void ReportInstanceEvent(ReportInstanceEventMessage message) { private void HandleReportInstanceEvent(ReportInstanceEventMessage message) {
message.Event.Accept(eventLogManager.CreateInstanceEventVisitor(message.EventGuid, message.UtcTime, agentGuid, message.InstanceGuid)); message.Event.Accept(eventLogManager.CreateInstanceEventVisitor(message.EventGuid, message.UtcTime, agentGuid, message.InstanceGuid));
} }
private void InstanceOutput(InstanceOutputMessage message) { private void HandleInstanceOutput(InstanceOutputMessage message) {
instanceLogManager.ReceiveLines(message.InstanceGuid, message.Lines); instanceLogManager.ReceiveLines(message.InstanceGuid, message.Lines);
} }
} }

View File

@@ -25,7 +25,7 @@ sealed class WebClientRegistrar(
MinecraftVersions minecraftVersions, MinecraftVersions minecraftVersions,
EventLogManager eventLogManager EventLogManager eventLogManager
) : IRpcServerClientRegistrar<IMessageToController, IMessageToWeb> { ) : IRpcServerClientRegistrar<IMessageToController, IMessageToWeb> {
public IMessageReceiver<IMessageToController> Register(RpcServerToClientConnection<IMessageToController, IMessageToWeb> connection) { public IMessageReceiver<IMessageToController> Register(RpcServerToClientConnection<IMessageToController, IMessageToWeb> connection, Guid clientGuid) {
var name = "WebClient-" + connection.SessionGuid; var name = "WebClient-" + connection.SessionGuid;
var init = new WebMessageHandlerActor.Init(connection, controllerState, instanceLogManager, userManager, roleManager, userRoleManager, userLoginManager, auditLogManager, agentManager, minecraftVersions, eventLogManager); var init = new WebMessageHandlerActor.Init(connection, controllerState, instanceLogManager, userManager, roleManager, userRoleManager, userLoginManager, auditLogManager, agentManager, minecraftVersions, eventLogManager);
return new IMessageReceiver<IMessageToController>.Actor(actorSystem.ActorOf(WebMessageHandlerActor.Factory(init), name)); return new IMessageReceiver<IMessageToController>.Actor(actorSystem.ActorOf(WebMessageHandlerActor.Factory(init), name));

View File

@@ -3,7 +3,6 @@ using Phantom.Common.Data;
using Phantom.Common.Data.Java; using Phantom.Common.Data.Java;
using Phantom.Common.Data.Minecraft; using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies; using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.Agent;
using Phantom.Common.Data.Web.AuditLog; using Phantom.Common.Data.Web.AuditLog;
using Phantom.Common.Data.Web.EventLog; using Phantom.Common.Data.Web.EventLog;
using Phantom.Common.Data.Web.Instance; using Phantom.Common.Data.Web.Instance;
@@ -64,32 +63,31 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
var senderActorInit = new WebMessageDataUpdateSenderActor.Init(init.Connection.MessageSender, controllerState, init.InstanceLogManager); var senderActorInit = new WebMessageDataUpdateSenderActor.Init(init.Connection.MessageSender, controllerState, init.InstanceLogManager);
Context.ActorOf(WebMessageDataUpdateSenderActor.Factory(senderActorInit), "DataUpdateSender"); Context.ActorOf(WebMessageDataUpdateSenderActor.Factory(senderActorInit), "DataUpdateSender");
ReceiveAndReplyLater<LogInMessage, Optional<LogInSuccess>>(LogIn); ReceiveAndReplyLater<LogInMessage, Optional<LogInSuccess>>(HandleLogIn);
Receive<LogOutMessage>(LogOut); Receive<LogOutMessage>(HandleLogOut);
ReceiveAndReply<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(GetAuthenticatedUser); ReceiveAndReply<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(GetAuthenticatedUser);
ReceiveAndReplyLater<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(CreateOrUpdateAdministratorUser); ReceiveAndReplyLater<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(HandleCreateOrUpdateAdministratorUser);
ReceiveAndReplyLater<CreateUserMessage, Result<CreateUserResult, UserActionFailure>>(CreateUser); ReceiveAndReplyLater<CreateUserMessage, Result<CreateUserResult, UserActionFailure>>(HandleCreateUser);
ReceiveAndReplyLater<GetUsersMessage, ImmutableArray<UserInfo>>(GetUsers); ReceiveAndReplyLater<GetUsersMessage, ImmutableArray<UserInfo>>(HandleGetUsers);
ReceiveAndReplyLater<GetRolesMessage, ImmutableArray<RoleInfo>>(GetRoles); ReceiveAndReplyLater<GetRolesMessage, ImmutableArray<RoleInfo>>(HandleGetRoles);
ReceiveAndReplyLater<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(GetUserRoles); ReceiveAndReplyLater<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(HandleGetUserRoles);
ReceiveAndReplyLater<ChangeUserRolesMessage, Result<ChangeUserRolesResult, UserActionFailure>>(ChangeUserRoles); ReceiveAndReplyLater<ChangeUserRolesMessage, Result<ChangeUserRolesResult, UserActionFailure>>(HandleChangeUserRoles);
ReceiveAndReplyLater<DeleteUserMessage, Result<DeleteUserResult, UserActionFailure>>(DeleteUser); ReceiveAndReplyLater<DeleteUserMessage, Result<DeleteUserResult, UserActionFailure>>(HandleDeleteUser);
ReceiveAndReply<CreateOrUpdateAgentMessage, Result<CreateOrUpdateAgentResult, UserActionFailure>>(CreateOrUpdateAgentMessage); ReceiveAndReplyLater<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(HandleCreateOrUpdateInstance);
ReceiveAndReply<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(GetAgentJavaRuntimes); ReceiveAndReplyLater<LaunchInstanceMessage, Result<LaunchInstanceResult, UserInstanceActionFailure>>(HandleLaunchInstance);
ReceiveAndReplyLater<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(CreateOrUpdateInstance); ReceiveAndReplyLater<StopInstanceMessage, Result<StopInstanceResult, UserInstanceActionFailure>>(HandleStopInstance);
ReceiveAndReplyLater<LaunchInstanceMessage, Result<LaunchInstanceResult, UserInstanceActionFailure>>(LaunchInstance); ReceiveAndReplyLater<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, UserInstanceActionFailure>>(HandleSendCommandToInstance);
ReceiveAndReplyLater<StopInstanceMessage, Result<StopInstanceResult, UserInstanceActionFailure>>(StopInstance); ReceiveAndReplyLater<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(HandleGetMinecraftVersions);
ReceiveAndReplyLater<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, UserInstanceActionFailure>>(SendCommandToInstance); ReceiveAndReply<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(HandleGetAgentJavaRuntimes);
ReceiveAndReplyLater<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(GetMinecraftVersions); ReceiveAndReplyLater<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>(HandleGetAuditLog);
ReceiveAndReplyLater<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>(GetAuditLog); ReceiveAndReplyLater<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>(HandleGetEventLog);
ReceiveAndReplyLater<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>(GetEventLog);
} }
private Task<Optional<LogInSuccess>> LogIn(LogInMessage message) { private Task<Optional<LogInSuccess>> HandleLogIn(LogInMessage message) {
return userLoginManager.LogIn(message.Username, message.Password); return userLoginManager.LogIn(message.Username, message.Password);
} }
private void LogOut(LogOutMessage message) { private void HandleLogOut(LogOutMessage message) {
_ = userLoginManager.LogOut(message.UserGuid, message.SessionToken); _ = userLoginManager.LogOut(message.UserGuid, message.SessionToken);
} }
@@ -97,87 +95,83 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
return userLoginManager.GetAuthenticatedUser(message.UserGuid, message.AuthToken); return userLoginManager.GetAuthenticatedUser(message.UserGuid, message.AuthToken);
} }
private Task<CreateOrUpdateAdministratorUserResult> CreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUserMessage message) { private Task<CreateOrUpdateAdministratorUserResult> HandleCreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUserMessage message) {
return userManager.CreateOrUpdateAdministrator(message.Username, message.Password); return userManager.CreateOrUpdateAdministrator(message.Username, message.Password);
} }
private Task<Result<CreateUserResult, UserActionFailure>> CreateUser(CreateUserMessage message) { private Task<Result<CreateUserResult, UserActionFailure>> HandleCreateUser(CreateUserMessage message) {
return userManager.Create(userLoginManager.GetLoggedInUser(message.AuthToken), message.Username, message.Password); return userManager.Create(userLoginManager.GetLoggedInUser(message.AuthToken), message.Username, message.Password);
} }
private Task<ImmutableArray<UserInfo>> GetUsers(GetUsersMessage message) { private Task<ImmutableArray<UserInfo>> HandleGetUsers(GetUsersMessage message) {
return userManager.GetAll(); return userManager.GetAll();
} }
private Task<ImmutableArray<RoleInfo>> GetRoles(GetRolesMessage message) { private Task<ImmutableArray<RoleInfo>> HandleGetRoles(GetRolesMessage message) {
return roleManager.GetAll(); return roleManager.GetAll();
} }
private Task<ImmutableDictionary<Guid, ImmutableArray<Guid>>> GetUserRoles(GetUserRolesMessage message) { private Task<ImmutableDictionary<Guid, ImmutableArray<Guid>>> HandleGetUserRoles(GetUserRolesMessage message) {
return userRoleManager.GetUserRoles(message.UserGuids); return userRoleManager.GetUserRoles(message.UserGuids);
} }
private Task<Result<ChangeUserRolesResult, UserActionFailure>> ChangeUserRoles(ChangeUserRolesMessage message) { private Task<Result<ChangeUserRolesResult, UserActionFailure>> HandleChangeUserRoles(ChangeUserRolesMessage message) {
return userRoleManager.ChangeUserRoles(userLoginManager.GetLoggedInUser(message.AuthToken), message.SubjectUserGuid, message.AddToRoleGuids, message.RemoveFromRoleGuids); return userRoleManager.ChangeUserRoles(userLoginManager.GetLoggedInUser(message.AuthToken), message.SubjectUserGuid, message.AddToRoleGuids, message.RemoveFromRoleGuids);
} }
private Task<Result<DeleteUserResult, UserActionFailure>> DeleteUser(DeleteUserMessage message) { private Task<Result<DeleteUserResult, UserActionFailure>> HandleDeleteUser(DeleteUserMessage message) {
return userManager.DeleteByGuid(userLoginManager.GetLoggedInUser(message.AuthToken), message.SubjectUserGuid); return userManager.DeleteByGuid(userLoginManager.GetLoggedInUser(message.AuthToken), message.SubjectUserGuid);
} }
private Result<CreateOrUpdateAgentResult, UserActionFailure> CreateOrUpdateAgentMessage(CreateOrUpdateAgentMessage message) { private Task<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message) {
return agentManager.CreateOrUpdateAgent(userLoginManager.GetLoggedInUser(message.AuthToken), message.AgentGuid, message.Configuration);
}
private ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>> GetAgentJavaRuntimes(GetAgentJavaRuntimesMessage message) {
return controllerState.AgentJavaRuntimesByGuid;
}
private Task<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>> CreateOrUpdateInstance(CreateOrUpdateInstanceMessage message) {
return agentManager.DoInstanceAction<AgentActor.CreateOrUpdateInstanceCommand, CreateOrUpdateInstanceResult>( return agentManager.DoInstanceAction<AgentActor.CreateOrUpdateInstanceCommand, CreateOrUpdateInstanceResult>(
userLoginManager.GetLoggedInUser(message.AuthToken),
Permission.CreateInstances, Permission.CreateInstances,
message.AuthToken,
message.Configuration.AgentGuid, message.Configuration.AgentGuid,
loggedInUserGuid => new AgentActor.CreateOrUpdateInstanceCommand(loggedInUserGuid, message.InstanceGuid, message.Configuration) loggedInUserGuid => new AgentActor.CreateOrUpdateInstanceCommand(loggedInUserGuid, message.InstanceGuid, message.Configuration)
); );
} }
private Task<Result<LaunchInstanceResult, UserInstanceActionFailure>> LaunchInstance(LaunchInstanceMessage message) { private Task<Result<LaunchInstanceResult, UserInstanceActionFailure>> HandleLaunchInstance(LaunchInstanceMessage message) {
return agentManager.DoInstanceAction<AgentActor.LaunchInstanceCommand, LaunchInstanceResult>( return agentManager.DoInstanceAction<AgentActor.LaunchInstanceCommand, LaunchInstanceResult>(
userLoginManager.GetLoggedInUser(message.AuthToken),
Permission.ControlInstances, Permission.ControlInstances,
message.AuthToken,
message.AgentGuid, message.AgentGuid,
loggedInUserGuid => new AgentActor.LaunchInstanceCommand(loggedInUserGuid, message.InstanceGuid) loggedInUserGuid => new AgentActor.LaunchInstanceCommand(loggedInUserGuid, message.InstanceGuid)
); );
} }
private Task<Result<StopInstanceResult, UserInstanceActionFailure>> StopInstance(StopInstanceMessage message) { private Task<Result<StopInstanceResult, UserInstanceActionFailure>> HandleStopInstance(StopInstanceMessage message) {
return agentManager.DoInstanceAction<AgentActor.StopInstanceCommand, StopInstanceResult>( return agentManager.DoInstanceAction<AgentActor.StopInstanceCommand, StopInstanceResult>(
userLoginManager.GetLoggedInUser(message.AuthToken),
Permission.ControlInstances, Permission.ControlInstances,
message.AuthToken,
message.AgentGuid, message.AgentGuid,
loggedInUserGuid => new AgentActor.StopInstanceCommand(loggedInUserGuid, message.InstanceGuid, message.StopStrategy) loggedInUserGuid => new AgentActor.StopInstanceCommand(loggedInUserGuid, message.InstanceGuid, message.StopStrategy)
); );
} }
private Task<Result<SendCommandToInstanceResult, UserInstanceActionFailure>> SendCommandToInstance(SendCommandToInstanceMessage message) { private Task<Result<SendCommandToInstanceResult, UserInstanceActionFailure>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) {
return agentManager.DoInstanceAction<AgentActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>( return agentManager.DoInstanceAction<AgentActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(
userLoginManager.GetLoggedInUser(message.AuthToken),
Permission.ControlInstances, Permission.ControlInstances,
message.AuthToken,
message.AgentGuid, message.AgentGuid,
loggedInUserGuid => new AgentActor.SendCommandToInstanceCommand(loggedInUserGuid, message.InstanceGuid, message.Command) loggedInUserGuid => new AgentActor.SendCommandToInstanceCommand(loggedInUserGuid, message.InstanceGuid, message.Command)
); );
} }
private Task<ImmutableArray<MinecraftVersion>> GetMinecraftVersions(GetMinecraftVersionsMessage message) { private Task<ImmutableArray<MinecraftVersion>> HandleGetMinecraftVersions(GetMinecraftVersionsMessage message) {
return minecraftVersions.GetVersions(CancellationToken.None); return minecraftVersions.GetVersions(CancellationToken.None);
} }
private Task<Result<ImmutableArray<AuditLogItem>, UserActionFailure>> GetAuditLog(GetAuditLogMessage message) { private ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>> HandleGetAgentJavaRuntimes(GetAgentJavaRuntimesMessage message) {
return controllerState.AgentJavaRuntimesByGuid;
}
private Task<Result<ImmutableArray<AuditLogItem>, UserActionFailure>> HandleGetAuditLog(GetAuditLogMessage message) {
return auditLogManager.GetMostRecentItems(userLoginManager.GetLoggedInUser(message.AuthToken), message.Count); return auditLogManager.GetMostRecentItems(userLoginManager.GetLoggedInUser(message.AuthToken), message.Count);
} }
private Task<Result<ImmutableArray<EventLogItem>, UserActionFailure>> GetEventLog(GetEventLogMessage message) { private Task<Result<ImmutableArray<EventLogItem>, UserActionFailure>> HandleGetEventLog(GetEventLogMessage message) {
return eventLogManager.GetMostRecentItems(userLoginManager.GetLoggedInUser(message.AuthToken), message.Count); return eventLogManager.GetMostRecentItems(userLoginManager.GetLoggedInUser(message.AuthToken), message.Count);
} }
} }

View File

@@ -1,5 +1,4 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using Phantom.Common.Data.Web.Users; using Phantom.Common.Data.Web.Users;
using Phantom.Controller.Database; using Phantom.Controller.Database;
using Phantom.Controller.Database.Entities; using Phantom.Controller.Database.Entities;
@@ -10,7 +9,7 @@ namespace Phantom.Controller.Services.Users.Sessions;
sealed class AuthenticatedUserCache { sealed class AuthenticatedUserCache {
private readonly ConcurrentDictionary<Guid, AuthenticatedUserInfo> authenticatedUsersByGuid = new (); private readonly ConcurrentDictionary<Guid, AuthenticatedUserInfo> authenticatedUsersByGuid = new ();
public bool TryGet(Guid userGuid, [NotNullWhen(true)] out AuthenticatedUserInfo? userInfo) { public bool TryGet(Guid userGuid, out AuthenticatedUserInfo? userInfo) {
return authenticatedUsersByGuid.TryGetValue(userGuid, out userInfo); return authenticatedUsersByGuid.TryGetValue(userGuid, out userInfo);
} }

View File

@@ -77,13 +77,8 @@ sealed class UserLoginManager {
return userGuid != null && authenticatedUserCache.TryGet(userGuid.Value, out var userInfo) ? new LoggedInUser(userInfo) : default; return userGuid != null && authenticatedUserCache.TryGet(userGuid.Value, out var userInfo) ? new LoggedInUser(userInfo) : default;
} }
public Optional<AuthenticatedUserInfo> GetAuthenticatedUser(Guid userGuid, ImmutableArray<byte> authToken) { public AuthenticatedUserInfo? GetAuthenticatedUser(Guid userGuid, ImmutableArray<byte> authToken) {
if (authenticatedUserCache.TryGet(userGuid, out var userInfo) && GetSessionBucket(authToken).Contains(userGuid, authToken)) { return authenticatedUserCache.TryGet(userGuid, out var userInfo) && GetSessionBucket(authToken).Contains(userGuid, authToken) ? userInfo : null;
return userInfo;
}
else {
return default;
}
} }
private sealed class UserSessionBucket { private sealed class UserSessionBucket {

View File

@@ -1,8 +1,8 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<LangVersion>14</LangVersion> <LangVersion>13</LangVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>

View File

@@ -1,7 +1,7 @@
# +---------------+ # +---------------+
# | Prepare build | # | Prepare build |
# +---------------+ # +---------------+
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/nightly/sdk:10.0 AS phantom-builder FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/nightly/sdk:9.0 AS phantom-builder
ARG TARGETARCH ARG TARGETARCH
ADD . /app ADD . /app
@@ -19,7 +19,7 @@ RUN find .artifacts/publish/*/* -maxdepth 0 -execdir mv '{}' 'release' \;
# +---------------------+ # +---------------------+
# | Phantom Agent image | # | Phantom Agent image |
# +---------------------+ # +---------------------+
FROM mcr.microsoft.com/dotnet/nightly/runtime:10.0 AS phantom-agent FROM mcr.microsoft.com/dotnet/nightly/runtime:9.0 AS phantom-agent
RUN mkdir /data && chmod 777 /data RUN mkdir /data && chmod 777 /data
WORKDIR /data WORKDIR /data
@@ -46,7 +46,7 @@ ENTRYPOINT ["dotnet", "/app/Phantom.Agent.dll"]
# +--------------------------+ # +--------------------------+
# | Phantom Controller image | # | Phantom Controller image |
# +--------------------------+ # +--------------------------+
FROM mcr.microsoft.com/dotnet/nightly/runtime:10.0 AS phantom-controller FROM mcr.microsoft.com/dotnet/nightly/runtime:9.0 AS phantom-controller
RUN mkdir /data && chmod 777 /data RUN mkdir /data && chmod 777 /data
WORKDIR /data WORKDIR /data

View File

@@ -1,14 +1,15 @@
<Project> <Project>
<ItemGroup> <ItemGroup>
<PackageReference Update="Microsoft.AspNetCore.Components.Authorization" Version="10.0.1" /> <PackageReference Update="Microsoft.AspNetCore.Components.Authorization" Version="9.0.9" />
<PackageReference Update="Microsoft.AspNetCore.Components.Web" Version="10.0.1" /> <PackageReference Update="Microsoft.AspNetCore.Components.Web" Version="9.0.9" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Update="Microsoft.EntityFrameworkCore.Tools" Version="10.0.1" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Relational" Version="9.0.9" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Relational" Version="10.0.1" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Tools" Version="9.0.9" />
<PackageReference Update="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" /> <PackageReference Update="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageReference Update="System.Linq.Async" Version="6.0.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -22,7 +23,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Update="Serilog" Version="4.3.0" /> <PackageReference Update="Serilog" Version="4.3.0" />
<PackageReference Update="Serilog.AspNetCore" Version="10.0.0" /> <PackageReference Update="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Update="Serilog.Sinks.Async" Version="2.1.0" /> <PackageReference Update="Serilog.Sinks.Async" Version="2.1.0" />
<PackageReference Update="Serilog.Sinks.Console" Version="6.0.0" /> <PackageReference Update="Serilog.Sinks.Console" Version="6.0.0" />
</ItemGroup> </ItemGroup>

View File

@@ -88,8 +88,8 @@ Use volumes to persist either the whole `/data` folder, or just `/data/data` if
* **Controller Communication** * **Controller Communication**
- `CONTROLLER_HOST` is the hostname of the Controller. - `CONTROLLER_HOST` is the hostname of the Controller.
- `CONTROLLER_PORT` is the Agent RPC port of the Controller. Default: `9401` - `CONTROLLER_PORT` is the Agent RPC port of the Controller. Default: `9401`
- `AGENT_KEY` is the [Agent Key](#secrets). Mutually exclusive with `AGENT_KEY_FILE`. - `AGENT_KEY` is the [Agent Key](#agent--web-keys). Mutually exclusive with `AGENT_KEY_FILE`.
- `AGENT_KEY_FILE` is a path to a file containing the [Agent Key](#secrets). Mutually exclusive with `AGENT_KEY`. - `AGENT_KEY_FILE` is a path to a file containing the [Agent Key](#agent--web-keys). Mutually exclusive with `AGENT_KEY`.
* **Agent Configuration** * **Agent Configuration**
- `MAX_INSTANCES` is the number of instances that can be created. - `MAX_INSTANCES` is the number of instances that can be created.
- `MAX_MEMORY` is the maximum amount of RAM that can be distributed among all instances. Use a positive integer with an optional suffix 'M' for MB, or 'G' for GB. Examples: `4096M`, `16G` - `MAX_MEMORY` is the maximum amount of RAM that can be distributed among all instances. Use a positive integer with an optional suffix 'M' for MB, or 'G' for GB. Examples: `4096M`, `16G`
@@ -110,8 +110,8 @@ Use volumes to persist the whole `/data` folder.
* **Controller Communication** * **Controller Communication**
- `CONTROLLER_HOST` is the hostname of the Controller. - `CONTROLLER_HOST` is the hostname of the Controller.
- `CONTROLLER_PORT` is the Web RPC port of the Controller. Default: `9402` - `CONTROLLER_PORT` is the Web RPC port of the Controller. Default: `9402`
- `WEB_KEY` is the [Web Key](#secrets). Mutually exclusive with `WEB_KEY_FILE`. - `WEB_KEY` is the [Web Key](#agent--web-keys). Mutually exclusive with `WEB_KEY_FILE`.
- `WEB_KEY_FILE` is a path to a file containing the [Web Key](#secrets). Mutually exclusive with `WEB_KEY`. - `WEB_KEY_FILE` is a path to a file containing the [Web Key](#agent--web-keys). Mutually exclusive with `WEB_KEY`.
* **Web Server** * **Web Server**
- `WEB_SERVER_HOST` is the host. Default: `0.0.0.0` - `WEB_SERVER_HOST` is the host. Default: `0.0.0.0`
- `WEB_SERVER_PORT` is the port. Default: `9400` - `WEB_SERVER_PORT` is the port. Default: `9400`

View File

@@ -142,16 +142,13 @@ public sealed class MessageSender<TMessageBase> {
messageReplyTracker.FailReply(frame.ReplyingToMessageId, MessageErrorException.From(frame.Error)); messageReplyTracker.FailReply(frame.ReplyingToMessageId, MessageErrorException.From(frame.Error));
} }
internal async Task Close(TimeSpan timeout) { internal async Task Close() {
messageQueue.Writer.TryComplete(); messageQueue.Writer.TryComplete();
try { try {
await messageQueueTask.WaitAsync(timeout); await messageQueueTask.WaitAsync(TimeSpan.FromSeconds(15));
} catch (TimeoutException) { } catch (TimeoutException) {
if (timeout != TimeSpan.Zero) {
logger.Warning("Could not finish processing message queue before timeout, forcibly shutting it down."); logger.Warning("Could not finish processing message queue before timeout, forcibly shutting it down.");
}
await shutdownCancellationTokenSource.CancelAsync(); await shutdownCancellationTokenSource.CancelAsync();
await messageQueueTask.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); await messageQueueTask.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
} catch (Exception) { } catch (Exception) {

View File

@@ -133,7 +133,7 @@ public sealed class RpcClient<TClientToServerMessage, TServerToClientMessage> :
} }
} finally { } finally {
if (sessionState.HasValue) { if (sessionState.HasValue) {
await ShutdownSessionState(sessionState.Value); await sessionState.Value.TryShutdown(logger, sendSessionTermination: cancellationToken.IsCancellationRequested);
} }
if (connection != null) { if (connection != null) {
@@ -157,15 +157,6 @@ public sealed class RpcClient<TClientToServerMessage, TServerToClientMessage> :
return new SessionState(frameSender, frameReader); return new SessionState(frameSender, frameReader);
} }
private async Task ShutdownSessionState(SessionState sessionState) {
if (connector.IsEnabled) {
await sessionState.TryShutdown(logger, sendSessionTermination: shutdownCancellationTokenSource.IsCancellationRequested);
}
else {
await sessionState.TryShutdownNow(logger);
}
}
private readonly record struct SessionState(RpcFrameSender<TClientToServerMessage> FrameSender, RpcFrameReader<TClientToServerMessage, TServerToClientMessage> FrameReader) { private readonly record struct SessionState(RpcFrameSender<TClientToServerMessage> FrameSender, RpcFrameReader<TClientToServerMessage, TServerToClientMessage> FrameReader) {
public void Update(ILogger logger, RpcClientToServerConnector<TClientToServerMessage, TServerToClientMessage>.Connection connection) { public void Update(ILogger logger, RpcClientToServerConnector<TClientToServerMessage, TServerToClientMessage>.Connection connection) {
TimeSpan currentPingInterval = FrameSender.PingInterval; TimeSpan currentPingInterval = FrameSender.PingInterval;
@@ -195,7 +186,7 @@ public sealed class RpcClient<TClientToServerMessage, TServerToClientMessage> :
logger.Information("Shutting down client..."); logger.Information("Shutting down client...");
try { try {
await MessageSender.Close(connector.IsEnabled ? TimeSpan.FromSeconds(15) : TimeSpan.Zero); await MessageSender.Close();
} catch (Exception e) { } catch (Exception e) {
logger.Error(e, "Caught exception while closing message sender."); logger.Error(e, "Caught exception while closing message sender.");
} }

View File

@@ -28,8 +28,6 @@ sealed class RpcClientToServerConnector<TClientToServerMessage, TServerToClientM
private bool wasRejectedDueToClosedSession = false; private bool wasRejectedDueToClosedSession = false;
private bool loggedCertificateValidationError = false; private bool loggedCertificateValidationError = false;
internal bool IsEnabled => !wasRejectedDueToClosedSession;
public RpcClientToServerConnector(string loggerName, RpcClientConnectionParameters parameters, MessageRegistries<TClientToServerMessage, TServerToClientMessage> messageRegistries) { public RpcClientToServerConnector(string loggerName, RpcClientConnectionParameters parameters, MessageRegistries<TClientToServerMessage, TServerToClientMessage> messageRegistries) {
this.logger = PhantomLogger.Create<RpcClientToServerConnector<TClientToServerMessage, TServerToClientMessage>>(loggerName); this.logger = PhantomLogger.Create<RpcClientToServerConnector<TClientToServerMessage, TServerToClientMessage>>(loggerName);
this.parameters = parameters; this.parameters = parameters;
@@ -145,30 +143,6 @@ sealed class RpcClientToServerConnector<TClientToServerMessage, TServerToClientM
return null; return null;
} }
private bool ValidateServerCertificate(object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors) {
if (certificate == null || sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNotAvailable)) {
logger.Error("Could not establish a secure connection, server did not provide a certificate.");
}
else if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch)) {
logger.Error("Could not establish a secure connection, server certificate has the wrong name: {Name}", certificate.Subject);
}
else if (!parameters.CertificateThumbprint.Check(certificate)) {
logger.Error("Could not establish a secure connection, server certificate does not match.");
}
else if (TlsSupport.CheckAlgorithm((X509Certificate2) certificate) is {} error) {
logger.Error("Could not establish a secure connection, server certificate rejected because it uses {ActualAlgorithmName} instead of {ExpectedAlgorithmName}.", error.ActualAlgorithmName, error.ExpectedAlgorithmName);
}
else if ((sslPolicyErrors & ~SslPolicyErrors.RemoteCertificateChainErrors) != SslPolicyErrors.None) {
logger.Error("Could not establish a secure connection, server certificate validation failed.");
}
else {
return true;
}
loggedCertificateValidationError = true;
return false;
}
private async Task<ConnectionResult?> AuthenticateAndPerformHandshake(RpcStream stream, CancellationToken cancellationToken) { private async Task<ConnectionResult?> AuthenticateAndPerformHandshake(RpcStream stream, CancellationToken cancellationToken) {
try { try {
loggedCertificateValidationError = false; loggedCertificateValidationError = false;
@@ -250,7 +224,7 @@ sealed class RpcClientToServerConnector<TClientToServerMessage, TServerToClientM
return new ConnectionResult(finalHandshakeResult == RpcFinalHandshakeResult.NewSession, pingInterval.Value, mappedMessageDefinitions); return new ConnectionResult(finalHandshakeResult == RpcFinalHandshakeResult.NewSession, pingInterval.Value, mappedMessageDefinitions);
default: default:
logger.Error("Server rejected client handshake with unknown error code: {ErrorCode}", finalHandshakeResult); logger.Error("Server rejected client due to unknown error.");
return null; return null;
} }
} }
@@ -289,6 +263,30 @@ sealed class RpcClientToServerConnector<TClientToServerMessage, TServerToClientM
return result.TypeMapping; return result.TypeMapping;
} }
private bool ValidateServerCertificate(object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors) {
if (certificate == null || sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNotAvailable)) {
logger.Error("Could not establish a secure connection, server did not provide a certificate.");
}
else if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch)) {
logger.Error("Could not establish a secure connection, server certificate has the wrong name: {Name}", certificate.Subject);
}
else if (!parameters.CertificateThumbprint.Check(certificate)) {
logger.Error("Could not establish a secure connection, server certificate does not match.");
}
else if (TlsSupport.CheckAlgorithm((X509Certificate2) certificate) is {} error) {
logger.Error("Could not establish a secure connection, server certificate rejected because it uses {ActualAlgorithmName} instead of {ExpectedAlgorithmName}.", error.ActualAlgorithmName, error.ExpectedAlgorithmName);
}
else if ((sslPolicyErrors & ~SslPolicyErrors.RemoteCertificateChainErrors) != SslPolicyErrors.None) {
logger.Error("Could not establish a secure connection, server certificate validation failed.");
}
else {
return true;
}
loggedCertificateValidationError = true;
return false;
}
private static async Task DisconnectSocket(Socket socket, RpcStream? stream) { private static async Task DisconnectSocket(Socket socket, RpcStream? stream) {
if (stream != null) { if (stream != null) {
await stream.DisposeAsync(); await stream.DisposeAsync();

View File

@@ -3,5 +3,5 @@
namespace Phantom.Utils.Rpc.Runtime.Server; namespace Phantom.Utils.Rpc.Runtime.Server;
public interface IRpcServerClientRegistrar<TClientToServerMessage, TServerToClientMessage> { public interface IRpcServerClientRegistrar<TClientToServerMessage, TServerToClientMessage> {
IMessageReceiver<TClientToServerMessage> Register(RpcServerToClientConnection<TClientToServerMessage, TServerToClientMessage> connection); IMessageReceiver<TClientToServerMessage> Register(RpcServerToClientConnection<TClientToServerMessage, TServerToClientMessage> connection, Guid clientGuid);
} }

View File

@@ -317,7 +317,7 @@ public sealed class RpcServer<TClientToServerMessage, TServerToClientMessage> {
try { try {
var connection = new RpcServerToClientConnection<TClientToServerMessage, TServerToClientMessage>(sharedData.ConnectionParameters, sharedData.MessageDefinitions.ToServer.Mapping, session, stream); var connection = new RpcServerToClientConnection<TClientToServerMessage, TServerToClientMessage>(sharedData.ConnectionParameters, sharedData.MessageDefinitions.ToServer.Mapping, session, stream);
var messageReceiver = sharedData.ClientRegistrar.Register(connection); var messageReceiver = sharedData.ClientRegistrar.Register(connection, clientGuid);
return new EstablishedConnection(session, connection, messageReceiver); return new EstablishedConnection(session, connection, messageReceiver);
} catch (Exception e) { } catch (Exception e) {

View File

@@ -108,7 +108,7 @@ sealed class RpcServerClientSession<TServerToClientMessage> : IRpcConnectionProv
} }
try { try {
await MessageSender.Close(TimeSpan.FromSeconds(15)); await MessageSender.Close();
} catch (Exception e) { } catch (Exception e) {
logger.Error(e, "Caught exception while closing message sender."); logger.Error(e, "Caught exception while closing message sender.");
} }

View File

@@ -12,7 +12,6 @@ public sealed class RpcServerToClientConnection<TClientToServerMessage, TServerT
private readonly RpcServerClientSession<TServerToClientMessage> session; private readonly RpcServerClientSession<TServerToClientMessage> session;
private readonly RpcStream stream; private readonly RpcStream stream;
public Guid ClientGuid => session.ClientGuid;
public Guid SessionGuid => session.SessionGuid; public Guid SessionGuid => session.SessionGuid;
public MessageSender<TServerToClientMessage> MessageSender => session.MessageSender; public MessageSender<TServerToClientMessage> MessageSender => session.MessageSender;

View File

@@ -2,9 +2,17 @@
namespace Phantom.Utils.Processes; namespace Phantom.Utils.Processes;
public sealed class OneShotProcess(ILogger logger, ProcessConfigurator configurator) { public sealed class OneShotProcess {
private readonly ILogger logger;
private readonly ProcessConfigurator configurator;
public event EventHandler<Process.Output>? OutputReceived; public event EventHandler<Process.Output>? OutputReceived;
public OneShotProcess(ILogger logger, ProcessConfigurator configurator) {
this.logger = logger;
this.configurator = configurator;
}
public async Task<bool> Run(CancellationToken cancellationToken) { public async Task<bool> Run(CancellationToken cancellationToken) {
using var process = configurator.CreateProcess(); using var process = configurator.CreateProcess();
process.OutputReceived += OutputReceived; process.OutputReceived += OutputReceived;

View File

@@ -31,12 +31,6 @@ public sealed class ProcessConfigurator {
set => startInfo.UseShellExecute = value; set => startInfo.UseShellExecute = value;
} }
public ProcessConfigurator() {
if (OperatingSystem.IsWindows()) {
startInfo.CreateNewProcessGroup = true;
}
}
public Process CreateProcess() { public Process CreateProcess() {
return new Process(new System.Diagnostics.Process { StartInfo = startInfo }); return new Process(new System.Diagnostics.Process { StartInfo = startInfo });
} }

View File

@@ -1,7 +1,7 @@
<div> <div>
<div class="progress-label">@ChildContent</div> <div class="progress-label">@ChildContent</div>
<div class="progress"> <div class="progress">
<div class="progress-bar" role="progressbar" style="width: @(Maximum <= 0 ? 0 : 100 * Value / Maximum)%;" aria-valuenow="@Value" aria-valuemin="0" aria-valuemax="@Maximum"></div> <div class="progress-bar" role="progressbar" style="width: @(100 * Value / Maximum)%;" aria-valuenow="@Value" aria-valuemin="0" aria-valuemax="@Maximum"></div>
</div> </div>
</div> </div>

View File

@@ -1,56 +1,31 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using Phantom.Common.Data;
using Phantom.Common.Data.Web.Agent; using Phantom.Common.Data.Web.Agent;
using Phantom.Common.Data.Web.Users;
using Phantom.Common.Messages.Web.ToController;
using Phantom.Utils.Events; using Phantom.Utils.Events;
using Phantom.Utils.Logging; using Phantom.Utils.Logging;
using Phantom.Web.Services.Authentication; using Phantom.Web.Services.Authentication;
using Phantom.Web.Services.Rpc;
namespace Phantom.Web.Services.Agents; namespace Phantom.Web.Services.Agents;
using AgentDictionary = ImmutableDictionary<Guid, Agent>; public sealed class AgentManager {
private readonly SimpleObservableState<ImmutableArray<Agent>> agents = new (PhantomLogger.Create<AgentManager>("Agents"), ImmutableArray<Agent>.Empty);
public sealed class AgentManager(ControllerConnection controllerConnection) { public EventSubscribers<ImmutableArray<Agent>> AgentsChanged => agents.Subs;
private readonly SimpleObservableState<AgentDictionary> agents = new (PhantomLogger.Create<AgentManager>("Agents"), AgentDictionary.Empty);
public EventSubscribers<AgentDictionary> AgentsChanged => agents.Subs;
internal void RefreshAgents(ImmutableArray<Agent> newAgents) { internal void RefreshAgents(ImmutableArray<Agent> newAgents) {
agents.SetTo(newAgents.ToImmutableDictionary(static agent => agent.AgentGuid)); agents.SetTo(newAgents);
} }
public AgentDictionary GetAll() { public ImmutableArray<Agent> GetAll() {
return agents.Value; return agents.Value;
} }
public Agent? GetByGuid(AuthenticatedUser? authenticatedUser, Guid agentGuid) { public ImmutableDictionary<Guid, Agent> ToDictionaryByGuid(AuthenticatedUser? authenticatedUser) {
if (authenticatedUser == null) { if (authenticatedUser == null) {
return null; return ImmutableDictionary<Guid, Agent>.Empty;
}
var agent = agents.Value.GetValueOrDefault(agentGuid);
return agent != null && authenticatedUser.Info.HasAccessToAgent(agent.AgentGuid) ? agent : null;
}
public AgentDictionary ToDictionaryByGuid(AuthenticatedUser? authenticatedUser) {
if (authenticatedUser == null) {
return AgentDictionary.Empty;
} }
return agents.Value return agents.Value
.Where(kvp => authenticatedUser.Info.HasAccessToAgent(kvp.Key)) .Where(agent => authenticatedUser.Info.HasAccessToAgent(agent.AgentGuid))
.ToImmutableDictionary(); .ToImmutableDictionary(static agent => agent.AgentGuid);
}
public async Task<Result<CreateOrUpdateAgentResult, UserActionFailure>> CreateOrUpdateAgent(AuthenticatedUser? authenticatedUser, Guid agentGuid, AgentConfiguration configuration, CancellationToken cancellationToken) {
if (authenticatedUser != null && authenticatedUser.Info.CheckPermission(Permission.ManageAllAgents)) {
var message = new CreateOrUpdateAgentMessage(authenticatedUser.Token, agentGuid, configuration);
return await controllerConnection.Send<CreateOrUpdateAgentMessage, Result<CreateOrUpdateAgentResult, UserActionFailure>>(message, cancellationToken);
}
else {
return UserActionFailure.NotAuthorized;
}
} }
} }

View File

@@ -87,7 +87,12 @@ public sealed class CustomAuthenticationStateProvider : ServerAuthenticationStat
} }
var session = await controllerConnection.Send<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(new GetAuthenticatedUser(userGuid, authToken), TimeSpan.FromSeconds(30), cancellationToken); var session = await controllerConnection.Send<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(new GetAuthenticatedUser(userGuid, authToken), TimeSpan.FromSeconds(30), cancellationToken);
return session.HasValue ? new AuthenticatedUser(session.Value, authToken) : null; if (session.Value is {} userInfo) {
return new AuthenticatedUser(userInfo, authToken);
}
else {
return null;
}
} }
private void SetLoadedSession(AuthenticatedUser authenticatedUser) { private void SetLoadedSession(AuthenticatedUser authenticatedUser) {

View File

@@ -19,12 +19,10 @@ public sealed class UserLoginManager(Navigation navigation, UserSessionBrowserSt
return false; return false;
} }
if (!result.HasValue) { if (result.Value is not var (userInfo, authToken)) {
return false; return false;
} }
var (userInfo, authToken) = result.Value;
Logger.Information("Successfully logged in {Username}.", username); Logger.Information("Successfully logged in {Username}.", username);
authenticationStateProvider.SetUnloadedSession(); authenticationStateProvider.SetUnloadedSession();

View File

@@ -15,9 +15,14 @@ namespace Phantom.Web.Services.Instances;
using InstanceDictionary = ImmutableDictionary<Guid, Instance>; using InstanceDictionary = ImmutableDictionary<Guid, Instance>;
public sealed class InstanceManager(ControllerConnection controllerConnection) { public sealed class InstanceManager {
private readonly ControllerConnection controllerConnection;
private readonly SimpleObservableState<InstanceDictionary> instances = new (PhantomLogger.Create<InstanceManager>("Instances"), InstanceDictionary.Empty); private readonly SimpleObservableState<InstanceDictionary> instances = new (PhantomLogger.Create<InstanceManager>("Instances"), InstanceDictionary.Empty);
public InstanceManager(ControllerConnection controllerConnection) {
this.controllerConnection = controllerConnection;
}
public EventSubscribers<InstanceDictionary> InstancesChanged => instances.Subs; public EventSubscribers<InstanceDictionary> InstancesChanged => instances.Subs;
internal void RefreshInstances(ImmutableArray<Instance> newInstances) { internal void RefreshInstances(ImmutableArray<Instance> newInstances) {

View File

@@ -1,9 +1,8 @@
@using Phantom.Web.Errors @using Phantom.Web.Services
@using Phantom.Web.Services
@inject Navigation Navigation @inject Navigation Navigation
<CascadingAuthenticationState> <CascadingAuthenticationState>
<Router AppAssembly="@typeof(App).Assembly" NotFoundPage="typeof(NotFound)"> <Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData"> <Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"> <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized> <NotAuthorized>
@@ -18,5 +17,11 @@
</AuthorizeRouteView> </AuthorizeRouteView>
<FocusOnNavigate RouteData="@routeData" Selector="h1" /> <FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found> </Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<h1>Not Found</h1>
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router> </Router>
</CascadingAuthenticationState> </CascadingAuthenticationState>

View File

@@ -1,5 +0,0 @@
@page "/error/404"
@layout MainLayout
<h1>Not Found</h1>
<p role="alert">Sorry, there's nothing at this address.</p>

View File

@@ -34,7 +34,7 @@
<script src="_framework/blazor.server.js"></script> <script src="_framework/blazor.server.js"></script>
@* ReSharper restore Html.PathError *@ @* ReSharper restore Html.PathError *@
<script src="lib/bootstrap/bootstrap.bundle.min.js"></script> <script src="lib/bootstrap/bootstrap.bundle.min.js"></script>
<script src="js/site.js?v=2"></script> <script src="js/site.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,6 +0,0 @@
@page "/agents/create"
@using Phantom.Common.Data.Web.Users
@attribute [Authorize(Permission.ManageAllAgentsPolicy)]
<h1>New Agent</h1>
<AgentAddOrEditForm EditedAgent="null" />

View File

@@ -1,37 +0,0 @@
@page "/agents/{AgentGuid:guid}/edit"
@attribute [Authorize(Permission.ManageAllAgentsPolicy)]
@using Phantom.Common.Data.Web.Agent
@using Phantom.Common.Data.Web.Users
@using Phantom.Web.Services.Agents
@inherits PhantomComponent
@inject AgentManager AgentManager
@if (isLoading) {
<h1>Edit Agent</h1>
<p>Loading...</p>
return;
}
@if (Agent == null) {
<h1>Agent Not Found</h1>
<p>Return to <a href="agents">all agents</a>.</p>
return;
}
<h1>Edit Agent: @Agent.Configuration.AgentName</h1>
<AgentAddOrEditForm EditedAgent="Agent" />
@code {
[Parameter]
public Guid AgentGuid { get; init; }
private Agent? Agent { get; set; }
private bool isLoading = true;
protected override async Task OnInitializedAsync() {
Agent = AgentManager.GetByGuid(await GetAuthenticatedUser(), AgentGuid);
isLoading = false;
}
}

View File

@@ -1,20 +1,14 @@
@page "/agents" @page "/agents"
@using System.Collections.Immutable @using System.Collections.Immutable
@using Phantom.Common.Data.Web.Agent @using Phantom.Common.Data.Web.Agent
@using Phantom.Common.Data.Web.Users
@using Phantom.Utils.Collections @using Phantom.Utils.Collections
@using Phantom.Utils.Cryptography @using Phantom.Utils.Cryptography
@using Phantom.Web.Services.Agents @using Phantom.Web.Services.Agents
@using Phantom.Web.Services.Authorization
@inherits PhantomComponent @inherits PhantomComponent
@inject AgentManager AgentManager @inject AgentManager AgentManager
<h1>Agents</h1> <h1>Agents</h1>
<PermissionView Permission="Permission.ManageAllAgents">
<a href="agents/create" class="btn btn-primary" role="button">New Agent</a>
</PermissionView>
<Table Items="agentTable"> <Table Items="agentTable">
<HeaderRow> <HeaderRow>
<Column Width="50%">Name</Column> <Column Width="50%">Name</Column>
@@ -28,46 +22,29 @@
<ItemRow Context="agent"> <ItemRow Context="agent">
@{ @{
var connectionKey = TokenGenerator.EncodeBytes(agent.ConnectionKey.AsSpan()); var connectionKey = TokenGenerator.EncodeBytes(agent.ConnectionKey.AsSpan());
var runtimeInfo = agent.RuntimeInfo; var configuration = agent.Configuration;
var usedInstances = agent.Stats?.RunningInstanceCount; var usedInstances = agent.Stats?.RunningInstanceCount;
var usedMemory = agent.Stats?.RunningInstanceMemory.InMegabytes; var usedMemory = agent.Stats?.RunningInstanceMemory.InMegabytes;
} }
<Cell> <Cell>
<p class="fw-semibold">@agent.Configuration.AgentName</p> <p class="fw-semibold">@agent.Name</p>
<small class="font-monospace text-uppercase">@agent.AgentGuid.ToString()</small> <small class="font-monospace text-uppercase">@agent.AgentGuid.ToString()</small>
</Cell> </Cell>
<Cell class="text-end"> <Cell class="text-end">
<ProgressBar Value="@(usedInstances ?? 0)" Maximum="@(runtimeInfo.MaxInstances ?? 0)"> <ProgressBar Value="@(usedInstances ?? 0)" Maximum="@configuration.MaxInstances">
@if (runtimeInfo.MaxInstances is {} maxInstances) { @(usedInstances?.ToString() ?? "?") / @configuration.MaxInstances.ToString()
<text>@(usedInstances?.ToString() ?? "?") / @maxInstances.ToString()</text>
}
else {
@:N/A
}
</ProgressBar> </ProgressBar>
</Cell> </Cell>
<Cell class="text-end"> <Cell class="text-end">
<ProgressBar Value="@(usedMemory ?? 0)" Maximum="@(runtimeInfo.MaxMemory?.InMegabytes ?? 0)"> <ProgressBar Value="@(usedMemory ?? 0)" Maximum="@configuration.MaxMemory.InMegabytes">
@if (runtimeInfo.MaxMemory is {} maxMemory) { @(usedMemory?.ToString() ?? "?") / @configuration.MaxMemory.InMegabytes.ToString() MB
<text>@(usedMemory?.ToString() ?? "?") / @maxMemory.InMegabytes MB</text>
}
else {
@:N/A
}
</ProgressBar> </ProgressBar>
</Cell> </Cell>
@if (runtimeInfo.VersionInfo is {} versionInfo) {
<Cell class="text-condensed"> <Cell class="text-condensed">
Build: <span class="font-monospace">@versionInfo.BuildVersion</span> Build: <span class="font-monospace">@configuration.BuildVersion</span>
<br> <br>
Protocol: <span class="font-monospace">v@(versionInfo.ProtocolVersion.ToString())</span> Protocol: <span class="font-monospace">v@(configuration.ProtocolVersion.ToString())</span>
</Cell> </Cell>
}
else {
<Cell>
N/A
</Cell>
}
@switch (agent.ConnectionStatus) { @switch (agent.ConnectionStatus) {
case AgentIsOnline: case AgentIsOnline:
<Cell class="fw-semibold text-center text-success">Online</Cell> <Cell class="fw-semibold text-center text-success">Online</Cell>
@@ -91,10 +68,7 @@
break; break;
} }
<Cell> <Cell>
<PermissionView Permission="Permission.ManageAllAgents"> <button type="button" class="btn btn-danger btn-sm" data-clipboard="@connectionKey" onclick="copyToClipboard(this);">Copy Agent Key</button>
<a href="agents/@agent.AgentGuid/edit" type="button" class="btn btn-primary btn-sm">Edit Agent</a>
<button type="button" class="btn btn-warning btn-sm" data-clipboard="@connectionKey" onclick="copyToClipboard(this);">Copy Agent Key</button>
</PermissionView>
</Cell> </Cell>
</ItemRow> </ItemRow>
<NoItemsRow> <NoItemsRow>
@@ -113,9 +87,8 @@
} }
AgentManager.AgentsChanged.Subscribe(this, agents => { AgentManager.AgentsChanged.Subscribe(this, agents => {
var sortedAgents = agents.Values var sortedAgents = agents.Where(agent => authenticatedUser.Info.HasAccessToAgent(agent.AgentGuid))
.Where(agent => authenticatedUser.Info.HasAccessToAgent(agent.AgentGuid)) .OrderBy(static agent => agent.Name)
.OrderBy(static agent => agent.Configuration.AgentName)
.ToImmutableArray(); .ToImmutableArray();
agentTable ??= new TableData<Agent, Guid>(); agentTable ??= new TableData<Agent, Guid>();

View File

@@ -3,11 +3,9 @@
@using System.Collections.Immutable @using System.Collections.Immutable
@using Phantom.Common.Data.Web.AuditLog @using Phantom.Common.Data.Web.AuditLog
@using Phantom.Common.Data.Web.Users @using Phantom.Common.Data.Web.Users
@using Phantom.Web.Services.Agents
@using Phantom.Web.Services.Instances @using Phantom.Web.Services.Instances
@using Phantom.Web.Services.Users @using Phantom.Web.Services.Users
@inherits PhantomComponent @inherits PhantomComponent
@inject AgentManager AgentManager
@inject AuditLogManager AuditLogManager @inject AuditLogManager AuditLogManager
@inject InstanceManager InstanceManager @inject InstanceManager InstanceManager
@inject UserManager UserManager @inject UserManager UserManager
@@ -57,7 +55,6 @@
private string? loadError; private string? loadError;
private ImmutableDictionary<Guid, string>? userNamesByGuid; private ImmutableDictionary<Guid, string>? userNamesByGuid;
private ImmutableDictionary<Guid, string> agentNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty; private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
protected override async Task OnInitializedAsync() { protected override async Task OnInitializedAsync() {
@@ -65,7 +62,6 @@
if (result) { if (result) {
logItems = result.Value; logItems = result.Value;
userNamesByGuid = (await UserManager.GetAll(CancellationToken)).ToImmutableDictionary(static user => user.Guid, static user => user.Name); userNamesByGuid = (await UserManager.GetAll(CancellationToken)).ToImmutableDictionary(static user => user.Guid, static user => user.Name);
agentNamesByGuid = AgentManager.GetAll().Values.ToImmutableDictionary(static agent => agent.AgentGuid, static agent => agent.Configuration.AgentName);
instanceNamesByGuid = InstanceManager.GetAll().Values.ToImmutableDictionary(static instance => instance.InstanceGuid, static instance => instance.Configuration.InstanceName); instanceNamesByGuid = InstanceManager.GetAll().Values.ToImmutableDictionary(static instance => instance.InstanceGuid, static instance => instance.Configuration.InstanceName);
} }
else { else {
@@ -79,9 +75,8 @@
private string? GetSubjectName(AuditLogSubjectType type, string id) { private string? GetSubjectName(AuditLogSubjectType type, string id) {
return type switch { return type switch {
AuditLogSubjectType.User => userNamesByGuid != null && userNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null,
AuditLogSubjectType.Agent => agentNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null,
AuditLogSubjectType.Instance => instanceNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null, AuditLogSubjectType.Instance => instanceNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null,
AuditLogSubjectType.User => userNamesByGuid != null && userNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null,
_ => null, _ => null,
}; };
} }

View File

@@ -65,7 +65,7 @@
var result = await EventLogManager.GetMostRecentItems(await GetAuthenticatedUser(), count: 50, CancellationToken); var result = await EventLogManager.GetMostRecentItems(await GetAuthenticatedUser(), count: 50, CancellationToken);
if (result) { if (result) {
logItems = result.Value; logItems = result.Value;
agentNamesByGuid = AgentManager.GetAll().Values.ToImmutableDictionary(static kvp => kvp.AgentGuid, static kvp => kvp.Configuration.AgentName); agentNamesByGuid = AgentManager.GetAll().ToImmutableDictionary(static kvp => kvp.AgentGuid, static kvp => kvp.Name);
instanceNamesByGuid = InstanceManager.GetAll().Values.ToImmutableDictionary(static instance => instance.InstanceGuid, static instance => instance.Configuration.InstanceName); instanceNamesByGuid = InstanceManager.GetAll().Values.ToImmutableDictionary(static instance => instance.InstanceGuid, static instance => instance.Configuration.InstanceName);
} }
else { else {

View File

@@ -76,7 +76,7 @@
protected override void OnInitialized() { protected override void OnInitialized() {
AgentManager.AgentsChanged.Subscribe(this, agents => { AgentManager.AgentsChanged.Subscribe(this, agents => {
this.agentNamesByGuid = agents.Select(static kvp => KeyValuePair.Create(kvp.Key, kvp.Value.Configuration.AgentName)).ToImmutableDictionary(); this.agentNamesByGuid = agents.ToImmutableDictionary(static agent => agent.AgentGuid, static agent => agent.Name);
InvokeAsync(StateHasChanged); InvokeAsync(StateHasChanged);
}); });
} }

View File

@@ -1,73 +0,0 @@
@using System.ComponentModel.DataAnnotations
@using Phantom.Common.Data.Web.Agent
@using Phantom.Common.Data.Web.Users
@using Phantom.Utils.Result
@using Phantom.Web.Services
@using Phantom.Web.Services.Agents
@inherits PhantomComponent
@inject AgentManager AgentManager
@inject Navigation Navigation
<Form Model="form" OnSubmit="AddOrEditAgent">
<div class="row">
<div class="col-xl-12 mb-3">
<FormTextInput Id="agent-name" Label="Agent Name" @bind-Value="form.AgentName" />
</div>
</div>
<FormButtonSubmit Label="@(EditedAgent == null ? "Create Agent" : "Edit Agent")" class="btn btn-primary" />
<FormSubmitError />
</Form>
@code {
[Parameter, EditorRequired]
public Agent? EditedAgent { get; init; }
private ConfigureAgentFormModel form = null!;
private sealed class ConfigureAgentFormModel : FormModel {
[Required(ErrorMessage = "Agent name is required.")]
[StringLength(100, ErrorMessage = "Agent name must be at most 100 characters.")]
public string AgentName { get; set; } = string.Empty;
}
protected override void OnInitialized() {
form = new ConfigureAgentFormModel();
if (EditedAgent != null) {
var configuration = EditedAgent.Configuration;
form.AgentName = configuration.AgentName;
}
}
private async Task AddOrEditAgent(EditContext context) {
await form.SubmitModel.StartSubmitting();
var agentGuid = EditedAgent?.AgentGuid ?? Guid.NewGuid();
var agentConfiguration = new AgentConfiguration(
form.AgentName
);
var result = await AgentManager.CreateOrUpdateAgent(await GetAuthenticatedUser(), agentGuid, agentConfiguration, CancellationToken);
switch (result.Variant()) {
case Ok<CreateOrUpdateAgentResult>(CreateOrUpdateAgentResult.Success):
await Navigation.NavigateTo("agents");
break;
case Ok<CreateOrUpdateAgentResult>(var createOrUpdateAgentResult):
form.SubmitModel.StopSubmitting(createOrUpdateAgentResult.ToSentence());
break;
case Err<UserActionFailure>(UserActionFailure.NotAuthorized):
form.SubmitModel.StopSubmitting("You do not have permission to create or edit agents.");
break;
default:
form.SubmitModel.StopSubmitting("Unknown error.");
break;
}
}
}

View File

@@ -18,10 +18,10 @@
@using Phantom.Web.Services.Instances @using Phantom.Web.Services.Instances
@using Phantom.Web.Services.Rpc @using Phantom.Web.Services.Rpc
@inherits PhantomComponent @inherits PhantomComponent
@inject AgentManager AgentManager
@inject ControllerConnection ControllerConnection
@inject InstanceManager InstanceManager
@inject Navigation Navigation @inject Navigation Navigation
@inject ControllerConnection ControllerConnection
@inject AgentManager AgentManager
@inject InstanceManager InstanceManager
<Form Model="form" OnSubmit="AddOrEditInstance"> <Form Model="form" OnSubmit="AddOrEditInstance">
@{ var selectedAgent = form.SelectedAgent; } @{ var selectedAgent = form.SelectedAgent; }
@@ -29,27 +29,21 @@
<div class="col-xl-7 mb-3"> <div class="col-xl-7 mb-3">
@{ @{
static RenderFragment GetAgentOption(Agent agent) { static RenderFragment GetAgentOption(Agent agent) {
var runtimeInfo = agent.RuntimeInfo; var configuration = agent.Configuration;
return return
@<option value="@agent.AgentGuid" disabled="@(agent.ConnectionStatus is not AgentIsOnline)"> @<option value="@agent.AgentGuid">
@agent.Configuration.AgentName @agent.Name
@if (agent.ConnectionStatus is not AgentIsOnline) { &bullet;
<text> &bullet; </text> @(agent.Stats?.RunningInstanceCount.ToString() ?? "?")/@(configuration.MaxInstances) @(configuration.MaxInstances == 1 ? "Instance" : "Instances")
<text>Offline</text> &bullet;
} @(agent.Stats?.RunningInstanceMemory.InMegabytes.ToString() ?? "?")/@(configuration.MaxMemory.InMegabytes) MB RAM
else if (runtimeInfo.MaxInstances is not null && runtimeInfo.MaxMemory is not null) {
<text> &bullet; </text>
<text>@(agent.Stats?.RunningInstanceCount.ToString() ?? "?")/@(runtimeInfo.MaxInstances) @(runtimeInfo.MaxInstances == 1 ? "Instance" : "Instances")</text>
<text> &bullet; </text>
<text>@(agent.Stats?.RunningInstanceMemory.InMegabytes.ToString() ?? "?")/@(runtimeInfo.MaxMemory.Value.InMegabytes) MB RAM</text>
}
</option>; </option>;
} }
} }
@if (EditedInstance == null) { @if (EditedInstance == null) {
<FormSelectInput Id="instance-agent" Label="Agent" @bind-Value="form.SelectedAgentGuid"> <FormSelectInput Id="instance-agent" Label="Agent" @bind-Value="form.SelectedAgentGuid">
<option value="" selected>Select which agent will run the instance...</option> <option value="" selected>Select which agent will run the instance...</option>
@foreach (var agent in allAgentsByGuid.Values.OrderBy(static agent => agent.Configuration.AgentName)) { @foreach (var agent in allAgentsByGuid.Values.Where(static agent => agent.ConnectionStatus is AgentIsOnline).OrderBy(static agent => agent.Name)) {
@GetAgentOption(agent) @GetAgentOption(agent)
} }
</FormSelectInput> </FormSelectInput>
@@ -107,8 +101,8 @@
</div> </div>
@{ @{
string? allowedServerPorts = selectedAgent?.RuntimeInfo.AllowedServerPorts?.ToString(); string? allowedServerPorts = selectedAgent?.Configuration.AllowedServerPorts?.ToString();
string? allowedRconPorts = selectedAgent?.RuntimeInfo.AllowedRconPorts?.ToString(); string? allowedRconPorts = selectedAgent?.Configuration.AllowedRconPorts?.ToString();
} }
<div class="col-sm-6 col-xl-2 mb-3"> <div class="col-sm-6 col-xl-2 mb-3">
<FormNumberInput Id="instance-server-port" @bind-Value="form.ServerPort" min="0" max="65535"> <FormNumberInput Id="instance-server-port" @bind-Value="form.ServerPort" min="0" max="65535">
@@ -147,11 +141,11 @@
} }
<FormNumberInput Id="instance-memory" Type="FormNumberInputType.Range" DebounceMillis="0" DisableTwoWayBinding="true" @bind-Value="form.MemoryUnits" min="@MinimumMemoryUnits" max="@maximumMemoryUnits" disabled="@(maximumMemoryUnits == 0)" class="form-range split-danger" style="@memoryInputSplitVar"> <FormNumberInput Id="instance-memory" Type="FormNumberInputType.Range" DebounceMillis="0" DisableTwoWayBinding="true" @bind-Value="form.MemoryUnits" min="@MinimumMemoryUnits" max="@maximumMemoryUnits" disabled="@(maximumMemoryUnits == 0)" class="form-range split-danger" style="@memoryInputSplitVar">
<LabelFragment> <LabelFragment>
@if (maximumMemoryUnits == 0 || selectedAgent?.RuntimeInfo.MaxMemory is not {} maxMemory) { @if (maximumMemoryUnits == 0) {
<text>RAM</text> <text>RAM</text>
} }
else { else {
<text>RAM &bullet; <code>@(form.MemoryAllocation?.InMegabytes ?? 0) / @(maxMemory.InMegabytes) MB</code></text> <text>RAM &bullet; <code>@(form.MemoryAllocation?.InMegabytes ?? 0) / @(selectedAgent?.Configuration.MaxMemory.InMegabytes) MB</code></text>
} }
</LabelFragment> </LabelFragment>
</FormNumberInput> </FormNumberInput>
@@ -215,7 +209,7 @@
public ImmutableArray<TaggedJavaRuntime> JavaRuntimesForSelectedAgent => TryGet(page.allAgentJavaRuntimes, SelectedAgentGuid, out var javaRuntimes) ? javaRuntimes : ImmutableArray<TaggedJavaRuntime>.Empty; public ImmutableArray<TaggedJavaRuntime> JavaRuntimesForSelectedAgent => TryGet(page.allAgentJavaRuntimes, SelectedAgentGuid, out var javaRuntimes) ? javaRuntimes : ImmutableArray<TaggedJavaRuntime>.Empty;
public ushort MaximumMemoryUnits => SelectedAgent?.RuntimeInfo.MaxMemory?.RawValue ?? 0; public ushort MaximumMemoryUnits => SelectedAgent?.Configuration.MaxMemory.RawValue ?? 0;
public ushort AvailableMemoryUnits => Math.Min((SelectedAgent?.AvailableMemory + editedInstanceRamAllocation)?.RawValue ?? MaximumMemoryUnits, MaximumMemoryUnits); public ushort AvailableMemoryUnits => Math.Min((SelectedAgent?.AvailableMemory + editedInstanceRamAllocation)?.RawValue ?? MaximumMemoryUnits, MaximumMemoryUnits);
private ushort selectedMemoryUnits = 4; private ushort selectedMemoryUnits = 4;
@@ -256,12 +250,12 @@
public sealed class ServerPortMustBeAllowedAttribute : FormValidationAttribute<ConfigureInstanceFormModel, int> { public sealed class ServerPortMustBeAllowedAttribute : FormValidationAttribute<ConfigureInstanceFormModel, int> {
protected override string FieldName => nameof(ServerPort); protected override string FieldName => nameof(ServerPort);
protected override bool IsValid(ConfigureInstanceFormModel model, int value) => model.SelectedAgent is not {} agent || agent.RuntimeInfo.AllowedServerPorts?.Contains((ushort) value) == true; protected override bool IsValid(ConfigureInstanceFormModel model, int value) => model.SelectedAgent is not {} agent || agent.Configuration.AllowedServerPorts?.Contains((ushort) value) == true;
} }
public sealed class RconPortMustBeAllowedAttribute : FormValidationAttribute<ConfigureInstanceFormModel, int> { public sealed class RconPortMustBeAllowedAttribute : FormValidationAttribute<ConfigureInstanceFormModel, int> {
protected override string FieldName => nameof(RconPort); protected override string FieldName => nameof(RconPort);
protected override bool IsValid(ConfigureInstanceFormModel model, int value) => model.SelectedAgent is not {} agent || agent.RuntimeInfo.AllowedRconPorts?.Contains((ushort) value) == true; protected override bool IsValid(ConfigureInstanceFormModel model, int value) => model.SelectedAgent is not {} agent || agent.Configuration.AllowedRconPorts?.Contains((ushort) value) == true;
} }
public sealed class RconPortMustDifferFromServerPortAttribute : FormValidationAttribute<ConfigureInstanceFormModel, int?> { public sealed class RconPortMustDifferFromServerPortAttribute : FormValidationAttribute<ConfigureInstanceFormModel, int?> {

View File

@@ -1,6 +1,6 @@
{ {
"sdk": { "sdk": {
"version": "10.0.0", "version": "9.0.0",
"rollForward": "latestMinor", "rollForward": "latestMinor",
"allowPrerelease": true "allowPrerelease": true
} }