Add tracing for default probing for managed assemblies (#740)
authorElinor Fung <47805090+elinor-fung@users.noreply.github.com>
Wed, 18 Dec 2019 04:56:16 +0000 (20:56 -0800)
committerGitHub <noreply@github.com>
Wed, 18 Dec 2019 04:56:16 +0000 (20:56 -0800)
18 files changed:
src/coreclr/src/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.CoreCLR.cs
src/coreclr/src/binder/assemblybinder.cpp
src/coreclr/src/binder/bindertracing.cpp
src/coreclr/src/binder/inc/assemblybinder.hpp
src/coreclr/src/binder/inc/bindertracing.h
src/coreclr/src/vm/ClrEtwAll.man
src/coreclr/src/vm/assemblynative.cpp
src/coreclr/src/vm/assemblynative.hpp
src/coreclr/src/vm/ecalllist.h
src/coreclr/tests/src/Loader/binding/tracing/AssemblyToLoad.csproj
src/coreclr/tests/src/Loader/binding/tracing/BinderEventListener.cs
src/coreclr/tests/src/Loader/binding/tracing/BinderTracingTest.DefaultProbing.cs [new file with mode: 0644]
src/coreclr/tests/src/Loader/binding/tracing/BinderTracingTest.EventHandlers.cs
src/coreclr/tests/src/Loader/binding/tracing/BinderTracingTest.cs
src/coreclr/tests/src/Loader/binding/tracing/BinderTracingTest.csproj
src/coreclr/tests/src/Loader/binding/tracing/Helpers.cs [new file with mode: 0644]
src/coreclr/tests/src/Loader/binding/tracing/Resource.fr-FR.resx [new file with mode: 0644]
src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs

index 84070a6..24cc945 100644 (file)
@@ -44,6 +44,9 @@ namespace System.Runtime.Loader
         [DllImport(RuntimeHelpers.QCall, CharSet = CharSet.Unicode)]
         internal static extern bool TraceAssemblyLoadFromResolveHandlerInvoked(string assemblyName, bool isTrackedAssembly, string requestingAssemblyPath, string? requestedAssemblyPath);
 
+        [DllImport(RuntimeHelpers.QCall, CharSet = CharSet.Unicode)]
+        internal static extern bool TraceSatelliteSubdirectoryPathProbed(string filePath, int hResult);
+
         private Assembly InternalLoadFromPath(string? assemblyPath, string? nativeImagePath)
         {
             RuntimeAssembly? loadedAssembly = null;
index c38ee58..e03d07f 100644 (file)
@@ -17,6 +17,7 @@
 
 #include "assembly.hpp"
 #include "applicationcontext.hpp"
+#include "bindertracing.h"
 #include "loadcontext.hpp"
 #include "bindresult.inl"
 #include "failurecache.hpp"
@@ -713,10 +714,13 @@ namespace BINDER_SPACE
 
     namespace
     {
+        typedef void (*OnPathProbed)(const WCHAR *path, HRESULT hr);
+
         HRESULT BindSatelliteResourceByProbingPaths(
             const StringArrayList   *pResourceRoots,
             AssemblyName            *pRequestedAssemblyName,
-            BindResult              *pBindResult)
+            BindResult              *pBindResult,
+            OnPathProbed            onPathProbed)
         {
             HRESULT hr = S_OK;
 
@@ -739,6 +743,7 @@ namespace BINDER_SPACE
                                                  FALSE /* fIsInGAC */,
                                                  FALSE /* fExplicitBindToNativeImage */,
                                                  &pAssembly);
+                onPathProbed(fileName, hr);
 
                 // Missing files are okay and expected when probing
                 if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND))
@@ -772,6 +777,7 @@ namespace BINDER_SPACE
         {
             SString &simpleName = pRequestedAssemblyName->GetSimpleName();
 
+            BinderTracing::PathSource pathSource = useNativeImages ? BinderTracing::PathSource::AppNativeImagePaths : BinderTracing::PathSource::AppPaths;
             // Loop through the binding paths looking for a matching assembly
             for (DWORD i = 0; i < pBindingPaths->GetCount(); i++)
             {
@@ -789,6 +795,7 @@ namespace BINDER_SPACE
                                                  FALSE, // fIsInGAC
                                                  useNativeImages, // fExplicitBindToNativeImage
                                                  &pAssembly);
+                BinderTracing::PathProbed(fileName, pathSource, hr);
 
                 if (FAILED(hr))
                 {
@@ -798,6 +805,7 @@ namespace BINDER_SPACE
                                                      FALSE, // fIsInGAC
                                                      useNativeImages, // fExplicitBindToNativeImage
                                                      &pAssembly);
+                    BinderTracing::PathProbed(fileName, pathSource, hr);
                 }
 
                 // Since we're probing, file not founds are ok and we should just try another
@@ -861,7 +869,8 @@ namespace BINDER_SPACE
 
             hr = BindSatelliteResourceByProbingPaths(pApplicationContext->GetPlatformResourceRoots(),
                                                      pRequestedAssemblyName,
-                                                     pBindResult);
+                                                     pBindResult,
+                                                     [](const WCHAR *path, HRESULT res) { BinderTracing::PathProbed(path, BinderTracing::PathSource::PlatformResourceRoots, res); });
 
             // We found a platform resource file with matching file name, but whose ref-def didn't match.  Fall
             // back to application resource lookup to handle case where a user creates resources with the same
@@ -875,7 +884,8 @@ namespace BINDER_SPACE
             {
                 IF_FAIL_GO(BindSatelliteResourceByProbingPaths(pApplicationContext->GetAppPaths(),
                                                                pRequestedAssemblyName,
-                                                               pBindResult));
+                                                               pBindResult,
+                                                               [](const WCHAR *path, HRESULT res) { BinderTracing::PathProbed(path, BinderTracing::PathSource::AppPaths, res); }));
             }
         }
         else
@@ -895,6 +905,7 @@ namespace BINDER_SPACE
                                      TRUE,  // fIsInGAC
                                      TRUE,  // fExplicitBindToNativeImage
                                      &pTPAAssembly);
+                    BinderTracing::PathProbed(fileName, BinderTracing::PathSource::ApplicationAssemblies, hr);
                 }
                 else
                 {
@@ -905,6 +916,7 @@ namespace BINDER_SPACE
                                      TRUE,  // fIsInGAC
                                      FALSE, // fExplicitBindToNativeImage
                                      &pTPAAssembly);
+                    BinderTracing::PathProbed(fileName, BinderTracing::PathSource::ApplicationAssemblies, hr);
                 }
 
                 // On file not found, simply fall back to app path probing
index f5523f0..26949a5 100644 (file)
@@ -207,6 +207,9 @@ namespace BinderTracing
             PopulateBindRequest(m_bindRequest);
 
         FireAssemblyLoadStop(m_bindRequest, m_resultAssembly, m_cached);
+
+        if (m_resultAssembly != nullptr)
+            m_resultAssembly->Release();
     }
 
     void AssemblyBindOperation::SetResult(PEAssembly *assembly, bool cached)
@@ -231,3 +234,8 @@ namespace BinderTracing
         return m_ignoreBind;
     }
 }
+
+void BinderTracing::PathProbed(const WCHAR *path, BinderTracing::PathSource source, HRESULT hr)
+{
+    FireEtwKnownPathProbed(GetClrInstanceId(), path, source, hr);
+}
\ No newline at end of file
index 435630c..1332b67 100644 (file)
@@ -24,14 +24,6 @@ class CLRPrivBinderCoreCLR;
 
 namespace BINDER_SPACE
 {
-    typedef enum
-    {
-        kBindingStoreGAC      = 0x01,
-        kBindingStoreManifest = 0x02,
-        kBindingStoreHost     = 0x04,
-        kBindingStoreContext  = 0x08
-    } BindingStore;
-
     class AssemblyBinder
     {
     public:
index a259f37..f42f7ae 100644 (file)
@@ -46,9 +46,21 @@ namespace BinderTracing
         bool m_checkedIgnoreBind;
         bool m_ignoreBind;
 
-        ReleaseHolder<PEAssembly> m_resultAssembly;
+        PEAssembly *m_resultAssembly;
         bool m_cached;
     };
+
+    // This must match the BindingPathSource value map in ClrEtwAll.man
+    enum PathSource
+    {
+        ApplicationAssemblies,
+        AppNativeImagePaths,
+        AppPaths,
+        PlatformResourceRoots,
+        SatelliteSubdirectory
+    };
+
+    void PathProbed(const WCHAR *path, PathSource source, HRESULT hr);
 };
 
 #endif // __BINDER_TRACING_H__
