using System; using System.IO; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using WindowsHelper.Models; using WindowsHelper.Services; namespace WindowsHelper { public class RpcHandler { private readonly JsonSerializerOptions jsonOptions; private readonly AccessibilityService accessibilityService; private readonly AudioService audioService; private readonly ShortcutMonitor? shortcutMonitor; private Action? audioCompletionHandler; public RpcHandler(ShortcutMonitor? shortcutMonitor = null) { this.shortcutMonitor = shortcutMonitor; // Use the generated converter settings from the models jsonOptions = WindowsHelper.Models.Converter.Settings; accessibilityService = new AccessibilityService(); audioService = new AudioService(); audioService.SoundPlaybackCompleted += OnSoundPlaybackCompleted; if (shortcutMonitor != null) { LogToStderr("RpcHandler: STA thread dispatch enabled via ShortcutMonitor"); } } public void ProcessRpcRequests(CancellationToken cancellationToken) { LogToStderr("RpcHandler: Starting RPC request processing loop."); try { string? line; while (!cancellationToken.IsCancellationRequested && (line = Console.ReadLine()) != null) { if (string.IsNullOrWhiteSpace(line)) { LogToStderr("Warning: Received empty line on stdin."); continue; } try { var request = JsonSerializer.Deserialize(line, jsonOptions); if (request != null) { LogToStderr($"RpcHandler: Received RPC Request ID {request.Id}, Method: {request.Method}"); _ = Task.Run(() => HandleRpcRequest(request), cancellationToken); } } catch (JsonException ex) { LogToStderr($"Error decoding RpcRequest from stdin: {ex.Message}. Line: {line}"); } } } catch (Exception ex) { LogToStderr($"Fatal error in RPC processing: {ex.Message}"); } LogToStderr("RpcHandler: RPC request processing loop finished."); } private async void HandleRpcRequest(RpcRequest request) { RpcResponse response; try { switch (request.Method) { case Method.GetAccessibilityTreeDetails: response = await HandleGetAccessibilityTreeDetails(request); break; case Method.GetAccessibilityContext: response = await HandleGetAccessibilityContext(request); break; case Method.PasteText: response = HandlePasteText(request); break; case Method.MuteSystemAudio: response = await HandleMuteSystemAudio(request); return; // Response sent after audio playback case Method.RestoreSystemAudio: response = HandleRestoreSystemAudio(request); break; default: LogToStderr($"Method not found: {request.Method} for ID: {request.Id}"); response = new RpcResponse { Id = request.Id.ToString(), Error = new Error { Code = -32601, Message = $"Method not found: {request.Method}" } }; break; } } catch (Exception ex) { LogToStderr($"Error handling request {request.Id}: {ex.Message}"); response = new RpcResponse { Id = request.Id.ToString(), Error = new Error { Code = -32603, Message = $"Internal error: {ex.Message}" } }; } SendRpcResponse(response); } private async Task HandleGetAccessibilityTreeDetails(RpcRequest request) { LogToStderr($"Handling getAccessibilityTreeDetails for ID: {request.Id}"); GetAccessibilityTreeDetailsParams? parameters = null; if (request.Params != null) { try { var json = JsonSerializer.Serialize(request.Params, jsonOptions); parameters = JsonSerializer.Deserialize(json, jsonOptions); } catch (Exception ex) { LogToStderr($"Error decoding params: {ex.Message}"); return new RpcResponse { Id = request.Id.ToString(), Error = new Error { Code = -32602, Message = $"Invalid params: {ex.Message}", Data = request.Params } }; } } // Get accessibility tree on UI thread var tree = await Task.Run(() => accessibilityService.FetchAccessibilityTree(parameters?.RootId)); return new RpcResponse { Id = request.Id.ToString(), Result = new GetAccessibilityTreeDetailsResult { Tree = tree } }; } private async Task HandleGetAccessibilityContext(RpcRequest request) { LogToStderr($"Handling getAccessibilityContext for ID: {request.Id}"); GetAccessibilityContextParams? parameters = null; if (request.Params != null) { try { var json = JsonSerializer.Serialize(request.Params, jsonOptions); parameters = JsonSerializer.Deserialize(json, jsonOptions); } catch (Exception ex) { LogToStderr($"Error decoding params: {ex.Message}"); return new RpcResponse { Id = request.Id.ToString(), Error = new Error { Code = -32602, Message = $"Invalid params: {ex.Message}", Data = request.Params } }; } } var editableOnly = parameters?.EditableOnly ?? false; var context = await Task.Run(() => accessibilityService.GetAccessibilityContext(editableOnly)); return new RpcResponse { Id = request.Id.ToString(), Result = new GetAccessibilityContextResult { Context = context } }; } private RpcResponse HandlePasteText(RpcRequest request) { LogToStderr($"Handling pasteText for ID: {request.Id}"); if (request.Params == null) { return new RpcResponse { Id = request.Id.ToString(), Error = new Error { Code = -32602, Message = "Missing params for pasteText" } }; } try { var json = JsonSerializer.Serialize(request.Params, jsonOptions); var parameters = JsonSerializer.Deserialize(json, jsonOptions); if (parameters != null) { var success = accessibilityService.PasteText(parameters.Transcript); return new RpcResponse { Id = request.Id.ToString(), Result = new PasteTextResult { Success = success, Message = success ? "Pasted successfully" : "Paste failed" } }; } } catch (Exception ex) { LogToStderr($"Error processing pasteText: {ex.Message}"); } return new RpcResponse { Id = request.Id.ToString(), Error = new Error { Code = -32603, Message = "Error during paste operation" } }; } private async Task HandleMuteSystemAudio(RpcRequest request) { LogToStderr($"Handling muteSystemAudio for ID: {request.Id}"); // Store the request ID for the completion handler var requestId = request.Id.ToString(); audioCompletionHandler = (id) => { LogToStderr($"rec-start.mp3 finished playing. Proceeding to mute system audio. ID: {id}"); var success = audioService.MuteSystemAudio(); var response = new RpcResponse { Id = id, Result = new MuteSystemAudioResult { Success = success, Message = success ? "Mute command sent" : "Failed to send mute command" } }; SendRpcResponse(response); audioCompletionHandler = null; }; // Play sound on STA thread if available (faster due to COM/audio API preferences) if (shortcutMonitor != null) { LogToStderr("Dispatching audio playback to STA thread"); await shortcutMonitor.InvokeOnStaAsync(async () => { await audioService.PlaySound("rec-start", requestId); return true; }); } else { await audioService.PlaySound("rec-start", requestId); } // Return dummy response (real response sent after audio completion) return new RpcResponse { Id = request.Id.ToString() }; } private RpcResponse HandleRestoreSystemAudio(RpcRequest request) { LogToStderr($"Handling restoreSystemAudio for ID: {request.Id}"); var success = audioService.RestoreSystemAudio(); if (success) { // Play sound asynchronously on STA thread if available (don't wait) if (shortcutMonitor != null) { shortcutMonitor.PostToSta(async () => { await audioService.PlaySound("rec-stop", request.Id.ToString()); }); } else { _ = audioService.PlaySound("rec-stop", request.Id.ToString()); } } return new RpcResponse { Id = request.Id.ToString(), Result = new RestoreSystemAudioResult { Success = success, Message = success ? "Restore command sent" : "Failed to send restore command" } }; } private void OnSoundPlaybackCompleted(object? sender, string requestId) { audioCompletionHandler?.Invoke(requestId); } private void SendRpcResponse(RpcResponse response) { try { var json = JsonSerializer.Serialize(response, jsonOptions); LogToStderr($"[RpcHandler] Sending response to stdout: {json}"); Console.WriteLine(json); Console.Out.Flush(); } catch (Exception ex) { LogToStderr($"Error encoding RpcResponse: {ex.Message}"); } } private void LogToStderr(string message) { var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); Console.Error.WriteLine($"[{timestamp}] {message}"); Console.Error.Flush(); } } }