This repository was archived by the owner on Jun 21, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Enable assembly resolver to ensure that Start Page and other icons can be found
#1220
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
6ce7fee
Re-enable AssemblyResolverPackage
jcansdale 7b0cc1a
Make assembly resolver follow rules from ProvideDependentAssembly att…
jcansdale d542218
Write to log if assembly is resolved
jcansdale ef22d3b
Avoid logging during assembly resolve event
jcansdale 48c3841
No need to load assembly resolver on GitSccProviderId
jcansdale 7674dbd
Add note about when `GitHub.VisualStudio` gets auto-loaded
jcansdale 13b5534
Tidy up
jcansdale 48ee1fb
Mark tests as public
jcansdale 7d6e8de
Merge branch 'master' into fixes/1217-devenv-config-not-updated
jcansdale File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,90 +1,124 @@ | ||
| using System; | ||
| using System.IO; | ||
| using System.Linq; | ||
| using System.Reflection; | ||
| using System.Globalization; | ||
| using System.Collections.Generic; | ||
| using System.Diagnostics.CodeAnalysis; | ||
| using System.Runtime.InteropServices; | ||
| using Microsoft.VisualStudio; | ||
| using Microsoft.VisualStudio.Shell; | ||
| using NLog; | ||
|
|
||
| namespace GitHub.VisualStudio | ||
| { | ||
| // This is the Git service GUID, which fires early and is used by GitHubService. | ||
| //[ProvideAutoLoad(Guids.GitSccProviderId)] | ||
| // This fires before ShellInitialized and SolutionExists. | ||
| //[ProvideAutoLoad(VSConstants.UICONTEXT.NoSolution_string)] | ||
| [ProvideAutoLoad(VSConstants.UICONTEXT.NoSolution_string)] | ||
| [Guid(Guids.guidAssemblyResolverPkgString)] | ||
| public class AssemblyResolverPackage : Package | ||
| { | ||
| // list of assemblies that should be resolved by name only | ||
| static readonly string[] ourAssemblies = | ||
| { | ||
| // resolver is required for these | ||
| "GitHub.UI", | ||
| "GitHub.VisualStudio.UI", | ||
|
|
||
| // these are signed by StrongNameSigner | ||
| "Markdig", | ||
| "Markdig.Wpf", | ||
| // list of assemblies that should be considered when resolving | ||
| IEnumerable<ProvideDependentAssemblyAttribute> dependentAssemblies; | ||
| string packageFolder; | ||
|
|
||
| // these are included just in case | ||
| "GitHub.UI.Reactive", | ||
| "System.Windows.Interactivity" | ||
| }; | ||
|
|
||
| readonly string extensionDir; | ||
| IDictionary<string, Assembly> resolvingAssemblies; | ||
| IDictionary<string, Exception> resolvingExceptions; | ||
|
|
||
| public AssemblyResolverPackage() | ||
| { | ||
| extensionDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); | ||
| AppDomain.CurrentDomain.AssemblyResolve += LoadAssemblyFromExtensionDir; | ||
| var asm = Assembly.GetExecutingAssembly(); | ||
| packageFolder = Path.GetDirectoryName(asm.Location); | ||
| dependentAssemblies = asm.GetCustomAttributes<ProvideDependentAssemblyAttribute>(); | ||
| resolvingAssemblies = new Dictionary<string, Assembly>(); | ||
| resolvingExceptions = new Dictionary<string, Exception>(); | ||
| AppDomain.CurrentDomain.AssemblyResolve += ResolveAssemblyFromPackageFolder; | ||
| } | ||
|
|
||
| protected override void Dispose(bool disposing) | ||
| { | ||
| AppDomain.CurrentDomain.AssemblyResolve -= LoadAssemblyFromExtensionDir; | ||
| AppDomain.CurrentDomain.AssemblyResolve -= ResolveAssemblyFromPackageFolder; | ||
|
|
||
| if (resolvingAssemblies.Count > 0 || resolvingExceptions.Count > 0) | ||
| { | ||
| // Avoid executing any logging code unless there is something to log. | ||
| WriteToLog(); | ||
| } | ||
|
|
||
| base.Dispose(disposing); | ||
| } | ||
|
|
||
| [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods")] | ||
| Assembly LoadAssemblyFromExtensionDir(object sender, ResolveEventArgs e) | ||
| [SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods")] | ||
| public static Assembly ResolveDependentAssembly( | ||
| ProvideDependentAssemblyAttribute dependentAssembly, string packageFolder, AssemblyName resolveAssemblyName) | ||
| { | ||
| try | ||
| if (dependentAssembly.AssemblyName == resolveAssemblyName.Name) | ||
| { | ||
| var name = new AssemblyName(e.Name).Name; | ||
| var filename = Path.Combine(extensionDir, name + ".dll"); | ||
| if (!File.Exists(filename)) | ||
| var file = dependentAssembly.CodeBase.Replace("$PackageFolder$", packageFolder); | ||
| if (File.Exists(file)) | ||
| { | ||
| return null; | ||
| var targetAssemblyName = AssemblyName.GetAssemblyName(file); | ||
|
|
||
| var codeBase = dependentAssembly as ProvideCodeBaseAttribute; | ||
| if (codeBase != null) | ||
| { | ||
| if (resolveAssemblyName.FullName == targetAssemblyName.FullName) | ||
| { | ||
| return Assembly.LoadFrom(file); | ||
| } | ||
| } | ||
|
|
||
| var bindingRedirection = dependentAssembly as ProvideBindingRedirectionAttribute; | ||
| if (bindingRedirection != null) | ||
| { | ||
| if (resolveAssemblyName.Version >= new Version(bindingRedirection.OldVersionLowerBound) && | ||
| resolveAssemblyName.Version <= new Version(bindingRedirection.OldVersionUpperBound)) | ||
| { | ||
| resolveAssemblyName.Version = targetAssemblyName.Version; | ||
| if (resolveAssemblyName.FullName == targetAssemblyName.FullName) | ||
| { | ||
| return Assembly.LoadFrom(file); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| var targetName = AssemblyName.GetAssemblyName(filename); | ||
| return null; | ||
| } | ||
|
|
||
| // Resolve any exact `FullName` matches. | ||
| if (e.Name != targetName.FullName) | ||
| Assembly ResolveAssemblyFromPackageFolder(object sender, ResolveEventArgs e) | ||
| { | ||
| try | ||
| { | ||
| var resolveAssemblyName = new AssemblyName(e.Name); | ||
| foreach (var dependentAssembly in dependentAssemblies) | ||
| { | ||
| // Resolve any version of our assemblies. | ||
| if (!ourAssemblies.Contains(name, StringComparer.OrdinalIgnoreCase)) | ||
| var resolvedAssembly = ResolveDependentAssembly(dependentAssembly, packageFolder, resolveAssemblyName); | ||
| if (resolvedAssembly != null) | ||
| { | ||
| return null; | ||
| return resolvingAssemblies[e.Name] = resolvedAssembly; | ||
| } | ||
| } | ||
|
|
||
| return Assembly.LoadFrom(filename); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| var log = string.Format(CultureInfo.CurrentCulture, | ||
| "Error occurred loading {0} from {1}.{2}{3}{4}", | ||
| e.Name, | ||
| Assembly.GetExecutingAssembly().Location, | ||
| Environment.NewLine, | ||
| ex, | ||
| Environment.NewLine); | ||
| VsOutputLogger.Write(log); | ||
| resolvingExceptions[e.Name] = ex; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| void WriteToLog() | ||
| { | ||
| var log = LogManager.GetCurrentClassLogger(); | ||
| foreach (var resolvedAssembly in resolvingAssemblies) | ||
| { | ||
| log.Info(CultureInfo.InvariantCulture, "Resolved '{0}' to '{1}'.", resolvedAssembly.Key, resolvedAssembly.Value.Location); | ||
| } | ||
|
|
||
| foreach (var resolvingException in resolvingExceptions) | ||
| { | ||
| log.Error(CultureInfo.InvariantCulture, "Error occurred loading '{0}' from '{1}'.\n{2}", resolvingException.Key, packageFolder, resolvingException.Value); | ||
| } | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,10 @@ | ||
| <ImageManifest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.microsoft.com/VisualStudio/ImageManifestSchema/2014"> | ||
|
|
||
| <Symbols> | ||
| <!-- We need to ensure that `GitHub.VisualStudio` has loaded before any of these resources are used. --> | ||
| <!-- This should happen when `AssemblyResolverPackage` auto-loads on `UICONTEXT.NoSolution`. --> | ||
| <String Name="Resources" Value="/GitHub.VisualStudio;component/Resources/icons" /> | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems the culprit was actually here. It tries to find an assembly by its simple name before that assembly has been loaded. I think when the There is a possible workaround for this: I did a spike to see if this might work for us, but it would involve moving a bunch of resources into |
||
|
|
||
| <Guid Name="guidImages" Value="{27841f47-070a-46d6-90be-a5cbbfc724ac}" /> | ||
| <ID Name="logo" Value="1" /> | ||
| <ID Name="arrow_left" Value="2" /> | ||
|
|
||
157 changes: 157 additions & 0 deletions
157
src/UnitTests/GitHub.VisualStudio/AssemblyResolverPackageTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Linq; | ||
| using System.Text; | ||
| using System.Threading.Tasks; | ||
| using GitHub.VisualStudio; | ||
| using Microsoft.VisualStudio.Shell; | ||
| using System.Reflection; | ||
| using System.IO; | ||
| using Xunit; | ||
|
|
||
| namespace UnitTests.GitHub.VisualStudio | ||
| { | ||
| public class AssemblyResolverPackageTests | ||
| { | ||
| public class TheResolveDependentAssemblyMethod | ||
| { | ||
| [Fact] | ||
| public void ProvideCodeBaseAttribute_MatchFullName() | ||
| { | ||
| var asm = Assembly.GetExecutingAssembly(); | ||
| var assemblyName = asm.GetName().Name; | ||
| var codeBase = $"$PackageFolder$\\{assemblyName}.dll"; | ||
| var provideCodeBase = new ProvideCodeBaseAttribute { AssemblyName = assemblyName, CodeBase = codeBase }; | ||
| var packageFolder = Path.GetDirectoryName(asm.Location); | ||
| var resolveAssemblyName = new AssemblyName(asm.FullName); | ||
|
|
||
| var resolvedAssembly = AssemblyResolverPackage.ResolveDependentAssembly(provideCodeBase, packageFolder, resolveAssemblyName); | ||
|
|
||
| Assert.Equal(asm, resolvedAssembly); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ProvideCodeBaseAttribute_DontMatchDifferentVersion() | ||
| { | ||
| var asm = Assembly.GetExecutingAssembly(); | ||
| var assemblyName = asm.GetName().Name; | ||
| var codeBase = $"$PackageFolder$\\{assemblyName}.dll"; | ||
| var provideCodeBase = new ProvideCodeBaseAttribute { AssemblyName = assemblyName, CodeBase = codeBase }; | ||
| var packageFolder = Path.GetDirectoryName(asm.Location); | ||
| var resolveAssemblyName = asm.GetName(); | ||
| resolveAssemblyName.Version = new Version("0.0.0.0"); | ||
|
|
||
| var resolvedAssembly = AssemblyResolverPackage.ResolveDependentAssembly(provideCodeBase, packageFolder, resolveAssemblyName); | ||
|
|
||
| Assert.Null(resolvedAssembly); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ProvideCodeBaseAttribute_DontMatchMissingCodeBase() | ||
| { | ||
| var asm = Assembly.GetExecutingAssembly(); | ||
| var assemblyName = asm.GetName().Name; | ||
| var codeBase = "__NothingToSeeHere___"; | ||
| var provideCodeBase = new ProvideCodeBaseAttribute { AssemblyName = assemblyName, CodeBase = codeBase }; | ||
| var dependentAssemblies = new[] { provideCodeBase }; | ||
| var packageFolder = Path.GetDirectoryName(asm.Location); | ||
| var resolveAssemblyName = new AssemblyName(asm.FullName); | ||
|
|
||
| var resolvedAssembly = AssemblyResolverPackage.ResolveDependentAssembly(provideCodeBase, packageFolder, resolveAssemblyName); | ||
|
|
||
| Assert.Null(resolvedAssembly); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ProvideCodeBaseAttribute_DontMatchPartialName() | ||
| { | ||
| var asm = Assembly.GetExecutingAssembly(); | ||
| var assemblyName = asm.GetName().Name; | ||
| var provideCodeBase = new ProvideCodeBaseAttribute { AssemblyName = assemblyName, CodeBase = $"$PackageFolder$\\{assemblyName}.dll" }; | ||
| var dependentAssemblies = new[] { provideCodeBase }; | ||
| var packageFolder = Path.GetDirectoryName(asm.Location); | ||
| var resolveAssemblyName = new AssemblyName(asm.GetName().Name); | ||
|
|
||
| var resolvedAssembly = AssemblyResolverPackage.ResolveDependentAssembly(provideCodeBase, packageFolder, resolveAssemblyName); | ||
|
|
||
| Assert.Null(resolvedAssembly); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ProvideBindingRedirectionAttribute_MatchOldVersionLowerBound() | ||
| { | ||
| var asm = Assembly.GetExecutingAssembly(); | ||
| var assemblyName = asm.GetName().Name; | ||
| var codeBase = $"$PackageFolder$\\{assemblyName}.dll"; | ||
| var oldVersionLowerBound = "0.0.0.0"; | ||
| var oldVersionUpperBound = oldVersionLowerBound; | ||
| var provideBindingRedirection = new ProvideBindingRedirectionAttribute | ||
| { | ||
| AssemblyName = assemblyName, | ||
| CodeBase = codeBase, | ||
| OldVersionLowerBound = oldVersionLowerBound, | ||
| OldVersionUpperBound = oldVersionUpperBound | ||
| }; | ||
| var dependentAssemblies = new[] { provideBindingRedirection }; | ||
| var packageFolder = Path.GetDirectoryName(asm.Location); | ||
| var resolveAssemblyName = asm.GetName(); | ||
| resolveAssemblyName.Version = new Version(oldVersionLowerBound); | ||
|
|
||
| var resolvedAssembly = AssemblyResolverPackage.ResolveDependentAssembly(provideBindingRedirection, packageFolder, resolveAssemblyName); | ||
|
|
||
| Assert.Equal(asm, resolvedAssembly); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ProvideBindingRedirectionAttribute_MatchOldVersionUpperBound() | ||
| { | ||
| var asm = Assembly.GetExecutingAssembly(); | ||
| var assemblyName = asm.GetName().Name; | ||
| var codeBase = $"$PackageFolder$\\{assemblyName}.dll"; | ||
| var oldVersionUpperBound = "1.1.1.1"; | ||
| var oldVersionLowerBound = oldVersionUpperBound; | ||
| var provideBindingRedirection = new ProvideBindingRedirectionAttribute | ||
| { | ||
| AssemblyName = assemblyName, | ||
| CodeBase = codeBase, | ||
| OldVersionLowerBound = oldVersionLowerBound, | ||
| OldVersionUpperBound = oldVersionUpperBound | ||
| }; | ||
| var dependentAssemblies = new[] { provideBindingRedirection }; | ||
| var packageFolder = Path.GetDirectoryName(asm.Location); | ||
| var resolveAssemblyName = asm.GetName(); | ||
| resolveAssemblyName.Version = new Version(oldVersionLowerBound); | ||
|
|
||
| var resolvedAssembly = AssemblyResolverPackage.ResolveDependentAssembly(provideBindingRedirection, packageFolder, resolveAssemblyName); | ||
|
|
||
| Assert.Equal(asm, resolvedAssembly); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ProvideBindingRedirectionAttribute_DontMatchOutOfBounds() | ||
| { | ||
| var asm = Assembly.GetExecutingAssembly(); | ||
| var assemblyName = asm.GetName().Name; | ||
| var codeBase = $"$PackageFolder$\\{assemblyName}.dll"; | ||
| var oldVersionLowerBound = "0.0.0.0"; | ||
| var oldVersionUpperBound = "1.1.1.1"; | ||
| var resolveVersion = new Version("2.2.2.2"); | ||
| var provideBindingRedirection = new ProvideBindingRedirectionAttribute | ||
| { | ||
| AssemblyName = assemblyName, | ||
| CodeBase = codeBase, | ||
| OldVersionLowerBound = oldVersionLowerBound, | ||
| OldVersionUpperBound = oldVersionUpperBound | ||
| }; | ||
| var dependentAssemblies = new[] { provideBindingRedirection }; | ||
| var packageFolder = Path.GetDirectoryName(asm.Location); | ||
| var resolveAssemblyName = asm.GetName(); | ||
| resolveAssemblyName.Version = resolveVersion; | ||
|
|
||
| var resolvedAssembly = AssemblyResolverPackage.ResolveDependentAssembly(provideBindingRedirection, packageFolder, resolveAssemblyName); | ||
|
|
||
| Assert.Null(resolvedAssembly); | ||
| } | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we log the resolved assemblies and resolve errors when VS is shutdown rather than when they occur?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For a few reasons:
Trace.WriteLine).I've tried to make it as predictable and light weight as possible. :-)