index c9bbae4..a912fac 100644 (file)
                             <opcode name="AssemblyLoadContextResolvingHandlerInvoked" message="$(string.RuntimePublisher.AssemblyLoadContextResolvingHandlerInvokedOpcodeMessage)" symbol="CLR_ALC_RESOLVING_HANDLER_INVOKED_OPCODE" value="12"/>
                             <opcode name="AppDomainAssemblyResolveHandlerInvoked" message="$(string.RuntimePublisher.AppDomainAssemblyResolveHandlerInvokedOpcodeMessage)" symbol="CLR_APPDOMAIN_ASSEMBLY_RESOLVE_HANDLER_INVOKED_OPCODE" value="13"/>
                             <opcode name="AssemblyLoadFromResolveHandlerInvoked" message="$(string.RuntimePublisher.AssemblyLoadFromResolveHandlerInvokedOpcodeMessage)" symbol="CLR_ASSEMBLY_LOAD_FROM_RESOLVE_HANDLER_INVOKED_OPCODE" value="14"/>
+                            <opcode name="KnownPathProbed" message="$(string.RuntimePublisher.KnownPathProbedOpcodeMessage)" symbol="CLR_BINDING_PATH_PROBED_OPCODE" value="15"/>
                         </opcodes>
                     </task>
                 <!--Next available ID is 33-->
                         <map value="5" message="$(string.RuntimePublisher.GCRootKind.Overflow)"/>
                     </valueMap>
                     <valueMap name="GCHandleKindMap">
-                      <map value="0x0" message="$(string.RuntimePublisher.GCHandleKind.WeakShortMessage)"/>
-                      <map value="0x1" message="$(string.RuntimePublisher.GCHandleKind.WeakLongMessage)"/>
-                      <map value="0x2" message="$(string.RuntimePublisher.GCHandleKind.StrongMessage)"/>
-                      <map value="0x3" message="$(string.RuntimePublisher.GCHandleKind.PinnedMessage)"/>
-                      <map value="0x4" message="$(string.RuntimePublisher.GCHandleKind.VariableMessage)"/>
-                      <map value="0x5" message="$(string.RuntimePublisher.GCHandleKind.RefCountedMessage)"/>
-                      <map value="0x6" message="$(string.RuntimePublisher.GCHandleKind.DependentMessage)"/>
-                      <map value="0x7" message="$(string.RuntimePublisher.GCHandleKind.AsyncPinnedMessage)"/>
-                      <map value="0x8" message="$(string.RuntimePublisher.GCHandleKind.SizedRefMessage)"/>
+                        <map value="0x0" message="$(string.RuntimePublisher.GCHandleKind.WeakShortMessage)"/>
+                        <map value="0x1" message="$(string.RuntimePublisher.GCHandleKind.WeakLongMessage)"/>
+                        <map value="0x2" message="$(string.RuntimePublisher.GCHandleKind.StrongMessage)"/>
+                        <map value="0x3" message="$(string.RuntimePublisher.GCHandleKind.PinnedMessage)"/>
+                        <map value="0x4" message="$(string.RuntimePublisher.GCHandleKind.VariableMessage)"/>
+                        <map value="0x5" message="$(string.RuntimePublisher.GCHandleKind.RefCountedMessage)"/>
+                        <map value="0x6" message="$(string.RuntimePublisher.GCHandleKind.DependentMessage)"/>
+                        <map value="0x7" message="$(string.RuntimePublisher.GCHandleKind.AsyncPinnedMessage)"/>
+                        <map value="0x8" message="$(string.RuntimePublisher.GCHandleKind.SizedRefMessage)"/>
+                    </valueMap>
+                    <valueMap name="KnownPathSourceMap">
+                        <map value="0x0" message="$(string.RuntimePublisher.KnownPathSource.ApplicationAssembliesMessage)"/>
+                        <map value="0x1" message="$(string.RuntimePublisher.KnownPathSource.AppNativeImagePathsMessage)"/>
+                        <map value="0x2" message="$(string.RuntimePublisher.KnownPathSource.AppPathsMessage)"/>
+                        <map value="0x3" message="$(string.RuntimePublisher.KnownPathSource.PlatformResourceRootsMessage)"/>
+                        <map value="0x4" message="$(string.RuntimePublisher.KnownPathSource.SatelliteSubdirectoryMessage)"/>
                     </valueMap>
 
                     <!-- BitMaps -->
                         </UserData>
                     </template>
 
-                    <template tid="MethodDetails">
-                        <data name="MethodID" inType="win:UInt64" outType="win:HexInt64" />
-                        <data name="TypeID" inType="win:UInt64" outType="win:HexInt64" />
-                        <data name="MethodToken" inType="win:UInt32" outType="win:HexInt32" />
-                        <data name="TypeParameterCount" inType="win:UInt32" />
-                        <data name="LoaderModuleID" inType="win:UInt64" outType="win:HexInt64" />
-                        <data name="TypeParameters" count="TypeParameterCount"  inType="win:UInt64" outType="win:HexInt64" />
-                        <UserData>
-                            <MethodDetails xmlns="myNs">
-                                <MethodID> %1 </MethodID>
-                                <TypeID> %2 </TypeID>
-                                <MethodToken> %3 </MethodToken>
-                                <TypeParameterCount> %4 </TypeParameterCount>
-                                <LoaderModuleID> %5 </LoaderModuleID>
-                            </MethodDetails>
-                        </UserData>
-                    </template>
-
                     <template tid="AssemblyLoadContextResolvingHandlerInvoked">
                         <data name="ClrInstanceID" inType="win:UInt16" />
                         <data name="AssemblyName" inType="win:UnicodeString" />
                         </UserData>
                     </template>
 
+                    <template tid="KnownPathProbed">
+                        <data name="ClrInstanceID" inType="win:UInt16" />
+                        <data name="FilePath" inType="win:UnicodeString" />
+                        <data name="Source" inType="win:UInt16" map="KnownPathSourceMap" />
+                        <data name="Result" inType="win:Int32" />
+                        <UserData>
+                            <KnownPathProbed xmlns="myNs">
+                                <ClrInstanceID> %1 </ClrInstanceID>
+                                <FilePath> %2 </FilePath>
+                                <Source> %3 </Source>
+                                <Result> %4 </Result>
+                            </KnownPathProbed>
+                        </UserData>
+                    </template>
+
+                    <template tid="MethodDetails">
+                        <data name="MethodID" inType="win:UInt64" outType="win:HexInt64" />
+                        <data name="TypeID" inType="win:UInt64" outType="win:HexInt64" />
+                        <data name="MethodToken" inType="win:UInt32" outType="win:HexInt32" />
+                        <data name="TypeParameterCount" inType="win:UInt32" />
+                        <data name="LoaderModuleID" inType="win:UInt64" outType="win:HexInt64" />
+                        <data name="TypeParameters" count="TypeParameterCount"  inType="win:UInt64" outType="win:HexInt64" />
+                        <UserData>
+                            <MethodDetails xmlns="myNs">
+                                <MethodID> %1 </MethodID>
+                                <TypeID> %2 </TypeID>
+                                <MethodToken> %3 </MethodToken>
+                                <TypeParameterCount> %4 </TypeParameterCount>
+                                <LoaderModuleID> %5 </LoaderModuleID>
+                            </MethodDetails>
+                        </UserData>
+                    </template>
+
                     <template tid="MethodLoadUnload">
                         <data name="MethodID" inType="win:UInt64" outType="win:HexInt64" />
                         <data name="ModuleID" inType="win:UInt64" outType="win:HexInt64" />
                            task="AssemblyLoader"
                            symbol="AssemblyLoadFromResolveHandlerInvoked" message="$(string.RuntimePublisher.AssemblyLoadFromResolveHandlerInvokedEventMessage)"/>
 
