diff --git a/.gitignore b/.gitignore index f58bf91..0ef2b70 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs +Tools/CopyFilesByRules.exe # Mono auto generated files mono_crash.* diff --git a/CopyFilesByRules/CopyFilesByRules.cpp b/CopyFilesByRules/CopyFilesByRules.cpp new file mode 100644 index 0000000..031531f --- /dev/null +++ b/CopyFilesByRules/CopyFilesByRules.cpp @@ -0,0 +1,344 @@ +// CopyFilesByRules.cpp +// Match files under SourceDir using a rule configuration file and copy them to TargetDir +// Our rule supports '*' and '?' for filename matching (NOT DIRECTORY PATH!!!) +// , and is case-insensitive. + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +struct Rule +{ + std::wstring patternLower; + bool softOnTargetLocked = false; +}; + +struct FileToCopy +{ + fs::path src; + bool softOnTargetLocked = false; +}; + +static std::wstring ToLower(std::wstring s) +{ + std::transform(s.begin(), s.end(), s.begin(), + [](wchar_t ch) { + return static_cast( + std::towlower(static_cast(ch)) + ); + }); + return s; +} + +static bool MatchPattern(const std::wstring& pattern, const std::wstring& text) +{ + size_t p = 0; + size_t t = 0; + size_t starPos = std::wstring::npos; + size_t matchPos = 0; + + while (t < text.size()) + { + if (p < pattern.size() && + (pattern[p] == text[t] || pattern[p] == L'?')) + { + ++p; + ++t; + } + else if (p < pattern.size() && pattern[p] == L'*') + { + starPos = p++; + matchPos = t; + } + else if (starPos != std::wstring::npos) + { + p = starPos + 1; + ++matchPos; + t = matchPos; + } + else + { + return false; + } + } + + while (p < pattern.size() && pattern[p] == L'*') + ++p; + + return p == pattern.size(); +} + +static void Trim(std::wstring& s) +{ + const wchar_t* ws = L" \t\r\n"; + auto start = s.find_first_not_of(ws); + if (start == std::wstring::npos) + { + s.clear(); + return; + } + auto end = s.find_last_not_of(ws); + s = s.substr(start, end - start + 1); +} + +static std::wstring NormalizePathArg(const wchar_t* arg) +{ + if (!arg) + return {}; + + std::wstring s(arg); + Trim(s); + + if (s.size() >= 2 && s.front() == L'"' && s.back() == L'"') + { + s = s.substr(1, s.size() - 2); + Trim(s); + } + + return s; +} + +int wmain(int argc, wchar_t* argv[]) +{ + if (argc != 4) + { + std::wcerr << L"ArgsNumberError: Expected 4 args, got " + << argc << L"\n"; + std::wcerr << L"[CopyFilesByRules] Usage:\n" + << L" CopyFilesByRules.exe \n"; + return 1; + } + + std::wstring sourceArg = NormalizePathArg(argv[1]); + std::wstring targetArg = NormalizePathArg(argv[2]); + std::wstring rulesArg = NormalizePathArg(argv[3]); + + std::wcout << L"[CopyFilesByRules] SourceDir: " << sourceArg << L"\n"; + std::wcout << L"[CopyFilesByRules] TargetDir: " << targetArg << L"\n"; + std::wcout << L"[CopyFilesByRules] RulesFile: " << rulesArg << L"\n"; + + fs::path sourceDir = sourceArg; + fs::path targetDir = targetArg; + fs::path rulesFile = rulesArg; + + try + { + if (!fs::exists(sourceDir) || !fs::is_directory(sourceDir)) + { + std::wcerr << L"[CopyFilesByRules][ERROR] SourceDir is not a directory: " + << sourceDir << L"\n"; + return 1; + } + + if (!fs::exists(rulesFile)) + { + std::wcerr << L"[CopyFilesByRules][ERROR] RulesFile not found: " + << rulesFile << L"\n"; + return 1; + } + + std::wifstream fin(rulesFile); + if (!fin) + { + std::wcerr << L"[CopyFilesByRules][ERROR] Failed to open RulesFile: " + << rulesFile << L"\n"; + return 1; + } + + fin.imbue(std::locale("")); + + std::vector rules; + std::wstring line; + + // Global soft switch (optional, @SoftOnTargetLocked) + bool globalSoftOnTargetLocked = false; + + while (std::getline(fin, line)) + { + if (!line.empty() && line[0] == 0xFEFF) + line.erase(line.begin()); + + Trim(line); + if (line.empty()) + continue; + + if (line[0] == L'#' || line[0] == L';') + continue; + + if (line[0] == L'@') + { + std::wstring lower = ToLower(line); + if (lower == L"@softontargetlocked" || lower == L"@soft_on_target_locked") + { + globalSoftOnTargetLocked = true; + std::wcout << L"[CopyFilesByRules] Option: Global SoftOnTargetLocked = true\n"; + } + else + { + std::wcout << L"[CopyFilesByRules] Unknown option line ignored: " + << line << L"\n"; + } + continue; + } + + // Lined rule format: pattern [| flags...] + Rule rule; + + std::wstring patternPart = line; + std::wstring flagsPart; + + auto barPos = line.find(L'|'); + if (barPos != std::wstring::npos) + { + patternPart = line.substr(0, barPos); + flagsPart = line.substr(barPos + 1); + Trim(patternPart); + Trim(flagsPart); + } + + if (patternPart.empty()) + continue; + + rule.patternLower = ToLower(patternPart); + rule.softOnTargetLocked = false; + + if (!flagsPart.empty()) + { + std::wstring flagsLower = ToLower(flagsPart); + // As long as 'flags' contains 'soft', the pattern is considered a soft rule in file copying. + if (flagsLower.find(L"soft") != std::wstring::npos) + { + rule.softOnTargetLocked = true; + } + } + + rules.push_back(rule); + } + + if (rules.empty()) + { + std::wcout << L"[CopyFilesByRules] No rules found in file: " + << rulesFile << L"\n"; + return 0; + } + + std::vector filesToCopy; + std::unordered_set seen; + + for (const auto& entry : fs::directory_iterator(sourceDir)) + { + if (!entry.is_regular_file()) + continue; + + fs::path srcPath = entry.path(); + std::wstring filename = srcPath.filename().wstring(); + std::wstring filenameLower = ToLower(filename); + + bool matched = false; + bool softOnTargetLocked = false; + + for (const auto& rule : rules) + { + if (MatchPattern(rule.patternLower, filenameLower)) + { + matched = true; + softOnTargetLocked = rule.softOnTargetLocked || globalSoftOnTargetLocked; + break; + } + } + + if (!matched) + continue; + + std::wstring key = ToLower(srcPath.wstring()); + if (seen.insert(key).second) + { + FileToCopy item; + item.src = srcPath; + item.softOnTargetLocked = softOnTargetLocked; + filesToCopy.push_back(std::move(item)); + } + } + + if (filesToCopy.empty()) + { + std::wcout << L"[CopyFilesByRules] No files matched any rule under: " + << sourceDir << L"\n"; + return 0; + } + + std::error_code ec; + fs::create_directories(targetDir, ec); + if (ec) + { + std::wcerr << L"[CopyFilesByRules][ERROR] Failed to create TargetDir: " + << targetDir << L" (error: " << ec.message().c_str() << L")\n"; + return 1; + } + + bool anyError = false; + + for (const auto& item : filesToCopy) + { + const fs::path& src = item.src; + bool ruleSoftOnTargetLocked = item.softOnTargetLocked; + + fs::path dst = targetDir / src.filename(); + + bool dstExistedBefore = fs::exists(dst); + + try + { + fs::copy_file(src, dst, + fs::copy_options::overwrite_existing); + std::wcout << L"[CopyFilesByRules] Copy \"" + << src.wstring() << L"\" -> \"" + << dst.wstring() << L"\"\n"; + } + catch (const fs::filesystem_error& ex) + { + // Soft error determination: + // 1) This pattern has enabled soft On Target Locked (by rule or globally), and + // 2) The target file already exists before copying (i.e., an error occurs when overwriting) + if (ruleSoftOnTargetLocked && dstExistedBefore) + { + std::wcerr << L"[CopyFilesByRules][WARN] Failed to overwrite existing file \"" + << dst.wstring() << L"\".\n" + << L" Source: " << src.wstring() << L"\n" + << L" Reason: " << ex.what() << L"\n" + << L" (SoftOnTargetLocked is enabled for this file, " + << L"so this is treated as non-fatal.)\n"; + continue; + } + + // All other cases are considered as fatal errors + std::wcerr << L"[CopyFilesByRules][ERROR] Failed to copy \"" + << src.wstring() << L"\" -> \"" + << dst.wstring() << L"\"\n" + << L" Reason: " << ex.what() << L"\n"; + anyError = true; + } + } + + if (anyError) + { + std::wcerr << L"[CopyFilesByRules] Some files failed to copy.\n"; + return 1; + } + + std::wcout << L"[CopyFilesByRules] Done.\n"; + return 0; + } + catch (const std::exception& ex) + { + std::wcerr << L"[CopyFilesByRules][ERROR] Exception: " << ex.what() << L"\n"; + return 1; + } +} \ No newline at end of file diff --git a/CopyFilesByRules/CopyFilesByRules.vcxproj b/CopyFilesByRules/CopyFilesByRules.vcxproj new file mode 100644 index 0000000..1f76e74 --- /dev/null +++ b/CopyFilesByRules/CopyFilesByRules.vcxproj @@ -0,0 +1,155 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 17.0 + Win32Proj + {6fbe4ffd-37e9-4b77-bd47-1be5e4c0c0ec} + CopyFilesByRules + 10.0 + + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + + + + + + + + + + + + + + + + + + + $(SolutionDir)Tools\ + $(Platform)\$(Configuration)\ + + + $(SolutionDir)Tools\ + $(Platform)\$(Configuration)\ + + + $(SolutionDir)Tools\ + $(Platform)\$(Configuration)\ + + + $(SolutionDir)Tools\ + $(Platform)\$(Configuration)\ + + + + Level3 + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp20 + MultiThreadedDebug + + + Console + false + + + + + Level3 + true + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp20 + MultiThreaded + + + Console + false + + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp20 + MultiThreadedDebug + + + Console + false + + + + + Level3 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp20 + MultiThreaded + + + Console + false + + + + + + + + + \ No newline at end of file diff --git a/CopyFilesByRules/CopyFilesByRules.vcxproj.filters b/CopyFilesByRules/CopyFilesByRules.vcxproj.filters new file mode 100644 index 0000000..097062d --- /dev/null +++ b/CopyFilesByRules/CopyFilesByRules.vcxproj.filters @@ -0,0 +1,22 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + 源文件 + + + \ No newline at end of file diff --git a/FastCopy.sln b/FastCopy.sln index 2345b1a..b5e3194 100644 --- a/FastCopy.sln +++ b/FastCopy.sln @@ -1,11 +1,12 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.14.36705.20 d17.14 +VisualStudioVersion = 17.14.36705.20 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FastCopy", "FastCopy\FastCopy.vcxproj", "{7BAED4AB-03D0-4AA9-AB8D-8D32B1D049F3}" ProjectSection(ProjectDependencies) = postProject {3CFC5459-E40B-4049-A227-81BD5675F3D7} = {3CFC5459-E40B-4049-A227-81BD5675F3D7} + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC} = {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC} EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FastCopyShellExtension", "FastCopyShellExtension\FastCopyShellExtension.vcxproj", "{17F52E4D-1E98-4288-9AA1-20A384171876}" @@ -25,6 +26,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RobocopyInjection", "RobocopyInjection\RobocopyInjection.vcxproj", "{3CFC5459-E40B-4049-A227-81BD5675F3D7}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CopyFilesByRules", "CopyFilesByRules\CopyFilesByRules.vcxproj", "{6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" + ProjectSection(SolutionItems) = preProject + Shared\CommonSharedSettings.cpp = Shared\CommonSharedSettings.cpp + Shared\CommonSharedSettings.h = Shared\CommonSharedSettings.h + Shared\DebugHelper.hpp = Shared\DebugHelper.hpp + Shared\var_init_once.h = Shared\var_init_once.h + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -209,10 +220,43 @@ Global {3CFC5459-E40B-4049-A227-81BD5675F3D7}.Release|x64.Build.0 = Release|x64 {3CFC5459-E40B-4049-A227-81BD5675F3D7}.Release|x86.ActiveCfg = Release|Win32 {3CFC5459-E40B-4049-A227-81BD5675F3D7}.Release|x86.Build.0 = Release|Win32 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Debug|Any CPU.ActiveCfg = Debug|x64 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Debug|Any CPU.Build.0 = Debug|x64 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Debug|ARM.ActiveCfg = Debug|x64 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Debug|ARM.Build.0 = Debug|x64 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Debug|arm64.ActiveCfg = Debug|x64 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Debug|arm64.Build.0 = Debug|x64 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Debug|x64.ActiveCfg = Debug|x64 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Debug|x64.Build.0 = Debug|x64 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Debug|x86.ActiveCfg = Debug|Win32 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Debug|x86.Build.0 = Debug|Win32 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Package|Any CPU.ActiveCfg = Debug|x64 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Package|Any CPU.Build.0 = Debug|x64 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Package|ARM.ActiveCfg = Debug|x64 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Package|ARM.Build.0 = Debug|x64 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Package|arm64.ActiveCfg = Debug|x64 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Package|arm64.Build.0 = Debug|x64 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Package|x64.ActiveCfg = Debug|x64 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Package|x64.Build.0 = Debug|x64 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Package|x86.ActiveCfg = Debug|Win32 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Package|x86.Build.0 = Debug|Win32 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Release|Any CPU.ActiveCfg = Release|x64 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Release|Any CPU.Build.0 = Release|x64 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Release|ARM.ActiveCfg = Release|x64 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Release|ARM.Build.0 = Release|x64 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Release|arm64.ActiveCfg = Release|x64 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Release|arm64.Build.0 = Release|x64 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Release|x64.ActiveCfg = Release|x64 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Release|x64.Build.0 = Release|x64 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Release|x86.ActiveCfg = Release|Win32 + {6FBE4FFD-37E9-4B77-BD47-1BE5E4C0C0EC}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {CEBFE837-48D4-414F-A7BD-302C0604CF5C} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7294B8FA-0F93-4F40-9E50-F2D25E007BF2} EndGlobalSection diff --git a/FastCopy/App.xaml b/FastCopy/App.xaml index b6f07c1..4d4b533 100644 --- a/FastCopy/App.xaml +++ b/FastCopy/App.xaml @@ -1,4 +1,4 @@ - + + diff --git a/FastCopy/CommandLineHandler.cpp b/FastCopy/CommandLineHandler.cpp index e1cb8f9..5df5c4f 100644 --- a/FastCopy/CommandLineHandler.cpp +++ b/FastCopy/CommandLineHandler.cpp @@ -1,4 +1,4 @@ -#include "pch.h" +#include "pch.h" #include "CommandLineHandler.h" #include #include @@ -6,6 +6,7 @@ #include "Console.h" #include "COMInitializeHelper.h" #include "CommandLine.h" +#include "CommonSharedSettings.h" static std::pair parseToastArgument(std::wstring_view argument) { diff --git a/FastCopy/CreateSuspend.h b/FastCopy/CreateSuspend.h index 2fb1acb..8a540d4 100644 Binary files a/FastCopy/CreateSuspend.h and b/FastCopy/CreateSuspend.h differ diff --git a/FastCopy/FastCopy.vcxproj b/FastCopy/FastCopy.vcxproj index 54c4831..3c591dc 100644 --- a/FastCopy/FastCopy.vcxproj +++ b/FastCopy/FastCopy.vcxproj @@ -135,19 +135,6 @@ $(SolutionDir)Public;%(AdditionalIncludeDirectories) $(SolutionDir)Public;%(AdditionalIncludeDirectories) - - mkdir "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\AppX" -xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\FastCopyShellExtension.dll" "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\AppX" /Y /C /I -xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\RobocopyInjection.dll" "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\AppX" /Y /C /I - - - mkdir "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\AppX" -xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\FastCopyShellExtension.dll" "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\AppX" /Y /C /I - - - mkdir "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\AppX" -xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\FastCopyShellExtension.dll" "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\AppX" /Y /C /I - @@ -164,19 +151,6 @@ xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\FastCopyShellExtensio true true - - mkdir "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\AppX" -xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\FastCopyShellExtension.dll" "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\AppX" /Y /C /I - - - mkdir "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\AppX" -xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\FastCopyShellExtension.dll" "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\AppX" /Y /C /I - - - mkdir "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\AppX" -xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\FastCopyShellExtension.dll" "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\AppX" /Y /C /I -xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\RobocopyInjection.dll" "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\AppX" /Y /C /I - @@ -193,19 +167,6 @@ xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\RobocopyInjection.dll true true - - mkdir "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\AppX" -xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\FastCopyShellExtension.dll" "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\AppX" /Y /C /I - - - mkdir "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\AppX" -xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\FastCopyShellExtension.dll" "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\AppX" /Y /C /I - - - mkdir "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\AppX" -xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\FastCopyShellExtension.dll" "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\AppX" /Y /C /I -xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\RobocopyInjection.dll" "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\AppX" /Y /C /I - @@ -217,6 +178,9 @@ xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\RobocopyInjection.dll + + + @@ -351,6 +315,17 @@ xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\RobocopyInjection.dll + + NotUsing + NotUsing + NotUsing + NotUsing + NotUsing + NotUsing + NotUsing + NotUsing + NotUsing + @@ -580,24 +555,24 @@ xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\RobocopyInjection.dll - + true Document true true - + Document true true true - + true true true - + true @@ -618,6 +593,17 @@ xcopy "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\RobocopyInjection.dll + + + + Document + Checking AppxDllRules.txt timestamp... + + echo AppxDllRules touched > "$(IntDir)AppxDllRules.stamp" + + $(IntDir)AppxDllRules.stamp + + + + + $(SolutionDir)Tools\AppxDllRules.txt + + $(SolutionDir)Tools\CopyFilesByRules.exe + + + + + + + + diff --git a/FastCopy/FastCopy.vcxproj.filters b/FastCopy/FastCopy.vcxproj.filters index ed8416d..6ade1e7 100644 --- a/FastCopy/FastCopy.vcxproj.filters +++ b/FastCopy/FastCopy.vcxproj.filters @@ -159,6 +159,9 @@ Utils\Robocopy + + ViewModel\Settings + @@ -309,6 +312,13 @@ Utils\Robocopy + + + ViewModel\Settings + + + Utils\Debug + @@ -569,11 +579,11 @@ - - - - + + + + @@ -594,4 +604,7 @@ + + + \ No newline at end of file diff --git a/FastCopy/Ntdll.cpp b/FastCopy/Ntdll.cpp index 9366bc9..a902d7f 100644 --- a/FastCopy/Ntdll.cpp +++ b/FastCopy/Ntdll.cpp @@ -1,4 +1,4 @@ -#include "pch.h" +#include "pch.h" #include "Ntdll.h" NtDll::NtSuspendProcessFunction NtDll::s_ntSuspendFunctionPointer() diff --git a/FastCopy/Ntdll.h b/FastCopy/Ntdll.h index 2c9b55a..761c0ba 100644 --- a/FastCopy/Ntdll.h +++ b/FastCopy/Ntdll.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include class NtDll diff --git a/FastCopy/RobocopyInjectDll.cpp b/FastCopy/RobocopyInjectDll.cpp index 36e008e..8d71bd0 100644 Binary files a/FastCopy/RobocopyInjectDll.cpp and b/FastCopy/RobocopyInjectDll.cpp differ diff --git a/FastCopy/RobocopyInjectDll.h b/FastCopy/RobocopyInjectDll.h index b5caa0e..faa0d7b 100644 Binary files a/FastCopy/RobocopyInjectDll.h and b/FastCopy/RobocopyInjectDll.h differ diff --git a/FastCopy/RobocopyProcess.cpp b/FastCopy/RobocopyProcess.cpp index 4756cd7..6f63ed9 100644 Binary files a/FastCopy/RobocopyProcess.cpp and b/FastCopy/RobocopyProcess.cpp differ diff --git a/FastCopy/RobocopyProcess.h b/FastCopy/RobocopyProcess.h index 6bc36aa..446e65c 100644 Binary files a/FastCopy/RobocopyProcess.h and b/FastCopy/RobocopyProcess.h differ diff --git a/FastCopy/SettingsViewModel.cpp b/FastCopy/SettingsViewModel.cpp index 2f7fb6d..518a69b 100644 --- a/FastCopy/SettingsViewModel.cpp +++ b/FastCopy/SettingsViewModel.cpp @@ -6,50 +6,172 @@ #include "Global.h" #include "SettingsChangeListener.h" +namespace FCCS = ::FastCopy::Settings; + namespace winrt::FastCopy::implementation { - bool SettingsViewModel::Notify() - { - return m_model.Get(Settings::Notify, true); - } - void SettingsViewModel::Notify(bool value) - { - m_model.Set(Settings::Notify, value); - } - int SettingsViewModel::MultipleWindowBehavior() - { - return m_model.Get(Settings::MultipleWindowBehavior, 0); - } - void SettingsViewModel::MultipleWindowBehavior(int value) - { - m_model.Set(Settings::MultipleWindowBehavior, value); - } - int SettingsViewModel::ThemeSelection() - { - return m_model.Get(Settings::ThemeSelection, 0); - } - void SettingsViewModel::ThemeSelection(int value) - { - m_model.Set(Settings::ThemeSelection, value); - Global::windowEffectHelper.SetTheme(static_cast(value)); - SettingsChangeListener::GetInstance().BroadcastThemeChange(); - } - int SettingsViewModel::BackgroundSelection() - { - return m_model.Get(Settings::BackgroundSelection, 0); - } - void SettingsViewModel::BackgroundSelection(int value) - { - m_model.Set(Settings::BackgroundSelection, value); - Global::windowEffectHelper.SetEffect(value); - SettingsChangeListener::GetInstance().BroadcastThemeChange(); - } - bool SettingsViewModel::DevMode() - { - return m_model.Get(Settings::DevMode, false); - } - void SettingsViewModel::DevMode(bool value) - { - m_model.Set(Settings::DevMode, value); - } + SettingsViewModel::SettingsViewModel() + { + m_dispatcher = winrt::Microsoft::UI::Dispatching::DispatcherQueue::GetForCurrentThread(); + + FCCS::CommonSharedSettings::Instance().RegisterChangeListener( + &SettingsViewModel::OnSharedSettingsChanged, + this); + m_sharedSettingsSubscribed = true; + } + + SettingsViewModel::~SettingsViewModel() + { + if (m_sharedSettingsSubscribed) + { + FCCS::CommonSharedSettings::Instance().UnregisterChangeListener( + &SettingsViewModel::OnSharedSettingsChanged, + this); + } + } + + bool SettingsViewModel::Notify() + { + return m_model.Get(Settings::Notify, true); + } + void SettingsViewModel::Notify(bool value) + { + m_model.Set(Settings::Notify, value); + } + int SettingsViewModel::MultipleWindowBehavior() + { + return m_model.Get(Settings::MultipleWindowBehavior, 0); + } + void SettingsViewModel::MultipleWindowBehavior(int value) + { + m_model.Set(Settings::MultipleWindowBehavior, value); + } + int SettingsViewModel::ThemeSelection() + { + return m_model.Get(Settings::ThemeSelection, 0); + } + void SettingsViewModel::ThemeSelection(int value) + { + m_model.Set(Settings::ThemeSelection, value); + Global::windowEffectHelper.SetTheme(static_cast(value)); + SettingsChangeListener::GetInstance().BroadcastThemeChange(); + } + int SettingsViewModel::BackgroundSelection() + { + return m_model.Get(Settings::BackgroundSelection, 0); + } + void SettingsViewModel::BackgroundSelection(int value) + { + m_model.Set(Settings::BackgroundSelection, value); + Global::windowEffectHelper.SetEffect(value); + SettingsChangeListener::GetInstance().BroadcastThemeChange(); + } + bool SettingsViewModel::DevMode() + { + return m_model.Get(Settings::DevMode, false); + } + void SettingsViewModel::DevMode(bool value) + { + m_model.Set(Settings::DevMode, value); + } + + bool SettingsViewModel::LoggerEnabled() + { + auto& s = FCCS::CommonSharedSettings::Instance(); + return s.GetBool(L"Logger", L"Enabled").value_or(false); + } + + void SettingsViewModel::LoggerEnabled(bool value) + { + auto& s = FCCS::CommonSharedSettings::Instance(); + s.SetBool(L"Logger", L"Enabled", value); + } + + int SettingsViewModel::LoggerVerbosity() + { + auto& s = FCCS::CommonSharedSettings::Instance(); + // Default 2 = Warn + auto v = s.GetInt(L"Logger", L"Verbosity").value_or(2); + if (v < 0) v = 0; + if (v > 5) v = 5; + return v; + } + + void SettingsViewModel::LoggerVerbosity(int value) + { + if (value < 0) value = 0; + if (value > 5) value = 5; + + auto& s = FCCS::CommonSharedSettings::Instance(); + s.SetInt(L"Logger", L"Verbosity", value); + } + + bool SettingsViewModel::LogDBEnabled() + { + auto& s = FCCS::CommonSharedSettings::Instance(); + return s.GetBool(L"Logger", L"DebugBreakEnabled").value_or(false); + } + + void SettingsViewModel::LogDBEnabled(bool value) + { + auto& s = FCCS::CommonSharedSettings::Instance(); + s.SetBool(L"Logger", L"DebugBreakEnabled", value); + } + + int SettingsViewModel::LogDBMinVerbositySelectedIndex() + { + auto& s = FCCS::CommonSharedSettings::Instance(); + // default value = Debug(4) + // 0 = None, 1 = Error, 2 = Warn, 3 = Info, 4 = Debug,5 = Trace + auto v = s.GetInt(L"Logger", L"DebugBreakMinVerbosity").value_or(4); + v = std::clamp(v - 1, 0, 4); + return v; + } + + void SettingsViewModel::LogDBMinVerbositySelectedIndex(int value) + { + value = std::clamp(value, 0, 4); + value += 1; // 1..5 + + auto& s = FCCS::CommonSharedSettings::Instance(); + s.SetInt(L"Logger", L"DebugBreakMinVerbosity", value); + } + + void __stdcall SettingsViewModel::OnSharedSettingsChanged(void* ctx) noexcept + { + auto self = static_cast(ctx); + if (!self) + return; + + auto &dispatcher = self->m_dispatcher; + if (!dispatcher) + return; + + //dispatcher.TryEnqueue([self]() + // { + // self->OnSharedSettingsChangedNotifyUI(); + // }); + + // Get a weak reference to the 'projection object' + auto weak = self->get_weak(); + + dispatcher.TryEnqueue([weak]() + { + if (auto strong = weak.get()) + { + // Back to implementation class + strong.get()->OnSharedSettingsChangedNotifyUI(); + } + }); + } + + void SettingsViewModel::OnSharedSettingsChangedNotifyUI() + { + RaisePropertyChangedEvent({ + L"LoggerEnabled", + L"LoggerVerbosity", + L"LogDBEnabled", + L"LogDBMinVerbositySelectedIndex" + }); + } } diff --git a/FastCopy/SettingsViewModel.h b/FastCopy/SettingsViewModel.h index cf615d5..cb0cd86 100644 --- a/FastCopy/SettingsViewModel.h +++ b/FastCopy/SettingsViewModel.h @@ -2,12 +2,15 @@ #include "SettingsViewModel.g.h" #include "Settings.h" +#include "CommonSharedSettings.h" +#include namespace winrt::FastCopy::implementation { struct SettingsViewModel : SettingsViewModelT { - SettingsViewModel() = default; + SettingsViewModel(); + ~SettingsViewModel(); bool Notify(); void Notify(bool value); @@ -23,7 +26,63 @@ namespace winrt::FastCopy::implementation bool DevMode(); void DevMode(bool value); + + bool LoggerEnabled(); + void LoggerEnabled(bool value); + + int LoggerVerbosity(); // 0~5 : Off..Trace + void LoggerVerbosity(int value); + + bool LogDBEnabled(); // Is enabled to break when Logger Verbosity >= DebugBreakMinVerbosity + void LogDBEnabled(bool value); + + int LogDBMinVerbositySelectedIndex(); // 0~4, for combobox index + void LogDBMinVerbositySelectedIndex(int value); + +#pragma region NotifyPropertyChanged Common Functions + winrt::event_token PropertyChanged(winrt::Microsoft::UI::Xaml::Data::PropertyChangedEventHandler const& handler) + { + m_propertyChangedToken = m_eventPropertyChanged.add(handler); + return m_propertyChangedToken; + } + void PropertyChanged(winrt::event_token token) + { + m_eventPropertyChanged.remove(token); + } + void RaisePropertyChangedEvent(std::wstring_view const& propertyName) + { + // Only instantiate the arguments class if the event has any listeners + if (m_eventPropertyChanged) + { + winrt::Microsoft::UI::Xaml::Data::PropertyChangedEventArgs args{ propertyName }; + m_eventPropertyChanged(*this, args); + } + } + + void RaisePropertyChangedEvent(std::initializer_list const& propertyNames) + { + // Only instantiate the argumens class (and only once) if the event has any listeners + if (m_eventPropertyChanged) + { + for (auto&& propertyName : propertyNames) + { + winrt::Microsoft::UI::Xaml::Data::PropertyChangedEventArgs args{ propertyName }; + m_eventPropertyChanged(*this, args); + } + } + } +#pragma endregion + private: + static void __stdcall OnSharedSettingsChanged(void* ctx) noexcept; + + void OnSharedSettingsChangedNotifyUI(); + + winrt::Microsoft::UI::Dispatching::DispatcherQueue m_dispatcher{ nullptr }; + bool m_sharedSettingsSubscribed{ false }; + winrt::event m_eventPropertyChanged; + winrt::event_token m_propertyChangedToken{}; + Settings m_model; }; } diff --git a/FastCopy/SettingsViewModel.idl b/FastCopy/SettingsViewModel.idl index 628f18b..fbf7924 100644 --- a/FastCopy/SettingsViewModel.idl +++ b/FastCopy/SettingsViewModel.idl @@ -1,8 +1,9 @@ -namespace FastCopy +namespace FastCopy { [bindable] [default_interface] runtimeclass SettingsViewModel + : Microsoft.UI.Xaml.Data.INotifyPropertyChanged { SettingsViewModel(); @@ -11,5 +12,9 @@ namespace FastCopy Int32 ThemeSelection; Int32 BackgroundSelection; Boolean DevMode; + Boolean LoggerEnabled; + Int32 LoggerVerbosity; + Boolean LogDBEnabled; + Int32 LogDBMinVerbositySelectedIndex; } } diff --git a/FastCopy/SettingsWindow.xaml b/FastCopy/SettingsWindow.xaml index d399629..33df6f9 100644 --- a/FastCopy/SettingsWindow.xaml +++ b/FastCopy/SettingsWindow.xaml @@ -1,4 +1,4 @@ - + - - - - - - - - - - + + + + + + + + + + + - - - + + + + + + + + + + True + + + + + + + + + + + + True + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + - - - - - - + + + + + - - - - - - - - - - - - - - + + + + + + - - - - - + + + + + + + + + + - - - - - - + + + + + + + + + + - - - - - - - - - - + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + diff --git a/FastCopy/SettingsWindow.xaml.cpp b/FastCopy/SettingsWindow.xaml.cpp index 6e18b4c..e402d26 100644 --- a/FastCopy/SettingsWindow.xaml.cpp +++ b/FastCopy/SettingsWindow.xaml.cpp @@ -1,4 +1,4 @@ -#include "pch.h" +#include "pch.h" #include "SettingsWindow.xaml.h" #if __has_include("SettingsWindow.g.cpp") #include "SettingsWindow.g.cpp" diff --git a/FastCopy/Strings/en-US/Resources.resw b/FastCopy/Strings/en-US/Resources.resw index 95781c4..064a1b8 100644 --- a/FastCopy/Strings/en-US/Resources.resw +++ b/FastCopy/Strings/en-US/Resources.resw @@ -165,6 +165,13 @@ Diagnostic Mode + + Configure whether to enable console window output redirection, or control the log output granularity of the Shell extension (DLL). + + + + Developer Options + duplicates @@ -186,6 +193,30 @@ Light + + Enable the internal breakpoint feature within log lines, which allows a break to be triggered when logs are output at critical points. This can be helpful when running with a debugger attached. + + + Enabled Breakpoint at Log Line + + + Configure the conditions for triggering a log breakpoint via the shortcut key (Ctrl+Alt+F1). Log lines whose level is lower than the filter level can still trigger a breakpoint. (This setting specifies the minimum breakpoint level.) + + + Min Log Level for Breakpoint + + + Control whether the Shell extension outputs logs to the debugger. + + + Enable ShellExt(Dll) Log Outputs + + + The higher the configured log filter level, the more detailed the output. + + + ShellExt Log Filter Level + Mica diff --git a/FastCopy/Strings/zh-CN/Resources.resw b/FastCopy/Strings/zh-CN/Resources.resw index 78902a2..863069a 100644 --- a/FastCopy/Strings/zh-CN/Resources.resw +++ b/FastCopy/Strings/zh-CN/Resources.resw @@ -165,6 +165,12 @@ 开发模式 + + 设置启用控制台窗口重定向输出,或者控制 Shell 扩展 (dll) 的日志输出细粒度 + + + 开发者选项 + 个重名文件 @@ -186,6 +192,30 @@ 亮色 + + 启用日志行内的内部中断功能,这将允许在关键位置输出日志时触发中断。当以调试器附加模式运行时,这会有一定帮助。 + + + 启用日志内部中断(仅限调试器附加模式) + + + 设置通过快捷键(Ctrl+Alt+F1)触发日志中断的条件,日志行本身的级别低于过滤级别的日志可以触发中断。(此项设置最小中断级别) + + + 最小日志中断级别 + + + 控制 Shell 扩展是否向调试器输出日志 + + + 启用 Shell 扩展日志输出 + + + 设置的日志过滤级别越高,输出内容越详细 + + + Shell 扩展的日志过滤输出级别 + Mica diff --git a/FastCopy/pch.h b/FastCopy/pch.h index 106897d..73045e9 100644 --- a/FastCopy/pch.h +++ b/FastCopy/pch.h @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. #pragma once @@ -32,4 +32,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include \ No newline at end of file diff --git a/FastCopyShellExtension/DebugPrint.cpp b/FastCopyShellExtension/DebugPrint.cpp new file mode 100644 index 0000000..ae3e6fd --- /dev/null +++ b/FastCopyShellExtension/DebugPrint.cpp @@ -0,0 +1,109 @@ +#include "DebugPrint.h" + +#include "CommonSharedSettings.h" +#include "var_init_once.h" + +namespace +{ + // Default verbosity levels + constexpr FastCopyLogger::Verbosity DefaultLogVerbosity = FastCopyLogger::Verbosity::Warn; + constexpr FastCopyLogger::Verbosity DefaultDebugBreakVerbosity = FastCopyLogger::Verbosity::Debug; + + // Get the verbosity level from the config and validate it + FastCopyLogger::Verbosity GetVerbosityFromConfig() noexcept { + try { + auto& s = FastCopy::Settings::CommonSharedSettings::Instance(); + + // Read the verbosity as an int from the configuration + auto v = s.GetInt(L"Logger", L"Verbosity") + .value_or(static_cast(DefaultLogVerbosity)); // Default to Warn (2) + + // Ensure the verbosity is within the allowed range + if (v < static_cast(FastCopyLogger::Verbosity::MinVerbosity)) { + v = static_cast(FastCopyLogger::Verbosity::MinVerbosity); + } + if (v > static_cast(FastCopyLogger::Verbosity::MaxVerbosity)) { + v = static_cast(FastCopyLogger::Verbosity::MaxVerbosity); + } + + // Return the enum value by casting back from int to Verbosity + return static_cast(v); + } + catch (...) { + return DefaultLogVerbosity; + } + } + + // Get the 'Enabled' setting from the config + bool GetEnabledFromConfig() noexcept { + try { + auto& s = FastCopy::Settings::CommonSharedSettings::Instance(); + return s.GetBool(L"Logger", L"Enabled").value_or(true); + } + catch (...) { + return true; // Default to enabled + } + } + + // Get the 'DebugBreakEnabled' setting from the config + bool GetDebugBreakEnabledFromConfig() noexcept { + try { + auto& s = FastCopy::Settings::CommonSharedSettings::Instance(); + return s.GetBool(L"Logger", L"DebugBreakEnabled").value_or(false); + } + catch (...) { + return false; // Default to disabled + } + } + + // Get the debug break verbosity level from the config + FastCopyLogger::Verbosity GetDebugBreakVerbosityFromConfig() noexcept { + try { + auto& s = FastCopy::Settings::CommonSharedSettings::Instance(); + + // Read the debug break verbosity as an int + auto v = s.GetInt(L"Logger", L"DebugBreakMinVerbosity") + .value_or(static_cast(DefaultDebugBreakVerbosity)); // Default to Debug (4) + + // Ensure the verbosity is within the valid range + if (v < static_cast(FastCopyLogger::Verbosity::Error)) { + v = static_cast(FastCopyLogger::Verbosity::Error); + } + if (v > static_cast(FastCopyLogger::Verbosity::MaxVerbosity)) { + v = static_cast(FastCopyLogger::Verbosity::MaxVerbosity); + } + + // Return the enum value by casting from int to Verbosity + return static_cast(v); + } + catch (...) { + return DefaultDebugBreakVerbosity; + } + } +} // namespace + +FastCopyLogger::FastCopyLogger() noexcept + : FastCopyLogger(GetVerbosityFromConfig()) +{ + SetEnabled(GetEnabledFromConfig()); + + // Register settings.ini change notification. + // This will refresh settings from configuration whenever there is a change + FastCopy::Settings::CommonSharedSettings::Instance().RegisterChangeListener( + [](void* /*context*/) noexcept { + auto& logger = FastCopyLogger::Instance(); + logger.OnConfigChanged( + GetEnabledFromConfig(), + GetVerbosityFromConfig(), + GetDebugBreakEnabledFromConfig(), + GetDebugBreakVerbosityFromConfig() // notice: Min verbosity filter level for debug break + ); + }, + nullptr); +} + +FastCopyLogger& FastCopyLogger::Instance() noexcept +{ + STATIC_INIT_ONCE(FastCopyLogger, s); + return *s; +} \ No newline at end of file diff --git a/FastCopyShellExtension/DebugPrint.h b/FastCopyShellExtension/DebugPrint.h new file mode 100644 index 0000000..175c557 --- /dev/null +++ b/FastCopyShellExtension/DebugPrint.h @@ -0,0 +1,440 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + + +namespace { + std::wstring Win32FormatGuid(REFGUID guid) noexcept + { + wchar_t buf[64] = {}; + if(!StringFromGUID2(guid, buf, ARRAYSIZE(buf))) return {}; + return buf; + } + + std::wstring utf8_to_wstring(char const* utf8_str) noexcept { + if (!utf8_str) return {}; + + // Get required buffer size (including null terminator) + int size_needed = MultiByteToWideChar( + CP_UTF8, // Code page: UTF-8 + 0, // Flags: no special handling + utf8_str, // Input narrow string + -1, // Length: -1 = auto-detect null terminator + nullptr, // Output buffer: none (get size) + 0 // Output buffer size: 0 (get size) + ); + + if (size_needed == 0) { + return {}; + } + + // Allocate buffer and convert + std::wstring wstr(size_needed - 1, L'\0'); // -1 to exclude null terminator + int converted = MultiByteToWideChar( + CP_UTF8, + 0, + utf8_str, + -1, + &wstr[0], // Output buffer + size_needed // Buffer size + ); + + if (converted == 0) { + return {}; + } + + return wstr; + } +} // namespace + +class FastCopyLogger +{ +public: + enum class Verbosity : int + { + MinVerbosity = 0, + Off = 0, + Error = 1, + Warn = 2, + Info = 3, + Debug = 4, + Trace = 5, + MaxVerbosity = 5, + }; + + static FastCopyLogger& Instance() noexcept; + + void SetEnabled(bool enabled) noexcept + { + m_enabled.store(enabled, std::memory_order_relaxed); + } + + bool IsEnabled() const noexcept + { + return m_enabled.load(std::memory_order_relaxed); + } + + void SetGlobalVerbosity(Verbosity v) noexcept + { + std::lock_guard guard(m_mutex); + m_initialVerbosity.store(v, std::memory_order_relaxed); + if (m_threadVerbosityCount == 0) + { + m_currentVerbosity.store(v, std::memory_order_relaxed); + } + } + + Verbosity GetGlobalVerbosity() const noexcept + { + return m_currentVerbosity.load(std::memory_order_relaxed); + } + + void OnConfigChanged(bool enabled, Verbosity verbosity, + bool BreakOnLog, Verbosity BreakMinLevel) noexcept + { + SetEnabled(enabled); + SetGlobalVerbosity(verbosity); + SetBreakOnLog(BreakOnLog, BreakMinLevel); + } + + bool SetThreadVerbosity(Verbosity verbosity) noexcept + { + auto& tv = GetThreadVerbosityRef(); + if (tv) // already in use + return false; + + tv = verbosity; + + std::lock_guard guard(m_mutex); + ++m_threadVerbosityCount; + + auto cur = m_currentVerbosity.load(std::memory_order_relaxed); + if (static_cast(cur) < static_cast(verbosity)) + { + m_currentVerbosity.store(verbosity, std::memory_order_relaxed); + } + + return true; + } + + void ResetThreadVerbosity() noexcept + { + auto& tv = GetThreadVerbosityRef(); + if (!tv) + return; + + tv.reset(); + + std::lock_guard guard(m_mutex); + if (--m_threadVerbosityCount == 0) + { + m_currentVerbosity.store(m_initialVerbosity.load(std::memory_order_relaxed), + std::memory_order_relaxed); + } + } + + class ScopedThreadVerbosity + { + public: + explicit ScopedThreadVerbosity(Verbosity v) noexcept + { + m_inUse = FastCopyLogger::Instance().SetThreadVerbosity(v); + } + + ~ScopedThreadVerbosity() + { + if (m_inUse) + FastCopyLogger::Instance().ResetThreadVerbosity(); + } + + ScopedThreadVerbosity(const ScopedThreadVerbosity&) = delete; + ScopedThreadVerbosity& operator=(const ScopedThreadVerbosity&) = delete; + + private: + bool m_inUse{ false }; + }; + + bool ShouldLog(Verbosity msgLevel) const noexcept + { + if (!m_enabled.load(std::memory_order_relaxed)) + return false; + + auto const& tv = GetThreadVerbosityRef(); + if (tv) + { + return static_cast(*tv) >= static_cast(msgLevel); + } + + auto cur = m_currentVerbosity.load(std::memory_order_relaxed); + return static_cast(cur) >= static_cast(msgLevel); + } + + // L"xxx {}", arg + template + void LogFmt(Verbosity lvl, + wchar_t const* func, + int line, + std::wformat_string fmt, + Args&&... args) noexcept + { + if (!ShouldLog(lvl)) + return; + + try + { + std::wstring body = std::format(fmt, std::forward(args)...); + std::wstring msg; + msg.reserve(body.size() + 128); + + std::wstring_view levelTag = LevelToTag(lvl); + std::format_to(std::back_inserter(msg), + L"[FastCopy][{}][PID:{}][TID:{}][{}:{}] {}", + levelTag, + static_cast(GetCurrentProcessId()), + static_cast(GetCurrentThreadId()), + func ? func : L"(null)", + line, + body); + + msg.push_back(L'\n'); + OutputDebugStringW(msg.c_str()); +#ifdef _DEBUG + if (m_breakOnLog.load(std::memory_order_relaxed) && + static_cast(lvl) <= + static_cast(m_breakMinLevel.load(std::memory_order_relaxed)) && + ::IsDebuggerPresent() && + IsDebugBreakKeyHeld()) + { + ::DebugBreak(); + } +#endif + } + catch (...) { +#ifdef _DEBUG + OutputDebugStringW(L"[FastCopy] LogFmt: failed to format log message.\n"); +#endif + } + } + + template + void LogFmtA(Verbosity lvl, + char const* func, + int line, + std::format_string fmt, + Args&&... args) noexcept + { + if (!ShouldLog(lvl)) + return; + + try + { + std::string bodyA = std::format(fmt, std::forward(args)...); + std::wstring body = utf8_to_wstring(bodyA.c_str()); + + std::wstring funcW = func ? utf8_to_wstring(func) : L"(null)"; + + std::wstring msg; + msg.reserve(body.size() + 128); + + std::wstring_view levelTag = LevelToTag(lvl); + std::format_to(std::back_inserter(msg), + L"[FastCopy][{}][PID:{}][TID:{}][{}:{}] {}", + levelTag, + static_cast(GetCurrentProcessId()), + static_cast(GetCurrentThreadId()), + funcW, + line, + body); + + msg.push_back(L'\n'); + OutputDebugStringW(msg.c_str()); +#ifdef _DEBUG + if (m_breakOnLog.load(std::memory_order_relaxed) && + static_cast(lvl) <= + static_cast(m_breakMinLevel.load(std::memory_order_relaxed)) && + ::IsDebuggerPresent() && + IsDebugBreakKeyHeld()) + { + ::DebugBreak(); + } +#endif + } + catch (...) { +#ifdef _DEBUG + OutputDebugStringW(L"[FastCopy] LogFmt: failed to format log message.\n"); +#endif + } + } + + void LogProcessInfo(wchar_t const* tag = L"DllMain") noexcept + { + wchar_t exePath[MAX_PATH] = {}; + GetModuleFileNameW(nullptr, exePath, ARRAYSIZE(exePath)); + + wchar_t const* exeName = wcsrchr(exePath, L'\\'); + exeName = exeName ? exeName + 1 : exePath; + + LogFmt(Verbosity::Info, + L"ProcessInfo", + __LINE__, + L"[{}] Host process '{}' (PID={}), path='{}'", + tag, + exeName, + static_cast(GetCurrentProcessId()), + exePath); + } + + void LogDllPath(HMODULE hModule, + wchar_t const* tag = L"DllMain") noexcept + { + wchar_t dllPath[MAX_PATH] = {}; + if (hModule) + GetModuleFileNameW(hModule, dllPath, ARRAYSIZE(dllPath)); + + LogFmt(Verbosity::Info, + L"DllInfo", + __LINE__, + L"[{}] DllModule={}, path='{}'", + tag, + static_cast(hModule), + dllPath[0] ? dllPath : L"(unknown)"); + } + + void SetBreakOnLog(bool enable, + Verbosity minLevel = Verbosity::Error) noexcept + { + m_breakOnLog.store(enable, std::memory_order_relaxed); + m_breakMinLevel.store(minLevel, std::memory_order_relaxed); + } + + bool IsBreakOnLogEnabled() const noexcept + { + return m_breakOnLog.load(std::memory_order_relaxed); + } + + Verbosity GetBreakOnLogMinLevel() const noexcept + { + return m_breakMinLevel.load(std::memory_order_relaxed); + } + + FastCopyLogger() noexcept; + ~FastCopyLogger() = default; + + explicit FastCopyLogger(Verbosity initial) noexcept + : m_initialVerbosity(initial), + m_currentVerbosity(initial) + { + } +private: + + static std::optional& GetThreadVerbosityRef() noexcept + { + static thread_local std::optional s_threadVerbosity; + return s_threadVerbosity; + } + + static std::wstring_view LevelToTag(Verbosity lvl) noexcept + { + switch (lvl) + { + case Verbosity::Off: return L"OFF"; + case Verbosity::Error: return L"ERR"; + case Verbosity::Warn: return L"WRN"; + case Verbosity::Info: return L"INF"; + case Verbosity::Debug: return L"DBG"; + case Verbosity::Trace: return L"TRC"; + } + return L"UNK"; + } + + static bool IsDebugBreakKeyHeld() noexcept + { + // Debug break key is Ctrl + Shift + F1 + SHORT const ctrl = ::GetAsyncKeyState(VK_CONTROL); + SHORT const shift = ::GetAsyncKeyState(VK_SHIFT); + SHORT const f1 = ::GetAsyncKeyState(VK_F1); + + return (ctrl & 0x8000) && + (shift & 0x8000) && + (f1 & 0x8000); + } +private: + std::atomic m_enabled{ true }; + std::atomic m_initialVerbosity{ Verbosity::Warn }; + std::atomic m_currentVerbosity{ Verbosity::Warn }; + std::atomic m_breakOnLog{ false }; + std::atomic m_breakMinLevel{ Verbosity::Error }; + + mutable std::mutex m_mutex; + int m_threadVerbosityCount{ 0 }; +}; + +#ifndef FC_LOG_DISABLE + +#define FC_LOG_AT(level, ...) \ + FastCopyLogger::Instance().LogFmt( \ + level, \ + __FUNCTIONW__, \ + __LINE__, \ + __VA_ARGS__) + +#define FC_LOG_ERROR(...) \ + FC_LOG_AT(FastCopyLogger::Verbosity::Error, __VA_ARGS__) + +#define FC_LOG_WARN(...) \ + FC_LOG_AT(FastCopyLogger::Verbosity::Warn, __VA_ARGS__) + +#define FC_LOG_INFO(...) \ + FC_LOG_AT(FastCopyLogger::Verbosity::Info, __VA_ARGS__) + +#define FC_LOG_DEBUG(...) \ + FC_LOG_AT(FastCopyLogger::Verbosity::Debug, __VA_ARGS__) + +#define FC_LOG_TRACE(...) \ + FC_LOG_AT(FastCopyLogger::Verbosity::Trace, __VA_ARGS__) + +#define FC_LOGA_AT(level, ...) \ + FastCopyLogger::Instance().LogFmtA( \ + level, \ + __FUNCTION__, \ + __LINE__, \ + __VA_ARGS__) + +#define FC_LOGA_ERROR(...) \ + FC_LOGA_AT(FastCopyLogger::Verbosity::Error, __VA_ARGS__) + +#define FC_LOGA_WARN(...) \ + FC_LOGA_AT(FastCopyLogger::Verbosity::Warn, __VA_ARGS__) + +#define FC_LOGA_INFO(...) \ + FC_LOGA_AT(FastCopyLogger::Verbosity::Info, __VA_ARGS__) + +#define FC_LOGA_DEBUG(...) \ + FC_LOGA_AT(FastCopyLogger::Verbosity::Debug, __VA_ARGS__) + +#define FC_LOGA_TRACE(...) \ + FC_LOGA_AT(FastCopyLogger::Verbosity::Trace, __VA_ARGS__) + +#else + +#define FC_LOG_AT(level, ...) ((void)0) +#define FC_LOG_ERROR(...) ((void)0) +#define FC_LOG_WARN(...) ((void)0) +#define FC_LOG_INFO(...) ((void)0) +#define FC_LOG_DEBUG(...) ((void)0) +#define FC_LOG_TRACE(...) ((void)0) + +#define FC_LOGA_AT(level, ...) ((void)0) +#define FC_LOGA_ERROR(...) ((void)0) +#define FC_LOGA_WARN(...) ((void)0) +#define FC_LOGA_INFO(...) ((void)0) +#define FC_LOGA_DEBUG(...) ((void)0) +#define FC_LOGA_TRACE(...) ((void)0) + +#endif \ No newline at end of file diff --git a/FastCopyShellExtension/FastCopyRootCommand.cpp b/FastCopyShellExtension/FastCopyRootCommand.cpp index 1313064..17a7c2e 100644 Binary files a/FastCopyShellExtension/FastCopyRootCommand.cpp and b/FastCopyShellExtension/FastCopyRootCommand.cpp differ diff --git a/FastCopyShellExtension/FastCopyRootCommand.h b/FastCopyShellExtension/FastCopyRootCommand.h index 4d7d977..b909611 100644 Binary files a/FastCopyShellExtension/FastCopyRootCommand.h and b/FastCopyShellExtension/FastCopyRootCommand.h differ diff --git a/FastCopyShellExtension/FastCopyShellExtension.vcxproj b/FastCopyShellExtension/FastCopyShellExtension.vcxproj index 94ebc27..be4d3b0 100644 --- a/FastCopyShellExtension/FastCopyShellExtension.vcxproj +++ b/FastCopyShellExtension/FastCopyShellExtension.vcxproj @@ -100,12 +100,24 @@ $(SolutionDir)$(Platform)\$(Configuration)\FastCopy + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)Shared; $(SolutionDir)$(Platform)\Release\FastCopy\ + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)Shared; $(SolutionDir)$(Platform)\$(Configuration)\FastCopy\ + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)Shared; + + + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)Shared; + + + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)Shared; + + + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)Shared; false @@ -131,6 +143,9 @@ FastCopyShellExtension.res + + call "$(SolutionDir)Tools\CopyFilesSimple.bat" "$(OutputPath)$(ProjectName).dll" "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\Appx" + @@ -156,6 +171,9 @@ FastCopyShellExtension.res + + call "$(SolutionDir)Tools\CopyFilesSimple.bat" "$(OutputPath)$(ProjectName).dll" "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\Appx" + @@ -181,6 +199,9 @@ FastCopyShellExtension.res + + call "$(SolutionDir)Tools\CopyFilesSimple.bat" "$(OutputPath)$(ProjectName).dll" "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\Appx" + @@ -202,12 +223,12 @@ Source.def %(AdditionalDependencies) - - xcopy "$(OutputPath)\$(ProjectName).dll" "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\Appx" /Y /I /C - FastCopyShellExtension.res + + call "$(SolutionDir)Tools\CopyFilesSimple.bat" "$(OutputPath)$(ProjectName).dll" "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\Appx" + @@ -236,6 +257,9 @@ FastCopyShellExtension.res + + call "$(SolutionDir)Tools\CopyFilesSimple.bat" "$(OutputPath)$(ProjectName).dll" "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\Appx" + @@ -264,15 +288,29 @@ FastCopyShellExtension.res + + call "$(SolutionDir)Tools\CopyFilesSimple.bat" "$(OutputPath)$(ProjectName).dll" "$(SolutionDir)$(Platform)\$(Configuration)\FastCopy\Appx" + + + NotUsing + NotUsing + NotUsing + NotUsing + NotUsing + NotUsing + + + + @@ -286,11 +324,16 @@ + + + + + diff --git a/FastCopyShellExtension/FastCopyShellExtension.vcxproj.filters b/FastCopyShellExtension/FastCopyShellExtension.vcxproj.filters index d1ff8ed..3905cca 100644 --- a/FastCopyShellExtension/FastCopyShellExtension.vcxproj.filters +++ b/FastCopyShellExtension/FastCopyShellExtension.vcxproj.filters @@ -58,6 +58,18 @@ Icons + + Source Files + + + Source Files + + + Source Files + + + Source Files + @@ -109,6 +121,21 @@ Icons + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + diff --git a/FastCopyShellExtension/FastCopySubCommand.cpp b/FastCopyShellExtension/FastCopySubCommand.cpp index c4664f8..07f6f98 100644 Binary files a/FastCopyShellExtension/FastCopySubCommand.cpp and b/FastCopyShellExtension/FastCopySubCommand.cpp differ diff --git a/FastCopyShellExtension/HostProcessHook.cpp b/FastCopyShellExtension/HostProcessHook.cpp new file mode 100644 index 0000000..148ba35 --- /dev/null +++ b/FastCopyShellExtension/HostProcessHook.cpp @@ -0,0 +1,93 @@ +#include "HostProcessHook.h" +#include "HostShutdownWatcher.h" +#include +#include + + +namespace +{ + using PFN_TerminateProcess = BOOL(WINAPI*)(HANDLE, UINT); + using PFN_CoUninitialize = void (WINAPI*)(void); + + PFN_TerminateProcess g_RealTerminateProcess = ::TerminateProcess; + PFN_CoUninitialize g_RealCoUninitialize = ::CoUninitialize; + + bool g_HooksInstalled = false; + + void WINAPI DetouredCoUninitialize() + { + HostShutdownWatcher::OnCoUninitialize(); + g_RealCoUninitialize(); + } + + BOOL WINAPI DetouredTerminateProcess(HANDLE hProcess, UINT uExitCode) + { + HostShutdownWatcher::OnTerminateProcess(hProcess, uExitCode); + return g_RealTerminateProcess(hProcess, uExitCode); + } +} + +bool HostProcessHook::Install() +{ + if (g_HooksInstalled) + return true; + + LONG error = NO_ERROR; + + DetourTransactionBegin(); + DetourUpdateThread(GetCurrentThread()); + + error = DetourAttach( + reinterpret_cast(&g_RealCoUninitialize), + reinterpret_cast(DetouredCoUninitialize) + ); + if (error != NO_ERROR) + { + DetourTransactionAbort(); + return false; + } + + error = DetourAttach( + reinterpret_cast(&g_RealTerminateProcess), + reinterpret_cast(DetouredTerminateProcess) + ); + if (error != NO_ERROR) + { + DetourTransactionAbort(); + return false; + } + + error = DetourTransactionCommit(); + if (error == NO_ERROR) + { + g_HooksInstalled = true; + return true; + } + else + { + DetourTransactionAbort(); + return false; + } +} + +void HostProcessHook::Uninstall() +{ + if (!g_HooksInstalled) + return; + + DetourTransactionBegin(); + DetourUpdateThread(GetCurrentThread()); + + DetourDetach( + reinterpret_cast(&g_RealCoUninitialize), + reinterpret_cast(DetouredCoUninitialize) + ); + DetourDetach( + reinterpret_cast(&g_RealTerminateProcess), + reinterpret_cast(DetouredTerminateProcess) + ); + + DetourTransactionCommit(); + + g_HooksInstalled = false; +} diff --git a/FastCopyShellExtension/HostProcessHook.h b/FastCopyShellExtension/HostProcessHook.h new file mode 100644 index 0000000..07e2c61 --- /dev/null +++ b/FastCopyShellExtension/HostProcessHook.h @@ -0,0 +1,9 @@ +#pragma once +#include + +class HostProcessHook +{ +public: + static bool Install(); + static void Uninstall(); +}; diff --git a/FastCopyShellExtension/HostShutdownWatcher.cpp b/FastCopyShellExtension/HostShutdownWatcher.cpp new file mode 100644 index 0000000..d330b9c --- /dev/null +++ b/FastCopyShellExtension/HostShutdownWatcher.cpp @@ -0,0 +1,68 @@ +#include "HostShutdownWatcher.h" + +std::atomic HostShutdownWatcher::s_state{ + HostShutdownWatcher::State::Running +}; +HostShutdownWatcher::CleanupCallback HostShutdownWatcher::s_callback = nullptr; +DWORD HostShutdownWatcher::s_ownerThreadId = 0; + +void HostShutdownWatcher::Initialize(CleanupCallback cb) +{ + s_callback = cb; + + /* + 0:000> k + # Child-SP RetAddr Call Site + 00 00000043`9a98f258 00007fff`a7246725 KERNELBASE!wil::details::DebugBreak+0x2 + 01 00000043`9a98f260 00007fff`a728d2fd FastCopyShellExtension!FastCopy::Settings::CommonSharedSettings::Shutdown+0xc5 [FastCopy\Shared\CommonSharedSettings.cpp @ 44] + 02 00000043`9a98f380 00007fff`a729d6af FastCopyShellExtension!FastCopyCleanupCallback+0x6d [FastCopy\FastCopyShellExtension\dllmain.cpp @ 68] + 03 00000043`9a98f4a0 00007fff`a729d146 FastCopyShellExtension!HostShutdownWatcher::OnTerminateProcess+0x6f [FastCopy\FastCopyShellExtension\HostShutdownWatcher.cpp @ 36] + 04 00000043`9a98f5e0 00007ff6`55c62085 FastCopyShellExtension!`anonymous namespace'::DetouredTerminateProcess+0x36 [FastCopy\FastCopyShellExtension\HostProcessHook.cpp @ 27] + 05 00000043`9a98f6f0 00007ff6`55c61290 DllHost!wWinMain+0x191 + 06 00000043`9a98f9b0 00007ff8`bec9259d DllHost!__scrt_common_main_seh+0x110 + 07 00000043`9a98f9f0 00007ff8`bf52af78 KERNEL32!BaseThreadInitThunk+0x1d + 08 00000043`9a98fa20 00000000`00000000 ntdll!RtlUserThreadStart+0x28 + 0:000> ~# + . 0 Id: 91c0.900c Suspend: 1 Teb: 00000043`9ab38000 Unfrozen + Start: DllHost!wWinMainCRTStartup (00007ff6`55c61310) + Priority: 0 Priority class: 32 Affinity: ffff + */ + s_ownerThreadId = ::GetCurrentThreadId(); // Get the thread ID of the host process +} + +void HostShutdownWatcher::OnCoUninitialize() +{ + if (s_ownerThreadId != 0 && ::GetCurrentThreadId() != s_ownerThreadId) + { + // Non-host thread: ignore this CoUninitialize + return; + } + State expected = State::Running; + s_state.compare_exchange_strong(expected, State::AfterCoUninit); +} + +void HostShutdownWatcher::OnDllCanUnloadNow() +{ + State expected = State::AfterCoUninit; + s_state.compare_exchange_strong(expected, State::AfterCanUnload); +} + +void HostShutdownWatcher::OnTerminateProcess(HANDLE hProcess, UINT uExitCode) +{ + DWORD pid = GetProcessId(hProcess); + if (pid == 0 || pid == GetCurrentProcessId()) + { + State state = s_state.load(std::memory_order_acquire); + if (state == State::AfterCoUninit) + { + if (s_callback) + { + __try { + s_callback(); + } + __except (EXCEPTION_EXECUTE_HANDLER) { + } + } + } + } +} diff --git a/FastCopyShellExtension/HostShutdownWatcher.h b/FastCopyShellExtension/HostShutdownWatcher.h new file mode 100644 index 0000000..edcbe5b --- /dev/null +++ b/FastCopyShellExtension/HostShutdownWatcher.h @@ -0,0 +1,25 @@ +#pragma once +#include +#include + +class HostShutdownWatcher +{ +public: + using CleanupCallback = void(*)(); + + static void Initialize(CleanupCallback cb); + static void OnCoUninitialize(); + static void OnDllCanUnloadNow(); + static void OnTerminateProcess(HANDLE hProcess, UINT uExitCode); + +private: + enum class State { + Running, + AfterCoUninit, + AfterCanUnload, + }; + + static std::atomic s_state; + static CleanupCallback s_callback; + static DWORD s_ownerThreadId; +}; diff --git a/FastCopyShellExtension/Recorder.cpp b/FastCopyShellExtension/Recorder.cpp index ced9e85..9bdc1ba 100644 --- a/FastCopyShellExtension/Recorder.cpp +++ b/FastCopyShellExtension/Recorder.cpp @@ -1,84 +1,174 @@ -#include "Recorder.h" + #include #include +#include +#include +#include + #include #include -#include + +#include "Recorder.h" #include "ShellItem.h" #include "Registry.h" +#include "DebugPrint.h" +#include "CommonSharedSettings.h" + #include #include -static auto GetLocalDataFolder() -{ - static std::filesystem::path ret = [] - { - wil::unique_cotaskmem_string localAppData; - SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, NULL, localAppData.put()); - return std::filesystem::path{ localAppData.get() } / LR"(packages\HO-COOH.RoboCopyEx\LocalCache\Local)"; - }(); - return ret; -} -static auto GetTimeString() +namespace { - //std::chrono::current_zone() gives exceptions on Windows 10, 17763 with MSVC cl.exe version 19.36.32535 - //auto ret = std::format(L"{}", std::chrono::zoned_time{ std::chrono::current_zone(), std::chrono::system_clock::now() }.get_local_time()); - auto ret = std::format(L"{}", std::chrono::system_clock::now()); - std::ranges::replace_if(ret, [](auto c) {return c == L'.' || c == L':'; }, L'-'); - return ret; + static std::wstring GetTimeString() + { + //std::chrono::current_zone() gives exceptions on Windows 10, 17763 with MSVC cl.exe version 19.36.32535 + //auto ret = std::format(L"{}", std::chrono::zoned_time{ std::chrono::current_zone(), std::chrono::system_clock::now() }.get_local_time()); + auto ret = std::format(L"{}", std::chrono::system_clock::now()); + std::ranges::replace_if(ret, [](auto c) {return c == L'.' || c == L':'; }, L'-'); + return ret; + } + + static wchar_t toFlag(CopyOperation op) + { + switch (op) + { + case CopyOperation::Copy: return L'C'; + case CopyOperation::Move: return L'M'; + case CopyOperation::Paste: return L'P'; + case CopyOperation::Delete: return L'D'; + default: break; + } + return L'?'; + } } Recorder::Recorder(CopyOperation op) { - std::filesystem::create_directories(GetLocalDataFolder()); - auto const filename = GetRecordFilePath(op).wstring(); - Registry::Record(filename); - m_fs = _wfopen(filename.data(), L"wb"); - if (!m_fs) - throw std::runtime_error{ "Cannot open file" }; + auto& s = FastCopy::Settings::CommonSharedSettings::Instance(); + auto const& folderOpt = s.GetLocalDataFolder(); + if (!folderOpt) + { + FC_LOG_ERROR(L"LocalDataFolder unavailable, skip recording."); + return; + } + + auto const& folder = *folderOpt; + + std::error_code ec; + std::filesystem::create_directories(folder, ec); + if (ec) + { + FC_LOG_WARN(L" create_directories('{}') failed, ec={}", + folder.wstring(), ec.value()); + } + + auto const filePath = Recorder::GetRecordFilePath(op); + if (filePath.empty()) + { + FC_LOG_WARN(L" GetRecordFilePath returned empty path, skip."); + return; + } + + Registry::Record(filePath.wstring()); + m_fs = _wfopen(filePath.c_str(), L"wb"); + if (!m_fs) + { + FC_LOG_ERROR(L" _wfopen('{}') failed.", filePath.wstring()); + return; // skip recording + } } Recorder& Recorder::operator<<(ShellItem& item) { - std::wstring buf {item.GetDisplayName()}; - std::transform(buf.begin(), buf.end(), buf.begin(), [](wchar_t c) { return c == L'\\' ? L'/' : c; }); - size_t const length = buf.size(); - fwrite(&length, sizeof(length), 1, m_fs); - fwrite(buf.data(), 2, length, m_fs); + // If recorder file is not available, skip recording. + // Getting LocalDataFolder can fail and result in an empty path + // , causing subsequent file opening operations to fail as well. + if (!m_fs) + return *this; - return *this; -} + std::wstring buf {item.GetDisplayName()}; + std::transform(buf.begin(), buf.end(), buf.begin(), [](wchar_t c) { return c == L'\\' ? L'/' : c; }); + size_t const length = buf.size(); + fwrite(&length, sizeof(length), 1, m_fs); + fwrite(buf.data(), 2, length, m_fs); -Recorder::~Recorder() -{ - if (m_fs) - fclose(m_fs); + return *this; } -static wchar_t toFlag(CopyOperation op) +Recorder::~Recorder() { - switch (op) - { - case CopyOperation::Copy: - return L'C'; - case CopyOperation::Move: - return L'M'; - case CopyOperation::Paste: - return L'P'; - case CopyOperation::Delete: - return L'D'; - } + if (m_fs) + fclose(m_fs); } std::filesystem::path Recorder::GetRecordFilePath(CopyOperation op) { - return GetLocalDataFolder() / std::format(L"{}{}.txt", toFlag(op), GetTimeString()); + auto& s = FastCopy::Settings::CommonSharedSettings::Instance(); + auto const& folderOpt = s.GetLocalDataFolder(); + if (!folderOpt) + { + FC_LOG_ERROR(L"LocalDataFolder unavailable, skip recording."); + return {}; + } + + auto const& folder = *folderOpt; + return folder / std::format(L"{}{}.txt", toFlag(op), GetTimeString()); } bool Recorder::HasRecord() { - return std::find_if(std::filesystem::directory_iterator{ GetLocalDataFolder() }, std::filesystem::directory_iterator{}, [](std::filesystem::directory_entry const& fileEntry) { - auto str = fileEntry.path().filename().wstring(); - return str.starts_with(L'C') || str.starts_with(L"M2") || str.starts_with(L'P'); - }) != std::filesystem::directory_iterator{}; + try + { + auto& s = FastCopy::Settings::CommonSharedSettings::Instance(); + auto const& folderOpt = s.GetLocalDataFolder(); + 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; + } + + std::error_code ec; + if (!std::filesystem::exists(folder, ec) || ec) + { + FC_LOG_WARN(L" folder '{}' not exists, ec={}", folder.wstring(), ec.value()); + return false; + } + + 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; + } + } + + if (ec) + { + FC_LOG_ERROR(L" iterate error, ec={}", ec.value()); + } + + return false; + } + catch (std::exception const& e) + { + FC_LOGA_ERROR("Recorder::HasRecord: exception: {}", e.what()); + } + catch (...) + { + FC_LOG_ERROR(L"Recorder::HasRecord: unknown exception."); + } + + return false; } diff --git a/FastCopyShellExtension/Recorder.h b/FastCopyShellExtension/Recorder.h index 0a560c2..9dececa 100644 --- a/FastCopyShellExtension/Recorder.h +++ b/FastCopyShellExtension/Recorder.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include #include #include "CopyOperation.h" @@ -12,7 +12,7 @@ class Recorder ~Recorder(); static bool HasRecord(); private: - FILE* m_fs; + FILE* m_fs = nullptr; /** * Return the file name of the record file diff --git a/FastCopyShellExtension/dllmain.cpp b/FastCopyShellExtension/dllmain.cpp index bfc6627..acc1f7a 100644 --- a/FastCopyShellExtension/dllmain.cpp +++ b/FastCopyShellExtension/dllmain.cpp @@ -1,4 +1,4 @@ -//// dllmain.cpp : Defines the entry point for the DLL application. +//// dllmain.cpp : Defines the entry point for the DLL application. #include #include #include @@ -6,6 +6,12 @@ #include "FastCopyRootCommand.h" #include "FastCopySubCommand.h" +#include "DebugHelper.hpp" +#include "DebugPrint.h" +#include "HostShutdownWatcher.h" +#include "HostProcessHook.h" +#include "CommonSharedSettings.h" + #pragma comment(lib,"runtimeobject") #pragma comment(lib, "Shlwapi.lib") @@ -13,6 +19,8 @@ CoCreatableClass(FastCopyRootCommand) CoCreatableClassWrlCreatorMapInclude(FastCopyRootCommand) +static HMODULE m_hCurrentModule = nullptr; + STDAPI DllGetActivationFactory(_In_ HSTRING activatableClassId, _COM_Outptr_ IActivationFactory** factory) { return Microsoft::WRL::Module::GetModule() @@ -21,14 +29,44 @@ STDAPI DllGetActivationFactory(_In_ HSTRING activatableClassId, _COM_Outptr_ IAc STDAPI DllCanUnloadNow() { - return Microsoft::WRL::Module::GetModule() - .GetObjectCount() == 0 ? S_OK : S_FALSE; + FC_LOG_DEBUG(L"DllCanUnloadNow"); + + // Tell the state machine: AfterCoUninit -> AfterCanUnload + HostShutdownWatcher::OnDllCanUnloadNow(); + + auto count = Microsoft::WRL::Module::GetModule().GetObjectCount(); + if (count == 0) { + FC_LOG_DEBUG(L"DllCanUnloadNow: count=0"); + return S_OK; + } + else { + FC_LOG_DEBUG(L"DllCanUnloadNow: count={}.", count); + return S_FALSE; + } } STDAPI DllGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _COM_Outptr_ void** instance) { - return Microsoft::WRL::Module::GetModule() + FC_LOG_DEBUG(L"DllGetClassObject: CLSID={}.", Win32FormatGuid(rclsid).c_str()); + auto hr = Microsoft::WRL::Module::GetModule() .GetClassObject(rclsid, riid, instance); + FC_LOG_DEBUG(L"DllGetClassObject: hr={}", hr); + return hr; +} + +static void __stdcall FastCopyCleanupCallback() +{ + auto& logger = FastCopyLogger::Instance(); + + logger.LogProcessInfo(L"FastCopyCleanupCallback (TerminateProcess path)"); + + // We need to turn off any logging operations first before proceeding. + logger.SetGlobalVerbosity(FastCopyLogger::Verbosity::Off); + logger.SetEnabled(false); + logger.SetBreakOnLog(false); + + // Shutdown Shared Settings, release resources + ::FastCopy::Settings::CommonSharedSettings::Instance().Shutdown(); } BOOL APIENTRY DllMain( HMODULE hModule, @@ -38,11 +76,81 @@ BOOL APIENTRY DllMain( HMODULE hModule, { switch (ul_reason_for_call) { - case DLL_PROCESS_ATTACH: - case DLL_THREAD_ATTACH: - case DLL_THREAD_DETACH: - case DLL_PROCESS_DETACH: - break; + case DLL_PROCESS_ATTACH: + { + DisableThreadLibraryCalls(hModule); + + m_hCurrentModule = hModule; + + // Initialize the state machine + HostShutdownWatcher::Initialize(&FastCopyCleanupCallback); + + // Install detours hook + HostProcessHook::Install(); + + auto& logger = FastCopyLogger::Instance(); + logger.LogProcessInfo(L"DLL_PROCESS_ATTACH"); + logger.LogDllPath(hModule, L"DLL_PROCESS_ATTACH"); + + DEBUG_WAIT_FOR_DEBUGGER(/* timeout= ms */60000); + } + break; + case DLL_PROCESS_DETACH: + { + // NOTE: + // Originally we suspected that dllhost.exe might skip CoFreeUnusedLibraries + // for unused COM modules, which would mean: + // - DllCanUnloadNow is never called, and + // - the process could end directly without going through DllMain. + // + // The "expected" graceful sequence was something like: + // CoUninitialize + // -> RtlDllShutdownInProgress + // -> DllCanUnloadNow arrives + // -> check COM ref counts == 0 + // -> DllCanUnloadNow returns S_OK + // -> FreeLibrary + // -> CallDllMain(DLL_PROCESS_DETACH) + // + // In practice this is NOT guaranteed. dllhost.exe can (and does) call + // TerminateProcess on itself after CoUninitialize, which bypasses + // DllMain(DLL_PROCESS_DETACH) entirely. This is consistent with the + // documentation: if the process is terminating (lpReserved != NULL), + // all other threads are already gone or have been terminated by + // ExitProcess/TerminateProcess, and DLL detach notifications are not + // guaranteed to run. + // + // To handle this reliably we now: + // - hook CoUninitialize and TerminateProcess via HostProcessHook + // - track the host state in HostShutdownWatcher + // - when CoUninitialize has been observed but DllCanUnloadNow has not + // been called afterwards, we invoke FastCopyCleanupCallback() from + // the TerminateProcess detour. + // + // FastCopyCleanupCallback() is responsible for shutting down our + // internal resources (e.g. CommonSharedSettings monitor thread, + // logging, etc.) on the "hard" TerminateProcess path. + // + // Therefore: + // - DLL_PROCESS_DETACH is only relied on for the normal FreeLibrary + // unload path; + // - the dllhost.exe TerminateProcess case is now handled explicitly + // by the hooks and does not depend on DllMain being called. + // + // For debugging you can still use the following breakpoints: + // > bp FastCopyShellExtension!DllCanUnloadNow + // > bp FastCopyShellExtension!DllMain + // > bp combase!CoUninitialize + // > bp ntdll!RtlDllShutdownInProgress + // > bp kernelbase!TerminateProcess + + // (No additional cleanup is required here; resource shutdown is handled + // either by normal object lifetime or by FastCopyCleanupCallback().) + } + break; + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + break; } return TRUE; } diff --git a/PackageDlls/boost_filesystem-vc143-mt-x64-1_82.dll b/PackageDlls/boost_filesystem-vc143-mt-x64-1_82.dll deleted file mode 100644 index d416c68..0000000 Binary files a/PackageDlls/boost_filesystem-vc143-mt-x64-1_82.dll and /dev/null differ diff --git a/PackageDlls/fmt.dll b/PackageDlls/fmt.dll deleted file mode 100644 index 6108d6d..0000000 Binary files a/PackageDlls/fmt.dll and /dev/null differ diff --git a/README.md b/README.md index a46a4b2..53143b2 100644 Binary files a/README.md and b/README.md differ diff --git a/RobocopyInjection/RobocopyInjection.vcxproj b/RobocopyInjection/RobocopyInjection.vcxproj index e17f621..49e7283 100644 --- a/RobocopyInjection/RobocopyInjection.vcxproj +++ b/RobocopyInjection/RobocopyInjection.vcxproj @@ -37,40 +37,40 @@ DynamicLibrary true - v145 + v143 Unicode DynamicLibrary false - v145 + v143 true Unicode DynamicLibrary false - v145 + v143 true Unicode DynamicLibrary true - v145 + v143 Unicode DynamicLibrary false - v145 + v143 true Unicode DynamicLibrary false - v145 + v143 true Unicode @@ -100,12 +100,24 @@ $(SolutionDir)$(Platform)\$(Configuration)\FastCopy + $(VC_IncludePath);$(WindowsSDK_IncludePath); $(SolutionDir)$(Platform)\$(Configuration)\FastCopy + $(VC_IncludePath);$(WindowsSDK_IncludePath); $(SolutionDir)$(Platform)\$(Configuration)\FastCopy + $(VC_IncludePath);$(WindowsSDK_IncludePath); + + + $(VC_IncludePath);$(WindowsSDK_IncludePath); + + + $(VC_IncludePath);$(WindowsSDK_IncludePath); + + + $(VC_IncludePath);$(WindowsSDK_IncludePath); diff --git a/Shared/CommonSharedSettings.cpp b/Shared/CommonSharedSettings.cpp new file mode 100644 index 0000000..71921bf --- /dev/null +++ b/Shared/CommonSharedSettings.cpp @@ -0,0 +1,344 @@ +#include "CommonSharedSettings.h" +#include "var_init_once.h" +#include "DebugHelper.hpp" + +#include +#include + +#include +#include +#include + +#include +#include + +namespace FastCopy::Settings +{ + using namespace std::literals; + + /* static */ + std::atomic CommonSharedSettings::s_manualShutdown{ false }; + + CommonSharedSettings& CommonSharedSettings::Instance() noexcept + { + STATIC_INIT_ONCE(CommonSharedSettings, s); + return *s; + } + + void CommonSharedSettings::Shutdown() + { + bool expected = false; + if (!s_manualShutdown.compare_exchange_strong(expected, + true, std::memory_order_relaxed)) + return; + + { + std::lock_guard lock(m_listenersMutex); + m_listeners.clear(); + } + + m_stopMonitor.store(true, std::memory_order_relaxed); + + HANDLE h = m_hChange; + if (h && h != INVALID_HANDLE_VALUE) + { + // Close the notification handle and + // wake up the WaitForSingleObject in the MonitorLoop + FindCloseChangeNotification(h); + m_hChange = nullptr; + } + + if (m_monitorThread.joinable()) + { + m_monitorThread.join(); + } + + // only work in debug mode + DEBUG_BREAK_HERE(); // make a breakpoint here to check if the destructor is called + } + + [[nodiscard]] + std::optional + CommonSharedSettings::BuildPackageSubPath(std::wstring_view subDir) + { + wil::unique_cotaskmem_string localAppData; + HRESULT const hr = SHGetKnownFolderPath( + FOLDERID_LocalAppData, + 0, + nullptr, + localAppData.put()); + if (FAILED(hr) || !localAppData) + { + return std::nullopt; + } + + std::array familyName{}; + UINT32 len = static_cast(familyName.size()); + LONG const rc = GetCurrentPackageFamilyName(&len, familyName.data()); + if (rc != ERROR_SUCCESS) + { + return std::nullopt; + } + + size_t const nameLen = + wcsnlen_s(familyName.data(), familyName.size()); + + std::filesystem::path result{ localAppData.get() }; + result /= L"Packages"; + result /= std::wstring_view{ familyName.data(), nameLen }; + result /= subDir; + + return result; + } + + CommonSharedSettings::CommonSharedSettings() + : m_localDataFolder( + BuildPackageSubPath(L"LocalCache\\Local")), + m_settingsFolder( + BuildPackageSubPath(L"Settings")) + { + if (m_settingsFolder && !m_settingsFolder->empty()) + { + std::filesystem::path ini = *m_settingsFolder; + ini /= L"settings.ini"; + m_settingsIniPath = std::move(ini); + + StartMonitorThread(); + } + } + + void CommonSharedSettings::StartMonitorThread() + { + if (!m_settingsIniPath || m_settingsIniPath->empty()) + return; + + auto dir = m_settingsIniPath->parent_path(); + if (dir.empty()) + return; + + m_hChange = FindFirstChangeNotificationW( + dir.c_str(), + FALSE, // Non-recursive + FILE_NOTIFY_CHANGE_LAST_WRITE | + FILE_NOTIFY_CHANGE_SIZE | + FILE_NOTIFY_CHANGE_FILE_NAME); + + if (!m_hChange || m_hChange == INVALID_HANDLE_VALUE) + { + m_hChange = nullptr; + return; + } + + m_monitorStarted.store(true, std::memory_order_relaxed); + + m_monitorThread = std::thread { + [this] + { + MonitorLoop(); + } + }; + } + + void CommonSharedSettings::MonitorLoop() + { + if (!m_hChange || m_hChange == INVALID_HANDLE_VALUE) + return; + + FILETIME lastWrite{}; + if (m_settingsIniPath && !m_settingsIniPath->empty()) + { + WIN32_FILE_ATTRIBUTE_DATA fad{}; + if (GetFileAttributesExW( + m_settingsIniPath->c_str(), + GetFileExInfoStandard, + &fad)) + { + lastWrite = fad.ftLastWriteTime; + } + } + + for (;;) + { + if (m_stopMonitor.load(std::memory_order_relaxed)) + break; + + DWORD dwWait = WaitForSingleObject(m_hChange, INFINITE); + if (dwWait != WAIT_OBJECT_0) + { + break; // Handle error or thread terminated + } + + if (m_settingsIniPath && !m_settingsIniPath->empty()) + { + WIN32_FILE_ATTRIBUTE_DATA cur{}; + if (GetFileAttributesExW( + m_settingsIniPath->c_str(), + GetFileExInfoStandard, + &cur)) + { + if (CompareFileTime(&cur.ftLastWriteTime, &lastWrite) != 0) + { + lastWrite = cur.ftLastWriteTime; + NotifyConfigChanged(); + } + } + } + + if (!FindNextChangeNotification(m_hChange)) + { + break; + } + } + + m_monitorStarted.store(false, std::memory_order_relaxed); + } + + void CommonSharedSettings::NotifyConfigChanged() + { + if (s_manualShutdown.load(std::memory_order_relaxed)) + return; + + std::vector listenersCopy; + { + std::lock_guard lock(m_listenersMutex); + listenersCopy = m_listeners; + } + + for (auto const& l : listenersCopy) + { + if (l.cb) + { + try + { + l.cb(l.ctx); + } + catch (...) + { + // ignore + } + } + } + } + + void CommonSharedSettings::RegisterChangeListener(ChangeCallback cb, void* context) + { + if (s_manualShutdown.load(std::memory_order_relaxed)) + return; + + if (!cb) + return; + + std::lock_guard lock(m_listenersMutex); + m_listeners.push_back(Listener{ cb, context }); + } + + void CommonSharedSettings::UnregisterChangeListener(ChangeCallback cb, void* context) + { + if (s_manualShutdown.load(std::memory_order_relaxed)) + return; + + std::lock_guard lock(m_listenersMutex); + + auto it = std::remove_if( + m_listeners.begin(), + m_listeners.end(), + [cb, context](Listener const& l) + { + return l.cb == cb && l.ctx == context; + }); + + m_listeners.erase(it, m_listeners.end()); + } + + std::optional CommonSharedSettings::GetString( + std::wstring_view section, + std::wstring_view key) const + { + if (!m_settingsIniPath || m_settingsIniPath->empty()) + return std::nullopt; + + std::lock_guard guard(m_ioMutex); + + wchar_t buf[256] = {}; + SetLastError(0); + + DWORD len = GetPrivateProfileStringW( + std::wstring(section).c_str(), + std::wstring(key).c_str(), + nullptr, + buf, + static_cast(std::size(buf)), + m_settingsIniPath->c_str()); + + DWORD err = GetLastError(); + if (err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND) + return std::nullopt; + + if (len == 0) + return std::nullopt; + + return std::wstring(buf, len); + } + + void CommonSharedSettings::SetString( + std::wstring_view section, + std::wstring_view key, + std::wstring_view value) + { + if (!m_settingsIniPath || m_settingsIniPath->empty()) + return; + + std::lock_guard guard(m_ioMutex); + + SetLastError(0); + WritePrivateProfileStringW( + std::wstring(section).c_str(), + std::wstring(key).c_str(), + std::wstring(value).c_str(), + m_settingsIniPath->c_str()); + } + + std::optional CommonSharedSettings::GetInt( + std::wstring_view section, + std::wstring_view key) const + { + auto s = GetString(section, key); + if (!s) + return std::nullopt; + + try + { + return std::stoi(*s); + } + catch (...) + { + return std::nullopt; + } + } + + void CommonSharedSettings::SetInt( + std::wstring_view section, + std::wstring_view key, + int value) + { + SetString(section, key, std::to_wstring(value)); + } + + std::optional CommonSharedSettings::GetBool( + std::wstring_view section, + std::wstring_view key) const + { + auto v = GetInt(section, key); + if (!v) + return std::nullopt; + return *v != 0; + } + + void CommonSharedSettings::SetBool( + std::wstring_view section, + std::wstring_view key, + bool value) + { + SetInt(section, key, value ? 1 : 0); + } +} diff --git a/Shared/CommonSharedSettings.h b/Shared/CommonSharedSettings.h new file mode 100644 index 0000000..0954c78 --- /dev/null +++ b/Shared/CommonSharedSettings.h @@ -0,0 +1,152 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace FastCopy::Settings +{ + class CommonSharedSettings + { + public: + CommonSharedSettings(const CommonSharedSettings&) = delete; + CommonSharedSettings& operator=(const CommonSharedSettings&) = delete; + CommonSharedSettings(CommonSharedSettings&&) = delete; + CommonSharedSettings& operator=(CommonSharedSettings&&) = delete; + + ~CommonSharedSettings() + { + // In non-DLL scenarios (regular EXE), ensure that background + // monitoring threads are properly stopped before the process exits. + // In DLL scenarios, if Shutdown is not explicitly called, + // the destructor might never run + // (for example,Host process may call TerminateProcess), + // so it also needs to cooperate with the DLL's cleanup callback. + Shutdown(); + } + + static CommonSharedSettings& Instance() noexcept; + + // Shutdown the background monitor thread and close the change notification handle. + // + // - EXE: the destructor already calls Shutdown(), so it is normally optional. + // You may still call it explicitly if you want deterministic cleanup. + // - DLL (e.g. shell extensions in dllhost.exe): the destructor might never run + // on the TerminateProcess path, so the host must call Shutdown() from an + // explicit cleanup point (e.g. FastCopyCleanupCallback in DllMain). + void Shutdown(); + + // Local data directory: %LocalAppData%\Packages\\LocalCache\Local + [[nodiscard]] + std::optional const& GetLocalDataFolder() const noexcept + { + return m_localDataFolder; + } + + // Settings Directory: %LocalAppData%\Packages\\Settings + [[nodiscard]] + std::optional const& GetSettingsFolder() const noexcept + { + return m_settingsFolder; + } + + // settings.ini full path:%LocalAppData%\Packages\\Settings\settings.ini + [[nodiscard]] + std::optional const& GetSettingsIniPath() const noexcept + { + return m_settingsIniPath; + } + + std::optional GetString( + std::wstring_view section, + std::wstring_view key) const; + + void SetString( + std::wstring_view section, + std::wstring_view key, + std::wstring_view value); + + std::optional GetInt( + std::wstring_view section, + std::wstring_view key) const; + + void SetInt( + std::wstring_view section, + std::wstring_view key, + int value); + + std::optional GetBool( + std::wstring_view section, + std::wstring_view key) const; + + void SetBool( + std::wstring_view section, + std::wstring_view key, + bool value); + + // --- Register Change Notification --- + // + // When settings.ini is modified (write time changes), + // all callbacks will be called sequentially in the background monitoring thread. + // + // Note: Callbacks are executed in a "background thread", + // should not throw exceptions, and should not perform UI operations. + + using ChangeCallback = void(*)(void* context); + + void RegisterChangeListener(ChangeCallback cb, void* context); + void UnregisterChangeListener(ChangeCallback cb, void* context); + + private: + CommonSharedSettings(); + + // MakePath: %LocalAppData%\Packages\\ + subDir + [[nodiscard]] + static std::optional + BuildPackageSubPath(std::wstring_view subDir); + + void StartMonitorThread(); + void MonitorLoop(); + void NotifyConfigChanged(); + + private: + std::optional m_localDataFolder; + std::optional m_settingsFolder; + std::optional m_settingsIniPath; + + // File Watch Handle + HANDLE m_hChange = nullptr; + std::atomic m_monitorStarted{ false }; + std::atomic m_stopMonitor{ false }; + static std::atomic s_manualShutdown; + std::thread m_monitorThread; + + // this is I/O mutex + mutable std::mutex m_ioMutex; + + // Change Callback + struct Listener + { + ChangeCallback cb{}; + void* ctx{}; + }; + + mutable std::mutex m_listenersMutex; + std::vector m_listeners; + }; + + // Get the current settings.ini path + inline std::filesystem::path GetFastCopyIniPath() + { + auto& instance = CommonSharedSettings::Instance(); + const auto& opt = instance.GetSettingsIniPath(); + return opt ? *opt : std::filesystem::path{}; + } +} diff --git a/Shared/DebugHelper.hpp b/Shared/DebugHelper.hpp new file mode 100644 index 0000000..f726ebc --- /dev/null +++ b/Shared/DebugHelper.hpp @@ -0,0 +1,109 @@ +#pragma once + +#include +#include + +namespace DebugHelper +{ + [[nodiscard]] constexpr bool is_debug_build() noexcept + { +#if defined(DEBUG) || defined(_DEBUG) || !defined(NDEBUG) + return true; +#else + return false; +#endif + } + + // Returns true if a debugger is currently attached. + [[nodiscard]] inline bool is_debugger_attached() noexcept + { + return ::IsDebuggerPresent() != FALSE; + } + + // Triggers a breakpoint. + inline void debug_break() noexcept + { +#if defined(_MSC_VER) + __debugbreak(); +#else + ::DebugBreak(); +#endif + } + + // Waits for a debugger to attach and breaks; returns false on timeout. + inline bool DebugBreakWaitForAttach( + std::chrono::milliseconds timeout = std::chrono::milliseconds{ -1 }, + std::chrono::milliseconds poll_interval = std::chrono::milliseconds{ 100 } + ) noexcept + { + const bool infinite_wait = (timeout.count() < 0); + const auto start = std::chrono::steady_clock::now(); + + long long poll_ms_ll = poll_interval.count(); + if (poll_ms_ll <= 0) + poll_ms_ll = 1; + const DWORD poll_ms = static_cast(poll_ms_ll); + + while (!is_debugger_attached()) + { + if (!infinite_wait) + { + const auto now = std::chrono::steady_clock::now(); + const auto elapsed = + std::chrono::duration_cast(now - start); + + if (elapsed >= timeout) + { + // Timeout: no debugger attached, continue execution. + return false; + } + } + + ::Sleep(poll_ms); + } + + // Debugger has attached; break here. + debug_break(); + return true; + } + + // In debug builds: break only if a debugger is attached. + inline void DebugBreakIfAttached() noexcept + { + if constexpr (is_debug_build()) + { + if (is_debugger_attached()) + debug_break(); + } + } + + // In debug builds: wait for debugger and break; in release, no-op. + inline bool SmartDebugBreakWaitForAttach( + std::chrono::milliseconds timeout = std::chrono::milliseconds{ -1 }, + std::chrono::milliseconds poll_interval = std::chrono::milliseconds{ 100 } + ) noexcept + { + if constexpr (is_debug_build()) + { + return DebugBreakWaitForAttach(timeout, poll_interval); + } + else + { + (void)timeout; + (void)poll_interval; + return false; + } + } + +} // namespace debug + +// Break on this line in debug build if a debugger is attached. +#define DEBUG_BREAK_HERE() \ + do { ::DebugHelper::DebugBreakIfAttached(); } while (false) + +// In debug build: wait up to timeout_ms for a debugger, then break here. +#define DEBUG_WAIT_FOR_DEBUGGER(timeout_ms) \ + do { \ + ::DebugHelper::SmartDebugBreakWaitForAttach( \ + std::chrono::milliseconds(static_cast(timeout_ms))); \ + } while (false) diff --git a/Shared/var_init_once.h b/Shared/var_init_once.h new file mode 100644 index 0000000..cba8b83 --- /dev/null +++ b/Shared/var_init_once.h @@ -0,0 +1,82 @@ +#pragma once + +#include // std::atexit +#include // std::once_flag, std::call_once +#include // placement new +#include // std::is_trivially_destructible_v +#include // HMODULE, GetProcAddress, GetModuleHandle, LoadLibraryEx, FreeLibrary + +// The following macros are used to initialize static variables once in a +// thread-safe manner while avoiding TLS, which is what MSVC uses for static +// variables. + +// Similar to: +// static T var_name(...); +#define STATIC_INIT_ONCE(T, var_name, ...) \ + T* var_name; \ + do { \ + static alignas(T) char static_init_once_storage_[sizeof(T)]; \ + static std::once_flag static_init_once_flag_; \ + std::call_once(static_init_once_flag_, []() { \ + new (static_init_once_storage_) T(__VA_ARGS__); \ + if constexpr (!std::is_trivially_destructible_v) { \ + std::atexit([]() { \ + reinterpret_cast(static_init_once_storage_)->~T(); \ + }); \ + } \ + }); \ + var_name = reinterpret_cast(static_init_once_storage_); \ + } while (0) + +// Similar to: +// static T var_name = initializer; +#define STATIC_INIT_ONCE_TRIVIAL(T, var_name, initializer) \ + static constinit T var_name; \ + do { \ + static_assert(std::is_trivially_destructible_v); \ + static std::once_flag static_init_once_flag_; \ + std::call_once(static_init_once_flag_, \ + []() { var_name = initializer; }); \ + } while (0) + +// Similar to: +// static T ptr = (T)GetProcAddress(GetModuleHandle(module_name), proc_name); +#define GET_PROC_ADDRESS_ONCE(T, ptr, module_name, proc_name) \ + static T ptr; \ + do { \ + static_assert(std::is_trivially_destructible_v); \ + static std::once_flag get_proc_address_once_flag_; \ + std::call_once(get_proc_address_once_flag_, []() { \ + HMODULE get_proc_address_once_module_ = \ + GetModuleHandle(module_name); \ + if (get_proc_address_once_module_) { \ + ptr = (T)GetProcAddress(get_proc_address_once_module_, \ + proc_name); \ + } \ + }); \ + } while (0) + +// Similar to: +// static T ptr = +// (T)GetProcAddress(LoadLibraryEx(module_name, nullptr, flags), proc_name); +#define LOAD_LIBRARY_GET_PROC_ADDRESS_ONCE(T, ptr, module_name, flags, \ + proc_name) \ + static T ptr; \ + do { \ + static_assert(std::is_trivially_destructible_v); \ + static std::once_flag get_proc_address_once_flag_; \ + std::call_once(get_proc_address_once_flag_, []() { \ + static HMODULE get_proc_address_once_module_ = \ + LoadLibraryEx(module_name, nullptr, flags); \ + if (get_proc_address_once_module_) { \ + ptr = (T)GetProcAddress(get_proc_address_once_module_, \ + proc_name); \ + if (!ptr) { \ + FreeLibrary(get_proc_address_once_module_); \ + } else { \ + std::atexit( \ + []() { FreeLibrary(get_proc_address_once_module_); }); \ + } \ + } \ + }); \ + } while (0) \ No newline at end of file diff --git a/Tools/AppxDllRules.txt b/Tools/AppxDllRules.txt new file mode 100644 index 0000000..7c2952a --- /dev/null +++ b/Tools/AppxDllRules.txt @@ -0,0 +1,32 @@ +# AppxDllRules.txt +# +# One pattern per line. +# - Matching is case-insensitive. +# - Patterns are applied to file names only (no directory path). +# - Optional "| soft" means: +# If the target file already exists and overwriting fails +# (for example, the file is in use), treat it as a soft error: +# log a warning but do NOT fail the whole build. +# +# To enable the same "soft on overwrite failure" behavior globally +# for all patterns, you can add ONE of the following directive lines: +# @SoftOnTargetLocked +# @soft_on_target_locked +# +# Lines starting with '#' or ';' are comments. + +# boost filesystem: must copy successfully (hard error on failure) +boost_filesystem-vc143-mt*-x64-1_86.dll + +# fmt family: fmt.dll / fmtd.dll (still hard error by default) +fmt*.dll + +# FastCopy shell extension: +# - "| soft" means: if the target DLL already exists and the overwrite fails, +# treat it as a soft error (warning only, build continues). +# use "| SoftOnTargetLocked", "| soft-on-target-locked" as well. +FastCopyShellExtension.dll | soft + +# Robocopy injection DLL: failure is a hard error (no "| soft"), maybe we need a sign? +RobocopyInjection.dll + diff --git a/Tools/CopyFilesSimple.bat b/Tools/CopyFilesSimple.bat new file mode 100644 index 0000000..f74888b --- /dev/null +++ b/Tools/CopyFilesSimple.bat @@ -0,0 +1,48 @@ +@echo off +setlocal + +REM Parameters: +REM %1 = Source DLL path (complete path including file name, directory name, and drive letter) +REM %2 = Target AppX directory (directory path) + +if "%~1"=="" ( + echo [CopyDllToAppx][ERROR] Source DLL path is not specified. + exit /b 1 +) + +if "%~2"=="" ( + echo [CopyDllToAppx][ERROR] Target AppX directory is not specified. + exit /b 1 +) + +set "SRC_DLL=%~1" +set "APPX_DIR=%~2" + +echo [CopyDllToAppx] SRC_DLL = "%SRC_DLL%" +echo [CopyDllToAppx] APPX_DIR = "%APPX_DIR%" + +REM Check whether the DLL exists. If not, report an error and terminate the build +if not exist "%SRC_DLL%" ( + echo [CopyDllToAppx][ERROR] "%SRC_DLL%" not found. + exit /b 1 +) + +REM Ensure that the target directory exists +if not exist "%APPX_DIR%" ( + mkdir "%APPX_DIR%" + if errorlevel 1 ( + echo [CopyDllToAppx][ERROR] Failed to create directory "%APPX_DIR%". + exit /b 1 + ) +) + +REM Copy DLL, terminate the build if failed +xcopy "%SRC_DLL%" "%APPX_DIR%\" /Y /I +if errorlevel 1 ( + echo [CopyDllToAppx][ERROR] Failed to copy "%SRC_DLL%" to "%APPX_DIR%". + exit /b 1 +) + +echo [CopyDllToAppx] Done. +endlocal +exit /b 0 \ No newline at end of file