// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. #include "stdafx.h" HOSTFXR_UTILITY::HOSTFXR_UTILITY() { } HOSTFXR_UTILITY::~HOSTFXR_UTILITY() { } // // Runs a standalone appliction. // The folder structure looks like this: // Application/ // hostfxr.dll // Application.exe // Application.dll // etc. // We get the full path to hostfxr.dll and Application.dll and run hostfxr_main, // passing in Application.dll. // Assuming we don't need Application.exe as the dll is the actual application. // HRESULT HOSTFXR_UTILITY::GetStandaloneHostfxrParameters( PCWSTR pwzExeAbsolutePath, // includes .exe file extension. PCWSTR pcwzApplicationPhysicalPath, PCWSTR pcwzArguments, HANDLE hEventLog, _Inout_ STRU* pStruHostFxrDllLocation, _Out_ DWORD* pdwArgCount, _Out_ BSTR** ppwzArgv ) { HRESULT hr = S_OK; STRU struDllPath; STRU struArguments; STRU struHostFxrPath; STRU struRuntimeConfigLocation; DWORD dwPosition; // Obtain the app name from the processPath section. if (FAILED(hr = struDllPath.Copy(pwzExeAbsolutePath))) { goto Finished; } dwPosition = struDllPath.LastIndexOf(L'.', 0); if (dwPosition == -1) { hr = E_FAIL; goto Finished; } hr = UTILITY::ConvertPathToFullPath(L".\\hostfxr.dll", pcwzApplicationPhysicalPath, &struHostFxrPath); if (FAILED(hr)) { goto Finished; } struDllPath.QueryStr()[dwPosition] = L'\0'; if (FAILED(hr = struDllPath.SyncWithBuffer())) { goto Finished; } if (!UTILITY::CheckIfFileExists(struHostFxrPath.QueryStr())) { // Most likely a full framework app. // Check that the runtime config file doesn't exist in the folder as another heuristic. if (FAILED(hr = struRuntimeConfigLocation.Copy(struDllPath)) || FAILED(hr = struRuntimeConfigLocation.Append(L".runtimeconfig.json"))) { goto Finished; } if (!UTILITY::CheckIfFileExists(struRuntimeConfigLocation.QueryStr())) { hr = E_APPLICATION_ACTIVATION_EXEC_FAILURE; UTILITY::LogEventF(hEventLog, EVENTLOG_ERROR_TYPE, ASPNETCORE_EVENT_INPROCESS_FULL_FRAMEWORK_APP, ASPNETCORE_EVENT_INPROCESS_FULL_FRAMEWORK_APP_MSG, pcwzApplicationPhysicalPath, hr); } else { // If a runtime config file does exist, report a file not found on the app.exe hr = HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); UTILITY::LogEventF(hEventLog, EVENTLOG_ERROR_TYPE, ASPNETCORE_EVENT_APPLICATION_EXE_NOT_FOUND, ASPNETCORE_EVENT_APPLICATION_EXE_NOT_FOUND_MSG, pcwzApplicationPhysicalPath, hr); } goto Finished; } if (FAILED(hr = pStruHostFxrDllLocation->Copy(struHostFxrPath))) { goto Finished; } if (FAILED(hr = struDllPath.Append(L".dll"))) { goto Finished; } if (!UTILITY::CheckIfFileExists(struDllPath.QueryStr())) { // Treat access issue as File not found hr = HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); goto Finished; } if (FAILED(hr = struArguments.Copy(struDllPath)) || FAILED(hr = struArguments.Append(L" ")) || FAILED(hr = struArguments.Append(pcwzArguments))) { goto Finished; } if (FAILED(hr = ParseHostfxrArguments( struArguments.QueryStr(), pwzExeAbsolutePath, pcwzApplicationPhysicalPath, hEventLog, pdwArgCount, ppwzArgv))) { goto Finished; } Finished: return hr; } BOOL HOSTFXR_UTILITY::IsDotnetExecutable(STRU *struExecutablePath) { if (struExecutablePath == NULL) { return FALSE; } return struExecutablePath->EndsWith(L"dotnet.exe") || struExecutablePath->EndsWith(L"dotnet"); } HRESULT HOSTFXR_UTILITY::GetHostFxrParameters( _In_ HANDLE hEventLog, _In_ PCWSTR pcwzProcessPath, _In_ PCWSTR pcwzApplicationPhysicalPath, _In_ PCWSTR pcwzArguments, _Inout_ STRU *pStruHostFxrDllLocation, _Inout_ STRU *pStruExeAbsolutePath, _Out_ DWORD *pdwArgCount, _Out_ BSTR **pbstrArgv ) { HRESULT hr = S_OK; STRU struSystemPathVariable; STRU struAbsolutePathToHostFxr; STRU struAbsolutePathToDotnet; STRU struEventMsg; STACK_STRU(struExpandedProcessPath, MAX_PATH); STACK_STRU(struExpandedArguments, MAX_PATH); // Copy and Expand the processPath and Arguments. if (FAILED(hr = struExpandedProcessPath.CopyAndExpandEnvironmentStrings(pcwzProcessPath)) || FAILED(hr = struExpandedArguments.CopyAndExpandEnvironmentStrings(pcwzArguments))) { goto Finished; } // Convert the process path an absolute path to our current application directory. // If the path is already an absolute path, it will be unchanged. hr = UTILITY::ConvertPathToFullPath( struExpandedProcessPath.QueryStr(), pcwzApplicationPhysicalPath, &struAbsolutePathToDotnet ); if (FAILED(hr)) { goto Finished; } // Check if the absolute path is to dotnet or not. if (HOSTFXR_UTILITY::IsDotnetExecutable(&struAbsolutePathToDotnet)) { // // The processPath ends with dotnet.exe or dotnet // like: C:\Program Files\dotnet\dotnet.exe, C:\Program Files\dotnet\dotnet, dotnet.exe, or dotnet. // Get the absolute path to dotnet. If the path is already an absolute path, it will return that path // if (FAILED(hr = HOSTFXR_UTILITY::GetAbsolutePathToDotnet(&struAbsolutePathToDotnet))) // Make sure to append the dotnet.exe path correctly here (pass in regular path)? { goto Finished; } if (FAILED(hr = GetAbsolutePathToHostFxr(&struAbsolutePathToDotnet, hEventLog, &struAbsolutePathToHostFxr))) { goto Finished; } if (FAILED(hr = HOSTFXR_UTILITY::ParseHostfxrArguments( struExpandedArguments.QueryStr(), struAbsolutePathToDotnet.QueryStr(), pcwzApplicationPhysicalPath, hEventLog, pdwArgCount, pbstrArgv))) { goto Finished; } if (FAILED(hr = pStruHostFxrDllLocation->Copy(struAbsolutePathToHostFxr))) { goto Finished; } if (FAILED(hr = pStruExeAbsolutePath->Copy(struAbsolutePathToDotnet))) { goto Finished; } } else { // // The processPath is a path to the application executable // like: C:\test\MyApp.Exe or MyApp.Exe // Check if the file exists, and if it does, get the parameters for a standalone application // if (UTILITY::CheckIfFileExists(struAbsolutePathToDotnet.QueryStr())) { hr = HOSTFXR_UTILITY::GetStandaloneHostfxrParameters( struAbsolutePathToDotnet.QueryStr(), pcwzApplicationPhysicalPath, struExpandedArguments.QueryStr(), hEventLog, pStruHostFxrDllLocation, pdwArgCount, pbstrArgv); if (FAILED(hr)) { goto Finished; } if (FAILED(hr = pStruExeAbsolutePath->Copy(struAbsolutePathToDotnet))) { goto Finished; } } else { // // If the processPath file does not exist and it doesn't include dotnet.exe or dotnet // then it is an invalid argument. // hr = HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND);; UTILITY::LogEventF(hEventLog, EVENTLOG_ERROR_TYPE, ASPNETCORE_EVENT_GENERAL_ERROR_MSG, ASPNETCORE_EVENT_INVALID_PROCESS_PATH_MSG, struExpandedProcessPath.QueryStr(), hr); } } Finished: return hr; } // // Forms the argument list in HOSTFXR_PARAMETERS. // Sets the ArgCount and Arguments. // Arg structure: // argv[0] = Path to exe activating hostfxr. // argv[1] = L"exec" // argv[2] = absolute path to dll. // HRESULT HOSTFXR_UTILITY::ParseHostfxrArguments( PCWSTR pwzArgumentsFromConfig, PCWSTR pwzExePath, PCWSTR pcwzApplicationPhysicalPath, HANDLE hEventLog, _Out_ DWORD* pdwArgCount, _Out_ BSTR** pbstrArgv ) { UNREFERENCED_PARAMETER(hEventLog); // TODO use event log to set errors. DBG_ASSERT(dwArgCount != NULL); DBG_ASSERT(pwzArgv != NULL); DBG_ASSERT(pwzExePath != NULL); HRESULT hr = S_OK; INT argc = 0; BSTR* argv = NULL; LPWSTR* pwzArgs = NULL; STRU struTempPath; INT intArgsProcessed = 0; // If we call CommandLineToArgvW with an empty string, argc is 5 for some interesting reason. // Protectively guard against this by check if the string is null or empty. if (pwzArgumentsFromConfig == NULL || wcscmp(pwzArgumentsFromConfig, L"") == 0) { hr = E_INVALIDARG; goto Finished; } pwzArgs = CommandLineToArgvW(pwzArgumentsFromConfig, &argc); if (pwzArgs == NULL) { hr = HRESULT_FROM_WIN32(GetLastError()); goto Failure; } argv = new BSTR[argc + 1]; argv[0] = SysAllocString(pwzExePath); if (argv[0] == NULL) { hr = E_OUTOFMEMORY; goto Failure; } // Try to convert the application dll from a relative to an absolute path // Don't record this failure as pwzArgs[0] may already be an absolute path to the dll. for (intArgsProcessed = 0; intArgsProcessed < argc; intArgsProcessed++) { DBG_ASSERT(pwzArgs[intArgsProcessed] != NULL); struTempPath.Copy(pwzArgs[intArgsProcessed]); if (struTempPath.EndsWith(L".dll")) { if (SUCCEEDED(UTILITY::ConvertPathToFullPath(pwzArgs[intArgsProcessed], pcwzApplicationPhysicalPath, &struTempPath))) { argv[intArgsProcessed + 1] = SysAllocString(struTempPath.QueryStr()); } else { argv[intArgsProcessed + 1] = SysAllocString(pwzArgs[intArgsProcessed]); } if (argv[intArgsProcessed + 1] == NULL) { hr = E_OUTOFMEMORY; goto Failure; } } else { argv[intArgsProcessed + 1] = SysAllocString(pwzArgs[intArgsProcessed]); if (argv[intArgsProcessed + 1] == NULL) { hr = E_OUTOFMEMORY; goto Failure; } } } *pbstrArgv = argv; *pdwArgCount = argc + 1; goto Finished; Failure: if (argv != NULL) { // intArgsProcess - 1 here as if we fail to allocated the ith string // we don't want to free it. for (INT i = 0; i < intArgsProcessed - 1; i++) { SysFreeString(argv[i]); } } delete[] argv; Finished: if (pwzArgs != NULL) { LocalFree(pwzArgs); DBG_ASSERT(pwzArgs == NULL); } return hr; } HRESULT HOSTFXR_UTILITY::GetAbsolutePathToDotnet( _Inout_ STRU* pStruAbsolutePathToDotnet ) { HRESULT hr = S_OK; // // If we are given an absolute path to dotnet.exe, we are done // if (UTILITY::CheckIfFileExists(pStruAbsolutePathToDotnet->QueryStr())) { goto Finished; } // // If the path was C:\Program Files\dotnet\dotnet // We need to try appending .exe and check if the file exists too. // if (FAILED(hr = pStruAbsolutePathToDotnet->Append(L".exe"))) { goto Finished; } if (UTILITY::CheckIfFileExists(pStruAbsolutePathToDotnet->QueryStr())) { goto Finished; } // At this point, we are calling where.exe to find dotnet. // If we encounter any failures, try getting dotnet.exe from the // backup location. if (!InvokeWhereToFindDotnet(pStruAbsolutePathToDotnet)) { hr = GetAbsolutePathToDotnetFromProgramFiles(pStruAbsolutePathToDotnet); } Finished: return hr; } HRESULT HOSTFXR_UTILITY::GetAbsolutePathToHostFxr( STRU* pStruAbsolutePathToDotnet, HANDLE hEventLog, STRU* pStruAbsolutePathToHostfxr ) { HRESULT hr = S_OK; STRU struHostFxrPath; STRU struHostFxrSearchExpression; STRU struHighestDotnetVersion; STRU struEventMsg; std::vector vVersionFolders; DWORD dwPosition = 0; if (FAILED(hr = struHostFxrPath.Copy(pStruAbsolutePathToDotnet))) { goto Finished; } dwPosition = struHostFxrPath.LastIndexOf(L'\\', 0); if (dwPosition == -1) { hr = E_FAIL; goto Finished; } struHostFxrPath.QueryStr()[dwPosition] = L'\0'; if (FAILED(hr = struHostFxrPath.SyncWithBuffer()) || FAILED(hr = struHostFxrPath.Append(L"\\"))) { goto Finished; } hr = struHostFxrPath.Append(L"host\\fxr"); if (FAILED(hr)) { goto Finished; } if (!UTILITY::DirectoryExists(&struHostFxrPath)) { hr = ERROR_BAD_ENVIRONMENT; UTILITY::LogEventF(hEventLog, EVENTLOG_ERROR_TYPE, ASPNETCORE_EVENT_HOSTFXR_DIRECTORY_NOT_FOUND, struEventMsg.QueryStr(), ASPNETCORE_EVENT_HOSTFXR_DIRECTORY_NOT_FOUND_MSG, struHostFxrPath.QueryStr(), hr); goto Finished; } // Find all folders under host\\fxr\\ for version numbers. hr = struHostFxrSearchExpression.Copy(struHostFxrPath); if (FAILED(hr)) { goto Finished; } hr = struHostFxrSearchExpression.Append(L"\\*"); if (FAILED(hr)) { goto Finished; } // As we use the logic from core-setup, we are opting to use std here. HOSTFXR_UTILITY::FindDotNetFolders(struHostFxrSearchExpression.QueryStr(), &vVersionFolders); if (vVersionFolders.size() == 0) { hr = HRESULT_FROM_WIN32(ERROR_BAD_ENVIRONMENT); UTILITY::LogEventF(hEventLog, EVENTLOG_ERROR_TYPE, ASPNETCORE_EVENT_HOSTFXR_DIRECTORY_NOT_FOUND, ASPNETCORE_EVENT_HOSTFXR_DIRECTORY_NOT_FOUND_MSG, struHostFxrPath.QueryStr(), hr); goto Finished; } hr = FindHighestDotNetVersion(vVersionFolders, &struHighestDotnetVersion); if (FAILED(hr)) { goto Finished; } if (FAILED(hr = struHostFxrPath.Append(L"\\")) || FAILED(hr = struHostFxrPath.Append(struHighestDotnetVersion.QueryStr())) || FAILED(hr = struHostFxrPath.Append(L"\\hostfxr.dll"))) { goto Finished; } if (!UTILITY::CheckIfFileExists(struHostFxrPath.QueryStr())) { // ASPNETCORE_EVENT_HOSTFXR_DLL_NOT_FOUND_MSG hr = HRESULT_FROM_WIN32(ERROR_FILE_INVALID); UTILITY::LogEventF(hEventLog, EVENTLOG_ERROR_TYPE, ASPNETCORE_EVENT_HOSTFXR_DLL_NOT_FOUND, ASPNETCORE_EVENT_HOSTFXR_DLL_NOT_FOUND_MSG, struHostFxrPath.QueryStr(), hr); goto Finished; } if (FAILED(hr = pStruAbsolutePathToHostfxr->Copy(struHostFxrPath))) { goto Finished; } Finished: return hr; } // // Tries to call where.exe to find the location of dotnet.exe. // Will check that the bitness of dotnet matches the current // worker process bitness. // Returns true if a valid dotnet was found, else false. // BOOL HOSTFXR_UTILITY::InvokeWhereToFindDotnet( _Inout_ STRU* pStruAbsolutePathToDotnet ) { HRESULT hr = S_OK; // Arguments to call where.exe STARTUPINFOW startupInfo = { 0 }; PROCESS_INFORMATION processInformation = { 0 }; SECURITY_ATTRIBUTES securityAttributes; CHAR pzFileContents[READ_BUFFER_SIZE]; HANDLE hStdOutReadPipe = INVALID_HANDLE_VALUE; HANDLE hStdOutWritePipe = INVALID_HANDLE_VALUE; LPWSTR pwzDotnetName = NULL; DWORD dwFilePointer; BOOL fIsWow64Process; BOOL fIsCurrentProcess64Bit; DWORD dwExitCode; STRU struDotnetSubstring; STRU struDotnetLocationsString; DWORD dwNumBytesRead; DWORD dwBinaryType; INT index = 0; INT prevIndex = 0; BOOL fProcessCreationResult = FALSE; BOOL fResult = FALSE; // Set the security attributes for the read/write pipe securityAttributes.nLength = sizeof(securityAttributes); securityAttributes.lpSecurityDescriptor = NULL; securityAttributes.bInheritHandle = TRUE; // Reset the path to dotnet as we will be using whether the string is // empty or not as state pStruAbsolutePathToDotnet->Reset(); // Create a read/write pipe that will be used for reading the result of where.exe if (!CreatePipe(&hStdOutReadPipe, &hStdOutWritePipe, &securityAttributes, 0)) { hr = HRESULT_FROM_WIN32(GetLastError()); goto Finished; } if (!SetHandleInformation(hStdOutReadPipe, HANDLE_FLAG_INHERIT, 0)) { hr = ERROR_FILE_INVALID; goto Finished; } // Set the stdout and err pipe to the write pipes. startupInfo.cb = sizeof(startupInfo); startupInfo.dwFlags |= STARTF_USESTDHANDLES; startupInfo.hStdOutput = hStdOutWritePipe; startupInfo.hStdError = hStdOutWritePipe; // CreateProcess requires a mutable string to be passed to commandline // See https://blogs.msdn.microsoft.com/oldnewthing/20090601-00/?p=18083/ pwzDotnetName = SysAllocString(L"\"where.exe\" dotnet.exe"); if (pwzDotnetName == NULL) { goto Finished; } // Create a process to invoke where.exe fProcessCreationResult = CreateProcessW(NULL, pwzDotnetName, NULL, NULL, TRUE, CREATE_NO_WINDOW, NULL, NULL, &startupInfo, &processInformation ); if (!fProcessCreationResult) { goto Finished; } // Wait for where.exe to return, waiting 2 seconds. if (WaitForSingleObject(processInformation.hProcess, 2000) != WAIT_OBJECT_0) { // Timeout occured, terminate the where.exe process and return. TerminateProcess(processInformation.hProcess, 2); hr = HRESULT_FROM_WIN32(ERROR_TIMEOUT); goto Finished; } // // where.exe will return 0 on success, 1 if the file is not found // and 2 if there was an error. Check if the exit code is 1 and set // a new hr result saying it couldn't find dotnet.exe // if (!GetExitCodeProcess(processInformation.hProcess, &dwExitCode)) { goto Finished; } // // In this block, if anything fails, we will goto our fallback of // looking in C:/Program Files/ // if (dwExitCode != 0) { goto Finished; } // Where succeeded. // Reset file pointer to the beginning of the file. dwFilePointer = SetFilePointer(hStdOutReadPipe, 0, NULL, FILE_BEGIN); if (dwFilePointer == INVALID_SET_FILE_POINTER) { goto Finished; } // // As the call to where.exe succeeded (dotnet.exe was found), ReadFile should not hang. // TODO consider putting ReadFile in a separate thread with a timeout to guarantee it doesn't block. // if (!ReadFile(hStdOutReadPipe, pzFileContents, READ_BUFFER_SIZE, &dwNumBytesRead, NULL)) { goto Finished; } if (dwNumBytesRead >= READ_BUFFER_SIZE) { // This shouldn't ever be this large. We could continue to call ReadFile in a loop, // however if someone had this many dotnet.exes on their machine. goto Finished; } hr = HRESULT_FROM_WIN32(GetLastError()); if (FAILED(hr = struDotnetLocationsString.CopyA(pzFileContents, dwNumBytesRead))) { goto Finished; } // Check the bitness of the currently running process // matches the dotnet.exe found. if (!IsWow64Process(GetCurrentProcess(), &fIsWow64Process)) { // Calling IsWow64Process failed goto Finished; } if (fIsWow64Process) { // 32 bit mode fIsCurrentProcess64Bit = FALSE; } else { // Check the SystemInfo to see if we are currently 32 or 64 bit. SYSTEM_INFO systemInfo; GetNativeSystemInfo(&systemInfo); fIsCurrentProcess64Bit = systemInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64; } while (TRUE) { index = struDotnetLocationsString.IndexOf(L"\r\n", prevIndex); if (index == -1) { break; } if (FAILED(hr = struDotnetSubstring.Copy(&struDotnetLocationsString.QueryStr()[prevIndex], index - prevIndex))) { goto Finished; } // \r\n is two wchars, so add 2 here. prevIndex = index + 2; if (GetBinaryTypeW(struDotnetSubstring.QueryStr(), &dwBinaryType) && fIsCurrentProcess64Bit == (dwBinaryType == SCS_64BIT_BINARY)) { // The bitness of dotnet matched with the current worker process bitness. if (FAILED(hr = pStruAbsolutePathToDotnet->Copy(struDotnetSubstring))) { goto Finished; } fResult = TRUE; break; } } Finished: if (hStdOutReadPipe != INVALID_HANDLE_VALUE) { CloseHandle(hStdOutReadPipe); } if (hStdOutWritePipe != INVALID_HANDLE_VALUE) { CloseHandle(hStdOutWritePipe); } if (processInformation.hProcess != INVALID_HANDLE_VALUE) { CloseHandle(processInformation.hProcess); } if (processInformation.hThread != INVALID_HANDLE_VALUE) { CloseHandle(processInformation.hThread); } if (pwzDotnetName != NULL) { SysFreeString(pwzDotnetName); } return fResult; } HRESULT HOSTFXR_UTILITY::GetAbsolutePathToDotnetFromProgramFiles( _Inout_ STRU* pStruAbsolutePathToDotnet ) { HRESULT hr = S_OK; BOOL fFound = FALSE; DWORD dwNumBytesRead = 0; DWORD dwPathSize = MAX_PATH; STRU struDotnetSubstring; while (!fFound) { if (FAILED(hr = struDotnetSubstring.Resize(dwPathSize))) { goto Finished; } dwNumBytesRead = GetEnvironmentVariable(L"ProgramFiles", struDotnetSubstring.QueryStr(), dwPathSize); if (dwNumBytesRead == 0) { hr = HRESULT_FROM_WIN32(GetLastError()); goto Finished; } else if (dwNumBytesRead >= dwPathSize) { // // The path to ProgramFiles should never be this long, but resize and try again. dwPathSize *= 2 + 30; // for dotnet substring } else { if (FAILED(hr = struDotnetSubstring.SyncWithBuffer()) || FAILED(hr = struDotnetSubstring.Append(L"\\dotnet\\dotnet.exe"))) { goto Finished; } if (!UTILITY::CheckIfFileExists(struDotnetSubstring.QueryStr())) { hr = HRESULT_FROM_WIN32(GetLastError()); goto Finished; } if (FAILED(hr = pStruAbsolutePathToDotnet->Copy(struDotnetSubstring))) { goto Finished; } fFound = TRUE; } } Finished: return hr; } HRESULT HOSTFXR_UTILITY::FindHighestDotNetVersion( _In_ std::vector vFolders, _Out_ STRU *pstrResult ) { HRESULT hr = S_OK; fx_ver_t max_ver(-1, -1, -1); for (const auto& dir : vFolders) { fx_ver_t fx_ver(-1, -1, -1); if (fx_ver_t::parse(dir, &fx_ver, false)) { // TODO using max instead of std::max works max_ver = max(max_ver, fx_ver); } } hr = pstrResult->Copy(max_ver.as_str().c_str()); // we check FAILED(hr) outside of function return hr; } VOID HOSTFXR_UTILITY::FindDotNetFolders( _In_ PCWSTR pszPath, _Out_ std::vector *pvFolders ) { HANDLE handle = NULL; WIN32_FIND_DATAW data = { 0 }; handle = FindFirstFileExW(pszPath, FindExInfoStandard, &data, FindExSearchNameMatch, NULL, 0); if (handle == INVALID_HANDLE_VALUE) { return; } do { std::wstring folder(data.cFileName); pvFolders->push_back(folder); } while (FindNextFileW(handle, &data)); FindClose(handle); }