+                    <event value="296" version="0" level="win:Informational"  template="KnownPathProbed"
+                           keywords ="AssemblyLoaderKeyword" opcode="KnownPathProbed"
+                           task="AssemblyLoader"
+                           symbol="KnownPathProbed" message="$(string.RuntimePublisher.KnownPathProbedEventMessage)"/>
+
                 </events>
             </provider>
 
                 <string id="RuntimePublisher.AssemblyLoadContextResolvingHandlerInvokedEventMessage" value="ClrInstanceID=%1;%nAssemblyName=%2;%nHandlerName=%3;%nAssemblyLoadContext=%4;%nResultAssemblyName=%5;%nResultAssemblyPath=%6" />
                 <string id="RuntimePublisher.AppDomainAssemblyResolveHandlerInvokedEventMessage" value="ClrInstanceID=%1;%nAssemblyName=%2;%nHandlerName=%3;%nResultAssemblyName=%4;%nResultAssemblyPath=%5" />
                 <string id="RuntimePublisher.AssemblyLoadFromResolveHandlerInvokedEventMessage" value="ClrInstanceID=%1;%nAssemblyName=%2;%nIsTrackedLoad=%3;%nRequestingAssemblyPath=%4;%nComputedRequestedAssemblyPath=%5" />
+                <string id="RuntimePublisher.KnownPathProbedEventMessage" value="ClrInstanceID=%1;%nFilePath=%2;%nSource=%3;%nResult=%4" />
                 <string id="RuntimePublisher.StackEventMessage" value="ClrInstanceID=%1;%nReserved1=%2;%nReserved2=%3;%nFrameCount=%4;%nStack=%5" />
                 <string id="RuntimePublisher.AppDomainMemAllocatedEventMessage" value="AppDomainID=%1;%nAllocated=%2;%nClrInstanceID=%3" />
                 <string id="RuntimePublisher.AppDomainMemSurvivedEventMessage" value="AppDomainID=%1;%nSurvived=%2;%nProcessSurvived=%3;%nClrInstanceID=%4" />
                 <string id="RuntimePublisher.TieredCompilationSettingsFlags.NoneMapMessage" value="None" />
                 <string id="RuntimePublisher.TieredCompilationSettingsFlags.QuickJitMapMessage" value="QuickJit" />
                 <string id="RuntimePublisher.TieredCompilationSettingsFlags.QuickJitForLoopsMapMessage" value="QuickJitForLoops" />
+                <string id="RuntimePublisher.KnownPathSource.ApplicationAssembliesMessage" value="ApplicationAssemblies" />
+                <string id="RuntimePublisher.KnownPathSource.AppNativeImagePathsMessage" value="AppNativeImagePaths" />
+                <string id="RuntimePublisher.KnownPathSource.AppPathsMessage" value="AppPaths" />
+                <string id="RuntimePublisher.KnownPathSource.PlatformResourceRootsMessage" value="PlatformResourceRoots" />
+                <string id="RuntimePublisher.KnownPathSource.SatelliteSubdirectoryMessage" value="SatelliteSubdirectory" />
 
                 <string id="RundownPublisher.AppDomain.ExecutableMapMessage" value="Executable" />
                 <string id="RundownPublisher.AppDomain.SharedMapMessage" value="Shared" />
                 <string id="RuntimePublisher.AssemblyLoadContextResolvingHandlerInvokedOpcodeMessage" value="AssemblyLoadContextResolvingHandlerInvoked" />
                 <string id="RuntimePublisher.AppDomainAssemblyResolveHandlerInvokedOpcodeMessage" value="AppDomainAssemblyResolveHandlerInvoked" />
                 <string id="RuntimePublisher.AssemblyLoadFromResolveHandlerInvokedOpcodeMessage" value="AssemblyLoadFromResolveHandlerInvoked" />
+                <string id="RuntimePublisher.KnownPathProbedOpcodeMessage" value="KnownPathProbed" />
 
                 <string id="RundownPublisher.MethodDCStartOpcodeMessage" value="DCStart" />
                 <string id="RundownPublisher.MethodDCEndOpcodeMessage" value="DCStop" />
index 4d27100..716345f 100644 (file)
@@ -1463,4 +1463,16 @@ void QCALLTYPE AssemblyNative::TraceAssemblyLoadFromResolveHandlerInvoked(LPCWST
     FireEtwAssemblyLoadFromResolveHandlerInvoked(GetClrInstanceId(), assemblyName, isTrackedAssembly, requestingAssemblyPath, requestedAssemblyPath);
 
     END_QCALL;
+}
+
+// static
+void QCALLTYPE AssemblyNative::TraceSatelliteSubdirectoryPathProbed(LPCWSTR filePath, HRESULT hr)
+{
+    QCALL_CONTRACT;
+
+    BEGIN_QCALL;
+
+    BinderTracing::PathProbed(filePath, BinderTracing::PathSource::SatelliteSubdirectory, hr);
+
+    END_QCALL;
 }
\ No newline at end of file
index 1d0aa0b..1d23f18 100644 (file)
@@ -136,6 +136,7 @@ public:
     static void QCALLTYPE TraceResolvingHandlerInvoked(LPCWSTR assemblyName, LPCWSTR handlerName, LPCWSTR alcName, LPCWSTR resultAssemblyName, LPCWSTR resultAssemblyPath);
     static void QCALLTYPE TraceAssemblyResolveHandlerInvoked(LPCWSTR assemblyName, LPCWSTR handlerName, LPCWSTR resultAssemblyName, LPCWSTR resultAssemblyPath);
     static void QCALLTYPE TraceAssemblyLoadFromResolveHandlerInvoked(LPCWSTR assemblyName, bool isTrackedAssembly, LPCWSTR requestingAssemblyPath, LPCWSTR requestedAssemblyPath);
+    static void QCALLTYPE TraceSatelliteSubdirectoryPathProbed(LPCWSTR filePath, HRESULT hr);
 };
 
 #endif
index b591f5c..aa7556a 100644 (file)
@@ -516,6 +516,7 @@ FCFuncStart(gAssemblyLoadContextFuncs)
     QCFuncElement("TraceResolvingHandlerInvoked", AssemblyNative::TraceResolvingHandlerInvoked)
     QCFuncElement("TraceAssemblyResolveHandlerInvoked", AssemblyNative::TraceAssemblyResolveHandlerInvoked)
     QCFuncElement("TraceAssemblyLoadFromResolveHandlerInvoked", AssemblyNative::TraceAssemblyLoadFromResolveHandlerInvoked)
+    QCFuncElement("TraceSatelliteSubdirectoryPathProbed", AssemblyNative::TraceSatelliteSubdirectoryPathProbed)
 FCFuncEnd()
 
 FCFuncStart(gAssemblyNameFuncs)
index c7ec026..cdf561c 100644 (file)
@@ -11,4 +11,7 @@
   <ItemGroup>
     <ProjectReference Include="AssemblyToLoadDependency.csproj" />
   </ItemGroup>
+  <ItemGroup>
+    <EmbeddedResource Include="Resource.fr-FR.resx" />
+  </ItemGroup>
 </Project>
index 5d77446..ba8251d 100644 (file)
@@ -36,12 +36,15 @@ namespace BinderTracingTests
         public List<HandlerInvocation> AppDomainAssemblyResolveHandlers { get; internal set; }
         public LoadFromHandlerInvocation AssemblyLoadFromHandler { get; internal set; }
 
+        public List<ProbedPath> ProbedPaths { get; internal set; }
+
         public List<BindOperation> NestedBinds { get; internal set; }
 
         public BindOperation()
         {
             AssemblyLoadContextResolvingHandlers = new List<HandlerInvocation>();
             AppDomainAssemblyResolveHandlers = new List<HandlerInvocation>();
+            ProbedPaths = new List<ProbedPath>();
             NestedBinds = new List<BindOperation>();
         }
 
@@ -85,6 +88,27 @@ namespace BinderTracingTests
         public string ComputedRequestedAssemblyPath { get; internal set; }
     }
 
+    internal class ProbedPath
+    {
+        public enum PathSource : ushort
+        {
+            ApplicationAssemblies,
+            AppNativeImagePaths,
+            AppPaths,
+            PlatformResourceRoots,
+            SatelliteSubdirectory
+        }
+
+        public string FilePath { get; internal set; }
+        public PathSource Source { get; internal set; }
+        public int Result { get; internal set; }
+
+        public override string ToString()
+        {
+            return $"{FilePath} - Source={Source}, Result={Result}";
+        }
+    }
+
     internal sealed class BinderEventListener : EventListener
     {
         private const EventKeywords TasksFlowActivityIds = (EventKeywords)0x80;
@@ -207,6 +231,17 @@ namespace BinderTracingTests
                     }
                     break;
                 }
