mirror of
				https://github.com/chylex/Minecraft-Phantom-Panel.git
				synced 2025-11-04 03:40:15 +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.Utils.Actor;
 | 
			
		||||
using Phantom.Utils.Logging;
 | 
			
		||||
using Phantom.Utils.Tasks;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace Phantom.Agent.Services;
 | 
			
		||||
@@ -18,7 +17,6 @@ public sealed class AgentServices {
 | 
			
		||||
 | 
			
		||||
	private AgentFolders AgentFolders { get; }
 | 
			
		||||
	private AgentState AgentState { get; }
 | 
			
		||||
	private TaskManager TaskManager { get; }
 | 
			
		||||
	private BackupManager BackupManager { get; }
 | 
			
		||||
 | 
			
		||||
	internal JavaRuntimeRepository JavaRuntimeRepository { get; }
 | 
			
		||||
@@ -30,13 +28,12 @@ public sealed class AgentServices {
 | 
			
		||||
		
 | 
			
		||||
		this.AgentFolders = agentFolders;
 | 
			
		||||
		this.AgentState = new AgentState();
 | 
			
		||||
		this.TaskManager = new TaskManager(PhantomLogger.Create<TaskManager, AgentServices>());
 | 
			
		||||
		this.BackupManager = new BackupManager(agentFolders, serviceConfiguration.MaxConcurrentCompressionTasks);
 | 
			
		||||
		
 | 
			
		||||
		this.JavaRuntimeRepository = new JavaRuntimeRepository();
 | 
			
		||||
		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");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -50,7 +47,6 @@ public sealed class AgentServices {
 | 
			
		||||
		Logger.Information("Stopping services...");
 | 
			
		||||
		
 | 
			
		||||
		await InstanceManager.Stop(new InstanceManagerActor.ShutdownCommand());
 | 
			
		||||
		await TaskManager.Stop();
 | 
			
		||||
		
 | 
			
		||||
		BackupManager.Dispose();
 | 
			
		||||
		
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ sealed class BackupScheduler : CancellableBackgroundTask {
 | 
			
		||||
	
 | 
			
		||||
	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.context = context;
 | 
			
		||||
		this.process = process;
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ namespace Phantom.Agent.Services.Instances;
 | 
			
		||||
sealed class InstanceManagerActor : ReceiveActor<InstanceManagerActor.ICommand> {
 | 
			
		||||
	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) {
 | 
			
		||||
		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 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<LaunchInstanceCommand, InstanceActionResult<LaunchInstanceResult>>(LaunchInstance);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,7 @@
 | 
			
		||||
using Phantom.Agent.Minecraft.Launcher;
 | 
			
		||||
using Phantom.Agent.Rpc;
 | 
			
		||||
using Phantom.Agent.Services.Backups;
 | 
			
		||||
using Phantom.Utils.Tasks;
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
	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.instanceGuid = instanceGuid;
 | 
			
		||||
		this.outputChannel = Channel.CreateBounded<string>(BufferOptions, OnLineDropped);
 | 
			
		||||
 
 | 
			
		||||
@@ -31,7 +31,7 @@ sealed class InstanceRunningState : IDisposable {
 | 
			
		||||
		this.Process = process;
 | 
			
		||||
		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.BackupCompleted += OnScheduledBackupCompleted;
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ public enum EventLogEventType {
 | 
			
		||||
	InstanceStopped,
 | 
			
		||||
	InstanceBackupSucceeded,
 | 
			
		||||
	InstanceBackupSucceededWithWarnings,
 | 
			
		||||
	InstanceBackupFailed,
 | 
			
		||||
	InstanceBackupFailed
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public static class EventLogEventTypeExtensions {
 | 
			
		||||
@@ -18,7 +18,7 @@ public static class EventLogEventTypeExtensions {
 | 
			
		||||
		{ EventLogEventType.InstanceStopped, EventLogSubjectType.Instance },
 | 
			
		||||
		{ EventLogEventType.InstanceBackupSucceeded, EventLogSubjectType.Instance },
 | 
			
		||||
		{ EventLogEventType.InstanceBackupSucceededWithWarnings, EventLogSubjectType.Instance },
 | 
			
		||||
		{ EventLogEventType.InstanceBackupFailed, EventLogSubjectType.Instance },
 | 
			
		||||
		{ EventLogEventType.InstanceBackupFailed, EventLogSubjectType.Instance }
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	static EventLogEventTypeExtensions() {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ using MemoryPack;
 | 
			
		||||
namespace Phantom.Common.Data.Web.Users;
 | 
			
		||||
 | 
			
		||||
[MemoryPackable(GenerateType.VersionTolerant)]
 | 
			
		||||
public sealed partial record LogInSuccess (
 | 
			
		||||
public sealed partial record LogInSuccess(
 | 
			
		||||
	[property: MemoryPackOrder(0)] Guid UserGuid,
 | 
			
		||||
	[property: MemoryPackOrder(1)] PermissionSet Permissions,
 | 
			
		||||
	[property: MemoryPackOrder(2)] ImmutableArray<byte> Token
 | 
			
		||||
 
 | 
			
		||||
@@ -14,9 +14,7 @@ public sealed partial class AuthToken {
 | 
			
		||||
	private readonly byte[] bytes;
 | 
			
		||||
 | 
			
		||||
	internal AuthToken(byte[]? bytes) {
 | 
			
		||||
		if (bytes == null) {
 | 
			
		||||
			throw new ArgumentNullException(nameof(bytes));
 | 
			
		||||
		}
 | 
			
		||||
		ArgumentNullException.ThrowIfNull(bytes);
 | 
			
		||||
 | 
			
		||||
		if (bytes.Length != Length) {
 | 
			
		||||
			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<UnregisterWebMessage>(1);
 | 
			
		||||
		ToController.Add<LogInMessage, LogInSuccess?>(2);
 | 
			
		||||
		ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(3);
 | 
			
		||||
		ToController.Add<CreateUserMessage, CreateUserResult>(4);
 | 
			
		||||
		ToController.Add<DeleteUserMessage, DeleteUserResult>(5);
 | 
			
		||||
		ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(6);
 | 
			
		||||
		ToController.Add<GetRolesMessage, ImmutableArray<RoleInfo>>(7);
 | 
			
		||||
		ToController.Add<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(8);
 | 
			
		||||
		ToController.Add<ChangeUserRolesMessage, ChangeUserRolesResult>(9);
 | 
			
		||||
		ToController.Add<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(10);
 | 
			
		||||
		ToController.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(11);
 | 
			
		||||
		ToController.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(12);
 | 
			
		||||
		ToController.Add<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(13);
 | 
			
		||||
		ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(14);
 | 
			
		||||
		ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(15);
 | 
			
		||||
		ToController.Add<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(16);
 | 
			
		||||
		ToController.Add<GetEventLogMessage, ImmutableArray<EventLogItem>>(17);
 | 
			
		||||
		ToController.Add<LogOutMessage>(3);
 | 
			
		||||
		ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(4);
 | 
			
		||||
		ToController.Add<CreateUserMessage, CreateUserResult>(5);
 | 
			
		||||
		ToController.Add<DeleteUserMessage, DeleteUserResult>(6);
 | 
			
		||||
		ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(7);
 | 
			
		||||
		ToController.Add<GetRolesMessage, ImmutableArray<RoleInfo>>(8);
 | 
			
		||||
		ToController.Add<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(9);
 | 
			
		||||
		ToController.Add<ChangeUserRolesMessage, ChangeUserRolesResult>(10);
 | 
			
		||||
		ToController.Add<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(11);
 | 
			
		||||
		ToController.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(12);
 | 
			
		||||
		ToController.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(13);
 | 
			
		||||
		ToController.Add<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(14);
 | 
			
		||||
		ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(15);
 | 
			
		||||
		ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(16);
 | 
			
		||||
		ToController.Add<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(17);
 | 
			
		||||
		ToController.Add<GetEventLogMessage, ImmutableArray<EventLogItem>>(18);
 | 
			
		||||
		ToController.Add<ReplyMessage>(127);
 | 
			
		||||
		
 | 
			
		||||
		ToWeb.Add<RegisterWebResultMessage>(0);
 | 
			
		||||
 
 | 
			
		||||
@@ -13,18 +13,18 @@ namespace Phantom.Controller.Database;
 | 
			
		||||
 | 
			
		||||
[SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")]
 | 
			
		||||
public class ApplicationDbContext : DbContext {
 | 
			
		||||
	public DbSet<UserEntity> Users { get; set; } = null!;
 | 
			
		||||
	public DbSet<RoleEntity> Roles { get; set; } = null!;
 | 
			
		||||
	public DbSet<PermissionEntity> Permissions { get; set; } = null!;
 | 
			
		||||
	public DbSet<UserEntity> Users { get; init; } = null!;
 | 
			
		||||
	public DbSet<RoleEntity> Roles { get; init; } = null!;
 | 
			
		||||
	public DbSet<PermissionEntity> Permissions { get; init; } = null!;
 | 
			
		||||
	
 | 
			
		||||
	public DbSet<UserRoleEntity> UserRoles { get; set; } = null!;
 | 
			
		||||
	public DbSet<UserPermissionEntity> UserPermissions { get; set; } = null!;
 | 
			
		||||
	public DbSet<RolePermissionEntity> RolePermissions { get; set; } = null!;
 | 
			
		||||
	public DbSet<UserRoleEntity> UserRoles { get; init; } = null!;
 | 
			
		||||
	public DbSet<UserPermissionEntity> UserPermissions { get; init; } = null!;
 | 
			
		||||
	public DbSet<RolePermissionEntity> RolePermissions { get; init; } = null!;
 | 
			
		||||
	
 | 
			
		||||
	public DbSet<AgentEntity> Agents { get; set; } = null!;
 | 
			
		||||
	public DbSet<InstanceEntity> Instances { get; set; } = null!;
 | 
			
		||||
	public DbSet<AuditLogEntity> AuditLog { get; set; } = null!;
 | 
			
		||||
	public DbSet<EventLogEntity> EventLog { get; set; } = null!;
 | 
			
		||||
	public DbSet<AgentEntity> Agents { get; init; } = null!;
 | 
			
		||||
	public DbSet<InstanceEntity> Instances { get; init; } = null!;
 | 
			
		||||
	public DbSet<AuditLogEntity> AuditLog { get; init; } = null!;
 | 
			
		||||
	public DbSet<EventLogEntity> EventLog { get; init; } = null!;
 | 
			
		||||
 | 
			
		||||
	public AgentEntityUpsert AgentUpsert { get; }
 | 
			
		||||
	public InstanceEntityUpsert InstanceUpsert { get; }
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ namespace Phantom.Controller.Database.Entities;
 | 
			
		||||
[SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")]
 | 
			
		||||
public sealed class AgentEntity {
 | 
			
		||||
	[Key]
 | 
			
		||||
	public Guid AgentGuid { get; set; }
 | 
			
		||||
	public Guid AgentGuid { get; init; }
 | 
			
		||||
	
 | 
			
		||||
	public string Name { get; set; }
 | 
			
		||||
	public ushort ProtocolVersion { get; set; }
 | 
			
		||||
 
 | 
			
		||||
@@ -13,16 +13,16 @@ public class AuditLogEntity : IDisposable {
 | 
			
		||||
	[Key]
 | 
			
		||||
	[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
 | 
			
		||||
	[SuppressMessage("ReSharper", "UnusedMember.Global")]
 | 
			
		||||
	public long Id { get; set; }
 | 
			
		||||
	public long Id { get; init; }
 | 
			
		||||
 | 
			
		||||
	public Guid? UserGuid { get; set; }
 | 
			
		||||
	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; set; }
 | 
			
		||||
	public AuditLogSubjectType SubjectType { get; set; }
 | 
			
		||||
	public string SubjectId { get; set; }
 | 
			
		||||
	public JsonDocument? Data { get; set; }
 | 
			
		||||
	public Guid? UserGuid { get; init; }
 | 
			
		||||
	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; init; }
 | 
			
		||||
	public AuditLogSubjectType SubjectType { get; init; }
 | 
			
		||||
	public string SubjectId { get; init; }
 | 
			
		||||
	public JsonDocument? Data { get; init; }
 | 
			
		||||
 | 
			
		||||
	public virtual UserEntity? User { get; set; }
 | 
			
		||||
	public virtual UserEntity? User { get; init; }
 | 
			
		||||
	
 | 
			
		||||
	[SuppressMessage("ReSharper", "UnusedMember.Global")]
 | 
			
		||||
	internal AuditLogEntity() {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,14 +11,14 @@ namespace Phantom.Controller.Database.Entities;
 | 
			
		||||
[SuppressMessage("ReSharper", "ClassWithVirtualMembersNeverInherited.Global")]
 | 
			
		||||
public sealed class EventLogEntity : IDisposable {
 | 
			
		||||
	[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 Guid? AgentGuid { get; set; }
 | 
			
		||||
	public EventLogEventType EventType { get; set; }
 | 
			
		||||
	public EventLogSubjectType SubjectType { get; set; }
 | 
			
		||||
	public string SubjectId { get; set; }
 | 
			
		||||
	public JsonDocument? Data { get; set; }
 | 
			
		||||
	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; init; }
 | 
			
		||||
	public EventLogEventType EventType { get; init; }
 | 
			
		||||
	public EventLogSubjectType SubjectType { get; init; }
 | 
			
		||||
	public string SubjectId { get; init; }
 | 
			
		||||
	public JsonDocument? Data { get; init; }
 | 
			
		||||
	
 | 
			
		||||
	[SuppressMessage("ReSharper", "UnusedMember.Global")]
 | 
			
		||||
	internal EventLogEntity() {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ namespace Phantom.Controller.Database.Entities;
 | 
			
		||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
 | 
			
		||||
public sealed class InstanceEntity {
 | 
			
		||||
	[Key]
 | 
			
		||||
	public Guid InstanceGuid { get; set; }
 | 
			
		||||
	public Guid InstanceGuid { get; init; }
 | 
			
		||||
 | 
			
		||||
	public Guid AgentGuid { get; set; }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ namespace Phantom.Controller.Database.Entities;
 | 
			
		||||
[Table("Permissions", Schema = "identity")]
 | 
			
		||||
public sealed class PermissionEntity {
 | 
			
		||||
	[Key]
 | 
			
		||||
	public string Id { get; set; }
 | 
			
		||||
	public string Id { get; init; }
 | 
			
		||||
 | 
			
		||||
	public PermissionEntity(string id) {
 | 
			
		||||
		Id = id;
 | 
			
		||||
 
 | 
			
		||||
@@ -7,9 +7,9 @@ namespace Phantom.Controller.Database.Entities;
 | 
			
		||||
[Table("Roles", Schema = "identity")]
 | 
			
		||||
public sealed class RoleEntity {
 | 
			
		||||
	[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) {
 | 
			
		||||
		RoleGuid = roleGuid;
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,8 @@ namespace Phantom.Controller.Database.Entities;
 | 
			
		||||
 | 
			
		||||
[Table("RolePermissions", Schema = "identity")]
 | 
			
		||||
public sealed class RolePermissionEntity {
 | 
			
		||||
	public Guid RoleGuid { get; set; }
 | 
			
		||||
	public string PermissionId { get; set; }
 | 
			
		||||
	public Guid RoleGuid { get; init; }
 | 
			
		||||
	public string PermissionId { get; init; }
 | 
			
		||||
	
 | 
			
		||||
	public RolePermissionEntity(Guid roleGuid, string permissionId) {
 | 
			
		||||
		RoleGuid = roleGuid;
 | 
			
		||||
 
 | 
			
		||||
@@ -7,9 +7,9 @@ namespace Phantom.Controller.Database.Entities;
 | 
			
		||||
[Table("Users", Schema = "identity")]
 | 
			
		||||
public sealed class UserEntity {
 | 
			
		||||
	[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 UserEntity(Guid userGuid, string name, string passwordHash) {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,8 @@ namespace Phantom.Controller.Database.Entities;
 | 
			
		||||
 | 
			
		||||
[Table("UserPermissions", Schema = "identity")]
 | 
			
		||||
public sealed class UserPermissionEntity {
 | 
			
		||||
	public Guid UserGuid { get; set; }
 | 
			
		||||
	public string PermissionId { get; set; }
 | 
			
		||||
	public Guid UserGuid { get; init; }
 | 
			
		||||
	public string PermissionId { get; init; }
 | 
			
		||||
 | 
			
		||||
	public UserPermissionEntity(Guid userGuid, string permissionId) {
 | 
			
		||||
		UserGuid = userGuid;
 | 
			
		||||
 
 | 
			
		||||
@@ -4,11 +4,11 @@ namespace Phantom.Controller.Database.Entities;
 | 
			
		||||
 | 
			
		||||
[Table("UserRoles", Schema = "identity")]
 | 
			
		||||
public sealed class UserRoleEntity {
 | 
			
		||||
	public Guid UserGuid { get; set; }
 | 
			
		||||
	public Guid RoleGuid { get; set; }
 | 
			
		||||
	public Guid UserGuid { get; init; }
 | 
			
		||||
	public Guid RoleGuid { get; init; }
 | 
			
		||||
 | 
			
		||||
	public UserEntity User { get; set; }
 | 
			
		||||
	public RoleEntity Role { get; set; }
 | 
			
		||||
	public UserEntity User { get; init; }
 | 
			
		||||
	public RoleEntity Role { get; init; }
 | 
			
		||||
 | 
			
		||||
	public UserRoleEntity(Guid userGuid, Guid roleGuid) {
 | 
			
		||||
		UserGuid = userGuid;
 | 
			
		||||
 
 | 
			
		||||
@@ -59,7 +59,7 @@ public sealed class ControllerServices : IDisposable {
 | 
			
		||||
		this.PermissionManager = new PermissionManager(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.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.BiDirectional;
 | 
			
		||||
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 });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public IStash Stash { get; set; } = null!;
 | 
			
		||||
	
 | 
			
		||||
	private readonly Guid agentGuid;
 | 
			
		||||
	private readonly RpcConnectionToClient<IMessageToAgent> connection;
 | 
			
		||||
	private readonly AgentRegistrationHandler agentRegistrationHandler;
 | 
			
		||||
 
 | 
			
		||||
@@ -69,6 +69,8 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
 | 
			
		||||
		
 | 
			
		||||
		ReceiveAsync<RegisterWebMessage>(HandleRegisterWeb);
 | 
			
		||||
		Receive<UnregisterWebMessage>(HandleUnregisterWeb);
 | 
			
		||||
		ReceiveAndReplyLater<LogInMessage, LogInSuccess?>(HandleLogIn);
 | 
			
		||||
		Receive<LogOutMessage>(HandleLogOut);
 | 
			
		||||
		ReceiveAndReplyLater<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(HandleCreateOrUpdateAdministratorUser);
 | 
			
		||||
		ReceiveAndReplyLater<CreateUserMessage, CreateUserResult>(HandleCreateUser);
 | 
			
		||||
		ReceiveAndReplyLater<GetUsersMessage, ImmutableArray<UserInfo>>(HandleGetUsers);
 | 
			
		||||
@@ -84,7 +86,6 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
 | 
			
		||||
		ReceiveAndReply<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(HandleGetAgentJavaRuntimes);
 | 
			
		||||
		ReceiveAndReplyLater<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(HandleGetAuditLog);
 | 
			
		||||
		ReceiveAndReplyLater<GetEventLogMessage, ImmutableArray<EventLogItem>>(HandleGetEventLog);
 | 
			
		||||
		ReceiveAndReplyLater<LogInMessage, LogInSuccess?>(HandleLogIn);
 | 
			
		||||
		Receive<ReplyMessage>(HandleReply);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -96,6 +97,14 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
 | 
			
		||||
		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) {
 | 
			
		||||
		return userManager.CreateOrUpdateAdministrator(message.Username, message.Password);
 | 
			
		||||
	}
 | 
			
		||||
@@ -156,10 +165,6 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
 | 
			
		||||
		return eventLogManager.GetMostRecentItems(message.Count);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private Task<LogInSuccess?> HandleLogIn(LogInMessage message) {
 | 
			
		||||
		return userLoginManager.LogIn(message.Username, message.Password);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void HandleReply(ReplyMessage message) {
 | 
			
		||||
		connection.Receive(message);
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,19 +2,23 @@
 | 
			
		||||
using System.Collections.Immutable;
 | 
			
		||||
using System.Security.Cryptography;
 | 
			
		||||
using Phantom.Common.Data.Web.Users;
 | 
			
		||||
using Phantom.Controller.Database;
 | 
			
		||||
using Phantom.Controller.Database.Repositories;
 | 
			
		||||
 | 
			
		||||
namespace Phantom.Controller.Services.Users; 
 | 
			
		||||
 | 
			
		||||
sealed class UserLoginManager {
 | 
			
		||||
	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 PermissionManager permissionManager;
 | 
			
		||||
	private readonly IDbContextProvider dbProvider;
 | 
			
		||||
	
 | 
			
		||||
	public UserLoginManager(UserManager userManager, PermissionManager permissionManager) {
 | 
			
		||||
	public UserLoginManager(UserManager userManager, PermissionManager permissionManager, IDbContextProvider dbProvider) {
 | 
			
		||||
		this.userManager = userManager;
 | 
			
		||||
		this.permissionManager = permissionManager;
 | 
			
		||||
		this.dbProvider = dbProvider;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public async Task<LogInSuccess?> LogIn(string username, string password) {
 | 
			
		||||
@@ -24,11 +28,37 @@ sealed class UserLoginManager {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		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) {
 | 
			
		||||
			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);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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.Runtime;
 | 
			
		||||
using Phantom.Utils.Runtime;
 | 
			
		||||
using Phantom.Utils.Tasks;
 | 
			
		||||
 | 
			
		||||
var shutdownCancellationTokenSource = new CancellationTokenSource();
 | 
			
		||||
var shutdownCancellationToken = shutdownCancellationTokenSource.Token;
 | 
			
		||||
@@ -64,14 +63,12 @@ try {
 | 
			
		||||
		return new RpcConfiguration(serviceName, host, port, connectionKey.Certificate);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var rpcTaskManager = new TaskManager(PhantomLogger.Create<TaskManager>("Rpc"));
 | 
			
		||||
	try {
 | 
			
		||||
		await Task.WhenAll(
 | 
			
		||||
			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)
 | 
			
		||||
		);
 | 
			
		||||
	} finally {
 | 
			
		||||
		await rpcTaskManager.Stop();
 | 
			
		||||
		NetMQConfig.Cleanup();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,12 @@
 | 
			
		||||
using Akka.Actor;
 | 
			
		||||
using System.Diagnostics.CodeAnalysis;
 | 
			
		||||
using Akka.Actor;
 | 
			
		||||
using Akka.Configuration;
 | 
			
		||||
using Akka.Dispatch;
 | 
			
		||||
using Akka.Dispatch.MessageQueues;
 | 
			
		||||
 | 
			
		||||
namespace Phantom.Utils.Actor.Mailbox;
 | 
			
		||||
 | 
			
		||||
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
 | 
			
		||||
public sealed class UnboundedJumpAheadMailbox : MailboxType, IProducesMessageQueue<UnboundedJumpAheadMessageQueue> {
 | 
			
		||||
	public const string Name = "unbounded-jump-ahead-mailbox";
 | 
			
		||||
	
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ public sealed class RpcConnectionToClient<TMessageBase> : RpcConnection<TMessage
 | 
			
		||||
	private readonly uint routingId;
 | 
			
		||||
 | 
			
		||||
	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) {
 | 
			
		||||
		this.socket = socket;
 | 
			
		||||
@@ -24,8 +24,8 @@ public sealed class RpcConnectionToClient<TMessageBase> : RpcConnection<TMessage
 | 
			
		||||
		bool hasClosed = false;
 | 
			
		||||
		
 | 
			
		||||
		lock (this) {
 | 
			
		||||
			if (!IsClosed) {
 | 
			
		||||
				IsClosed = true;
 | 
			
		||||
			if (!isClosed) {
 | 
			
		||||
				isClosed = true;
 | 
			
		||||
				hasClosed = true;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ public class EnvironmentVariablesTests {
 | 
			
		||||
	private readonly HashSet<string> createdVariables = new ();
 | 
			
		||||
 | 
			
		||||
	private static void Discard<T>(T value) {
 | 
			
		||||
		var _ = value;
 | 
			
		||||
		_ = value;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private string CreateVariable(string value) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,8 @@
 | 
			
		||||
using System.Collections.Immutable;
 | 
			
		||||
using System.Diagnostics.CodeAnalysis;
 | 
			
		||||
 | 
			
		||||
namespace Phantom.Utils.Collections;
 | 
			
		||||
 | 
			
		||||
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) {
 | 
			
		||||
		var builder = ImmutableArray.CreateBuilder<TSource>();
 | 
			
		||||
		
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis;
 | 
			
		||||
namespace Phantom.Utils.Collections;
 | 
			
		||||
 | 
			
		||||
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 ();
 | 
			
		||||
 | 
			
		||||
	public TRow this[int index] => rowList[index];
 | 
			
		||||
 
 | 
			
		||||
@@ -8,19 +8,13 @@ public abstract class CancellableBackgroundTask {
 | 
			
		||||
	protected ILogger Logger { get; }
 | 
			
		||||
	protected CancellationToken CancellationToken { get; }
 | 
			
		||||
 | 
			
		||||
	private readonly TaskManager taskManager;
 | 
			
		||||
	private readonly string taskName;
 | 
			
		||||
	
 | 
			
		||||
	protected CancellableBackgroundTask(ILogger logger, TaskManager taskManager, string taskName) {
 | 
			
		||||
	protected CancellableBackgroundTask(ILogger logger) {
 | 
			
		||||
		this.Logger = logger;
 | 
			
		||||
		this.CancellationToken = cancellationTokenSource.Token;
 | 
			
		||||
		
 | 
			
		||||
		this.taskManager = taskManager;
 | 
			
		||||
		this.taskName = taskName;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	protected void Start() {
 | 
			
		||||
		taskManager.Run(taskName, Run);
 | 
			
		||||
		Task.Run(Run, CancellationToken.None);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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> {
 | 
			
		||||
	private Result() {}
 | 
			
		||||
@@ -42,7 +44,7 @@ public abstract record Result<TError> {
 | 
			
		||||
		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();
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
 
 | 
			
		||||
@@ -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() {
 | 
			
		||||
		var stored = await sessionBrowserStorage.Delete();
 | 
			
		||||
		if (stored != null) {
 | 
			
		||||
			sessionManager.Remove(stored.UserGuid, stored.Token);
 | 
			
		||||
		if (stored != null && sessionManager.Remove(stored.UserGuid, stored.Token)) {
 | 
			
		||||
			await controllerConnection.Send(new LogOutMessage(stored.UserGuid, stored.Token));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		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;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	internal void Remove(Guid userGuid, ImmutableArray<byte> token) {
 | 
			
		||||
	internal bool Remove(Guid userGuid, ImmutableArray<byte> token) {
 | 
			
		||||
		if (userSessions.TryGetValue(userGuid, out var sessions)) {
 | 
			
		||||
			sessions.RemoveToken(token);
 | 
			
		||||
			return true;
 | 
			
		||||
		}
 | 
			
		||||
		else {
 | 
			
		||||
			return false;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,6 @@ using Phantom.Utils.Logging;
 | 
			
		||||
using Phantom.Utils.Rpc;
 | 
			
		||||
using Phantom.Utils.Rpc.Sockets;
 | 
			
		||||
using Phantom.Utils.Runtime;
 | 
			
		||||
using Phantom.Utils.Tasks;
 | 
			
		||||
using Phantom.Web;
 | 
			
		||||
using Phantom.Web.Services;
 | 
			
		||||
using Phantom.Web.Services.Rpc;
 | 
			
		||||
@@ -59,8 +58,7 @@ try {
 | 
			
		||||
	var rpcSocket = RpcClientSocket.Connect(rpcConfiguration, WebMessageRegistries.Definitions, new RegisterWebMessage(webToken));
 | 
			
		||||
 | 
			
		||||
	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, taskManager, applicationProperties, rpcSocket.Connection);
 | 
			
		||||
	var webApplication = WebLauncher.CreateApplication(webConfiguration, applicationProperties, rpcSocket.Connection);
 | 
			
		||||
 | 
			
		||||
	using var actorSystem = ActorSystemFactory.Create("Web");
 | 
			
		||||
	
 | 
			
		||||
@@ -88,7 +86,6 @@ try {
 | 
			
		||||
		await WebLauncher.Launch(webConfiguration, webApplication);
 | 
			
		||||
	} finally {
 | 
			
		||||
		shutdownCancellationTokenSource.Cancel();
 | 
			
		||||
		await taskManager.Stop();
 | 
			
		||||
		
 | 
			
		||||
		rpcDisconnectSemaphore.Release();
 | 
			
		||||
		await rpcTask;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
using Microsoft.AspNetCore.DataProtection;
 | 
			
		||||
using Phantom.Common.Messages.Web;
 | 
			
		||||
using Phantom.Utils.Rpc.Runtime;
 | 
			
		||||
using Phantom.Utils.Tasks;
 | 
			
		||||
using Phantom.Web.Services;
 | 
			
		||||
using Serilog;
 | 
			
		||||
using ILogger = Serilog.ILogger;
 | 
			
		||||
@@ -13,7 +12,7 @@ static class WebLauncher {
 | 
			
		||||
		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 builder = WebApplication.CreateBuilder(new WebApplicationOptions {
 | 
			
		||||
			ApplicationName = assembly.GetName().Name,
 | 
			
		||||
@@ -29,7 +28,6 @@ static class WebLauncher {
 | 
			
		||||
			builder.WebHost.UseStaticWebAssets();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		builder.Services.AddSingleton(taskManager);
 | 
			
		||||
		builder.Services.AddSingleton(applicationProperties);
 | 
			
		||||
		builder.Services.AddSingleton(controllerConnection);
 | 
			
		||||
		builder.Services.AddPhantomServices();
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user