-
Notifications
You must be signed in to change notification settings - Fork 256
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
Add component debugger #726
Changes from all commits
ce5ccd6
b079869
5ef7fee
f2ddb61
c86f8bd
92e3b89
ffd65a0
f78a1b7
492c07c
65f8b41
2af0f11
4dcffa0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
// 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.Collections.Immutable; | ||
using System.ComponentModel.Composition; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.VisualStudio.ProjectSystem; | ||
|
||
namespace Roslyn.ComponentDebugger | ||
{ | ||
[Export(ExportContractNames.Scopes.ConfiguredProject, typeof(IProjectCapabilitiesProvider))] | ||
[AppliesTo(ProjectCapabilities.CSharp + " | " + ProjectCapabilities.VB)] | ||
public class CapabilityProvider : ConfiguredProjectCapabilitiesProviderBase | ||
{ | ||
private readonly IProjectSnapshotService snapshotService; | ||
|
||
[ImportingConstructor] | ||
[System.Obsolete("This exported object must be obtained through the MEF export provider.", error: true)] | ||
public CapabilityProvider(ConfiguredProject configuredProject, IProjectSnapshotService snapshotService) | ||
: base(nameof(CapabilityProvider), configuredProject) | ||
{ | ||
this.snapshotService = snapshotService; | ||
} | ||
|
||
protected override async Task<ImmutableHashSet<string>> GetCapabilitiesAsync(CancellationToken cancellationToken) | ||
{ | ||
// an alternative design could be to have 'IsRoslynComponent' just define the <Capability... directly in the managed.core targets | ||
// but that would require a specific roslyn version to work, this allows it to be backwards compatible with older SDKs | ||
var caps = Empty.CapabilitiesSet; | ||
|
||
var snapshot = await snapshotService.GetLatestVersionAsync(ConfiguredProject, cancellationToken: cancellationToken).ConfigureAwait(false); | ||
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. Thanks @davkean and @tmeschter. I switched the cap provider over to this, which is the same way the 'get command line args' stuff works. Is that a valid way to handle getting it via dataflow? 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. No. This needs to chain similar to what I pointed out here: https://github.com/dotnet/project-system/blob/feature/AspNet/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/Web/ProjectTypeGuidsDataSource.cs. These systems flow data from one data source -> another, you need to transform the data as it flows along the pipe. This code just grabs the latest version from the snapshot pipe and sets up no relationship between the pipes, so we never know to call this pipe when the data changes. 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. But isn't that handled by the base class? It sets up the pipe stuff and calls this method as it changes? I guess thats the bit I don't understand. 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. No. Here you basically saying "I am the start of a pipe, and I will tell you when there are changes". What you want to say is "My pipe is based on the snapshot data, so when it changes, call me". 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 stuff is complicated and hard to get right - I can sit down and write it with you. |
||
var isRoslynComponentProperty = snapshot.Value.ProjectInstance.GetPropertyValue(Constants.RoslynComponentPropertyName); | ||
var isComponent = string.Compare(isRoslynComponentProperty.Trim(), "true", System.StringComparison.OrdinalIgnoreCase) == 0; | ||
if (isComponent) | ||
{ | ||
caps = caps.Add(Constants.RoslynComponentCapability); | ||
} | ||
return caps; | ||
} | ||
} | ||
} |
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 RoslynComponentPropertyName = "IsRoslynComponent"; | ||
|
||
public const string RoslynComponentCapability = "RoslynComponent"; | ||
|
||
public const string CommandName = "DebugRoslynComponent"; | ||
|
||
public const string TargetProjectPropertyName = "targetProject"; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
// 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)] | ||
public class DebugProfileProvider : IDebugProfileLaunchTargetsProvider | ||
{ | ||
private readonly ConfiguredProject _configuredProject; | ||
private readonly IDebugTokenReplacer _tokenReplacer; | ||
private readonly AsyncLazy<string> _compilerRoot; | ||
|
||
[ImportingConstructor] | ||
[Obsolete("This exported object must be obtained through the MEF export provider.", error: true)] | ||
public DebugProfileProvider(ConfiguredProject configuredProject, IDebugTokenReplacer tokenReplacer, SVsServiceProvider? serviceProvider) | ||
{ | ||
_configuredProject = configuredProject; | ||
_tokenReplacer = tokenReplacer; | ||
_compilerRoot = new AsyncLazy<string>(() => GetCompilerRootAsync(serviceProvider), ThreadHelper.JoinableTaskFactory); | ||
} | ||
chsienki marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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 TryGetTargetProjectAsync(profile).ConfigureAwait(false); | ||
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().ConfigureAwait(false); | ||
settings.Executable = Path.Combine(compilerRoot, compiler); | ||
|
||
// try and get the configured version of the target project | ||
var targetProject = await targetProjectUnconfigured.GetSuggestedConfiguredProjectAsync().ConfigureAwait(false); | ||
if (targetProject is object) | ||
{ | ||
// get its compilation args | ||
var args = await targetProject.GetCompilationArgumentsAsync().ConfigureAwait(false); | ||
|
||
// append the command line args to the debugger launch | ||
settings.Arguments = string.Join(" ", args); | ||
chsienki marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
// https://github.com/dotnet/roslyn-sdk/issues/728 : better error handling | ||
return new IDebugLaunchSettings[] { settings }; | ||
} | ||
|
||
private static async Task<string> GetCompilerRootAsync(SVsServiceProvider? serviceProvider) | ||
{ | ||
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); | ||
|
||
// https://github.com/dotnet/roslyn-sdk/issues/729 | ||
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"); | ||
} | ||
|
||
private async Task<UnconfiguredProject?> TryGetTargetProjectAsync(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).ConfigureAwait(false); | ||
replacedProjectPath = _configuredProject.UnconfiguredProject.MakeRooted(replacedProjectPath); | ||
|
||
targetProject = _configuredProject.Services.ProjectService.LoadedUnconfiguredProjects.SingleOrDefault(p => p.FullPath == replacedProjectPath); | ||
} | ||
|
||
return targetProject; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<UserControl x:Class="Roslyn.ComponentDebugger.DebuggerOptions" | ||
x:ClassModifier="internal" | ||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" | ||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" | ||
Width="Auto" | ||
HorizontalAlignment="Left" | ||
VerticalAlignment="Top"> | ||
<Grid Margin="0,2,0,0"> | ||
<Grid.ColumnDefinitions> | ||
<ColumnDefinition Width="158" /> | ||
<ColumnDefinition Width="350" /> | ||
<ColumnDefinition Width="Auto" /> | ||
</Grid.ColumnDefinitions> | ||
|
||
<!-- https://github.com/dotnet/roslyn-sdk/issues/730 : Localization --> | ||
<Label Margin="4,4,3,5">Target Project:</Label> | ||
<ComboBox Grid.Column="1" Margin="5,7,2,6" ItemsSource="{Binding ProjectNames}" SelectedIndex="{Binding SelectedProjectIndex, Mode=TwoWay}" /> | ||
</Grid> | ||
</UserControl> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// 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.Windows.Controls; | ||
|
||
namespace Roslyn.ComponentDebugger | ||
{ | ||
/// <summary> | ||
/// Interaction logic for DebuggerOptions.xaml | ||
/// </summary> | ||
internal sealed partial class DebuggerOptions : UserControl | ||
{ | ||
public DebuggerOptions() | ||
{ | ||
InitializeComponent(); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
// 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.IO; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
using Microsoft.VisualStudio.ProjectSystem; | ||
using Microsoft.VisualStudio.ProjectSystem.Debug; | ||
|
||
namespace Roslyn.ComponentDebugger | ||
{ | ||
internal class DebuggerOptionsViewModel : INotifyPropertyChanged | ||
{ | ||
private IWritableLaunchProfile? _launchProfile; | ||
|
||
private readonly ImmutableArray<ConfiguredProject> _targetProjects; | ||
|
||
private readonly IEnumerable<string> _targetProjectNames; | ||
|
||
public event PropertyChangedEventHandler? PropertyChanged; | ||
|
||
public DebuggerOptionsViewModel(ImmutableArray<ConfiguredProject> targetProjects) | ||
{ | ||
_targetProjects = targetProjects; | ||
_targetProjectNames = _targetProjects.Select(t => Path.GetFileNameWithoutExtension(t.UnconfiguredProject.FullPath)); | ||
} | ||
|
||
public IEnumerable<string> ProjectNames { get => _targetProjectNames; } | ||
|
||
public IWritableLaunchProfile? LaunchProfile | ||
{ | ||
get => _launchProfile; | ||
set | ||
{ | ||
_launchProfile = value; | ||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedProjectIndex))); | ||
} | ||
} | ||
|
||
public int SelectedProjectIndex | ||
{ | ||
get | ||
{ | ||
if (LaunchProfile?.OtherSettings.ContainsKey(Constants.TargetProjectPropertyName) == true) | ||
{ | ||
var target = LaunchProfile.OtherSettings[Constants.TargetProjectPropertyName].ToString(); | ||
for (var i = 0; i < _targetProjects.Length; i++) | ||
{ | ||
if (_targetProjects[i].UnconfiguredProject.FullPath.Equals(target, StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
return i; | ||
} | ||
} | ||
} | ||
return -1; | ||
} | ||
set | ||
{ | ||
if (LaunchProfile is object) | ||
{ | ||
var newTargetProject = _targetProjects[value].UnconfiguredProject; | ||
LaunchProfile.OtherSettings[Constants.TargetProjectPropertyName] = newTargetProject.FullPath; | ||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedProjectIndex))); | ||
} | ||
} | ||
} | ||
} | ||
} |
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.
This component will load for every C# and VB project, we need a better approach for this. Is there something unique about these projects?
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.
This also loads for Shared Projects.
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.
It can apply to any project that has the
<IsRoslyComponent>true</IsRoslynComponent>
. So we load the provider for all projects and add the capability to any that match that.Is there some better way to achieve that? I mentioned in the comments we could do it via the targets, but that does require a matching roslyn version.
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.
Is there a targets file that it imports we can match on?
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.
No, its just a regular old csharp project.
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.
Can you expand on what you mean by this? You mean if the project itself has a package reference to a different version of the compiler?
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.
Yeah. We could drop the capability provider altogether if we just added the following to the targets file
But anyone using an older version of the compiler via NuGet wouldn't get the capability, and be able to use it. That might be worth the tradeoff to remove some of the complexity here tho.