+                case "KnownPathProbed":
+                {
+                    ProbedPath probedPath = ParseKnownPathProbedEvent(GetData, GetDataString);
+                    lock (eventsLock)
+                    {
+                        Assert.IsTrue(bindOperations.ContainsKey(data.ActivityId), $"{data.EventName} should have a matching AssemblyBindStart");
+                        BindOperation bind = bindOperations[data.ActivityId];
+                        bind.ProbedPaths.Add(probedPath);
+                    }
+                    break;
+                }
             }
         }
 
@@ -259,5 +294,16 @@ namespace BinderTracingTests
             };
             return loadFrom;
         }
+
+        private ProbedPath ParseKnownPathProbedEvent(Func<string, object> getData, Func<string, string> getDataString)
+        {
+            var probedPath = new ProbedPath()
+            {
+                FilePath = getDataString("FilePath"),
+                Source = (ProbedPath.PathSource)getData("Source"),
+                Result = (int)getData("Result"),
+            };
+            return probedPath;
+        }
     }
 }
diff --git a/src/coreclr/tests/src/Loader/binding/tracing/BinderTracingTest.DefaultProbing.cs b/src/coreclr/tests/src/Loader/binding/tracing/BinderTracingTest.DefaultProbing.cs
new file mode 100644 (file)
index 0000000..dcef9f7
--- /dev/null
@@ -0,0 +1,347 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Loader;
+using System.Runtime.InteropServices;
+
+using TestLibrary;
+
+namespace BinderTracingTests
+{
+    partial class BinderTracingTest
+    {
+        private static CultureInfo SatelliteCulture = CultureInfo.CreateSpecificCulture("fr-FR");
+
+        // Assembly found in app path:
+        //   KnownPathProbed : AppNativeImagePaths  (DLL)   [COR_E_FILENOTFOUND]
+        //   KnownPathProbed : AppNativeImagePaths  (EXE)   [COR_E_FILENOTFOUND]
+        //   KnownPathProbed : AppPaths             (DLL)   [S_OK]
+        // Note: corerun always sets APP_PATH and APP_NI_PATH. In regular use cases,
+        // the customer would have to explicitly configure the app to set those.
+        [BinderTest]
+        public static BindOperation AssemblyInAppPath()
+        {
+            AssemblyName assemblyName = new AssemblyName(DependentAssemblyName);
+            CustomALC alc = new CustomALC(nameof(AssemblyInAppPath));
+            Assembly asm = alc.LoadFromAssemblyName(assemblyName);
+
+            return new BindOperation()
+            {
+                AssemblyName = assemblyName,
+                AssemblyLoadContext = alc.ToString(),
+                Success = true,
+                ResultAssemblyName = asm.GetName(),
+                ResultAssemblyPath = asm.Location,
+                Cached = false,
+                ProbedPaths = new List<ProbedPath>()
+                {
+                    new ProbedPath()
+                    {
+                        FilePath = Helpers.GetProbingFilePath(ProbedPath.PathSource.AppNativeImagePaths, assemblyName.Name, isExe: false),
+                        Source = ProbedPath.PathSource.AppNativeImagePaths,
+                        Result = COR_E_FILENOTFOUND
+                    },
+                    new ProbedPath()
+                    {
+                        FilePath = Helpers.GetProbingFilePath(ProbedPath.PathSource.AppNativeImagePaths, assemblyName.Name, isExe: true),
+                        Source = ProbedPath.PathSource.AppNativeImagePaths,
+                        Result = COR_E_FILENOTFOUND
+                    },
+                    new ProbedPath()
+                    {
+                        FilePath = Helpers.GetProbingFilePath(ProbedPath.PathSource.AppPaths, assemblyName.Name, isExe: false),
+                        Source = ProbedPath.PathSource.AppPaths,
+                        Result = S_OK
+                    }
+                }
+            };
+        }
+
+        // Assembly not found:
+        //   KnownPathProbed : AppNativeImagePaths  (DLL)   [COR_E_FILENOTFOUND]
+        //   KnownPathProbed : AppNativeImagePaths  (EXE)   [COR_E_FILENOTFOUND]
+        //   KnownPathProbed : AppPaths             (DLL)   [COR_E_FILENOTFOUND]
+        //   KnownPathProbed : AppPaths             (EXE)   [COR_E_FILENOTFOUND]
+        // Note: corerun always sets APP_PATH and APP_NI_PATH. In regular use cases,
+        // the customer would have to explicitly configure the app to set those.
+        [BinderTest]
+        public static BindOperation NonExistentAssembly()
+        {
+            string assemblyName = "DoesNotExist";
+            try
+            {
+                Assembly.Load(assemblyName);
+            }
+            catch { }
+
+            return new BindOperation()
+            {
+                AssemblyName = new AssemblyName(assemblyName),
+                AssemblyLoadContext = DefaultALC,
+                RequestingAssembly = Assembly.GetExecutingAssembly().GetName(),
+                RequestingAssemblyLoadContext = DefaultALC,
+                Success = false,
+                Cached = false,
+                ProbedPaths = new List<ProbedPath>()
+                {
+                    new ProbedPath()
+                    {
+                        FilePath = Helpers.GetProbingFilePath(ProbedPath.PathSource.AppNativeImagePaths, assemblyName, isExe: false),
+                        Source = ProbedPath.PathSource.AppNativeImagePaths,
+                        Result = COR_E_FILENOTFOUND
+                    },
+                    new ProbedPath()
+                    {
+                        FilePath = Helpers.GetProbingFilePath(ProbedPath.PathSource.AppNativeImagePaths, assemblyName, isExe: true),
+                        Source = ProbedPath.PathSource.AppNativeImagePaths,
+                        Result = COR_E_FILENOTFOUND
+                    },
+                    new ProbedPath()
+                    {
+                        FilePath = Helpers.GetProbingFilePath(ProbedPath.PathSource.AppPaths, assemblyName, isExe: false),
+                        Source = ProbedPath.PathSource.AppPaths,
+                        Result = COR_E_FILENOTFOUND
+                    },
+                    new ProbedPath()
+                    {
+                        FilePath = Helpers.GetProbingFilePath(ProbedPath.PathSource.AppPaths, assemblyName, isExe: true),
+                        Source = ProbedPath.PathSource.AppPaths,
+                        Result = COR_E_FILENOTFOUND
+                    }
+                }
+            };
+        }
+
+        // Satellite assembly found in app path:
+        //   KnownPathProbed : AppPaths             (DLL)   [S_OK]
+        // Note: corerun always sets APP_PATH and APP_NI_PATH. In regular use cases,
+        // the customer would have to explicitly configure the app to set those.
+        [BinderTest(isolate: true)]
+        public static BindOperation SatelliteAssembly_AppPath()
+        {
+            AssemblyName assemblyName = new AssemblyName($"{DependentAssemblyName}.resources");
+            assemblyName.CultureInfo = SatelliteCulture;
+            Assembly asm = AssemblyLoadContext.Default.LoadFromAssemblyName(assemblyName);
+
+            return new BindOperation()
+            {
+                AssemblyName = assemblyName,
+                AssemblyLoadContext = DefaultALC,
+                Success = true,
+                ResultAssemblyName = asm.GetName(),
+                ResultAssemblyPath = asm.Location,
+                Cached = false,
+                ProbedPaths = new List<ProbedPath>()
+                {
+                    new ProbedPath()
+                    {
+                        FilePath = Helpers.GetProbingFilePath(ProbedPath.PathSource.AppPaths, assemblyName.Name, SatelliteCulture.Name),
+                        Source = ProbedPath.PathSource.AppPaths,
+                        Result = S_OK
+                    }
+                }
+            };
+        }
+
+        // Satellite assembly found in culture subdirectory (custom ALC):
+        //   KnownPathProbed : SatelliteSubdirectory    [S_OK]
+        [BinderTest]
+        public static BindOperation SatelliteAssembly_CultureSubdirectory()
+        {
+            AssemblyName assemblyName = new AssemblyName($"{SubdirectoryAssemblyName}.resources");
+            assemblyName.CultureInfo = SatelliteCulture;
+            CustomALC alc = new CustomALC(nameof(SatelliteAssembly_CultureSubdirectory));
+            alc.LoadFromAssemblyPath(Helpers.GetAssemblyInSubdirectoryPath(SubdirectoryAssemblyName));
+            Assembly asm = alc.LoadFromAssemblyName(assemblyName);
+
+            return new BindOperation()
+            {
+                AssemblyName = assemblyName,
+                AssemblyLoadContext = alc.ToString(),
+                Success = true,
+                ResultAssemblyName = asm.GetName(),
+                ResultAssemblyPath = asm.Location,
+                Cached = false,
+                ProbedPaths = new List<ProbedPath>()
+                {
+                    new ProbedPath()
+                    {
+                        FilePath = Helpers.GetProbingFilePath(ProbedPath.PathSource.SatelliteSubdirectory, assemblyName.Name, SatelliteCulture.Name, Helpers.GetSubdirectoryPath()),
+                        Source = ProbedPath.PathSource.SatelliteSubdirectory,
+                        Result = S_OK
+                    }
+                }
+            };
+        }
+
+        // Satellite assembly found in culture subdirectory (default ALC):
+        //   KnownPathProbed : AppPaths                 [COR_E_FILENOTFOUND]
+        //   KnownPathProbed : SatelliteSubdirectory    [S_OK]
+        // Note: corerun always sets APP_PATH and APP_NI_PATH. In regular use cases,
+        // the customer would have to explicitly configure the app to set those.
+        [BinderTest(isolate: true)]
+        public static BindOperation SatelliteAssembly_CultureSubdirectory_DefaultALC()
+        {
+            AssemblyName assemblyName = new AssemblyName($"{SubdirectoryAssemblyName}.resources");
+            assemblyName.CultureInfo = SatelliteCulture;
+
+            // https://github.com/dotnet/corefx/issues/42477
+            _ = AssemblyLoadContext.Default;
+
+            Assembly OnAppDomainAssemblyResolve(object sender, ResolveEventArgs args)
+            {
+                AssemblyName requested = new AssemblyName(args.Name);
+                return requested.Name == SubdirectoryAssemblyName
+                    ? Assembly.LoadFile(Helpers.GetAssemblyInSubdirectoryPath(SubdirectoryAssemblyName))
+                    : null;
+            };
+
+            AppDomain.CurrentDomain.AssemblyResolve += OnAppDomainAssemblyResolve;
+            Assembly asm = Assembly.Load(assemblyName);
+            AppDomain.CurrentDomain.AssemblyResolve -= OnAppDomainAssemblyResolve;
+
+            return new BindOperation()
+            {
+                AssemblyName = assemblyName,
+                AssemblyLoadContext = DefaultALC,
+                RequestingAssembly = Assembly.GetExecutingAssembly().GetName(),
+                RequestingAssemblyLoadContext = DefaultALC,
+                Success = true,
+                ResultAssemblyName = asm.GetName(),
+                ResultAssemblyPath = asm.Location,
+                Cached = false,
+                ProbedPaths = new List<ProbedPath>()
+                {
+                    new ProbedPath()
+                    {
+                        FilePath = Helpers.GetProbingFilePath(ProbedPath.PathSource.AppPaths, assemblyName.Name, SatelliteCulture.Name),
+                        Source = ProbedPath.PathSource.AppPaths,
+                        Result = COR_E_FILENOTFOUND
+                    },
+                    new ProbedPath()
+                    {
+                        FilePath = Helpers.GetProbingFilePath(ProbedPath.PathSource.SatelliteSubdirectory, assemblyName.Name, SatelliteCulture.Name, Helpers.GetSubdirectoryPath()),
+                        Source = ProbedPath.PathSource.SatelliteSubdirectory,
+                        Result = S_OK
+                    }
+                }
+            };
+        }
+
+        // Satellite assembly found in lower-case culture subdirectory
+        // On non-Linux:
+        //   KnownPathProbed : SatelliteSubdirectory    (exact case)    [S_OK]
+        // On Linux (case-sensitive):
+        //   KnownPathProbed : SatelliteSubdirectory    (exact case)    [COR_E_FILENOTFOUND]
+        //   KnownPathProbed : SatelliteSubdirectory    (lower-case)    [S_OK]
+        [BinderTest]
+        public static BindOperation SatelliteAssembly_CultureSubdirectory_Lowercase()
+        {
+            AssemblyName assemblyName = new AssemblyName($"{SubdirectoryAssemblyName}.resources");
+            assemblyName.CultureInfo = SatelliteCulture;
+            CustomALC alc = new CustomALC(nameof(SatelliteAssembly_CultureSubdirectory));
+            alc.LoadFromAssemblyPath(Helpers.GetAssemblyInSubdirectoryPath(SubdirectoryAssemblyName));
+
+            Assembly asm;
+            string subdirectoryPath = Helpers.GetSubdirectoryPath();
+            string cultureSubdirectory = Path.Combine(subdirectoryPath, SatelliteCulture.Name);
+            string cultureSubdirectoryLower = Path.Combine(subdirectoryPath, SatelliteCulture.Name.ToLowerInvariant());
+            try
+            {
+                Directory.Move(cultureSubdirectory, cultureSubdirectoryLower);
+                asm = alc.LoadFromAssemblyName(assemblyName);
+            }
+            finally
+            {
+                Directory.Move(cultureSubdirectoryLower, cultureSubdirectory);
+            }
+
+            var probedPaths = new List<ProbedPath>()
+            {
+                new ProbedPath()
+                {
+                    FilePath = Helpers.GetProbingFilePath(ProbedPath.PathSource.SatelliteSubdirectory, assemblyName.Name, SatelliteCulture.Name, subdirectoryPath),
+                    Source = ProbedPath.PathSource.SatelliteSubdirectory,
+                    Result = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? COR_E_FILENOTFOUND : S_OK
+                }
+            };
+
+            // On Linux, the path with a lower-case culture name should also be probed
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+            {
+                probedPaths.Add(new ProbedPath()
+                    {
+                        FilePath = Helpers.GetProbingFilePath(ProbedPath.PathSource.SatelliteSubdirectory, assemblyName.Name, SatelliteCulture.Name.ToLowerInvariant(), subdirectoryPath),
+                        Source = ProbedPath.PathSource.SatelliteSubdirectory,
+                        Result = S_OK
+                    });
+            }
+
+            return new BindOperation()
+            {
+                AssemblyName = assemblyName,
+                AssemblyLoadContext = alc.ToString(),
+                Success = true,
+                ResultAssemblyName = asm.GetName(),
+                ResultAssemblyPath = asm.Location,
+                Cached = false,
+                ProbedPaths = probedPaths
+            };
+        }
+
+        // Satellite assembly not found
+        // On non-Linux:
+        //   KnownPathProbed : SatelliteSubdirectory    (exact case)    [COR_E_FILENOTFOUND]
+        // On Linux (case-sensitive):
+        //   KnownPathProbed : SatelliteSubdirectory    (exact case)    [COR_E_FILENOTFOUND]
+        //   KnownPathProbed : SatelliteSubdirectory    (lower-case)    [COR_E_FILENOTFOUND]
+        [BinderTest]
+        public static BindOperation SatelliteAssembly_NotFound()
+        {
+            string cultureName = "en-GB";
+            AssemblyName assemblyName = new AssemblyName($"{SubdirectoryAssemblyName}.resources");
+            assemblyName.CultureInfo = new CultureInfo(cultureName);
+            CustomALC alc = new CustomALC(nameof(SatelliteAssembly_CultureSubdirectory));
+            alc.LoadFromAssemblyPath(Helpers.GetAssemblyInSubdirectoryPath(SubdirectoryAssemblyName));
+            Assert.Throws<FileNotFoundException>(() => alc.LoadFromAssemblyName(assemblyName));
+
+            var probedPaths = new List<ProbedPath>()
+            {
+                new ProbedPath()
+                {
+                    FilePath = Helpers.GetProbingFilePath(ProbedPath.PathSource.SatelliteSubdirectory, assemblyName.Name, cultureName, Helpers.GetSubdirectoryPath()),
+                    Source = ProbedPath.PathSource.SatelliteSubdirectory,
+                    Result = COR_E_FILENOTFOUND
+                }
+            };
+
+            // On Linux (case-sensitive), the path with a lower-case culture name should also be probed
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+            {
+                probedPaths.Add(new ProbedPath()
+                    {
+                        FilePath = Helpers.GetProbingFilePath(ProbedPath.PathSource.SatelliteSubdirectory, assemblyName.Name, cultureName.ToLowerInvariant(), Helpers.GetSubdirectoryPath()),
+                        Source = ProbedPath.PathSource.SatelliteSubdirectory,
+                        Result = COR_E_FILENOTFOUND
+                    });
+            }
+
+            return new BindOperation()
+            {
+                AssemblyName = assemblyName,
+                AssemblyLoadContext = alc.ToString(),
+                Success = false,
+                Cached = false,
+                ProbedPaths = probedPaths
+            };
+        }
+    }
+}
index 0eaefef..9896ef4 100644 (file)
@@ -232,7 +232,7 @@ namespace BinderTracingTests
         [BinderTest(isolate: true)]
         public static BindOperation AssemblyLoadFromResolveHandler_LoadDependency()
         {
-            string assemblyPath = GetAssemblyInSubdirectoryPath(SubdirectoryAssemblyName);
+            string assemblyPath = Helpers.GetAssemblyInSubdirectoryPath(SubdirectoryAssemblyName);
             Assembly asm = Assembly.LoadFrom(assemblyPath);
             Type t = asm.GetType(DependentAssemblyTypeName);
             MethodInfo method = t.GetMethod("UseDependentAssembly", BindingFlags.Public | BindingFlags.Static);
@@ -343,12 +343,6 @@ namespace BinderTracingTests
             };
         }
 
