Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reimplementation of in memory profile support #180

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
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
14 changes: 13 additions & 1 deletion SmartCmdArgs/SmartCmdArgs.Shared/CmdArgsOptionPage.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell;
using SmartCmdArgs.Helper;
using System.ComponentModel;
using System.Linq;
Expand Down Expand Up @@ -78,6 +78,7 @@ public CmdArgsOptionPage() : base()
private bool _manageWorkingDirectories;
private bool _manageLaunchApplication;
private bool _vcsSupportEnabled;
private bool _useCpsVirtualProfile;
private bool _useSolutionDir;
private bool _macroEvaluationEnabled;

Expand Down Expand Up @@ -111,6 +112,7 @@ public bool UseMonospaceFont
set => SetAndNotify(value, ref _useMonospaceFont);
}


[Category("Appearance")]
[DisplayName("Display Tags for CLAs")]
[Description("If enabled the item tag 'CLA' is displayed for Command Line Arguments. Normally the tag 'ENV' is only displayed for environment varibales.")]
Expand Down Expand Up @@ -201,6 +203,16 @@ public bool VcsSupportEnabled
set => SetAndNotify(value, ref _vcsSupportEnabled);
}

[Category("Settings Defaults")]
[DisplayName("Use CPS Virtual Profile")]
[Description("If enabled a virtual profile is created for CPS projects and only this profile is changed by the extension.")]
[DefaultValue(false)]
public bool UseCpsVirtualProfile
{
get => _useCpsVirtualProfile;
set => SetAndNotify(value, ref _useCpsVirtualProfile);
}

[Category("Settings Defaults")]
[DisplayName("Use Solution Directory")]
[Description("If enabled all arguments of every project will be stored in a single file next to the *.sln file. (Only if version control support is enabled)")]
Expand Down
15 changes: 9 additions & 6 deletions SmartCmdArgs/SmartCmdArgs.Shared/CmdArgsPackage.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// <copyright file="CmdArgsPackage.cs" company="Company">
// Copyright (c) Company. All rights reserved.
// </copyright>
Expand Down Expand Up @@ -68,7 +68,8 @@ public sealed class CmdArgsPackage : AsyncPackage
private ISuoDataService suoDataService;
private ILifeCycleService lifeCycleService;
private IVsEventHandlingService vsEventHandling;
private IFileStorageEventHandlingService fileStorageEventHandling;
private IFileStorageEventHandlingService fileStorageEventHandling;
private ICpsProjectConfigService cpsProjectConfigService;

private ToolWindowViewModel toolWindowViewModel;
private TreeViewModel treeViewModel;
Expand Down Expand Up @@ -104,7 +105,8 @@ public CmdArgsPackage()
suoDataService = ServiceProvider.GetRequiredService<ISuoDataService>();
lifeCycleService = ServiceProvider.GetRequiredService<ILifeCycleService>();
vsEventHandling = ServiceProvider.GetRequiredService<IVsEventHandlingService>();
fileStorageEventHandling = ServiceProvider.GetRequiredService<IFileStorageEventHandlingService>();
fileStorageEventHandling = ServiceProvider.GetRequiredService<IFileStorageEventHandlingService>();
cpsProjectConfigService = ServiceProvider.GetRequiredService<ICpsProjectConfigService>();
}

protected override void Dispose(bool disposing)
Expand Down Expand Up @@ -169,7 +171,8 @@ private ServiceProvider ConfigureServices()
services.AddLazySingleton(x => GetDialogPage<CmdArgsOptionPage>());
services.AddLazySingleton<SettingsViewModel>();
services.AddLazySingleton<ToolWindowViewModel>();
services.AddLazySingleton<TreeViewModel>();
services.AddLazySingleton<TreeViewModel>();
services.AddLazySingleton<ICpsProjectConfigService, CpsProjectConfigService>();
services.AddLazySingleton<IProjectConfigService, ProjectConfigService>();
services.AddSingleton<IVisualStudioHelperService, VisualStudioHelperService>();
services.AddSingleton<IFileStorageService, FileStorageService>();
Expand Down Expand Up @@ -260,9 +263,9 @@ public List<string> GetLaunchProfiles(Guid projGuid)
var project = vsHelper.HierarchyForProjectGuid(projGuid);

List<string> launchProfiles = null;
if (project?.IsCpsProject() == true)
if (project?.SupportsLaunchProfiles() == true)
{
launchProfiles = CpsProjectSupport.GetLaunchProfileNames(project.GetProject())?.ToList();
launchProfiles = cpsProjectConfigService.GetLaunchProfileNames(project.GetProject())?.ToList();
}

