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

Use a named pipe to communicate projectinfo in vscode #10521

Merged
merged 34 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
5b3f408
Update ProjectInfo synchronization to use a named pipe instead of file
ryzngard Jun 20, 2024
1dbd46d
Clean up and move some logic into extension methods
ryzngard Jun 21, 2024
1718f43
Use pooled arrays
ryzngard Jun 21, 2024
cbe4b94
Remove file based project info settings
ryzngard Jun 22, 2024
b872a10
Woops
ryzngard Jun 22, 2024
16c6010
Make asynchronous
ryzngard Jun 22, 2024
c5a5faa
Fix tests and add serialization test
ryzngard Jun 22, 2024
d641652
Update src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Projec…
ryzngard Jun 24, 2024
c48aac9
PR feedback. Allocate less. Bump some logs to information. Dispose st…
ryzngard Jun 24, 2024
c32f25f
Merge branch 'named_pipe' of https://github.com/ryzngard/razor into n…
ryzngard Jun 24, 2024
98c99c6
Add ownership validation
ryzngard Jun 24, 2024
7c006a8
Stackalloc
ryzngard Jun 24, 2024
4a445c2
Stackalloc for writing size
ryzngard Jun 24, 2024
2985b2e
usings...
ryzngard Jun 24, 2024
7cde45f
Dispose named pipe
ryzngard Jun 24, 2024
3aee1bf
Clean up and move to abstract to make testing easier
ryzngard Jun 25, 2024
c8fe2dc
PR feedback and publicapi fixes
ryzngard Jun 25, 2024
2a675c3
Update src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Projec…
ryzngard Jun 25, 2024
6c4c79e
Move to RZLS
ryzngard Jun 26, 2024
6717529
Remove because cut/paste doesn't in VS
ryzngard Jun 26, 2024
537b019
Be more clear about async work. Use local functions to help scope
ryzngard Jun 26, 2024
e915417
Copilot forgot my using!
ryzngard Jun 26, 2024
60f3661
Clean up loop expectation
ryzngard Jun 26, 2024
1979f0e
Fix tests
ryzngard Jun 27, 2024
05ab5ff
Update PublicAPI.Unshipped.txt
ryzngard Jun 27, 2024
3fa3205
Productive discussion! Let's gooooo!
ryzngard Jun 27, 2024
22c52d5
LKJDLKSJFLKSJDFLKJDFLJSLFKDJSLDKFJ:LKj:LjKL:DJl;jak;ljfsdkl;ajfa;lksj…
ryzngard Jun 27, 2024
7103fb1
Be more clear about disposal state
ryzngard Jun 27, 2024
d548c02
Use disposal token
ryzngard Jun 27, 2024
e6cc53a
PR feedback
ryzngard Jun 27, 2024
36dc320
Merge branch 'main' into named_pipe
ryzngard Jun 27, 2024
81de399
Update src/Razor/src/Microsoft.AspNetCore.Razor.ExternalAccess.Roslyn…
ryzngard Jun 27, 2024
88a359c
Feedback
ryzngard Jun 27, 2024
1f9ae6d
Merge branch 'named_pipe' of https://github.com/ryzngard/razor into n…
ryzngard Jun 27, 2024
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
@@ -1,6 +1,6 @@
#nullable enable
Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.RazorWorkspaceListener
Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.RazorWorkspaceListener.Dispose() -> void
Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.RazorWorkspaceListener.EnsureInitialized(Microsoft.CodeAnalysis.Workspace! workspace, string! projectInfoFileName) -> void
Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.RazorWorkspaceListener.EnsureInitialized(Microsoft.CodeAnalysis.Workspace! workspace, string! pipeName) -> void
Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.RazorWorkspaceListener.NotifyDynamicFile(Microsoft.CodeAnalysis.ProjectId! projectId) -> void
Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.RazorWorkspaceListener.RazorWorkspaceListener(Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,31 @@

namespace Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace;

internal static class RazorProjectInfoSerializer
internal static class RazorProjectInfoFactory
{
private static readonly StringComparison s_stringComparison;

static RazorProjectInfoSerializer()
static RazorProjectInfoFactory()
{
s_stringComparison = RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
? StringComparison.Ordinal
: StringComparison.OrdinalIgnoreCase;
}

public static async Task SerializeAsync(Project project, string configurationFileName, ILogger? logger, CancellationToken cancellationToken)
public static async Task<RazorProjectInfo?> ConvertAsync(Project project, ILogger? logger, CancellationToken cancellationToken)
{
var projectPath = Path.GetDirectoryName(project.FilePath);
if (projectPath is null)
{
logger?.LogTrace("projectPath is null, skipping writing info for {projectId}", project.Id);
return;
logger?.LogTrace("projectPath is null, skip conversion for {projectId}", project.Id);
ryzngard marked this conversation as resolved.
Show resolved Hide resolved
return null;
}

var intermediateOutputPath = Path.GetDirectoryName(project.CompilationOutputInfo.AssemblyPath);
if (intermediateOutputPath is null)
{
logger?.LogTrace("intermediatePath is null, skipping writing info for {projectId}", project.Id);
return;
logger?.LogTrace("intermediatePath is null, skip conversion for {projectId}", project.Id);
return null;
}

// First, lets get the documents, because if there aren't any, we can skip out early
Expand All @@ -53,7 +53,7 @@ public static async Task SerializeAsync(Project project, string configurationFil
if (documents.Length == 0)
{
logger?.LogTrace("No razor documents for {projectId}", project.Id);
return;
return null;
}

var csharpLanguageVersion = (project.ParseOptions as CSharpParseOptions)?.LanguageVersion ?? LanguageVersion.Default;
Expand Down Expand Up @@ -86,18 +86,14 @@ public static async Task SerializeAsync(Project project, string configurationFil

var projectWorkspaceState = ProjectWorkspaceState.Create(tagHelpers, csharpLanguageVersion);

var configurationFilePath = Path.Combine(intermediateOutputPath, configurationFileName);

var projectInfo = new RazorProjectInfo(
return new RazorProjectInfo(
projectKey: new ProjectKey(intermediateOutputPath),
filePath: project.FilePath!,
configuration: configuration,
rootNamespace: defaultNamespace,
displayName: project.Name,
projectWorkspaceState: projectWorkspaceState,
documents: documents);

WriteToFile(configurationFilePath, projectInfo, logger);
}

private static RazorConfiguration ComputeRazorConfigurationOptions(AnalyzerConfigOptionsProvider options, ILogger? logger, out string defaultNamespace)
Expand Down Expand Up @@ -126,38 +122,6 @@ private static RazorConfiguration ComputeRazorConfigurationOptions(AnalyzerConfi
return razorConfiguration;
}

private static void WriteToFile(string configurationFilePath, RazorProjectInfo projectInfo, ILogger? logger)
{
// We need to avoid having an incomplete file at any point, but our
// project configuration is large enough that it will be written as multiple operations.
var tempFilePath = string.Concat(configurationFilePath, ".temp");
var tempFileInfo = new FileInfo(tempFilePath);

if (tempFileInfo.Exists)
{
// This could be caused by failures during serialization or early process termination.
logger?.LogTrace("deleting existing file {filePath}", tempFilePath);
tempFileInfo.Delete();
}

// This needs to be in explicit brackets because the operation needs to be completed
// by the time we move the temp file into its place
using (var stream = tempFileInfo.Create())
{
projectInfo.SerializeTo(stream);
}

var fileInfo = new FileInfo(configurationFilePath);
if (fileInfo.Exists)
{
logger?.LogTrace("deleting existing file {filePath}", configurationFilePath);
fileInfo.Delete();
}

logger?.LogTrace("Moving {tmpPath} to {newPath}", tempFilePath, configurationFilePath);
File.Move(tempFileInfo.FullName, configurationFilePath);
}

internal static ImmutableArray<DocumentSnapshotHandle> GetDocuments(Project project, string projectPath)
{
using var documents = new PooledArrayBuilder<DocumentSnapshotHandle>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license. See License.txt in the project root for license information.

using System.Collections.Immutable;
using System.IO.Pipes;
using Microsoft.AspNetCore.Razor.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.Extensions.Logging;
Expand All @@ -13,20 +14,24 @@ public class RazorWorkspaceListener : IDisposable
private static readonly TimeSpan s_debounceTime = TimeSpan.FromMilliseconds(500);

private readonly ILogger _logger;

private string? _projectInfoFileName;
private Workspace? _workspace;

// Use an immutable dictionary for ImmutableInterlocked operations. The value isn't checked, just
// the existance of the key so work is only done for projects with dynamic files.
private ImmutableDictionary<ProjectId, bool> _projectsWithDynamicFile = ImmutableDictionary<ProjectId, bool>.Empty;
private readonly CancellationTokenSource _disposeTokenSource = new();
private readonly AsyncBatchingWorkQueue<ProjectId> _workQueue;
private readonly AsyncBatchingWorkQueue<Work> _workQueue;

record Work(ProjectId ProjectId);
ryzngard marked this conversation as resolved.
Show resolved Hide resolved
record UpdateWork(ProjectId ProjectId) : Work(ProjectId);
record RemovalWork(ProjectId ProjectId, string IntermediateOutputPath) : Work(ProjectId);

private Stream? _stream;

public RazorWorkspaceListener(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger(nameof(RazorWorkspaceListener));
_workQueue = new(TimeSpan.FromMilliseconds(500), UpdateCurrentProjectsAsync, EqualityComparer<ProjectId>.Default, _disposeTokenSource.Token);
_workQueue = new(TimeSpan.FromMilliseconds(500), UpdateCurrentProjectsAsync, EqualityComparer<Work>.Default, _disposeTokenSource.Token);
}

public void Dispose()
ryzngard marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -45,17 +50,38 @@ public void Dispose()
_disposeTokenSource.Dispose();
}

public void EnsureInitialized(Workspace workspace, string projectInfoFileName)
public void EnsureInitialized(Workspace workspace, string pipeName)
jaredpar marked this conversation as resolved.
Show resolved Hide resolved
{
// Make sure we don't hook up the event handler multiple times
ryzngard marked this conversation as resolved.
Show resolved Hide resolved
if (_stream is not null)
{
return;
}

_logger.LogTrace("Opening named pipe server: {0}", pipeName);
Copy link
Member

Choose a reason for hiding this comment

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

Do all the processes write to a single log stream or does each one write to a different one? If different then strongly suggest you include the process id in any named pipe logging. Will save you hair pulling exercises in the future when trying to figure out which process is doing what 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In VS Code this goes to the vs code output window, so I think we'll be okay. We don't write to a file on disk :)

var stream = new NamedPipeServerStream(
pipeName,
PipeDirection.Out,
maxNumberOfServerInstances: 1,
PipeTransmissionMode.Byte,
PipeOptions.CurrentUserOnly | PipeOptions.Asynchronous);

EnsureInitialized(workspace, stream);
}

// Internal for testing
internal void EnsureInitialized(Workspace workspace, Stream stream)
{
// Make sure we don't hook up the event handler multiple times
if (_projectInfoFileName is not null)
if (_stream is not null)
{
return;
}

_projectInfoFileName = projectInfoFileName;
_workspace = workspace;
_workspace.WorkspaceChanged += Workspace_WorkspaceChanged;

_stream = stream;
}

public void NotifyDynamicFile(ProjectId projectId)
Expand All @@ -72,7 +98,7 @@ public void NotifyDynamicFile(ProjectId projectId)

// Schedule a task, in case adding a dynamic file is the last thing that happens
_logger.LogTrace("{projectId} scheduling task due to dynamic file", projectId);
_workQueue.AddWork(projectId);
_workQueue.AddWork(new UpdateWork(projectId));
}

private void Workspace_WorkspaceChanged(object? sender, WorkspaceChangeEventArgs e)
Expand Down Expand Up @@ -101,7 +127,7 @@ private void Workspace_WorkspaceChanged(object? sender, WorkspaceChangeEventArgs
break;

case WorkspaceChangeKind.ProjectRemoved:
RemoveProject(e.ProjectId.AssumeNotNull());
RemoveProject(e.OldSolution.GetProject(e.ProjectId.AssumeNotNull()).AssumeNotNull());
break;

case WorkspaceChangeKind.ProjectAdded:
Expand Down Expand Up @@ -131,7 +157,7 @@ private void Workspace_WorkspaceChanged(object? sender, WorkspaceChangeEventArgs
case WorkspaceChangeKind.SolutionRemoved:
foreach (var project in e.OldSolution.Projects)
{
RemoveProject(project.Id);
RemoveProject(project);
}

break;
Expand All @@ -141,14 +167,24 @@ private void Workspace_WorkspaceChanged(object? sender, WorkspaceChangeEventArgs
}
}

private void RemoveProject(ProjectId projectId)
private void RemoveProject(Project project)
{
ImmutableInterlocked.TryRemove(ref _projectsWithDynamicFile, projectId, out var _);
if (ImmutableInterlocked.TryRemove(ref _projectsWithDynamicFile, project.Id, out var _))
{
var intermediateOutputPath = Path.GetDirectoryName(project.CompilationOutputInfo.AssemblyPath);
if (intermediateOutputPath is null)
{
_logger?.LogTrace("intermediatePath is null, skipping notification of removal for {projectId}", project.Id);
return;
}

_workQueue.AddWork(new RemovalWork(project.Id, intermediateOutputPath));
}
}

private void EnqueueUpdate(Project? project)
{
if (_projectInfoFileName is null ||
if (_stream is null ||
project is not
{
Language: LanguageNames.CSharp
Expand All @@ -164,35 +200,63 @@ private void EnqueueUpdate(Project? project)
}

var projectId = project.Id;
_workQueue.AddWork(projectId);
_workQueue.AddWork(new UpdateWork(projectId));
}

private async ValueTask UpdateCurrentProjectsAsync(ImmutableArray<ProjectId> projectIds, CancellationToken cancellationToken)
private async ValueTask UpdateCurrentProjectsAsync(ImmutableArray<Work> work, CancellationToken cancellationToken)
{
var solution = _workspace.AssumeNotNull().CurrentSolution;
_stream.AssumeNotNull();

foreach (var projectId in projectIds)
foreach (var unit in work)
{
if (_disposeTokenSource.IsCancellationRequested)
try
{
return;
}
if (_disposeTokenSource.IsCancellationRequested)
{
return;
}

if (unit is RemovalWork removalWork)
{
await ReportRemovalAsync(removalWork, cancellationToken).ConfigureAwait(false);
}

var project = solution.GetProject(projectId);
if (project is null)
var project = solution.GetProject(unit.ProjectId);
if (project is null)
{
_logger?.LogTrace("Project {projectId} is not in workspace", unit.ProjectId);
continue;
}

await UpdateProjectAsync(project, solution, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger?.LogTrace("Project {projectId} is not in workspace", projectId);
continue;
_logger?.LogError(ex, "Encountered exception while processing unit: {message}", ex.Message);
}

await SerializeProjectAsync(project, solution, cancellationToken).ConfigureAwait(false);
}

await _stream.AssumeNotNull().FlushAsync(cancellationToken).ConfigureAwait(false);
}

private Task ReportRemovalAsync(RemovalWork unit, CancellationToken cancellationToken)
{
_logger?.LogTrace("Reporting removal of {projectId}", unit.ProjectId);
return _stream.AssumeNotNull().WriteProjectInfoRemovalAsync(unit.IntermediateOutputPath, cancellationToken);
}

// private protected for testing
private protected virtual Task SerializeProjectAsync(Project project, Solution solution, CancellationToken cancellationToken)
private protected virtual async Task UpdateProjectAsync(Project project, Solution solution, CancellationToken cancellationToken)
{
_logger?.LogTrace("Serializing information for {projectId}", project.Id);
return RazorProjectInfoSerializer.SerializeAsync(project, _projectInfoFileName.AssumeNotNull(), _logger, cancellationToken);
var projectInfo = await RazorProjectInfoFactory.ConvertAsync(project, _logger, cancellationToken).ConfigureAwait(false);
if (projectInfo is null)
{
_logger?.LogTrace("Skipped writing data for {projectId}", project.Id);
return;
}

await _stream.AssumeNotNull().WriteProjectInfoAsync(projectInfo, cancellationToken).ConfigureAwait(false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ internal class DefaultLanguageServerFeatureOptions : LanguageServerFeatureOption

public override bool SupportsFileManipulation => true;

public override string ProjectConfigurationFileName => "project.razor.bin";

public override string CSharpVirtualDocumentSuffix => DefaultCSharpVirtualDocumentSuffix;

public override string HtmlVirtualDocumentSuffix => DefaultHtmlVirtualDocumentSuffix;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ internal class ConfigurableLanguageServerFeatureOptions : LanguageServerFeatureO
private readonly LanguageServerFeatureOptions _defaults = new DefaultLanguageServerFeatureOptions();

private readonly bool? _supportsFileManipulation;
private readonly string? _projectConfigurationFileName;
private readonly string? _csharpVirtualDocumentSuffix;
private readonly string? _htmlVirtualDocumentSuffix;
private readonly bool? _singleServerCompletionSupport;
Expand All @@ -27,7 +26,6 @@ internal class ConfigurableLanguageServerFeatureOptions : LanguageServerFeatureO
private readonly bool? _forceRuntimeCodeGeneration;

public override bool SupportsFileManipulation => _supportsFileManipulation ?? _defaults.SupportsFileManipulation;
public override string ProjectConfigurationFileName => _projectConfigurationFileName ?? _defaults.ProjectConfigurationFileName;
public override string CSharpVirtualDocumentSuffix => _csharpVirtualDocumentSuffix ?? DefaultLanguageServerFeatureOptions.DefaultCSharpVirtualDocumentSuffix;
public override string HtmlVirtualDocumentSuffix => _htmlVirtualDocumentSuffix ?? DefaultLanguageServerFeatureOptions.DefaultHtmlVirtualDocumentSuffix;
public override bool SingleServerCompletionSupport => _singleServerCompletionSupport ?? _defaults.SingleServerCompletionSupport;
Expand All @@ -52,7 +50,6 @@ public ConfigurableLanguageServerFeatureOptions(string[] args)
}

TryProcessBoolOption(nameof(SupportsFileManipulation), ref _supportsFileManipulation, option, args, i);
TryProcessStringOption(nameof(ProjectConfigurationFileName), ref _projectConfigurationFileName, option, args, i);
TryProcessStringOption(nameof(CSharpVirtualDocumentSuffix), ref _csharpVirtualDocumentSuffix, option, args, i);
TryProcessStringOption(nameof(HtmlVirtualDocumentSuffix), ref _htmlVirtualDocumentSuffix, option, args, i);
TryProcessBoolOption(nameof(SingleServerCompletionSupport), ref _singleServerCompletionSupport, option, args, i);
Expand Down

This file was deleted.

Loading
Loading