| | | 1 | | using System; |
| | | 2 | | using System.Collections.Generic; |
| | | 3 | | using System.Linq; |
| | | 4 | | using System.Threading; |
| | | 5 | | using System.Threading.Tasks; |
| | | 6 | | using SwitchBlade.Contracts; |
| | | 7 | | using SwitchBlade.Core; |
| | | 8 | | |
| | | 9 | | namespace SwitchBlade.Services |
| | | 10 | | { |
| | | 11 | | /// <summary> |
| | | 12 | | /// Runs UIA providers out-of-process via the UIA Worker Client with streaming. |
| | | 13 | | /// Uses a separate lock so slow UIA scans don't block core window updates. |
| | | 14 | | /// </summary> |
| | 58 | 15 | | public class UiaProviderRunner(IUiaWorkerClient uiaWorkerClient, ILogger? logger = null) : IProviderRunner, IDisposa |
| | | 16 | | { |
| | 59 | 17 | | private readonly IUiaWorkerClient _uiaWorkerClient = uiaWorkerClient ?? throw new ArgumentNullException(nameof(u |
| | 58 | 18 | | private readonly ILogger? _logger = logger; |
| | 58 | 19 | | private readonly SemaphoreSlim _uiaRefreshLock = new(1, 1); |
| | | 20 | | private bool _disposed; |
| | | 21 | | |
| | | 22 | | /// <inheritdoc /> |
| | | 23 | | public Task RunAsync( |
| | | 24 | | IList<IWindowProvider> providers, |
| | | 25 | | IEnumerable<string> disabledPlugins, |
| | | 26 | | IEnumerable<string> handledProcesses, |
| | | 27 | | Action<IWindowProvider, List<WindowItem>> onResults) |
| | 16 | 28 | | { |
| | | 29 | | // Non-blocking: skip if a previous UIA scan is still running. |
| | 16 | 30 | | if (!_uiaRefreshLock.Wait(0)) |
| | 3 | 31 | | { |
| | 3 | 32 | | _logger?.Log("UIA refresh skipped: previous UIA scan still in progress."); |
| | 3 | 33 | | return Task.CompletedTask; |
| | | 34 | | } |
| | | 35 | | |
| | | 36 | | // Fire-and-forget: runs independently of the fast-path refresh. |
| | 13 | 37 | | _ = Task.Run(async () => |
| | 13 | 38 | | { |
| | 13 | 39 | | try |
| | 13 | 40 | | { |
| | 13 | 41 | | var uiaDisabled = new HashSet<string>( |
| | 10 | 42 | | providers.Where(p => disabledPlugins.Contains(p.PluginName)).Select(p => p.PluginName), |
| | 13 | 43 | | StringComparer.OrdinalIgnoreCase); |
| | 13 | 44 | | |
| | 13 | 45 | | // Build a lookup for fast provider resolution by name |
| | 13 | 46 | | var providerLookup = providers.ToDictionary( |
| | 10 | 47 | | p => p.PluginName, |
| | 10 | 48 | | p => p, |
| | 13 | 49 | | StringComparer.OrdinalIgnoreCase); |
| | 13 | 50 | | |
| | 13 | 51 | | // Pre-build map for O(1) fallback lookup |
| | 13 | 52 | | var processProviderMap = BuildProcessToProviderMap(providers); |
| | 13 | 53 | | |
| | 13 | 54 | | _logger?.Log($"[UIA] Starting streaming scan for {providers.Count} UIA providers..."); |
| | 13 | 55 | | |
| | 13 | 56 | | // Stream results as each plugin completes |
| | 50 | 57 | | await foreach (var pluginResult in _uiaWorkerClient.ScanStreamingAsync(uiaDisabled, handledProcesses |
| | 7 | 58 | | { |
| | 7 | 59 | | if (pluginResult.Error != null) |
| | 2 | 60 | | { |
| | 2 | 61 | | _logger?.Log($"[UIA] Plugin {pluginResult.PluginName} error: {pluginResult.Error}"); |
| | 2 | 62 | | } |
| | 13 | 63 | | |
| | 13 | 64 | | // Find the provider for this plugin's results |
| | 7 | 65 | | if (!providerLookup.TryGetValue(pluginResult.PluginName, out var uiaProvider)) |
| | 4 | 66 | | { |
| | 13 | 67 | | // Fallback: dynamically resolve by process name |
| | 4 | 68 | | if (pluginResult.Windows?.FirstOrDefault() is { } firstWindow |
| | 4 | 69 | | && processProviderMap.TryGetValue(firstWindow.ProcessName, out var resolvedProvider)) |
| | 2 | 70 | | { |
| | 2 | 71 | | uiaProvider = resolvedProvider; |
| | 2 | 72 | | } |
| | 4 | 73 | | } |
| | 13 | 74 | | |
| | 7 | 75 | | if (uiaProvider == null) |
| | 2 | 76 | | { |
| | 2 | 77 | | _logger?.Log($"[UIA] No provider found for plugin {pluginResult.PluginName}, skipping result |
| | 2 | 78 | | continue; |
| | 13 | 79 | | } |
| | 13 | 80 | | |
| | 13 | 81 | | // Convert to WindowItems and set Source |
| | 5 | 82 | | var windowItems = pluginResult.Windows? |
| | 4 | 83 | | .Select(w => new WindowItem |
| | 4 | 84 | | { |
| | 4 | 85 | | Hwnd = new IntPtr(w.Hwnd), |
| | 4 | 86 | | Title = w.Title, |
| | 4 | 87 | | ProcessName = w.ProcessName, |
| | 4 | 88 | | ExecutablePath = w.ExecutablePath, |
| | 4 | 89 | | IsFallback = w.IsFallback, |
| | 4 | 90 | | Source = uiaProvider |
| | 4 | 91 | | }) |
| | 5 | 92 | | .ToList() ?? []; |
| | 13 | 93 | | |
| | 5 | 94 | | _logger?.Log($"[UIA] Plugin {pluginResult.PluginName} returned {windowItems.Count} windows - pro |
| | 13 | 95 | | |
| | 13 | 96 | | // Process and emit event IMMEDIATELY |
| | 5 | 97 | | onResults(uiaProvider, windowItems); |
| | 5 | 98 | | } |
| | 13 | 99 | | |
| | 10 | 100 | | _logger?.Log("[UIA] Streaming scan complete."); |
| | 10 | 101 | | } |
| | 3 | 102 | | catch (Exception ex) |
| | 3 | 103 | | { |
| | 3 | 104 | | _logger?.LogError($"UIA Worker streaming error: {ex.Message}", ex); |
| | 3 | 105 | | } |
| | 13 | 106 | | finally |
| | 13 | 107 | | { |
| | 13 | 108 | | _uiaRefreshLock.Release(); |
| | 13 | 109 | | } |
| | 13 | 110 | | }); |
| | | 111 | | |
| | 13 | 112 | | return Task.CompletedTask; |
| | 29 | 113 | | } |
| | | 114 | | |
| | | 115 | | private static Dictionary<string, IWindowProvider> BuildProcessToProviderMap(IList<IWindowProvider> providers) |
| | 13 | 116 | | { |
| | 13 | 117 | | var map = new Dictionary<string, IWindowProvider>(StringComparer.OrdinalIgnoreCase); |
| | 59 | 118 | | foreach (var provider in providers) |
| | 10 | 119 | | { |
| | 10 | 120 | | if (provider is IProviderExclusionSettings exclusionSettings) |
| | 10 | 121 | | { |
| | 34 | 122 | | foreach (var process in exclusionSettings.GetHandledProcesses()) |
| | 2 | 123 | | { |
| | 2 | 124 | | if (!map.ContainsKey(process)) |
| | 2 | 125 | | { |
| | 2 | 126 | | map[process] = provider; |
| | 2 | 127 | | } |
| | 2 | 128 | | } |
| | 10 | 129 | | } |
| | 10 | 130 | | } |
| | 13 | 131 | | return map; |
| | 13 | 132 | | } |
| | | 133 | | |
| | | 134 | | public void Dispose() |
| | 24 | 135 | | { |
| | 33 | 136 | | if (_disposed) return; |
| | 15 | 137 | | _disposed = true; |
| | 15 | 138 | | _uiaRefreshLock.Dispose(); |
| | 15 | 139 | | GC.SuppressFinalize(this); |
| | 24 | 140 | | } |
| | | 141 | | } |
| | | 142 | | } |