Skip to content
This repository was archived by the owner on Jun 21, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 80 additions & 46 deletions src/GitHub.VisualStudio/AssemblyResolverPackage.cs
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();
Copy link
Contributor

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?

Copy link
Collaborator Author

@jcansdale jcansdale Sep 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a few reasons:

  1. Since this package is always loaded, I wanted to avoid loading any extra assemblies as VS starts.
  2. Avoid the possability of recursive assembly resolving (in a previous version there was an issue with Trace.WriteLine).
  3. Avoid doing any file creation or writes as VS starts.

I've tried to make it as predictable and light weight as possible. :-)

}

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);
}
}
}
}
3 changes: 3 additions & 0 deletions src/GitHub.VisualStudio/GitHub.VisualStudio.imagemanifest
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" />
Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 Team Explorer related assemblies get initialized might have changed in VS 15.3. This can cause GitHub.VisualStudio not to get pre-loaded and the icons fail to load.

There is a possible workaround for this:
microsoft/PTVS@ec1b895

I did a spike to see if this might work for us, but it would involve moving a bunch of resources into GitHub.UI and fixing a bunch of other issues (not a point fix). This might be worth investigating at some future time.


<Guid Name="guidImages" Value="{27841f47-070a-46d6-90be-a5cbbfc724ac}" />
<ID Name="logo" Value="1" />
<ID Name="arrow_left" Value="2" />
Expand Down
157 changes: 157 additions & 0 deletions src/UnitTests/GitHub.VisualStudio/AssemblyResolverPackageTests.cs
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);
}
}
}
}
1 change: 1 addition & 0 deletions src/UnitTests/UnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@
<Compile Include="GitHub.UI\Converters.cs" />
<Compile Include="GitHub.UI\TestAutomation\ResourceValueTests.cs" />
<Compile Include="GitHub.UI\TwoFactorInputTests.cs" />
<Compile Include="GitHub.VisualStudio\AssemblyResolverPackageTests.cs" />
<Compile Include="GitHub.VisualStudio\Services\JsonConnectionCacheTests.cs" />
<Compile Include="GitHub.VisualStudio\Services\ConnectionManagerTests.cs" />
<Compile Include="GitHub.VisualStudio\Services\RepositoryPublishServiceTests.cs" />
Expand Down