mirror of https://github.com/tongzx/nt5src
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
746 lines
20 KiB
746 lines
20 KiB
/*++
|
|
|
|
Copyright (c) 1991 Microsoft Corporation
|
|
|
|
Module Name:
|
|
|
|
mprbase.cxx
|
|
|
|
Abstract:
|
|
|
|
Contains implementations of base classes that supply common code for
|
|
Multi-Provider Router operations.
|
|
Contains:
|
|
CMprOperation::Perform
|
|
CRoutedOperation::ValidateParameters
|
|
CRoutedOperation::GetResult
|
|
|
|
Author:
|
|
|
|
Anirudh Sahni (anirudhs) 11-Oct-1995
|
|
|
|
Environment:
|
|
|
|
User Mode -Win32
|
|
|
|
Notes:
|
|
|
|
Revision History:
|
|
|
|
11-Oct-1995 AnirudhS
|
|
Created.
|
|
|
|
05-May-1999 jschwart
|
|
Make provider addition/removal dynamic
|
|
|
|
--*/
|
|
|
|
//
|
|
// Includes
|
|
//
|
|
|
|
#include "precomp.hxx"
|
|
#include <malloc.h> // _alloca
|
|
|
|
//
|
|
// External Globals and Statics
|
|
//
|
|
extern DWORD GlobalNumActiveProviders;
|
|
|
|
CRoutedOperation::CPathCache CRoutedOperation::_PathCache;
|
|
|
|
//
|
|
// Defines
|
|
//
|
|
|
|
|
|
//
|
|
// Local Function Prototypes
|
|
//
|
|
|
|
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Function: CMprOperation::Perform
|
|
//
|
|
// Purpose: See header file
|
|
//
|
|
// History: 11-Oct-95 AnirudhS Created.
|
|
//
|
|
// Notes:
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
|
|
DWORD CMprOperation::Perform()
|
|
{
|
|
DWORD status = WN_SUCCESS;
|
|
|
|
__try
|
|
{
|
|
//
|
|
// Ask the derived class to validate the API parameters
|
|
//
|
|
status = ValidateParameters();
|
|
}
|
|
__except (EXCEPTION_EXECUTE_HANDLER)
|
|
{
|
|
status = GetExceptionCode();
|
|
if (status != EXCEPTION_ACCESS_VIOLATION)
|
|
{
|
|
MPR_LOG2(ERROR,"CMprOperation(%s): Unexpected exception %#lx\n",_OpName,status);
|
|
}
|
|
|
|
status = WN_BAD_POINTER;
|
|
}
|
|
|
|
if (status == WN_SUCCESS)
|
|
{
|
|
//
|
|
// Ask the derived class to perform the operation
|
|
//
|
|
status = GetResult();
|
|
}
|
|
|
|
if (status != WN_SUCCESS)
|
|
{
|
|
SetLastError(status);
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Function: CRoutedOperation::Perform
|
|
//
|
|
// Purpose: See header file
|
|
//
|
|
// History: 27-May-99 jschwart Created.
|
|
//
|
|
// Notes: Since the CMprOperation should have no knowledge of
|
|
// providers, deal with provider-related locking/checking
|
|
// in the CRoutedOperation class, which is meant for
|
|
// APIs that use the providers.
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
|
|
DWORD CRoutedOperation::Perform(BOOL fCheckProviders)
|
|
{
|
|
//
|
|
// If an API that uses this class creates another instance of the class
|
|
// (e.g., CGetConnectionPerformance uses CGetConnection), there needs
|
|
// to be a way to prevent the second call from trying to acquire the
|
|
// exclusive lock while the original call holds the shared lock.
|
|
//
|
|
if (fCheckProviders)
|
|
{
|
|
MprCheckProviders();
|
|
}
|
|
|
|
CProviderSharedLock PLock;
|
|
|
|
return CMprOperation::Perform();
|
|
}
|
|
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Function: CRoutedOperation::ValidateParameters
|
|
//
|
|
// Purpose: See header file
|
|
//
|
|
// History: 11-Oct-95 AnirudhS Created.
|
|
//
|
|
// Notes:
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
|
|
DWORD CRoutedOperation::ValidateParameters()
|
|
{
|
|
//
|
|
// Ask the derived class to validate the API parameters.
|
|
// Also, if the API caller passed in a specific NP name, the derived
|
|
// class should pass it back here, to be validated here. The provider
|
|
// is looked up and stored in _pSpecifiedProvider.
|
|
// If the API caller passed in a remote name that can be used as a
|
|
// hint for routing, the derived class should pass it back here. A
|
|
// pointer to it is stored in _RemoteName and used later, in GetResult(),
|
|
// to help pick an efficient provider routing order.
|
|
//
|
|
LPCWSTR pwszProviderName = NULL;
|
|
LPCWSTR pwszRemoteName = NULL;
|
|
LPCWSTR pwszLocalName = NULL;
|
|
|
|
ASSERT(MPRProviderLock.Have());
|
|
|
|
DWORD status = ValidateRoutedParameters(&pwszProviderName,
|
|
&pwszRemoteName,
|
|
&pwszLocalName);
|
|
|
|
if (status == WN_SUCCESS)
|
|
{
|
|
//
|
|
// Optimization: Store away the drive type. In GetResult(),
|
|
// we need only call the providers if the local name is
|
|
// a remote drive. _uDriveType is initialized to DRIVE_REMOTE
|
|
//
|
|
|
|
if (! IS_EMPTY_STRING(pwszLocalName) && pwszLocalName[1] == L':')
|
|
{
|
|
WCHAR wszRootPath[] = L" :\\";
|
|
|
|
wszRootPath[0] = pwszLocalName[0];
|
|
_uDriveType = GetDriveType(wszRootPath);
|
|
}
|
|
|
|
// This probes pwszRemoteName as well as saving its length
|
|
RtlInitUnicodeString(&_RemoteName, pwszRemoteName);
|
|
|
|
if (! IS_EMPTY_STRING(pwszProviderName))
|
|
{
|
|
//
|
|
// Level 1 init for MprFindProviderByName
|
|
//
|
|
if (!(GlobalInitLevel & FIRST_LEVEL)) {
|
|
status = MprLevel1Init();
|
|
if (status != WN_SUCCESS) {
|
|
return status;
|
|
}
|
|
}
|
|
|
|
_pSpecifiedProvider = MprFindProviderByName(pwszProviderName);
|
|
if (_pSpecifiedProvider == NULL)
|
|
{
|
|
return WN_BAD_PROVIDER;
|
|
}
|
|
}
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Function: CRoutedOperation::GetResult
|
|
//
|
|
// Purpose: See header file
|
|
//
|
|
// History: 11-Oct-95 AnirudhS Created.
|
|
//
|
|
// Notes:
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
|
|
DWORD CRoutedOperation::GetResult()
|
|
{
|
|
DWORD status = WN_SUCCESS;
|
|
LPPROVIDER *ProviderArray;
|
|
|
|
//
|
|
// Only call the providers if it's a remote drive
|
|
//
|
|
|
|
if (_uDriveType != DRIVE_REMOTE)
|
|
{
|
|
return WN_NOT_CONNECTED;
|
|
}
|
|
|
|
INIT_IF_NECESSARY(NETWORK_LEVEL, status);
|
|
|
|
//
|
|
// If there are no providers, return NO_NETWORK
|
|
//
|
|
if (GlobalNumActiveProviders == 0)
|
|
{
|
|
return WN_NO_NETWORK;
|
|
}
|
|
|
|
// Array of pointers into the GlobalProviderInfo array.
|
|
DWORD numProviders;
|
|
|
|
__try
|
|
{
|
|
ProviderArray = (LPPROVIDER *) _alloca(GlobalNumProviders * sizeof(LPPROVIDER));
|
|
}
|
|
__except (EXCEPTION_EXECUTE_HANDLER)
|
|
{
|
|
status = WN_OUT_OF_MEMORY;
|
|
}
|
|
|
|
if (status != WN_SUCCESS)
|
|
{
|
|
return status;
|
|
}
|
|
|
|
//
|
|
// Find the list of providers to call for this request.
|
|
//
|
|
if (_pSpecifiedProvider != NULL)
|
|
{
|
|
//
|
|
// The caller requested a particular Provider
|
|
//
|
|
ProviderArray[0] = _pSpecifiedProvider;
|
|
numProviders = 1;
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// A Provider name was not specified. Therefore, we must
|
|
// create an ordered list and pick the best one.
|
|
//
|
|
status = FindCallOrder(
|
|
&_RemoteName,
|
|
ProviderArray,
|
|
&numProviders,
|
|
NETWORK_TYPE
|
|
);
|
|
if (status != WN_SUCCESS)
|
|
{
|
|
return status;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Loop through the list of providers until one answers the request,
|
|
// or the list is exhausted.
|
|
//
|
|
DWORD statusFlag = 0; // Mask of combined error returns
|
|
DWORD FirstNetPathError = WN_SUCCESS; // First NO_NET or BAD_NAME error
|
|
DWORD FirstSignificantError = WN_SUCCESS; // First "other" error, used in
|
|
// aggressive routing only
|
|
|
|
status = WN_NOT_SUPPORTED; // Returned if no providers respond
|
|
|
|
for (DWORD i=0; i<numProviders; i++)
|
|
{
|
|
_LastProvider = ProviderArray[i];
|
|
|
|
if (_pProviderFunction != NULL &&
|
|
_LastProvider->*_pProviderFunction == NULL)
|
|
{
|
|
//
|
|
// The provider doesn't supply the required entry point.
|
|
//
|
|
status = WN_NOT_SUPPORTED;
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// Ask the derived class to try the provider.
|
|
//
|
|
__try
|
|
{
|
|
MPR_LOG2(ROUTE, "%s: trying %ws ...\n",
|
|
OpName(), _LastProvider->Resource.lpProvider);
|
|
status = TestProvider(_LastProvider);
|
|
MPR_LOG(ROUTE, "... provider returned %lu\n", status);
|
|
}
|
|
__except (EXCEPTION_EXECUTE_HANDLER)
|
|
{
|
|
status = GetExceptionCode();
|
|
MPR_LOG(ROUTE, "... provider threw EXCEPTION %#lx\n", status);
|
|
if (status != EXCEPTION_ACCESS_VIOLATION)
|
|
{
|
|
MPR_LOG3(ERROR,
|
|
"%s: Unexpected Exception %#lx "
|
|
"calling %ws provider\n",
|
|
OpName(), status,
|
|
_LastProvider->Resource.lpProvider);
|
|
}
|
|
status = WN_BAD_POINTER;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Decide whether to stop trying other providers and return the
|
|
// error immediately, or continue trying other providers.
|
|
// There are two algorithms for routing to providers, called
|
|
// "lazy routing" and "aggressive routing". In lazy routing,
|
|
// we always stop routing, unless the error was an insignificant
|
|
// one (such as WN_BAD_NETNAME) indicating that the call may be
|
|
// meant for some other provider. In aggressive routing, we
|
|
// always continue routing, except on a few special errors (such
|
|
// as WN_SUCCESS).
|
|
//
|
|
switch (status)
|
|
{
|
|
|
|
//////////////////////////////////////////////////////////////
|
|
// Always stop routing on these errors, even if routing //
|
|
// aggressively //
|
|
//////////////////////////////////////////////////////////////
|
|
|
|
case WN_SUCCESS:
|
|
case WN_MORE_DATA:
|
|
//
|
|
// The provider successfully operated on this path, so add it
|
|
// to the cache...
|
|
//
|
|
_PathCache.AddEntry(&_RemoteName, _LastProvider);
|
|
//
|
|
// ... and fall through
|
|
//
|
|
case WN_BAD_POINTER:
|
|
case WN_ALREADY_CONNECTED:
|
|
case WN_CANCEL:
|
|
goto CleanExit;
|
|
|
|
|
|
//////////////////////////////////////////////////////////////
|
|
// Always continue routing on these errors //
|
|
// Classify them so that if we later decide to return one //
|
|
// of them to the caller, we pick the most sensible one //
|
|
//////////////////////////////////////////////////////////////
|
|
|
|
case WN_NOT_SUPPORTED:
|
|
//
|
|
// Ignore the error
|
|
//
|
|
break;
|
|
|
|
case WN_NO_MORE_DEVICES:
|
|
statusFlag |= NO_DEVICES;
|
|
break;
|
|
|
|
case WN_NOT_CONNECTED:
|
|
statusFlag |= NOT_CONNECTED;
|
|
break;
|
|
|
|
case WN_NOT_CONTAINER:
|
|
statusFlag |= NOT_CONTAINER;
|
|
break;
|
|
|
|
case WN_NO_NETWORK:
|
|
case WN_FUNCTION_BUSY:
|
|
case WN_NO_NET_OR_BAD_PATH:
|
|
case WN_NOT_LOGGED_ON:
|
|
statusFlag |= NO_NET;
|
|
if (FirstNetPathError == WN_SUCCESS)
|
|
{
|
|
FirstNetPathError = status;
|
|
}
|
|
break;
|
|
|
|
case WN_BAD_NETNAME:
|
|
case ERROR_BAD_NETPATH:
|
|
case WN_BAD_LOCALNAME:
|
|
case WN_BAD_VALUE:
|
|
case WN_BAD_LEVEL:
|
|
case ERROR_REM_NOT_LIST:
|
|
statusFlag |= BAD_NAME;
|
|
if (FirstNetPathError == WN_SUCCESS)
|
|
{
|
|
FirstNetPathError = status;
|
|
}
|
|
break;
|
|
|
|
|
|
//////////////////////////////////////////////////////////////
|
|
// On other errors, stop routing if lazy, continue if //
|
|
// aggressive //
|
|
//////////////////////////////////////////////////////////////
|
|
|
|
default:
|
|
if (_AggressiveRouting)
|
|
{
|
|
// Remember the first one of these errors. It will take
|
|
// precedence over other errors.
|
|
if (FirstSignificantError == WN_SUCCESS)
|
|
{
|
|
FirstSignificantError = status;
|
|
}
|
|
break;
|
|
|
|
// Note that if multiple providers return WN_EXTENDED_ERROR,
|
|
// we'll return the error reported by the last one rather
|
|
// than the first.
|
|
}
|
|
else
|
|
{
|
|
// Return this error immediately
|
|
goto CleanExit;
|
|
}
|
|
} // switch
|
|
} // for all providers
|
|
|
|
//
|
|
// If a specific provider was tried then return the error from that provider.
|
|
// Otherwise, concoct the best return code from the errors returned.
|
|
//
|
|
if (numProviders > 1)
|
|
{
|
|
if (FirstSignificantError != WN_SUCCESS)
|
|
{
|
|
status = FirstSignificantError;
|
|
}
|
|
else if (statusFlag & NO_DEVICES)
|
|
{
|
|
status = WN_NO_MORE_DEVICES;
|
|
}
|
|
else if (statusFlag & NOT_CONNECTED)
|
|
{
|
|
status = WN_NOT_CONNECTED;
|
|
}
|
|
else if (statusFlag & NOT_CONTAINER)
|
|
{
|
|
status = WN_NOT_CONTAINER;
|
|
}
|
|
else if (statusFlag & (NO_NET | BAD_NAME))
|
|
{
|
|
if ((statusFlag & (NO_NET | BAD_NAME)) == (NO_NET | BAD_NAME))
|
|
{
|
|
//
|
|
// Mix of special errors occured.
|
|
// Pass back the combined error message.
|
|
//
|
|
status = WN_NO_NET_OR_BAD_PATH;
|
|
}
|
|
else
|
|
{
|
|
status = FirstNetPathError;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ASSERT(status == WN_NOT_SUPPORTED);
|
|
}
|
|
}
|
|
|
|
CleanExit:
|
|
|
|
MPR_LOG2(ROUTE, "CRoutedOperation(%s): returning %lu\n\n", OpName(), status);
|
|
|
|
return status;
|
|
}
|
|
|
|
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Function: CRoutedOperation::CPathCache::Construct
|
|
//
|
|
// Purpose: Constructor, called explicitly to avoid dependence on CRT
|
|
//
|
|
// History: 09-Apr-96 AnirudhS Created.
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
|
|
void CRoutedOperation::CPathCache::Construct()
|
|
{
|
|
InitializeCriticalSection(&_Lock);
|
|
RtlZeroMemory(_RecentPaths, sizeof(_RecentPaths));
|
|
InitializeListHead(&_ListHead);
|
|
_NumFree = PATH_CACHE_SIZE;
|
|
}
|
|
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Function: CRoutedOperation::CPathCache::Destroy
|
|
//
|
|
// Purpose: Destructor, called explicitly to avoid dependence on CRT
|
|
//
|
|
// History: 09-Apr-96 AnirudhS Created.
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
|
|
void CRoutedOperation::CPathCache::Destroy()
|
|
{
|
|
//
|
|
// This is really needed only if the DLL is being unloaded because of
|
|
// a FreeLibrary call, not if the process is exiting
|
|
//
|
|
for (DWORD i = _NumFree; i < PATH_CACHE_SIZE; i++)
|
|
{
|
|
LocalFree(_RecentPaths[i].Path.Buffer);
|
|
}
|
|
DeleteCriticalSection(&_Lock);
|
|
}
|
|
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Function: CRoutedOperation::CPathCache::AddEntry
|
|
//
|
|
// Purpose: Add an entry to the cache
|
|
//
|
|
// History: 09-Apr-96 AnirudhS Created.
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
|
|
void CRoutedOperation::CPathCache::AddEntry(
|
|
const UNICODE_STRING * Path,
|
|
LPPROVIDER Provider
|
|
)
|
|
{
|
|
if (Path->Length == 0 || Path->Length >= (MAX_PATH*sizeof(WCHAR)))
|
|
{
|
|
//
|
|
// Don't add empty or too-long paths to the cache
|
|
//
|
|
return;
|
|
}
|
|
|
|
ASSERT(Path->MaximumLength == Path->Length + sizeof(UNICODE_NULL));
|
|
|
|
EnterCriticalSection(&_Lock);
|
|
|
|
CacheEntry *pEntry = NULL; // Entry to write
|
|
|
|
//
|
|
// See if there's a matching path string in the cache already
|
|
//
|
|
for (PLIST_ENTRY pLinks = _ListHead.Flink;
|
|
pLinks != &_ListHead;
|
|
pLinks = pLinks->Flink)
|
|
{
|
|
pEntry = CONTAINING_RECORD(pLinks, CacheEntry, Links);
|
|
|
|
if (RtlEqualUnicodeString(&pEntry->Path, (PUNICODE_STRING) Path, TRUE))
|
|
{
|
|
break;
|
|
}
|
|
|
|
pEntry = NULL;
|
|
}
|
|
|
|
if (pEntry == NULL)
|
|
{
|
|
//
|
|
// No matching entry.
|
|
// If there's a free entry in the array, use it.
|
|
// Otherwise overwrite the last entry in the list.
|
|
//
|
|
if (_NumFree > 0)
|
|
{
|
|
_NumFree--;
|
|
pEntry = &_RecentPaths[_NumFree];
|
|
|
|
//
|
|
// Add this new entry to the list.
|
|
//
|
|
InsertHeadList(&_ListHead, &pEntry->Links);
|
|
}
|
|
else
|
|
{
|
|
ASSERT(!IsListEmpty(&_ListHead));
|
|
pEntry = CONTAINING_RECORD(_ListHead.Blink, CacheEntry, Links);
|
|
}
|
|
|
|
//
|
|
// Copy the path string into the cache. Re-use the string buffer,
|
|
// unless it's too small.
|
|
//
|
|
if (pEntry->Path.MaximumLength < Path->MaximumLength)
|
|
{
|
|
//
|
|
// Re-allocate the string buffer. Allocate twice as much space
|
|
// as needed, but never more than MAX_PATH Unicode characters.
|
|
// Note, here we know that MaximumLength <= MAX_PATH characters.
|
|
//
|
|
HLOCAL NewBuffer = LocalAlloc(
|
|
0,
|
|
min(Path->MaximumLength * 2, MAX_PATH * sizeof(WCHAR))
|
|
);
|
|
if (NewBuffer == NULL)
|
|
{
|
|
//
|
|
// Couldn't allocate. Don't add to the cache.
|
|
// (If it was unused, this cache entry is lost forever.
|
|
// CODEWORK try to recover it in this case?)
|
|
//
|
|
goto CleanExit;
|
|
}
|
|
|
|
LocalFree(pEntry->Path.Buffer);
|
|
pEntry->Path.Buffer = (PWSTR) NewBuffer;
|
|
pEntry->Path.MaximumLength = (USHORT)LocalSize(NewBuffer);
|
|
}
|
|
|
|
RtlCopyUnicodeString(&pEntry->Path, (PUNICODE_STRING) Path);
|
|
}
|
|
|
|
//
|
|
// Remember the provider in the cache. (This overwrites any previously
|
|
// remembered provider for the path.)
|
|
//
|
|
pEntry->Provider = Provider;
|
|
MPR_LOG2(ROUTE, "cache: cached %ws for %ws\n", Provider->Resource.lpProvider, Path->Buffer);
|
|
|
|
//
|
|
// Move this entry to the front of the list, if it isn't there already.
|
|
//
|
|
if (_ListHead.Flink != &pEntry->Links)
|
|
{
|
|
RemoveEntryList(&pEntry->Links);
|
|
InsertHeadList(&_ListHead, &pEntry->Links);
|
|
}
|
|
|
|
CleanExit:
|
|
|
|
LeaveCriticalSection(&_Lock);
|
|
}
|
|
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Function: CRoutedOperation::CPathCache::FindEntry
|
|
//
|
|
// Purpose: Search for an entry in the cache
|
|
//
|
|
// History: 09-Apr-96 AnirudhS Created.
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
|
|
LPPROVIDER CRoutedOperation::CPathCache::FindEntry(
|
|
const UNICODE_STRING * Path
|
|
)
|
|
{
|
|
if (Path->Length == 0)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
ASSERT(Path->MaximumLength == Path->Length + sizeof(UNICODE_NULL));
|
|
|
|
EnterCriticalSection(&_Lock);
|
|
|
|
//
|
|
// Search forward in the list for a matching path string
|
|
//
|
|
LPPROVIDER Provider = NULL;
|
|
for (PLIST_ENTRY pLinks = _ListHead.Flink;
|
|
pLinks != &_ListHead;
|
|
pLinks = pLinks->Flink)
|
|
{
|
|
CacheEntry *pEntry = CONTAINING_RECORD(pLinks, CacheEntry, Links);
|
|
|
|
if (RtlEqualUnicodeString(&pEntry->Path, (PUNICODE_STRING) Path, TRUE))
|
|
{
|
|
Provider = pEntry->Provider;
|
|
|
|
//
|
|
// Move this entry to the front of the list, if it isn't there already.
|
|
//
|
|
if (_ListHead.Flink != &pEntry->Links)
|
|
{
|
|
RemoveEntryList(&pEntry->Links);
|
|
InsertHeadList(&_ListHead, &pEntry->Links);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
LeaveCriticalSection(&_Lock);
|
|
|
|
MPR_LOG2(ROUTE, "cache: found %ws for %ws\n",
|
|
(Provider ? Provider->Resource.lpProvider : L"no cached provider"),
|
|
Path->Buffer);
|
|
|
|
return Provider;
|
|
}
|