Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 7892451

Browse filesBrowse files
Add custom satellite assemblies resolution (#4136)
Add custom satellite assemblies resolution
1 parent 9caf0b6 commit 7892451
Copy full SHA for 7892451

File tree

2 files changed

+115
-17
lines changed
Filter options

2 files changed

+115
-17
lines changed

‎eng/Versions.props

Copy file name to clipboardExpand all lines: eng/Versions.props
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<Project>
33
<PropertyGroup>
44
<!-- This repo version -->
5-
<VersionPrefix>17.4.0</VersionPrefix>
5+
<VersionPrefix>17.4.1</VersionPrefix>
66
<PreReleaseVersionLabel>release</PreReleaseVersionLabel>
77
<!-- Opt-out repo features -->
88
<UsingToolXliff>false</UsingToolXliff>

‎src/Microsoft.TestPlatform.Common/Utilities/AssemblyResolver.cs

Copy file name to clipboardExpand all lines: src/Microsoft.TestPlatform.Common/Utilities/AssemblyResolver.cs
+114-16Lines changed: 114 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.IO;
88
using System.Linq;
99
using System.Reflection;
10+
using System.Threading;
1011

1112
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
1213
using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions;
@@ -31,6 +32,7 @@ internal class AssemblyResolver : IDisposable
3132
/// Specifies whether the resolver is disposed or not
3233
/// </summary>
3334
private bool _isDisposed;
35+
private Stack<string>? _currentlyResolvingResources;
3436

3537
/// <summary>
3638
/// Assembly resolver for platform
@@ -120,7 +122,71 @@ internal void AddSearchDirectories(IEnumerable<string> directories)
120122

121123
TPDebug.Assert(requestedName != null && !requestedName.Name.IsNullOrEmpty(), "AssemblyResolver.OnResolve: requested is null or name is empty!");
122124

123-
foreach (var dir in _searchDirectories)
125+
// Workaround: adding expected folder for the satellite assembly related to the current CurrentThread.CurrentUICulture relative to the current assembly location.
126+
// After the move to the net461 the runtime doesn't resolve anymore the satellite assembly correctly.
127+
// The expected workflow should be https://learn.microsoft.com/en-us/dotnet/core/extensions/package-and-deploy-resources#net-framework-resource-fallback-process
128+
// But the resolution never fallback to the CultureInfo.Parent folder and fusion log return a failure like:
129+
// ...
130+
// LOG: The same bind was seen before, and was failed with hr = 0x80070002.
131+
// ERR: Unrecoverable error occurred during pre - download check(hr = 0x80070002).
132+
// ...
133+
// The bizarre thing is that as a result we're failing caller task like discovery and when for reporting reason
134+
// we're accessing again to the resource it works.
135+
// Looks like a loading timing issue but we're not in control of the assembly loader order.
136+
var isResource = requestedName.Name.EndsWith(".resources");
137+
string[]? satelliteLocation = null;
138+
139+
// We help to resolve only test platform resources to be less invasive as possible with the default/expected behavior
140+
if (isResource && requestedName.Name.StartsWith("Microsoft.VisualStudio.TestPlatform"))
141+
{
142+
try
143+
{
144+
string? currentAssemblyLocation = null;
145+
try
146+
{
147+
currentAssemblyLocation = Assembly.GetExecutingAssembly().Location;
148+
// In .NET 5 and later versions, for bundled assemblies, the value returned is an empty string.
149+
currentAssemblyLocation = currentAssemblyLocation == string.Empty ? null : Path.GetDirectoryName(currentAssemblyLocation);
150+
}
151+
catch (NotSupportedException)
152+
{
153+
// https://learn.microsoft.com/en-us/dotnet/api/system.reflection.assembly.location
154+
}
155+
156+
if (currentAssemblyLocation is not null)
157+
{
158+
List<string> satelliteLocations = new();
159+
160+
// We mimic the satellite workflow and we add CurrentUICulture and CurrentUICulture.Parent folder in order
161+
string? currentUICulture = Thread.CurrentThread.CurrentUICulture?.Name;
162+
if (currentUICulture is not null)
163+
{
164+
satelliteLocations.Add(Path.Combine(currentAssemblyLocation, currentUICulture));
165+
}
166+
167+
// CurrentUICulture.Parent
168+
string? parentCultureInfo = Thread.CurrentThread.CurrentUICulture?.Parent?.Name;
169+
if (parentCultureInfo is not null)
170+
{
171+
satelliteLocations.Add(Path.Combine(currentAssemblyLocation, parentCultureInfo));
172+
}
173+
174+
if (satelliteLocations.Count > 0)
175+
{
176+
satelliteLocation = satelliteLocations.ToArray();
177+
}
178+
}
179+
}
180+
catch (Exception ex)
181+
{
182+
// We catch here because this is a workaround, we're trying to substitute the expected workflow of the runtime
183+
// and this shouldn't be needed, but if we fail we want to log what's happened and give a chance to the in place
184+
// resolution workflow
185+
EqtTrace.Error($"AssemblyResolver.OnResolve: Exception during the custom satellite resolution\n{ex}");
186+
}
187+
}
188+
189+
foreach (var dir in (satelliteLocation is not null) ? _searchDirectories.Union(satelliteLocation) : _searchDirectories)
124190
{
125191
if (dir.IsNullOrEmpty())
126192
{
@@ -134,29 +200,61 @@ internal void AddSearchDirectories(IEnumerable<string> directories)
134200
var assemblyPath = Path.Combine(dir, requestedName.Name + extension);
135201
try
136202
{
137-
if (!File.Exists(assemblyPath))
203+
bool pushed = false;
204+
try
138205
{
139-
EqtTrace.Info("AssemblyResolver.OnResolve: {0}: Assembly path does not exist: '{1}', returning.", args.Name, assemblyPath);
206+
if (isResource)
207+
{
208+
// Check for recursive resource lookup.
209+
// This can happen when we are on non-english locale, and we try to load mscorlib.resources
210+
// (or potentially some other resources). This will trigger a new Resolve and call the method
211+
// we are currently in. If then some code in this Resolve method (like File.Exists) will again
212+
// try to access mscorlib.resources it will end up recursing forever.
140213

141-
continue;
142-
}
214+
if (_currentlyResolvingResources != null && _currentlyResolvingResources.Count > 0 && _currentlyResolvingResources.Contains(assemblyPath))
215+
{
216+
EqtTrace.Info("AssemblyResolver.OnResolve: {0}: Assembly is searching for itself recursively: '{1}', returning as not found.", args.Name, assemblyPath);
217+
_resolvedAssemblies[args.Name] = null;
218+
return null;
219+
}
143220

144-
AssemblyName foundName = _platformAssemblyLoadContext.GetAssemblyNameFromPath(assemblyPath);
221+
_currentlyResolvingResources ??= new Stack<string>(4);
222+
_currentlyResolvingResources.Push(assemblyPath);
223+
pushed = true;
224+
}
145225

146-
if (!RequestedAssemblyNameMatchesFound(requestedName, foundName))
147-
{
148-
EqtTrace.Info("AssemblyResolver.OnResolve: {0}: File exists but version/public key is wrong. Try next extension.", args.Name);
149-
continue; // File exists but version/public key is wrong. Try next extension.
150-
}
226+
if (!File.Exists(assemblyPath))
227+
{
228+
EqtTrace.Info("AssemblyResolver.OnResolve: {0}: Assembly path does not exist: '{1}', returning.", args.Name, assemblyPath);
229+
230+
continue;
231+
}
232+
233+
AssemblyName foundName = _platformAssemblyLoadContext.GetAssemblyNameFromPath(assemblyPath);
234+
235+
if (!RequestedAssemblyNameMatchesFound(requestedName, foundName))
236+
{
237+
EqtTrace.Info("AssemblyResolver.OnResolve: {0}: File exists but version/public key is wrong. Try next extension.", args.Name);
238+
continue; // File exists but version/public key is wrong. Try next extension.
239+
}
151240

152-
EqtTrace.Info("AssemblyResolver.OnResolve: {0}: Loading assembly '{1}'.", args.Name, assemblyPath);
241+
EqtTrace.Info("AssemblyResolver.OnResolve: {0}: Loading assembly '{1}'.", args.Name, assemblyPath);
153242

154-
assembly = _platformAssemblyLoadContext.LoadAssemblyFromPath(assemblyPath);
155-
_resolvedAssemblies[args.Name] = assembly;
243+
assembly = _platformAssemblyLoadContext.LoadAssemblyFromPath(assemblyPath);
244+
_resolvedAssemblies[args.Name] = assembly;
156245

157-
EqtTrace.Info("AssemblyResolver.OnResolve: Resolved assembly: {0}, from path: {1}", args.Name, assemblyPath);
246+
EqtTrace.Info("AssemblyResolver.OnResolve: Resolved assembly: {0}, from path: {1}", args.Name, assemblyPath);
158247

159-
return assembly;
248+
return assembly;
249+
}
250+
finally
251+
{
252+
if (isResource && pushed)
253+
{
254+
_currentlyResolvingResources?.Pop();
255+
}
256+
257+
}
160258
}
161259
catch (FileLoadException ex)
162260
{

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.