diff --git a/OpenAlprWebhookProcessor/ProcessorHub/IProcessorHub.cs b/OpenAlprWebhookProcessor/ProcessorHub/IProcessorHub.cs index 76798d7a..97ccac71 100644 --- a/OpenAlprWebhookProcessor/ProcessorHub/IProcessorHub.cs +++ b/OpenAlprWebhookProcessor/ProcessorHub/IProcessorHub.cs @@ -10,6 +10,10 @@ public interface IProcessorHub Task ProcessInformationLogged(string log); + Task OpenAlprAgentConnected(string agentId, string ipAddress); + + Task OpenAlprAgentDisconnected(string agentId, string ipAddress); + Task ScrapeFinished(); } } diff --git a/OpenAlprWebhookProcessor/WebhookProcessor/OpenAlprWebsocket/OpenAlprWebsocketClient.cs b/OpenAlprWebhookProcessor/WebhookProcessor/OpenAlprWebsocket/OpenAlprWebsocketClient.cs index 7b7ad0e4..b4a7ad5e 100644 --- a/OpenAlprWebhookProcessor/WebhookProcessor/OpenAlprWebsocket/OpenAlprWebsocketClient.cs +++ b/OpenAlprWebhookProcessor/WebhookProcessor/OpenAlprWebsocket/OpenAlprWebsocketClient.cs @@ -7,7 +7,7 @@ using System.Collections.Concurrent; using System.Text.RegularExpressions; using System.Text.Json; -using static System.Runtime.InteropServices.JavaScript.JSType; +using Microsoft.Extensions.Logging; namespace OpenAlprWebhookProcessor.WebhookProcessor.OpenAlprWebsocket { @@ -19,10 +19,14 @@ public partial class OpenAlprWebsocketClient private readonly string _agentId; + private readonly ILogger _logger; + public OpenAlprWebsocketClient( + ILogger logger, string agentId, WebSocket webSocket) { + _logger = logger; _agentId = agentId; _webSocket = webSocket; _availableResponses = new ConcurrentDictionary(); @@ -89,6 +93,7 @@ public bool TryGetAgentStatusResponse( } catch (Exception ex) { + _logger.LogError(ex, "Failed to deserialize AgentStatusResponse"); agentStatusResponse = null; return false; } diff --git a/OpenAlprWebhookProcessor/WebhookProcessor/OpenAlprWebsocket/WebsocketClientOrganizer.cs b/OpenAlprWebhookProcessor/WebhookProcessor/OpenAlprWebsocket/WebsocketClientOrganizer.cs index c50ea96f..231b2dd1 100644 --- a/OpenAlprWebhookProcessor/WebhookProcessor/OpenAlprWebsocket/WebsocketClientOrganizer.cs +++ b/OpenAlprWebhookProcessor/WebhookProcessor/OpenAlprWebsocket/WebsocketClientOrganizer.cs @@ -1,12 +1,7 @@ using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; using System; using System.Collections.Concurrent; using System.Diagnostics; -using System.Linq; -using System.Net.WebSockets; -using System.Text; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -16,13 +11,8 @@ public class WebsocketClientOrganizer : BackgroundService { private readonly ConcurrentDictionary _connectedClients; - private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - - private readonly ILogger _logger; - - public WebsocketClientOrganizer(ILogger logger) + public WebsocketClientOrganizer() { - _logger = logger; _connectedClients = new ConcurrentDictionary(); } @@ -35,9 +25,17 @@ public bool AddAgent( string agentId, OpenAlprWebsocketClient webSocketClient) { - return _connectedClients.TryAdd( + var wasUpdated = false; + + _connectedClients.AddOrUpdate( agentId, - webSocketClient); + webSocketClient, (key, oldValue) => + { + wasUpdated = true; + return webSocketClient; + }); + + return wasUpdated; } public async Task RemoveAgentAsync( diff --git a/OpenAlprWebhookProcessor/WebhookProcessor/OpenAlprWebsocket/WebsocketController.cs b/OpenAlprWebhookProcessor/WebhookProcessor/OpenAlprWebsocket/WebsocketController.cs index 5236babc..d2bcf986 100644 --- a/OpenAlprWebhookProcessor/WebhookProcessor/OpenAlprWebsocket/WebsocketController.cs +++ b/OpenAlprWebhookProcessor/WebhookProcessor/OpenAlprWebsocket/WebsocketController.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Http; +using System; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -6,6 +7,8 @@ using System.Threading.Tasks; using System.Threading; using System.Text.Json; +using Microsoft.AspNetCore.SignalR; +using System.Linq; namespace OpenAlprWebhookProcessor.WebhookProcessor.OpenAlprWebsocket { @@ -20,14 +23,18 @@ public class WebsocketController : ControllerBase private readonly WebsocketClientOrganizer _websocketClientOrganizer; + private readonly IHubContext _processorHub; + public WebsocketController( ILogger logger, + IHubContext processorHub, ProcessorContext processorContext, WebsocketClientOrganizer websocketClientOrganizer) { _logger = logger; _processorContext = processorContext; _websocketClientOrganizer = websocketClientOrganizer; + _processorHub = processorHub; } [HttpPost("/api/accountinfo")] @@ -50,7 +57,7 @@ public async Task GetWebsocket(CancellationToken cancellationToken) { if (HttpContext.WebSockets.IsWebSocketRequest) { - _logger.LogInformation("websocket connection received."); + _logger.LogInformation("Websocket connection received."); var agent = await _processorContext.Agents .AsNoTracking() @@ -58,22 +65,35 @@ public async Task GetWebsocket(CancellationToken cancellationToken) var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); - var webSocketClient = new OpenAlprWebsocketClient(agent.Uid, webSocket); + var webSocketClient = new OpenAlprWebsocketClient( + _logger, + agent.Uid, + webSocket); if (_websocketClientOrganizer.AddAgent(agent.Uid, webSocketClient)) + { + _logger.LogError("Multiple websocket connections for the same agent, overwriting old connection: {agentId}.", agent.Uid); + } + + await _processorHub.Clients.All.OpenAlprAgentConnected(agent.Uid, HttpContext.Connection.RemoteIpAddress.ToString()); + + try { await webSocketClient.ConsumeMessagesAsync(cancellationToken); await _websocketClientOrganizer.RemoveAgentAsync(agent.Uid, cancellationToken); } - else + catch (Exception ex) { - _logger.LogError("Unable to add websocket connection for agent: {agentId}.", agent.Uid); + _logger.LogError(ex, "Websocket connection closed ungracefully."); + + await _websocketClientOrganizer.RemoveAgentAsync(agent.Uid, cancellationToken); + await _processorHub.Clients.All.OpenAlprAgentDisconnected(agent.Uid, HttpContext.Connection.RemoteIpAddress.ToString()); } } else { - _logger.LogInformation("non websocket connection received."); + _logger.LogInformation("Non websocket connection received."); HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest; } } diff --git a/OpenAlprWebhookProcessor/angularapp/src/app/settings/openalpr-agent/openalpr-agent.component.ts b/OpenAlprWebhookProcessor/angularapp/src/app/settings/openalpr-agent/openalpr-agent.component.ts index a8255ab9..0f061df6 100644 --- a/OpenAlprWebhookProcessor/angularapp/src/app/settings/openalpr-agent/openalpr-agent.component.ts +++ b/OpenAlprWebhookProcessor/angularapp/src/app/settings/openalpr-agent/openalpr-agent.component.ts @@ -96,6 +96,9 @@ export class OpenalprAgentComponent implements OnInit { key: "Version", value: this.agentStatus.version, }); + }, + (error) => { + this.agentStatus.isConnected = false; }); } diff --git a/OpenAlprWebhookProcessor/angularapp/src/app/settings/systemLogs/systemLogs.component.ts b/OpenAlprWebhookProcessor/angularapp/src/app/settings/systemLogs/systemLogs.component.ts index 4e086b42..be5d8afe 100644 --- a/OpenAlprWebhookProcessor/angularapp/src/app/settings/systemLogs/systemLogs.component.ts +++ b/OpenAlprWebhookProcessor/angularapp/src/app/settings/systemLogs/systemLogs.component.ts @@ -41,6 +41,7 @@ export class SystemLogsComponent implements OnInit, AfterViewInit, OnDestroy { this.subscribeForLogs(); }) } + public subscribeForLogs() { this.subscriptions.add(this.signalRHub.processInformationLogged.subscribe(logInformation => { this.logMessages.unshift(logInformation); diff --git a/OpenAlprWebhookProcessor/angularapp/src/app/signalr/signalr.service.ts b/OpenAlprWebhookProcessor/angularapp/src/app/signalr/signalr.service.ts index 1976c916..935ba609 100644 --- a/OpenAlprWebhookProcessor/angularapp/src/app/signalr/signalr.service.ts +++ b/OpenAlprWebhookProcessor/angularapp/src/app/signalr/signalr.service.ts @@ -14,6 +14,7 @@ export class SignalrService { public licensePlateReceived = new Subject(); public licensePlateAlerted = new Subject(); public processInformationLogged = new Subject(); + public openAlprAgentConnected = new Subject(); public isConnected: boolean; public connectionStatusChanged: Subject = new Subject(); @@ -43,6 +44,17 @@ export class SignalrService { this.processInformationLogged.next(logMessage); }); + this.hubConnection.on('OpenAlprAgentConnected', (agentId, ipAddress) => { + this.snackbarService.create(`OpenALPR Agent Connected: ${agentId}`, SnackBarType.Connected, `IP Address: ${ipAddress}`); + }); + + this.hubConnection.on('OpenAlprAgentDisconnected', (agentId, ipAddress) => { + this.snackbarService.create( + `OpenALPR Agent Disconnected: ${agentId}`, + SnackBarType.Disconnected, + "IP Address: " + ipAddress); + }); + this.hubConnection.on('LicensePlateRecorded', (plateNumber) => { this.licensePlateReceived.next(plateNumber); }); diff --git a/OpenAlprWebhookProcessor/angularapp/src/app/snackbar/snackbar.component.html b/OpenAlprWebhookProcessor/angularapp/src/app/snackbar/snackbar.component.html index 682019d6..3cd7706d 100644 --- a/OpenAlprWebhookProcessor/angularapp/src/app/snackbar/snackbar.component.html +++ b/OpenAlprWebhookProcessor/angularapp/src/app/snackbar/snackbar.component.html @@ -1,8 +1,13 @@
-
+
{{getIcon}}
-
- {{data.message}} +
+
+ {{data.message}} +
+
+ {{data.message2}} +
\ No newline at end of file diff --git a/OpenAlprWebhookProcessor/angularapp/src/app/snackbar/snackbar.service.ts b/OpenAlprWebhookProcessor/angularapp/src/app/snackbar/snackbar.service.ts index 206bd86f..3fd84af7 100644 --- a/OpenAlprWebhookProcessor/angularapp/src/app/snackbar/snackbar.service.ts +++ b/OpenAlprWebhookProcessor/angularapp/src/app/snackbar/snackbar.service.ts @@ -9,15 +9,16 @@ import { SnackBarType } from './snackbartype'; export class SnackbarService { constructor(private snackBar: MatSnackBar) { } - create(message: string, snackBarType: SnackBarType) { + create(message: string, snackBarType: SnackBarType, message2?: string) { this.snackBar.openFromComponent( SnackbarComponent, { horizontalPosition: 'right', verticalPosition: 'bottom', - duration: 1500, + duration: 3000, data: { message: message, + message2: message2, snackType: snackBarType }, });