mirror of
https://github.com/chylex/Minecraft-Phantom-Panel.git
synced 2024-11-25 07:42:58 +01:00
Compare commits
No commits in common. "d591318340f8bbf0b05fb9b314c94e3b96d66815" and "94148add2d642ca5d514f76ef59a8c76548d7d5f" have entirely different histories.
d591318340
...
94148add2d
@ -6,6 +6,7 @@ using Phantom.Agent.Services.Instances;
|
|||||||
using Phantom.Common.Data.Agent;
|
using Phantom.Common.Data.Agent;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
|
using Phantom.Utils.Tasks;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
namespace Phantom.Agent.Services;
|
namespace Phantom.Agent.Services;
|
||||||
@ -17,6 +18,7 @@ public sealed class AgentServices {
|
|||||||
|
|
||||||
private AgentFolders AgentFolders { get; }
|
private AgentFolders AgentFolders { get; }
|
||||||
private AgentState AgentState { get; }
|
private AgentState AgentState { get; }
|
||||||
|
private TaskManager TaskManager { get; }
|
||||||
private BackupManager BackupManager { get; }
|
private BackupManager BackupManager { get; }
|
||||||
|
|
||||||
internal JavaRuntimeRepository JavaRuntimeRepository { get; }
|
internal JavaRuntimeRepository JavaRuntimeRepository { get; }
|
||||||
@ -28,12 +30,13 @@ public sealed class AgentServices {
|
|||||||
|
|
||||||
this.AgentFolders = agentFolders;
|
this.AgentFolders = agentFolders;
|
||||||
this.AgentState = new AgentState();
|
this.AgentState = new AgentState();
|
||||||
|
this.TaskManager = new TaskManager(PhantomLogger.Create<TaskManager, AgentServices>());
|
||||||
this.BackupManager = new BackupManager(agentFolders, serviceConfiguration.MaxConcurrentCompressionTasks);
|
this.BackupManager = new BackupManager(agentFolders, serviceConfiguration.MaxConcurrentCompressionTasks);
|
||||||
|
|
||||||
this.JavaRuntimeRepository = new JavaRuntimeRepository();
|
this.JavaRuntimeRepository = new JavaRuntimeRepository();
|
||||||
this.InstanceTicketManager = new InstanceTicketManager(agentInfo, controllerConnection);
|
this.InstanceTicketManager = new InstanceTicketManager(agentInfo, controllerConnection);
|
||||||
|
|
||||||
var instanceManagerInit = new InstanceManagerActor.Init(controllerConnection, agentFolders, AgentState, JavaRuntimeRepository, InstanceTicketManager, BackupManager);
|
var instanceManagerInit = new InstanceManagerActor.Init(controllerConnection, agentFolders, AgentState, JavaRuntimeRepository, InstanceTicketManager, TaskManager, BackupManager);
|
||||||
this.InstanceManager = ActorSystem.ActorOf(InstanceManagerActor.Factory(instanceManagerInit), "InstanceManager");
|
this.InstanceManager = ActorSystem.ActorOf(InstanceManagerActor.Factory(instanceManagerInit), "InstanceManager");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,6 +50,7 @@ public sealed class AgentServices {
|
|||||||
Logger.Information("Stopping services...");
|
Logger.Information("Stopping services...");
|
||||||
|
|
||||||
await InstanceManager.Stop(new InstanceManagerActor.ShutdownCommand());
|
await InstanceManager.Stop(new InstanceManagerActor.ShutdownCommand());
|
||||||
|
await TaskManager.Stop();
|
||||||
|
|
||||||
BackupManager.Dispose();
|
BackupManager.Dispose();
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ sealed class BackupScheduler : CancellableBackgroundTask {
|
|||||||
|
|
||||||
public event EventHandler<BackupCreationResult>? BackupCompleted;
|
public event EventHandler<BackupCreationResult>? BackupCompleted;
|
||||||
|
|
||||||
public BackupScheduler(InstanceContext context, InstanceProcess process, int serverPort) : base(PhantomLogger.Create<BackupScheduler>(context.ShortName)) {
|
public BackupScheduler(InstanceContext context, InstanceProcess process, int serverPort) : base(PhantomLogger.Create<BackupScheduler>(context.ShortName), context.Services.TaskManager, "Backup scheduler for " + context.ShortName) {
|
||||||
this.backupManager = context.Services.BackupManager;
|
this.backupManager = context.Services.BackupManager;
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.process = process;
|
this.process = process;
|
||||||
|
@ -20,7 +20,7 @@ namespace Phantom.Agent.Services.Instances;
|
|||||||
sealed class InstanceManagerActor : ReceiveActor<InstanceManagerActor.ICommand> {
|
sealed class InstanceManagerActor : ReceiveActor<InstanceManagerActor.ICommand> {
|
||||||
private static readonly ILogger Logger = PhantomLogger.Create<InstanceManagerActor>();
|
private static readonly ILogger Logger = PhantomLogger.Create<InstanceManagerActor>();
|
||||||
|
|
||||||
public readonly record struct Init(ControllerConnection ControllerConnection, AgentFolders AgentFolders, AgentState AgentState, JavaRuntimeRepository JavaRuntimeRepository, InstanceTicketManager InstanceTicketManager, BackupManager BackupManager);
|
public readonly record struct Init(ControllerConnection ControllerConnection, AgentFolders AgentFolders, AgentState AgentState, JavaRuntimeRepository JavaRuntimeRepository, InstanceTicketManager InstanceTicketManager, TaskManager TaskManager, BackupManager BackupManager);
|
||||||
|
|
||||||
public static Props<ICommand> Factory(Init init) {
|
public static Props<ICommand> Factory(Init init) {
|
||||||
return Props<ICommand>.Create(() => new InstanceManagerActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume });
|
return Props<ICommand>.Create(() => new InstanceManagerActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume });
|
||||||
@ -47,7 +47,7 @@ sealed class InstanceManagerActor : ReceiveActor<InstanceManagerActor.ICommand>
|
|||||||
var minecraftServerExecutables = new MinecraftServerExecutables(init.AgentFolders.ServerExecutableFolderPath);
|
var minecraftServerExecutables = new MinecraftServerExecutables(init.AgentFolders.ServerExecutableFolderPath);
|
||||||
var launchServices = new LaunchServices(minecraftServerExecutables, init.JavaRuntimeRepository);
|
var launchServices = new LaunchServices(minecraftServerExecutables, init.JavaRuntimeRepository);
|
||||||
|
|
||||||
this.instanceServices = new InstanceServices(init.ControllerConnection, init.BackupManager, launchServices);
|
this.instanceServices = new InstanceServices(init.ControllerConnection, init.TaskManager, init.BackupManager, launchServices);
|
||||||
|
|
||||||
ReceiveAndReply<ConfigureInstanceCommand, InstanceActionResult<ConfigureInstanceResult>>(ConfigureInstance);
|
ReceiveAndReply<ConfigureInstanceCommand, InstanceActionResult<ConfigureInstanceResult>>(ConfigureInstance);
|
||||||
ReceiveAndReply<LaunchInstanceCommand, InstanceActionResult<LaunchInstanceResult>>(LaunchInstance);
|
ReceiveAndReply<LaunchInstanceCommand, InstanceActionResult<LaunchInstanceResult>>(LaunchInstance);
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
using Phantom.Agent.Minecraft.Launcher;
|
using Phantom.Agent.Minecraft.Launcher;
|
||||||
using Phantom.Agent.Rpc;
|
using Phantom.Agent.Rpc;
|
||||||
using Phantom.Agent.Services.Backups;
|
using Phantom.Agent.Services.Backups;
|
||||||
|
using Phantom.Utils.Tasks;
|
||||||
|
|
||||||
namespace Phantom.Agent.Services.Instances;
|
namespace Phantom.Agent.Services.Instances;
|
||||||
|
|
||||||
sealed record InstanceServices(ControllerConnection ControllerConnection, BackupManager BackupManager, LaunchServices LaunchServices);
|
sealed record InstanceServices(ControllerConnection ControllerConnection, TaskManager TaskManager, BackupManager BackupManager, LaunchServices LaunchServices);
|
||||||
|
@ -22,7 +22,7 @@ sealed class InstanceLogSender : CancellableBackgroundTask {
|
|||||||
|
|
||||||
private int droppedLinesSinceLastSend;
|
private int droppedLinesSinceLastSend;
|
||||||
|
|
||||||
public InstanceLogSender(ControllerConnection controllerConnection, Guid instanceGuid, string loggerName) : base(PhantomLogger.Create<InstanceLogSender>(loggerName)) {
|
public InstanceLogSender(ControllerConnection controllerConnection, TaskManager taskManager, Guid instanceGuid, string loggerName) : base(PhantomLogger.Create<InstanceLogSender>(loggerName), taskManager, "Instance log sender for " + loggerName) {
|
||||||
this.controllerConnection = controllerConnection;
|
this.controllerConnection = controllerConnection;
|
||||||
this.instanceGuid = instanceGuid;
|
this.instanceGuid = instanceGuid;
|
||||||
this.outputChannel = Channel.CreateBounded<string>(BufferOptions, OnLineDropped);
|
this.outputChannel = Channel.CreateBounded<string>(BufferOptions, OnLineDropped);
|
||||||
|
@ -31,7 +31,7 @@ sealed class InstanceRunningState : IDisposable {
|
|||||||
this.Process = process;
|
this.Process = process;
|
||||||
this.cancellationToken = cancellationToken;
|
this.cancellationToken = cancellationToken;
|
||||||
|
|
||||||
this.logSender = new InstanceLogSender(context.Services.ControllerConnection, context.InstanceGuid, context.ShortName);
|
this.logSender = new InstanceLogSender(context.Services.ControllerConnection, context.Services.TaskManager, context.InstanceGuid, context.ShortName);
|
||||||
|
|
||||||
this.backupScheduler = new BackupScheduler(context, process, configuration.ServerPort);
|
this.backupScheduler = new BackupScheduler(context, process, configuration.ServerPort);
|
||||||
this.backupScheduler.BackupCompleted += OnScheduledBackupCompleted;
|
this.backupScheduler.BackupCompleted += OnScheduledBackupCompleted;
|
||||||
|
@ -7,7 +7,7 @@ public enum EventLogEventType {
|
|||||||
InstanceStopped,
|
InstanceStopped,
|
||||||
InstanceBackupSucceeded,
|
InstanceBackupSucceeded,
|
||||||
InstanceBackupSucceededWithWarnings,
|
InstanceBackupSucceededWithWarnings,
|
||||||
InstanceBackupFailed
|
InstanceBackupFailed,
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class EventLogEventTypeExtensions {
|
public static class EventLogEventTypeExtensions {
|
||||||
@ -18,7 +18,7 @@ public static class EventLogEventTypeExtensions {
|
|||||||
{ EventLogEventType.InstanceStopped, EventLogSubjectType.Instance },
|
{ EventLogEventType.InstanceStopped, EventLogSubjectType.Instance },
|
||||||
{ EventLogEventType.InstanceBackupSucceeded, EventLogSubjectType.Instance },
|
{ EventLogEventType.InstanceBackupSucceeded, EventLogSubjectType.Instance },
|
||||||
{ EventLogEventType.InstanceBackupSucceededWithWarnings, EventLogSubjectType.Instance },
|
{ EventLogEventType.InstanceBackupSucceededWithWarnings, EventLogSubjectType.Instance },
|
||||||
{ EventLogEventType.InstanceBackupFailed, EventLogSubjectType.Instance }
|
{ EventLogEventType.InstanceBackupFailed, EventLogSubjectType.Instance },
|
||||||
};
|
};
|
||||||
|
|
||||||
static EventLogEventTypeExtensions() {
|
static EventLogEventTypeExtensions() {
|
||||||
|
@ -4,7 +4,7 @@ using MemoryPack;
|
|||||||
namespace Phantom.Common.Data.Web.Users;
|
namespace Phantom.Common.Data.Web.Users;
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
public sealed partial record LogInSuccess(
|
public sealed partial record LogInSuccess (
|
||||||
[property: MemoryPackOrder(0)] Guid UserGuid,
|
[property: MemoryPackOrder(0)] Guid UserGuid,
|
||||||
[property: MemoryPackOrder(1)] PermissionSet Permissions,
|
[property: MemoryPackOrder(1)] PermissionSet Permissions,
|
||||||
[property: MemoryPackOrder(2)] ImmutableArray<byte> Token
|
[property: MemoryPackOrder(2)] ImmutableArray<byte> Token
|
||||||
|
@ -14,7 +14,9 @@ public sealed partial class AuthToken {
|
|||||||
private readonly byte[] bytes;
|
private readonly byte[] bytes;
|
||||||
|
|
||||||
internal AuthToken(byte[]? bytes) {
|
internal AuthToken(byte[]? bytes) {
|
||||||
ArgumentNullException.ThrowIfNull(bytes);
|
if (bytes == null) {
|
||||||
|
throw new ArgumentNullException(nameof(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
if (bytes.Length != Length) {
|
if (bytes.Length != Length) {
|
||||||
throw new ArgumentOutOfRangeException(nameof(bytes), "Invalid token length: " + bytes.Length + ". Token length must be exactly " + Length + " bytes.");
|
throw new ArgumentOutOfRangeException(nameof(bytes), "Invalid token length: " + bytes.Length + ". Token length must be exactly " + Length + " bytes.");
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
using System.Collections.Immutable;
|
|
||||||
using MemoryPack;
|
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.Web.ToController;
|
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
|
||||||
public sealed partial record LogOutMessage(
|
|
||||||
[property: MemoryPackOrder(0)] Guid UserGuid,
|
|
||||||
[property: MemoryPackOrder(1)] ImmutableArray<byte> SessionToken
|
|
||||||
) : IMessageToController;
|
|
@ -24,22 +24,21 @@ public static class WebMessageRegistries {
|
|||||||
ToController.Add<RegisterWebMessage>(0);
|
ToController.Add<RegisterWebMessage>(0);
|
||||||
ToController.Add<UnregisterWebMessage>(1);
|
ToController.Add<UnregisterWebMessage>(1);
|
||||||
ToController.Add<LogInMessage, LogInSuccess?>(2);
|
ToController.Add<LogInMessage, LogInSuccess?>(2);
|
||||||
ToController.Add<LogOutMessage>(3);
|
ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(3);
|
||||||
ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(4);
|
ToController.Add<CreateUserMessage, CreateUserResult>(4);
|
||||||
ToController.Add<CreateUserMessage, CreateUserResult>(5);
|
ToController.Add<DeleteUserMessage, DeleteUserResult>(5);
|
||||||
ToController.Add<DeleteUserMessage, DeleteUserResult>(6);
|
ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(6);
|
||||||
ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(7);
|
ToController.Add<GetRolesMessage, ImmutableArray<RoleInfo>>(7);
|
||||||
ToController.Add<GetRolesMessage, ImmutableArray<RoleInfo>>(8);
|
ToController.Add<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(8);
|
||||||
ToController.Add<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(9);
|
ToController.Add<ChangeUserRolesMessage, ChangeUserRolesResult>(9);
|
||||||
ToController.Add<ChangeUserRolesMessage, ChangeUserRolesResult>(10);
|
ToController.Add<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(10);
|
||||||
ToController.Add<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(11);
|
ToController.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(11);
|
||||||
ToController.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(12);
|
ToController.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(12);
|
||||||
ToController.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(13);
|
ToController.Add<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(13);
|
||||||
ToController.Add<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(14);
|
ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(14);
|
||||||
ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(15);
|
ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(15);
|
||||||
ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(16);
|
ToController.Add<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(16);
|
||||||
ToController.Add<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(17);
|
ToController.Add<GetEventLogMessage, ImmutableArray<EventLogItem>>(17);
|
||||||
ToController.Add<GetEventLogMessage, ImmutableArray<EventLogItem>>(18);
|
|
||||||
ToController.Add<ReplyMessage>(127);
|
ToController.Add<ReplyMessage>(127);
|
||||||
|
|
||||||
ToWeb.Add<RegisterWebResultMessage>(0);
|
ToWeb.Add<RegisterWebResultMessage>(0);
|
||||||
|
@ -13,18 +13,18 @@ namespace Phantom.Controller.Database;
|
|||||||
|
|
||||||
[SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")]
|
[SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")]
|
||||||
public class ApplicationDbContext : DbContext {
|
public class ApplicationDbContext : DbContext {
|
||||||
public DbSet<UserEntity> Users { get; init; } = null!;
|
public DbSet<UserEntity> Users { get; set; } = null!;
|
||||||
public DbSet<RoleEntity> Roles { get; init; } = null!;
|
public DbSet<RoleEntity> Roles { get; set; } = null!;
|
||||||
public DbSet<PermissionEntity> Permissions { get; init; } = null!;
|
public DbSet<PermissionEntity> Permissions { get; set; } = null!;
|
||||||
|
|
||||||
public DbSet<UserRoleEntity> UserRoles { get; init; } = null!;
|
public DbSet<UserRoleEntity> UserRoles { get; set; } = null!;
|
||||||
public DbSet<UserPermissionEntity> UserPermissions { get; init; } = null!;
|
public DbSet<UserPermissionEntity> UserPermissions { get; set; } = null!;
|
||||||
public DbSet<RolePermissionEntity> RolePermissions { get; init; } = null!;
|
public DbSet<RolePermissionEntity> RolePermissions { get; set; } = null!;
|
||||||
|
|
||||||
public DbSet<AgentEntity> Agents { get; init; } = null!;
|
public DbSet<AgentEntity> Agents { get; set; } = null!;
|
||||||
public DbSet<InstanceEntity> Instances { get; init; } = null!;
|
public DbSet<InstanceEntity> Instances { get; set; } = null!;
|
||||||
public DbSet<AuditLogEntity> AuditLog { get; init; } = null!;
|
public DbSet<AuditLogEntity> AuditLog { get; set; } = null!;
|
||||||
public DbSet<EventLogEntity> EventLog { get; init; } = null!;
|
public DbSet<EventLogEntity> EventLog { get; set; } = null!;
|
||||||
|
|
||||||
public AgentEntityUpsert AgentUpsert { get; }
|
public AgentEntityUpsert AgentUpsert { get; }
|
||||||
public InstanceEntityUpsert InstanceUpsert { get; }
|
public InstanceEntityUpsert InstanceUpsert { get; }
|
||||||
|
@ -9,7 +9,7 @@ namespace Phantom.Controller.Database.Entities;
|
|||||||
[SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")]
|
[SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")]
|
||||||
public sealed class AgentEntity {
|
public sealed class AgentEntity {
|
||||||
[Key]
|
[Key]
|
||||||
public Guid AgentGuid { get; init; }
|
public Guid AgentGuid { get; set; }
|
||||||
|
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public ushort ProtocolVersion { get; set; }
|
public ushort ProtocolVersion { get; set; }
|
||||||
|
@ -13,16 +13,16 @@ public class AuditLogEntity : IDisposable {
|
|||||||
[Key]
|
[Key]
|
||||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||||
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||||
public long Id { get; init; }
|
public long Id { get; set; }
|
||||||
|
|
||||||
public Guid? UserGuid { get; init; }
|
public Guid? UserGuid { get; set; }
|
||||||
public DateTime UtcTime { get; init; } // Note: Converting to UTC is not best practice, but for historical records it's good enough.
|
public DateTime UtcTime { get; set; } // Note: Converting to UTC is not best practice, but for historical records it's good enough.
|
||||||
public AuditLogEventType EventType { get; init; }
|
public AuditLogEventType EventType { get; set; }
|
||||||
public AuditLogSubjectType SubjectType { get; init; }
|
public AuditLogSubjectType SubjectType { get; set; }
|
||||||
public string SubjectId { get; init; }
|
public string SubjectId { get; set; }
|
||||||
public JsonDocument? Data { get; init; }
|
public JsonDocument? Data { get; set; }
|
||||||
|
|
||||||
public virtual UserEntity? User { get; init; }
|
public virtual UserEntity? User { get; set; }
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||||
internal AuditLogEntity() {
|
internal AuditLogEntity() {
|
||||||
|
@ -11,14 +11,14 @@ namespace Phantom.Controller.Database.Entities;
|
|||||||
[SuppressMessage("ReSharper", "ClassWithVirtualMembersNeverInherited.Global")]
|
[SuppressMessage("ReSharper", "ClassWithVirtualMembersNeverInherited.Global")]
|
||||||
public sealed class EventLogEntity : IDisposable {
|
public sealed class EventLogEntity : IDisposable {
|
||||||
[Key]
|
[Key]
|
||||||
public Guid EventGuid { get; init; }
|
public Guid EventGuid { get; set; }
|
||||||
|
|
||||||
public DateTime UtcTime { get; init; } // Note: Converting to UTC is not best practice, but for historical records it's good enough.
|
public DateTime UtcTime { get; set; } // Note: Converting to UTC is not best practice, but for historical records it's good enough.
|
||||||
public Guid? AgentGuid { get; init; }
|
public Guid? AgentGuid { get; set; }
|
||||||
public EventLogEventType EventType { get; init; }
|
public EventLogEventType EventType { get; set; }
|
||||||
public EventLogSubjectType SubjectType { get; init; }
|
public EventLogSubjectType SubjectType { get; set; }
|
||||||
public string SubjectId { get; init; }
|
public string SubjectId { get; set; }
|
||||||
public JsonDocument? Data { get; init; }
|
public JsonDocument? Data { get; set; }
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||||
internal EventLogEntity() {
|
internal EventLogEntity() {
|
||||||
|
@ -11,7 +11,7 @@ namespace Phantom.Controller.Database.Entities;
|
|||||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
|
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
|
||||||
public sealed class InstanceEntity {
|
public sealed class InstanceEntity {
|
||||||
[Key]
|
[Key]
|
||||||
public Guid InstanceGuid { get; init; }
|
public Guid InstanceGuid { get; set; }
|
||||||
|
|
||||||
public Guid AgentGuid { get; set; }
|
public Guid AgentGuid { get; set; }
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ namespace Phantom.Controller.Database.Entities;
|
|||||||
[Table("Permissions", Schema = "identity")]
|
[Table("Permissions", Schema = "identity")]
|
||||||
public sealed class PermissionEntity {
|
public sealed class PermissionEntity {
|
||||||
[Key]
|
[Key]
|
||||||
public string Id { get; init; }
|
public string Id { get; set; }
|
||||||
|
|
||||||
public PermissionEntity(string id) {
|
public PermissionEntity(string id) {
|
||||||
Id = id;
|
Id = id;
|
||||||
|
@ -7,9 +7,9 @@ namespace Phantom.Controller.Database.Entities;
|
|||||||
[Table("Roles", Schema = "identity")]
|
[Table("Roles", Schema = "identity")]
|
||||||
public sealed class RoleEntity {
|
public sealed class RoleEntity {
|
||||||
[Key]
|
[Key]
|
||||||
public Guid RoleGuid { get; init; }
|
public Guid RoleGuid { get; set; }
|
||||||
|
|
||||||
public string Name { get; init; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
public RoleEntity(Guid roleGuid, string name) {
|
public RoleEntity(Guid roleGuid, string name) {
|
||||||
RoleGuid = roleGuid;
|
RoleGuid = roleGuid;
|
||||||
|
@ -4,8 +4,8 @@ namespace Phantom.Controller.Database.Entities;
|
|||||||
|
|
||||||
[Table("RolePermissions", Schema = "identity")]
|
[Table("RolePermissions", Schema = "identity")]
|
||||||
public sealed class RolePermissionEntity {
|
public sealed class RolePermissionEntity {
|
||||||
public Guid RoleGuid { get; init; }
|
public Guid RoleGuid { get; set; }
|
||||||
public string PermissionId { get; init; }
|
public string PermissionId { get; set; }
|
||||||
|
|
||||||
public RolePermissionEntity(Guid roleGuid, string permissionId) {
|
public RolePermissionEntity(Guid roleGuid, string permissionId) {
|
||||||
RoleGuid = roleGuid;
|
RoleGuid = roleGuid;
|
||||||
|
@ -7,9 +7,9 @@ namespace Phantom.Controller.Database.Entities;
|
|||||||
[Table("Users", Schema = "identity")]
|
[Table("Users", Schema = "identity")]
|
||||||
public sealed class UserEntity {
|
public sealed class UserEntity {
|
||||||
[Key]
|
[Key]
|
||||||
public Guid UserGuid { get; init; }
|
public Guid UserGuid { get; set; }
|
||||||
|
|
||||||
public string Name { get; init; }
|
public string Name { get; set; }
|
||||||
public string PasswordHash { get; set; }
|
public string PasswordHash { get; set; }
|
||||||
|
|
||||||
public UserEntity(Guid userGuid, string name, string passwordHash) {
|
public UserEntity(Guid userGuid, string name, string passwordHash) {
|
||||||
|
@ -4,8 +4,8 @@ namespace Phantom.Controller.Database.Entities;
|
|||||||
|
|
||||||
[Table("UserPermissions", Schema = "identity")]
|
[Table("UserPermissions", Schema = "identity")]
|
||||||
public sealed class UserPermissionEntity {
|
public sealed class UserPermissionEntity {
|
||||||
public Guid UserGuid { get; init; }
|
public Guid UserGuid { get; set; }
|
||||||
public string PermissionId { get; init; }
|
public string PermissionId { get; set; }
|
||||||
|
|
||||||
public UserPermissionEntity(Guid userGuid, string permissionId) {
|
public UserPermissionEntity(Guid userGuid, string permissionId) {
|
||||||
UserGuid = userGuid;
|
UserGuid = userGuid;
|
||||||
|
@ -4,11 +4,11 @@ namespace Phantom.Controller.Database.Entities;
|
|||||||
|
|
||||||
[Table("UserRoles", Schema = "identity")]
|
[Table("UserRoles", Schema = "identity")]
|
||||||
public sealed class UserRoleEntity {
|
public sealed class UserRoleEntity {
|
||||||
public Guid UserGuid { get; init; }
|
public Guid UserGuid { get; set; }
|
||||||
public Guid RoleGuid { get; init; }
|
public Guid RoleGuid { get; set; }
|
||||||
|
|
||||||
public UserEntity User { get; init; }
|
public UserEntity User { get; set; }
|
||||||
public RoleEntity Role { get; init; }
|
public RoleEntity Role { get; set; }
|
||||||
|
|
||||||
public UserRoleEntity(Guid userGuid, Guid roleGuid) {
|
public UserRoleEntity(Guid userGuid, Guid roleGuid) {
|
||||||
UserGuid = userGuid;
|
UserGuid = userGuid;
|
||||||
|
@ -59,7 +59,7 @@ public sealed class ControllerServices : IDisposable {
|
|||||||
this.PermissionManager = new PermissionManager(dbProvider);
|
this.PermissionManager = new PermissionManager(dbProvider);
|
||||||
|
|
||||||
this.UserRoleManager = new UserRoleManager(dbProvider);
|
this.UserRoleManager = new UserRoleManager(dbProvider);
|
||||||
this.UserLoginManager = new UserLoginManager(UserManager, PermissionManager, dbProvider);
|
this.UserLoginManager = new UserLoginManager(UserManager, PermissionManager);
|
||||||
this.AuditLogManager = new AuditLogManager(dbProvider);
|
this.AuditLogManager = new AuditLogManager(dbProvider);
|
||||||
this.EventLogManager = new EventLogManager(ActorSystem, dbProvider, shutdownCancellationToken);
|
this.EventLogManager = new EventLogManager(ActorSystem, dbProvider, shutdownCancellationToken);
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Phantom.Common.Data.Replies;
|
using Akka.Actor;
|
||||||
|
using Phantom.Common.Data.Replies;
|
||||||
using Phantom.Common.Messages.Agent;
|
using Phantom.Common.Messages.Agent;
|
||||||
using Phantom.Common.Messages.Agent.BiDirectional;
|
using Phantom.Common.Messages.Agent.BiDirectional;
|
||||||
using Phantom.Common.Messages.Agent.ToAgent;
|
using Phantom.Common.Messages.Agent.ToAgent;
|
||||||
@ -18,6 +19,8 @@ sealed class AgentMessageHandlerActor : ReceiveActor<IMessageToController> {
|
|||||||
return Props<IMessageToController>.Create(() => new AgentMessageHandlerActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume });
|
return Props<IMessageToController>.Create(() => new AgentMessageHandlerActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IStash Stash { get; set; } = null!;
|
||||||
|
|
||||||
private readonly Guid agentGuid;
|
private readonly Guid agentGuid;
|
||||||
private readonly RpcConnectionToClient<IMessageToAgent> connection;
|
private readonly RpcConnectionToClient<IMessageToAgent> connection;
|
||||||
private readonly AgentRegistrationHandler agentRegistrationHandler;
|
private readonly AgentRegistrationHandler agentRegistrationHandler;
|
||||||
|
@ -69,8 +69,6 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
|
|||||||
|
|
||||||
ReceiveAsync<RegisterWebMessage>(HandleRegisterWeb);
|
ReceiveAsync<RegisterWebMessage>(HandleRegisterWeb);
|
||||||
Receive<UnregisterWebMessage>(HandleUnregisterWeb);
|
Receive<UnregisterWebMessage>(HandleUnregisterWeb);
|
||||||
ReceiveAndReplyLater<LogInMessage, LogInSuccess?>(HandleLogIn);
|
|
||||||
Receive<LogOutMessage>(HandleLogOut);
|
|
||||||
ReceiveAndReplyLater<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(HandleCreateOrUpdateAdministratorUser);
|
ReceiveAndReplyLater<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(HandleCreateOrUpdateAdministratorUser);
|
||||||
ReceiveAndReplyLater<CreateUserMessage, CreateUserResult>(HandleCreateUser);
|
ReceiveAndReplyLater<CreateUserMessage, CreateUserResult>(HandleCreateUser);
|
||||||
ReceiveAndReplyLater<GetUsersMessage, ImmutableArray<UserInfo>>(HandleGetUsers);
|
ReceiveAndReplyLater<GetUsersMessage, ImmutableArray<UserInfo>>(HandleGetUsers);
|
||||||
@ -86,6 +84,7 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
|
|||||||
ReceiveAndReply<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(HandleGetAgentJavaRuntimes);
|
ReceiveAndReply<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(HandleGetAgentJavaRuntimes);
|
||||||
ReceiveAndReplyLater<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(HandleGetAuditLog);
|
ReceiveAndReplyLater<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(HandleGetAuditLog);
|
||||||
ReceiveAndReplyLater<GetEventLogMessage, ImmutableArray<EventLogItem>>(HandleGetEventLog);
|
ReceiveAndReplyLater<GetEventLogMessage, ImmutableArray<EventLogItem>>(HandleGetEventLog);
|
||||||
|
ReceiveAndReplyLater<LogInMessage, LogInSuccess?>(HandleLogIn);
|
||||||
Receive<ReplyMessage>(HandleReply);
|
Receive<ReplyMessage>(HandleReply);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,14 +96,6 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
|
|||||||
connection.Close();
|
connection.Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<LogInSuccess?> HandleLogIn(LogInMessage message) {
|
|
||||||
return userLoginManager.LogIn(message.Username, message.Password);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleLogOut(LogOutMessage message) {
|
|
||||||
_ = userLoginManager.LogOut(message.UserGuid, message.SessionToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task<CreateOrUpdateAdministratorUserResult> HandleCreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUserMessage message) {
|
private Task<CreateOrUpdateAdministratorUserResult> HandleCreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUserMessage message) {
|
||||||
return userManager.CreateOrUpdateAdministrator(message.Username, message.Password);
|
return userManager.CreateOrUpdateAdministrator(message.Username, message.Password);
|
||||||
}
|
}
|
||||||
@ -165,6 +156,10 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
|
|||||||
return eventLogManager.GetMostRecentItems(message.Count);
|
return eventLogManager.GetMostRecentItems(message.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Task<LogInSuccess?> HandleLogIn(LogInMessage message) {
|
||||||
|
return userLoginManager.LogIn(message.Username, message.Password);
|
||||||
|
}
|
||||||
|
|
||||||
private void HandleReply(ReplyMessage message) {
|
private void HandleReply(ReplyMessage message) {
|
||||||
connection.Receive(message);
|
connection.Receive(message);
|
||||||
}
|
}
|
||||||
|
@ -2,23 +2,19 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using Phantom.Common.Data.Web.Users;
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Controller.Database;
|
|
||||||
using Phantom.Controller.Database.Repositories;
|
|
||||||
|
|
||||||
namespace Phantom.Controller.Services.Users;
|
namespace Phantom.Controller.Services.Users;
|
||||||
|
|
||||||
sealed class UserLoginManager {
|
sealed class UserLoginManager {
|
||||||
private const int SessionIdBytes = 20;
|
private const int SessionIdBytes = 20;
|
||||||
private readonly ConcurrentDictionary<Guid, List<ImmutableArray<byte>>> sessionTokensByUserGuid = new ();
|
private readonly ConcurrentDictionary<string, List<ImmutableArray<byte>>> sessionTokensByUsername = new ();
|
||||||
|
|
||||||
private readonly UserManager userManager;
|
private readonly UserManager userManager;
|
||||||
private readonly PermissionManager permissionManager;
|
private readonly PermissionManager permissionManager;
|
||||||
private readonly IDbContextProvider dbProvider;
|
|
||||||
|
|
||||||
public UserLoginManager(UserManager userManager, PermissionManager permissionManager, IDbContextProvider dbProvider) {
|
public UserLoginManager(UserManager userManager, PermissionManager permissionManager) {
|
||||||
this.userManager = userManager;
|
this.userManager = userManager;
|
||||||
this.permissionManager = permissionManager;
|
this.permissionManager = permissionManager;
|
||||||
this.dbProvider = dbProvider;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<LogInSuccess?> LogIn(string username, string password) {
|
public async Task<LogInSuccess?> LogIn(string username, string password) {
|
||||||
@ -28,37 +24,11 @@ sealed class UserLoginManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var token = ImmutableArray.Create(RandomNumberGenerator.GetBytes(SessionIdBytes));
|
var token = ImmutableArray.Create(RandomNumberGenerator.GetBytes(SessionIdBytes));
|
||||||
var sessionTokens = sessionTokensByUserGuid.GetOrAdd(user.UserGuid, static _ => new List<ImmutableArray<byte>>());
|
var sessionTokens = sessionTokensByUsername.GetOrAdd(username, static _ => new List<ImmutableArray<byte>>());
|
||||||
lock (sessionTokens) {
|
lock (sessionTokens) {
|
||||||
sessionTokens.Add(token);
|
sessionTokens.Add(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
await using (var db = dbProvider.Lazy()) {
|
|
||||||
var auditLogWriter = new AuditLogRepository(db).Writer(user.UserGuid);
|
|
||||||
auditLogWriter.UserLoggedIn(user);
|
|
||||||
|
|
||||||
await db.Ctx.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new LogInSuccess(user.UserGuid, await permissionManager.FetchPermissionsForUserId(user.UserGuid), token);
|
return new LogInSuccess(user.UserGuid, await permissionManager.FetchPermissionsForUserId(user.UserGuid), token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LogOut(Guid userGuid, ImmutableArray<byte> sessionToken) {
|
|
||||||
if (!sessionTokensByUserGuid.TryGetValue(userGuid, out var sessionTokens)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (sessionTokens) {
|
|
||||||
if (sessionTokens.RemoveAll(token => token.SequenceEqual(sessionToken)) == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await using var db = dbProvider.Lazy();
|
|
||||||
|
|
||||||
var auditLogWriter = new AuditLogRepository(db).Writer(userGuid);
|
|
||||||
auditLogWriter.UserLoggedOut(userGuid);
|
|
||||||
|
|
||||||
await db.Ctx.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ using Phantom.Utils.Logging;
|
|||||||
using Phantom.Utils.Rpc;
|
using Phantom.Utils.Rpc;
|
||||||
using Phantom.Utils.Rpc.Runtime;
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
using Phantom.Utils.Runtime;
|
using Phantom.Utils.Runtime;
|
||||||
|
using Phantom.Utils.Tasks;
|
||||||
|
|
||||||
var shutdownCancellationTokenSource = new CancellationTokenSource();
|
var shutdownCancellationTokenSource = new CancellationTokenSource();
|
||||||
var shutdownCancellationToken = shutdownCancellationTokenSource.Token;
|
var shutdownCancellationToken = shutdownCancellationTokenSource.Token;
|
||||||
@ -63,12 +64,14 @@ try {
|
|||||||
return new RpcConfiguration(serviceName, host, port, connectionKey.Certificate);
|
return new RpcConfiguration(serviceName, host, port, connectionKey.Certificate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var rpcTaskManager = new TaskManager(PhantomLogger.Create<TaskManager>("Rpc"));
|
||||||
try {
|
try {
|
||||||
await Task.WhenAll(
|
await Task.WhenAll(
|
||||||
RpcServerRuntime.Launch(ConfigureRpc("Agent", agentRpcServerHost, agentRpcServerPort, agentKeyData), AgentMessageRegistries.Definitions, controllerServices.AgentRegistrationHandler, controllerServices.ActorSystem, shutdownCancellationToken),
|
RpcServerRuntime.Launch(ConfigureRpc("Agent", agentRpcServerHost, agentRpcServerPort, agentKeyData), AgentMessageRegistries.Definitions, controllerServices.AgentRegistrationHandler, controllerServices.ActorSystem, shutdownCancellationToken),
|
||||||
RpcServerRuntime.Launch(ConfigureRpc("Web", webRpcServerHost, webRpcServerPort, webKeyData), WebMessageRegistries.Definitions, controllerServices.WebRegistrationHandler, controllerServices.ActorSystem, shutdownCancellationToken)
|
RpcServerRuntime.Launch(ConfigureRpc("Web", webRpcServerHost, webRpcServerPort, webKeyData), WebMessageRegistries.Definitions, controllerServices.WebRegistrationHandler, controllerServices.ActorSystem, shutdownCancellationToken)
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
|
await rpcTaskManager.Stop();
|
||||||
NetMQConfig.Cleanup();
|
NetMQConfig.Cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using Akka.Actor;
|
||||||
using Akka.Actor;
|
|
||||||
using Akka.Configuration;
|
using Akka.Configuration;
|
||||||
using Akka.Dispatch;
|
using Akka.Dispatch;
|
||||||
using Akka.Dispatch.MessageQueues;
|
using Akka.Dispatch.MessageQueues;
|
||||||
|
|
||||||
namespace Phantom.Utils.Actor.Mailbox;
|
namespace Phantom.Utils.Actor.Mailbox;
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
|
|
||||||
public sealed class UnboundedJumpAheadMailbox : MailboxType, IProducesMessageQueue<UnboundedJumpAheadMessageQueue> {
|
public sealed class UnboundedJumpAheadMailbox : MailboxType, IProducesMessageQueue<UnboundedJumpAheadMessageQueue> {
|
||||||
public const string Name = "unbounded-jump-ahead-mailbox";
|
public const string Name = "unbounded-jump-ahead-mailbox";
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ public sealed class RpcConnectionToClient<TMessageBase> : RpcConnection<TMessage
|
|||||||
private readonly uint routingId;
|
private readonly uint routingId;
|
||||||
|
|
||||||
internal event EventHandler<RpcClientConnectionClosedEventArgs>? Closed;
|
internal event EventHandler<RpcClientConnectionClosedEventArgs>? Closed;
|
||||||
private bool isClosed;
|
public bool IsClosed { get; private set; }
|
||||||
|
|
||||||
internal RpcConnectionToClient(ServerSocket socket, uint routingId, MessageRegistry<TMessageBase> messageRegistry, MessageReplyTracker replyTracker) : base(messageRegistry, replyTracker) {
|
internal RpcConnectionToClient(ServerSocket socket, uint routingId, MessageRegistry<TMessageBase> messageRegistry, MessageReplyTracker replyTracker) : base(messageRegistry, replyTracker) {
|
||||||
this.socket = socket;
|
this.socket = socket;
|
||||||
@ -24,8 +24,8 @@ public sealed class RpcConnectionToClient<TMessageBase> : RpcConnection<TMessage
|
|||||||
bool hasClosed = false;
|
bool hasClosed = false;
|
||||||
|
|
||||||
lock (this) {
|
lock (this) {
|
||||||
if (!isClosed) {
|
if (!IsClosed) {
|
||||||
isClosed = true;
|
IsClosed = true;
|
||||||
hasClosed = true;
|
hasClosed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ public class EnvironmentVariablesTests {
|
|||||||
private readonly HashSet<string> createdVariables = new ();
|
private readonly HashSet<string> createdVariables = new ();
|
||||||
|
|
||||||
private static void Discard<T>(T value) {
|
private static void Discard<T>(T value) {
|
||||||
_ = value;
|
var _ = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string CreateVariable(string value) {
|
private string CreateVariable(string value) {
|
||||||
|
@ -1,8 +1,18 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
namespace Phantom.Utils.Collections;
|
namespace Phantom.Utils.Collections;
|
||||||
|
|
||||||
public static class EnumerableExtensions {
|
public static class EnumerableExtensions {
|
||||||
|
[SuppressMessage("ReSharper", "LoopCanBeConvertedToQuery")]
|
||||||
|
public static IEnumerable<TSource> WhereNotNull<TSource>(this IEnumerable<TSource?> items) {
|
||||||
|
foreach (var item in items) {
|
||||||
|
if (item is not null) {
|
||||||
|
yield return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task<ImmutableArray<TSource>> ToImmutableArrayAsync<TSource>(this IAsyncEnumerable<TSource> source, CancellationToken cancellationToken = default) {
|
public static async Task<ImmutableArray<TSource>> ToImmutableArrayAsync<TSource>(this IAsyncEnumerable<TSource> source, CancellationToken cancellationToken = default) {
|
||||||
var builder = ImmutableArray.CreateBuilder<TSource>();
|
var builder = ImmutableArray.CreateBuilder<TSource>();
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis;
|
|||||||
namespace Phantom.Utils.Collections;
|
namespace Phantom.Utils.Collections;
|
||||||
|
|
||||||
public sealed class TableData<TRow, TKey> : IReadOnlyList<TRow>, IReadOnlyDictionary<TKey, TRow> where TRow : notnull where TKey : notnull {
|
public sealed class TableData<TRow, TKey> : IReadOnlyList<TRow>, IReadOnlyDictionary<TKey, TRow> where TRow : notnull where TKey : notnull {
|
||||||
private readonly List<TRow> rowList = new ();
|
private readonly List<TRow> rowList = new();
|
||||||
private readonly Dictionary<TKey, TRow> rowDictionary = new ();
|
private readonly Dictionary<TKey, TRow> rowDictionary = new ();
|
||||||
|
|
||||||
public TRow this[int index] => rowList[index];
|
public TRow this[int index] => rowList[index];
|
||||||
|
@ -8,13 +8,19 @@ public abstract class CancellableBackgroundTask {
|
|||||||
protected ILogger Logger { get; }
|
protected ILogger Logger { get; }
|
||||||
protected CancellationToken CancellationToken { get; }
|
protected CancellationToken CancellationToken { get; }
|
||||||
|
|
||||||
protected CancellableBackgroundTask(ILogger logger) {
|
private readonly TaskManager taskManager;
|
||||||
|
private readonly string taskName;
|
||||||
|
|
||||||
|
protected CancellableBackgroundTask(ILogger logger, TaskManager taskManager, string taskName) {
|
||||||
this.Logger = logger;
|
this.Logger = logger;
|
||||||
this.CancellationToken = cancellationTokenSource.Token;
|
this.CancellationToken = cancellationTokenSource.Token;
|
||||||
|
|
||||||
|
this.taskManager = taskManager;
|
||||||
|
this.taskName = taskName;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void Start() {
|
protected void Start() {
|
||||||
Task.Run(Run, CancellationToken.None);
|
taskManager.Run(taskName, Run);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Run() {
|
private async Task Run() {
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
namespace Phantom.Utils.Tasks;
|
||||||
|
|
||||||
namespace Phantom.Utils.Tasks;
|
|
||||||
|
|
||||||
public abstract record Result<TValue, TError> {
|
public abstract record Result<TValue, TError> {
|
||||||
private Result() {}
|
private Result() {}
|
||||||
@ -44,7 +42,7 @@ public abstract record Result<TError> {
|
|||||||
return new Fail(error);
|
return new Fail(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static implicit operator Result<TError>([SuppressMessage("ReSharper", "UnusedParameter.Global")] Result.OkType _) {
|
public static implicit operator Result<TError>(Result.OkType _) {
|
||||||
return new Ok();
|
return new Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
76
Utils/Phantom.Utils/Tasks/TaskManager.cs
Normal file
76
Utils/Phantom.Utils/Tasks/TaskManager.cs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using Phantom.Utils.Collections;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace Phantom.Utils.Tasks;
|
||||||
|
|
||||||
|
public sealed class TaskManager {
|
||||||
|
private readonly ILogger logger;
|
||||||
|
private readonly CancellationTokenSource cancellationTokenSource = new ();
|
||||||
|
private readonly CancellationToken cancellationToken;
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<Task, string> runningTasks = new (ReferenceEqualityComparer<Task>.Instance);
|
||||||
|
|
||||||
|
public TaskManager(ILogger logger) {
|
||||||
|
this.logger = logger;
|
||||||
|
this.cancellationToken = cancellationTokenSource.Token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private T Add<T>(string name, T task) where T : Task {
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
runningTasks.TryAdd(task, name);
|
||||||
|
task.ContinueWith(OnFinished, CancellationToken.None, TaskContinuationOptions.RunContinuationsAsynchronously, TaskScheduler.Default);
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFinished(Task task) {
|
||||||
|
runningTasks.TryRemove(task, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Run(string name, Action action) {
|
||||||
|
return Add(name, Task.Run(action, cancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Run(string name, Func<Task> taskFunc) {
|
||||||
|
return Add(name, Task.Run(taskFunc, cancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<T> Run<T>(string name, Func<Task<T>> taskFunc) {
|
||||||
|
return Add(name, Task.Run(taskFunc, cancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Stop() {
|
||||||
|
logger.Information("Stopping task manager...");
|
||||||
|
|
||||||
|
cancellationTokenSource.Cancel();
|
||||||
|
|
||||||
|
var remainingTasksAwaiterTask = WaitForRemainingTasks();
|
||||||
|
while (true) {
|
||||||
|
var logStateTimeoutTask = Task.Delay(TimeSpan.FromSeconds(10), CancellationToken.None);
|
||||||
|
var completedTask = await Task.WhenAny(remainingTasksAwaiterTask, logStateTimeoutTask);
|
||||||
|
if (completedTask == logStateTimeoutTask) {
|
||||||
|
var remainingTaskNames = runningTasks.Values.Order().ToList();
|
||||||
|
var remainingTaskNameList = string.Join('\n', remainingTaskNames.Select(static name => "- " + name));
|
||||||
|
logger.Warning("Waiting for {TaskCount} task(s) to finish:\n{TaskNames}", remainingTaskNames.Count, remainingTaskNameList);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runningTasks.Clear();
|
||||||
|
cancellationTokenSource.Dispose();
|
||||||
|
|
||||||
|
logger.Information("Task manager stopped.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WaitForRemainingTasks() {
|
||||||
|
foreach (var task in runningTasks.Keys) {
|
||||||
|
try {
|
||||||
|
await task;
|
||||||
|
} catch (Exception) {
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
Utils/Phantom.Utils/Threading/ThreadSafeLinkedList.cs
Normal file
39
Utils/Phantom.Utils/Threading/ThreadSafeLinkedList.cs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
namespace Phantom.Utils.Threading;
|
||||||
|
|
||||||
|
public sealed class ThreadSafeLinkedList<T> : IDisposable {
|
||||||
|
private readonly LinkedList<T> list = new ();
|
||||||
|
private readonly SemaphoreSlim semaphore = new (1, 1);
|
||||||
|
|
||||||
|
public async Task Add(T item, bool toFront, CancellationToken cancellationToken) {
|
||||||
|
await semaphore.WaitAsync(cancellationToken);
|
||||||
|
try {
|
||||||
|
if (toFront) {
|
||||||
|
list.AddFirst(item);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
list.AddLast(item);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<T?> TryTakeFromFront(CancellationToken cancellationToken) {
|
||||||
|
await semaphore.WaitAsync(cancellationToken);
|
||||||
|
try {
|
||||||
|
var firstNode = list.First;
|
||||||
|
if (firstNode == null) {
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.RemoveFirst();
|
||||||
|
return firstNode.Value;
|
||||||
|
} finally {
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
semaphore.Dispose();
|
||||||
|
}
|
||||||
|
}
|
28
Utils/Phantom.Utils/Threading/ThreadSafeStructRef.cs
Normal file
28
Utils/Phantom.Utils/Threading/ThreadSafeStructRef.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
namespace Phantom.Utils.Threading;
|
||||||
|
|
||||||
|
public sealed class ThreadSafeStructRef<T> : IDisposable where T : struct {
|
||||||
|
private T? value;
|
||||||
|
private readonly SemaphoreSlim semaphore = new (1, 1);
|
||||||
|
|
||||||
|
public async Task<T?> Get(CancellationToken cancellationToken) {
|
||||||
|
await semaphore.WaitAsync(cancellationToken);
|
||||||
|
try {
|
||||||
|
return value;
|
||||||
|
} finally {
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Set(T? value, CancellationToken cancellationToken) {
|
||||||
|
await semaphore.WaitAsync(cancellationToken);
|
||||||
|
try {
|
||||||
|
this.value = value;
|
||||||
|
} finally {
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
semaphore.Dispose();
|
||||||
|
}
|
||||||
|
}
|
@ -53,8 +53,8 @@ public sealed class UserLoginManager {
|
|||||||
|
|
||||||
public async Task LogOut() {
|
public async Task LogOut() {
|
||||||
var stored = await sessionBrowserStorage.Delete();
|
var stored = await sessionBrowserStorage.Delete();
|
||||||
if (stored != null && sessionManager.Remove(stored.UserGuid, stored.Token)) {
|
if (stored != null) {
|
||||||
await controllerConnection.Send(new LogOutMessage(stored.UserGuid, stored.Token));
|
sessionManager.Remove(stored.UserGuid, stored.Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
await navigation.NavigateTo(string.Empty);
|
await navigation.NavigateTo(string.Empty);
|
||||||
|
@ -23,13 +23,9 @@ public sealed class UserSessionManager {
|
|||||||
return userSessions.TryGetValue(userGuid, out var sessions) && sessions.HasToken(token) ? sessions.UserInfo : null;
|
return userSessions.TryGetValue(userGuid, out var sessions) && sessions.HasToken(token) ? sessions.UserInfo : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal bool Remove(Guid userGuid, ImmutableArray<byte> token) {
|
internal void Remove(Guid userGuid, ImmutableArray<byte> token) {
|
||||||
if (userSessions.TryGetValue(userGuid, out var sessions)) {
|
if (userSessions.TryGetValue(userGuid, out var sessions)) {
|
||||||
sessions.RemoveToken(token);
|
sessions.RemoveToken(token);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ using Phantom.Utils.Logging;
|
|||||||
using Phantom.Utils.Rpc;
|
using Phantom.Utils.Rpc;
|
||||||
using Phantom.Utils.Rpc.Sockets;
|
using Phantom.Utils.Rpc.Sockets;
|
||||||
using Phantom.Utils.Runtime;
|
using Phantom.Utils.Runtime;
|
||||||
|
using Phantom.Utils.Tasks;
|
||||||
using Phantom.Web;
|
using Phantom.Web;
|
||||||
using Phantom.Web.Services;
|
using Phantom.Web.Services;
|
||||||
using Phantom.Web.Services.Rpc;
|
using Phantom.Web.Services.Rpc;
|
||||||
@ -58,7 +59,8 @@ try {
|
|||||||
var rpcSocket = RpcClientSocket.Connect(rpcConfiguration, WebMessageRegistries.Definitions, new RegisterWebMessage(webToken));
|
var rpcSocket = RpcClientSocket.Connect(rpcConfiguration, WebMessageRegistries.Definitions, new RegisterWebMessage(webToken));
|
||||||
|
|
||||||
var webConfiguration = new WebLauncher.Configuration(PhantomLogger.Create("Web"), webServerHost, webServerPort, webBasePath, dataProtectionKeysPath, shutdownCancellationToken);
|
var webConfiguration = new WebLauncher.Configuration(PhantomLogger.Create("Web"), webServerHost, webServerPort, webBasePath, dataProtectionKeysPath, shutdownCancellationToken);
|
||||||
var webApplication = WebLauncher.CreateApplication(webConfiguration, applicationProperties, rpcSocket.Connection);
|
var taskManager = new TaskManager(PhantomLogger.Create<TaskManager>("Web"));
|
||||||
|
var webApplication = WebLauncher.CreateApplication(webConfiguration, taskManager, applicationProperties, rpcSocket.Connection);
|
||||||
|
|
||||||
using var actorSystem = ActorSystemFactory.Create("Web");
|
using var actorSystem = ActorSystemFactory.Create("Web");
|
||||||
|
|
||||||
@ -86,6 +88,7 @@ try {
|
|||||||
await WebLauncher.Launch(webConfiguration, webApplication);
|
await WebLauncher.Launch(webConfiguration, webApplication);
|
||||||
} finally {
|
} finally {
|
||||||
shutdownCancellationTokenSource.Cancel();
|
shutdownCancellationTokenSource.Cancel();
|
||||||
|
await taskManager.Stop();
|
||||||
|
|
||||||
rpcDisconnectSemaphore.Release();
|
rpcDisconnectSemaphore.Release();
|
||||||
await rpcTask;
|
await rpcTask;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.DataProtection;
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
using Phantom.Common.Messages.Web;
|
using Phantom.Common.Messages.Web;
|
||||||
using Phantom.Utils.Rpc.Runtime;
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
|
using Phantom.Utils.Tasks;
|
||||||
using Phantom.Web.Services;
|
using Phantom.Web.Services;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using ILogger = Serilog.ILogger;
|
using ILogger = Serilog.ILogger;
|
||||||
@ -12,7 +13,7 @@ static class WebLauncher {
|
|||||||
public string HttpUrl => "http://" + Host + ":" + Port;
|
public string HttpUrl => "http://" + Host + ":" + Port;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static WebApplication CreateApplication(Configuration config, ApplicationProperties applicationProperties, RpcConnectionToServer<IMessageToController> controllerConnection) {
|
internal static WebApplication CreateApplication(Configuration config, TaskManager taskManager, ApplicationProperties applicationProperties, RpcConnectionToServer<IMessageToController> controllerConnection) {
|
||||||
var assembly = typeof(WebLauncher).Assembly;
|
var assembly = typeof(WebLauncher).Assembly;
|
||||||
var builder = WebApplication.CreateBuilder(new WebApplicationOptions {
|
var builder = WebApplication.CreateBuilder(new WebApplicationOptions {
|
||||||
ApplicationName = assembly.GetName().Name,
|
ApplicationName = assembly.GetName().Name,
|
||||||
@ -28,6 +29,7 @@ static class WebLauncher {
|
|||||||
builder.WebHost.UseStaticWebAssets();
|
builder.WebHost.UseStaticWebAssets();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
builder.Services.AddSingleton(taskManager);
|
||||||
builder.Services.AddSingleton(applicationProperties);
|
builder.Services.AddSingleton(applicationProperties);
|
||||||
builder.Services.AddSingleton(controllerConnection);
|
builder.Services.AddSingleton(controllerConnection);
|
||||||
builder.Services.AddPhantomServices();
|
builder.Services.AddPhantomServices();
|
||||||
|
Loading…
Reference in New Issue
Block a user