-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Use work done progress to handle restore server side #81233
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
Changes from 2 commits
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 |
|---|---|---|
|
|
@@ -4,8 +4,11 @@ | |
|
|
||
| using System.Collections.Immutable; | ||
| using System.Composition; | ||
| using Microsoft.CodeAnalysis.ErrorReporting; | ||
dibarbet marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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; | ||
|
|
@@ -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"; | ||
|
|
||
|
|
@@ -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)); | ||
|
Member
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. Why no progress reporting?
Member
Author
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. Progress is reported via the work done progress mechanism below instead. BackgroundThis 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."); | ||
| } | ||
|
|
@@ -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); | ||
|
Member
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. Is
Member
Author
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.
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, | ||
dibarbet marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Message = LanguageServerResources.Restore_started, | ||
| Percentage = 0, | ||
| }); | ||
|
|
||
| var success = true; | ||
| foreach (var path in pathsToRestore) | ||
| { | ||
| var arguments = new string[] { "restore", path }; | ||
|
|
@@ -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(); | ||
|
|
||
|
|
@@ -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); | ||
| } | ||
| } | ||
|
|
||
|
|
||
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.
Do you feel the new WorkDoneProgress code is exercised well by the existing tests?
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.
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.