< Summary

Information
Class: SwitchBlade.Services.UiaProviderRunner
Assembly: SwitchBlade
File(s): D:\a\switchblade\switchblade\Services\UiaProviderRunner.cs
Tag: 203_23722840422
Line coverage
100%
Covered lines: 108
Uncovered lines: 0
Coverable lines: 108
Total lines: 142
Line coverage: 100%
Branch coverage
100%
Covered branches: 16
Total branches: 16
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%22100%
RunAsync(...)100%44100%
BuildProcessToProviderMap(...)100%88100%
Dispose()100%22100%

File(s)

D:\a\switchblade\switchblade\Services\UiaProviderRunner.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Linq;
 4using System.Threading;
 5using System.Threading.Tasks;
 6using SwitchBlade.Contracts;
 7using SwitchBlade.Core;
 8
 9namespace 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>
 5815    public class UiaProviderRunner(IUiaWorkerClient uiaWorkerClient, ILogger? logger = null) : IProviderRunner, IDisposa
 16    {
 5917        private readonly IUiaWorkerClient _uiaWorkerClient = uiaWorkerClient ?? throw new ArgumentNullException(nameof(u
 5818        private readonly ILogger? _logger = logger;
 5819        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)
 1628        {
 29            // Non-blocking: skip if a previous UIA scan is still running.
 1630            if (!_uiaRefreshLock.Wait(0))
 331            {
 332                _logger?.Log("UIA refresh skipped: previous UIA scan still in progress.");
 333                return Task.CompletedTask;
 34            }
 35
 36            // Fire-and-forget: runs independently of the fast-path refresh.
 1337            _ = Task.Run(async () =>
 1338            {
 1339                try
 1340                {
 1341                    var uiaDisabled = new HashSet<string>(
 1042                        providers.Where(p => disabledPlugins.Contains(p.PluginName)).Select(p => p.PluginName),
 1343                        StringComparer.OrdinalIgnoreCase);
 1344
 1345                    // Build a lookup for fast provider resolution by name
 1346                    var providerLookup = providers.ToDictionary(
 1047                        p => p.PluginName,
 1048                        p => p,
 1349                        StringComparer.OrdinalIgnoreCase);
 1350
 1351                    // Pre-build map for O(1) fallback lookup
 1352                    var processProviderMap = BuildProcessToProviderMap(providers);
 1353
 1354                    _logger?.Log($"[UIA] Starting streaming scan for {providers.Count} UIA providers...");
 1355
 1356                    // Stream results as each plugin completes
 5057                    await foreach (var pluginResult in _uiaWorkerClient.ScanStreamingAsync(uiaDisabled, handledProcesses
 758                    {
 759                        if (pluginResult.Error != null)
 260                        {
 261                            _logger?.Log($"[UIA] Plugin {pluginResult.PluginName} error: {pluginResult.Error}");
 262                        }
 1363
 1364                        // Find the provider for this plugin's results
 765                        if (!providerLookup.TryGetValue(pluginResult.PluginName, out var uiaProvider))
 466                        {
 1367                            // Fallback: dynamically resolve by process name
 468                            if (pluginResult.Windows?.FirstOrDefault() is { } firstWindow
 469                                && processProviderMap.TryGetValue(firstWindow.ProcessName, out var resolvedProvider))
 270                            {
 271                                uiaProvider = resolvedProvider;
 272                            }
 473                        }
 1374
 775                        if (uiaProvider == null)
 276                        {
 277                            _logger?.Log($"[UIA] No provider found for plugin {pluginResult.PluginName}, skipping result
 278                            continue;
 1379                        }
 1380
 1381                        // Convert to WindowItems and set Source
 582                        var windowItems = pluginResult.Windows?
 483                            .Select(w => new WindowItem
 484                            {
 485                                Hwnd = new IntPtr(w.Hwnd),
 486                                Title = w.Title,
 487                                ProcessName = w.ProcessName,
 488                                ExecutablePath = w.ExecutablePath,
 489                                IsFallback = w.IsFallback,
 490                                Source = uiaProvider
 491                            })
 592                            .ToList() ?? [];
 1393
 594                        _logger?.Log($"[UIA] Plugin {pluginResult.PluginName} returned {windowItems.Count} windows - pro
 1395
 1396                        // Process and emit event IMMEDIATELY
 597                        onResults(uiaProvider, windowItems);
 598                    }
 1399
 10100                    _logger?.Log("[UIA] Streaming scan complete.");
 10101                }
 3102                catch (Exception ex)
 3103                {
 3104                    _logger?.LogError($"UIA Worker streaming error: {ex.Message}", ex);
 3105                }
 13106                finally
 13107                {
 13108                    _uiaRefreshLock.Release();
 13109                }
 13110            });
 111
 13112            return Task.CompletedTask;
 29113        }
 114
 115        private static Dictionary<string, IWindowProvider> BuildProcessToProviderMap(IList<IWindowProvider> providers)
 13116        {
 13117            var map = new Dictionary<string, IWindowProvider>(StringComparer.OrdinalIgnoreCase);
 59118            foreach (var provider in providers)
 10119            {
 10120                if (provider is IProviderExclusionSettings exclusionSettings)
 10121                {
 34122                    foreach (var process in exclusionSettings.GetHandledProcesses())
 2123                    {
 2124                        if (!map.ContainsKey(process))
 2125                        {
 2126                            map[process] = provider;
 2127                        }
 2128                    }
 10129                }
 10130            }
 13131            return map;
 13132        }
 133
 134        public void Dispose()
 24135        {
 33136            if (_disposed) return;
 15137            _disposed = true;
 15138            _uiaRefreshLock.Dispose();
 15139            GC.SuppressFinalize(this);
 24140        }
 141    }
 142}