-        private static string GetAssemblyInSubdirectoryPath(string assemblyName)
-        {
-            string appPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
-            return Path.Combine(appPath, "DependentAssemblies", $"{assemblyName}.dll");
-        }
-
         private enum HandlerReturn
         {
             Null,
@@ -429,7 +423,7 @@ namespace BinderTracingTests
                     return null;
 
                 string name = handlerReturn == HandlerReturn.RequestedAssembly ? assemblyName.Name : $"{assemblyName.Name}Mismatch";
-                string assemblyPath = GetAssemblyInSubdirectoryPath(name);
+                string assemblyPath = Helpers.GetAssemblyInSubdirectoryPath(name);
 
                 if (!File.Exists(assemblyPath))
                     return null;
index 1af3333..2a19f5d 100644 (file)
@@ -39,6 +39,9 @@ namespace BinderTracingTests
         private const string DependentAssemblyTypeName = "AssemblyToLoad.Program";
         private const string SubdirectoryAssemblyName = "AssemblyToLoad_Subdirectory";
 
+        private const int S_OK = unchecked((int)0);
+        private const int COR_E_FILENOTFOUND = unchecked((int)0x80070002);
+
         private static readonly AssemblyName CoreLibName = typeof(object).Assembly.GetName();
 
         [BinderTest]
@@ -176,7 +179,16 @@ namespace BinderTracingTests
                 Success = true,
                 ResultAssemblyName = asm.GetName(),
                 ResultAssemblyPath = asm.Location,
-                Cached = false
+                Cached = false,
+                ProbedPaths = new List<ProbedPath>()
+                {
+                    new ProbedPath()
+                    {
+                        FilePath = asm.Location,
+                        Source = ProbedPath.PathSource.ApplicationAssemblies,
+                        Result = S_OK
+                    }
+                }
             };
         }
 
