Skip to content
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

Shell extension does not unload from explorer.exe when unregistered from the system #115

Open
end2endzone opened this issue Sep 24, 2022 · 15 comments
Labels
bug Something isn't working help wanted Extra attention is needed

Comments

@end2endzone
Copy link
Owner

Describe the bug
After the shell extension is unregistered from the system, explorer.exe does not unload sa.shellextension.dll.

To Reproduce
Steps to reproduce the behavior:

  1. Install as normal
  2. Register the shell extension by running register.bat inside an administrator command prompt.
  3. Right-click on a directory to show that the shell extension is registered on the system and loads additional context menus.
  4. Unregister the shell extension by running unregister.bat inside an administrator command prompt.
  5. Try to uninstall the application.
  6. Windows Installer refuse to proceed with the uninstallation and says Windows Explorer is using the files.

Expected behavior
Explorer.exe should release/unload shell anything dlls when unregistering ShellAnything.

Screenshots
After unregistering the application, if we try to uninstall the application, the following dialog is displayed:
image

Environment

  • OS: Windows 10 64 bit

Additional context
N/A

@end2endzone end2endzone added the bug Something isn't working label Sep 24, 2022
@end2endzone end2endzone added this to the 0.8.0 milestone Sep 24, 2022
@end2endzone
Copy link
Owner Author

Files are not unloaded after unregistration according to Process Explorer:
image

@end2endzone
Copy link
Owner Author

From https://learn.microsoft.com/en-us/previous-versions/windows/desktop/legacy/cc144064(v=vs.85)?redirectedfrom=MSDN :

The Shell automatically unloads any DLL when its usage count is zero, but only after the DLL has not been used for a period of time. This inactive period might be unacceptably long at times, especially when a Shell extension DLL is being debugged. You can shorten the inactive period by adding the following information to the registry.

  HKEY_LOCAL_MACHINE
   Software
      Microsoft
         Windows
            CurrentVersion
               Explorer
                  AlwaysUnloadDll

@end2endzone
Copy link
Owner Author

From https://www.tenforums.com/performance-maintenance/163171-unload-dll-ram-memory-tweak-question.html :

This is one of those useless tweaks that has been around since at least XP days. It has largely disappeared but can still be found. At least it is harmless. And finally, the setting hasn't been supported since Windows 2000. Unsupported registry entries are silently ignored.

@end2endzone
Copy link
Owner Author

end2endzone commented Feb 6, 2023

