Skip to content

Conversation

@AlexAdasCca
Copy link

@AlexAdasCca AlexAdasCca commented Nov 30, 2025

Pull Request

Description

This pull request makes several improvements and fixes across the project:

  1. DLL dependency handling and post-build configuration

    • Updated the project configuration so that runtime dependencies (DLLs) such as Boost and fmt are correctly handled after build.
    • Introduced a small helper tool that inspects the build output, determines the required DLLs, and copies them into the appropriate output directory.
    • This aims to reduce “missing DLL” issues at runtime and make the deployment and local development experience more predictable.
  2. Fix for context menu crash due to incorrect user data directory

    • Fixed an issue where the right-click context menu failed to appear and the application crashed when running as a packaged app.
    • The root cause was that the user data directory path was constructed using the Package Family Name instead of the Package Name, which resulted in a non-existent path.
    • The previous code path did not properly handle the resulting exception, causing a runtime crash.
    • The new implementation:
      • Uses appropriate AppX-related Win32 APIs to obtain the correct user data directory.
      • Adds robust exception handling around path resolution and access.
    • As a result, the context menu now behaves as expected and no longer crashes under these conditions.
  3. Boost upgrade to 1.86 and process/thread handling

    • Upgraded Boost to version 1.86.
    • Due to the lack of a direct thread handle for child processes (process children) in this configuration, the current implementation uses thread enumeration as a workaround to obtain the necessary handle.
    • In the longer term, the plan is to migrate fully to boost-process v2 APIs, which provide better extensibility and officially supported ways to access the required process information.
  4. Debug output system for shell extension

    • Added a lightweight debug output/logging system to the shell extension module.
    • This will make it easier to diagnose issues in the future by providing clearer diagnostics during development and debugging.
  5. Reliable DLL unload and host process termination handling (dllhost.exe)

    • Investigated and documented the actual DLL unload behavior when the shell extension is hosted inside dllhost.exe.
    • Previously, comments in the code assumed that DllCanUnloadNow would always be called before the module is unloaded and that the following “ideal” sequence would occur:
      • CoUninitialize
      • RtlDllShutdownInProgress
      • DllCanUnloadNow → COM refcounts reach 0 → FreeLibrary
      • DllMain(DLL_PROCESS_DETACH)
    • In reality, dllhost.exe may call TerminateProcess after CoUninitialize without:
      • ever calling CoFreeUnusedLibraries / DllCanUnloadNow, or
      • ever calling DllMain(DLL_PROCESS_DETACH) for the shell extension.
    • This behavior is consistent with the Windows documentation: when a process is terminating, other threads may already be gone or forcibly terminated, and DLL detach notifications are not guaranteed to run when TerminateProcess is used.
    • To handle this reliably, the shell extension now:
      • Hooks CoUninitialize and TerminateProcess via HostProcessHook.
      • Tracks the host state and owner thread (the DllMain(DLL_PROCESS_ATTACH) / DllHost!wWinMain thread) via HostShutdownWatcher.
      • Only treats CoUninitialize calls on the recorded owner thread as the “host COM shutdown” signal, avoiding interference from other modules calling CoUninitialize on background threads.
      • When CoUninitialize has been observed on the owner thread and no subsequent DllCanUnloadNow has been called from our module, a dedicated cleanup callback (FastCopyCleanupCallback) is invoked from the TerminateProcess detour.
    • FastCopyCleanupCallback is responsible for shutting down internal resources that must be cleaned up even on the hard TerminateProcess path, such as:
      • Stopping the background monitor thread in CommonSharedSettings.
      • Closing change-notification handles and flushing logs.
    • The existing comments around DLL_PROCESS_DETACH have been updated to:
      • Reflect the actual behavior of dllhost.exe.
      • Clarify that normal unload via FreeLibrary is handled by DllMain(DLL_PROCESS_DETACH) and object lifetimes, while the TerminateProcess path is handled explicitly by the hooks and the cleanup callback.
    • A diagram of the updated DLL unload / process termination flow in dllhost.exe is included in the documentation section below.

Motivation and context:

  • Ensure the application can reliably find and load all required DLLs after build.
  • Eliminate crashes caused by incorrect user data paths when running as a packaged app.
  • Keep third-party dependencies up to date (Boost 1.86) while preparing for a future migration to boost-process v2.
  • Improve maintainability and debuggability of the shell extension via structured debug output.
  • Ensure deterministic and safe cleanup of the shell extension when hosted by dllhost.exe, even when the host terminates the process via TerminateProcess without calling DllMain(DLL_PROCESS_DETACH).

