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

Component debugger #738

Merged
merged 9 commits into from
Mar 8, 2021
Merged
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Immutable;
using System.ComponentModel.Composition;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using Microsoft.VisualStudio.ProjectSystem;
using Microsoft.VisualStudio.ProjectSystem.VS;

namespace Roslyn.ComponentDebugger
{
[Export]
[AppliesTo("(" + ProjectCapabilities.CSharp + " | " + ProjectCapabilities.VB + ") & !" + ProjectCapabilities.SharedAssetsProject)]
public class CommandLineArgumentsDataSource : UnconfiguredProjectHostBridge<IProjectVersionedValue<IProjectSubscriptionUpdate>, IProjectVersionedValue<ImmutableArray<string>>, IProjectVersionedValue<ImmutableArray<string>>>
{
private readonly IActiveConfiguredProjectSubscriptionService activeProjectSubscriptionService;

[ImportingConstructor]
public CommandLineArgumentsDataSource(IProjectThreadingService projectThreadingService, IActiveConfiguredProjectSubscriptionService activeProjectSubscriptionService)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
public CommandLineArgumentsDataSource(IProjectThreadingService projectThreadingService, IActiveConfiguredProjectSubscriptionService activeProjectSubscriptionService)
public CommandLineArgumentsDataSource(IProjectThreadingServicea? projectThreadingService, IActiveConfiguredProjectSubscriptionService activeProjectSubscriptionService)

Given the projectThreadingService?. below this seems to be the correct annotation

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, its not. the ? is just to shut up the analyzer which doesn't seem to be nullable aware :/

I'll explicitly suppress it though, to make it clear.

: base(projectThreadingService.JoinableTaskContext)
{
this.activeProjectSubscriptionService = activeProjectSubscriptionService;
}

public async Task<ImmutableArray<string>> GetArgsAsync()
{
using (JoinableCollection.Join())
{
await this.InitializeAsync();
return this.AppliedValue?.Value ?? ImmutableArray<string>.Empty;
}
}

protected override bool BlockInitializeOnFirstAppliedValue => true;

protected override Task InitializeInnerCoreAsync(CancellationToken cancellationToken) => Task.CompletedTask;

protected override IDisposable LinkExternalInput(ITargetBlock<IProjectVersionedValue<IProjectSubscriptionUpdate>> targetBlock)
{
JoinUpstreamDataSources(this.activeProjectSubscriptionService.ProjectBuildRuleSource);
return activeProjectSubscriptionService.ProjectBuildRuleSource.SourceBlock.LinkTo(target: targetBlock,
linkOptions: new DataflowLinkOptions { PropagateCompletion = true },
initialDataAsNew: true,
suppressVersionOnlyUpdates: true,
ruleNames: Constants.CommandLineArgsRuleName);
}

protected override Task<IProjectVersionedValue<ImmutableArray<string>>> PreprocessAsync(IProjectVersionedValue<IProjectSubscriptionUpdate> input, IProjectVersionedValue<ImmutableArray<string>>? previousOutput)
{
var description = input.Value.ProjectChanges[Constants.CommandLineArgsRuleName];
return Task.FromResult<IProjectVersionedValue<ImmutableArray<string>>>(new ProjectVersionedValue<ImmutableArray<string>>(description.After.Items.Keys.ToImmutableArray(), input.DataSourceVersions));
}

protected override Task ApplyAsync(IProjectVersionedValue<ImmutableArray<string>> value)
{
AppliedValue = value;
return Task.CompletedTask;
}
}
}
17 changes: 17 additions & 0 deletions src/VisualStudio.Roslyn.SDK/ComponentDebugger/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace Roslyn.ComponentDebugger
{
internal static class Constants
{
public const string RoslynComponentCapability = "RoslynComponent";

public const string CommandName = "DebugRoslynComponent";

public const string TargetProjectPropertyName = "targetProject";
Copy link
Member

Choose a reason for hiding this comment

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

Is this an MSBuild property? If so should be Pascal cased

Copy link
Member Author

Choose a reason for hiding this comment

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

No. Its the name of the property in the json launch settings file, which should be camelCased. I'll try and think of a clearer name.

Copy link
Member

Choose a reason for hiding this comment

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

Ah okay, yeah camelCase is the only answer then.


public const string CommandLineArgsRuleName = "CompilerCommandLineArgs";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.VisualStudio.ProjectSystem;
using Microsoft.VisualStudio.ProjectSystem.Debug;
using Microsoft.VisualStudio.ProjectSystem.VS.Debug;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Threading;
using Task = System.Threading.Tasks.Task;

namespace Roslyn.ComponentDebugger
{
[Export(typeof(IDebugProfileLaunchTargetsProvider))]
[AppliesTo(Constants.RoslynComponentCapability)]
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want to update the projects in the templates to have this capability?

public class DebugProfileProvider : IDebugProfileLaunchTargetsProvider
{
private readonly ConfiguredProject _configuredProject;
private readonly LaunchSettingsManager _launchSettingsManager;
private readonly IProjectThreadingService _threadingService;
private readonly AsyncLazy<string> _compilerRoot;

[ImportingConstructor]
[Obsolete("This exported object must be obtained through the MEF export provider.", error: true)]
public DebugProfileProvider(ConfiguredProject configuredProject, LaunchSettingsManager launchSettingsManager, SVsServiceProvider? serviceProvider, IProjectThreadingService threadingService)
{
_configuredProject = configuredProject;
_launchSettingsManager = launchSettingsManager;
_threadingService = threadingService;

_compilerRoot = new AsyncLazy<string>(() => GetCompilerRootAsync(serviceProvider), _threadingService.JoinableTaskFactory);
}

public Task OnAfterLaunchAsync(DebugLaunchOptions launchOptions, ILaunchProfile profile) => Task.CompletedTask;

public Task OnBeforeLaunchAsync(DebugLaunchOptions launchOptions, ILaunchProfile profile) => Task.CompletedTask;

public bool SupportsProfile(ILaunchProfile? profile) => Constants.CommandName.Equals(profile?.CommandName, StringComparison.Ordinal);

public async Task<IReadOnlyList<IDebugLaunchSettings>> QueryDebugTargetsAsync(DebugLaunchOptions launchOptions, ILaunchProfile? profile)
{
// set up the managed (net fx) debugger to start a process
// https://github.com/dotnet/roslyn-sdk/issues/729
var settings = new DebugLaunchSettings(launchOptions)
{
LaunchDebugEngineGuid = Microsoft.VisualStudio.ProjectSystem.Debug.DebuggerEngines.ManagedOnlyEngine,
LaunchOperation = DebugLaunchOperation.CreateProcess
};

// try and get the target project
var targetProjectUnconfigured = await _launchSettingsManager.TryGetProjectForLaunchAsync(profile);
if (targetProjectUnconfigured is object)
{
settings.CurrentDirectory = Path.GetDirectoryName(targetProjectUnconfigured.FullPath);
var compiler = _configuredProject.Capabilities.Contains(ProjectCapabilities.VB) ? "vbc.exe" : "csc.exe";
var compilerRoot = await _compilerRoot.GetValueAsync();
settings.Executable = Path.Combine(compilerRoot, compiler);

// get its compilation args
var args = await targetProjectUnconfigured.GetCompilationArgumentsAsync();

// append the command line args to the debugger launch
settings.Arguments = string.Join(" ", args);
}

// https://github.com/dotnet/roslyn-sdk/issues/728 : better error handling
return new IDebugLaunchSettings[] { settings };
}

private async Task<string> GetCompilerRootAsync(SVsServiceProvider? serviceProvider)
{
await _threadingService.SwitchToUIThread();
Copy link
Member

Choose a reason for hiding this comment

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

Why do we not need a ConfigureAwait here?

Copy link
Member Author

Choose a reason for hiding this comment

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

The explicit point of this is to change the captured context: .ConfigureAwait(true) would be incorrect, .ConfigureAwait(false) would be redundant, so the analyzer doesn't complain about it.


// https://github.com/dotnet/roslyn-sdk/issues/729 : don't hardcode net fx compiler
object rootDir = string.Empty;
var shell = (IVsShell?)serviceProvider?.GetService(typeof(SVsShell));
shell?.GetProperty((int)__VSSPROPID2.VSSPROPID_InstallRootDir, out rootDir);
return Path.Combine((string)rootDir, "MSBuild", "Current", "Bin", "Roslyn");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace Roslyn.ComponentDebugger
{
internal class DebuggerOptionsViewModel : INotifyPropertyChanged
{
private readonly Action<int> indexChanged;

private IEnumerable<string> _projectNames = ImmutableArray<string>.Empty;

private int _selectedProjectIndex = -1;

public event PropertyChangedEventHandler? PropertyChanged;

public DebuggerOptionsViewModel(Action<int> indexChanged)
{
this.indexChanged = indexChanged;
}

public IEnumerable<string> ProjectNames
{
get => _projectNames;
set
{
_projectNames = value;
NotifyPropertyChanged();
}
}

public int SelectedProjectIndex
{
get => _selectedProjectIndex;
set
{
_selectedProjectIndex = value;
NotifyPropertyChanged();
indexChanged(value);
}
}

private void NotifyPropertyChanged([CallerMemberName]string propertyName = "")
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.ComponentModel.Composition;
using System.Threading.Tasks;
using Microsoft.VisualStudio.ProjectSystem;
using Microsoft.VisualStudio.ProjectSystem.Debug;

namespace Roslyn.ComponentDebugger
{
[Export]
public class LaunchSettingsManager
{
private readonly UnconfiguredProject owningProject;
private readonly IDebugTokenReplacer tokenReplacer;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
private readonly UnconfiguredProject owningProject;
private readonly IDebugTokenReplacer tokenReplacer;
private readonly UnconfiguredProject _owningProject;
private readonly IDebugTokenReplacer _tokenReplacer;


[ImportingConstructor]
public LaunchSettingsManager(UnconfiguredProject owningProject, IDebugTokenReplacer tokenReplacer)
{
this.owningProject = owningProject;
this.tokenReplacer = tokenReplacer;
}

public async Task<UnconfiguredProject?> TryGetProjectForLaunchAsync(ILaunchProfile? profile)
{
UnconfiguredProject? targetProject = null;
object? value = null;
profile?.OtherSettings?.TryGetValue(Constants.TargetProjectPropertyName, out value);

if (value is string targetProjectPath)
{
// expand any variables in the path, and root it based on this project
var replacedProjectPath = await tokenReplacer.ReplaceTokensInStringAsync(targetProjectPath, true);
replacedProjectPath = owningProject.MakeRooted(replacedProjectPath);

targetProject = ((IProjectService2)owningProject.Services.ProjectService).GetLoadedProject(replacedProjectPath);
}
return targetProject;
}

public void WriteProjectForLaunch(IWritableLaunchProfile profile, UnconfiguredProject targetProject)
{
var rootedPath = this.owningProject.MakeRelative(targetProject.FullPath);
profile.OtherSettings[Constants.TargetProjectPropertyName] = rootedPath;
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Immutable;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq;
using System.Windows.Controls;
using Microsoft.VisualStudio.ProjectSystem;
using Microsoft.VisualStudio.ProjectSystem.Debug;
using Microsoft.VisualStudio.Utilities;
using Task = System.Threading.Tasks.Task;

namespace Roslyn.ComponentDebugger
{
[Export(typeof(ILaunchSettingsUIProvider))]
[AppliesTo(Constants.RoslynComponentCapability)]
public class LaunchSettingsProvider : ILaunchSettingsUIProvider
{
private readonly IProjectThreadingService _threadingService;
private readonly UnconfiguredProject _unconfiguredProject;
private readonly LaunchSettingsManager _launchSettingsManager;
private readonly DebuggerOptionsViewModel _viewModel;

private ImmutableArray<UnconfiguredProject> _projects;
private IWritableLaunchProfile? _launchProfile;

[ImportingConstructor]
[Obsolete("This exported object must be obtained through the MEF export provider.", error: true)]
public LaunchSettingsProvider(IProjectThreadingService threadingService, UnconfiguredProject unconfiguredProject, LaunchSettingsManager launchSettingsManager)
{
_threadingService = threadingService;
_unconfiguredProject = unconfiguredProject;
_launchSettingsManager = launchSettingsManager;
_viewModel = new DebuggerOptionsViewModel(IndexChanged);
}

public string CommandName { get => Constants.CommandName; }

// https://github.com/dotnet/roslyn-sdk/issues/730 : localization
public string FriendlyName { get => "Roslyn Component"; }

public UserControl? CustomUI { get => new DebuggerOptions() { DataContext = _viewModel }; }

public void ProfileSelected(IWritableLaunchSettings curSettings)
{
_launchProfile = curSettings?.ActiveProfile;
_threadingService.ExecuteSynchronously(UpdateViewModelAsync);
}

public bool ShouldEnableProperty(string propertyName)
{
// we disable all the default options for a debugger.
// in the future we might want to enable env vars and (potentially) the exe to allow
// customization of the compiler used?
return false;
}

private async Task UpdateViewModelAsync()
{
var targetProjects = ArrayBuilder<UnconfiguredProject>.GetInstance();

// get the output assembly for this project
var projectArgs = await _unconfiguredProject.GetCompilationArgumentsAsync().ConfigureAwait(false);
var targetArg = projectArgs.LastOrDefault(a => a.StartsWith("/out:", StringComparison.OrdinalIgnoreCase));
var target = Path.GetFileName(targetArg);

var projectService = _unconfiguredProject.Services.ProjectService;
foreach (var targetProjectUnconfigured in projectService.LoadedUnconfiguredProjects)
{
// check if the args contain the project as an analyzer ref
foreach (var arg in await targetProjectUnconfigured.GetCompilationArgumentsAsync().ConfigureAwait(false))
{
if (arg.StartsWith("/analyzer", StringComparison.OrdinalIgnoreCase)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (arg.StartsWith("/analyzer", StringComparison.OrdinalIgnoreCase)
if (arg.StartsWith("/analyzer:", StringComparison.OrdinalIgnoreCase)

To be consistent with the StartsWith above.

&& arg.EndsWith(target, StringComparison.OrdinalIgnoreCase))
{
targetProjects.Add(targetProjectUnconfigured);
}
}
}
_projects = targetProjects.ToImmutableAndFree();

var launchTargetProject = await _launchSettingsManager.TryGetProjectForLaunchAsync(_launchProfile?.ToLaunchProfile());
var index = _projects.IndexOf(launchTargetProject!);

_viewModel.ProjectNames = _projects.Select(p => Path.GetFileNameWithoutExtension(p.FullPath));
_viewModel.SelectedProjectIndex = index;
}

private void IndexChanged(int newIndex)
{
if (_launchProfile is object && !_projects.IsDefaultOrEmpty && newIndex >= 0 && newIndex < _projects.Length)
{
var project = _projects[newIndex];
_launchSettingsManager.WriteProjectForLaunch(_launchProfile, project);
}
}
}
}
Loading