mirror of
				https://github.com/chylex/Minecraft-Phantom-Panel.git
				synced 2025-10-31 02:17:16 +01:00 
			
		
		
		
	Compare commits
	
		
			3 Commits
		
	
	
		
			94148add2d
			...
			d591318340
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| d591318340 | |||
| c7b57fac97 | |||
| 137a2a53c3 | 
| @@ -6,7 +6,6 @@ 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; | ||||||
| @@ -18,7 +17,6 @@ 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; } | ||||||
| @@ -30,13 +28,12 @@ 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, TaskManager, BackupManager); | 		var instanceManagerInit = new InstanceManagerActor.Init(controllerConnection, agentFolders, AgentState, JavaRuntimeRepository, InstanceTicketManager, BackupManager); | ||||||
| 		this.InstanceManager = ActorSystem.ActorOf(InstanceManagerActor.Factory(instanceManagerInit), "InstanceManager"); | 		this.InstanceManager = ActorSystem.ActorOf(InstanceManagerActor.Factory(instanceManagerInit), "InstanceManager"); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -50,7 +47,6 @@ 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), context.Services.TaskManager, "Backup scheduler for " + context.ShortName) { | 	public BackupScheduler(InstanceContext context, InstanceProcess process, int serverPort) : base(PhantomLogger.Create<BackupScheduler>(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, TaskManager TaskManager, BackupManager BackupManager); | 	public readonly record struct Init(ControllerConnection ControllerConnection, AgentFolders AgentFolders, AgentState AgentState, JavaRuntimeRepository JavaRuntimeRepository, InstanceTicketManager InstanceTicketManager, 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.TaskManager, init.BackupManager, launchServices); | 		this.instanceServices = new InstanceServices(init.ControllerConnection, init.BackupManager, launchServices); | ||||||
| 		 | 		 | ||||||
| 		ReceiveAndReply<ConfigureInstanceCommand, InstanceActionResult<ConfigureInstanceResult>>(ConfigureInstance); | 		ReceiveAndReply<ConfigureInstanceCommand, InstanceActionResult<ConfigureInstanceResult>>(ConfigureInstance); | ||||||
| 		ReceiveAndReply<LaunchInstanceCommand, InstanceActionResult<LaunchInstanceResult>>(LaunchInstance); | 		ReceiveAndReply<LaunchInstanceCommand, InstanceActionResult<LaunchInstanceResult>>(LaunchInstance); | ||||||
|   | |||||||
| @@ -1,8 +1,7 @@ | |||||||
| 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, TaskManager TaskManager, BackupManager BackupManager, LaunchServices LaunchServices); | sealed record InstanceServices(ControllerConnection ControllerConnection, BackupManager BackupManager, LaunchServices LaunchServices); | ||||||
|   | |||||||
| @@ -22,7 +22,7 @@ sealed class InstanceLogSender : CancellableBackgroundTask { | |||||||
| 	 | 	 | ||||||
| 	private int droppedLinesSinceLastSend; | 	private int droppedLinesSinceLastSend; | ||||||
|  |  | ||||||
| 	public InstanceLogSender(ControllerConnection controllerConnection, TaskManager taskManager, Guid instanceGuid, string loggerName) : base(PhantomLogger.Create<InstanceLogSender>(loggerName), taskManager, "Instance log sender for " + loggerName) { | 	public InstanceLogSender(ControllerConnection controllerConnection, Guid instanceGuid, string loggerName) : base(PhantomLogger.Create<InstanceLogSender>(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.Services.TaskManager, context.InstanceGuid, context.ShortName); | 		this.logSender = new InstanceLogSender(context.Services.ControllerConnection, 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() { | ||||||
|   | |||||||
| @@ -14,9 +14,7 @@ public sealed partial class AuthToken { | |||||||
| 	private readonly byte[] bytes; | 	private readonly byte[] bytes; | ||||||
|  |  | ||||||
| 	internal AuthToken(byte[]? bytes) { | 	internal AuthToken(byte[]? bytes) { | ||||||
| 		if (bytes == null) { | 		ArgumentNullException.ThrowIfNull(bytes); | ||||||
| 			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."); | ||||||
|   | |||||||
| @@ -0,0 +1,10 @@ | |||||||
|  | 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,21 +24,22 @@ 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<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(3); | 		ToController.Add<LogOutMessage>(3); | ||||||
| 		ToController.Add<CreateUserMessage, CreateUserResult>(4); | 		ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(4); | ||||||
| 		ToController.Add<DeleteUserMessage, DeleteUserResult>(5); | 		ToController.Add<CreateUserMessage, CreateUserResult>(5); | ||||||
| 		ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(6); | 		ToController.Add<DeleteUserMessage, DeleteUserResult>(6); | ||||||
| 		ToController.Add<GetRolesMessage, ImmutableArray<RoleInfo>>(7); | 		ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(7); | ||||||
| 		ToController.Add<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(8); | 		ToController.Add<GetRolesMessage, ImmutableArray<RoleInfo>>(8); | ||||||
| 		ToController.Add<ChangeUserRolesMessage, ChangeUserRolesResult>(9); | 		ToController.Add<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(9); | ||||||
| 		ToController.Add<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(10); | 		ToController.Add<ChangeUserRolesMessage, ChangeUserRolesResult>(10); | ||||||
| 		ToController.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(11); | 		ToController.Add<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(11); | ||||||
| 		ToController.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(12); | 		ToController.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(12); | ||||||
| 		ToController.Add<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(13); | 		ToController.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(13); | ||||||
| 		ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(14); | 		ToController.Add<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(14); | ||||||
| 		ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(15); | 		ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(15); | ||||||
| 		ToController.Add<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(16); | 		ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(16); | ||||||
| 		ToController.Add<GetEventLogMessage, ImmutableArray<EventLogItem>>(17); | 		ToController.Add<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(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; set; } = null!; | 	public DbSet<UserEntity> Users { get; init; } = null!; | ||||||
| 	public DbSet<RoleEntity> Roles { get; set; } = null!; | 	public DbSet<RoleEntity> Roles { get; init; } = null!; | ||||||
| 	public DbSet<PermissionEntity> Permissions { get; set; } = null!; | 	public DbSet<PermissionEntity> Permissions { get; init; } = null!; | ||||||
| 	 | 	 | ||||||
| 	public DbSet<UserRoleEntity> UserRoles { get; set; } = null!; | 	public DbSet<UserRoleEntity> UserRoles { get; init; } = null!; | ||||||
| 	public DbSet<UserPermissionEntity> UserPermissions { get; set; } = null!; | 	public DbSet<UserPermissionEntity> UserPermissions { get; init; } = null!; | ||||||
| 	public DbSet<RolePermissionEntity> RolePermissions { get; set; } = null!; | 	public DbSet<RolePermissionEntity> RolePermissions { get; init; } = null!; | ||||||
| 	 | 	 | ||||||
| 	public DbSet<AgentEntity> Agents { get; set; } = null!; | 	public DbSet<AgentEntity> Agents { get; init; } = null!; | ||||||
| 	public DbSet<InstanceEntity> Instances { get; set; } = null!; | 	public DbSet<InstanceEntity> Instances { get; init; } = null!; | ||||||
| 	public DbSet<AuditLogEntity> AuditLog { get; set; } = null!; | 	public DbSet<AuditLogEntity> AuditLog { get; init; } = null!; | ||||||
| 	public DbSet<EventLogEntity> EventLog { get; set; } = null!; | 	public DbSet<EventLogEntity> EventLog { get; init; } = 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; set; } | 	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; } | ||||||
|   | |||||||
| @@ -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; set; } | 	public long Id { get; init; } | ||||||
|  |  | ||||||
| 	public Guid? UserGuid { get; set; } | 	public Guid? UserGuid { get; init; } | ||||||
| 	public DateTime UtcTime { get; set; } // Note: Converting to UTC is not best practice, but for historical records it's good enough. | 	public DateTime UtcTime { get; init; } // Note: Converting to UTC is not best practice, but for historical records it's good enough. | ||||||
| 	public AuditLogEventType EventType { get; set; } | 	public AuditLogEventType EventType { get; init; } | ||||||
| 	public AuditLogSubjectType SubjectType { get; set; } | 	public AuditLogSubjectType SubjectType { get; init; } | ||||||
| 	public string SubjectId { get; set; } | 	public string SubjectId { get; init; } | ||||||
| 	public JsonDocument? Data { get; set; } | 	public JsonDocument? Data { get; init; } | ||||||
|  |  | ||||||
| 	public virtual UserEntity? User { get; set; } | 	public virtual UserEntity? User { get; init; } | ||||||
| 	 | 	 | ||||||
| 	[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; set; } | 	public Guid EventGuid { get; init; } | ||||||
|  |  | ||||||
| 	public DateTime UtcTime { get; set; } // Note: Converting to UTC is not best practice, but for historical records it's good enough. | 	public DateTime UtcTime { get; init; } // Note: Converting to UTC is not best practice, but for historical records it's good enough. | ||||||
| 	public Guid? AgentGuid { get; set; } | 	public Guid? AgentGuid { get; init; } | ||||||
| 	public EventLogEventType EventType { get; set; } | 	public EventLogEventType EventType { get; init; } | ||||||
| 	public EventLogSubjectType SubjectType { get; set; } | 	public EventLogSubjectType SubjectType { get; init; } | ||||||
| 	public string SubjectId { get; set; } | 	public string SubjectId { get; init; } | ||||||
| 	public JsonDocument? Data { get; set; } | 	public JsonDocument? Data { get; init; } | ||||||
| 	 | 	 | ||||||
| 	[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; set; } | 	public Guid InstanceGuid { get; init; } | ||||||
|  |  | ||||||
| 	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; set; } | 	public string Id { get; init; } | ||||||
|  |  | ||||||
| 	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; set; } | 	public Guid RoleGuid { get; init; } | ||||||
|  |  | ||||||
| 	public string Name { get; set; } | 	public string Name { get; init; } | ||||||
|  |  | ||||||
| 	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; set; } | 	public Guid RoleGuid { get; init; } | ||||||
| 	public string PermissionId { get; set; } | 	public string PermissionId { get; init; } | ||||||
| 	 | 	 | ||||||
| 	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; set; } | 	public Guid UserGuid { get; init; } | ||||||
|  |  | ||||||
| 	public string Name { get; set; } | 	public string Name { get; init; } | ||||||
| 	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; set; } | 	public Guid UserGuid { get; init; } | ||||||
| 	public string PermissionId { get; set; } | 	public string PermissionId { get; init; } | ||||||
|  |  | ||||||
| 	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; set; } | 	public Guid UserGuid { get; init; } | ||||||
| 	public Guid RoleGuid { get; set; } | 	public Guid RoleGuid { get; init; } | ||||||
|  |  | ||||||
| 	public UserEntity User { get; set; } | 	public UserEntity User { get; init; } | ||||||
| 	public RoleEntity Role { get; set; } | 	public RoleEntity Role { get; init; } | ||||||
|  |  | ||||||
| 	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); | 		this.UserLoginManager = new UserLoginManager(UserManager, PermissionManager, dbProvider); | ||||||
| 		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,5 +1,4 @@ | |||||||
| using Akka.Actor; | using Phantom.Common.Data.Replies; | ||||||
| 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; | ||||||
| @@ -19,8 +18,6 @@ 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,6 +69,8 @@ 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); | ||||||
| @@ -84,7 +86,6 @@ 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); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -96,6 +97,14 @@ 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); | ||||||
| 	} | 	} | ||||||
| @@ -156,10 +165,6 @@ 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,19 +2,23 @@ | |||||||
| 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<string, List<ImmutableArray<byte>>> sessionTokensByUsername = new (); | 	private readonly ConcurrentDictionary<Guid, List<ImmutableArray<byte>>> sessionTokensByUserGuid = 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) { | 	public UserLoginManager(UserManager userManager, PermissionManager permissionManager, IDbContextProvider dbProvider) { | ||||||
| 		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) { | ||||||
| @@ -24,11 +28,37 @@ sealed class UserLoginManager { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		var token = ImmutableArray.Create(RandomNumberGenerator.GetBytes(SessionIdBytes)); | 		var token = ImmutableArray.Create(RandomNumberGenerator.GetBytes(SessionIdBytes)); | ||||||
| 		var sessionTokens = sessionTokensByUsername.GetOrAdd(username, static _ => new List<ImmutableArray<byte>>()); | 		var sessionTokens = sessionTokensByUserGuid.GetOrAdd(user.UserGuid, 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,7 +10,6 @@ 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; | ||||||
| @@ -64,14 +63,12 @@ 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,10 +1,12 @@ | |||||||
| using Akka.Actor; | using System.Diagnostics.CodeAnalysis; | ||||||
|  | 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; | ||||||
| 	public bool IsClosed { get; private set; } | 	private bool isClosed; | ||||||
|  |  | ||||||
| 	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) { | ||||||
| 		var _ = value; | 		_ = value; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	private string CreateVariable(string value) { | 	private string CreateVariable(string value) { | ||||||
|   | |||||||
| @@ -1,18 +1,8 @@ | |||||||
| 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>(); | ||||||
| 		 | 		 | ||||||
|   | |||||||
| @@ -8,19 +8,13 @@ public abstract class CancellableBackgroundTask { | |||||||
| 	protected ILogger Logger { get; } | 	protected ILogger Logger { get; } | ||||||
| 	protected CancellationToken CancellationToken { get; } | 	protected CancellationToken CancellationToken { get; } | ||||||
|  |  | ||||||
| 	private readonly TaskManager taskManager; | 	protected CancellableBackgroundTask(ILogger logger) { | ||||||
| 	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() { | ||||||
| 		taskManager.Run(taskName, Run); | 		Task.Run(Run, CancellationToken.None); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	private async Task Run() { | 	private async Task Run() { | ||||||
|   | |||||||
| @@ -1,4 +1,6 @@ | |||||||
| namespace Phantom.Utils.Tasks; | using System.Diagnostics.CodeAnalysis; | ||||||
|  |  | ||||||
|  | namespace Phantom.Utils.Tasks; | ||||||
|  |  | ||||||
| public abstract record Result<TValue, TError> { | public abstract record Result<TValue, TError> { | ||||||
| 	private Result() {} | 	private Result() {} | ||||||
| @@ -42,7 +44,7 @@ public abstract record Result<TError> { | |||||||
| 		return new Fail(error); | 		return new Fail(error); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	public static implicit operator Result<TError>(Result.OkType _) { | 	public static implicit operator Result<TError>([SuppressMessage("ReSharper", "UnusedParameter.Global")] Result.OkType _) { | ||||||
| 		return new Ok(); | 		return new Ok(); | ||||||
| 	} | 	} | ||||||
| 	 | 	 | ||||||
|   | |||||||
| @@ -1,76 +0,0 @@ | |||||||
| 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 |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -1,39 +0,0 @@ | |||||||
| 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(); |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -1,28 +0,0 @@ | |||||||
| 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) { | 		if (stored != null && sessionManager.Remove(stored.UserGuid, stored.Token)) { | ||||||
| 			sessionManager.Remove(stored.UserGuid, stored.Token); | 			await controllerConnection.Send(new LogOutMessage(stored.UserGuid, stored.Token)); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		await navigation.NavigateTo(string.Empty); | 		await navigation.NavigateTo(string.Empty); | ||||||
|   | |||||||
| @@ -23,9 +23,13 @@ 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 void Remove(Guid userGuid, ImmutableArray<byte> token) { | 	internal bool 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,7 +9,6 @@ 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; | ||||||
| @@ -59,8 +58,7 @@ 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 taskManager = new TaskManager(PhantomLogger.Create<TaskManager>("Web")); | 	var webApplication = WebLauncher.CreateApplication(webConfiguration, applicationProperties, rpcSocket.Connection); | ||||||
| 	var webApplication = WebLauncher.CreateApplication(webConfiguration, taskManager, applicationProperties, rpcSocket.Connection); |  | ||||||
|  |  | ||||||
| 	using var actorSystem = ActorSystemFactory.Create("Web"); | 	using var actorSystem = ActorSystemFactory.Create("Web"); | ||||||
| 	 | 	 | ||||||
| @@ -88,7 +86,6 @@ 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,7 +1,6 @@ | |||||||
| 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; | ||||||
| @@ -13,7 +12,7 @@ static class WebLauncher { | |||||||
| 		public string HttpUrl => "http://" + Host + ":" + Port; | 		public string HttpUrl => "http://" + Host + ":" + Port; | ||||||
| 	} | 	} | ||||||
| 	 | 	 | ||||||
| 	internal static WebApplication CreateApplication(Configuration config, TaskManager taskManager, ApplicationProperties applicationProperties, RpcConnectionToServer<IMessageToController> controllerConnection) { | 	internal static WebApplication CreateApplication(Configuration config, 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, | ||||||
| @@ -29,7 +28,6 @@ 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(); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user