return launchProfiles ?? new List<string>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.VisualStudio.ProjectSystem;
using EnvDTE;
using Microsoft.VisualStudio.ProjectSystem;
using Microsoft.VisualStudio.ProjectSystem.Debug;
using Microsoft.VisualStudio.ProjectSystem.Properties;
using SmartCmdArgs.DataSerialization;
Expand All @@ -7,6 +8,11 @@
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks.Dataflow;
#if DYNAMIC_VSProjectManaged
using System.Reflection;
using System.Reflection.Emit;
using Expression = System.Linq.Expressions.Expression;
#endif

// This isolation of Microsoft.VisualStudio.ProjectSystem dependencies into one file ensures compatibility
// across various Visual Studio installations. This is crucial because not all Visual Studio workloads
Expand All @@ -19,11 +25,32 @@
// As such, ensuring compatibility requires knowledge of the version Visual Studio redirects to, which varies
// by Visual Studio installation version.

namespace SmartCmdArgs.Helper
namespace SmartCmdArgs.Services
{
public static class CpsProjectSupport
public interface ICpsProjectConfigService
{
private static bool TryGetProjectServices(EnvDTE.Project project, out IUnconfiguredProjectServices unconfiguredProjectServices, out IProjectServices projectServices)
string GetActiveLaunchProfileName(Project project);
void GetItemsFromConfig(Project project, List<CmdItemJson> allArgs, bool includeArgs, bool includeEnvVars, bool includeWorkDir, bool includeLaunchApp);
IEnumerable<string> GetLaunchProfileNames(Project project);
IDisposable ListenToLaunchProfileChanges(Project project, Action listener);
void SetActiveLaunchProfileByName(Project project, string profileName);
void SetActiveLaunchProfileToVirtualProfile(Project project);
void SetConfig(Project project, string arguments, IDictionary<string, string> envVars, string workDir, string launchApp);
}

public class CpsProjectConfigService : ICpsProjectConfigService
{
public static string VirtualProfileName = "Smart CLI Args";

private readonly IOptionsSettingsService optionsSettingsService;

public CpsProjectConfigService(
IOptionsSettingsService optionsSettingsService)
{
this.optionsSettingsService = optionsSettingsService;
}

private bool TryGetProjectServices(EnvDTE.Project project, out IUnconfiguredProjectServices unconfiguredProjectServices, out IProjectServices projectServices)
{
IVsBrowseObjectContext context = project as IVsBrowseObjectContext;
if (context == null && project != null)
Expand Down Expand Up @@ -55,7 +82,7 @@ private static bool TryGetProjectServices(EnvDTE.Project project, out IUnconfigu
}
}

public static string GetActiveLaunchProfileName(EnvDTE.Project project)
public string GetActiveLaunchProfileName(EnvDTE.Project project)
{
if (TryGetProjectServices(project, out IUnconfiguredProjectServices unconfiguredProjectServices, out IProjectServices projectServices))
{
Expand All @@ -65,7 +92,21 @@ public static string GetActiveLaunchProfileName(EnvDTE.Project project)
return null;
}

public static IEnumerable<string> GetLaunchProfileNames(EnvDTE.Project project)
public void SetActiveLaunchProfileByName(EnvDTE.Project project, string profileName)
{
if (TryGetProjectServices(project, out IUnconfiguredProjectServices unconfiguredProjectServices, out IProjectServices projectServices))
{
var launchSettingsProvider = unconfiguredProjectServices.ExportProvider.GetExportedValue<ILaunchSettingsProvider>();
projectServices.ThreadingPolicy.ExecuteSynchronously(async () =>
{
await launchSettingsProvider.SetActiveProfileAsync(profileName);
});
}
}

public void SetActiveLaunchProfileToVirtualProfile(EnvDTE.Project project) => SetActiveLaunchProfileByName(project, VirtualProfileName);

public IEnumerable<string> GetLaunchProfileNames(EnvDTE.Project project)
{
if (TryGetProjectServices(project, out IUnconfiguredProjectServices unconfiguredProjectServices, out IProjectServices projectServices))
{
Expand All @@ -75,10 +116,11 @@ public static IEnumerable<string> GetLaunchProfileNames(EnvDTE.Project project)
return null;
}

public static IDisposable ListenToLaunchProfileChanges(EnvDTE.Project project, Action listener)
public IDisposable ListenToLaunchProfileChanges(EnvDTE.Project project, Action listener)
{
if (TryGetProjectServices(project, out IUnconfiguredProjectServices unconfiguredProjectServices, out IProjectServices projectServices))
{

var launchSettingsProvider = unconfiguredProjectServices.ExportProvider.GetExportedValue<ILaunchSettingsProvider>();

if (launchSettingsProvider == null)
Expand All @@ -92,20 +134,29 @@ public static IDisposable ListenToLaunchProfileChanges(EnvDTE.Project project, A
return null;
}

public static void SetCpsProjectConfig(EnvDTE.Project project, string arguments, IDictionary<string, string> envVars, string workDir, string launchApp)
public void SetConfig(EnvDTE.Project project, string arguments, IDictionary<string, string> envVars, string workDir, string launchApp)
{
IUnconfiguredProjectServices unconfiguredProjectServices;
IProjectServices projectServices;

if (TryGetProjectServices(project, out unconfiguredProjectServices, out projectServices))
{
var launchSettingsProvider = unconfiguredProjectServices.ExportProvider.GetExportedValue<ILaunchSettingsProvider>();
var activeLaunchProfile = launchSettingsProvider?.ActiveProfile;
ILaunchProfile baseLaunchProfile = null;
if (optionsSettingsService.UseCpsVirtualProfile)
{
baseLaunchProfile = launchSettingsProvider.CurrentSnapshot.Profiles.FirstOrDefault(x => x.Name == VirtualProfileName);
}

if (baseLaunchProfile == null)
{
baseLaunchProfile = launchSettingsProvider?.ActiveProfile;
}

if (activeLaunchProfile == null)
if (baseLaunchProfile == null)
return;

WritableLaunchProfile writableLaunchProfile = new WritableLaunchProfile(activeLaunchProfile);
var writableLaunchProfile = WritableLaunchProfile.GetWritableLaunchProfile(baseLaunchProfile);

if (arguments != null)
writableLaunchProfile.CommandLineArgs = arguments;
Expand All @@ -119,30 +170,31 @@ public static void SetCpsProjectConfig(EnvDTE.Project project, string arguments,
if (launchApp != null)
writableLaunchProfile.CommandName = launchApp;

// Does not work on VS2015, which should be okay ...
// We don't hold references for VS2015, where the interface is called IThreadHandling
IProjectThreadingService projectThreadingService = projectServices.ThreadingPolicy;
projectThreadingService.ExecuteSynchronously(() =>
if (optionsSettingsService.UseCpsVirtualProfile)
{
writableLaunchProfile.Name = VirtualProfileName;
writableLaunchProfile.DoNotPersist = true;
}

projectServices.ThreadingPolicy.ExecuteSynchronously(() =>
{
return launchSettingsProvider.AddOrUpdateProfileAsync(writableLaunchProfile, addToFront: false);
});
}
}

public static List<CmdItemJson> GetCpsProjectAllArguments(EnvDTE.Project project, bool includeArgs, bool includeEnvVars, bool includeWorkDir, bool includeLaunchApp)
public void GetItemsFromConfig(EnvDTE.Project project, List<CmdItemJson> allArgs, bool includeArgs, bool includeEnvVars, bool includeWorkDir, bool includeLaunchApp)
{
IUnconfiguredProjectServices unconfiguredProjectServices;
IProjectServices projectServices;

var result = new List<CmdItemJson>();

if (TryGetProjectServices(project, out unconfiguredProjectServices, out projectServices))
{
var launchSettingsProvider = unconfiguredProjectServices.ExportProvider.GetExportedValue<ILaunchSettingsProvider>();
var launchProfiles = launchSettingsProvider?.CurrentSnapshot?.Profiles;

if (launchProfiles == null)
return result;
return;

foreach (var profile in launchProfiles)
{
Expand Down Expand Up @@ -173,17 +225,18 @@ public static List<CmdItemJson> GetCpsProjectAllArguments(EnvDTE.Project project

if (profileGrp.Items.Count > 0)
{
result.Add(profileGrp);
allArgs.Add(profileGrp);
}
}
}

return result;
}
}

class WritableLaunchProfile : ILaunchProfile
public class WritableLaunchProfile : ILaunchProfile //must be public to avoid having to declare our dynamic assembly a friend
#if VS17 && ! DYNAMIC_VSProjectManaged
, IPersistOption
#endif
{
// ILaunchProfile
public string Name { set; get; }
public string CommandName { set; get; }
public string ExecutablePath { set; get; }
Expand All @@ -194,8 +247,15 @@ class WritableLaunchProfile : ILaunchProfile
public ImmutableDictionary<string, string> EnvironmentVariables { set; get; }
public ImmutableDictionary<string, object> OtherSettings { set; get; }

// IPersistOption
public bool DoNotPersist { get; set; }
#if DYNAMIC_VSProjectManaged
private static Func<ILaunchProfile, bool> LaunchProfileIsDoNotPersistFunc;
#endif
private static Lazy<Type> IPersistOptionType = new Lazy<Type>(() => typeof(ILaunchProfile).Assembly.GetType("Microsoft.VisualStudio.ProjectSystem.Debug.IPersistOption"));
public WritableLaunchProfile(ILaunchProfile launchProfile)
{
// ILaunchProfile
Name = launchProfile.Name;
ExecutablePath = launchProfile.ExecutablePath;
CommandName = launchProfile.CommandName;
Expand All @@ -205,6 +265,79 @@ public WritableLaunchProfile(ILaunchProfile launchProfile)
LaunchUrl = launchProfile.LaunchUrl;
EnvironmentVariables = launchProfile.EnvironmentVariables;
OtherSettings = launchProfile.OtherSettings;
#if VS17
#if DYNAMIC_VSProjectManaged
if (LaunchProfileIsDoNotPersistFunc == null)
{
if (IPersistOptionType.Value == null)
LaunchProfileIsDoNotPersistFunc = (_) => false;
else
{
var instanceParam = Expression.Parameter(typeof(ILaunchProfile));
var asIPersist = Expression.TypeAs(instanceParam, IPersistOptionType.Value);
var expr = Expression.Condition(Expression.Equal(asIPersist, Expression.Constant(null)), Expression.Constant(false), Expression.Property(asIPersist, nameof(DoNotPersist)));
LaunchProfileIsDoNotPersistFunc = Expression.Lambda<Func<ILaunchProfile, bool>>(expr, instanceParam).Compile();
}
}
DoNotPersist = LaunchProfileIsDoNotPersistFunc(launchProfile);

#else
if (launchProfile is IPersistOption persistOptionLaunchProfile)
{
// IPersistOption
DoNotPersist = persistOptionLaunchProfile.DoNotPersist;
}
#endif
#endif
}

private static Func<ILaunchProfile, WritableLaunchProfile> getWritableProfileFunc;
internal static WritableLaunchProfile GetWritableLaunchProfile(ILaunchProfile profile)
{
#if DYNAMIC_VSProjectManaged
if (getWritableProfileFunc == null && IPersistOptionType.Value != null)
{
var ourType = typeof(WritableLaunchProfile);
var asmName = new AssemblyName() { Name = "SmartCLIArgsDynamicAsm" };
asmName.SetPublicKey(ourType.Assembly.GetName().GetPublicKey());
var assemBuilder = AssemblyBuilder.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run);

var classBuilder = assemBuilder.DefineDynamicModule("SmartCLIArgsDynamicMod").DefineType("DynamicWritableLaunchProfile", TypeAttributes.NotPublic | TypeAttributes.Class, ourType);
classBuilder.AddInterfaceImplementation(IPersistOptionType.Value);
// not sure why AssemblyBuilder is a baby true IL code doesn't define interface impelmentations that are just inherited
var persist_get = classBuilder.DefineMethod("get_" + nameof(DoNotPersist), MethodAttributes.Virtual | MethodAttributes.Public, typeof(bool), Type.EmptyTypes);
var il = persist_get.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.EmitCall(OpCodes.Callvirt, ourType.GetMethod(persist_get.Name), null);
il.Emit(OpCodes.Ret);


classBuilder.DefineMethodOverride(persist_get, IPersistOptionType.Value.GetMethod(persist_get.Name));

var constructorArgTypes = new[] { typeof(ILaunchProfile) };
var constructor = classBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, constructorArgTypes);
var baseConstructor = ourType.GetConstructor(constructorArgTypes);
il = constructor.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Call, baseConstructor);
il.Emit(OpCodes.Nop);
il.Emit(OpCodes.Nop);
il.Emit(OpCodes.Ret);
var DynamicWritableLaunchProfileType = classBuilder.CreateType();
var constructorInfo = DynamicWritableLaunchProfileType.GetConstructor(constructorArgTypes);
var instanceParam = Expression.Parameter(typeof(ILaunchProfile));
var expr = Expression.TypeAs(Expression.New(constructorInfo, instanceParam), ourType);
getWritableProfileFunc = Expression.Lambda<Func<ILaunchProfile, WritableLaunchProfile>>(expr, instanceParam).Compile();

}
if (IPersistOptionType.Value != null)
return getWritableProfileFunc(profile);
#endif
return new WritableLaunchProfile(profile);


}

}
}
Loading