Fixes # (issue number)

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration.

  • Built the project with the updated configuration and verified that required DLLs (e.g. Boost, fmt) are copied to the output directory and the application starts without missing-DLL errors.
  • Manually tested the right-click context menu in the packaged application:
    • Confirmed the menu appears as expected.
    • Confirmed that the application no longer crashes when resolving the user data directory.
  • Verified that the new shell extension debug output is emitted as expected during typical operations, without impacting normal behavior.
  • Verified the DLL unload and process termination behavior in dllhost.exe:
    • Observed the call sequence involving CoUninitialize, DllCanUnloadNow, DllMain, and TerminateProcess via debugger breakpoints.
    • Confirmed that FastCopyCleanupCallback is invoked on the intended TerminateProcess path and that background threads/handles (e.g. CommonSharedSettings monitor) are properly shut down.
  • Performed a smoke test of core functionality to ensure no regressions after upgrading Boost to 1.86.

Example (adjust as needed):

  • Manual test on Windows (packaged app)
  • Manual test on Windows (unpackaged / development build)
  • Additional automated tests (if applicable)

Dependencies

Mention any dependencies this PR relies on or blocks.

  • Updates Boost to version 1.86.
  • No new external runtime dependencies are introduced beyond existing libraries and the new internal helper tool.

Depends on # (PR)
Blocks # (PR)

Screenshots/Documentation

Include any relevant screenshots or gifs that demonstrate the UI changes. Include links to updated documentation if applicable.

  • 74f068bde5ae0d7e56342c42fdeddcd1
  • 4a098c4dc02dcdcbacdf5aa24880a765
  • 81502d5d5820bfa6f7772dde7526c8cf
  • image
  • c185340719013ee54198a58ff76df164

Checklist for Reviewers

  • Code follows the project's style guidelines
  • Adequate tests have been added
  • Functionality and bug fixes are confirmed via tests
  • Existing tests have been adapted to accommodate the changes
  • The changes do not generate new warnings/errors

Request for Specific Feedback

Optional: Mention any particular aspects of the PR where you would like to receive specific feedback.

  • Feedback on the current thread enumeration workaround used with Boost 1.86 and whether there are concerns about performance or edge cases.
  • Feedback on the new AppX-related path resolution and exception handling, especially in scenarios where the packaged app runs under unusual user profiles or restricted environments.
  • Feedback on the debug output system in the shell extension (format, verbosity, and potential impact on performance).
  • Feedback on the DLL unload / host termination handling in dllhost.exe:
    • Whether the current HostShutdownWatcher / HostProcessHook approach is clear and maintainable.
    • Whether the documented unload sequence and diagrams make the behavior easy to reason about.

Additional Information

Anything else you want to add (links to documentation, style guides, etc.)

  • Future work: investigate and plan a full migration to boost-process v2 APIs to remove the thread enumeration workaround and leverage official extensibility points.
  • Future work: consider adding more automated coverage around the shell extension’s lifecycle when hosted by dllhost.exe (including the TerminateProcess path).

Note

Add rule-based post-build DLL packaging, shared settings + logging controls, and reliable shell extension shutdown; update UI and boost runtime to 1.86.

  • Build/Packaging:
    • Add CopyFilesByRules tool and Tools/AppxDllRules.txt; wire into FastCopy.vcxproj to copy runtime DLLs to $(TargetDir)AppX post-build.
    • Replace hardcoded package DLL items with $(TargetDir) entries; add Tools/CopyFilesSimple.bat and ShellExtension post-build to copy its DLL.
    • Update dependencies to boost_filesystem-vc143-...-1_86.dll.
  • Shared Infrastructure:
    • Introduce Shared/CommonSharedSettings (INI-backed, change notifications), DebugHelper.hpp, and var_init_once.h; include across projects.
  • Shell Extension:
    • Add lightweight logger (DebugPrint.*) with verbosity/breakpoint controls sourced from shared settings.
    • Implement HostProcessHook (Detours) and HostShutdownWatcher to handle CoUninitialize/TerminateProcess and ensure cleanup via FastCopyCleanupCallback.
    • Enhance dllmain, DllCanUnloadNow, and commands to log and use new Recorder; make Recorder pathing robust via shared settings.
  • App/UI:
    • Extend SettingsViewModel to INotifyPropertyChanged; add logger settings (enable, verbosity, debug-break) synced via CommonSharedSettings.
    • Update SettingsWindow.xaml with Developer Options expander; add resources/strings; merge SettingsExpander_Resource.
  • Robocopy/Process:
    • Add CreateSuspend handler; obtain first thread handle for APC injection; minor helper and NTDLL tweaks.
  • Solution/Config:
    • Add CopyFilesByRules project and Shared solution items; adjust project include paths/toolsets and .gitignore.

