-
Notifications
You must be signed in to change notification settings - Fork 415
Expand file tree
/
Copy pathWorkspace.cs
More file actions
576 lines (455 loc) · 24.4 KB
/
Workspace.cs
File metadata and controls
576 lines (455 loc) · 24.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
// 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.
using Microsoft.VisualStudio.LanguageServices.ProjectSystem;
using Microsoft.VisualStudio.ProjectSystem.OperationProgress;
using Microsoft.VisualStudio.ProjectSystem.Properties;
using Microsoft.VisualStudio.ProjectSystem.Utilities;
using Microsoft.VisualStudio.ProjectSystem.VS;
using Microsoft.VisualStudio.Threading;
using Microsoft.VisualStudio.Threading.Tasks;
namespace Microsoft.VisualStudio.ProjectSystem.LanguageServices;
/// <summary>
/// Models a Roslyn workspace, which is tied to a project configuration slice.
/// Subscribes to data for that slice, and populates the Roslyn workspace appropriately.
/// Is created and disposed along with the project's configuration slice.
/// </summary>
internal sealed class Workspace : OnceInitializedOnceDisposedUnderLockAsync, IWorkspace
{
internal enum WorkspaceState
{
Uninitialized = 0,
Initialized,
Failed,
Disposed
}
private const string ProjectBuildRuleName = CompilerCommandLineArgs.SchemaName;
private readonly DisposableBag _disposableBag;
private readonly ProjectConfigurationSlice _slice;
private readonly UnconfiguredProject _unconfiguredProject;
private readonly Guid _projectGuid;
private readonly UpdateHandlers _updateHandlers;
private readonly IProjectDiagnosticOutputService _logger;
private readonly IActiveEditorContextTracker _activeEditorContextTracker;
private readonly OrderPrecedenceImportCollection<ICommandLineParserService> _commandLineParserServices;
private readonly IDataProgressTrackerService _dataProgressTrackerService;
private readonly Lazy<IWorkspaceProjectContextFactory> _workspaceProjectContextFactory;
private readonly IProjectFaultHandlerService _faultHandlerService;
private readonly JoinableTaskCollection _joinableTaskCollection;
private readonly JoinableTaskFactory _joinableTaskFactory;
private readonly CancellationToken _unloadCancellationToken;
private readonly string _baseDirectory;
/// <summary>Completes when the workspace has integrated build data.</summary>
private readonly TaskCompletionSource _contextCreated = new(TaskCreationOptions.RunContinuationsAsynchronously);
/// <summary>The current state of this workspace.</summary>
private WorkspaceState _state;
/// <summary>Operation progress reporting for evaluation dataflow, as this blocks IntelliSense.</summary>
private IDataProgressTrackerServiceRegistration? _evaluationProgressRegistration;
/// <summary>Operation progress reporting for build dataflow, as this blocks IntelliSense.</summary>
private IDataProgressTrackerServiceRegistration? _buildProgressRegistration;
/// <summary>The Roslyn context object that backs this workspace.</summary>
private IWorkspaceProjectContext? _context;
/// <summary>Roslyn's identifier for this workspace context.</summary>
private string? _contextId;
/// <summary>Whether we have seen evaluation data yet.</summary>
private bool _seenEvaluation;
/// <summary>Gets whether this workspace represents the primary active configuration.</summary>
public bool IsPrimary { get; internal set; }
#region IWorkspace
public IWorkspaceProjectContext Context => _context ?? throw new InvalidOperationException("Workspace has not been initialized.");
public string ContextId => _contextId ?? throw new InvalidOperationException("Workspace has not been initialized.");
public object HostSpecificErrorReporter => Context;
#endregion
internal Workspace(
ProjectConfigurationSlice slice,
UnconfiguredProject unconfiguredProject,
Guid projectGuid,
UpdateHandlers updateHandlers,
IProjectDiagnosticOutputService logger,
IActiveEditorContextTracker activeEditorContextTracker,
OrderPrecedenceImportCollection<ICommandLineParserService> commandLineParserServices,
IDataProgressTrackerService dataProgressTrackerService,
Lazy<IWorkspaceProjectContextFactory> workspaceProjectContextFactory,
IProjectFaultHandlerService faultHandlerService,
JoinableTaskCollection joinableTaskCollection,
JoinableTaskFactory joinableTaskFactory,
JoinableTaskContextNode joinableTaskContextNode,
CancellationToken unloadCancellationToken)
: base(joinableTaskContextNode)
{
_slice = slice;
_unconfiguredProject = unconfiguredProject;
_projectGuid = projectGuid;
_updateHandlers = updateHandlers;
_logger = logger;
_activeEditorContextTracker = activeEditorContextTracker;
_commandLineParserServices = commandLineParserServices;
_dataProgressTrackerService = dataProgressTrackerService;
_workspaceProjectContextFactory = workspaceProjectContextFactory;
_faultHandlerService = faultHandlerService;
_joinableTaskCollection = joinableTaskCollection;
_joinableTaskFactory = joinableTaskFactory;
_unloadCancellationToken = unloadCancellationToken;
_baseDirectory = Path.GetDirectoryName(_unconfiguredProject.FullPath);
// We take ownership of the lifetime of the provided update handlers, and dispose them
// when this workspace is disposed.
_disposableBag = new() { updateHandlers };
}
protected override Task InitializeCoreAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
protected override Task DisposeCoreUnderLockAsync(bool initialized)
{
_state = WorkspaceState.Disposed;
_contextCreated.TrySetCanceled();
_disposableBag.Dispose();
IsPrimary = false;
return Task.CompletedTask;
}
/// <summary>
/// Adds an object that will be disposed along with this <see cref="Workspace"/> instance.
/// </summary>
/// <param name="disposable">The object to dispose when this object is disposed.</param>
public void ChainDisposal(IDisposable disposable)
{
Verify.NotDisposed(this);
_disposableBag.Add(disposable);
}
/// <summary>
/// Integrates project updates into the workspace.
/// </summary>
/// <remarks>
/// <para>
/// This method must always receive an evaluation update first. After that point,
/// both evaluation and build updates may arrive in any order, so long as values
/// of each type are ordered correctly.
/// </para>
/// <para>
/// Calls must not overlap. This method is not thread-safe. This method is designed
/// to be called from a dataflow ActionBlock, which will serialize calls, so we
/// needn't perform any locking or protection here.
/// </para>
/// </remarks>
/// <param name="update">The project update to integrate.</param>
/// <returns>A task that completes when the update has been integrated.</returns>
internal async Task OnWorkspaceUpdateAsync(IProjectVersionedValue<WorkspaceUpdate> update)
{
Verify.NotDisposed(this);
await InitializeAsync(_unloadCancellationToken);
Assumes.True(_state is WorkspaceState.Uninitialized or WorkspaceState.Initialized);
await _joinableTaskFactory.RunAsync(
async () =>
{
// Calls never overlap. No synchronisation is needed here.
// We can receive either evaluation OR build data first.
if (TryTransition(WorkspaceState.Uninitialized, WorkspaceState.Initialized))
{
// Note that we create operation progress registrations using the first primary (active) configuration
// within the slice. Over time this may change, but we keep the same registration to the first seen.
ConfiguredProject configuredProject = update.Value switch
{
{ EvaluationUpdate: EvaluationUpdate update } => update.ConfiguredProject,
{ BuildUpdate: BuildUpdate update } => update.ConfiguredProject,
_ => throw Assumes.NotReachable()
};
_evaluationProgressRegistration = _dataProgressTrackerService.RegisterForIntelliSense(this, configuredProject, "LanguageServiceHost.Workspace.Evaluation");
_buildProgressRegistration = _dataProgressTrackerService.RegisterForIntelliSense(this, configuredProject, "LanguageServiceHost.Workspace.ProjectBuild");
_disposableBag.Add(_evaluationProgressRegistration);
_disposableBag.Add(_buildProgressRegistration);
}
await (update.Value switch
{
{ EvaluationUpdate: not null } => OnEvaluationUpdateAsync(update.Derive(u => u.EvaluationUpdate!)),
{ BuildUpdate: not null } => OnBuildUpdateAsync(update.Derive(u => u.BuildUpdate!)),
_ => throw Assumes.NotReachable()
});
});
}
private async Task OnEvaluationUpdateAsync(IProjectVersionedValue<EvaluationUpdate> evaluationUpdate)
{
Assumes.True(_state is WorkspaceState.Initialized);
Assumes.NotNull(_evaluationProgressRegistration);
if (!_seenEvaluation)
{
_seenEvaluation = true;
await ProcessInitialEvaluationDataAsync(_unloadCancellationToken);
}
await OnProjectChangedAsync(
_evaluationProgressRegistration,
evaluationUpdate,
hasChange: e => HasChange(e.Value.EvaluationRuleUpdate) || e.Value.SourceItemsUpdate.ProjectChanges.HasChange(),
applyFunc: ApplyProjectEvaluation,
_unloadCancellationToken);
return;
async Task ProcessInitialEvaluationDataAsync(CancellationToken cancellationToken)
{
_logger.WriteLine("Initializing workspace from evaluation data");
IProjectRuleSnapshot snapshot = evaluationUpdate.Value.EvaluationRuleUpdate.CurrentState[ConfigurationGeneral.SchemaName];
snapshot.Properties.TryGetValue(ConfigurationGeneral.LanguageServiceNameProperty, out string? languageName);
snapshot.Properties.TryGetValue(ConfigurationGeneral.TargetPathProperty, out string? binOutputPath);
snapshot.Properties.TryGetValue(ConfigurationGeneral.MSBuildProjectFullPathProperty, out string? projectFilePath);
snapshot.Properties.TryGetValue(ConfigurationGeneral.AssemblyNameProperty, out string? assemblyName);
snapshot.Properties.TryGetValue(ConfigurationGeneral.CommandLineArgsForDesignTimeEvaluationProperty, out string? commandLineArgsForDesignTimeEvaluation);
if (string.IsNullOrEmpty(languageName) || string.IsNullOrEmpty(binOutputPath) || string.IsNullOrEmpty(projectFilePath))
{
// Insufficient data to initialize the language service.
_state = WorkspaceState.Failed;
throw new("Insufficient project data to initialize the language service.");
}
try
{
_contextId = GetWorkspaceProjectContextId(projectFilePath, _projectGuid, _slice);
_disposableBag.Add(_activeEditorContextTracker.RegisterContext(_contextId));
object? hostObject = _unconfiguredProject.Services.HostObject;
// Call into Roslyn to initialize language service for this project
_context = await _workspaceProjectContextFactory.Value.CreateProjectContextAsync(
languageName,
_contextId,
projectFilePath,
_projectGuid,
hostObject,
binOutputPath,
assemblyName,
cancellationToken);
_disposableBag.Add(_context);
// Update additional properties within a batch to avoid thread pool starvation.
// https://github.com/dotnet/project-system/issues/8027
_context.StartBatch();
try
{
_context.LastDesignTimeBuildSucceeded = false; // By default, turn off diagnostics until the first design time build succeeds for this project.
// Pass along any early approximation we have of the command line options
#pragma warning disable CS0618 // This was obsoleted in favor of the one that takes an array, but here just the string is easier; we'll un-Obsolete this API
_context.SetOptions(commandLineArgsForDesignTimeEvaluation ?? "");
#pragma warning restore CS0618 // Type or member is obsolete
}
finally
{
await _context.EndBatchAsync();
}
_contextCreated.TrySetResult();
}
catch (Exception ex)
{
_state = WorkspaceState.Failed;
await _faultHandlerService.ReportFaultAsync(ex, _unconfiguredProject, ProjectFaultSeverity.LimitedFunctionality);
_context?.Dispose();
// We will never initialize now. Ensure anyone waiting on initialization sees the error.
_contextCreated.TrySetException(ex);
_disposableBag.Dispose();
// Let the exception escape, to unsubscribe data sources.
throw;
}
}
void ApplyProjectEvaluation(
IProjectVersionedValue<EvaluationUpdate> update,
ContextState contextState,
CancellationToken cancellationToken)
{
IComparable version = GetConfiguredProjectVersion(update);
ProcessProjectEvaluationHandlers();
ProcessSourceItemsHandlers();
void ProcessProjectEvaluationHandlers()
{
// This is the ConfiguredProject currently bound to the slice owned by this workspace.
// It may change over time, such as in response to changing the active configuration,
// for example from Debug to Release.
ConfiguredProject configuredProject = update.Value.ConfiguredProject;
foreach (IProjectEvaluationHandler evaluationHandler in _updateHandlers.EvaluationHandlers)
{
cancellationToken.ThrowIfCancellationRequested();
IProjectChangeDescription projectChange = update.Value.EvaluationRuleUpdate.ProjectChanges[evaluationHandler.ProjectEvaluationRule];
if (projectChange.Difference.AnyChanges)
{
evaluationHandler.Handle(Context, configuredProject.ProjectConfiguration, version, projectChange, contextState, _logger);
}
}
}
void ProcessSourceItemsHandlers()
{
IImmutableDictionary<string, IProjectChangeDescription> changes = update.Value.SourceItemsUpdate.ProjectChanges;
if (changes.HasChange())
{
foreach (ISourceItemsHandler sourceItemsHandler in _updateHandlers.SourceItemHandlers)
{
cancellationToken.ThrowIfCancellationRequested();
sourceItemsHandler.Handle(Context, version, changes, contextState, _logger);
}
}
}
}
bool HasChange(IProjectSubscriptionUpdate evaluationUpdate)
{
foreach (string ruleName in _updateHandlers.EvaluationRules)
{
if (evaluationUpdate.ProjectChanges[ruleName].Difference.AnyChanges)
{
return true;
}
}
return false;
}
static string GetWorkspaceProjectContextId(string projectFilePath, Guid projectGuid, ProjectConfigurationSlice slice)
{
// WorkspaceContextId must be unique across the entire solution for the life of the solution, therefore as we fire
// up a workspace context per implicitly active config, we factor in both the full path of the project, the GUID of
// project and the name of the config. This will be unique across regardless of whether projects are added or renamed
// to match this project's original name. We include file path to make debugging easier on the Roslyn side.
//
// NOTE: Roslyn also uses this name as the default "AssemblyName" until we explicitly set it, so we need to make
// sure it doesn't contain any invalid path characters.
//
// For example:
// C:\Project\Project.csproj ({72B509BD-C502-4707-ADFD-E2D43867CF45})
// C:\Project\MultiTarget.csproj (net45 {72B509BD-C502-4707-ADFD-E2D43867CF45})
// C:\Project\MultiTarget.csproj (net6.0 {72B509BD-C502-4707-ADFD-E2D43867CF45})
if (slice.Dimensions.Count == 0)
return $"{projectFilePath} ({projectGuid.ToString("B").ToUpperInvariant()})";
else
return $"{projectFilePath} ({string.Join(";", slice.Dimensions.Values)} {projectGuid.ToString("B").ToUpperInvariant()})";
}
}
private async Task OnBuildUpdateAsync(IProjectVersionedValue<BuildUpdate> update)
{
Assumes.True(_state is WorkspaceState.Initialized);
Assumes.NotNull(_buildProgressRegistration);
// The Roslyn workspace context is created when the first evaluation data arrives.
// Upstream caller must ensure that build updates are delayed until the first
// evaluation update is processed.
Assumes.True(_seenEvaluation);
await OnProjectChangedAsync(
_buildProgressRegistration,
update,
hasChange: e => e.Value.BuildRuleUpdate.ProjectChanges[ProjectBuildRuleName].Difference.AnyChanges,
applyFunc: ApplyProjectBuild,
_unloadCancellationToken);
return;
void ApplyProjectBuild(
IProjectVersionedValue<BuildUpdate> update,
ContextState state,
CancellationToken cancellationToken)
{
IProjectChangeDescription projectChange = update.Value.BuildRuleUpdate.ProjectChanges[ProjectBuildRuleName];
ProcessCommandLine();
ProcessProjectBuildFailure(projectChange.After);
void ProcessCommandLine()
{
SetContextCommandLine();
InvokeCommandLineUpdateHandlers();
void SetContextCommandLine()
{
var orderedSource = projectChange.After.Items as IDataWithOriginalSource<KeyValuePair<string, IImmutableDictionary<string, string>>>;
Assumes.NotNull(orderedSource);
// Pass command line to Roslyn
Context.SetOptions(orderedSource.SourceData.Select(pair => pair.Key).ToImmutableArray());
}
void InvokeCommandLineUpdateHandlers()
{
ICommandLineParserService? parser = _commandLineParserServices.FirstOrDefault()?.Value;
Assumes.Present(parser);
IComparable version = GetConfiguredProjectVersion(update);
BuildOptions added = parser.Parse(projectChange.Difference.AddedItems, _baseDirectory);
BuildOptions removed = parser.Parse(projectChange.Difference.RemovedItems, _baseDirectory);
foreach (ICommandLineHandler commandLineHandler in _updateHandlers.CommandLineHandlers)
{
cancellationToken.ThrowIfCancellationRequested();
commandLineHandler.Handle(Context, version, added, removed, state, _logger);
}
}
}
void ProcessProjectBuildFailure(IProjectRuleSnapshot snapshot)
{
// If 'CompileDesignTime' didn't run due to a preceding failed target, or a failure in itself, IsEvaluationSucceeded returns false.
//
// We still forward those 'removes' of references, sources, etc onto Roslyn to avoid duplicate/incorrect results when the next
// successful build occurs, because it will be diff between it and this failed build.
bool succeeded = snapshot.IsEvaluationSucceeded();
if (Context.LastDesignTimeBuildSucceeded != succeeded)
{
_logger.WriteLine(succeeded ? "Last design-time build succeeded, turning semantic errors back on." : "Last design-time build failed, turning semantic errors off.");
Context.LastDesignTimeBuildSucceeded = succeeded;
}
}
}
}
private bool TryTransition(WorkspaceState initialState, WorkspaceState newState)
{
if (_state == initialState)
{
_state = newState;
return true;
}
return false;
}
private Task OnProjectChangedAsync<T>(
IDataProgressTrackerServiceRegistration registration,
IProjectVersionedValue<T> update,
Func<IProjectVersionedValue<T>, bool> hasChange,
Action<IProjectVersionedValue<T>, ContextState, CancellationToken> applyFunc,
CancellationToken cancellationToken)
{
return ExecuteUnderLockAsync(ApplyProjectChangesUnderLockAsync, cancellationToken);
Task ApplyProjectChangesUnderLockAsync(CancellationToken cancellationToken)
{
if (!hasChange(update))
{
// No change since the last update. We must still update operation progress, but can skip creating a batch.
UpdateProgressRegistration();
return Task.CompletedTask;
}
return ApplyInBatchAsync();
async Task ApplyInBatchAsync()
{
ContextState contextState = new(
isActiveEditorContext: _activeEditorContextTracker.IsActiveEditorContext(ContextId),
isActiveConfiguration: IsPrimary);
Context.StartBatch();
try
{
applyFunc(update, contextState, cancellationToken);
}
finally
{
await Context.EndBatchAsync();
UpdateProgressRegistration();
}
}
void UpdateProgressRegistration()
{
// Notify operation progress that we've now processed these versions of our input, if they are
// up-to-date with the latest version that produced, then we no longer considered "in progress".
registration?.NotifyOutputDataCalculated(update.DataSourceVersions);
}
}
}
private static IComparable GetConfiguredProjectVersion(IProjectValueVersions update)
{
return update.DataSourceVersions[ProjectDataSources.ConfiguredProjectVersion];
}
public async Task WriteAsync(Func<IWorkspace, Task> action, CancellationToken cancellationToken)
{
Requires.NotNull(action, nameof(action));
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(_unloadCancellationToken, cancellationToken).Token;
await WhenContextCreated(cancellationToken);
await ExecuteUnderLockAsync(_ => action(this), cancellationToken);
}
public async Task<T> WriteAsync<T>(Func<IWorkspace, Task<T>> action, CancellationToken cancellationToken)
{
Requires.NotNull(action, nameof(action));
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(_unloadCancellationToken, cancellationToken).Token;
await WhenContextCreated(cancellationToken);
return await ExecuteUnderLockAsync(_ => action(this), cancellationToken);
}
private async Task WhenContextCreated(CancellationToken cancellationToken)
{
Verify.NotDisposed(this);
// Join the same collection that's used by our dataflow nodes, so that if we are called on
// the main thread, we don't block anything that might prohibit dataflow from progressing
// this workspace's initialisation (leading to deadlock).
using (_joinableTaskCollection.Join())
{
// Ensure we have received enough data to create the context.
await _contextCreated.Task.WithCancellation(cancellationToken);
Verify.NotDisposed(this);
}
}
}