PageRenderTime 44ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs

https://gitlab.com/dotnetfoundation/sdk
C# | 278 lines | 222 code | 39 blank | 17 comment | 30 complexity | ed0cbfbaeb2bb27fac0d222b0f428d45 MD5 | raw file
  1. // Copyright (c) .NET Foundation. All rights reserved.
  2. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  3. #nullable enable
  4. using System;
  5. using System.Collections.Immutable;
  6. using System.Diagnostics;
  7. using System.IO;
  8. using System.Linq;
  9. using System.Text;
  10. using System.Threading;
  11. using System.Threading.Tasks;
  12. using Microsoft.CodeAnalysis;
  13. using Microsoft.CodeAnalysis.CSharp;
  14. using Microsoft.CodeAnalysis.ExternalAccess.Watch.Api;
  15. using Microsoft.CodeAnalysis.Text;
  16. using Microsoft.Extensions.Tools.Internal;
  17. namespace Microsoft.DotNet.Watcher.Tools
  18. {
  19. internal sealed class CompilationHandler : IDisposable
  20. {
  21. private readonly IReporter _reporter;
  22. private Task<(Solution, WatchHotReloadService)>? _initializeTask;
  23. private Solution? _currentSolution;
  24. private WatchHotReloadService? _hotReloadService;
  25. private IDeltaApplier? _deltaApplier;
  26. public CompilationHandler(IReporter reporter)
  27. {
  28. _reporter = reporter;
  29. }
  30. public async ValueTask InitializeAsync(DotNetWatchContext context, CancellationToken cancellationToken)
  31. {
  32. Debug.Assert(context.ProjectGraph is not null);
  33. if (_deltaApplier is null)
  34. {
  35. var hotReloadProfile = HotReloadProfileReader.InferHotReloadProfile(context.ProjectGraph, _reporter);
  36. _deltaApplier = hotReloadProfile switch
  37. {
  38. HotReloadProfile.BlazorWebAssembly => new BlazorWebAssemblyDeltaApplier(_reporter),
  39. HotReloadProfile.BlazorHosted => new BlazorWebAssemblyHostedDeltaApplier(_reporter),
  40. _ => new DefaultDeltaApplier(_reporter),
  41. };
  42. }
  43. await _deltaApplier.InitializeAsync(context, cancellationToken);
  44. if (_currentSolution is not null)
  45. {
  46. _currentSolution.Workspace.Dispose();
  47. _currentSolution = null;
  48. }
  49. _initializeTask = Task.Run(() => CompilationWorkspaceProvider.CreateWorkspaceAsync(context.FileSet.Project.ProjectPath, _reporter, cancellationToken), cancellationToken);
  50. return;
  51. }
  52. public async ValueTask<bool> TryHandleFileChange(DotNetWatchContext context, FileItem file, CancellationToken cancellationToken)
  53. {
  54. HotReloadEventSource.Log.HotReloadStart(HotReloadEventSource.StartType.CompilationHandler);
  55. if (!file.FilePath.EndsWith(".cs", StringComparison.Ordinal) &&
  56. !file.FilePath.EndsWith(".razor", StringComparison.Ordinal) &&
  57. !file.FilePath.EndsWith(".cshtml", StringComparison.Ordinal))
  58. {
  59. HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.CompilationHandler);
  60. return false;
  61. }
  62. if (!await EnsureSolutionInitializedAsync())
  63. {
  64. HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.CompilationHandler);
  65. return false;
  66. }
  67. Debug.Assert(_hotReloadService != null);
  68. Debug.Assert(_currentSolution != null);
  69. Debug.Assert(_deltaApplier != null);
  70. Solution? updatedSolution = null;
  71. ProjectId updatedProjectId;
  72. if (_currentSolution.Projects.SelectMany(p => p.Documents).FirstOrDefault(d => string.Equals(d.FilePath, file.FilePath, StringComparison.OrdinalIgnoreCase)) is Document documentToUpdate)
  73. {
  74. var sourceText = await GetSourceTextAsync(file.FilePath);
  75. updatedSolution = documentToUpdate.WithText(sourceText).Project.Solution;
  76. updatedProjectId = documentToUpdate.Project.Id;
  77. }
  78. else if (_currentSolution.Projects.SelectMany(p => p.AdditionalDocuments).FirstOrDefault(d => string.Equals(d.FilePath, file.FilePath, StringComparison.OrdinalIgnoreCase)) is AdditionalDocument additionalDocument)
  79. {
  80. var sourceText = await GetSourceTextAsync(file.FilePath);
  81. updatedSolution = _currentSolution.WithAdditionalDocumentText(additionalDocument.Id, sourceText, PreservationMode.PreserveValue);
  82. updatedProjectId = additionalDocument.Project.Id;
  83. }
  84. else
  85. {
  86. _reporter.Verbose($"Could not find document with path {file.FilePath} in the workspace.");
  87. HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.CompilationHandler);
  88. return false;
  89. }
  90. var (updates, hotReloadDiagnostics) = await _hotReloadService.EmitSolutionUpdateAsync(updatedSolution, cancellationToken);
  91. // hotReloadDiagnostics currently includes semantic Warnings and Errors for types being updated. We want to limit rude edits to the class
  92. // of unrecoverable errors that a user cannot fix and requires an app rebuild.
  93. var rudeEdits = hotReloadDiagnostics.RemoveAll(d => d.Severity == DiagnosticSeverity.Warning || !d.Descriptor.Id.StartsWith("ENC", StringComparison.Ordinal));
  94. if (rudeEdits.IsDefaultOrEmpty && updates.IsDefaultOrEmpty)
  95. {
  96. // It's possible that there are compilation errors which prevented the solution update
  97. // from being updated. Let's look to see if there are compilation errors.
  98. var diagnostics = GetDiagnostics(updatedSolution, cancellationToken);
  99. if (diagnostics.IsDefaultOrEmpty)
  100. {
  101. _reporter.Verbose("No deltas modified. Applying changes to clear diagnostics.");
  102. await _deltaApplier.Apply(context, file.FilePath, updates, cancellationToken);
  103. // Even if there were diagnostics, continue treating this as a success
  104. _reporter.Output("No hot reload changes to apply.");
  105. }
  106. else
  107. {
  108. _reporter.Verbose("Found compilation errors during hot reload. Reporting it in application UI.");
  109. await _deltaApplier.ReportDiagnosticsAsync(context, diagnostics, cancellationToken);
  110. }
  111. HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.CompilationHandler);
  112. // Return true so that the watcher continues to keep the current hot reload session alive. If there were errors, this allows the user to fix errors and continue
  113. // working on the running app.
  114. return true;
  115. }
  116. if (!rudeEdits.IsDefaultOrEmpty)
  117. {
  118. // Rude edit.
  119. _reporter.Output("Unable to apply hot reload because of a rude edit. Rebuilding the app...");
  120. foreach (var diagnostic in hotReloadDiagnostics)
  121. {
  122. _reporter.Verbose(CSharpDiagnosticFormatter.Instance.Format(diagnostic));
  123. }
  124. HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.CompilationHandler);
  125. return false;
  126. }
  127. _currentSolution = updatedSolution;
  128. var applyState = await _deltaApplier.Apply(context, file.FilePath, updates, cancellationToken);
  129. _reporter.Verbose($"Received {(applyState ? "successful" : "failed")} apply from delta applier.");
  130. HotReloadEventSource.Log.HotReloadEnd(HotReloadEventSource.StartType.CompilationHandler);
  131. if (applyState)
  132. {
  133. _reporter.Output($"Hot reload of changes succeeded.");
  134. }
  135. return applyState;
  136. }
  137. private ImmutableArray<string> GetDiagnostics(Solution solution, CancellationToken cancellationToken)
  138. {
  139. var @lock = new object();
  140. var builder = ImmutableArray<string>.Empty;
  141. Parallel.ForEach(solution.Projects, project =>
  142. {
  143. if (!project.TryGetCompilation(out var compilation))
  144. {
  145. return;
  146. }
  147. var compilationDiagnostics = compilation.GetDiagnostics(cancellationToken);
  148. if (compilationDiagnostics.IsDefaultOrEmpty)
  149. {
  150. return;
  151. }
  152. var projectDiagnostics = ImmutableArray<string>.Empty;
  153. foreach (var item in compilationDiagnostics)
  154. {
  155. if (item.Severity == DiagnosticSeverity.Error)
  156. {
  157. var diagnostic = CSharpDiagnosticFormatter.Instance.Format(item);
  158. _reporter.Output("\x1B[40m\x1B[31m" + diagnostic);
  159. projectDiagnostics = projectDiagnostics.Add(diagnostic);
  160. }
  161. }
  162. lock (@lock)
  163. {
  164. builder = builder.AddRange(projectDiagnostics);
  165. }
  166. });
  167. return builder;
  168. }
  169. private async ValueTask<bool> EnsureSolutionInitializedAsync()
  170. {
  171. if (_currentSolution != null)
  172. {
  173. return true;
  174. }
  175. if (_initializeTask is null)
  176. {
  177. return false;
  178. }
  179. try
  180. {
  181. (_currentSolution, _hotReloadService) = await _initializeTask;
  182. return true;
  183. }
  184. catch (Exception ex)
  185. {
  186. _reporter.Warn(ex.Message);
  187. return false;
  188. }
  189. }
  190. private async ValueTask<SourceText> GetSourceTextAsync(string filePath)
  191. {
  192. var zeroLengthRetryPerformed = false;
  193. for (var attemptIndex = 0; attemptIndex < 6; attemptIndex++)
  194. {
  195. try
  196. {
  197. // File.OpenRead opens the file with FileShare.Read. This may prevent IDEs from saving file
  198. // contents to disk
  199. SourceText sourceText;
  200. using (var stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
  201. {
  202. sourceText = SourceText.From(stream, Encoding.UTF8);
  203. }
  204. if (!zeroLengthRetryPerformed && sourceText.Length == 0)
  205. {
  206. zeroLengthRetryPerformed = true;
  207. // VSCode (on Windows) will sometimes perform two separate writes when updating a file on disk.
  208. // In the first update, it clears the file contents, and in the second, it writes the intended
  209. // content.
  210. // It's atypical that a file being watched for hot reload would be empty. We'll use this as a
  211. // hueristic to identify this case and perform an additional retry reading the file after a delay.
  212. await Task.Delay(20);
  213. using var stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
  214. sourceText = SourceText.From(stream, Encoding.UTF8);
  215. }
  216. return sourceText;
  217. }
  218. catch (IOException) when (attemptIndex < 5)
  219. {
  220. await Task.Delay(20 * (attemptIndex + 1));
  221. }
  222. }
  223. Debug.Fail("This shouldn't happen.");
  224. return null;
  225. }
  226. public void Dispose()
  227. {
  228. _hotReloadService?.EndSession();
  229. if (_deltaApplier is not null)
  230. {
  231. _deltaApplier.Dispose();
  232. }
  233. if (_currentSolution is not null)
  234. {
  235. _currentSolution.Workspace.Dispose();
  236. }
  237. }
  238. }
  239. }