@@ -185,30 +197,10 @@ namespace BinderTracingTests
         {
             BindOperation bind = PlatformAssembly();
             bind.Cached = true;
+            bind.ProbedPaths.Clear();
             return bind;
         }
 
-        [BinderTest]
-        public static BindOperation NonExistentAssembly()
-        {
-            string assemblyName = "DoesNotExist";
-            try
-            {
-                Assembly.Load(assemblyName);
-            }
-            catch { }
-
-            return new BindOperation()
-            {
-                AssemblyName = new AssemblyName(assemblyName),
-                AssemblyLoadContext = DefaultALC,
-                RequestingAssembly = Assembly.GetExecutingAssembly().GetName(),
-                RequestingAssemblyLoadContext = DefaultALC,
-                Success = false,
-                Cached = false
-            };
-        }
-
         [BinderTest(isolate: true)]
         public static BindOperation Reflection()
         {
@@ -471,96 +463,7 @@ namespace BinderTracingTests
             Assert.IsTrue(binds.Length == 1, $"Bind event count for {assemblyName} - expected: 1, actual: {binds.Length}");
             BindOperation actual = binds[0];
 
-            ValidateBindOperation(expected, actual);
-        }
-
-        private static void ValidateBindOperation(BindOperation expected, BindOperation actual)
-        {
-            ValidateAssemblyName(expected.AssemblyName, actual.AssemblyName, nameof(BindOperation.AssemblyName));
-            Assert.AreEqual(expected.AssemblyPath ?? string.Empty, actual.AssemblyPath, $"Unexpected value for {nameof(BindOperation.AssemblyPath)} on event");
-            Assert.AreEqual(expected.AssemblyLoadContext, actual.AssemblyLoadContext, $"Unexpected value for {nameof(BindOperation.AssemblyLoadContext)} on event");
-            Assert.AreEqual(expected.RequestingAssemblyLoadContext ?? string.Empty, actual.RequestingAssemblyLoadContext, $"Unexpected value for {nameof(BindOperation.RequestingAssemblyLoadContext)} on event");
-            ValidateAssemblyName(expected.RequestingAssembly, actual.RequestingAssembly, nameof(BindOperation.RequestingAssembly));
-
-            Assert.AreEqual(expected.Success, actual.Success, $"Unexpected value for {nameof(BindOperation.Success)} on event");
-            Assert.AreEqual(expected.ResultAssemblyPath ?? string.Empty, actual.ResultAssemblyPath, $"Unexpected value for {nameof(BindOperation.ResultAssemblyPath)} on event");
-            Assert.AreEqual(expected.Cached, actual.Cached, $"Unexpected value for {nameof(BindOperation.Cached)} on event");
-            ValidateAssemblyName(expected.ResultAssemblyName, actual.ResultAssemblyName, nameof(BindOperation.ResultAssemblyName));
-
-            ValidateHandlerInvocations(expected.AssemblyLoadContextResolvingHandlers, actual.AssemblyLoadContextResolvingHandlers, "AssemblyLoadContextResolving");
-            ValidateHandlerInvocations(expected.AppDomainAssemblyResolveHandlers, actual.AppDomainAssemblyResolveHandlers, "AppDomainAssemblyResolve");
-            ValidateLoadFromHandlerInvocation(expected.AssemblyLoadFromHandler, actual.AssemblyLoadFromHandler);
-
-            ValidateNestedBinds(expected.NestedBinds, actual.NestedBinds);
-        }
-
-        private static bool AssemblyNamesMatch(AssemblyName name1, AssemblyName name2)
-        {
-            if (name1 == null || name2 == null)
-                return name1 == null && name2 == null;
-
-            return name1.Name == name2.Name
-                && ((name1.Version == null && name2.Version == null) || name1.Version == name2.Version)
-                && ((string.IsNullOrEmpty(name1.CultureName) && string.IsNullOrEmpty(name1.CultureName)) || name1.CultureName == name2.CultureName);
-        }
-
-        private static void ValidateAssemblyName(AssemblyName expected, AssemblyName actual, string propertyName)
-        {
-            Assert.IsTrue(AssemblyNamesMatch(expected, actual), $"Unexpected value for {propertyName} on event - expected: {expected}, actual: {actual}");
-        }
-
-        private static void ValidateHandlerInvocations(List<HandlerInvocation> expected, List<HandlerInvocation> actual, string eventName)
-        {
-            Assert.AreEqual(expected.Count, actual.Count, $"Unexpected handler invocation count for {eventName}");
-
-            foreach (var match in expected)
-            {
-                Predicate<HandlerInvocation> pred = h =>
-                    AssemblyNamesMatch(h.AssemblyName, match.AssemblyName)
-                        && h.HandlerName == match.HandlerName
-                        && h.AssemblyLoadContext == match.AssemblyLoadContext
-                        && AssemblyNamesMatch(h.ResultAssemblyName, match.ResultAssemblyName)
-                        && h.ResultAssemblyPath == match.ResultAssemblyPath;
-                Assert.IsTrue(actual.Exists(pred), $"Handler invocation not found: {match.ToString()}");
-            }
-        }
-
-        private static void ValidateLoadFromHandlerInvocation(LoadFromHandlerInvocation expected, LoadFromHandlerInvocation actual)
-        {
-            if (expected == null || actual == null)
-            {
-                Assert.IsNull(expected);
-                Assert.IsNull(actual);
-                return;
-            }
-
-            ValidateAssemblyName(expected.AssemblyName, actual.AssemblyName, nameof(LoadFromHandlerInvocation.AssemblyName));
-            Assert.AreEqual(expected.IsTrackedLoad, actual.IsTrackedLoad, $"Unexpected value for {nameof(LoadFromHandlerInvocation.IsTrackedLoad)} on event");
-            Assert.AreEqual(expected.RequestingAssemblyPath, actual.RequestingAssemblyPath, $"Unexpected value for {nameof(LoadFromHandlerInvocation.RequestingAssemblyPath)} on event");
-            Assert.AreEqual(expected.ComputedRequestedAssemblyPath, actual.ComputedRequestedAssemblyPath, $"Unexpected value for {nameof(LoadFromHandlerInvocation.ComputedRequestedAssemblyPath)} on event");
-        }
-
-        private static bool BindOperationsMatch(BindOperation bind1, BindOperation bind2)
-        {
-            try
-            {
-                ValidateBindOperation(bind1, bind2);
-            }
-            catch (AssertTestException e)
-            {
-                return false;
-            }
-
-            return true;
-        }
-
-        private static void ValidateNestedBinds(List<BindOperation> expected, List<BindOperation> actual)
-        {
-            foreach (var match in expected)
-            {
-                Predicate<BindOperation> pred = b => BindOperationsMatch(match, b);
-                Assert.IsTrue(actual.Exists(pred), $"Nested bind operation not found: {match.ToString()}");
-            }
+            Helpers.ValidateBindOperation(expected, actual);
         }
     }
 }