Written by Cursor Bugbot for commit 988d5e4. This will update automatically on new commits. Configure here.

@AlexAdasCca
Copy link
Author

AlexAdasCca commented Dec 6, 2025

I admit that I implemented a rather foolish design at the log level. I will provide a final update in the near future, and I envision that it will no longer use the concept of threshold, but instead use optional masks. On the UI settings side, it appears that you can use checkboxes to select the desired combination of logs. I also added the output of stack information, although this feature has the same implementation in the debugger.

Alternatively, simply discard these confusing excessive designs: No Level! no Verbosity!

P.S : 最初设计这部分日志,是出于后面在进一步提出集成到 Shell,使得 RoboCopyEx 在资源管理器处理任何文件的移动/剪切,复制,和剪贴板的捕获方面获得优先权,这样就不必每次都打开右键菜单选择二级菜单,然后使用旁路的Copy/Paste 进行操作了。而上述设计是相当复杂的,预计可能会消耗我更多的业余时间。也需要较长的周期去调试,如果组件能够在源代码级别就使用一整套适合开发人员调试和观察的处理,那么将获得比每次附加 VS 的内部调试器获得更多的有趣信息:例如你可以不中断程序观察数据流和控制流的关键上下文信息,这可以发现超出你设计时预想的可能执行路径。为了避免侵入性,我采取了分级别、DEBUG宏和调试器探测模式相互结合的形式,并且允许一部分的类实例在所运行的线程上执行临时输出提升(让线程变得唠叨或停滞),这将在作用域过后恢复线程原来的管理级别(即默认情况下的全局级别[新的设计中我将改为使用选项的组合],每个线程的临时级别可以覆盖全局级别)。
在 DebugHelper.hpp 中提供调试断点和调试器等待功能,以便于可以附加外部调试器。没关系,这只在 Debug 下将额外的代码编译至二进制文件,并且等待附加超时后程序将正常执行。

@HO-COOH
Copy link
Owner

HO-COOH commented Dec 10, 2025

OK, this is a big one. Let's get it done, step by step.

  1. Remove the dedicated CopyFilesByRules project and your related logic in all the project files. Add this file in the shared folder, name it CopyMissingDlls.targets.
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <!--
    Shared MSBuild task to copy missing DLLs from a source directory to a target directory.
    Import this file in your .vcxproj to use the CopyMissingDllsTask.
    
    Usage:
      <CopyMissingDllsTask SourceDir="$(TargetDir)" TargetDir="$(TargetDir)AppX\" />
  -->
  <UsingTask TaskName="CopyMissingDllsTask" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
    <ParameterGroup>
      <SourceDir ParameterType="System.String" Required="true" />
      <TargetDir ParameterType="System.String" Required="true" />
    </ParameterGroup>
    <Task>
      <Using Namespace="System" />
      <Using Namespace="System.IO" />
      <Code Type="Fragment" Language="cs"><![CDATA[
if (!Directory.Exists(SourceDir))
{
    Log.LogError("[CopyMissingDlls] SourceDir does not exist: {0}", SourceDir);
    return false;
}

Log.LogMessage(MessageImportance.Normal, "[CopyMissingDlls] SourceDir: {0}", SourceDir);
Log.LogMessage(MessageImportance.Normal, "[CopyMissingDlls] TargetDir: {0}", TargetDir);

try
{
    Directory.CreateDirectory(TargetDir);
}
catch (Exception ex)
{
    Log.LogError("[CopyMissingDlls] Failed to create TargetDir: {0} (error: {1})", TargetDir, ex.Message);
    return false;
}

int copiedCount = 0;
bool anyError = false;

foreach (var srcPath in Directory.GetFiles(SourceDir, "*.dll"))
{
    string filename = Path.GetFileName(srcPath);
    string dstPath = Path.Combine(TargetDir, filename);
    
    if (File.Exists(dstPath))
        continue;
    
    try
    {
        File.Copy(srcPath, dstPath);
        Log.LogMessage(MessageImportance.Normal, "[CopyMissingDlls] Copied: {0}", filename);
        copiedCount++;
    }
    catch (Exception ex)
    {
        Log.LogError("[CopyMissingDlls] Failed to copy \"{0}\": {1}", filename, ex.Message);
        anyError = true;
    }
}

if (copiedCount == 0)
{
    Log.LogMessage(MessageImportance.Normal, "[CopyMissingDlls] No missing DLLs to copy.");
}
else
{
    Log.LogMessage(MessageImportance.Normal, "[CopyMissingDlls] Copied {0} DLL(s).", copiedCount);
}

return !anyError;
]]></Code>
    </Task>
  </UsingTask>
