// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. #include "precomp.h" #undef RemoveDirectory using namespace System; using namespace System::Collections::Generic; using namespace System::Runtime::InteropServices; using namespace Xunit; using namespace WixInternal::TestSupport; namespace DutilTests { const int PREWAIT = 20; const int POSTWAIT = 480; const int FULLWAIT = 500; const int SILENCEPERIOD = 100; struct RegKey { HRESULT hr; HKEY hkRoot; LPCWSTR wzSubKey; REG_KEY_BITNESS kbKeyBitness; BOOL fRecursive; }; struct Directory { HRESULT hr; LPCWSTR wzPath; BOOL fRecursive; }; struct Results { RegKey *rgRegKeys; DWORD cRegKeys; Directory *rgDirectories; DWORD cDirectories; }; public delegate void MonGeneralDelegate(HRESULT, LPVOID); public delegate void MonDriveStatusDelegate(WCHAR, BOOL, LPVOID); public delegate void MonDirectoryDelegate(HRESULT, LPCWSTR, BOOL, LPVOID, LPVOID); public delegate void MonRegKeyDelegate(HRESULT, HKEY, LPCWSTR, REG_KEY_BITNESS, BOOL, LPVOID, LPVOID); static void MonGeneral( __in HRESULT /*hrResult*/, __in_opt LPVOID /*pvContext*/ ) { Assert::True(false); } static void MonDriveStatus( __in WCHAR /*chDrive*/, __in BOOL /*fArriving*/, __in_opt LPVOID /*pvContext*/ ) { } static void MonDirectory( __in HRESULT hrResult, __in_z LPCWSTR wzPath, __in_z BOOL fRecursive, __in_opt LPVOID pvContext, __in_opt LPVOID pvDirectoryContext ) { Assert::Equal(S_OK, hrResult); Assert::Equal(0, reinterpret_cast(pvDirectoryContext)); HRESULT hr = S_OK; Results *pResults = reinterpret_cast(pvContext); hr = MemEnsureArraySize(reinterpret_cast(&pResults->rgDirectories), pResults->cDirectories + 1, sizeof(Directory), 5); NativeAssert::ValidReturnCode(hr, S_OK); ++pResults->cDirectories; pResults->rgDirectories[pResults->cDirectories - 1].hr = hrResult; pResults->rgDirectories[pResults->cDirectories - 1].wzPath = wzPath; pResults->rgDirectories[pResults->cDirectories - 1].fRecursive = fRecursive; } static void MonRegKey( __in HRESULT hrResult, __in HKEY hkRoot, __in_z LPCWSTR wzSubKey, __in REG_KEY_BITNESS kbKeyBitness, __in_z BOOL fRecursive, __in_opt LPVOID pvContext, __in_opt LPVOID pvRegKeyContext ) { Assert::Equal(S_OK, hrResult); Assert::Equal(0, reinterpret_cast(pvRegKeyContext)); HRESULT hr = S_OK; Results *pResults = reinterpret_cast(pvContext); hr = MemEnsureArraySize(reinterpret_cast(&pResults->rgRegKeys), pResults->cRegKeys + 1, sizeof(RegKey), 5); NativeAssert::ValidReturnCode(hr, S_OK); ++pResults->cRegKeys; pResults->rgRegKeys[pResults->cRegKeys - 1].hr = hrResult; pResults->rgRegKeys[pResults->cRegKeys - 1].hkRoot = hkRoot; pResults->rgRegKeys[pResults->cRegKeys - 1].wzSubKey = wzSubKey; pResults->rgRegKeys[pResults->cRegKeys - 1].kbKeyBitness = kbKeyBitness; pResults->rgRegKeys[pResults->cRegKeys - 1].fRecursive = fRecursive; } public ref class MonUtil { public: void ClearResults(Results *pResults) { ReleaseNullMem(pResults->rgDirectories); pResults->cDirectories = 0; ReleaseNullMem(pResults->rgRegKeys); pResults->cRegKeys = 0; } void RemoveDirectory(LPCWSTR wzPath) { DWORD dwRetryCount = 0; const DWORD c_dwMaxRetryCount = 100; const DWORD c_dwRetryInterval = 50; HRESULT hr = DirEnsureDelete(wzPath, TRUE, TRUE); // Monitoring a directory opens a handle to that directory, which means delete requests for that directory will succeed // (and deletion will be "pending" until our monitor handle is closed) // but deletion of the directory containing that directory cannot complete until the handle is closed. This means DirEnsureDelete() // can sometimes encounter HRESULT_FROM_WIN32(ERROR_DIR_NOT_EMPTY) failures, which just means it needs to retry a bit later // (after the waiter thread wakes up, it will release the handle) while (HRESULT_FROM_WIN32(ERROR_DIR_NOT_EMPTY) == hr && c_dwMaxRetryCount > dwRetryCount) { ::Sleep(c_dwRetryInterval); ++dwRetryCount; hr = DirEnsureDelete(wzPath, TRUE, TRUE); } NativeAssert::ValidReturnCode(hr, S_OK, S_FALSE, E_PATHNOTFOUND); } void TestDirectory(MON_HANDLE handle, Results *pResults) { HRESULT hr = S_OK; LPWSTR sczShallowPath = NULL; LPWSTR sczParentPath = NULL; LPWSTR sczDeepPath = NULL; LPWSTR sczChildPath = NULL; LPWSTR sczChildFilePath = NULL; try { hr = PathExpand(&sczShallowPath, L"%TEMP%\\MonUtilTest\\", PATH_EXPAND_ENVIRONMENT); NativeAssert::ValidReturnCode(hr, S_OK); hr = PathExpand(&sczParentPath, L"%TEMP%\\MonUtilTest\\sub\\folder\\that\\might\\not\\", PATH_EXPAND_ENVIRONMENT); NativeAssert::ValidReturnCode(hr, S_OK); hr = PathExpand(&sczDeepPath, L"%TEMP%\\MonUtilTest\\sub\\folder\\that\\might\\not\\exist\\", PATH_EXPAND_ENVIRONMENT); NativeAssert::ValidReturnCode(hr, S_OK); hr = PathExpand(&sczChildPath, L"%TEMP%\\MonUtilTest\\sub\\folder\\that\\might\\not\\exist\\some\\sub\\folder\\", PATH_EXPAND_ENVIRONMENT); NativeAssert::ValidReturnCode(hr, S_OK); hr = PathExpand(&sczChildFilePath, L"%TEMP%\\MonUtilTest\\sub\\folder\\that\\might\\not\\exist\\some\\sub\\folder\\file.txt", PATH_EXPAND_ENVIRONMENT); NativeAssert::ValidReturnCode(hr, S_OK); RemoveDirectory(sczShallowPath); hr = MonAddDirectory(handle, sczDeepPath, TRUE, SILENCEPERIOD, NULL); NativeAssert::ValidReturnCode(hr, S_OK); hr = DirEnsureExists(sczParentPath, NULL); NativeAssert::ValidReturnCode(hr, S_OK, S_FALSE); // Make sure creating the parent directory does nothing, even after silence period ::Sleep(FULLWAIT); Assert::Equal(0, pResults->cDirectories); // Now create the target path, no notification until after the silence period hr = DirEnsureExists(sczDeepPath, NULL); NativeAssert::ValidReturnCode(hr, S_OK, S_FALSE); ::Sleep(PREWAIT); Assert::Equal(0, pResults->cDirectories); // Now after the full silence period, it should have triggered ::Sleep(POSTWAIT); Assert::Equal(1, pResults->cDirectories); NativeAssert::ValidReturnCode(pResults->rgDirectories[0].hr, S_OK); // Now delete the directory, along with a ton of parents. This verifies MonUtil will keep watching the closest parent that still exists. RemoveDirectory(sczShallowPath); ::Sleep(FULLWAIT); Assert::Equal(2, pResults->cDirectories); NativeAssert::ValidReturnCode(pResults->rgDirectories[1].hr, S_OK); // Create the parent directory again, still should be nothing even after full silence period hr = DirEnsureExists(sczParentPath, NULL); NativeAssert::ValidReturnCode(hr, S_OK, S_FALSE); ::Sleep(FULLWAIT); Assert::Equal(2, pResults->cDirectories); hr = DirEnsureExists(sczChildPath, NULL); NativeAssert::ValidReturnCode(hr, S_OK, S_FALSE); ::Sleep(PREWAIT); Assert::Equal(2, pResults->cDirectories); ::Sleep(POSTWAIT); Assert::Equal(3, pResults->cDirectories); NativeAssert::ValidReturnCode(pResults->rgDirectories[2].hr, S_OK); // Write a file to a deep child subfolder, and make sure it's detected hr = FileFromString(sczChildFilePath, 0, L"contents", FILE_ENCODING_UTF16_WITH_BOM); NativeAssert::ValidReturnCode(hr, S_OK); ::Sleep(PREWAIT); Assert::Equal(3, pResults->cDirectories); ::Sleep(POSTWAIT); Assert::Equal(4, pResults->cDirectories); NativeAssert::ValidReturnCode(pResults->rgDirectories[2].hr, S_OK); RemoveDirectory(sczParentPath); ::Sleep(FULLWAIT); Assert::Equal(5, pResults->cDirectories); NativeAssert::ValidReturnCode(pResults->rgDirectories[3].hr, S_OK); // Now remove the directory from the list of things to monitor, and confirm changes are no longer tracked hr = MonRemoveDirectory(handle, sczDeepPath, TRUE); NativeAssert::ValidReturnCode(hr, S_OK); ::Sleep(PREWAIT); hr = DirEnsureExists(sczDeepPath, NULL); NativeAssert::ValidReturnCode(hr, S_OK, S_FALSE); ::Sleep(FULLWAIT); Assert::Equal(5, pResults->cDirectories); NativeAssert::ValidReturnCode(pResults->rgDirectories[3].hr, S_OK); // Finally, add it back so we can test multiple things to monitor at once hr = MonAddDirectory(handle, sczDeepPath, TRUE, SILENCEPERIOD, NULL); NativeAssert::ValidReturnCode(hr, S_OK); } finally { ReleaseStr(sczShallowPath); ReleaseStr(sczDeepPath); ReleaseStr(sczParentPath); } } void TestRegKey(MON_HANDLE handle, Results *pResults) { HRESULT hr = S_OK; LPCWSTR wzShallowRegKey = L"Software\\MonUtilTest\\"; LPCWSTR wzParentRegKey = L"Software\\MonUtilTest\\sub\\folder\\that\\might\\not\\"; LPCWSTR wzDeepRegKey = L"Software\\MonUtilTest\\sub\\folder\\that\\might\\not\\exist\\"; LPCWSTR wzChildRegKey = L"Software\\MonUtilTest\\sub\\folder\\that\\might\\not\\exist\\some\\sub\\folder\\"; HKEY hk = NULL; try { hr = RegDelete(HKEY_CURRENT_USER, wzShallowRegKey, REG_KEY_32BIT, TRUE); NativeAssert::ValidReturnCode(hr, S_OK, S_FALSE, E_PATHNOTFOUND); hr = MonAddRegKey(handle, HKEY_CURRENT_USER, wzDeepRegKey, REG_KEY_DEFAULT, TRUE, SILENCEPERIOD, NULL); NativeAssert::ValidReturnCode(hr, S_OK); hr = RegCreate(HKEY_CURRENT_USER, wzParentRegKey, KEY_SET_VALUE | KEY_QUERY_VALUE | KEY_WOW64_32KEY, &hk); ReleaseRegKey(hk); // Make sure creating the parent key does nothing, even after silence period ::Sleep(FULLWAIT); NativeAssert::ValidReturnCode(hr, S_OK, S_FALSE); Assert::Equal(0, pResults->cRegKeys); // Now create the target path, no notification until after the silence period hr = RegCreate(HKEY_CURRENT_USER, wzDeepRegKey, KEY_SET_VALUE | KEY_QUERY_VALUE | KEY_WOW64_32KEY, &hk); NativeAssert::ValidReturnCode(hr, S_OK, S_FALSE); ReleaseRegKey(hk); ::Sleep(PREWAIT); Assert::Equal(0, pResults->cRegKeys); // Now after the full silence period, it should have triggered ::Sleep(POSTWAIT); Assert::Equal(1, pResults->cRegKeys); NativeAssert::ValidReturnCode(pResults->rgRegKeys[0].hr, S_OK); // Now delete the directory, along with a ton of parents. This verifies MonUtil will keep watching the closest parent that still exists. hr = RegDelete(HKEY_CURRENT_USER, wzShallowRegKey, REG_KEY_32BIT, TRUE); NativeAssert::ValidReturnCode(hr, S_OK, S_FALSE, E_PATHNOTFOUND); ::Sleep(PREWAIT); Assert::Equal(1, pResults->cRegKeys); ::Sleep(FULLWAIT); Assert::Equal(2, pResults->cRegKeys); NativeAssert::ValidReturnCode(pResults->rgRegKeys[1].hr, S_OK); // Create the parent directory again, still should be nothing even after full silence period hr = RegCreate(HKEY_CURRENT_USER, wzParentRegKey, KEY_SET_VALUE | KEY_QUERY_VALUE | KEY_WOW64_32KEY, &hk); NativeAssert::ValidReturnCode(hr, S_OK, S_FALSE); ReleaseRegKey(hk); ::Sleep(FULLWAIT); Assert::Equal(2, pResults->cRegKeys); hr = RegCreate(HKEY_CURRENT_USER, wzChildRegKey, KEY_SET_VALUE | KEY_QUERY_VALUE | KEY_WOW64_32KEY, &hk); NativeAssert::ValidReturnCode(hr, S_OK, S_FALSE); ::Sleep(PREWAIT); Assert::Equal(2, pResults->cRegKeys); ::Sleep(FULLWAIT); Assert::Equal(3, pResults->cRegKeys); NativeAssert::ValidReturnCode(pResults->rgRegKeys[2].hr, S_OK); // Write a registry value to some deep child subkey, and make sure it's detected hr = RegWriteString(hk, L"valuename", L"testvalue"); NativeAssert::ValidReturnCode(hr, S_OK); ReleaseRegKey(hk); ::Sleep(PREWAIT); Assert::Equal(3, pResults->cRegKeys); ::Sleep(FULLWAIT); Assert::Equal(4, pResults->cRegKeys); NativeAssert::ValidReturnCode(pResults->rgRegKeys[2].hr, S_OK); hr = RegDelete(HKEY_CURRENT_USER, wzDeepRegKey, REG_KEY_32BIT, TRUE); NativeAssert::ValidReturnCode(hr, S_OK); ::Sleep(FULLWAIT); Assert::Equal(5, pResults->cRegKeys); // Now remove the regkey from the list of things to monitor, and confirm changes are no longer tracked hr = MonRemoveRegKey(handle, HKEY_CURRENT_USER, wzDeepRegKey, REG_KEY_DEFAULT, TRUE); NativeAssert::ValidReturnCode(hr, S_OK); hr = RegCreate(HKEY_CURRENT_USER, wzDeepRegKey, KEY_SET_VALUE | KEY_QUERY_VALUE | KEY_WOW64_32KEY, &hk); NativeAssert::ValidReturnCode(hr, S_OK, S_FALSE); ReleaseRegKey(hk); ::Sleep(FULLWAIT); Assert::Equal(5, pResults->cRegKeys); } finally { ReleaseRegKey(hk); } } void TestMoreThan64(MON_HANDLE handle, Results *pResults) { HRESULT hr = S_OK; LPWSTR sczBaseDir = NULL; LPWSTR sczDir = NULL; LPWSTR sczFile = NULL; try { hr = PathExpand(&sczBaseDir, L"%TEMP%\\ScalabilityTest\\", PATH_EXPAND_ENVIRONMENT); NativeAssert::ValidReturnCode(hr, S_OK); for (DWORD i = 0; i < 200; ++i) { hr = StrAllocFormatted(&sczDir, L"%ls%u\\", sczBaseDir, i); NativeAssert::ValidReturnCode(hr, S_OK); hr = DirEnsureExists(sczDir, NULL); NativeAssert::ValidReturnCode(hr, S_OK, S_FALSE); hr = MonAddDirectory(handle, sczDir, FALSE, SILENCEPERIOD, NULL); NativeAssert::ValidReturnCode(hr, S_OK); } hr = PathConcat(sczDir, L"file.txt", &sczFile); NativeAssert::ValidReturnCode(hr, S_OK); hr = FileFromString(sczFile, 0, L"contents", FILE_ENCODING_UTF16_WITH_BOM); NativeAssert::ValidReturnCode(hr, S_OK); ::Sleep(FULLWAIT); Assert::Equal(1, pResults->cDirectories); for (DWORD i = 0; i < 199; ++i) { hr = StrAllocFormatted(&sczDir, L"%ls%u\\", sczBaseDir, i); NativeAssert::ValidReturnCode(hr, S_OK); hr = MonRemoveDirectory(handle, sczDir, FALSE); NativeAssert::ValidReturnCode(hr, S_OK); } ::Sleep(FULLWAIT); hr = FileFromString(sczFile, 0, L"contents2", FILE_ENCODING_UTF16_WITH_BOM); NativeAssert::ValidReturnCode(hr, S_OK); ::Sleep(FULLWAIT); Assert::Equal(2, pResults->cDirectories); for (DWORD i = 0; i < 199; ++i) { hr = StrAllocFormatted(&sczDir, L"%ls%u\\", sczBaseDir, i); NativeAssert::ValidReturnCode(hr, S_OK); hr = MonAddDirectory(handle, sczDir, FALSE, SILENCEPERIOD, NULL); NativeAssert::ValidReturnCode(hr, S_OK); } ::Sleep(FULLWAIT); hr = FileFromString(sczFile, 0, L"contents3", FILE_ENCODING_UTF16_WITH_BOM); NativeAssert::ValidReturnCode(hr, S_OK); ::Sleep(FULLWAIT); Assert::Equal(3, pResults->cDirectories); } finally { ReleaseStr(sczBaseDir); ReleaseStr(sczDir); ReleaseStr(sczFile); } } [Fact(Skip = "Test demonstrates failure")] void MonUtilTest() { HRESULT hr = S_OK; MON_HANDLE handle = NULL; List^ gcHandles = gcnew List(); Results *pResults = (Results *)MemAlloc(sizeof(Results), TRUE); Assert::True(NULL != pResults); try { // These ensure the function pointers we send point to this thread's appdomain, which helps with assembly binding when running tests within msbuild MonGeneralDelegate^ fpMonGeneral = gcnew MonGeneralDelegate(MonGeneral); GCHandle gchMonGeneral = GCHandle::Alloc(fpMonGeneral); gcHandles->Add(gchMonGeneral); IntPtr ipMonGeneral = Marshal::GetFunctionPointerForDelegate(fpMonGeneral); MonDriveStatusDelegate^ fpMonDriveStatus = gcnew MonDriveStatusDelegate(MonDriveStatus); GCHandle gchMonDriveStatus = GCHandle::Alloc(fpMonDriveStatus); gcHandles->Add(gchMonDriveStatus); IntPtr ipMonDriveStatus = Marshal::GetFunctionPointerForDelegate(fpMonDriveStatus); MonDirectoryDelegate^ fpMonDirectory = gcnew MonDirectoryDelegate(MonDirectory); GCHandle gchMonDirectory = GCHandle::Alloc(fpMonDirectory); gcHandles->Add(gchMonDirectory); IntPtr ipMonDirectory = Marshal::GetFunctionPointerForDelegate(fpMonDirectory); MonRegKeyDelegate^ fpMonRegKey = gcnew MonRegKeyDelegate(MonRegKey); GCHandle gchMonRegKey = GCHandle::Alloc(fpMonRegKey); gcHandles->Add(gchMonRegKey); IntPtr ipMonRegKey = Marshal::GetFunctionPointerForDelegate(fpMonRegKey); // "Silence period" is 100 ms hr = MonCreate(&handle, static_cast(ipMonGeneral.ToPointer()), static_cast(ipMonDriveStatus.ToPointer()), static_cast(ipMonDirectory.ToPointer()), static_cast(ipMonRegKey.ToPointer()), pResults); NativeAssert::ValidReturnCode(hr, S_OK); hr = RegInitialize(); NativeAssert::ValidReturnCode(hr, S_OK); TestDirectory(handle, pResults); ClearResults(pResults); TestRegKey(handle, pResults); ClearResults(pResults); TestMoreThan64(handle, pResults); ClearResults(pResults); } finally { ReleaseMon(handle); for each (GCHandle gcHandle in gcHandles) { gcHandle.Free(); } ReleaseMem(pResults->rgDirectories); ReleaseMem(pResults->rgRegKeys); ReleaseMem(pResults); RegUninitialize(); } } }; }