index ef59ad4..e1f3ece 100644 (file)
@@ -4,8 +4,10 @@
   </PropertyGroup>
   <ItemGroup>
     <Compile Include="BinderTracingTest.cs" />
+    <Compile Include="BinderTracingTest.DefaultProbing.cs" />
     <Compile Include="BinderTracingTest.EventHandlers.cs" />
     <Compile Include="BinderEventListener.cs" />
+    <Compile Include="Helpers.cs" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="$(SourceDir)Common/CoreCLRTestLibrary/CoreCLRTestLibrary.csproj" />
@@ -19,6 +21,7 @@
       <ReferenceOutputAssembly>false</ReferenceOutputAssembly>
       <OutputItemType>Content</OutputItemType>
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+      <Targets>Build;SatelliteDllsProjectOutputGroup</Targets>
     </ProjectReference>
     <ProjectReference Include="AssemblyToLoad.csproj">
       <Properties>AssemblyNameSuffix=SubdirectoryMismatch</Properties>
   </ItemGroup>
 
   <Target Name="MoveDependentAssembliesToSubdirectory" AfterTargets="CopyFilesToOutputDirectory">
+    <PropertyGroup>
+      <AssemblyToLoadResourceCulture>fr-FR</AssemblyToLoadResourceCulture>
+    </PropertyGroup>
     <ItemGroup>
-      <AssembliesToMove Include="$(OutDir)/AssemblyToLoad_*.*" />
+      <SatelliteAssembliesToMove Include="$(OutDir)/AssemblyToLoad_*.resources.*" />
+      <AssembliesToMove Include="$(OutDir)/AssemblyToLoad_*.*" Exclude="@(SatelliteAssembliesToMove)" />
       <AssembliesToMove Include="$(OutDir)/AssemblyToLoadDependency.*" />
     </ItemGroup>
     <Move SourceFiles="@(AssembliesToMove)" DestinationFolder="$(OutDir)/DependentAssemblies" />
+    <Move SourceFiles="@(SatelliteAssembliesToMove)" DestinationFolder="$(OutDir)/DependentAssemblies/$(AssemblyToLoadResourceCulture)" />
   </Target>
 </Project>
diff --git a/src/coreclr/tests/src/Loader/binding/tracing/Helpers.cs b/src/coreclr/tests/src/Loader/binding/tracing/Helpers.cs
new file mode 100644 (file)
index 0000000..585631f
--- /dev/null
@@ -0,0 +1,161 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Loader;
+
+using TestLibrary;
+
+namespace BinderTracingTests
+{
+    internal class Helpers
+    {
+        public static void ValidateBindOperation(BindOperation expected, BindOperation actual)
+        {
+            ValidateAssemblyName(expected.AssemblyName, actual.AssemblyName, nameof(BindOperation.AssemblyName));
+            Assert.AreEqual(expected.AssemblyPath ?? string.Empty, actual.AssemblyPath, $"Unexpected value for {nameof(BindOperation.AssemblyPath)} on event");
+            Assert.AreEqual(expected.AssemblyLoadContext, actual.AssemblyLoadContext, $"Unexpected value for {nameof(BindOperation.AssemblyLoadContext)} on event");
+            Assert.AreEqual(expected.RequestingAssemblyLoadContext ?? string.Empty, actual.RequestingAssemblyLoadContext, $"Unexpected value for {nameof(BindOperation.RequestingAssemblyLoadContext)} on event");
+            ValidateAssemblyName(expected.RequestingAssembly, actual.RequestingAssembly, nameof(BindOperation.RequestingAssembly));
+
+            Assert.AreEqual(expected.Success, actual.Success, $"Unexpected value for {nameof(BindOperation.Success)} on event");
+            Assert.AreEqual(expected.ResultAssemblyPath ?? string.Empty, actual.ResultAssemblyPath, $"Unexpected value for {nameof(BindOperation.ResultAssemblyPath)} on event");
+            Assert.AreEqual(expected.Cached, actual.Cached, $"Unexpected value for {nameof(BindOperation.Cached)} on event");
+            ValidateAssemblyName(expected.ResultAssemblyName, actual.ResultAssemblyName, nameof(BindOperation.ResultAssemblyName));
+
+            ValidateHandlerInvocations(expected.AssemblyLoadContextResolvingHandlers, actual.AssemblyLoadContextResolvingHandlers, "AssemblyLoadContextResolving");
+            ValidateHandlerInvocations(expected.AppDomainAssemblyResolveHandlers, actual.AppDomainAssemblyResolveHandlers, "AppDomainAssemblyResolve");
+            ValidateLoadFromHandlerInvocation(expected.AssemblyLoadFromHandler, actual.AssemblyLoadFromHandler);
+
+            ValidateProbedPaths(expected.ProbedPaths, actual.ProbedPaths);
+
+            ValidateNestedBinds(expected.NestedBinds, actual.NestedBinds);
+        }
+
+        public static string GetAssemblyInSubdirectoryPath(string assemblyName)
+        {
+            return Path.Combine(GetSubdirectoryPath(), $"{assemblyName}.dll");
+        }
+
+        public static string GetSubdirectoryPath()
+        {
+            string appPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
+            return Path.Combine(appPath, "DependentAssemblies");
+        }
+
+        public static string GetProbingFilePath(ProbedPath.PathSource pathSource, string assemblyName, string culture, string baseAssemblyDirectory = null)
+        {
+            return GetProbingFilePath(pathSource, assemblyName, false, culture, baseAssemblyDirectory);
+        }
+
+        public static string GetProbingFilePath(ProbedPath.PathSource pathSource, string assemblyName, bool isExe)
+        {
+            return GetProbingFilePath(pathSource, assemblyName, isExe, null, null);
+        }
+
+        private static string GetProbingFilePath(ProbedPath.PathSource pathSource, string assemblyName, bool isExe, string culture, string baseAssemblyDirectory)
+        {
+            string baseDirectory = baseAssemblyDirectory ?? Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
+            if (!string.IsNullOrEmpty(culture))
+                baseDirectory = Path.Combine(baseDirectory, culture);
+
+            string extension = isExe ? "exe" : "dll";
+            switch (pathSource)
+            {
+                case ProbedPath.PathSource.AppNativeImagePaths:
+                    return Path.Combine(baseDirectory, $"{assemblyName}.ni.{extension}");
+                case ProbedPath.PathSource.AppPaths:
+                case ProbedPath.PathSource.SatelliteSubdirectory:
+                    return Path.Combine(baseDirectory, $"{assemblyName}.{extension}");
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(pathSource));
+            }
+        }
+
+        private static bool AssemblyNamesMatch(AssemblyName name1, AssemblyName name2)
+        {
+            if (name1 == null || name2 == null)
+                return name1 == null && name2 == null;
+
+            return name1.Name == name2.Name
+                && ((name1.Version == null && name2.Version == null) || name1.Version == name2.Version)
+                && ((string.IsNullOrEmpty(name1.CultureName) && string.IsNullOrEmpty(name2.CultureName)) || name1.CultureName == name2.CultureName);
+        }
+
+        private static void ValidateAssemblyName(AssemblyName expected, AssemblyName actual, string propertyName)
+        {
+            Assert.IsTrue(AssemblyNamesMatch(expected, actual), $"Unexpected value for {propertyName} on event - expected: {expected}, actual: {actual}");
+        }
+
+        private static void ValidateHandlerInvocations(List<HandlerInvocation> expected, List<HandlerInvocation> actual, string eventName)
+        {
+            Assert.AreEqual(expected.Count, actual.Count, $"Unexpected handler invocation count for {eventName}");
+
+            foreach (var match in expected)
+            {
+                Predicate<HandlerInvocation> pred = h =>
+                    AssemblyNamesMatch(h.AssemblyName, match.AssemblyName)
+                        && h.HandlerName == match.HandlerName
+                        && h.AssemblyLoadContext == match.AssemblyLoadContext
+                        && AssemblyNamesMatch(h.ResultAssemblyName, match.ResultAssemblyName)
+                        && h.ResultAssemblyPath == match.ResultAssemblyPath;
+                Assert.IsTrue(actual.Exists(pred), $"Handler invocation not found: {match.ToString()}");
+            }
+        }
+
+        private static void ValidateLoadFromHandlerInvocation(LoadFromHandlerInvocation expected, LoadFromHandlerInvocation actual)
+        {
+            if (expected == null || actual == null)
+            {
+                Assert.IsNull(expected);
+                Assert.IsNull(actual);
+                return;
+            }
+
+            ValidateAssemblyName(expected.AssemblyName, actual.AssemblyName, nameof(LoadFromHandlerInvocation.AssemblyName));
+            Assert.AreEqual(expected.IsTrackedLoad, actual.IsTrackedLoad, $"Unexpected value for {nameof(LoadFromHandlerInvocation.IsTrackedLoad)} on event");
+            Assert.AreEqual(expected.RequestingAssemblyPath, actual.RequestingAssemblyPath, $"Unexpected value for {nameof(LoadFromHandlerInvocation.RequestingAssemblyPath)} on event");
+            Assert.AreEqual(expected.ComputedRequestedAssemblyPath, actual.ComputedRequestedAssemblyPath, $"Unexpected value for {nameof(LoadFromHandlerInvocation.ComputedRequestedAssemblyPath)} on event");
+        }
+
+        private static void ValidateProbedPaths(List<ProbedPath> expected, List<ProbedPath> actual)
+        {
+            foreach (var match in expected)
+            {
+                Predicate<ProbedPath> pred = p => p.FilePath == match.FilePath
+                    && p.Source == match.Source
+                    && p.Result == match.Result;
+                Assert.IsTrue(actual.Exists(pred), $"Probed path not found: {match.ToString()}");
+            }
+        }
+
+        private static bool BindOperationsMatch(BindOperation bind1, BindOperation bind2)
+        {
+            try
+            {
+                ValidateBindOperation(bind1, bind2);
+            }
+            catch (AssertTestException e)
+            {
+                return false;
+            }
+
+            return true;
+        }
+
+        private static void ValidateNestedBinds(List<BindOperation> expected, List<BindOperation> actual)
+        {
+            foreach (var match in expected)
+            {
+                Predicate<BindOperation> pred = b => BindOperationsMatch(match, b);
+                Assert.IsTrue(actual.Exists(pred), $"Nested bind operation not found: {match.ToString()}");
+            }
+        }
+    }
+}
diff --git a/src/coreclr/tests/src/Loader/binding/tracing/Resource.fr-FR.resx b/src/coreclr/tests/src/Loader/binding/tracing/Resource.fr-FR.resx
new file mode 100644 (file)
index 0000000..f59158e
--- /dev/null
@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+  <!--
+    Microsoft ResX Schema
+
+    Version 2.0
+
+    The primary goals of this format is to allow a simple XML format
+    that is mostly human readable. The generation and parsing of the
+    various data types are done through the TypeConverter classes
+    associated with the data types.
+
+    Example:
+
+    ... ado.net/XML headers & schema ...
+    <resheader name="resmimetype">text/microsoft-resx</resheader>
+    <resheader name="version">2.0</resheader>
+    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+        <value>[base64 mime encoded serialized .NET Framework object]</value>
+    </data>
+    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+        <comment>This is a comment</comment>
+    </data>
+
+    There are any number of "resheader" rows that contain simple
+    name/value pairs.
+
+    Each data row contains a name, and value. The row also contains a
+    type or mimetype. Type corresponds to a .NET class that support
+    text/value conversion through the TypeConverter architecture.
+    Classes that don't support this are serialized and stored with the
+    mimetype set.
+
+    The mimetype is used for serialized objects, and tells the
+    ResXResourceReader how to depersist the object. This is currently not
+    extensible. For a given mimetype the value must be set accordingly:
+
+    Note - application/x-microsoft.net.object.binary.base64 is the format
+    that the ResXResourceWriter will generate, however the reader can
+    read any of the formats listed below.
+
+    mimetype: application/x-microsoft.net.object.binary.base64
+    value   : The object must be serialized with
+            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+            : and then encoded with base64 encoding.
+
+    mimetype: application/x-microsoft.net.object.soap.base64
+    value   : The object must be serialized with
+            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+            : and then encoded with base64 encoding.
+
+    mimetype: application/x-microsoft.net.object.bytearray.base64
+    value   : The object must be serialized into a byte array
+            : using a System.ComponentModel.TypeConverter
+            : and then encoded with base64 encoding.
+    -->
+  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+    <xsd:element name="root" msdata:IsDataSet="true">
+      <xsd:complexType>
+        <xsd:choice maxOccurs="unbounded">
+          <xsd:element name="metadata">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" />
+              </xsd:sequence>
+              <xsd:attribute name="name" use="required" type="xsd:string" />
+              <xsd:attribute name="type" type="xsd:string" />
+              <xsd:attribute name="mimetype" type="xsd:string" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="assembly">
+            <xsd:complexType>
+              <xsd:attribute name="alias" type="xsd:string" />
+              <xsd:attribute name="name" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="data">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="resheader">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" />
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:choice>
+      </xsd:complexType>
+    </xsd:element>
+  </xsd:schema>
+  <resheader name="resmimetype">
+    <value>text/microsoft-resx</value>
+  </resheader>
+  <resheader name="version">
+    <value>2.0</value>
+  </resheader>
+  <resheader name="reader">
+    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <resheader name="writer">
+    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <data name="Message" xml:space="preserve">
+    <value>FR-FR</value>
+  </data>
+</root>
index 740239e..152b5f2 100644 (file)
@@ -743,21 +743,28 @@ namespace System.Runtime.Loader
 
             string assemblyPath = Path.Combine(parentDirectory, assemblyName.CultureName!, $"{assemblyName.Name}.dll");
 
-            if (Internal.IO.File.InternalExists(assemblyPath))
+            bool exists = Internal.IO.File.InternalExists(assemblyPath);
+            if (!exists && Path.IsCaseSensitive)
             {
-                return parentALC.LoadFromAssemblyPath(assemblyPath);
-            }
-            else if (Path.IsCaseSensitive)
-            {
-                assemblyPath = Path.Combine(parentDirectory, assemblyName.CultureName!.ToLowerInvariant(), $"{assemblyName.Name}.dll");
-
-                if (Internal.IO.File.InternalExists(assemblyPath))
+#if CORECLR
+                if (AssemblyLoadContext.IsTracingEnabled())
                 {
-                    return parentALC.LoadFromAssemblyPath(assemblyPath);
+                    AssemblyLoadContext.TraceSatelliteSubdirectoryPathProbed(assemblyPath, HResults.COR_E_FILENOTFOUND);
                 }
+#endif // CORECLR
+                assemblyPath = Path.Combine(parentDirectory, assemblyName.CultureName!.ToLowerInvariant(), $"{assemblyName.Name}.dll");
+                exists = Internal.IO.File.InternalExists(assemblyPath);
             }
 
-            return null;
+            Assembly? asm = exists ? parentALC.LoadFromAssemblyPath(assemblyPath) : null;
+#if CORECLR
+            if (AssemblyLoadContext.IsTracingEnabled())
+            {
+                AssemblyLoadContext.TraceSatelliteSubdirectoryPathProbed(assemblyPath, exists ? HResults.S_OK : HResults.COR_E_FILENOTFOUND);
+            }
+#endif // CORECLR
+
+            return asm;
         }
 
         internal IntPtr GetResolvedUnmanagedDll(Assembly assembly, string unmanagedDllName)