</Project>

This is simple enough, copy all missing dlls from SourceDir to TargetDir. Then in FastCopy.vcxproj, above the <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" /> line, add

  <Import Project="$(SolutionDir)Shared\CopyMissingDlls.targets" />
  <!-- After build, copy missing DLLs from $(TargetDir) to the AppX directory -->
  <Target Name="CopyRuntimeFilesToAppx" AfterTargets="Build" Condition="'$(Platform)'=='x64'">
    <CopyMissingDllsTask SourceDir="$(TargetDir)" TargetDir="$(TargetDir)AppX\" />
  </Target>

@AlexAdasCca
Copy link
Author

i will check it later. @HO-COOH

Copy link
Owner

@HO-COOH HO-COOH left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, fix these first

<x:String x:Key="ThemeGlyph">&#xE790;</x:String>
<x:String x:Key="WindowBackgroundGlyph">&#xE81E;</x:String>
<x:String x:Key="DevModeGlyph">&#xEC7A;</x:String>
<ScrollViewer
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't. The settings window should ideally be simple enough. If you need scroling then you are complicating things. Just calculate the needed window size, and make it fixed.

m_fs = _wfopen(filename.data(), L"wb");
if (!m_fs)
throw std::runtime_error{ "Cannot open file" };
auto& s = FastCopy::Settings::CommonSharedSettings::Instance();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename s to be something meaningful

}) != std::filesystem::directory_iterator{};
try
{
auto& s = FastCopy::Settings::CommonSharedSettings::Instance();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename s too

Comment on lines +124 to +135
if (!folderOpt) {
FC_LOG_ERROR(L"LocalDataFolder unavailable, skip recording.");
return false;
}

auto const& folder = *folderOpt;

if (folder.empty())
{
FC_LOG_ERROR(L"LocalDataFolder empty, skip recording.");
return false;
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary check. You are already in a try-catch block, just access the optional. It should throw if its value not set.

Comment on lines +144 to +155
for (auto it = std::filesystem::directory_iterator{ folder, ec };
it != std::filesystem::directory_iterator{} && !ec;
++it)
{
auto name = it->path().filename().wstring();
if (!name.empty() &&
(name.starts_with(L'C') || name.starts_with(L"M2") || name.starts_with(L'P')))
{
FC_LOG_DEBUG(L" found '{}'", name);
return true;
}
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's bad about my original std::find_if 😂

static bool HasRecord();
private:
FILE* m_fs;
FILE* m_fs = nullptr;
Copy link
Owner

@HO-COOH HO-COOH Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessary. wfopen returns NULL if not opened, but okay

};

// Get the current settings.ini path
inline std::filesystem::path GetFastCopyIniPath()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is not called anywhere. Is it written by AI? 😅

}

// Returns true if a debugger is currently attached.
[[nodiscard]] inline bool is_debugger_attached() noexcept
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What? Why?

@HO-COOH
Copy link
Owner

HO-COOH commented Dec 10, 2025

Please, explain the necessity of HostProcessHook? This seems very technical and I don't seem to understand. You can update in the OP and notify me.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is being reviewed by Cursor Bugbot

Details

Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

{
DetourTransactionAbort();
return false;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Invalid DetourTransactionAbort after DetourTransactionCommit returns

When DetourTransactionCommit() fails and returns an error, the code calls DetourTransactionAbort() in the else branch. However, once DetourTransactionCommit() has been called, the transaction is already terminated (it either commits successfully or aborts internally on failure). Calling DetourTransactionAbort() after DetourTransactionCommit() has returned is undefined behavior since there is no active transaction to abort. The DetourTransactionAbort() call on line 68 is incorrect and could cause unexpected behavior.

Fix in Cursor Fix in Web

strong.get()->OnSharedSettingsChangedNotifyUI();
}
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Use-after-free race in settings change callback

The OnSharedSettingsChanged callback receives a raw this pointer as ctx and accesses self->m_dispatcher and self->get_weak() without synchronization. The CommonSharedSettings::NotifyConfigChanged() copies the listener list under a lock, then releases the lock and iterates over the copy to call callbacks. If the destructor runs concurrently (calling UnregisterChangeListener() and destroying the object), the callback can access a freed object because unregistering only removes from the live list, not the already-copied list being iterated. This results in a use-after-free race condition.

Additional Locations (1)

Fix in Cursor Fix in Web

return false;
}

std::error_code ec;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessary either. Just let it throw

if (!cb)
return;

std::lock_guard lock(m_listenersMutex);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use {} initialization

SetLastError(0);

DWORD len = GetPrivateProfileStringW(
std::wstring(section).c_str(),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants