-
-
Notifications
You must be signed in to change notification settings - Fork 27
An attempt to fix infinite explorer crashing issue with dynamic taskbar creation check #268
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR attempts to fix infinite Explorer crashing issues by replacing a timeout-based approach with a dynamic taskbar readiness detection mechanism. Instead of waiting a fixed 1000ms for the taskbar to stabilize, the code now uses a MainWindow.ExplorerRestarting flag that is set when WM_TASKBARCREATED is received and cleared when the taskbar is confirmed ready.
Key Changes:
- Introduces
ExplorerRestartingvolatile flag to coordinate taskbar recovery across threads - Implements
WaitForExplorerReadyAsync()to probe taskbar readiness every 200ms with an 8-second timeout - Refactors
WndProcmessage handler to defer tray icon recreation until Explorer stabilizes - Simplifies error recovery logic in
TaskbarWindow.UpdatePosition()
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 14 comments.
| File | Description |
|---|---|
| FluentFlyoutWPF/MainWindow.xaml.cs | Adds explorer restart detection logic with ExplorerRestarting flag, implements async probing for taskbar readiness, and refactors tray icon recreation to use safer deferred approach |
| FluentFlyoutWPF/Windows/TaskbarWindow.xaml.cs | Makes Win32 API methods public for cross-class access, adds early return in UpdatePosition() when explorer is restarting, and simplifies recovery logic by removing retry attempt tracking |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| private TaskbarWindow? taskbarWindow; | ||
|
|
||
| internal static volatile bool ExplorerRestarting = false; |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The volatile keyword alone does not guarantee thread-safety for flag operations that involve reading and writing. While volatile ensures visibility of changes across threads, it doesn't provide atomicity for read-modify-write operations. If ExplorerRestarting is checked and then set in a non-atomic way, race conditions could still occur. Consider using Interlocked operations (e.g., Interlocked.Exchange) or a lock to ensure proper synchronization, especially since this flag is being used to coordinate critical operations across multiple threads (UI thread in WndProc and background tasks in UpdatePosition).
| else | ||
| { | ||
| Logger.Warn("Explorer did not stabilize within timeout; keeping integration disabled"); | ||
| } |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ExplorerRestarting flag is never reset to false when WaitForExplorerReadyAsync() times out (returns false). This means if Explorer fails to stabilize within 8 seconds, the flag remains true indefinitely, permanently disabling taskbar integration until the application is restarted. The flag should be reset even in the timeout case to allow recovery attempts.
FluentFlyoutWPF/MainWindow.xaml.cs
Outdated
| //private nint WndProc(nint hwnd, int msg, nint wParam, nint lParam, ref bool handled) | ||
| //{ | ||
| // // Listen for TaskbarCreated message (when explorer.exe restarts) | ||
| // if (msg == WM_TASKBARCREATED) | ||
| // { | ||
| // //Logger.Info("TaskbarCreated message received - recreating tray icon"); | ||
| // //nIcon.Visibility = Visibility.Collapsed; // remove tray icon | ||
|
|
||
| // //if (SettingsManager.Current.NIconHide) | ||
| // // return 0; | ||
|
|
||
| // //nIcon.Visibility = Visibility.Visible; // re-add tray icon | ||
| // //nIcon.Register(); | ||
| // Logger.Debug("Restarted without tray icon stuff cus its buggy asf"); | ||
| // } | ||
| // return 0; | ||
| //} |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Large block of commented-out code (30+ lines) should be removed rather than left in the codebase. Commented code creates maintenance burden and confusion. If this recovery logic needs to be preserved for reference, it should be documented in version control history or in a design document.
| //private nint WndProc(nint hwnd, int msg, nint wParam, nint lParam, ref bool handled) | |
| //{ | |
| // // Listen for TaskbarCreated message (when explorer.exe restarts) | |
| // if (msg == WM_TASKBARCREATED) | |
| // { | |
| // //Logger.Info("TaskbarCreated message received - recreating tray icon"); | |
| // //nIcon.Visibility = Visibility.Collapsed; // remove tray icon | |
| // //if (SettingsManager.Current.NIconHide) | |
| // // return 0; | |
| // //nIcon.Visibility = Visibility.Visible; // re-add tray icon | |
| // //nIcon.Register(); | |
| // Logger.Debug("Restarted without tray icon stuff cus its buggy asf"); | |
| // } | |
| // return 0; | |
| //} |
| // Logger.Warn("Taskbar Widget window handle is zero, attempting recovery..."); | ||
|
|
||
| // Dispatcher.BeginInvoke(async () => | ||
| // { | ||
| // await Task.Delay(1000); // delay before recovery to let taskbar stabilize | ||
| // try | ||
| // { | ||
| // _mainWindow?.RecreateTaskbarWindow(); | ||
| // } | ||
| // catch (Exception ex) | ||
| // { | ||
| // Logger.Error(ex, "Failed to signal MainWindow to recover Taskbar Widget window"); | ||
| // _recoveryAttempts++; | ||
| // } | ||
| // }, DispatcherPriority.Background); | ||
|
|
||
| // return; | ||
| //} |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Large block of commented-out code (28 lines) should be removed. The new implementation at lines 435-451 replaces this logic, so the old code serves no purpose and reduces code clarity.
| // Logger.Warn("Taskbar Widget window handle is zero, attempting recovery..."); | |
| // Dispatcher.BeginInvoke(async () => | |
| // { | |
| // await Task.Delay(1000); // delay before recovery to let taskbar stabilize | |
| // try | |
| // { | |
| // _mainWindow?.RecreateTaskbarWindow(); | |
| // } | |
| // catch (Exception ex) | |
| // { | |
| // Logger.Error(ex, "Failed to signal MainWindow to recover Taskbar Widget window"); | |
| // _recoveryAttempts++; | |
| // } | |
| // }, DispatcherPriority.Background); | |
| // return; | |
| //} |
| Logger.Error(ex, "Failed to signal MainWindow to recover Taskbar Widget window"); | ||
| _recoveryAttempts++; | ||
| } | ||
| _mainWindow?.RecreateTaskbarWindow(); |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The simplified recovery logic no longer includes error handling or recovery attempt tracking. The previous implementation had a try-catch block to handle exceptions during RecreateTaskbarWindow and tracked recovery attempts to prevent infinite loops. Without these safeguards, if RecreateTaskbarWindow() throws an exception, it could lead to unhandled exceptions in the dispatcher queue. Consider adding error handling around the RecreateTaskbarWindow call.
| _mainWindow?.RecreateTaskbarWindow(); | |
| try | |
| { | |
| _mainWindow?.RecreateTaskbarWindow(); | |
| } | |
| catch (System.Exception ex) | |
| { | |
| Logger.Error(ex, "Failed to recover Taskbar Widget window"); | |
| } |
FluentFlyoutWPF/MainWindow.xaml.cs
Outdated
| IntPtr taskbar = TaskbarWindow.FindWindow("Shell_TrayWnd", null); | ||
| if (taskbar != IntPtr.Zero) | ||
| { | ||
| if (TaskbarWindow.GetWindowRect(taskbar, out TaskbarWindow.RECT rect)) |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace this call with a call to managed code if possible.
| // --- Win32 APIs --- | ||
| [DllImport("user32.dll", SetLastError = true)] | ||
| private static extern IntPtr FindWindow(string lpClassName, string? lpWindowName); | ||
| public static extern IntPtr FindWindow(string lpClassName, string? lpWindowName); |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minimise the use of unmanaged code.
| public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); | ||
|
|
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minimise the use of unmanaged code.
| public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); | |
| private static extern bool GetWindowRectNative(IntPtr hWnd, out RECT lpRect); | |
| /// <summary> | |
| /// Safely retrieves the bounding rectangle of the specified window as a managed <see cref="Rect"/>. | |
| /// </summary> | |
| /// <param name="hWnd">Handle to the window.</param> | |
| /// <param name="rect">On success, receives the window rectangle.</param> | |
| /// <returns><c>true</c> if the rectangle was retrieved successfully; otherwise, <c>false</c>.</returns> | |
| public static bool TryGetWindowRect(IntPtr hWnd, out Rect rect) | |
| { | |
| rect = Rect.Empty; | |
| if (hWnd == IntPtr.Zero) | |
| return false; | |
| if (!GetWindowRectNative(hWnd, out RECT nativeRect)) | |
| return false; | |
| var width = nativeRect.Right - nativeRect.Left; | |
| var height = nativeRect.Bottom - nativeRect.Top; | |
| rect = new Rect(nativeRect.Left, nativeRect.Top, width, height); | |
| return true; | |
| } |
FluentFlyoutWPF/MainWindow.xaml.cs
Outdated
| if (taskbar != IntPtr.Zero) | ||
| { | ||
| if (TaskbarWindow.GetWindowRect(taskbar, out TaskbarWindow.RECT rect)) | ||
| { | ||
| if (rect.Right > rect.Left && rect.Bottom > rect.Top) | ||
| { | ||
| return true; // taskbar exists and has geometry | ||
| } | ||
| } |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These 'if' statements can be combined.
| if (taskbar != IntPtr.Zero) | |
| { | |
| if (TaskbarWindow.GetWindowRect(taskbar, out TaskbarWindow.RECT rect)) | |
| { | |
| if (rect.Right > rect.Left && rect.Bottom > rect.Top) | |
| { | |
| return true; // taskbar exists and has geometry | |
| } | |
| } | |
| if (taskbar != IntPtr.Zero | |
| && TaskbarWindow.GetWindowRect(taskbar, out TaskbarWindow.RECT rect) | |
| && rect.Right > rect.Left | |
| && rect.Bottom > rect.Top) | |
| { | |
| return true; // taskbar exists and has geometry |
FluentFlyoutWPF/MainWindow.xaml.cs
Outdated
| if (taskbar != IntPtr.Zero) | ||
| { | ||
| if (TaskbarWindow.GetWindowRect(taskbar, out TaskbarWindow.RECT rect)) | ||
| { | ||
| if (rect.Right > rect.Left && rect.Bottom > rect.Top) | ||
| { | ||
| return true; // taskbar exists and has geometry | ||
| } | ||
| } |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These 'if' statements can be combined.
| if (taskbar != IntPtr.Zero) | |
| { | |
| if (TaskbarWindow.GetWindowRect(taskbar, out TaskbarWindow.RECT rect)) | |
| { | |
| if (rect.Right > rect.Left && rect.Bottom > rect.Top) | |
| { | |
| return true; // taskbar exists and has geometry | |
| } | |
| } | |
| if (taskbar != IntPtr.Zero && | |
| TaskbarWindow.GetWindowRect(taskbar, out TaskbarWindow.RECT rect) && | |
| rect.Right > rect.Left && | |
| rect.Bottom > rect.Top) | |
| { | |
| return true; // taskbar exists and has geometry |
|
that was brutal 😿 |
|
Hi, thank you for contributing, I appreciate it! Quickly looking through the code the implementation looks great, but I noticed that you left a lot of commented code in and removed some try catch blocks and logging. If possible, could you add them back in areas where needed? Other than that, do you think we should keep ExplorerRestarting flag to true even if the 8 second loop has passed? Thanks again! |
|
I've updated the comments and added the try-catch block back. I think if taskbar isnt ready after 8 seconds is kinda sus, either the taskbar is never ready, maybe due to other plugins that modifies the taskbar and causing issue? One known issue i observed is that an app called start11 could modify the taskbar window causing further instability, but this would be subjected case-by-case to any third party plugins and their implementation, and would be abit of a pain to just create a case for each one of them. I think flipping the flag back to false after 8 seconds would mean that this PR essentially increases the 1000ms fixed wait time to 8000ms wait time, but allow wait to end earlier if the taskbar is ready; OR if never flipping it to false, then the taskbar features are never loaded again, maybe consider exit the program or showing a message telling the user that taskbar is buggy? The easier solution would be yes, flipping it back to false, I think 8 seconds is more than enough for any heavy loading, but I think the ultimate decision of this would be how you imagine the app behave. What do you think? I'll finalise the relevant changes in the next commit, depending on what we decide here. |
|
Or, keeping it as true, and maybe the next explorer restart externally (by the buggy plugin, or by the user manually) solves the buggy taskbar, then the taskbar window can reload properly, but never actively triggers a explorer restart by force loading TaskbarWindow, I think would be a better option? In this case since it would be handled again when this app receives |
|
Thanks for the quick implementation :) I guess your points make sense! In that case we can keep it set to true, but for edge-case scenarios where the taskbar might take longer than 8 seconds to restart (like really slow computers), perhaps we should extend the timeout to a minute (maybe with exponential backoff if it's resource intensive?), what do you think? EDIT: I also noticed that there's a duplicate import and the formatting could be fixed up just a little bit. Are you able to look into that as well? thanks! |
|
sorry for the messy code 😭, should be fixed now i think exponential backoff would be a great idea, i might get it in later, its getting late today, for now its 1 minute timeout |
Instead of relying on timeout (1000ms) to wait for taskbar to become stable (which in many case stabilisation takes more than a second), now relies
MainWindow.ExplorerRestartingas a flag to determine if taskbar is stableThe
ExplorerRestartingflag is flipped to true whenWM_TASKBARCREATEDis received, then flipped back to false whenWaitForExplorerReadyAsync()returns, indicating its ready.WaitForExplorerReadyAsync()works by probing taskbar every 200ms, checking if window exists (FindWindow()) and has a valid geometry. This happens until taskbar is ready or timed out (defaulted to 8000ms).