|
| 1 | +// 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.md file in the project root for more information. |
| 2 | + |
| 3 | +using Microsoft.Internal.Performance; |
| 4 | +using Microsoft.VisualStudio.ProjectSystem.PackageRestore; |
| 5 | +using Microsoft.VisualStudio.Threading; |
| 6 | +using NuGet.SolutionRestoreManager; |
| 7 | + |
| 8 | +namespace Microsoft.VisualStudio.ProjectSystem.VS.PackageRestore; |
| 9 | + |
| 10 | +[Export(typeof(INuGetRestoreService))] |
| 11 | +[Export(ExportContractNames.Scopes.UnconfiguredProject, typeof(IProjectDynamicLoadComponent))] |
| 12 | +[AppliesTo(ProjectCapability.PackageReferences)] |
| 13 | +internal class NuGetRestoreService : OnceInitializedOnceDisposed, INuGetRestoreService, IProjectDynamicLoadComponent, IVsProjectRestoreInfoSource |
| 14 | +{ |
| 15 | + private readonly UnconfiguredProject _project; |
| 16 | + private readonly IVsSolutionRestoreService3 _solutionRestoreService3; |
| 17 | + private readonly IVsSolutionRestoreService4 _solutionRestoreService4; |
| 18 | + private readonly IProjectAsynchronousTasksService _projectAsynchronousTasksService; |
| 19 | + private readonly IProjectFaultHandlerService _faultHandlerService; |
| 20 | + |
| 21 | + /// <summary> |
| 22 | + /// Save the configured project versions that might get nominations. |
| 23 | + /// </summary> |
| 24 | + private readonly Dictionary<ProjectConfiguration, IComparable> _savedNominatedConfiguredVersion = new(); |
| 25 | + |
| 26 | + /// <summary> |
| 27 | + /// Re-usable task that completes when there is a new nomination |
| 28 | + /// </summary> |
| 29 | + private TaskCompletionSource<bool>? _whenNominatedTask; |
| 30 | + |
| 31 | + private bool _enabled; |
| 32 | + private bool _restoring; |
| 33 | + private bool _updatesCompleted; |
| 34 | + |
| 35 | + [ImportingConstructor] |
| 36 | + public NuGetRestoreService( |
| 37 | + UnconfiguredProject project, |
| 38 | + IVsSolutionRestoreService3 solutionRestoreService3, |
| 39 | + IVsSolutionRestoreService4 solutionRestoreService4, |
| 40 | + [Import(ExportContractNames.Scopes.UnconfiguredProject)] IProjectAsynchronousTasksService projectAsynchronousTasksService, |
| 41 | + IProjectFaultHandlerService faultHandlerService) |
| 42 | + { |
| 43 | + _project = project; |
| 44 | + _solutionRestoreService3 = solutionRestoreService3; |
| 45 | + _solutionRestoreService4 = solutionRestoreService4; |
| 46 | + _projectAsynchronousTasksService = projectAsynchronousTasksService; |
| 47 | + _faultHandlerService = faultHandlerService; |
| 48 | + } |
| 49 | + |
| 50 | + public async Task<bool> NominateAsync(ProjectRestoreInfo restoreData, IReadOnlyCollection<PackageRestoreConfiguredInput> inputVersions, CancellationToken cancellationToken) |
| 51 | + { |
| 52 | + try |
| 53 | + { |
| 54 | + _restoring = true; |
| 55 | + |
| 56 | + Task<bool> restoreOperation = _solutionRestoreService3.NominateProjectAsync(_project.FullPath, new VsProjectRestoreInfo(restoreData), cancellationToken); |
| 57 | + |
| 58 | + SaveNominatedConfiguredVersions(inputVersions); |
| 59 | + |
| 60 | + return await restoreOperation; |
| 61 | + } |
| 62 | + finally |
| 63 | + { |
| 64 | + CodeMarkers.Instance.CodeMarker(CodeMarkerTimerId.PerfPackageRestoreEnd); |
| 65 | + _restoring = false; |
| 66 | + } |
| 67 | + } |
| 68 | + |
| 69 | + public Task UpdateWithoutNominationAsync(IReadOnlyCollection<PackageRestoreConfiguredInput> inputVersions) |
| 70 | + { |
| 71 | + SaveNominatedConfiguredVersions(inputVersions); |
| 72 | + |
| 73 | + return Task.CompletedTask; |
| 74 | + } |
| 75 | + |
| 76 | + public void NotifyComplete() |
| 77 | + { |
| 78 | + lock (SyncObject) |
| 79 | + { |
| 80 | + _updatesCompleted = true; |
| 81 | + _whenNominatedTask?.TrySetCanceled(); |
| 82 | + } |
| 83 | + } |
| 84 | + |
| 85 | + public void NotifyFaulted(Exception e) |
| 86 | + { |
| 87 | + lock (SyncObject) |
| 88 | + { |
| 89 | + _updatesCompleted = true; |
| 90 | + _whenNominatedTask?.SetException(e); |
| 91 | + } |
| 92 | + } |
| 93 | + |
| 94 | + public Task WhenNominated(CancellationToken cancellationToken) |
| 95 | + { |
| 96 | + lock (SyncObject) |
| 97 | + { |
| 98 | + if (cancellationToken.IsCancellationRequested) |
| 99 | + { |
| 100 | + cancellationToken.ThrowIfCancellationRequested(); |
| 101 | + } |
| 102 | + |
| 103 | + if (!CheckIfHasPendingNomination()) |
| 104 | + { |
| 105 | + return Task.CompletedTask; |
| 106 | + } |
| 107 | + |
| 108 | + _whenNominatedTask ??= new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); |
| 109 | + } |
| 110 | + |
| 111 | + return _whenNominatedTask.Task.WithCancellation(cancellationToken); |
| 112 | + } |
| 113 | + |
| 114 | + public string Name => _project.FullPath; |
| 115 | + |
| 116 | + public bool HasPendingNomination => CheckIfHasPendingNomination(); |
| 117 | + |
| 118 | + public Task LoadAsync() |
| 119 | + { |
| 120 | + _enabled = true; |
| 121 | + |
| 122 | + EnsureInitialized(); |
| 123 | + |
| 124 | + return Task.CompletedTask; |
| 125 | + } |
| 126 | + |
| 127 | + public Task UnloadAsync() |
| 128 | + { |
| 129 | + lock (SyncObject) |
| 130 | + { |
| 131 | + _enabled = false; |
| 132 | + |
| 133 | + _whenNominatedTask?.TrySetCanceled(); |
| 134 | + } |
| 135 | + |
| 136 | + return Task.CompletedTask; |
| 137 | + } |
| 138 | + |
| 139 | + protected override void Initialize() |
| 140 | + { |
| 141 | + RegisterProjectRestoreInfoSource(); |
| 142 | + } |
| 143 | + |
| 144 | + protected override void Dispose(bool disposing) |
| 145 | + { |
| 146 | + } |
| 147 | + |
| 148 | + private void SaveNominatedConfiguredVersions(IReadOnlyCollection<PackageRestoreConfiguredInput> configuredInputs) |
| 149 | + { |
| 150 | + lock (SyncObject) |
| 151 | + { |
| 152 | + _savedNominatedConfiguredVersion.Clear(); |
| 153 | + |
| 154 | + foreach (var configuredInput in configuredInputs) |
| 155 | + { |
| 156 | + _savedNominatedConfiguredVersion[configuredInput.ProjectConfiguration] = configuredInput.ConfiguredProjectVersion; |
| 157 | + } |
| 158 | + |
| 159 | + if (_whenNominatedTask is not null) |
| 160 | + { |
| 161 | + if (_whenNominatedTask.TrySetResult(true)) |
| 162 | + { |
| 163 | + _whenNominatedTask = null; |
| 164 | + } |
| 165 | + } |
| 166 | + } |
| 167 | + } |
| 168 | + |
| 169 | + private bool CheckIfHasPendingNomination() |
| 170 | + { |
| 171 | + lock (SyncObject) |
| 172 | + { |
| 173 | + Assumes.Present(_project.Services.ActiveConfiguredProjectProvider); |
| 174 | + Assumes.Present(_project.Services.ActiveConfiguredProjectProvider.ActiveConfiguredProject); |
| 175 | + |
| 176 | + // Nuget should not wait for projects that failed DTB |
| 177 | + if (!_enabled || _updatesCompleted) |
| 178 | + { |
| 179 | + return false; |
| 180 | + } |
| 181 | + |
| 182 | + // Avoid possible deadlock. |
| 183 | + // Because RestoreCoreAsync() is called inside a dataflow block it will not be called with new data |
| 184 | + // until the old task finishes. So, if the project gets nominating restore, it will not get updated data. |
| 185 | + if (_restoring) |
| 186 | + { |
| 187 | + return false; |
| 188 | + } |
| 189 | + |
| 190 | + ConfiguredProject? activeConfiguredProject = _project.Services.ActiveConfiguredProjectProvider.ActiveConfiguredProject; |
| 191 | + |
| 192 | + // After the first nomination, we should check the saved nominated version |
| 193 | + return IsSavedNominationOutOfDate(activeConfiguredProject); |
| 194 | + } |
| 195 | + } |
| 196 | + |
| 197 | + private bool IsSavedNominationOutOfDate(ConfiguredProject activeConfiguredProject) |
| 198 | + { |
| 199 | + if (!_savedNominatedConfiguredVersion.TryGetValue(activeConfiguredProject.ProjectConfiguration, |
| 200 | + out IComparable latestSavedVersionForActiveConfiguredProject) || |
| 201 | + activeConfiguredProject.ProjectVersion.IsLaterThan(latestSavedVersionForActiveConfiguredProject)) |
| 202 | + { |
| 203 | + return true; |
| 204 | + } |
| 205 | + |
| 206 | + if (_savedNominatedConfiguredVersion.Count == 1) |
| 207 | + { |
| 208 | + return false; |
| 209 | + } |
| 210 | + |
| 211 | + foreach (ConfiguredProject loadedProject in activeConfiguredProject.UnconfiguredProject.LoadedConfiguredProjects) |
| 212 | + { |
| 213 | + if (_savedNominatedConfiguredVersion.TryGetValue(loadedProject.ProjectConfiguration, out IComparable savedProjectVersion)) |
| 214 | + { |
| 215 | + if (loadedProject.ProjectVersion.IsLaterThan(savedProjectVersion)) |
| 216 | + { |
| 217 | + return true; |
| 218 | + } |
| 219 | + } |
| 220 | + } |
| 221 | + |
| 222 | + return false; |
| 223 | + } |
| 224 | + |
| 225 | + private void RegisterProjectRestoreInfoSource() |
| 226 | + { |
| 227 | + // Register before this project receives any data flows containing possible nominations. |
| 228 | + // This is needed because we need to register before any nuget restore or before the solution load. |
| 229 | +#pragma warning disable RS0030 // Do not used banned APIs |
| 230 | + var registerRestoreInfoSourceTask = Task.Run(() => |
| 231 | + { |
| 232 | + return _solutionRestoreService4.RegisterRestoreInfoSourceAsync(this, _projectAsynchronousTasksService.UnloadCancellationToken); |
| 233 | + }); |
| 234 | +#pragma warning restore RS0030 // Do not used banned APIs |
| 235 | + |
| 236 | + _faultHandlerService.Forget(registerRestoreInfoSourceTask, _project, ProjectFaultSeverity.Recoverable); |
| 237 | + } |
| 238 | +} |
0 commit comments