-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
Copy pathRequestBuilder.cs
1566 lines (1344 loc) · 68.9 KB
/
RequestBuilder.cs
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
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Build.BackEnd.Logging;
using Microsoft.Build.Collections;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Eventing;
using Microsoft.Build.Exceptions;
using Microsoft.Build.Execution;
using Microsoft.Build.Experimental.BuildCheck;
using Microsoft.Build.Experimental.BuildCheck.Infrastructure;
using Microsoft.Build.Framework;
using Microsoft.Build.Internal;
using Microsoft.Build.Shared;
using Microsoft.Build.TelemetryInfra;
using NodeLoggingContext = Microsoft.Build.BackEnd.Logging.NodeLoggingContext;
using ProjectLoggingContext = Microsoft.Build.BackEnd.Logging.ProjectLoggingContext;
#nullable disable
namespace Microsoft.Build.BackEnd
{
/// <summary>
/// Implementation of IRequestBuilder
/// </summary>
internal class RequestBuilder : IRequestBuilder, IRequestBuilderCallback, IBuildComponent
{
/// <summary>
/// The dedicated scheduler object.
/// </summary>
private static readonly TaskScheduler s_dedicatedScheduler = new DedicatedThreadsTaskScheduler();
/// <summary>
/// The event used to signal that this request should immediately terminate.
/// </summary>
private ManualResetEvent _terminateEvent;
/// <summary>
/// The event used to signal that this request should wake up from its wait state.
/// </summary>
private AutoResetEvent _continueEvent;
/// <summary>
/// The results used when a build request entry continues.
/// </summary>
private IDictionary<int, BuildResult> _continueResults;
/// <summary>
/// Queue of actions to call when a resource request is responded to.
/// </summary>
private ConcurrentQueue<Action<ResourceResponse>> _pendingResourceRequests;
/// <summary>
/// The task representing the currently-executing build request.
/// </summary>
private Task _requestTask;
/// <summary>
/// The cancellation token source for the currently-executing build request.
/// </summary>
private CancellationTokenSource _cancellationTokenSource;
/// <summary>
/// The build request entry being built.
/// </summary>
private BuildRequestEntry _requestEntry;
/// <summary>
/// The component host.
/// </summary>
private IBuildComponentHost _componentHost;
/// <summary>
/// The node logging context
/// </summary>
private NodeLoggingContext _nodeLoggingContext;
/// <summary>
/// The project logging context
/// </summary>
private ProjectLoggingContext _projectLoggingContext;
/// <summary>
/// The target builder.
/// </summary>
private ITargetBuilder _targetBuilder;
/// <summary>
/// Block type
/// </summary>
private BlockType _blockType = BlockType.Unblocked;
/// <summary>
/// Flag indicating we are in an MSBuild callback
/// </summary>
private bool _inMSBuildCallback = false;
/// <summary>
/// Flag indicating whether this request builder has been zombied by a cancellation request.
/// </summary>
private bool _isZombie = false;
/// <summary>
/// Creates a new request builder.
/// </summary>
internal RequestBuilder()
{
_terminateEvent = new ManualResetEvent(false);
_continueEvent = new AutoResetEvent(false);
_pendingResourceRequests = new ConcurrentQueue<Action<ResourceResponse>>();
}
/// <summary>
/// The event raised when a new build request should be issued.
/// </summary>
public event NewBuildRequestsDelegate OnNewBuildRequests;
/// <summary>
/// The event raised when the build request has completed.
/// </summary>
public event BuildRequestCompletedDelegate OnBuildRequestCompleted;
/// <summary>
/// The event raised when the build request has completed.
/// </summary>
public event BuildRequestBlockedDelegate OnBuildRequestBlocked;
/// <summary>
/// The event raised when resources are requested.
/// </summary>
public event ResourceRequestDelegate OnResourceRequest;
/// <summary>
/// The current block type
/// </summary>
private enum BlockType
{
/// <summary>
/// We are blocked waiting on a target in progress.
/// </summary>
BlockedOnTargetInProgress,
/// <summary>
/// We are blocked waiting for results from child requests.
/// </summary>
BlockedOnChildRequests,
/// <summary>
/// We are blocked because we have yielded control
/// </summary>
Yielded,
/// <summary>
/// We are not blocked at all.
/// </summary>
Unblocked
}
/// <summary>
/// Retrieves the request entry associated with this RequestBuilder.
/// </summary>
internal BuildRequestEntry RequestEntry
{
get
{
VerifyIsNotZombie();
return _requestEntry;
}
}
/// <summary>
/// Returns true if this RequestBuilder has an active build request
/// </summary>
internal bool HasActiveBuildRequest
{
get
{
VerifyIsNotZombie();
return (_requestTask?.IsCompleted == false) || (_componentHost.LegacyThreadingData.MainThreadSubmissionId != -1);
}
}
/// <summary>
/// Starts a build request
/// </summary>
/// <param name="loggingContext">The logging context for the node.</param>
/// <param name="entry">The entry to build.</param>
public void BuildRequest(NodeLoggingContext loggingContext, BuildRequestEntry entry)
{
ErrorUtilities.VerifyThrowArgumentNull(loggingContext);
ErrorUtilities.VerifyThrowArgumentNull(entry);
ErrorUtilities.VerifyThrow(_componentHost != null, "Host not set.");
ErrorUtilities.VerifyThrow(_targetBuilder == null, "targetBuilder not null");
ErrorUtilities.VerifyThrow(_nodeLoggingContext == null, "nodeLoggingContext not null");
ErrorUtilities.VerifyThrow(_requestEntry == null, "requestEntry not null");
ErrorUtilities.VerifyThrow(!_terminateEvent.WaitOne(0), "Cancel already called");
_nodeLoggingContext = loggingContext;
_blockType = BlockType.Unblocked;
_requestEntry = entry;
_requestEntry.Continue();
_continueResults = null;
_targetBuilder = (ITargetBuilder)_componentHost.GetComponent(BuildComponentType.TargetBuilder);
VerifyEntryInActiveState();
InitializeOperatingEnvironment();
StartBuilderThread();
}
/// <summary>
/// Continues a build request
/// </summary>
public void ContinueRequest()
{
ErrorUtilities.VerifyThrow(HasActiveBuildRequest, "Request not building");
ErrorUtilities.VerifyThrow(!_terminateEvent.WaitOne(0), "Request already terminated");
ErrorUtilities.VerifyThrow(!_continueEvent.WaitOne(0), "Request already continued");
VerifyEntryInReadyState();
_continueResults = _requestEntry.Continue();
ErrorUtilities.VerifyThrow(_blockType == BlockType.BlockedOnTargetInProgress || _blockType == BlockType.Yielded || (_continueResults != null), "Unexpected null results for request {0} (nr {1})", _requestEntry.Request.GlobalRequestId, _requestEntry.Request.NodeRequestId);
// Setting the continue event will wake up the build thread, which is suspended in StartNewBuildRequests.
_continueEvent.Set();
}
/// <summary>
/// Continues a build request after receiving a resource response.
/// </summary>
public void ContinueRequestWithResources(ResourceResponse response)
{
ErrorUtilities.VerifyThrow(HasActiveBuildRequest, "Request not building");
ErrorUtilities.VerifyThrow(!_terminateEvent.WaitOne(0), "Request already terminated");
ErrorUtilities.VerifyThrow(!_pendingResourceRequests.IsEmpty, "No pending resource requests");
VerifyEntryInActiveOrWaitingState();
_pendingResourceRequests.Dequeue()(response);
}
/// <summary>
/// Terminates the build request
/// </summary>
/// <remarks>
/// Once we have entered this method, no more methods will be invoked on this class (save ShutdownComponent)
/// as we should no longer be receiving any messages from the BuildManager.
/// </remarks>
public void CancelRequest()
{
this.BeginCancel();
this.WaitForCancelCompletion();
}
/// <summary>
/// Starts to cancel an existing request.
/// </summary>
public void BeginCancel()
{
_terminateEvent.Set();
// Cancel the current build.
if (_cancellationTokenSource != null)
{
if (_cancellationTokenSource.IsCancellationRequested)
{
return;
}
_cancellationTokenSource.Cancel();
}
}
/// <summary>
/// Waits for the cancellation until it's completed, and cleans up the internal states.
/// </summary>
public void WaitForCancelCompletion()
{
// Wait for the request thread to terminate.
if (_requestTask != null)
{
bool taskCleanedUp = false;
try
{
taskCleanedUp = _requestTask.Wait(BuildParameters.RequestBuilderShutdownTimeout);
}
catch (AggregateException e) when (InnerExceptionsAreAllCancelledExceptions(e))
{
// ignore -- just indicates that the task finished cancelling before we got a chance to wait on it.
taskCleanedUp = true;
}
if (!taskCleanedUp)
{
// This can happen when a task has locked us up.
_projectLoggingContext.LogError(new BuildEventFileInfo(String.Empty), "FailedToReceiveTaskThreadStatus", BuildParameters.RequestBuilderShutdownTimeout);
ErrorUtilities.ThrowInvalidOperation("UnableToCancel");
}
}
_isZombie = true;
}
private bool InnerExceptionsAreAllCancelledExceptions(AggregateException e)
{
return e.Flatten().InnerExceptions.All(ex => ex is TaskCanceledException || ex is OperationCanceledException);
}
#region IRequestBuilderCallback Members
/// <summary>
/// This method instructs the request builder to build the specified projects using the specified parameters. This is
/// what is ultimately used by something like an MSBuild task which needs to invoke a project-to-project reference. IBuildEngine
/// and IBuildEngine2 have BuildProjectFile methods which boil down to an invocation of this method as well.
/// </summary>
/// <param name="projectFiles">An array of projects to be built.</param>
/// <param name="properties">The property groups to use for each project. Must be the same number as there are project files.</param>
/// <param name="toolsVersions">The tools version to use for each project. Must be the same number as there are project files.</param>
/// <param name="targets">The targets to be built. Each project will be built with the same targets.</param>
/// <param name="waitForResults">True to wait for the results </param>
/// <param name="skipNonexistentTargets">If set, skip targets that are not defined in the projects to be built.</param>
/// <returns>True if the requests were satisfied, false if they were aborted.</returns>
public async Task<BuildResult[]> BuildProjects(string[] projectFiles, PropertyDictionary<ProjectPropertyInstance>[] properties, string[] toolsVersions, string[] targets, bool waitForResults, bool skipNonexistentTargets = false)
{
VerifyIsNotZombie();
ErrorUtilities.VerifyThrowArgumentNull(projectFiles);
ErrorUtilities.VerifyThrowArgumentNull(properties);
ErrorUtilities.VerifyThrowArgumentNull(targets);
ErrorUtilities.VerifyThrowArgumentNull(toolsVersions);
ErrorUtilities.VerifyThrow(_componentHost != null, "No host object set");
ErrorUtilities.VerifyThrow(projectFiles.Length == properties.Length, "Properties and project counts not the same");
ErrorUtilities.VerifyThrow(projectFiles.Length == toolsVersions.Length, "Tools versions and project counts not the same");
FullyQualifiedBuildRequest[] requests = new FullyQualifiedBuildRequest[projectFiles.Length];
for (int i = 0; i < projectFiles.Length; ++i)
{
if (!Path.IsPathRooted(projectFiles[i]))
{
projectFiles[i] = Path.Combine(_requestEntry.ProjectRootDirectory, projectFiles[i]);
}
// Canonicalize
projectFiles[i] = FileUtilities.NormalizePath(projectFiles[i]);
// A tools version specified by an MSBuild task or similar has priority
string explicitToolsVersion = toolsVersions[i];
// Otherwise go to any explicit tools version on the project who made this callback
if (explicitToolsVersion == null && _requestEntry.RequestConfiguration.ExplicitToolsVersionSpecified)
{
explicitToolsVersion = _requestEntry.RequestConfiguration.ToolsVersion;
}
// Otherwise let the BuildRequestConfiguration figure out what tools version will be used
BuildRequestData data = new BuildRequestData(projectFiles[i], properties[i].ToDictionary(), explicitToolsVersion, targets, null);
BuildRequestConfiguration config = new BuildRequestConfiguration(data, _componentHost.BuildParameters.DefaultToolsVersion);
ProjectIsolationMode isolateProjects = _componentHost.BuildParameters.ProjectIsolationMode;
bool skipStaticGraphIsolationConstraints = (isolateProjects != ProjectIsolationMode.False && _requestEntry.RequestConfiguration.ShouldSkipIsolationConstraintsForReference(config.ProjectFullPath))
|| isolateProjects == ProjectIsolationMode.MessageUponIsolationViolation;
requests[i] = new FullyQualifiedBuildRequest(
config: config,
targets: targets,
resultsNeeded: waitForResults,
skipStaticGraphIsolationConstraints: skipStaticGraphIsolationConstraints,
flags: skipNonexistentTargets
? BuildRequestDataFlags.SkipNonexistentTargets
: BuildRequestDataFlags.None);
}
// Send the requests off
BuildResult[] results = await StartNewBuildRequests(requests);
ErrorUtilities.VerifyThrow(requests.Length == results.Length, "# results != # requests");
return results;
}
/// <summary>
/// This method is called when the current request needs to build a target which is already in progress on this configuration, but which
/// is being built by another request.
/// </summary>
/// <param name="blockingGlobalRequestId">The id of the request on which we are blocked.</param>
/// <param name="blockingTarget">The target on which we are blocked.</param>
/// <param name="partialBuildResult">A BuildResult with results from an incomplete build request.</param>
public async Task BlockOnTargetInProgress(int blockingGlobalRequestId, string blockingTarget, BuildResult partialBuildResult = null)
{
VerifyIsNotZombie();
SaveOperatingEnvironment();
_blockType = BlockType.BlockedOnTargetInProgress;
RaiseOnBlockedRequest(blockingGlobalRequestId, blockingTarget, partialBuildResult);
WaitHandle[] handles = [_terminateEvent, _continueEvent];
int handle;
if (IsBuilderUsingLegacyThreadingSemantics(_componentHost, _requestEntry))
{
handle = WaitHandle.WaitAny(handles);
}
else
{
handle = await handles.ToTask();
}
RestoreOperatingEnvironment();
if (handle == 0)
{
// We've been aborted
throw new BuildAbortedException();
}
_blockType = BlockType.Unblocked;
VerifyEntryInActiveState();
}
/// <summary>
/// Yields the node.
/// </summary>
public void Yield()
{
VerifyIsNotZombie();
SaveOperatingEnvironment();
_blockType = BlockType.Yielded;
RaiseOnBlockedRequest(_requestEntry.Request.GlobalRequestId, null);
}
/// <summary>
/// Waits for the node to be reacquired.
/// </summary>
public void Reacquire()
{
VerifyIsNotZombie();
RaiseOnBlockedRequest(_requestEntry.Request.GlobalRequestId, String.Empty);
WaitHandle[] handles = [_terminateEvent, _continueEvent];
int handle = WaitHandle.WaitAny(handles);
RestoreOperatingEnvironment();
if (handle == 0)
{
// We've been aborted
throw new BuildAbortedException();
}
_blockType = BlockType.Unblocked;
VerifyEntryInActiveState();
}
/// <summary>
/// Enters the state where we are going to perform a build request callback.
/// </summary>
public void EnterMSBuildCallbackState()
{
VerifyIsNotZombie();
ErrorUtilities.VerifyThrow(!_inMSBuildCallback, "Already in an MSBuild callback!");
_inMSBuildCallback = true;
}
/// <summary>
/// Exits the build request callback state.
/// </summary>
public void ExitMSBuildCallbackState()
{
VerifyIsNotZombie();
ErrorUtilities.VerifyThrow(_inMSBuildCallback, "Not in an MSBuild callback!");
_inMSBuildCallback = false;
}
/// <summary>
/// Requests CPU resources from the scheduler.
/// </summary>
public int RequestCores(object monitorLockObject, int requestedCores, bool waitForCores)
{
ErrorUtilities.VerifyThrow(Monitor.IsEntered(monitorLockObject), "Not running under the given lock");
VerifyIsNotZombie();
// The task may be calling RequestCores from multiple threads and the call may be blocking, so in general, we have to maintain
// a queue of pending requests.
ResourceResponse responseObject = null;
using AutoResetEvent responseEvent = new AutoResetEvent(false);
_pendingResourceRequests.Enqueue((response) =>
{
responseObject = response;
responseEvent.Set();
});
RaiseResourceRequest(ResourceRequest.CreateAcquireRequest(_requestEntry.Request.GlobalRequestId, requestedCores, waitForCores));
// Wait for one of two events to be signaled: 1) The build was canceled, 2) The response to our request was received.
WaitHandle[] waitHandles = [_terminateEvent, responseEvent];
int waitResult;
// Drop the lock so that the same task can call ReleaseCores from other threads to unblock itself.
Monitor.Exit(monitorLockObject);
try
{
waitResult = WaitHandle.WaitAny(waitHandles);
}
finally
{
// Now re-take the lock before continuing.
Monitor.Enter(monitorLockObject);
}
if (waitResult == 0)
{
// We've been aborted.
throw new BuildAbortedException();
}
VerifyEntryInActiveOrWaitingState();
return responseObject.NumCores;
}
/// <summary>
/// Returns CPU resources to the scheduler.
/// </summary>
public void ReleaseCores(int coresToRelease)
{
VerifyIsNotZombie();
RaiseResourceRequest(ResourceRequest.CreateReleaseRequest(_requestEntry.Request.GlobalRequestId, coresToRelease));
}
#endregion
#region IBuildComponent Members
/// <summary>
/// Sets the component host.
/// </summary>
/// <param name="host">The component host.</param>
public void InitializeComponent(IBuildComponentHost host)
{
ErrorUtilities.VerifyThrowArgumentNull(host);
ErrorUtilities.VerifyThrow(_componentHost == null, "RequestBuilder already initialized.");
_componentHost = host;
}
/// <summary>
/// Shuts down this component
/// </summary>
public void ShutdownComponent()
{
_componentHost = null;
}
#endregion
/// <summary>
/// Returns true if this builder is using legacy threading semantics.
/// </summary>
internal static bool IsBuilderUsingLegacyThreadingSemantics(IBuildComponentHost host, BuildRequestEntry entry)
{
return host.BuildParameters.LegacyThreadingSemantics && (host.LegacyThreadingData.MainThreadSubmissionId == entry.Request.SubmissionId);
}
/// <summary>
/// This method waits for the specified handles, but will also spawn a request builder "thread" if that event is set.
/// This mechanism is used to implement running RequestBuilder threads on the main UI thread in VS.
/// </summary>
/// <returns>The index of the handle which was signaled.</returns>
internal static int WaitWithBuilderThreadStart(WaitHandle[] handles, bool recursive, LegacyThreadingData threadingData, int submissionId)
{
WaitHandle[] allHandles = new WaitHandle[handles.Length + 1];
allHandles[0] = threadingData.GetStartRequestBuilderMainThreadEventForSubmission(submissionId);
Array.Copy(handles, 0, allHandles, 1, handles.Length);
while (true)
{
try
{
int signaledIndex = WaitHandle.WaitAny(allHandles, Timeout.Infinite);
if (signaledIndex == 0)
{
// Grab the request builder reserved for running on this thread.
RequestBuilder builder = threadingData.InstanceForMainThread;
// This clears out the value so we can re-enter with legacy-threading semantics on another request builder
// which must use this same thread. It is safe to perform this operation because request activations cannot
// happen in parallel on the same thread, so there is no race.
threadingData.InstanceForMainThread = null;
// Now wait for the request to build.
builder.RequestThreadProc(setThreadParameters: false).Wait();
}
else
{
// We were signalled on one of the other handles. Return control to the caller.
return signaledIndex - 1;
}
}
finally
{
// If this was the top level submission doing the waiting, we are done with this submission and it's
// main thread building context
if (!recursive)
{
// Set the event indicating the legacy thread is no longer being used, so it is safe to clean up.
threadingData.SignalLegacyThreadEnd(submissionId);
}
}
}
}
/// <summary>
/// Class factory for component creation.
/// </summary>
internal static IBuildComponent CreateComponent(BuildComponentType type)
{
ErrorUtilities.VerifyThrow(type == BuildComponentType.RequestBuilder, "Cannot create components of type {0}", type);
return new RequestBuilder();
}
/// <summary>
/// Starts the thread used to build
/// </summary>
private void StartBuilderThread()
{
ErrorUtilities.VerifyThrow(_requestTask == null, "Already have a task.");
_cancellationTokenSource = new CancellationTokenSource();
// IMPLEMENTATION NOTE: It may look strange that we are creating new tasks here which immediately turn around and create
// more tasks that look async. The reason for this is that while these methods are technically async, they really only
// unwind at very specific times according to the needs of MSBuild, in particular when we are waiting for results from
// another project or when we are Yielding the Build Engine while running certain tasks. Essentially, the Request Builder
// and related components form a giant state machine and the tasks are used to implement one very deep co-routine.
if (IsBuilderUsingLegacyThreadingSemantics(_componentHost, _requestEntry))
{
// Create a task which completes when the legacy threading task thread is finished.
_componentHost.LegacyThreadingData.SignalLegacyThreadStart(this);
_requestTask = Task.Factory.StartNew(
() =>
{
// If this is a very quick-running request, it is possible that the request will have built and completed in
// the time between when StartBuilderThread is called, and when the threadpool gets around to actually servicing
// this request. If that's the case, it's also possible that ShutdownComponent() could have already been called,
// in which case the componentHost will be null.
// In that circumstance, by definition we don't have anyone who will want to wait on the LegacyThreadInactiveEvent
// task, so we can safely just return. Take a snapshot so that we don't fall victim to componentHost being set
// to null between the null check and asking the LegacyThreadingData for the Task.
IBuildComponentHost componentHostSnapshot = _componentHost;
if (componentHostSnapshot?.LegacyThreadingData != null)
{
return componentHostSnapshot.LegacyThreadingData.GetLegacyThreadInactiveTask(_requestEntry.Request.SubmissionId);
}
else
{
return Task.FromResult<object>(null);
}
},
_cancellationTokenSource.Token,
TaskCreationOptions.None,
TaskScheduler.Default).Unwrap();
}
else
{
ErrorUtilities.VerifyThrow(_componentHost.LegacyThreadingData.MainThreadSubmissionId != _requestEntry.Request.SubmissionId, "Can't start builder thread when we are using legacy threading semantics for this request.");
// We do not run in STA by default. Most code does not
// require the STA apartment and the .Net default is to
// create threads with MTA semantics. We provide this
// switch so that those few tasks which may require it
// can be made to work.
if (Environment.GetEnvironmentVariable("MSBUILDFORCESTA") == "1")
{
_requestTask = Task.Factory.StartNew(
() =>
{
return this.RequestThreadProc(setThreadParameters: true);
},
_cancellationTokenSource.Token,
TaskCreationOptions.None,
AwaitExtensions.OneSTAThreadPerTaskSchedulerInstance).Unwrap();
}
else
{
// Start up the request thread. When it starts it will begin building our current entry.
_requestTask = Task.Factory.StartNew(
() =>
{
return this.RequestThreadProc(setThreadParameters: true);
},
_cancellationTokenSource.Token,
TaskCreationOptions.None,
s_dedicatedScheduler).Unwrap();
}
}
}
/// <summary>
/// Set some parameters common to all worker threads we use
/// </summary>
private void SetCommonWorkerThreadParameters()
{
CultureInfo.CurrentCulture = _componentHost.BuildParameters.Culture;
CultureInfo.CurrentUICulture = _componentHost.BuildParameters.UICulture;
Thread.CurrentThread.Priority = _componentHost.BuildParameters.BuildThreadPriority;
Thread.CurrentThread.IsBackground = true;
// NOTE: This is safe to do because we have specified long-running so we get our own new thread.
string threadName = "RequestBuilder thread";
#if FEATURE_APARTMENT_STATE
if (Thread.CurrentThread.GetApartmentState() == ApartmentState.STA)
{
// NOTE: This is safe to do because the STA scheduler always gives us our own new thread.
threadName = "RequestBuilder STA thread";
}
#endif
if (string.IsNullOrEmpty(Thread.CurrentThread.Name))
{
Thread.CurrentThread.Name = threadName;
}
}
/// <summary>
/// Asserts that the entry is in the ready state.
/// </summary>
private void VerifyEntryInReadyState()
{
ErrorUtilities.VerifyThrow(_requestEntry.State == BuildRequestEntryState.Ready, "Entry is not in the Ready state, it is in the {0} state.", _requestEntry.State);
}
/// <summary>
/// Asserts that the entry is in the active state.
/// </summary>
private void VerifyEntryInActiveState()
{
ErrorUtilities.VerifyThrow(_requestEntry.State == BuildRequestEntryState.Active, "Entry is not in the Active state, it is in the {0} state.", _requestEntry.State);
}
/// <summary>
/// Asserts that the entry is in the active or waiting state.
/// </summary>
private void VerifyEntryInActiveOrWaitingState()
{
ErrorUtilities.VerifyThrow(_requestEntry.State == BuildRequestEntryState.Active || _requestEntry.State == BuildRequestEntryState.Waiting,
"Entry is not in the Active or Waiting state, it is in the {0} state.", _requestEntry.State);
}
/// <summary>
/// The entry point for the request builder thread.
/// Launch the project and gather the results, reporting them back to the BuildRequestEngine.
/// </summary>
private async Task RequestThreadProc(bool setThreadParameters)
{
Exception thrownException = null;
BuildResult result = null;
try
{
if (setThreadParameters)
{
SetCommonWorkerThreadParameters();
}
MSBuildEventSource.Log.RequestThreadProcStart();
VerifyEntryInActiveState();
result = await BuildProject();
MSBuildEventSource.Log.RequestThreadProcStop();
}
catch (InvalidProjectFileException ex)
{
if (_projectLoggingContext != null)
{
_projectLoggingContext.LogInvalidProjectFileError(ex);
}
else
{
_nodeLoggingContext.LogInvalidProjectFileError(ex);
}
thrownException = ex;
}
// This is a workaround for https://github.com/dotnet/msbuild/issues/2064. It catches the exception case and turns it into a more understandable warning.
catch (UnbuildableProjectTypeException ex)
{
thrownException = ex;
if (_projectLoggingContext is null)
{
_nodeLoggingContext.LogWarning("SolutionParseUnknownProjectType", ex.Message);
}
else
{
_projectLoggingContext.LogWarning("SolutionParseUnknownProjectType", ex.Message);
}
}
catch (Exception ex)
{
thrownException = ex;
if (ex is BuildAbortedException)
{
// The build was likely cancelled. We do not need to log an error in this case.
}
else if (ex is InternalLoggerException)
{
string realMessage = TaskLoggingHelper.GetInnerExceptionMessageString(ex);
LoggingContext loggingContext = ((LoggingContext)_projectLoggingContext) ?? _nodeLoggingContext;
loggingContext.LogError(
BuildEventFileInfo.Empty,
"FatalErrorWhileLoggingWithInnerException",
realMessage);
loggingContext.LogCommentFromText(MessageImportance.Low, ex.ToString());
}
else if (ex is ThreadAbortException)
{
// Do nothing. This will happen when the thread is forcibly terminated because we are shutting down, for example
// when the unit test framework terminates.
throw;
}
else if (ex is not CriticalTaskException)
{
(((LoggingContext)_projectLoggingContext) ?? _nodeLoggingContext).LogError(BuildEventFileInfo.Empty, "UnhandledMSBuildError", ex.ToString());
}
if (ExceptionHandling.IsCriticalException(ex))
{
// Dump all engine exceptions to a temp file
// so that we have something to go on in the
// event of a failure
ExceptionHandling.DumpExceptionToFile(ex);
// This includes InternalErrorException, which we definitely want a callstack for.
// Fortunately the default console UnhandledExceptionHandler will log the callstack even
// for unhandled exceptions thrown from threads other than the main thread, like here.
// Less fortunately NUnit doesn't.
// This is fatal: process will terminate: make sure the
// debugger launches
ErrorUtilities.ThrowInternalError(ex.Message, ex);
throw;
}
}
finally
{
_blockType = BlockType.Unblocked;
if (thrownException != null)
{
ErrorUtilities.VerifyThrow(result == null, "Result already set when exception was thrown.");
result = new BuildResult(_requestEntry.Request, thrownException);
}
ReportResultAndCleanUp(result);
}
}
/// <summary>
/// Reports this result to the engine and cleans up.
/// </summary>
private void ReportResultAndCleanUp(BuildResult result)
{
if (_projectLoggingContext != null)
{
try
{
_projectLoggingContext.LogProjectFinished(result.OverallResult == BuildResultCode.Success);
}
catch (Exception ex) when (!ExceptionHandling.IsCriticalException(ex))
{
if (result.Exception == null)
{
result.Exception = ex;
}
}
}
// Clear out our state now in case any of these callbacks cause the engine to try and immediately
// reuse this builder.
BuildRequestEntry entryToComplete = _requestEntry;
_nodeLoggingContext = null;
_requestEntry = null;
if (_targetBuilder != null)
{
((IBuildComponent)_targetBuilder).ShutdownComponent();
}
if (_componentHost.BuildParameters.SaveOperatingEnvironment)
{
entryToComplete.RequestConfiguration.SavedCurrentDirectory = NativeMethodsShared.GetCurrentDirectory();
entryToComplete.RequestConfiguration.SavedEnvironmentVariables = CommunicationsUtilities.GetEnvironmentVariables();
}
entryToComplete.Complete(result);
RaiseBuildRequestCompleted(entryToComplete);
}
/// <summary>
/// This is called back when this request needs to issue new requests and possible wait on them. This method will
/// block the builder's thread if any of the requests require us to wait for their results.
/// </summary>
/// <param name="requests">The list of build requests to be built.</param>
/// <returns>The results, or null if the build should terminate.</returns>
private async Task<BuildResult[]> StartNewBuildRequests(FullyQualifiedBuildRequest[] requests)
{
// Determine if we need to wait for results from any of these requests.
// UNDONE: Currently we never set ResultsNeeded to anything but true. The purpose of this flag would be
// to issue another top-level build request which no other request depends on, but which must finish in order for
// the build to be considered complete. This would be brand new semantics.
bool waitForResults = false;
foreach (FullyQualifiedBuildRequest request in requests)
{
if (request.ResultsNeeded)
{
waitForResults = true;
break;
}
}
_blockType = BlockType.BlockedOnChildRequests;
// Save the current operating environment, if necessary
if (waitForResults)
{
SaveOperatingEnvironment();
}
// Issue the requests to the engine
RaiseOnNewBuildRequests(requests);
// TODO: OPTIMIZATION: By returning null here, we commit to having to unwind the stack all the
// way back to RequestThreadProc and then shutting down the thread before we can receive the
// results and continue with them. It is not always the case that this will be desirable, however,
// particularly if the results we need are immediately available. In those cases, it would be
// useful to wait here for a short period in case those results become available - one second
// might be enough. This means we may occasionally get more than one builder thread lying around
// waiting for something to happen, but that would be short lived. At the same time it would
// allow these already-available results to be utilized immediately without the unwind
// semantics.
// Now wait for results if we are supposed to.
BuildResult[] results;
if (waitForResults)
{
WaitHandle[] handles = [_terminateEvent, _continueEvent];
int handle;
if (IsBuilderUsingLegacyThreadingSemantics(_componentHost, _requestEntry))
{
handle = RequestBuilder.WaitWithBuilderThreadStart(handles, true, _componentHost.LegacyThreadingData, _requestEntry.Request.SubmissionId);
}
else if (_inMSBuildCallback)
{
CultureInfo savedCulture = CultureInfo.CurrentCulture;
CultureInfo savedUICulture = CultureInfo.CurrentUICulture;
handle = await handles.ToTask();
CultureInfo.CurrentCulture = savedCulture;
CultureInfo.CurrentUICulture = savedUICulture;
}
else
{
handle = WaitHandle.WaitAny(handles);
}
// If this is not a shutdown case, then the entry should be in the active state.
if (handle == 1)
{
// Restore the operating environment.
RestoreOperatingEnvironment();
VerifyEntryInActiveState();
}
results = GetResultsForContinuation(requests, handle == 1);
}
else
{
results = Array.Empty<BuildResult>();
}
ErrorUtilities.VerifyThrow(requests.Length == results.Length, "# results != # requests");
_blockType = BlockType.Unblocked;
return results;