|
| 1 | +using Microsoft.AspNetCore.SignalR; |
| 2 | + |
| 3 | +using System; |
| 4 | +using System.Collections.Concurrent; |
| 5 | +using System.Threading.Tasks; |
| 6 | + |
| 7 | +using Common.Domain.Models; |
| 8 | +using UiaPeek.Domain; |
| 9 | +using ChromiumPeek.Domain.Models; |
| 10 | + |
| 11 | +namespace ChromiumPeek.Domain.Hubs |
| 12 | +{ |
| 13 | + /// <summary> |
| 14 | + /// SignalR hub for handling UI Automation (UIA) peek operations. |
| 15 | + /// Provides real-time communication for heartbeat checks and |
| 16 | + /// ancestor chain inspection at specific screen coordinates. |
| 17 | + /// </summary> |
| 18 | + public class ChromiumPeekHub(IChromiumPeekRepository repository) : Hub |
| 19 | + { |
| 20 | + // Collection of active recording sessions keyed by a unique session id. |
| 21 | + private readonly static ConcurrentDictionary<string, ConcurrentBag<ChromiumChainModel>> s_sessions = new(); |
| 22 | + |
| 23 | + // Repository used for querying UIA elements at coordinates. |
| 24 | + private readonly IChromiumPeekRepository _repository = repository; |
| 25 | + |
| 26 | + // Sends a heartbeat message to the caller. |
| 27 | + // This can be used by clients to verify the connection is alive. |
| 28 | + [HubMethodName(name: nameof(SendHeartbeat))] |
| 29 | + public Task SendHeartbeat() |
| 30 | + { |
| 31 | + // Notify the calling client with a heartbeat message. |
| 32 | + return Clients.Caller.SendAsync( |
| 33 | + method: "ReceiveHeartbeat", |
| 34 | + arg1: new HubResponseModel("Heartbeat received - connection is alive")); |
| 35 | + } |
| 36 | + |
| 37 | + // Resolves the UIA element at the given screen coordinates and |
| 38 | + // returns its ancestor chain back to the caller. |
| 39 | + [HubMethodName(name: $"{nameof(SendPeek)}At")] |
| 40 | + public Task SendPeek(RecorderPointModel point) |
| 41 | + { |
| 42 | + // Query the repository to get the UIA ancestor chain at the given coordinates. |
| 43 | + var peekResponse = _repository.Peek(x: point.XPos, y: point.YPos); |
| 44 | + |
| 45 | + // Send the result back to the calling client. |
| 46 | + return Clients.Caller.SendAsync( |
| 47 | + method: "ReceivePeek", |
| 48 | + arg1: new HubResponseModel(peekResponse)); |
| 49 | + } |
| 50 | + |
| 51 | + // Resolves the UIA element at the given screen coordinates and |
| 52 | + // returns its ancestor chain back to the caller. |
| 53 | + [HubMethodName(name: $"{nameof(SendPeek)}Focused")] |
| 54 | + public Task SendPeek() |
| 55 | + { |
| 56 | + // Query the repository to get the UIA ancestor chain from the currently focused element. |
| 57 | + var peekResponse = _repository.Peek(); |
| 58 | + |
| 59 | + // Send the result back to the calling client. |
| 60 | + return Clients.Caller.SendAsync( |
| 61 | + method: "ReceivePeek", |
| 62 | + arg1: new HubResponseModel(peekResponse)); |
| 63 | + } |
| 64 | + |
| 65 | + // Starts a new recording session for the current SignalR caller. |
| 66 | + [HubMethodName(name: $"{nameof(StartRecordingSession)}")] |
| 67 | + public Task StartRecordingSession() |
| 68 | + { |
| 69 | + // Generate a unique identifier for this caller's recording session. |
| 70 | + var session = Guid.NewGuid().ToString(); |
| 71 | + |
| 72 | + // Initialize storage for this session's recorded events/actions. |
| 73 | + // Assumes `_sessions` is a (thread-safe) dictionary keyed by session id. |
| 74 | + s_sessions[session] = []; |
| 75 | + |
| 76 | + // Notify ONLY the invoking client that the session has started and |
| 77 | + // return the session id as the payload. The client should listen to |
| 78 | + // "RecordingSessionStarted" and extract the `Value` field. |
| 79 | + return Clients.Caller.SendAsync( |
| 80 | + method: "RecordingSessionStarted", |
| 81 | + arg1: new HubResponseModel(session)); |
| 82 | + } |
| 83 | + |
| 84 | + // Stops an existing recording session for the current SignalR caller. |
| 85 | + [HubMethodName(name: $"{nameof(StopRecordingSession)}")] |
| 86 | + public Task StopRecordingSession(string session) |
| 87 | + { |
| 88 | + // Remove the session from the active sessions collection. |
| 89 | + s_sessions.TryRemove(session, out var chains); |
| 90 | + |
| 91 | + // Notify ONLY the invoking client that the session has stopped. |
| 92 | + return Clients.Caller.SendAsync( |
| 93 | + method: "RecordingSessionStopped", |
| 94 | + arg1: new HubResponseModel(chains)); |
| 95 | + } |
| 96 | + |
| 97 | + /// <summary> |
| 98 | + /// Lightweight envelope for hub-to-client messages that carry a single value. |
| 99 | + /// </summary> |
| 100 | + /// <param name="value">The payload to send to the client.</param> |
| 101 | + private sealed class HubResponseModel(object value) |
| 102 | + { |
| 103 | + /// <summary> |
| 104 | + /// The payload carried by this response. Using <see cref="object"/> allows |
| 105 | + /// any serializable value (string, number, DTO, etc.). |
| 106 | + /// </summary> |
| 107 | + public object Value { get; init; } = value; |
| 108 | + } |
| 109 | + } |
| 110 | +} |
0 commit comments