diff --git a/.editorconfig b/.editorconfig index 0962e7ad2..734d59e63 100644 --- a/.editorconfig +++ b/.editorconfig @@ -111,6 +111,10 @@ dotnet_style_qualification_for_event = false:silent dotnet_style_qualification_for_method = false:silent dotnet_style_qualification_for_property = false:silent dotnet_style_qualification_for_field = false:silent + +# IDE0130: Namespace does not match folder structure +dotnet_diagnostic.IDE0130.severity = error + [*.cs] csharp_indent_labels = one_less_than_current csharp_using_directive_placement = outside_namespace:silent @@ -167,3 +171,6 @@ dotnet_diagnostic.CS8600.severity = error # CS8602: Dereference of a possibly null reference. dotnet_diagnostic.CS8602.severity = error + +# CS8631: The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match constraint type. +dotnet_diagnostic.CS8631.severity = error diff --git a/Server/Components/Devices/ChatCard.razor.cs b/Server/Components/Devices/ChatCard.razor.cs index 998811f18..c319a20ec 100644 --- a/Server/Components/Devices/ChatCard.razor.cs +++ b/Server/Components/Devices/ChatCard.razor.cs @@ -1,12 +1,12 @@ -using Microsoft.AspNetCore.Components; +using Immense.SimpleMessenger; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using Remotely.Server.Hubs; +using Remotely.Server.Models.Messages; using Remotely.Server.Services; using Remotely.Shared.Enums; using Remotely.Shared.ViewModels; using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; namespace Remotely.Server.Components.Devices; @@ -30,10 +30,13 @@ public partial class ChatCard : AuthComponentBase, IDisposable [Inject] private IJsInterop JsInterop { get; init; } = null!; + [Inject] + private IMessenger Messenger { get; init; } = null!; + public void Dispose() { AppState.PropertyChanged -= AppState_PropertyChanged; - CircuitConnection.MessageReceived -= CircuitConnection_MessageReceived; + Messenger.Unregister(this, CircuitConnection.ConnectionId); GC.SuppressFinalize(this); } @@ -46,59 +49,61 @@ protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); AppState.PropertyChanged += AppState_PropertyChanged; - CircuitConnection.MessageReceived += CircuitConnection_MessageReceived; + await Messenger.Register( + this, + CircuitConnection.ConnectionId, + HandleChatMessageReceived); } - private async void AppState_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + private async Task HandleChatMessageReceived(ChatReceivedMessage message) { - if (e.PropertyName == Session.SessionId) + if (message.DeviceId != Session.DeviceId) { - await InvokeAsync(StateHasChanged); + return; } - } - private async void CircuitConnection_MessageReceived(object? sender, Models.CircuitEvent e) - { - if (e.EventName == Models.CircuitEventName.ChatReceived) + var session = AppState.DevicesFrameChatSessions.Find(x => x.DeviceId == message.DeviceId); + + if (session is null) { - var deviceId = (string)e.Params[0]; + return; + } - if (deviceId == Session.DeviceId) + if (message.DidDisconnect) + { + session.ChatHistory.Add(new ChatHistoryItem() { - var deviceName = (string)e.Params[1]; - var message = (string)e.Params[2]; - var disconnected = (bool)e.Params[3]; - - var session = AppState.DevicesFrameChatSessions.Find(x => x.DeviceId == deviceId); - - if (disconnected) - { - session.ChatHistory.Add(new ChatHistoryItem() - { - Message = $"{Session.DeviceName} disconnected.", - Origin = ChatHistoryItemOrigin.System - }); - } - else - { - session.ChatHistory.Add(new ChatHistoryItem() - { - Message = message, - Origin = ChatHistoryItemOrigin.Device - }); - } - - if (!session.IsExpanded) - { - session.MissedChats++; - } - - await InvokeAsync(StateHasChanged); - - JsInterop.ScrollToEnd(_chatMessagesWindow); - } + Message = $"{Session.DeviceName} disconnected.", + Origin = ChatHistoryItemOrigin.System + }); + } + else + { + session.ChatHistory.Add(new ChatHistoryItem() + { + Message = message.MessageText, + Origin = ChatHistoryItemOrigin.Device + }); } + + if (!session.IsExpanded) + { + session.MissedChats++; + } + + await InvokeAsync(StateHasChanged); + + JsInterop.ScrollToEnd(_chatMessagesWindow); } + + private async void AppState_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (e.PropertyName == Session.SessionId) + { + await InvokeAsync(StateHasChanged); + } + } + private void CloseChatCard() { AppState.DevicesFrameChatSessions.RemoveAll(x => x.DeviceId == Session.DeviceId); diff --git a/Server/Components/Devices/ChatFrame.razor.cs b/Server/Components/Devices/ChatFrame.razor.cs index db7dd1d47..59c0dcfe5 100644 --- a/Server/Components/Devices/ChatFrame.razor.cs +++ b/Server/Components/Devices/ChatFrame.razor.cs @@ -1,7 +1,10 @@ -using Microsoft.AspNetCore.Components; +using Immense.SimpleMessenger; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using Remotely.Server.Hubs; +using Remotely.Server.Models.Messages; using Remotely.Server.Services; +using Remotely.Shared.Entities; using Remotely.Shared.Enums; using Remotely.Shared.ViewModels; using System; @@ -20,10 +23,13 @@ public partial class ChatFrame : AuthComponentBase, IDisposable [Inject] private ICircuitConnection CircuitConnection { get; init; } = null!; + [Inject] + private IMessenger Messenger { get; init; } = null!; + public void Dispose() { AppState.PropertyChanged -= AppState_PropertyChanged; - CircuitConnection.MessageReceived -= CircuitConnection_MessageReceived; + Messenger.Unregister(this, CircuitConnection.ConnectionId); GC.SuppressFinalize(this); } @@ -31,44 +37,36 @@ protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); AppState.PropertyChanged += AppState_PropertyChanged; - CircuitConnection.MessageReceived += CircuitConnection_MessageReceived; + await Messenger.Register( + this, + CircuitConnection.ConnectionId, + HandleChatMessageReceived); } - private void CircuitConnection_MessageReceived(object? sender, Models.CircuitEvent e) + private async Task HandleChatMessageReceived(ChatReceivedMessage message) { - if (e.EventName == Models.CircuitEventName.ChatReceived) + if (AppState.DevicesFrameChatSessions.Exists(x => x.DeviceId == message.DeviceId) || + message.DidDisconnect) { - var deviceId = (string)e.Params[0]; - - if (!AppState.DevicesFrameChatSessions.Exists(x => x.DeviceId == deviceId)) - { - var deviceName = (string)e.Params[1]; - var message = (string)e.Params[2]; - var disconnected = (bool)e.Params[3]; - - if (disconnected) - { - return; - } + return; + } - var newChat = new ChatSession() - { - DeviceId = deviceId, - DeviceName = deviceName, - IsExpanded = true - }; + var newChat = new ChatSession() + { + DeviceId = message.DeviceId, + DeviceName = message.DeviceName, + IsExpanded = true + }; - newChat.ChatHistory.Add(new ChatHistoryItem() - { - Message = message, - Origin = ChatHistoryItemOrigin.Device - }); + newChat.ChatHistory.Add(new ChatHistoryItem() + { + Message = message.MessageText, + Origin = ChatHistoryItemOrigin.Device + }); - AppState.DevicesFrameChatSessions.Add(newChat); + AppState.DevicesFrameChatSessions.Add(newChat); - InvokeAsync(StateHasChanged); - } - } + await InvokeAsync(StateHasChanged); } private void AppState_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) diff --git a/Server/Components/Devices/DeviceCard.razor.cs b/Server/Components/Devices/DeviceCard.razor.cs index cf3267bae..e11b2eb24 100644 --- a/Server/Components/Devices/DeviceCard.razor.cs +++ b/Server/Components/Devices/DeviceCard.razor.cs @@ -1,13 +1,10 @@ -using Immense.RemoteControl.Server.Abstractions; +using Immense.SimpleMessenger; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.Web; -using Microsoft.AspNetCore.SignalR; -using Microsoft.Extensions.Logging; -using Remotely.Server.Auth; using Remotely.Server.Enums; using Remotely.Server.Hubs; -using Remotely.Server.Models; +using Remotely.Server.Models.Messages; using Remotely.Server.Services; using Remotely.Shared.Entities; using Remotely.Shared.Enums; @@ -15,7 +12,6 @@ using Remotely.Shared.ViewModels; using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading.Tasks; @@ -69,10 +65,13 @@ public partial class DeviceCard : AuthComponentBase, IDisposable [Inject] private IUpgradeService UpgradeService { get; init; } = null!; + [Inject] + private IMessenger Messenger { get; init; } = null!; + public void Dispose() { AppState.PropertyChanged -= AppState_PropertyChanged; - CircuitConnection.MessageReceived -= CircuitConnection_MessageReceived; + Messenger.Unregister(this, CircuitConnection.ConnectionId); GC.SuppressFinalize(this); } @@ -84,7 +83,48 @@ protected override async Task OnInitializedAsync() _currentVersion = UpgradeService.GetCurrentVersion(); _deviceGroups = DataService.GetDeviceGroups(UserName); AppState.PropertyChanged += AppState_PropertyChanged; - CircuitConnection.MessageReceived += CircuitConnection_MessageReceived; + await Messenger.Register( + this, + CircuitConnection.ConnectionId, + HandleDeviceStateChanged); + } + + private async Task HandleDeviceStateChanged(DeviceStateChangedMessage message) + { + if (message.Device.ID != Device.ID) + { + return; + } + + // TODO: It would be cool to decorate user-editable properties + // with a "UserEditable" attribute, then use a source generator + // to create/update a method that copies property values for + // those that do not have the attribute. We could do the same + // with reflection, but this method is called too frequently, + // and the performance hit would likely be significant. + + // If the card is expanded, only update the immutable UI + // elements, so any changes to the form fields aren't lost. + if (IsExpanded) + { + Device.CurrentUser = message.Device.CurrentUser; + Device.Platform = message.Device.Platform; + Device.TotalStorage = message.Device.TotalStorage; + Device.UsedStorage = message.Device.UsedStorage; + Device.Drives = message.Device.Drives; + Device.CpuUtilization = message.Device.CpuUtilization; + Device.TotalMemory = message.Device.TotalMemory; + Device.UsedMemory = message.Device.UsedMemory; + Device.AgentVersion = message.Device.AgentVersion; + Device.LastOnline = message.Device.LastOnline; + Device.PublicIP = message.Device.PublicIP; + Device.MacAddresses = message.Device.MacAddresses; + } + else + { + Device = message.Device; + } + await InvokeAsync(StateHasChanged); } private void AppState_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) @@ -97,25 +137,6 @@ private void AppState_PropertyChanged(object? sender, System.ComponentModel.Prop } } - private async void CircuitConnection_MessageReceived(object? sender, CircuitEvent e) - { - switch (e.EventName) - { - case CircuitEventName.DeviceUpdate: - case CircuitEventName.DeviceWentOffline: - { - if (e.Params?.FirstOrDefault() is Device device && - device.ID == Device?.ID) - { - Device = device; - await InvokeAsync(StateHasChanged); - } - break; - } - default: - break; - } - } private void ContextMenuOpening(MouseEventArgs args) { if (GetCardState() == DeviceCardState.Normal) @@ -331,7 +352,7 @@ private async Task UninstallAgent() await CircuitConnection.UninstallAgents(new[] { Device.ID }); AppState.DevicesFrameFocusedDevice = null; AppState.DevicesFrameFocusedCardState = DeviceCardState.Normal; - ParentFrame.Refresh(); + await ParentFrame.Refresh(); } } diff --git a/Server/Components/Devices/DevicesFrame.razor.cs b/Server/Components/Devices/DevicesFrame.razor.cs index 677010e6b..63384906f 100644 --- a/Server/Components/Devices/DevicesFrame.razor.cs +++ b/Server/Components/Devices/DevicesFrame.razor.cs @@ -1,22 +1,22 @@ -using Microsoft.AspNetCore.Authorization; +using Immense.SimpleMessenger; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Components; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; using Remotely.Server.Enums; using Remotely.Server.Hubs; -using Remotely.Server.Models; +using Remotely.Server.Models.Messages; using Remotely.Server.Services; using Remotely.Shared.Attributes; using Remotely.Shared.Entities; using Remotely.Shared.Utilities; using System; using System.Collections; -using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; +using System.Threading; using System.Threading.Tasks; namespace Remotely.Server.Components.Devices; @@ -29,7 +29,7 @@ public partial class DevicesFrame : AuthComponentBase, IDisposable private readonly string _deviceGroupNone = Guid.NewGuid().ToString(); private readonly List _deviceGroups = new(); private readonly List _devicesForPage = new(); - private readonly object _devicesLock = new(); + private readonly SemaphoreSlim _devicesLock = new(1,1); private readonly List _filteredDevices = new(); private readonly List _sortableProperties = new(); private int _currentPage = 1; @@ -50,24 +50,34 @@ public partial class DevicesFrame : AuthComponentBase, IDisposable private IDataService DataService { get; init; } = null!; [Inject] - private IJsInterop JsInterop { get; init; } = null!; + private IToastService ToastService { get; init; } = null!; [Inject] - private IToastService ToastService { get; init; } = null!; + private IMessenger Messenger { get; init; } = null!; private int TotalPages => (int)Math.Max(1, Math.Ceiling((decimal)_filteredDevices.Count / _devicesPerPage)); public void Dispose() { - CircuitConnection.MessageReceived -= CircuitConnection_MessageReceived; + Messenger.Unregister(this, CircuitConnection.ConnectionId); + Messenger.Unregister(this, CircuitConnection.ConnectionId); + Messenger.Unregister(this, CircuitConnection.ConnectionId); + AppState.PropertyChanged -= AppState_PropertyChanged; GC.SuppressFinalize(this); } - public void Refresh() + private async Task HandleDisplayNotificationMessage(DisplayNotificationMessage message) + { + AppState.AddTerminalLine(message.ConsoleText); + ToastService.ShowToast(message.ToastText, classString: message.ClassName); + await InvokeAsync(StateHasChanged); + } + + public async Task Refresh() { - LoadDevices(); - InvokeAsync(StateHasChanged); + await LoadDevices(); + await InvokeAsync(StateHasChanged); } protected override async Task OnInitializedAsync() @@ -76,7 +86,21 @@ protected override async Task OnInitializedAsync() EnsureUserSet(); - CircuitConnection.MessageReceived += CircuitConnection_MessageReceived; + await Messenger.Register( + this, + CircuitConnection.ConnectionId, + HandleDisplayNotificationMessage); + + await Messenger.Register( + this, + CircuitConnection.ConnectionId, + HandleDeviceStateChangedMessage); + + await Messenger.Register( + this, + CircuitConnection.ConnectionId, + HandleScriptResultMessage); + AppState.PropertyChanged += AppState_PropertyChanged; _deviceGroups.Clear(); @@ -91,17 +115,43 @@ protected override async Task OnInitializedAsync() _sortableProperties.AddRange(sortableProperties); - LoadDevices(); + await LoadDevices(); + } + + private async Task HandleScriptResultMessage(ScriptResultMessage message) + { + await AddScriptResult(message.ScriptResult); } - protected override bool ShouldRender() + private async Task HandleDeviceStateChangedMessage(DeviceStateChangedMessage message) { - var shouldRender = base.ShouldRender(); - if (shouldRender) + await _devicesLock.WaitAsync(); + + try { - FilterDevices(); + var device = message.Device; + + foreach (var collection in new[] { _allDevices, _devicesForPage }) + { + var index = collection.FindIndex(x => x.ID == device.ID); + if (index > -1) + { + collection[index] = device; + } + } + + Debouncer.Debounce(TimeSpan.FromSeconds(2), Refresh); + } + finally + { + _devicesLock.Release(); } - return shouldRender; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + await FilterDevices(); } private async Task AddScriptResult(ScriptResult result) @@ -138,65 +188,16 @@ private void AppState_PropertyChanged(object? sender, PropertyChangedEventArgs e } } - private async void CircuitConnection_MessageReceived(object? sender, CircuitEvent args) - { - switch (args.EventName) - { - case CircuitEventName.DeviceUpdate: - case CircuitEventName.DeviceWentOffline: - { - if (args.Params?.FirstOrDefault() is Device device) - { - lock (_devicesLock) - { - var index = _allDevices.FindIndex(x => x.ID == device.ID); - if (index > -1) - { - _allDevices[index] = device; - } - - index = _devicesForPage.FindIndex(x => x.ID == device.ID); - if (index > -1) - { - _devicesForPage[index] = device; - } - } - Debouncer.Debounce(TimeSpan.FromSeconds(2), Refresh); - } - break; - } - case CircuitEventName.DisplayMessage: - { - var terminalMessage = (string)args.Params[0]; - var toastMessage = (string)args.Params[1]; - var className = (string)args.Params[2]; - AppState.AddTerminalLine(terminalMessage); - ToastService.ShowToast(toastMessage, classString: className); - await InvokeAsync(StateHasChanged); - } - break; - case CircuitEventName.ScriptResult: - { - if (args.Params[0] is ScriptResult result) - { - await AddScriptResult(result); - } - } - break; - default: - break; - } - } - private void ClearSelectedCard() { AppState.DevicesFrameFocusedDevice = string.Empty; AppState.DevicesFrameFocusedCardState = DeviceCardState.Normal; } - private void FilterDevices() + private async Task FilterDevices() { - lock (_devicesLock) + await _devicesLock.WaitAsync(); + try { _filteredDevices.Clear(); var appendDevices = new List(); @@ -262,8 +263,12 @@ private void FilterDevices() _devicesForPage.Clear(); _devicesForPage.AddRange(appendDevices.Concat(devicesForPage)); - } + } + finally + { + _devicesLock.Release(); + } } @@ -277,17 +282,18 @@ private string GetSortIcon() return $"oi-sort-{_sortDirection.ToString().ToLower()}"; } - private void HandleRefreshClicked() + private async Task HandleRefreshClicked() { - Refresh(); + await Refresh(); ToastService.ShowToast("Devices refreshed."); } - private void LoadDevices() + private async Task LoadDevices() { EnsureUserSet(); - lock (_devicesLock) + await _devicesLock.WaitAsync(); + try { _allDevices.Clear(); @@ -297,8 +303,12 @@ private void LoadDevices() _allDevices.AddRange(devices); } + finally + { + _devicesLock.Release(); + } - FilterDevices(); + await FilterDevices(); } private void PageDown() { diff --git a/Server/Components/Devices/Terminal.razor.cs b/Server/Components/Devices/Terminal.razor.cs index f907029e9..217c7031e 100644 --- a/Server/Components/Devices/Terminal.razor.cs +++ b/Server/Components/Devices/Terminal.razor.cs @@ -1,10 +1,12 @@ using Immense.RemoteControl.Server.Abstractions; +using Immense.SimpleMessenger; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Web; using Microsoft.Extensions.Logging; using Remotely.Server.Components.ModalContents; using Remotely.Server.Hubs; +using Remotely.Server.Models.Messages; using Remotely.Server.Services; using Remotely.Shared.Entities; using Remotely.Shared.Enums; @@ -38,6 +40,9 @@ public partial class Terminal : AuthComponentBase, IDisposable [Inject] private IDataService DataService { get; init; } = null!; + [Inject] + private IMessenger Messenger { get; init; } = null!; + private string InputText { get => _inputText ?? string.Empty; @@ -90,7 +95,7 @@ private string InputText public void Dispose() { AppState.PropertyChanged -= AppState_PropertyChanged; - CircuitConnection.MessageReceived -= CircuitConnection_MessageReceived; + Messenger.Unregister(this, CircuitConnection.ConnectionId); GC.SuppressFinalize(this); } @@ -106,9 +111,32 @@ protected override Task OnAfterRenderAsync(bool firstRender) protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); - CircuitConnection.MessageReceived += CircuitConnection_MessageReceived; + await Messenger.Register( + this, + CircuitConnection.ConnectionId, + HandlePowerShellCompletionsMessage); AppState.PropertyChanged += AppState_PropertyChanged; } + + private async Task HandlePowerShellCompletionsMessage(PowerShellCompletionsMessage message) + { + var completion = message.Completion; + var intent = message.Intent; + + switch (intent) + { + case CompletionIntent.ShowAll: + await DisplayCompletions(completion.CompletionMatches); + break; + case CompletionIntent.NextResult: + ApplyCompletion(completion); + break; + default: + break; + } + AppState.InvokePropertyChanged(nameof(AppState.TerminalLines)); + } + private void ApplyCompletion(PwshCommandCompletion completion) { try @@ -142,27 +170,6 @@ private void AppState_PropertyChanged(object? sender, System.ComponentModel.Prop } } - private async void CircuitConnection_MessageReceived(object? sender, Models.CircuitEvent e) - { - if (e.EventName == Models.CircuitEventName.PowerShellCompletions) - { - var completion = (PwshCommandCompletion)e.Params[0]; - var intent = (CompletionIntent)e.Params[1]; - - switch (intent) - { - case CompletionIntent.ShowAll: - await DisplayCompletions(completion.CompletionMatches); - break; - case CompletionIntent.NextResult: - ApplyCompletion(completion); - break; - default: - break; - } - AppState.InvokePropertyChanged(nameof(AppState.TerminalLines)); - } - } private async Task DisplayCompletions(List completionMatches) { var deviceId = AppState.DevicesFrameSelectedDevices.FirstOrDefault(); diff --git a/Server/Components/LoaderHarness.razor b/Server/Components/LoaderHarness.razor index 66bd1e83a..d231994b2 100644 --- a/Server/Components/LoaderHarness.razor +++ b/Server/Components/LoaderHarness.razor @@ -1,4 +1,4 @@ -@using Nihs.SimpleMessenger; +@using Immense.SimpleMessenger; @using Remotely.Server.Models.Messages; @inject IMessenger Messenger diff --git a/Server/Hubs/AgentHub.cs b/Server/Hubs/AgentHub.cs index 6d19fb5eb..5b8da3e36 100644 --- a/Server/Hubs/AgentHub.cs +++ b/Server/Hubs/AgentHub.cs @@ -1,8 +1,10 @@ using Immense.RemoteControl.Server.Hubs; +using Immense.SimpleMessenger; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Remotely.Server.Models; +using Remotely.Server.Models.Messages; using Remotely.Server.Services; using Remotely.Shared; using Remotely.Shared.Dtos; @@ -24,6 +26,7 @@ public class AgentHub : Hub private readonly ICircuitManager _circuitManager; private readonly IDataService _dataService; private readonly IExpiringTokenService _expiringTokenService; + private readonly IMessenger _messenger; private readonly ILogger _logger; private readonly IAgentHubSessionCache _serviceSessionCache; private readonly IHubContext _viewerHubContext; @@ -34,6 +37,7 @@ public class AgentHub : Hub IHubContext viewerHubContext, ICircuitManager circuitManager, IExpiringTokenService expiringTokenService, + IMessenger messenger, ILogger logger) { _dataService = dataService; @@ -42,6 +46,7 @@ public class AgentHub : Hub _appConfig = appConfig; _circuitManager = circuitManager; _expiringTokenService = expiringTokenService; + _messenger = messenger; _logger = logger; } @@ -65,20 +70,21 @@ public class AgentHub : Hub } } - public Task Chat(string message, bool disconnected, string browserConnectionId) + public async Task Chat(string messageText, bool disconnected, string browserConnectionId) { if (Device is null) { - return Task.CompletedTask; + return; } - if (_circuitManager.TryGetConnection(browserConnectionId, out var connection)) + if (_circuitManager.TryGetConnection(browserConnectionId, out _)) { - return connection.InvokeCircuitEvent(CircuitEventName.ChatReceived, Device.ID, $"{Device.DeviceName}", message, disconnected); + var message = new ChatReceivedMessage(Device.ID, $"{Device.DeviceName}", messageText, disconnected); + await _messenger.Send(message, browserConnectionId); } else { - return Clients.Caller.SendChatMessage( + await Clients.Caller.SendChatMessage( senderName: string.Empty, message: string.Empty, orgName: string.Empty, @@ -152,7 +158,8 @@ public async Task DeviceCameOnline(DeviceClientDto device) foreach (var connection in connections) { - await connection.InvokeCircuitEvent(CircuitEventName.DeviceUpdate, Device); + var message = new DeviceStateChangedMessage(Device); + await _messenger.Send(message, connection.ConnectionId); } return true; } @@ -212,7 +219,8 @@ public async Task DeviceHeartbeat(DeviceClientDto device) foreach (var connection in connections) { - _ = connection.InvokeCircuitEvent(CircuitEventName.DeviceUpdate, Device); + var message = new DeviceStateChangedMessage(Device); + await _messenger.Send(message, connection.ConnectionId); } @@ -220,19 +228,22 @@ public async Task DeviceHeartbeat(DeviceClientDto device) } - public Task DisplayMessage(string consoleMessage, string popupMessage, string className, string requesterID) + public Task DisplayMessage(string consoleMessage, string popupMessage, string className, string requesterId) { - return _circuitManager.InvokeOnConnection(requesterID, CircuitEventName.DisplayMessage, consoleMessage, popupMessage, className); + var message = new DisplayNotificationMessage(consoleMessage, popupMessage, className); + return _messenger.Send(message, requesterId); } - public Task DownloadFile(string fileID, string requesterID) + public Task DownloadFile(string fileID, string requesterId) { - return _circuitManager.InvokeOnConnection(requesterID, CircuitEventName.DownloadFile, fileID); + var message = new DownloadFileMessage(fileID); + return _messenger.Send(message, requesterId); } - public Task DownloadFileProgress(int progressPercent, string requesterID) + public Task DownloadFileProgress(int progressPercent, string requesterId) { - return _circuitManager.InvokeOnConnection(requesterID, CircuitEventName.DownloadFileProgress, progressPercent); + var message = new DownloadFileProgressMessage(progressPercent); + return _messenger.Send(message, requesterId); } public string GetServerUrl() @@ -245,7 +256,7 @@ public string GetServerVerificationToken() return $"{Device?.ServerVerificationToken}"; } - public override Task OnDisconnectedAsync(Exception? exception) + public override async Task OnDisconnectedAsync(Exception? exception) { try { @@ -265,10 +276,11 @@ public override Task OnDisconnectedAsync(Exception? exception) foreach (var connection in connections) { - connection.InvokeCircuitEvent(CircuitEventName.DeviceWentOffline, Device); + var message = new DeviceStateChangedMessage(Device); + await _messenger.Send(message, connection.ConnectionId); } } - return base.OnDisconnectedAsync(exception); + await base.OnDisconnectedAsync(exception); } finally { @@ -278,7 +290,8 @@ public override Task OnDisconnectedAsync(Exception? exception) public Task ReturnPowerShellCompletions(PwshCommandCompletion completion, CompletionIntent intent, string senderConnectionId) { - return _circuitManager.InvokeOnConnection(senderConnectionId, CircuitEventName.PowerShellCompletions, completion, intent); + var message = new PowerShellCompletionsMessage(completion, intent); + return _messenger.Send(message, senderConnectionId); } public async Task ScriptResult(string scriptResultId) @@ -289,9 +302,8 @@ public async Task ScriptResult(string scriptResultId) return; } - _ = await _circuitManager.InvokeOnConnection($"{result.Value.SenderConnectionID}", - CircuitEventName.ScriptResult, - result.Value); + var message = new ScriptResultMessage(result.Value); + await _messenger.Send(message, $"{result.Value.SenderConnectionID}"); } public void ScriptResultViaApi(string commandID, string requestID) @@ -305,7 +317,8 @@ public Task SendConnectionFailedToViewers(List viewerIDs) public Task SendLogs(string logChunk, string requesterConnectionId) { - return _circuitManager.InvokeOnConnection(requesterConnectionId, CircuitEventName.RemoteLogsReceived, logChunk); + var message = new ReceiveLogsMessage(logChunk); + return _messenger.Send(message, requesterConnectionId); } public void SetServerVerificationToken(string verificationToken) { @@ -316,9 +329,10 @@ public void SetServerVerificationToken(string verificationToken) Device.ServerVerificationToken = verificationToken; _dataService.SetServerVerificationToken(Device.ID, verificationToken); } - public Task TransferCompleted(string transferID, string requesterID) + public Task TransferCompleted(string transferId, string requesterId) { - return _circuitManager.InvokeOnConnection(requesterID, CircuitEventName.TransferCompleted, transferID); + var message = new TransferCompleteMessage(transferId); + return _messenger.Send(message, requesterId); } private async Task CheckForDeviceBan(params string[] deviceIdNameOrIPs) { diff --git a/Server/Hubs/CircuitConnection.cs b/Server/Hubs/CircuitConnection.cs index 1c07dc2cc..48e1683cd 100644 --- a/Server/Hubs/CircuitConnection.cs +++ b/Server/Hubs/CircuitConnection.cs @@ -1,15 +1,12 @@ -using Immense.RemoteControl.Server.Abstractions; -using Immense.RemoteControl.Server.Services; +using Immense.RemoteControl.Server.Services; using Immense.RemoteControl.Shared; using Immense.RemoteControl.Shared.Helpers; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Components.Authorization; +using Immense.SimpleMessenger; using Microsoft.AspNetCore.Components.Server.Circuits; -using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; -using Remotely.Server.Auth; using Remotely.Server.Models; +using Remotely.Server.Models.Messages; using Remotely.Server.Services; using Remotely.Shared; using Remotely.Shared.Entities; @@ -20,7 +17,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; @@ -28,7 +24,8 @@ namespace Remotely.Server.Hubs; public interface ICircuitConnection { - event EventHandler? MessageReceived; + string ConnectionId { get; } + RemotelyUser User { get; } Task DeleteRemoteLogs(string deviceId); @@ -39,7 +36,6 @@ public interface ICircuitConnection Task GetRemoteLogs(string deviceId); - Task InvokeCircuitEvent(CircuitEventName eventName, params object[] args); Task ReinstallAgents(string[] deviceIDs); Task> RemoteControl(string deviceID, bool viewOnly); @@ -80,12 +76,11 @@ public class CircuitConnection : CircuitHandler, ICircuitConnection private readonly ICircuitManager _circuitManager; private readonly IDataService _dataService; private readonly IRemoteControlSessionCache _remoteControlSessionCache; - private readonly ConcurrentQueue _eventQueue = new(); private readonly IExpiringTokenService _expiringTokenService; private readonly ILogger _logger; private readonly IAgentHubSessionCache _agentSessionCache; + private readonly IMessenger _messenger; private readonly IToastService _toastService; - private readonly ManualResetEventSlim _initSignal = new(); private RemotelyUser? _user; public CircuitConnection( @@ -99,6 +94,7 @@ public class CircuitConnection : CircuitHandler, ICircuitConnection IExpiringTokenService expiringTokenService, IRemoteControlSessionCache remoteControlSessionCache, IAgentHubSessionCache agentSessionCache, + IMessenger messenger, ILogger logger) { _dataService = dataService; @@ -111,33 +107,17 @@ public class CircuitConnection : CircuitHandler, ICircuitConnection _expiringTokenService = expiringTokenService; _remoteControlSessionCache = remoteControlSessionCache; _agentSessionCache = agentSessionCache; + _messenger = messenger; _logger = logger; } - public event EventHandler? MessageReceived; - public string ConnectionId { get; } = Guid.NewGuid().ToString(); public RemotelyUser User { - get - { - if (_initSignal.Wait(TimeSpan.FromSeconds(5)) && _user is not null) - { - return _user; - } - _logger.LogError("Failed to resolve user."); - throw new InvalidOperationException("Failed to resolve user."); - } - internal set - { - _user = value; - if (_user is not null) - { - _initSignal.Set(); - } - } + get => _user ?? throw new InvalidOperationException("User is not set."); + internal set => _user = value; } @@ -212,12 +192,6 @@ public Task GetRemoteLogs(string deviceId) return _agentHubContext.Clients.Client(key).GetLogs(ConnectionId); } - public Task InvokeCircuitEvent(CircuitEventName eventName, params object[] args) - { - _eventQueue.Enqueue(new CircuitEvent(eventName, args)); - return Task.Run(ProcessMessages); - } - public override async Task OnCircuitClosedAsync(Circuit circuit, CancellationToken cancellationToken) { if (!string.IsNullOrWhiteSpace(ConnectionId)) @@ -237,9 +211,8 @@ public override async Task OnCircuitOpenedAsync(Circuit circuit, CancellationTok _toastService.ShowToast2("Authorization failure.", Enums.ToastType.Error); return; } - _user = userResult.Value; + User = userResult.Value; _circuitManager.TryAddConnection(ConnectionId, this); - _initSignal.Set(); } await base.OnCircuitOpenedAsync(circuit, cancellationToken); } @@ -256,10 +229,13 @@ public async Task> RemoteControl(string deviceId, { if (!_agentSessionCache.TryGetByDeviceId(deviceId, out var targetDevice)) { - MessageReceived?.Invoke(this, new CircuitEvent(CircuitEventName.DisplayMessage, + var message = new DisplayNotificationMessage( "The selected device is not online.", "Device is not online.", - "bg-warning")); + "bg-warning"); + + await _messenger.Send(message, ConnectionId); + return Result.Fail("Device is not online."); } @@ -281,19 +257,24 @@ public async Task> RemoteControl(string deviceId, if (sessionCount >= _appConfig.RemoteControlSessionLimit) { - MessageReceived?.Invoke(this, new CircuitEvent(CircuitEventName.DisplayMessage, + var message = new DisplayNotificationMessage( "There are already the maximum amount of active remote control sessions for your organization.", "Max number of concurrent sessions reached.", - "bg-warning")); + "bg-warning"); + + await _messenger.Send(message, ConnectionId); + return Result.Fail("Max number of concurrent sessions reached."); } if (!_agentSessionCache.TryGetConnectionId(targetDevice.ID, out var serviceConnectionId)) { - MessageReceived?.Invoke(this, new CircuitEvent(CircuitEventName.DisplayMessage, - "Service connection not found.", - "Service connection not found.", - "bg-warning")); + var message = new DisplayNotificationMessage( + "Service connection not found.", + "Service connection not found.", + "bg-warning"); + + await _messenger.Send(message, ConnectionId); return Result.Fail("Service connection not found."); } @@ -463,24 +444,30 @@ public async Task UninstallAgents(string[] deviceIDs) _dataService.RemoveDevices(deviceIDs); } - public Task UpdateTags(string deviceID, string tags) + public async Task UpdateTags(string deviceID, string tags) { if (_dataService.DoesUserHaveAccessToDevice(deviceID, User)) { if (tags.Length > 200) { - MessageReceived?.Invoke(this, new CircuitEvent(CircuitEventName.DisplayMessage, + var message = new CircuitEvent(CircuitEventName.DisplayMessage, $"Tag must be 200 characters or less. Supplied length is {tags.Length}.", "Tag must be under 200 characters.", - "bg-warning")); + "bg-warning"); + + await _messenger.Send(message, ConnectionId); + return; } - _dataService.UpdateTags(deviceID, tags); - MessageReceived?.Invoke(this, new CircuitEvent(CircuitEventName.DisplayMessage, + + await _dataService.UpdateTags(deviceID, tags); + + var successMessage = new DisplayNotificationMessage( "Device updated successfully.", "Device updated.", - "bg-success")); + "bg-success"); + + await _messenger.Send(successMessage, ConnectionId); } - return Task.CompletedTask; } public async Task WakeDevice(Device device) @@ -606,23 +593,6 @@ private IEnumerable GetActiveConnectionsForUserOrg(IEnumerable d } } - private void ProcessMessages() - { - lock (_eventQueue) - { - while (_eventQueue.TryDequeue(out var circuitEvent)) - { - try - { - MessageReceived?.Invoke(this, circuitEvent); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error while invoking circuit event."); - } - } - } - } private async Task SendWakeCommand(Device deviceToWake, IEnumerable peerDevices) { diff --git a/Server/Models/Messages/ChatReceivedMessage.cs b/Server/Models/Messages/ChatReceivedMessage.cs new file mode 100644 index 000000000..e1cc999b2 --- /dev/null +++ b/Server/Models/Messages/ChatReceivedMessage.cs @@ -0,0 +1,3 @@ +namespace Remotely.Server.Models.Messages; + +public record ChatReceivedMessage(string DeviceId, string DeviceName, string MessageText, bool DidDisconnect = false); \ No newline at end of file diff --git a/Server/Models/Messages/DeviceStateChangedMessage.cs b/Server/Models/Messages/DeviceStateChangedMessage.cs new file mode 100644 index 000000000..ae5798717 --- /dev/null +++ b/Server/Models/Messages/DeviceStateChangedMessage.cs @@ -0,0 +1,5 @@ +using Remotely.Shared.Entities; + +namespace Remotely.Server.Models.Messages; + +public record DeviceStateChangedMessage(Device Device); diff --git a/Server/Models/Messages/DisplayNotificationMessage.cs b/Server/Models/Messages/DisplayNotificationMessage.cs new file mode 100644 index 000000000..5aee95ca3 --- /dev/null +++ b/Server/Models/Messages/DisplayNotificationMessage.cs @@ -0,0 +1,3 @@ +namespace Remotely.Server.Models.Messages; + +public record DisplayNotificationMessage(string ConsoleText, string ToastText, string ClassName); \ No newline at end of file diff --git a/Server/Models/Messages/DownloadFileMessage.cs b/Server/Models/Messages/DownloadFileMessage.cs new file mode 100644 index 000000000..82484d111 --- /dev/null +++ b/Server/Models/Messages/DownloadFileMessage.cs @@ -0,0 +1,3 @@ +namespace Remotely.Server.Models.Messages; + +public record DownloadFileMessage(string MessageId); diff --git a/Server/Models/Messages/DownloadFileProgressMessage.cs b/Server/Models/Messages/DownloadFileProgressMessage.cs new file mode 100644 index 000000000..971f27f37 --- /dev/null +++ b/Server/Models/Messages/DownloadFileProgressMessage.cs @@ -0,0 +1,3 @@ +namespace Remotely.Server.Models.Messages; + +public record DownloadFileProgressMessage(int ProgressPercent); diff --git a/Server/Models/Messages/PowerShellCompletionsMessage.cs b/Server/Models/Messages/PowerShellCompletionsMessage.cs new file mode 100644 index 000000000..615abd6bc --- /dev/null +++ b/Server/Models/Messages/PowerShellCompletionsMessage.cs @@ -0,0 +1,6 @@ +using Remotely.Shared.Enums; +using Remotely.Shared.Models; + +namespace Remotely.Server.Models.Messages; + +public record PowerShellCompletionsMessage(PwshCommandCompletion Completion, CompletionIntent Intent); \ No newline at end of file diff --git a/Server/Models/Messages/ReceiveLogsMessage.cs b/Server/Models/Messages/ReceiveLogsMessage.cs new file mode 100644 index 000000000..7fd419596 --- /dev/null +++ b/Server/Models/Messages/ReceiveLogsMessage.cs @@ -0,0 +1,3 @@ +namespace Remotely.Server.Models.Messages; + +public record ReceiveLogsMessage(string LogChunk); \ No newline at end of file diff --git a/Server/Models/Messages/ScriptResultMessage.cs b/Server/Models/Messages/ScriptResultMessage.cs new file mode 100644 index 000000000..010185cfa --- /dev/null +++ b/Server/Models/Messages/ScriptResultMessage.cs @@ -0,0 +1,5 @@ +using Remotely.Shared.Entities; + +namespace Remotely.Server.Models.Messages; + +public record ScriptResultMessage(ScriptResult ScriptResult); diff --git a/Server/Models/Messages/ShowLoaderMessage.cs b/Server/Models/Messages/ShowLoaderMessage.cs index 94537209d..afe8ed2e6 100644 --- a/Server/Models/Messages/ShowLoaderMessage.cs +++ b/Server/Models/Messages/ShowLoaderMessage.cs @@ -1,13 +1,3 @@ namespace Remotely.Server.Models.Messages; -public class ShowLoaderMessage -{ - public ShowLoaderMessage(bool isShown, string statusMessage) - { - IsShown = isShown; - StatusMessage = statusMessage; - } - - public bool IsShown { get; } - public string StatusMessage { get; } -} \ No newline at end of file +public record ShowLoaderMessage(bool IsShown, string StatusMessage); \ No newline at end of file diff --git a/Server/Models/Messages/TransferCompleteMessage.cs b/Server/Models/Messages/TransferCompleteMessage.cs new file mode 100644 index 000000000..c8ddc5140 --- /dev/null +++ b/Server/Models/Messages/TransferCompleteMessage.cs @@ -0,0 +1,3 @@ +namespace Remotely.Server.Models.Messages; + +public record TransferCompleteMessage(string TransferId); \ No newline at end of file diff --git a/Server/Pages/DeviceDetails.razor.cs b/Server/Pages/DeviceDetails.razor.cs index 9fb5149eb..03e4e8f60 100644 --- a/Server/Pages/DeviceDetails.razor.cs +++ b/Server/Pages/DeviceDetails.razor.cs @@ -1,8 +1,10 @@ -using Microsoft.AspNetCore.Components; +using Immense.SimpleMessenger; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Web; using Remotely.Server.Components; using Remotely.Server.Hubs; +using Remotely.Server.Models.Messages; using Remotely.Server.Services; using Remotely.Shared.Entities; using Remotely.Shared.Enums; @@ -52,6 +54,9 @@ public partial class DeviceDetails : AuthComponentBase [Inject] private IToastService ToastService { get; set; } = null!; + [Inject] + private IMessenger Messenger { get; init; } = null!; + protected override async Task OnInitializedAsync() { @@ -74,18 +79,18 @@ protected override async Task OnInitializedAsync() } _deviceGroups = DataService.GetDeviceGroups(UserName); - CircuitConnection.MessageReceived += CircuitConnection_MessageReceived; + await Messenger.Register( + this, + CircuitConnection.ConnectionId, + HandleReceiveLogsMessage); + _isLoading = false; } - private void CircuitConnection_MessageReceived(object? sender, Models.CircuitEvent e) + private async Task HandleReceiveLogsMessage(ReceiveLogsMessage message) { - if (e.EventName == Models.CircuitEventName.RemoteLogsReceived) - { - var logChunk = (string)e.Params[0]; - _logLines.Enqueue(logChunk); - InvokeAsync(StateHasChanged); - } + _logLines.Enqueue(message.LogChunk); + await InvokeAsync(StateHasChanged); } private async Task DeleteLogs() diff --git a/Server/Program.cs b/Server/Program.cs index 9920579b3..706771988 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -14,7 +14,6 @@ using Microsoft.Extensions.Hosting; using Microsoft.OpenApi.Models; using Microsoft.Extensions.Logging; -using Npgsql; using Remotely.Server.Areas.Identity; using Remotely.Server.Auth; using Remotely.Server.Data; @@ -28,10 +27,10 @@ using Remotely.Shared.Services; using System; using Serilog; -using Nihs.SimpleMessenger; using Microsoft.AspNetCore.RateLimiting; using RatePolicyNames = Remotely.Server.RateLimiting.PolicyNames; using Remotely.Shared.Entities; +using Immense.SimpleMessenger; var builder = WebApplication.CreateBuilder(args); var configuration = builder.Configuration; @@ -211,7 +210,7 @@ services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); -services.AddScoped((services) => new WeakReferenceMessenger()); +services.AddSingleton(WeakReferenceMessenger.Default); services.AddRemoteControlServer(config => { diff --git a/Server/Server.csproj b/Server/Server.csproj index baf9eee8c..fe8b43c69 100644 --- a/Server/Server.csproj +++ b/Server/Server.csproj @@ -32,7 +32,6 @@ - diff --git a/Server/Services/CircuitManager.cs b/Server/Services/CircuitManager.cs index ad44fa7b8..2ff26bff3 100644 --- a/Server/Services/CircuitManager.cs +++ b/Server/Services/CircuitManager.cs @@ -15,7 +15,6 @@ public interface ICircuitManager ICollection Connections { get; } bool TryAddConnection(string id, ICircuitConnection connection); bool TryRemoveConnection(string id, [NotNullWhen(true)] out ICircuitConnection? connection); - Task InvokeOnConnection(string id, CircuitEventName eventName, params object[] args); bool TryGetConnection(string id, [NotNullWhen(true)] out ICircuitConnection? connection); } public class CircuitManager : ICircuitManager @@ -30,23 +29,6 @@ public CircuitManager(ILogger logger) public ICollection Connections => _connections.Values; - public Task InvokeOnConnection(string browserConnectionId, CircuitEventName eventName, params object[] args) - { - try - { - if (_connections.TryGetValue(browserConnectionId, out var result)) - { - result.InvokeCircuitEvent(eventName, args); - return Task.FromResult(true); - } - return Task.FromResult(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error while invoking circuit event."); - return Task.FromResult(false); - } - } public bool TryAddConnection(string id, ICircuitConnection connection) { diff --git a/Server/Services/ClientAppState.cs b/Server/Services/ClientAppState.cs index 0994f2ecd..3f6272f48 100644 --- a/Server/Services/ClientAppState.cs +++ b/Server/Services/ClientAppState.cs @@ -1,7 +1,6 @@ -using Nihs.ConcurrentList; -using Remotely.Server.Enums; +using Remotely.Server.Enums; using Remotely.Shared.Enums; -using Remotely.Shared.Models; +using Remotely.Shared.Primitives; using Remotely.Shared.ViewModels; using System.Collections.Concurrent; using System.ComponentModel; diff --git a/Server/Services/LoaderService.cs b/Server/Services/LoaderService.cs index 37d87437d..67effd4a7 100644 --- a/Server/Services/LoaderService.cs +++ b/Server/Services/LoaderService.cs @@ -1,5 +1,5 @@ using Immense.RemoteControl.Shared.Primitives; -using Nihs.SimpleMessenger; +using Immense.SimpleMessenger; using Remotely.Server.Models.Messages; using System; using System.Threading.Tasks; diff --git a/Server/Services/ToastService.cs b/Server/Services/ToastService.cs index a42af683b..79006e023 100644 --- a/Server/Services/ToastService.cs +++ b/Server/Services/ToastService.cs @@ -1,6 +1,6 @@ -using Nihs.ConcurrentList; -using Remotely.Server.Enums; +using Remotely.Server.Enums; using Remotely.Server.Models; +using Remotely.Shared.Primitives; using System; using System.Timers; diff --git a/Shared/Primitives/ConcurrentList.cs b/Shared/Primitives/ConcurrentList.cs new file mode 100644 index 000000000..5ad3fe93d --- /dev/null +++ b/Shared/Primitives/ConcurrentList.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Remotely.Shared.Primitives; + +/// +/// A simple, lock-based implementation of a thread-safe List. +/// Note that a copy is returned when enumerating the list. +/// +/// +public class ConcurrentList : IList +{ + + private readonly List _list = new(); + private readonly object _lock = new(); + + public int Count + { + get + { + lock (_lock) + { + return _list.Count; + } + } + } + + public bool IsReadOnly => false; + + public T this[int index] + { + get + { + lock (_lock) + { + return _list[index]; + } + } + set + { + lock (_lock) + { + _list[index] = value; + } + } + } + public void Add(T item) + { + lock (_lock) + { + _list.Add(item); + } + } + + public void AddRange(IEnumerable collection) + { + lock (_lock) + { + _list.AddRange(collection); + } + } + + public void Clear() + { + lock (_lock) + { + _list.Clear(); + } + } + + public bool Contains(T item) + { + lock (_lock) + { + return _list.Contains(item); + } + } + + public void CopyTo(T[] array, int arrayIndex) + { + lock (_lock) + { + _list.CopyTo(array, arrayIndex); + } + } + + public bool Exists(Predicate predicate) + { + lock (_lock) + { + return _list.Exists(predicate); + } + } + + public List FindAll(Predicate predicate) + { + lock (_lock) + { + return _list.FindAll(predicate); + } + } + + public T? Find(Predicate predicate) + { + lock (_lock) + { + return _list.Find(predicate); + } + } + + public int FindIndex(Predicate predicate) + { + lock (_lock) + { + return _list.FindIndex(predicate); + } + } + public T? FindLast(Predicate predicate) + { + lock (_lock) + { + return _list.FindLast(predicate); + } + } + + public int FindLastIndex(Predicate predicate) + { + lock (_lock) + { + return _list.FindLastIndex(predicate); + } + } + + public IEnumerator GetEnumerator() + { + lock (_lock) + { + return _list.ToList().GetEnumerator(); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + lock (_lock) + { + return _list.ToList().GetEnumerator(); + } + } + + public int IndexOf(T item) + { + lock (_lock) + { + return _list.IndexOf(item); + } + } + + public void Insert(int index, T item) + { + lock (_lock) + { + _list.Insert(index, item); + } + } + + public bool Remove(T item) + { + lock (_lock) + { + return _list.Remove(item); + } + } + + public void RemoveAll(Predicate predicate) + { + lock (_lock) + { + _list.RemoveAll(predicate); + } + } + + public void RemoveAt(int index) + { + lock (_lock) + { + _list.RemoveAt(index); + } + } + public void RemoveRange(int index, int count) + { + lock (_lock) + { + _list.RemoveRange(index, count); + } + } +} \ No newline at end of file diff --git a/Shared/Shared.csproj b/Shared/Shared.csproj index 191838c32..b64fd1115 100644 --- a/Shared/Shared.csproj +++ b/Shared/Shared.csproj @@ -11,7 +11,6 @@ - diff --git a/Shared/Utilities/Debouncer.cs b/Shared/Utilities/Debouncer.cs index 50bb58a19..7d2102363 100644 --- a/Shared/Utilities/Debouncer.cs +++ b/Shared/Utilities/Debouncer.cs @@ -26,4 +26,20 @@ public static void Debounce(TimeSpan wait, Action action, [CallerMemberName] str _timers.TryAdd(key, timer); timer.Start(); } + + public static void Debounce(TimeSpan wait, Func func, [CallerMemberName] string key = "") + { + if (_timers.TryRemove(key, out var timer)) + { + timer.Stop(); + timer.Dispose(); + } + timer = new Timer(wait.TotalMilliseconds) + { + AutoReset = false + }; + timer.Elapsed += (s, e) => func(); + _timers.TryAdd(key, timer); + timer.Start(); + } } \ No newline at end of file diff --git a/Shared/ViewModels/ChatSession.cs b/Shared/ViewModels/ChatSession.cs index 3d7a30f8c..630484bec 100644 --- a/Shared/ViewModels/ChatSession.cs +++ b/Shared/ViewModels/ChatSession.cs @@ -1,4 +1,4 @@ -using Nihs.ConcurrentList; +using Remotely.Shared.Primitives; using System; namespace Remotely.Shared.ViewModels; diff --git a/Tests/Server.Tests/AgentHubTests.cs b/Tests/Server.Tests/AgentHubTests.cs index 474c1c06a..9b59783c9 100644 --- a/Tests/Server.Tests/AgentHubTests.cs +++ b/Tests/Server.Tests/AgentHubTests.cs @@ -13,8 +13,10 @@ using System.Threading; using System.Threading.Tasks; using Remotely.Shared.Interfaces; +using Immense.SimpleMessenger; +using Remotely.Tests; -namespace Remotely.Tests; +namespace Remotely.Server.Tests; [TestClass] public class AgentHubTests @@ -34,17 +36,19 @@ public async Task DeviceCameOnline_BannedByName() var viewerHub = new Mock>(); var expiringTokenService = new Mock(); var serviceSessionCache = new Mock(); + var messenger = new Mock(); var logger = new Mock>(); appConfig.Setup(x => x.BannedDevices).Returns(new string[] { $"{_testData.Org1Device1.DeviceName}" }); var hub = new AgentHub( - _dataService, - appConfig.Object, - serviceSessionCache.Object, - viewerHub.Object, - circuitManager.Object, + _dataService, + appConfig.Object, + serviceSessionCache.Object, + viewerHub.Object, + circuitManager.Object, expiringTokenService.Object, + messenger.Object, logger.Object); var hubClients = new Mock>(); @@ -71,6 +75,7 @@ public async Task DeviceCameOnline_BannedById() var viewerHub = new Mock>(); var expiringTokenService = new Mock(); var serviceSessionCache = new Mock(); + var messenger = new Mock(); var logger = new Mock>(); appConfig.Setup(x => x.BannedDevices).Returns(new string[] { _testData.Org1Device1.ID }); @@ -82,6 +87,7 @@ public async Task DeviceCameOnline_BannedById() viewerHub.Object, circuitManager.Object, expiringTokenService.Object, + messenger.Object, logger.Object); var hubClients = new Mock>(); @@ -123,7 +129,7 @@ private class CallerContext : HubCallerContext public override void Abort() { - + } } } diff --git a/Tests/Server.Tests/CircuitConnectionTests.cs b/Tests/Server.Tests/CircuitConnectionTests.cs index 77a5f5bd8..03b51f553 100644 --- a/Tests/Server.Tests/CircuitConnectionTests.cs +++ b/Tests/Server.Tests/CircuitConnectionTests.cs @@ -1,6 +1,7 @@ #nullable enable using Castle.Core.Logging; using Immense.RemoteControl.Server.Services; +using Immense.SimpleMessenger; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -37,6 +38,7 @@ public class CircuitConnectionTests private Mock _toastService; private Mock _expiringTokenService; private Mock _remoteControlSessionCache; + private Mock _messenger; private Mock _agentSessionCache; private Mock> _logger; private CircuitConnection _circuitConnection; @@ -57,6 +59,7 @@ public async Task Init() _toastService = new Mock(); _expiringTokenService = new Mock(); _remoteControlSessionCache = new Mock(); + _messenger = new Mock(); _agentSessionCache = new Mock(); _logger = new Mock>(); @@ -71,6 +74,7 @@ public async Task Init() _expiringTokenService.Object, _remoteControlSessionCache.Object, _agentSessionCache.Object, + _messenger.Object, _logger.Object); } diff --git a/Tests/Shared.Tests/ConcurrentListTests.cs b/Tests/Shared.Tests/ConcurrentListTests.cs new file mode 100644 index 000000000..2d611284a --- /dev/null +++ b/Tests/Shared.Tests/ConcurrentListTests.cs @@ -0,0 +1,80 @@ +using Remotely.Shared.Primitives; + +namespace Remotely.Shared.Tests; + +[TestClass] +public class ConcurrentListTests +{ + private readonly int _startCount = 500_000; + private ConcurrentList _list = new(); + + [TestInitialize] + public void Setup() + { + _list = new ConcurrentList(); + for (var i = 0; i < _startCount; i++) + { + _list.Add(i); + } + } + + + [TestMethod] + public void MultipleOperations_GivenMultipleThreads_Ok() + { + Assert.IsTrue(_list.Contains(500)); + Assert.IsTrue(_list.Contains(100_002)); + + var reset1 = new ManualResetEvent(false); + var reset2 = new ManualResetEvent(false); + var exceptions = 0; + + // Add and remove items from two separate background threads. + _ = Task.Run(() => + { + for (var i = 0; i < 5_000; i++) + { + try + { + Assert.IsTrue(_list.Remove(500 + i)); + _list.RemoveAt(100_000); + _list.Add(42); + _list.Insert(200_000, 100); + } + catch + { + Interlocked.Increment(ref exceptions); + } + } + reset1.Set(); + }); + + _ = Task.Run(() => + { + for (var i = 5_000; i < 10_000; i++) + { + try + { + Assert.IsTrue(_list.Remove(500 + i)); + _list.RemoveAt(100_000); + _list.Add(42); + _list.Insert(200_000, 100); + } + catch + { + Interlocked.Increment(ref exceptions); + } + } + reset2.Set(); + }); + + reset1.WaitOne(); + reset2.WaitOne(); + + Assert.IsFalse(_list.Contains(500)); + Assert.IsFalse(_list.Contains(100_002)); + // We should have the original count with which we started. + Assert.AreEqual(_startCount, _list.Count); + Assert.AreEqual(0, exceptions); + } +} \ No newline at end of file