diff --git a/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj b/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj
index a355aad787..54088cfa9b 100644
--- a/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj
+++ b/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj
@@ -345,6 +345,7 @@
+
diff --git a/src/GitHub.VisualStudio/GitHubPackage.cs b/src/GitHub.VisualStudio/GitHubPackage.cs
index 2f368e96c0..741b9ccfe8 100644
--- a/src/GitHub.VisualStudio/GitHubPackage.cs
+++ b/src/GitHub.VisualStudio/GitHubPackage.cs
@@ -11,6 +11,7 @@
using GitHub.Logging;
using GitHub.Services;
using GitHub.Settings;
+using GitHub.VisualStudio.Helpers;
using GitHub.VisualStudio.Commands;
using GitHub.Services.Vssdk.Commands;
using GitHub.ViewModels.GitHubPane;
@@ -166,8 +167,10 @@ public sealed class ServiceProviderPackage : AsyncPackage, IServiceProviderPacka
public const string ServiceProviderPackageId = "D5CE1488-DEDE-426D-9E5B-BFCCFBE33E53";
static readonly ILogger log = LogManager.ForContext();
- protected override Task InitializeAsync(CancellationToken cancellationToken, IProgress progress)
+ protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress)
{
+ await CheckBindingPathsAsync();
+
AddService(typeof(IGitHubServiceProvider), CreateService, true);
AddService(typeof(IVSGitExt), CreateService, true);
AddService(typeof(IUsageTracker), CreateService, true);
@@ -175,9 +178,28 @@ protected override Task InitializeAsync(CancellationToken cancellationToken, IPr
AddService(typeof(ILoginManager), CreateService, true);
AddService(typeof(IGitHubToolWindowManager), CreateService, true);
AddService(typeof(IPackageSettings), CreateService, true);
- return Task.CompletedTask;
}
+#if DEBUG
+ async Task CheckBindingPathsAsync()
+ {
+ try
+ {
+ // When running in the Exp instance, ensure there is only one active binding path.
+ // This is necessary when the regular (AllUsers) extension is also installed.
+ // See: https://github.com/github/VisualStudio/issues/2006
+ await JoinableTaskFactory.SwitchToMainThreadAsync();
+ BindingPathHelper.CheckBindingPaths(GetType().Assembly, this);
+ }
+ catch (Exception e)
+ {
+ log.Error(e, nameof(CheckBindingPathsAsync));
+ }
+ }
+#else
+ Task CheckBindingPathsAsync() => Task.CompletedTask;
+#endif
+
public async Task ShowGitHubPane()
{
var pane = ShowToolWindow(new Guid(GitHubPane.GitHubPaneGuid));
diff --git a/src/GitHub.VisualStudio/Helpers/BindingPathHelper.cs b/src/GitHub.VisualStudio/Helpers/BindingPathHelper.cs
new file mode 100644
index 0000000000..da47e8c755
--- /dev/null
+++ b/src/GitHub.VisualStudio/Helpers/BindingPathHelper.cs
@@ -0,0 +1,110 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Diagnostics;
+using System.Globalization;
+using System.Collections.Generic;
+using GitHub.Logging;
+using Microsoft.VisualStudio.Shell;
+using Microsoft.VisualStudio.Shell.Interop;
+using Microsoft.VisualStudio.Shell.Settings;
+using Microsoft.VisualStudio.Settings;
+using Serilog;
+
+namespace GitHub.VisualStudio.Helpers
+{
+ ///
+ /// This a workaround for extensions that define a ProvideBindingPath attribute and
+ /// install for AllUsers.
+ ///
+ ///
+ /// Extensions that are installed for AllUsers, will also be installed for all
+ /// instances of Visual Studio - including the experimental (Exp) instance which
+ /// is used in development. This isn't a problem so long as all features that
+ /// exist in the AllUsers extension, also exist in the extension that is being
+ /// developed.
+ ///
+ /// When an extension uses the ProvideBindingPath attribute, the binding path for
+ /// the AllUsers extension gets installed at the same time as the one in development.
+ /// This doesn't matter when an assembly is strong named and is loaded using its
+ /// full name (including version number). When an assembly is loaded using its
+ /// simple name, assemblies from the AllUsers extension can end up loaded at the
+ /// same time as the extension being developed. This can happen when an assembly
+ /// is loaded from XAML or an .imagemanifest.
+ ///
+ /// This is a workaround for that issue. The
+ /// method will check to see if a reference assembly could be loaded from an alternative
+ /// binding path. It will return any alternative paths that is finds.
+ /// See https://github.com/github/VisualStudio/issues/1995
+ ///
+ public static class BindingPathHelper
+ {
+ static readonly ILogger log = LogManager.ForContext(typeof(BindingPathHelper));
+
+ internal static void CheckBindingPaths(Assembly assembly, IServiceProvider serviceProvider)
+ {
+ log.Information("Looking for assembly on wrong binding path");
+
+ ThreadHelper.CheckAccess();
+ var bindingPaths = FindBindingPaths(serviceProvider);
+ var bindingPath = FindRedundantBindingPaths(bindingPaths, assembly.Location)
+ .FirstOrDefault();
+ if (bindingPath == null)
+ {
+ log.Information("No incorrect binding path found");
+ return;
+ }
+
+ // Log what has been detected
+ log.Warning("Found assembly on wrong binding path {BindingPath}", bindingPath);
+
+ var message = string.Format(CultureInfo.CurrentCulture, @"Found assembly on wrong binding path:
+{0}
+
+Would you like to learn more about this issue?", bindingPath);
+ var action = VsShellUtilities.ShowMessageBox(serviceProvider, message, "GitHub for Visual Studio", OLEMSGICON.OLEMSGICON_WARNING,
+ OLEMSGBUTTON.OLEMSGBUTTON_YESNO, OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
+ if (action == 6) // Yes = 6, No = 7
+ {
+ Process.Start("https://github.com/github/VisualStudio/issues/2006");
+ }
+ }
+
+ ///
+ /// Find any alternative binding path that might have been installed by an AllUsers extension.
+ ///
+ /// A list of binding paths to search
+ /// A reference assembly that has been loaded from the correct path.
+ /// A list of redundant binding paths.
+ public static IList FindRedundantBindingPaths(IEnumerable bindingPaths, string assemblyLocation)
+ {
+ var fileName = Path.GetFileName(assemblyLocation);
+ return bindingPaths
+ .Select(p => (path: p, file: Path.Combine(p, fileName)))
+ .Where(pf => File.Exists(pf.file))
+ .Where(pf => !pf.file.Equals(assemblyLocation, StringComparison.OrdinalIgnoreCase))
+ .Select(pf => pf.path)
+ .ToList();
+ }
+
+ ///
+ /// Find Visual Studio's list of binding paths.
+ ///
+ /// A list of binding paths.
+ public static IEnumerable FindBindingPaths(IServiceProvider serviceProvider)
+ {
+ const string bindingPaths = "BindingPaths";
+ var manager = new ShellSettingsManager(serviceProvider);
+ var store = manager.GetReadOnlySettingsStore(SettingsScope.Configuration);
+ foreach (var guid in store.GetSubCollectionNames(bindingPaths))
+ {
+ var guidPath = Path.Combine(bindingPaths, guid);
+ foreach (var path in store.GetPropertyNames(guidPath))
+ {
+ yield return path;
+ }
+ }
+ }
+ }
+}
diff --git a/test/GitHub.VisualStudio.UnitTests/GlobalSuppressions.cs b/test/GitHub.VisualStudio.UnitTests/GlobalSuppressions.cs
new file mode 100644
index 0000000000..42af364f88
--- /dev/null
+++ b/test/GitHub.VisualStudio.UnitTests/GlobalSuppressions.cs
@@ -0,0 +1,10 @@
+
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible",
+ Justification = "It's okay for nested unit test types to be visible")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores",
+ Justification = "It's okay for unit test names to contain underscores")]
diff --git a/test/GitHub.VisualStudio.UnitTests/Helpers/BindingPathHelperTests.cs b/test/GitHub.VisualStudio.UnitTests/Helpers/BindingPathHelperTests.cs
new file mode 100644
index 0000000000..a650ff87a7
--- /dev/null
+++ b/test/GitHub.VisualStudio.UnitTests/Helpers/BindingPathHelperTests.cs
@@ -0,0 +1,38 @@
+using System.IO;
+using System.Collections.Generic;
+using GitHub.VisualStudio.Helpers;
+using NUnit.Framework;
+
+public static class BindingPathHelperTests
+{
+ public class TheFindRedundantBindingPathsMethod
+ {
+ [TestCase]
+ public void Redundant_Binding_Paths_Contains_Alternative_Path()
+ {
+ var alternativeLocation = GetType().Assembly.Location;
+ var fileName = Path.GetFileName(alternativeLocation);
+ var alternativeDir = Path.GetDirectoryName(alternativeLocation);
+ var assemblyDir = @"c:\target";
+ var assemblyLocation = Path.Combine(assemblyDir, fileName);
+ var bindingPaths = new List { alternativeDir, assemblyDir };
+
+ var paths = BindingPathHelper.FindRedundantBindingPaths(bindingPaths, assemblyLocation);
+
+ Assert.That(paths, Contains.Item(alternativeDir));
+ Assert.That(paths, Does.Not.Contain(assemblyDir));
+ }
+
+ [TestCase]
+ public void No_Redundant_Binding_Paths()
+ {
+ var assemblyLocation = GetType().Assembly.Location;
+ var assemblyDir = Path.GetDirectoryName(assemblyLocation);
+ var bindingPaths = new List { assemblyDir };
+
+ var paths = BindingPathHelper.FindRedundantBindingPaths(bindingPaths, assemblyLocation);
+
+ Assert.That(paths, Does.Not.Contain(assemblyDir));
+ }
+ }
+}