diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsCache.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsCache.cs index 3bd8de31c..47446e5ec 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsCache.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsCache.cs @@ -57,13 +57,17 @@ internal class TabsCache } public bool Contains(AutomationElement tab) + { + return Contains(RuntimeIdToKey(tab)); + } + + public bool Contains(string runtimeId) { lock (_sync) { - var key = RuntimeIdToKey(tab); - if (key != null) + if (runtimeId != null) { - return _knownTabs.Contains(key); + return _knownTabs.Contains(runtimeId); } } return false; diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsFocusEventDispatcher.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsFocusEventDispatcher.cs new file mode 100644 index 000000000..1859d0a15 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsFocusEventDispatcher.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Threading; +using System.Windows.Automation; +using Flow.Launcher.Plugin.BrowserBookmark.Tabs; +using static Flow.Launcher.Plugin.BrowserBookmark.Main; + +internal sealed class TabsFocusEventDispatcher : IDisposable +{ + private static readonly string ClassName = nameof(TabsFocusEventDispatcher); + private readonly BlockingCollection> _queue = []; + private readonly Thread _worker; + private readonly CancellationTokenSource _cts = new(); + private readonly TabsWalker _walker; + private readonly TabsTracker _tracker; + + public TabsFocusEventDispatcher(TabsWalker walker, TabsTracker tracker) + { + _worker = new Thread(WorkerLoop) + { + IsBackground = true, + Name = "FocusEventWorker" + }; + _worker.Start(); + _walker = walker; + _tracker = tracker; + } + + public void Enqueue(string url, AutomationElement element, int processId) + { + if (!_queue.IsAddingCompleted) + _queue.Add(Tuple.Create(url, element, processId)); + } + + void WorkerLoop() + { + try + { + foreach (var element in _queue.GetConsumingEnumerable(_cts.Token)) + { + HandleFocus(element); + } + } + catch (OperationCanceledException) + { + // shutting down + } + } + + void HandleFocus(Tuple tuple) + { + var url = tuple.Item1; + var rootElement = tuple.Item2; + var processId = tuple.Item3; + + try + { + using var process = Process.GetProcessById(processId); + + var currentTab = _walker.GetCurrentTabFromWindow(rootElement, process, CancellationToken.None); + if (currentTab != null) + { + _tracker.RegisterTab(url, rootElement, currentTab); + } + } + catch (ArgumentException) + { + Context.API.LogError(ClassName, $"Process {processId} no longer runs"); + } + catch (Exception ex) + { + Context.API.LogException(ClassName, "Exception", ex); + } + } + + public void Dispose() + { + _cts.Cancel(); + _queue.CompleteAdding(); + _worker.Join(); + _cts.Dispose(); + _queue.Dispose(); + } +} diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsTracker.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsTracker.cs index bc5088885..c0e3fd97b 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsTracker.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsTracker.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Threading; using System.Windows.Automation; using BrowserTabs; using Flow.Launcher.Plugin.BrowserBookmark.Models; @@ -23,10 +22,11 @@ public class TabsTracker : IDisposable private static readonly HashSet chromiumProcessNames = new HashSet(["msedge", "chrome", "brave", "vivaldi", "opera", "chromium"], StringComparer.OrdinalIgnoreCase); private static readonly HashSet firefoxProcessNames = new HashSet(["firefox"], StringComparer.OrdinalIgnoreCase); private readonly TabsWalker _walker = new(); - private string? _expectedUrl; + private readonly Queue _expectedUrls = []; private Dictionary UrlToBrowserTab { get; } = []; private readonly object _sync = new(); + private TabsFocusEventDispatcher _focusHandlerDispatcher; private AutomationFocusChangedEventHandler? _focusHandler; private readonly HashSet _browserWindowsTracked = []; private bool _initialized; @@ -76,6 +76,7 @@ public class TabsTracker : IDisposable if (_initialized) return; + _focusHandlerDispatcher = new(_walker, this); _focusHandler = OnFocusChanged; Automation.AddAutomationFocusChangedEventHandler(_focusHandler); _initialized = true; @@ -94,17 +95,14 @@ public class TabsTracker : IDisposable _focusHandler = null; _initialized = false; } + _focusHandlerDispatcher?.Dispose(); } public void ExpectUrl(string url) { lock (_sync) { - if (_expectedUrl != null) - { - Context.API.LogError(ClassName, $"Opening {url} while older is still not resolved ({_expectedUrl}). Forgetting the older."); - } - _expectedUrl = url; + _expectedUrls.Enqueue(url); } } @@ -130,22 +128,27 @@ public class TabsTracker : IDisposable private void OnFocusChanged(object sender, AutomationFocusChangedEventArgs e) { - string? urlToBind; lock (_sync) { - urlToBind = _expectedUrl; + if (_expectedUrls.Count == 0) + return; } - if (urlToBind is null) - return; try { - Context.API.LogDebug(ClassName, $"Searching for... {urlToBind}"); - if (sender is not AutomationElement element) return; - int pid = element.Current.ProcessId; + int pid = 0; + try + { + pid = element.Current.ProcessId; + } + catch (ElementNotAvailableException) + { + return; + } + using var process = Process.GetProcessById(pid); var chromium = chromiumProcessNames.Contains(process.ProcessName); @@ -161,23 +164,21 @@ public class TabsTracker : IDisposable Context.API.LogDebug(ClassName, $"The root element is {rootElement.Current.Name}"); - var currentTab = _walker.GetCurrentTabFromWindow(rootElement, process, CancellationToken.None); - if (currentTab != null) + string? urlToBind; + lock (_sync) { - lock (_sync) - { - Context.API.LogDebug(ClassName, $"Registering {urlToBind} as tab: {currentTab.Title}"); - UrlToBrowserTab[urlToBind] = currentTab; - _expectedUrl = null; + if (_expectedUrls.Count == 0) + return; - // required to take the tab into account by Flow Launcher main UI search window - Context.API.ReQuery(); - } - - Automation.AddStructureChangedEventHandler(rootElement, TreeScope.Subtree, OnStructureChanged); - Automation.AddAutomationEventHandler(WindowPattern.WindowClosedEvent, rootElement, TreeScope.Subtree, OnWindowClosed); - _browserWindowsTracked.Add(rootElement); + urlToBind = _expectedUrls.Dequeue(); } + if (urlToBind is null) + return; + + Context.API.LogDebug(ClassName, $"Searching for... {urlToBind}"); + + // Further handling requires waiting for tabs so its better to run it on a separate thread + _focusHandlerDispatcher?.Enqueue(urlToBind, rootElement, pid); } catch (Exception ex) { @@ -187,22 +188,37 @@ public class TabsTracker : IDisposable private void OnStructureChanged(object sender, StructureChangedEventArgs e) { - // TODO: Consider ChildAdded to handle new tabs appearance instead of AutomationFocusChangedEventHandler - // However think twice if it is worthwhile as the current approach might already be a good one - //if (e.StructureChangeType == StructureChangeType.ChildAdded) + //if (e.StructureChangeType == StructureChangeType.ChildAdded || e.StructureChangeType == StructureChangeType.ChildrenBulkAdded) //{ //} - if (e.StructureChangeType == StructureChangeType.ChildRemoved) + var eventRuntimeId = TabsCache.RuntimeIdToKey(e.GetRuntimeId()); + switch (e.StructureChangeType) { - var eventRuntimeId = TabsCache.RuntimeIdToKey(e.GetRuntimeId()); - foreach (var window in _browserWindowsTracked) - { - var windowRuntimeId = TabsCache.RuntimeIdToKey(window); - if (windowRuntimeId != null && eventRuntimeId.StartsWith(windowRuntimeId)) + // TODO: Consider ChildAdded to handle new tabs appearance instead of AutomationFocusChangedEventHandler + // However think twice if it is worthwhile as the current approach based on focus might already be a good one + //case StructureChangeType.ChildAdded: + // Context.API.LogDebug(ClassName, $"StructureChangeType.ChildAdded occurred on {eventRuntimeId}"); + // break; + //case StructureChangeType.ChildrenBulkAdded: + // Context.API.LogDebug(ClassName, $"StructureChangeType.ChildrenBulkAdded occurred on {eventRuntimeId}"); + // break; + //case StructureChangeType.ChildrenInvalidated: + // _walker.CheckTabExistence(eventRuntimeId, "StructureChangeType.ChildrenInvalidated"); + // break; + //case StructureChangeType.ChildrenReordered: + // _walker.CheckTabExistence(eventRuntimeId, "StructureChangeType.ChildrenReordered"); + // break; + case StructureChangeType.ChildRemoved: + case StructureChangeType.ChildrenBulkRemoved: + foreach (var window in _browserWindowsTracked) { - _walker.RescanTabsForContainer(window); + var windowRuntimeId = TabsCache.RuntimeIdToKey(window); + if (windowRuntimeId != null && eventRuntimeId.StartsWith(windowRuntimeId)) + { + _walker.RescanTabsForContainer(window); + } } - } + break; } } @@ -224,4 +240,20 @@ public class TabsTracker : IDisposable _walker?.RemoveAllTabs(wnd); } } + + internal void RegisterTab(string url, AutomationElement rootElement, BrowserTab currentTab) + { + lock (_sync) + { + Context.API.LogDebug(ClassName, $"Registering {url} as tab: {currentTab.Title}"); + UrlToBrowserTab[url] = currentTab; + + // required to take the tab into account by Flow Launcher main UI search window + Context.API.ReQuery(); + } + + Automation.AddStructureChangedEventHandler(rootElement, TreeScope.Subtree, OnStructureChanged); + Automation.AddAutomationEventHandler(WindowPattern.WindowClosedEvent, rootElement, TreeScope.Subtree, OnWindowClosed); + _browserWindowsTracked.Add(rootElement); + } } diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsWalker.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsWalker.cs index 78f285538..c0e32a3f2 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsWalker.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Tabs/TabsWalker.cs @@ -68,7 +68,7 @@ internal class TabsWalker if (tabs.Count == 0) { Context.API.LogDebug(ClassName, "No valid tabs found"); - Task.Delay(_tabRetryInterval, cancellationToken); + Thread.Sleep(_tabRetryInterval); continue; } @@ -100,7 +100,7 @@ internal class TabsWalker } Context.API.LogDebug(ClassName, "No new tab found"); - Task.Delay(_tabRetryInterval, cancellationToken); + Thread.Sleep(_tabRetryInterval); } Context.API.LogDebug(ClassName, "Timeout waiting for new tab"); @@ -127,4 +127,12 @@ internal class TabsWalker Context.API.LogDebug(ClassName, "Removing all tabs in a window"); _cache.RemoveAllNonExistentTabs(browserWindow, []); } + + internal void CheckTabExistence(string runtimeId, string reason = "") + { + if (_cache.Contains(runtimeId)) + { + Context.API.LogDebug(ClassName, $"Tab exists {reason}"); + } + } }