Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Expand Up @@ -74,9 +74,8 @@ await File.WriteAllTextAsync(projectPath, $"""
documents: files,
locations: annotatedLocations);

// Perform restore and mock up project restore client handler
// Perform restore
ProcessUtilities.Run("dotnet", $"restore --project {projectPath}");
Copy link
Member

Choose a reason for hiding this comment

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

Do you feel the new WorkDoneProgress code is exercised well by the existing tests?

Copy link
Member Author

Choose a reason for hiding this comment

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

Restore wasn't really exercised by the tests at all really. We likely need integration tests for this that actually do the full load / restore loop.

lspClient.AddClientLocalRpcTarget(ProjectDependencyHelper.ProjectNeedsRestoreName, (string[] projectFilePaths) => { });

// Listen for project initialization
var projectInitialized = new TaskCompletionSource();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ internal sealed class CanonicalMiscFilesProjectLoader : LanguageServerProjectLoa
{
private readonly Lazy<string> _canonicalDocumentPath;

/// <summary>
/// Avoid showing restore notifications for misc files - it ends up being noisy and confusing
/// as every file is a misc file on first open until we detect a project for it.
/// </summary>
protected override bool EnableProgressReporting => false;

public CanonicalMiscFilesProjectLoader(
LanguageServerWorkspaceFactory workspaceFactory,
IFileChangeWatcher fileChangeWatcher,
Expand All @@ -37,7 +43,8 @@ public CanonicalMiscFilesProjectLoader(
IAsynchronousOperationListenerProvider listenerProvider,
ProjectLoadTelemetryReporter projectLoadTelemetry,
ServerConfigurationFactory serverConfigurationFactory,
IBinLogPathProvider binLogPathProvider)
IBinLogPathProvider binLogPathProvider,
DotnetCliHelper dotnetCliHelper)
: base(
workspaceFactory,
fileChangeWatcher,
Expand All @@ -46,7 +53,8 @@ public CanonicalMiscFilesProjectLoader(
listenerProvider,
projectLoadTelemetry,
serverConfigurationFactory,
binLogPathProvider)
binLogPathProvider,
dotnetCliHelper)
{
_canonicalDocumentPath = new Lazy<string>(() =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ public FileBasedProgramsProjectSystem(
IAsynchronousOperationListenerProvider listenerProvider,
ProjectLoadTelemetryReporter projectLoadTelemetry,
ServerConfigurationFactory serverConfigurationFactory,
IBinLogPathProvider binLogPathProvider)
IBinLogPathProvider binLogPathProvider,
DotnetCliHelper dotnetCliHelper)
: base(
workspaceFactory,
fileChangeWatcher,
Expand All @@ -53,7 +54,8 @@ public FileBasedProgramsProjectSystem(
listenerProvider,
projectLoadTelemetry,
serverConfigurationFactory,
binLogPathProvider)
binLogPathProvider,
dotnetCliHelper)
{
_lspServices = lspServices;
_logger = loggerFactory.CreateLogger<FileBasedProgramsProjectSystem>();
Expand All @@ -66,7 +68,8 @@ public FileBasedProgramsProjectSystem(
listenerProvider,
projectLoadTelemetry,
serverConfigurationFactory,
binLogPathProvider);
binLogPathProvider,
dotnetCliHelper);
}

private string GetDocumentFilePath(DocumentUri uri) => uri.ParsedUri is { } parsedUri ? ProtocolConversions.GetDocumentFilePathFromUri(parsedUri) : uri.UriString;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ internal sealed class FileBasedProgramsWorkspaceProviderFactory(
IAsynchronousOperationListenerProvider listenerProvider,
ProjectLoadTelemetryReporter projectLoadTelemetry,
ServerConfigurationFactory serverConfigurationFactory,
IBinLogPathProvider binLogPathProvider) : ILspMiscellaneousFilesWorkspaceProviderFactory
IBinLogPathProvider binLogPathProvider,
DotnetCliHelper dotnetCliHelper) : ILspMiscellaneousFilesWorkspaceProviderFactory
{
public ILspMiscellaneousFilesWorkspaceProvider CreateLspMiscellaneousFilesWorkspaceProvider(ILspServices lspServices, HostServices hostServices)
{
Expand All @@ -48,6 +49,7 @@ public ILspMiscellaneousFilesWorkspaceProvider CreateLspMiscellaneousFilesWorksp
listenerProvider,
projectLoadTelemetry,
serverConfigurationFactory,
binLogPathProvider);
binLogPathProvider,
dotnetCliHelper);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ internal abstract class LanguageServerProjectLoader
private readonly ILogger _logger;
private readonly ProjectLoadTelemetryReporter _projectLoadTelemetryReporter;
private readonly IBinLogPathProvider _binLogPathProvider;
private readonly DotnetCliHelper _dotnetCliHelper;
protected readonly ImmutableDictionary<string, string> AdditionalProperties;

/// <summary>
Expand Down Expand Up @@ -84,6 +85,11 @@ public sealed record Primordial(ProjectSystemProjectFactory PrimordialProjectFac
public sealed record LoadedTargets(ImmutableArray<LoadedProject> LoadedProjectTargets) : ProjectLoadState;
}

/// <summary>
/// Indicates whether loads should report UI progress to the client for this loader.
/// </summary>
protected virtual bool EnableProgressReporting => true;

protected LanguageServerProjectLoader(
LanguageServerWorkspaceFactory workspaceFactory,
IFileChangeWatcher fileChangeWatcher,
Expand All @@ -92,7 +98,8 @@ protected LanguageServerProjectLoader(
IAsynchronousOperationListenerProvider listenerProvider,
ProjectLoadTelemetryReporter projectLoadTelemetry,
ServerConfigurationFactory serverConfigurationFactory,
IBinLogPathProvider binLogPathProvider)
IBinLogPathProvider binLogPathProvider,
DotnetCliHelper dotnetCliHelper)
{
_workspaceFactory = workspaceFactory;
_fileChangeWatcher = fileChangeWatcher;
Expand All @@ -101,6 +108,7 @@ protected LanguageServerProjectLoader(
_logger = loggerFactory.CreateLogger(nameof(LanguageServerProjectLoader));
_projectLoadTelemetryReporter = projectLoadTelemetry;
_binLogPathProvider = binLogPathProvider;
_dotnetCliHelper = dotnetCliHelper;

AdditionalProperties = BuildAdditionalProperties(serverConfigurationFactory.ServerConfiguration);

Expand Down Expand Up @@ -176,12 +184,8 @@ private async ValueTask ReloadProjectsAsync(ImmutableSegmentedList<ProjectToLoad

if (GlobalOptionService.GetOption(LanguageServerProjectSystemOptionsStorage.EnableAutomaticRestore) && projectsThatNeedRestore.Any())
{
// Tell the client to restore any projects with unresolved dependencies.
// This should eventually move entirely server side once we have a mechanism for reporting generic project load progress.
// Tracking: https://github.com/dotnet/vscode-csharp/issues/6675
//
// The request blocks to ensure we aren't trying to run a design time build at the same time as a restore.
await ProjectDependencyHelper.SendProjectNeedsRestoreRequestAsync(projectsThatNeedRestore, cancellationToken);
// This request blocks to ensure we aren't trying to run a design time build at the same time as a restore.
await ProjectDependencyHelper.RestoreProjectsAsync(projectsThatNeedRestore, EnableProgressReporting, _dotnetCliHelper, _logger, cancellationToken);
}
}
finally
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ public LanguageServerProjectSystem(
IAsynchronousOperationListenerProvider listenerProvider,
ProjectLoadTelemetryReporter projectLoadTelemetry,
ServerConfigurationFactory serverConfigurationFactory,
IBinLogPathProvider binLogPathProvider)
IBinLogPathProvider binLogPathProvider,
DotnetCliHelper dotnetCliHelper)
: base(
workspaceFactory,
fileChangeWatcher,
Expand All @@ -44,7 +45,8 @@ public LanguageServerProjectSystem(
listenerProvider,
projectLoadTelemetry,
serverConfigurationFactory,
binLogPathProvider)
binLogPathProvider,
dotnetCliHelper)
{
_logger = loggerFactory.CreateLogger(nameof(LanguageServerProjectSystem));
_hostProjectFactory = workspaceFactory.HostProjectFactory;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// See the LICENSE file in the project root for more information.

using System.Collections.Immutable;
using System.Text.Json.Serialization;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Microsoft.CodeAnalysis.LanguageServer.LanguageServer;
using Microsoft.CodeAnalysis.MSBuild;
using Microsoft.CodeAnalysis.PooledObjects;
Expand All @@ -16,8 +16,6 @@ namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;

internal static class ProjectDependencyHelper
{
internal const string ProjectNeedsRestoreName = "workspace/_roslyn_projectNeedsRestore";

internal static bool NeedsRestore(ProjectFileInfo newProjectFileInfo, ProjectFileInfo? previousProjectFileInfo, ILogger logger)
{
if (previousProjectFileInfo is null)
Expand Down Expand Up @@ -121,20 +119,24 @@ static bool SatisfiesVersion(VersionRange requestedVersionRange, NuGetVersion pr
}
}

internal static async Task SendProjectNeedsRestoreRequestAsync(ImmutableArray<string> projectPaths, CancellationToken cancellationToken)
internal static async Task RestoreProjectsAsync(ImmutableArray<string> projectPaths, bool enableProgressReporting, DotnetCliHelper dotnetCliHelper, ILogger logger, CancellationToken cancellationToken)
{
if (projectPaths.IsEmpty)
return;

Contract.ThrowIfNull(LanguageServerHost.Instance, "We don't have an LSP channel yet to send this request through.");

var languageServerManager = LanguageServerHost.Instance.GetRequiredLspService<IClientLanguageServerManager>();
var workDoneProgressManager = LanguageServerHost.Instance.GetRequiredLspService<WorkDoneProgressManager>();

// Ensure we only pass unique paths back to be restored.
var unresolvedParams = new UnresolvedDependenciesParams([.. projectPaths.Distinct()]);
await languageServerManager.SendRequestAsync(ProjectNeedsRestoreName, unresolvedParams, cancellationToken);
try
{
await RestoreHandler.RestoreAsync(projectPaths, workDoneProgressManager, dotnetCliHelper, logger, enableProgressReporting, cancellationToken);
}
catch (OperationCanceledException)
{
// Restore was cancelled. This is not a failure, it just leaves the project unrestored or partially restored (same as if the user cancelled a CLI restore).
// We don't want this exception to bubble up to the project load queue however as it may need to additional work after this call.
logger.LogWarning("Project restore was canceled.");
}
}

private sealed record UnresolvedDependenciesParams(
[property: JsonPropertyName("projectFilePaths")] string[] ProjectFilePaths);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@

using System.Collections.Immutable;
using System.Composition;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.Threading;
using Roslyn.LanguageServer.Protocol;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.LanguageServer.Handler;
Expand All @@ -18,7 +21,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler;
[Method(MethodName)]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class RestoreHandler(DotnetCliHelper dotnetCliHelper, ILoggerFactory loggerFactory) : ILspServiceRequestHandler<RestoreParams, RestorePartialResult[]>
internal sealed class RestoreHandler(DotnetCliHelper dotnetCliHelper, ILoggerFactory loggerFactory) : ILspServiceRequestHandler<RestoreParams, RestoreResult>
{
internal const string MethodName = "workspace/_roslyn_restore";

Expand All @@ -28,26 +31,24 @@ internal sealed class RestoreHandler(DotnetCliHelper dotnetCliHelper, ILoggerFac

private readonly ILogger<RestoreHandler> _logger = loggerFactory.CreateLogger<RestoreHandler>();

public async Task<RestorePartialResult[]> HandleRequestAsync(RestoreParams request, RequestContext context, CancellationToken cancellationToken)
public async Task<RestoreResult> HandleRequestAsync(RestoreParams request, RequestContext context, CancellationToken cancellationToken)
{
Contract.ThrowIfNull(context.Solution);
using var progress = BufferedProgress.Create(request.PartialResultToken);

progress.Report(new RestorePartialResult(LanguageServerResources.Restore, LanguageServerResources.Restore_started));
Copy link
Member

Choose a reason for hiding this comment

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

Why no progress reporting?

Copy link
Member Author

Choose a reason for hiding this comment

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

Progress is reported via the work done progress mechanism below instead.

Background

This part of the handler handles manual restore requests from the client. Previously we were using custom client side code to make a streaming request to the server and show progress on the client.

Now the client makes a normal request, and the server enables progress reporting via work done progress.


var restorePaths = GetRestorePaths(request, context.Solution, context);
if (restorePaths.IsEmpty)
{
_logger.LogDebug($"Restore was requested but no paths were provided.");
progress.Report(new RestorePartialResult(LanguageServerResources.Restore, LanguageServerResources.Nothing_found_to_restore));
return progress.GetValues() ?? [];
return new RestoreResult(true);
}

var workDoneProgressManager = context.GetRequiredService<WorkDoneProgressManager>();
_logger.LogDebug($"Running restore on {restorePaths.Length} paths, starting with '{restorePaths.First()}'.");
bool success = await RestoreAsync(restorePaths, progress, cancellationToken);

progress.Report(new RestorePartialResult(LanguageServerResources.Restore, $"{LanguageServerResources.Restore_complete}{Environment.NewLine}"));
if (success)
// We let cancellation here bubble up to the client as this is a client initiated operation.
var didSucceed = await RestoreAsync(restorePaths, workDoneProgressManager, dotnetCliHelper, _logger, enableProgressReporting: true, cancellationToken);

if (didSucceed)
{
_logger.LogDebug($"Restore completed successfully.");
}
Expand All @@ -56,13 +57,44 @@ public async Task<RestorePartialResult[]> HandleRequestAsync(RestoreParams reque
_logger.LogError($"Restore completed with errors.");
}

return progress.GetValues() ?? [];
return new RestoreResult(didSucceed);
}

/// <returns>True if all restore invocations exited with code 0. Otherwise, false.</returns>
private async Task<bool> RestoreAsync(ImmutableArray<string> pathsToRestore, BufferedProgress<RestorePartialResult> progress, CancellationToken cancellationToken)
public static async Task<bool> RestoreAsync(
ImmutableArray<string> pathsToRestore,
WorkDoneProgressManager workDoneProgressManager,
DotnetCliHelper dotnetCliHelper,
ILogger logger,
bool enableProgressReporting,
CancellationToken cancellationToken)
{
bool success = true;
using var progress = await workDoneProgressManager.CreateWorkDoneProgressAsync(reportProgressToClient: enableProgressReporting, cancellationToken);
// Ensure we're observing cancellation token from the work done progress (to allow client cancellation).
cancellationToken = progress.CancellationToken;
return await RestoreCoreAsync(pathsToRestore, progress, dotnetCliHelper, logger, cancellationToken);
Copy link
Member

Choose a reason for hiding this comment

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

Is progress.CancellationToken already linked to cancellationToken? Or we don't want to cancel RestoreCoreAsync, when the original cancellationToken parameter value is canceled?

Copy link
Member Author

Choose a reason for hiding this comment

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

Is progress.CancellationToken already linked to cancellationToken

Yeah, progress.CancellationToken is linked to the cancellation token and client side token.


}

private static async Task<bool> RestoreCoreAsync(
ImmutableArray<string> pathsToRestore,
IWorkDoneProgressReporter progress,
DotnetCliHelper dotnetCliHelper,
ILogger logger,
CancellationToken cancellationToken)
{
// Report the start of the work done progress to the client.
progress.Report(new WorkDoneProgressBegin()
{
Title = LanguageServerResources.Restore,
// Adds a cancel button to the client side progress UI.
// Cancellation here is fine, it just means the restore will be incomplete (same as a cntrl+C for a CLI restore).
Cancellable = true,
Message = LanguageServerResources.Restore_started,
Percentage = 0,
});

var success = true;
foreach (var path in pathsToRestore)
{
var arguments = new string[] { "restore", path };
Expand All @@ -77,8 +109,8 @@ private async Task<bool> RestoreAsync(ImmutableArray<string> pathsToRestore, Buf
process?.Kill();
});

process.OutputDataReceived += (sender, args) => ReportProgress(progress, stageName, args.Data);
process.ErrorDataReceived += (sender, args) => ReportProgress(progress, stageName, args.Data);
process.OutputDataReceived += (sender, args) => ReportProgressInEvent(progress, stageName, args.Data);
process.ErrorDataReceived += (sender, args) => ReportProgressInEvent(progress, stageName, args.Data);
process.BeginOutputReadLine();
process.BeginErrorReadLine();

Expand All @@ -91,14 +123,43 @@ private async Task<bool> RestoreAsync(ImmutableArray<string> pathsToRestore, Buf
}
}

// Report work done progress completion
progress.Report(
new WorkDoneProgressEnd()
{
Message = LanguageServerResources.Restore_complete
});

logger.LogInformation(LanguageServerResources.Restore_complete);
return success;

static void ReportProgress(BufferedProgress<RestorePartialResult> progress, string stage, string? restoreOutput)
void ReportProgressInEvent(IWorkDoneProgressReporter progress, string stage, string? restoreOutput)
{
if (restoreOutput != null)
if (restoreOutput == null)
return;

try
{
progress.Report(new RestorePartialResult(stage, restoreOutput));
ReportProgress(progress, stage, restoreOutput);
}
catch (Exception)
{
// Catch everything to ensure the exception doesn't escape the event handler.
// Errors already reported via ReportNonFatalErrorUnlessCancelledAsync.
}
}

void ReportProgress(IWorkDoneProgressReporter progress, string stage, string message)
{
logger.LogInformation("{stage}: {Output}", stage, message);
var report = new WorkDoneProgressReport()
{
Message = stage,
Percentage = null,
Cancellable = true,
};

progress.Report(report);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,4 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler;
internal sealed record RestoreParams(
// An empty set of project file paths means restore all projects in the workspace.
[property: JsonPropertyName("projectFilePaths")] string[] ProjectFilePaths
) : IPartialResultParams<RestorePartialResult>
{
[JsonPropertyName(Methods.PartialResultTokenName)]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IProgress<RestorePartialResult>? PartialResultToken { get; set; }
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

namespace Microsoft.CodeAnalysis.LanguageServer.Handler;

internal sealed record RestorePartialResult(
[property: JsonPropertyName("stage")] string Stage,
[property: JsonPropertyName("message")] string Message
internal sealed record RestoreResult(
[property: JsonPropertyName("success")] bool Success
);
Loading
Loading