Some thoughts about why this might be occurring:

  1. ShellAnything recently moved to dynamic linked libraries (DLL) for GLOG and libexprtk libraries (ShellAnything API in C #103). There might be a hook or something similar which prevents explorer.exe to unload the main DLL because it can't also unload glog.dll or libexprtk.dll.
  2. ShellAnything uses its own DLL registering and unregistering code.
    • The first builds were compiled on Visual Studio Express which did not provide a version of [MFC and ATL] (https://learn.microsoft.com/en-us/cpp/mfc/mfc-and-atl) so manual DLL registration/unregistration was needed.
    • The code should be updated now that Visual Studio Community is available with support for ATL.
  3. In order to force explorer.exe to "refresh" shell extensions and unload the ones that are unregistered, the code has to "notify" Windows about the change. This is implemented in file shellext.cpp in functions DllRegisterServer() and DllUnregisterServer() with a call to SHChangeNotify(SHCNE_ASSOCCHANGED, 0, 0, 0); (version 0.7.0 and current version). A new API call or a new flag might be required in order to notify explorer.exe.
  4. Microsoft might have implemented a new feature in Windows 10 or 11 (after Windows 7) that might conflict with our code. The result being that explorer.exe never unloads sa.shellextension.dll after it is being unregistered.

EDIT:

  1. Invalid. See comment below.
  2. Invalid. Following commit 22d3d87, ShellAnything uses ATL for registering the shell extension. The *.rgs recipe file is still manually written but its structure matches other reputable sources such as:

@end2endzone
Copy link
Owner Author

end2endzone commented Feb 6, 2023

Possible solutions:

end2endzone added a commit that referenced this issue Feb 8, 2023
…icrosoft provided code as described in solution 2 of #115."

This reverts commit ffcc940.

Revert "Added new registry modification code as suggested in solution 2 of #115."

This reverts commit 06b6b9c.
end2endzone added a commit that referenced this issue Feb 8, 2023
@end2endzone
Copy link
Owner Author

end2endzone commented Feb 12, 2023

  • Take a look at registry key:
    HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Shell Extensions\Cached. It contains an entry about ShellAnything's extension. The key contains a value matching ShellAnything's class id. The value is still in the registry after the shell extension is unregistered. This might be an issue.
    Note: There is no reference to the shell extension under HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Shell Extensions\Cached.

end2endzone added a commit that referenced this issue Feb 14, 2023
@end2endzone end2endzone added the help wanted Extra attention is needed label Feb 21, 2023
@end2endzone
Copy link
Owner Author

Fixed Cache registry key in e7d4b32.

@end2endzone
Copy link
Owner Author

Disabled option 6: Rewrite the shell extension's high level code based on the code from https://www.codeproject.com/Articles/441/The-Complete-Idiot-s-Guide-to-Writing-Shell-Extens#gettingstarted.

@end2endzone
Copy link
Owner Author

There does not seems to be a way to "force" or tell File Explorer to unload the Shell Extension once it is unregistered.

According to https://stackoverflow.com/questions/11365944/how-to-unload-c-shell-extension-dll-properly, Windows Installer should be able to use MoveFile to move a loaded dll. This could be an acceptable solution.

@end2endzone end2endzone added the on hold The issue is started but progress has stopped label May 3, 2023
end2endzone added a commit that referenced this issue May 3, 2023
* feature-issue115: (39 commits)
  Enabled building branch "feature-issue115" on AppVeyor.
  Removed "Cached" shell extension from HKEY_CURRENT_USER as per #115.
  Changed "unknown interface" logs in function CContextMenu::QueryInterface() from "WARNINGS" to "INFO".
  Changing implementation for reference counting
  Added another implementation for QueryInterface which might be helpful for invetigating #115
  Added STRICT definition as per new VS2019 ALT project.
  Moved CContextMenu::m_previousMenu from static to a protected member variable.
  Disabled debugging hook.
  Silenced logs from CContextMenu::GetCommandString()
  Greatly simplified IUnknown::QueryInterface() and DllGetClassObject() implementations.
  Updated implementations of QueryInterface()
  Moved dtor scope to protected/private for CClassFactory and CContextMenu classes. Renamed m_cRef to m_refCount. Slightly modified IUnknown::AddRef() implementations.
  Fixed an invalid implementation of GuidToString().
  Added few code optimizations for the Shell Extension.
  Moved code from shellext.h and shellext.cpp to dedicated files.
  restored shellext.h
  duplicate shellext.h to CCriticalSection.h
  restored shellext.h
  duplicate shellext.h to CContextMenu.h
  restored shellext.h
  ...
@end2endzone end2endzone removed this from the 0.8.0 milestone May 3, 2023
@end2endzone end2endzone removed the on hold The issue is started but progress has stopped label Jan 1, 2024
@end2endzone
Copy link
Owner Author

end2endzone commented Jan 1, 2024

ShellAnything recently moved to dynamic linked libraries (DLL) for GLOG and libexprtk libraries (#103). There might be a hook or something similar which prevents explorer.exe to unload the main DLL because it can't also unload glog.dll or libexprtk.dll.

This has been proven incorrect. A dumb/empty new shell extension was created (see code attached below). The shell extension is really simple and has no dependencies on other DLL. Once unregistered, File Explorer is still holding to the shell extension's dll file. The file can be moved but not deleted. Waiting 2h+ did not resolved the issue.

See the following code used for testing: FooBarShellExt.zip
Note: This is a stripped down version of a shell extension from an GPLv2 licensed projet.

end2endzone added a commit that referenced this issue Jan 4, 2024
…w() #115

Removed conflicting code in CContextMenu.cpp which was overriding ATL code.
Fixed custom DllMain() code which was using HRESULT instead of BOOL for return type.
Removed debugging code: SA_ENABLE_ATTACH_HOOK_DEBUGGING.
end2endzone added a commit that referenced this issue Jan 10, 2024
end2endzone added a commit that referenced this issue Jan 10, 2024
end2endzone added a commit that referenced this issue Jan 10, 2024
Moved "find missing elements" code from main.cpp to user_feedback.cpp.
#115.
@end2endzone
Copy link
Owner Author

I have implemented an utility to restart File Explorer so that uninstalling the shell extension can be done without rebooting. The plan is to modify the uninstaller to show instructions to run the File Explorer restart before actually uninstalling.

@end2endzone
Copy link
Owner Author

The plan is to show a dialog when uninstalling such as the following:

--------------------------------------------------------------------------------------------------------
Before you uninstall
Should you renew File Explorer before you continue?
--------------------------------------------------------------------------------------------------------

Shell Extensions do not uninstall as easily as other softwares. Shell Extensions DLLs cannot be deleted because
File Explorer usually have a lock on the file. There is an issue with File Explorer which do not automatically
release shell extensions dll even if they have been unregistered from the system. Because of the lock, these dll
files cannot be deleted. A system reboot is usually the prefered option to make sure all dll files are released
and can be deleted.

ShellAnything provides a workaround which allow complete uninstallation without rebooting the system.
File Explorer Renew is an utility provided with ShellAnything that can close and reopen all File Explorer windows.
If the shell extension is unregistered, it will renew all windows and release the lock on the shell extension dll.

To enable the workaround, close the uninstaller, unregister the shell extension (unregister.bat)
and then run "file_explorer_renew.exe".

You can also continue normally and reboot the system to complete the uninstall process.

image

It seems that showing a dialog or a message box during the uninstall process is a bad idea. This is a bad practice because the uninstaller is usually executed in Silent Mode :

I wouldn't mess with this uninstall sequence. Any modal dialog that pops up when the installation is run in silent mode (which it is from add/remove) could cause your entire product to be axed in a corporate environment.

See the following references:

@end2endzone
Copy link
Owner Author

end2endzone commented Jan 11, 2024

Forcing a restart of File Explorer (without the user's consent) may also be a bad idea:

According to the link above, it appears the appropriate solution is to use MoveFile:

You can use the same mechanism in your homebrew installer and move the old file that is in use out of its original location and move the other one in right away. Any new application instance will then make use of the new shell extension (*), while the old one will continue to be used by any of the running applications that loaded it at one point or another.

This would allow the uninstallation of an old version and the installation of a new ShellAnything version. However, the new installed version would not be available to the system:

[Windows have] rules that apply to DLL loading and prevent modules with the same name to be loaded again under some circumstances.

end2endzone added a commit that referenced this issue Jan 12, 2024
Added a new dialog in the installer about instructions before uninstalling.
@end2endzone
Copy link
Owner Author

Added a new dialog in the installer about instructions before uninstalling. See e453904 for details.

end2endzone added a commit that referenced this issue Jan 13, 2024
* feature-issue115:
  Created a shortcut for `file_explorer_renew.exe` in Start Menu. #115 Added a new dialog in the installer about instructions before uninstalling.
  Updated INSTALL.md with a new section: Uninstall Shell Extensions without rebooting. #115
  * Added license header to all new source files.
  * Renamed `refresh_file_explorer` to `file_explorer_renew`.
  Modified refresh_file_explorer for the following: * Modified implementations in file_explorer.cpp to use heap memory instead of stack memory for large strings. * Fixed GetFileExplorerWindowPaths() to loop through all IShellWindows::Item() elements instead of stoping at the first index that reports a "no item at specified index". * Modified OpenFileExplorerWindow() to support lanching explorer.exe with no argument. * Start a default explorer.exe process before trying to renew previous directory windows. * Fixed GetProcessExecPathFromProcessId() that used hardcoded process id `1234`. * Fixed KillFileExplorerProcesses() that waits 30 seconds before reporting an failure. Sometimes, File Explorer can take a long time before closing when killed.
  Enabled actual killing/opening of File Explorer instances in refresh_file_explorer. #115.
  Renamed user.* to user_feedback.*. Moved "find missing elements" code from main.cpp to user_feedback.cpp. #115.
  Partial working refresh_file_explorer. #115
  Updated refresh_file_explorer application. Now showing current File Explorer paths in window. #115.
  Updated code for refresh_file_explorer.exe. Now getting paths from all File Explorer windows. #115.
  Created empty shell Windowed application `refresh_file_explorer.exe`.
  Fixed failing unit test: TestShellExtension.testDefaultDllCanUnloadNow() #115 Removed conflicting code in CContextMenu.cpp which was overriding ATL code. Fixed custom DllMain() code which was using HRESULT instead of BOOL for return type. Removed debugging code: SA_ENABLE_ATTACH_HOOK_DEBUGGING.
  Replace custom registration code in favor of ATL generated code (first draft) #115.
  Delete deprecated file logger_stub.h
  Renamed shellext.* to utils.*.
  Squashed commit of branch 'master' to branch 'feature-issue115'. #115
@end2endzone
Copy link
Owner Author

Leaving the issue opened in case someone else has an actual way of solving the problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

1 participant