< Summary

Information
Class: SwitchBlade.Services.WindowOrchestrationService
Assembly: SwitchBlade
File(s): D:\a\switchblade\switchblade\Services\WindowOrchestrationService.cs
Tag: 203_23722840422
Line coverage
100%
Covered lines: 166
Uncovered lines: 0
Coverable lines: 166
Total lines: 263
Line coverage: 100%
Branch coverage
100%
Covered branches: 86
Total branches: 86
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%1414100%
get_AllWindows()100%11100%
RefreshAsync()100%2424100%
LaunchUiaRefresh()100%44100%
ProcessProviderResults(...)100%2222100%
EmitEvent(...)100%22100%
HasExistingRealItems(...)100%66100%
get_CacheCount()100%11100%
Dispose()100%1010100%

File(s)

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

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Linq;
 4using System.Threading;
 5using System.Threading.Tasks;
 6using SwitchBlade.Contracts;
 7
 8namespace SwitchBlade.Services
 9{
 10    /// <summary>
 11    /// Coordinates parallel execution of window providers.
 12    /// Handles structural diffing, caching, and icon population (via IWindowReconciler).
 13    ///
 14    /// Delegates provider execution to <see cref="IProviderRunner"/> strategies:
 15    /// - In-process runners for fast, non-UIA providers
 16    /// - Out-of-process runners for UIA providers (prevents memory leaks)
 17    /// </summary>
 5218    public class WindowOrchestrationService(
 5219        IEnumerable<IWindowProvider> providers,
 5220        IWindowReconciler reconciler,
 5221        IUiaWorkerClient uiaWorkerClient,
 5222        INativeInteropWrapper nativeInterop,
 5223        IProviderRunner fastRunner,
 5224        IProviderRunner uiaRunner,
 5225        ILogger? logger = null) : IWindowOrchestrationService, IDisposable
 26    {
 6227        private readonly List<IWindowProvider> _providers = providers?.ToList() ?? throw new ArgumentNullException(nameo
 6028        private readonly IWindowReconciler _reconciler = reconciler ?? throw new ArgumentNullException(nameof(reconciler
 5829        private readonly INativeInteropWrapper _nativeInterop = nativeInterop ?? throw new ArgumentNullException(nameof(
 5630        private readonly IProviderRunner _fastRunner = fastRunner ?? throw new ArgumentNullException(nameof(fastRunner))
 5531        private readonly IProviderRunner _uiaRunner = uiaRunner ?? throw new ArgumentNullException(nameof(uiaRunner));
 5432        private readonly IUiaWorkerClient _uiaWorkerClient = uiaWorkerClient ?? throw new ArgumentNullException(nameof(u
 5233        private readonly ILogger? _logger = logger;
 5234        private readonly List<WindowItem> _allWindows = [];
 35
 5236        private readonly Lock _lock = new();
 37        // Re-entrancy guard for fast (Non-UIA) providers.
 5238        private readonly SemaphoreSlim _fastRefreshLock = new(1, 1);
 39        private bool _disposed;
 40
 41        public event EventHandler<WindowListUpdatedEventArgs>? WindowListUpdated;
 42
 43        public IReadOnlyList<WindowItem> AllWindows
 44        {
 45            get
 4046            {
 47                lock (_lock)
 4048                {
 4049                    return [.. _allWindows];
 50                }
 4051            }
 52        }
 53
 54        public async Task RefreshAsync(IEnumerable<string> disabledPlugins)
 6055        {
 56            // Non-blocking re-entrancy guard for fast (Non-UIA) providers.
 6057            if (!await _fastRefreshLock.WaitAsync(0))
 358            {
 359                _logger?.Log("RefreshAsync skipped: fast-path scan already in progress.");
 360                return;
 61            }
 62
 63            try
 5764            {
 5765                disabledPlugins ??= new HashSet<string>();
 66
 67                // Clear process cache for fresh lookups
 5768                _nativeInterop.ClearProcessCache();
 69
 70                // 1. Reload settings and gather handled processes (for all providers)
 5771                var handledProcesses = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 29572                foreach (var provider in _providers)
 6273                {
 74                    try
 6275                    {
 6276                        if (provider is IConfigurablePlugin configurable)
 5577                        {
 5578                            configurable.ReloadSettings();
 5379                        }
 80
 6081                        if (provider is IProviderExclusionSettings exclusionSettings)
 5882                        {
 18083                            foreach (var p in exclusionSettings.GetHandledProcesses())
 384                            {
 385                                handledProcesses.Add(p);
 386                            }
 5887                        }
 6088                    }
 289                    catch (Exception ex)
 290                    {
 291                        _logger?.LogError($"Error reloading settings for {provider.PluginName}", ex);
 292                    }
 6293                }
 94
 95                // 2. Inject exclusions (for all providers)
 29596                foreach (var provider in _providers)
 6297                {
 6298                    if (provider is IProviderExclusionSettings exclusionSettings)
 6099                    {
 60100                        exclusionSettings.SetExclusions(handledProcesses);
 60101                    }
 62102                }
 103
 104                // 3. Split providers into UIA (out-of-process) and non-UIA (in-process)
 119105                var nonUiaProviders = _providers.Where(p => !(p is IExtrusionStrategy s && s.IsUiaProvider)).ToList();
 119106                var uiaProviders = _providers.Where(p => p is IExtrusionStrategy s && s.IsUiaProvider).ToList();
 107
 108                // 4a. Run fast providers via the fast runner strategy
 57109                await _fastRunner.RunAsync(nonUiaProviders, disabledPlugins, handledProcesses, ProcessProviderResults);
 110
 111                // 4b. Run UIA providers via the UIA runner strategy (background)
 112                // UIA providers: fire-and-forget in background to avoid blocking core updates
 54113                if (uiaProviders.Count > 0)
 11114                {
 11115                    _ = LaunchUiaRefresh(uiaProviders, disabledPlugins, handledProcesses);
 11116                }
 54117            }
 3118            catch (Exception ex)
 3119            {
 3120                _logger?.LogError("Critical error during RefreshAsync", ex);
 3121            }
 122            finally
 57123            {
 57124                _fastRefreshLock.Release();
 57125            }
 60126        }
 127
 128        private async Task LaunchUiaRefresh(List<IWindowProvider> uiaProviders, IEnumerable<string> disabledPlugins, IEn
 11129        {
 130            try
 11131            {
 11132                _logger?.Log($"Launching background UIA refresh for {uiaProviders.Count} providers");
 11133                await _uiaRunner.RunAsync(uiaProviders, disabledPlugins, handledProcesses, ProcessProviderResults);
 9134            }
 2135            catch (Exception ex)
 2136            {
 2137                _logger?.LogError("Background UIA refresh failed", ex);
 2138            }
 11139        }
 140
 141        private void ProcessProviderResults(IWindowProvider provider, List<WindowItem> results)
 53142        {
 53143            WindowListUpdatedEventArgs args = null!;
 53144            List<WindowItem>? reconciled = null;
 145            lock (_lock)
 53146            {
 147                // Check LKG condition
 85148                if (results.Count > 0 && results.All(r => r.IsFallback))
 7149                {
 7150                    bool hasExistingRealItems = HasExistingRealItems(provider);
 7151                    if (hasExistingRealItems)
 3152                    {
 4153                        _logger?.Log($"[LKG] {provider.PluginName}: Transient failure (only fallback items received). Pr
 154
 155                        // DEFER Event Emission to outside the lock
 3156                        args = new WindowListUpdatedEventArgs(provider, false);
 3157                        goto EmitAndReturn;
 158                    }
 4159                }
 160
 50161                long start = System.Diagnostics.Stopwatch.GetTimestamp();
 162
 163                // Normal path: Replace existing items with new results
 130164                for (int i = _allWindows.Count - 1; i >= 0; i--)
 15165                {
 15166                    if (_allWindows[i].Source == provider)
 12167                        _allWindows.RemoveAt(i);
 15168                }
 169
 50170                reconciled = _reconciler.Reconcile(results, provider);
 50171                _allWindows.AddRange(reconciled);
 172
 50173                args = new WindowListUpdatedEventArgs(provider, true);
 174
 50175                if (_logger != null && SwitchBlade.Core.Logger.IsDebugEnabled)
 1176                {
 1177                    var elapsed = System.Diagnostics.Stopwatch.GetElapsedTime(start);
 1178                    _logger.Log($"[Perf] Reconciled {reconciled.Count} items for {provider.PluginName} in {elapsed.Total
 1179                }
 50180            }
 181
 53182        EmitAndReturn:
 183            // Emit event IMMEDIATELY so UI shows text - icons will pop in later
 53184            EmitEvent(args);
 185
 186            // If we jumped here from LKG, reconciled is null/empty, so we shouldn't populate icons.
 52187            if (reconciled != null && reconciled.Count > 0)
 28188            {
 28189                Task.Run(() =>
 28190                {
 28191                    try
 28192                    {
 28193                        _reconciler.PopulateIcons(reconciled);
 26194                    }
 2195                    catch (Exception ex)
 2196                    {
 2197                        _logger?.LogError($"Error populating icons for {provider.PluginName}", ex);
 2198                    }
 56199                });
 28200            }
 52201            return;
 52202        }
 203
 204        private void EmitEvent(WindowListUpdatedEventArgs args)
 53205        {
 53206            WindowListUpdated?.Invoke(this, args);
 52207        }
 208
 209        /// <summary>
 210        /// Checks whether any cached items for the given provider are non-fallback (real).
 211        /// Extracted for deterministic branch coverage of both conditions.
 212        /// </summary>
 213        private bool HasExistingRealItems(IWindowProvider provider)
 11214        {
 49215            foreach (var w in _allWindows)
 10216            {
 10217                if (w.Source == provider && !w.IsFallback)
 4218                    return true;
 6219            }
 7220            return false;
 11221        }
 222
 223        #region Encapsulated Cache Mutators (Delegated to Reconciler)
 224
 225        /// <summary>
 226        /// Gets the total number of cached window items (HWND + Provider records).
 227        /// Used for memory diagnostics.
 228        /// </summary>
 1229        public int CacheCount => _reconciler.CacheCount;
 230
 231        #endregion
 232
 233        public void Dispose()
 6234        {
 7235            if (_disposed) return;
 5236            _disposed = true;
 237
 23238            foreach (var provider in _providers)
 4239            {
 4240                if (provider is IDisposable disposable)
 4241                {
 242                    try
 4243                    {
 4244                        disposable.Dispose();
 2245                    }
 2246                    catch (Exception ex)
 2247                    {
 2248                        _logger?.LogError($"Error disposing provider {provider.PluginName}", ex);
 2249                    }
 4250                }
 4251            }
 252
 5253            _uiaWorkerClient.Dispose();
 5254            _fastRefreshLock.Dispose();
 255
 5256            if (_uiaRunner is IDisposable disposableRunner)
 4257            {
 4258                disposableRunner.Dispose();
 4259            }
 5260            GC.SuppressFinalize(this);
 6261        }
 262    }
 263}