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

Add component debugger #726

Merged
merged 12 commits into from
Feb 17, 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
7 changes: 7 additions & 0 deletions Roslyn-SDK.sln
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "Microsoft.CodeAnalysis.Visu
EndProject
Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "Microsoft.CodeAnalysis.VisualBasic.SourceGenerators.Testing.XUnit.UnitTests", "tests\Microsoft.CodeAnalysis.Testing\Microsoft.CodeAnalysis.VisualBasic.SourceGenerators.Testing.XUnit.UnitTests\Microsoft.CodeAnalysis.VisualBasic.SourceGenerators.Testing.XUnit.UnitTests.vbproj", "{92BD1781-5DB4-4F72-BCCB-0D64C0790A2B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Roslyn.ComponentDebugger", "src\VisualStudio.Roslyn.SDK\ComponentDebugger\Roslyn.ComponentDebugger.csproj", "{7E91C1C7-A836-4378-8ABC-9298C13228D1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -539,6 +541,10 @@ Global
{92BD1781-5DB4-4F72-BCCB-0D64C0790A2B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{92BD1781-5DB4-4F72-BCCB-0D64C0790A2B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{92BD1781-5DB4-4F72-BCCB-0D64C0790A2B}.Release|Any CPU.Build.0 = Release|Any CPU
{7E91C1C7-A836-4378-8ABC-9298C13228D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7E91C1C7-A836-4378-8ABC-9298C13228D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7E91C1C7-A836-4378-8ABC-9298C13228D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7E91C1C7-A836-4378-8ABC-9298C13228D1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -635,6 +641,7 @@ Global
{7D9C0EF5-7383-4E35-811B-3288B3C806F3} = {9905147E-CC1F-42A0-BD27-05586C583DF7}
{7C3FE60E-055B-4E0C-BB85-C7E94A640074} = {9905147E-CC1F-42A0-BD27-05586C583DF7}
{92BD1781-5DB4-4F72-BCCB-0D64C0790A2B} = {9905147E-CC1F-42A0-BD27-05586C583DF7}
{7E91C1C7-A836-4378-8ABC-9298C13228D1} = {F9B73995-76C6-4056-ADA9-18342F951361}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {56695AA9-EA80-47A7-8562-E51285906C54}
Expand Down
2 changes: 1 addition & 1 deletion eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<UsingToolNetFrameworkReferenceAssemblies>true</UsingToolNetFrameworkReferenceAssemblies>
<UsingToolMicrosoftNetCompilers>true</UsingToolMicrosoftNetCompilers>
<UsingToolSymbolUploader>true</UsingToolSymbolUploader>
<MicrosoftNetCompilersToolsetVersion>3.8.0-4.20464.1</MicrosoftNetCompilersToolsetVersion>
<MicrosoftNetCompilersToolsetVersion>3.8.0-5.final</MicrosoftNetCompilersToolsetVersion>
<!-- Force prior version due to https://github.com/microsoft/vstest/pull/2192 and https://github.com/microsoft/vstest/pull/2067 -->
<MicrosoftNETTestSdkVersion>16.1.1</MicrosoftNETTestSdkVersion>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 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)]
public class CapabilityProvider : ConfiguredProjectCapabilitiesProviderBase
{
[ImportingConstructor]
[System.Obsolete("This exported object must be obtained through the MEF export provider.", error: true)]
public CapabilityProvider(ConfiguredProject configuredProject)
: base(nameof(CapabilityProvider), configuredProject)
{
}

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;
if (await IsRoslynComponentAsync(this.ConfiguredProject, cancellationToken).ConfigureAwait(false))
Copy link
Member

@davkean davkean Feb 16, 2021

Choose a reason for hiding this comment

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

This is mixing dataflow and live project data, which you can't do.

You need to listen to and pull on the ProjectInstance to get the property via dataflow, here's an example: https://github.com/dotnet/project-system/blob/b7fd4c439d1d39b656d89a8b88464fabffcc8e59/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/Web/ProjectTypeGuidsDataSource.cs.

Copy link
Member Author

Choose a reason for hiding this comment

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

@davkean Oh, interesting. There are classes in the devdiv repo that use this exact pattern. The base class seems to subscribe to events, so I figured it was designed to explicitly not need dataflow stuff.

I'll try and use the example to create a dataflow version.

{
caps = caps.Add(Constants.RoslynComponentCapability);
}
return caps;
}

private static Task<bool> IsRoslynComponentAsync(ConfiguredProject configuredProject, CancellationToken token = default)
=> configuredProject.Services.ProjectLockService.ReadLockAsync(
async access =>
{
var project = await access.GetProjectAsync(configuredProject).ConfigureAwait(false);
var isRoslynComponentProperty = project.GetProperty(Constants.RoslynComponentPropertyName);
var isComponent = string.Compare(isRoslynComponentProperty?.EvaluatedValue.Trim(), "true", System.StringComparison.OrdinalIgnoreCase) == 0;
return isComponent;
},
token);
}
}
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 RoslynComponentPropertyName = "IsRoslynComponent";

public const string RoslynComponentCapability = "RoslynComponent";

public const string CommandName = "DebugRoslynComponent";

public const string TargetProjectPropertyName = "targetProject";
}
}
105 changes: 105 additions & 0 deletions src/VisualStudio.Roslyn.SDK/ComponentDebugger/DebugProfileProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// 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 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 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 = GetCompilerRoot(serviceProvider);
}

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.OrdinalIgnoreCase);

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";
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);
}
}

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

private static string GetCompilerRoot(SVsServiceProvider? serviceProvider)
{
// 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;
}
}
}
19 changes: 19 additions & 0 deletions src/VisualStudio.Roslyn.SDK/ComponentDebugger/DebuggerOptions.xaml
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)));
}
}
}
}
}
Loading