25
25
using Microsoft . Build . Utilities ;
26
26
27
27
using BackendNativeMethods = Microsoft . Build . BackEnd . NativeMethods ;
28
+ using Task = System . Threading . Tasks . Task ;
29
+ using DotNetFrameworkArchitecture = Microsoft . Build . Shared . DotNetFrameworkArchitecture ;
30
+ using Microsoft . Build . Framework ;
31
+ using Microsoft . Build . BackEnd . Logging ;
28
32
29
33
namespace Microsoft . Build . BackEnd
30
34
{
@@ -49,6 +53,11 @@ internal abstract class NodeProviderOutOfProcBase
49
53
/// </summary>
50
54
private const int TimeoutForNewNodeCreation = 30000 ;
51
55
56
+ /// <summary>
57
+ /// The amount of time to wait for an out-of-proc node to exit.
58
+ /// </summary>
59
+ private const int TimeoutForWaitForExit = 30000 ;
60
+
52
61
/// <summary>
53
62
/// The build component host.
54
63
/// </summary>
@@ -95,9 +104,30 @@ protected void ShutdownConnectedNodes(List<NodeContext> contextsToShutDown, bool
95
104
// Send the build completion message to the nodes, causing them to shutdown or reset.
96
105
_processesToIgnore . Clear ( ) ;
97
106
107
+ // We wait for child nodes to exit to avoid them changing the terminal
108
+ // after this process terminates.
109
+ bool waitForExit = ! enableReuse &&
110
+ ! Console . IsInputRedirected &&
111
+ Traits . Instance . EscapeHatches . EnsureStdOutForChildNodesIsPrimaryStdout ;
112
+
113
+ Task [ ] waitForExitTasks = waitForExit && contextsToShutDown . Count > 0 ? new Task [ contextsToShutDown . Count ] : null ;
114
+ int i = 0 ;
115
+ var loggingService = _componentHost . LoggingService ;
98
116
foreach ( NodeContext nodeContext in contextsToShutDown )
99
117
{
100
- nodeContext ? . SendData ( new NodeBuildComplete ( enableReuse ) ) ;
118
+ if ( nodeContext is null )
119
+ {
120
+ continue ;
121
+ }
122
+ nodeContext . SendData ( new NodeBuildComplete ( enableReuse ) ) ;
123
+ if ( waitForExit )
124
+ {
125
+ waitForExitTasks [ i ++ ] = nodeContext . WaitForExitAsync ( loggingService ) ;
126
+ }
127
+ }
128
+ if ( waitForExitTasks != null )
129
+ {
130
+ Task . WaitAll ( waitForExitTasks ) ;
101
131
}
102
132
}
103
133
@@ -138,7 +168,7 @@ protected void ShutdownAllNodes(bool nodeReuse, NodeContextTerminateDelegate ter
138
168
{
139
169
// If we're able to connect to such a process, send a packet requesting its termination
140
170
CommunicationsUtilities . Trace ( "Shutting down node with pid = {0}" , nodeProcess . Id ) ;
141
- NodeContext nodeContext = new NodeContext ( 0 , nodeProcess . Id , nodeStream , factory , terminateNode ) ;
171
+ NodeContext nodeContext = new NodeContext ( 0 , nodeProcess , nodeStream , factory , terminateNode ) ;
142
172
nodeContext . SendData ( new NodeBuildComplete ( false /* no node reuse */ ) ) ;
143
173
nodeStream . Dispose ( ) ;
144
174
}
@@ -204,7 +234,7 @@ protected NodeContext GetNode(string msbuildLocation, string commandLineArgs, in
204
234
{
205
235
// Connection successful, use this node.
206
236
CommunicationsUtilities . Trace ( "Successfully connected to existed node {0} which is PID {1}" , nodeId , nodeProcess . Id ) ;
207
- return new NodeContext ( nodeId , nodeProcess . Id , nodeStream , factory , terminateNode ) ;
237
+ return new NodeContext ( nodeId , nodeProcess , nodeStream , factory , terminateNode ) ;
208
238
}
209
239
}
210
240
}
@@ -242,20 +272,20 @@ protected NodeContext GetNode(string msbuildLocation, string commandLineArgs, in
242
272
#endif
243
273
244
274
// Create the node process
245
- int msbuildProcessId = LaunchNode ( msbuildLocation , commandLineArgs ) ;
246
- _processesToIgnore . Add ( GetProcessesToIgnoreKey ( hostHandshake , msbuildProcessId ) ) ;
275
+ Process msbuildProcess = LaunchNode ( msbuildLocation , commandLineArgs ) ;
276
+ _processesToIgnore . Add ( GetProcessesToIgnoreKey ( hostHandshake , msbuildProcess . Id ) ) ;
247
277
248
278
// Note, when running under IMAGEFILEEXECUTIONOPTIONS registry key to debug, the process ID
249
279
// gotten back from CreateProcess is that of the debugger, which causes this to try to connect
250
280
// to the debugger process. Instead, use MSBUILDDEBUGONSTART=1
251
281
252
282
// Now try to connect to it.
253
- Stream nodeStream = TryConnectToProcess ( msbuildProcessId , TimeoutForNewNodeCreation , hostHandshake ) ;
283
+ Stream nodeStream = TryConnectToProcess ( msbuildProcess . Id , TimeoutForNewNodeCreation , hostHandshake ) ;
254
284
if ( nodeStream != null )
255
285
{
256
286
// Connection successful, use this node.
257
- CommunicationsUtilities . Trace ( "Successfully connected to created node {0} which is PID {1}" , nodeId , msbuildProcessId ) ;
258
- return new NodeContext ( nodeId , msbuildProcessId , nodeStream , factory , terminateNode ) ;
287
+ CommunicationsUtilities . Trace ( "Successfully connected to created node {0} which is PID {1}" , nodeId , msbuildProcess . Id ) ;
288
+ return new NodeContext ( nodeId , msbuildProcess , nodeStream , factory , terminateNode ) ;
259
289
}
260
290
}
261
291
@@ -391,7 +421,7 @@ private Stream TryConnectToProcess(int nodeProcessId, int timeout, Handshake han
391
421
/// <summary>
392
422
/// Creates a new MSBuild process
393
423
/// </summary>
394
- private int LaunchNode ( string msbuildLocation , string commandLineArgs )
424
+ private Process LaunchNode ( string msbuildLocation , string commandLineArgs )
395
425
{
396
426
// Should always have been set already.
397
427
ErrorUtilities . VerifyThrowInternalLength ( msbuildLocation , nameof ( msbuildLocation ) ) ;
@@ -490,7 +520,7 @@ private int LaunchNode(string msbuildLocation, string commandLineArgs)
490
520
}
491
521
492
522
CommunicationsUtilities . Trace ( "Successfully launched {1} node with PID {0}" , process . Id , exeName ) ;
493
- return process . Id ;
523
+ return process ;
494
524
}
495
525
else
496
526
{
@@ -548,7 +578,7 @@ out processInfo
548
578
}
549
579
550
580
CommunicationsUtilities . Trace ( "Successfully launched {1} node with PID {0}" , childProcessId , exeName ) ;
551
- return childProcessId ;
581
+ return Process . GetProcessById ( childProcessId ) ;
552
582
}
553
583
}
554
584
@@ -582,6 +612,13 @@ private static string GetCurrentHost()
582
612
/// </summary>
583
613
internal class NodeContext
584
614
{
615
+ enum ExitPacketState
616
+ {
617
+ None ,
618
+ ExitPacketQueued ,
619
+ ExitPacketSent
620
+ }
621
+
585
622
// The pipe(s) used to communicate with the node.
586
623
private Stream _clientToServerStream ;
587
624
private Stream _serverToClientStream ;
@@ -597,9 +634,9 @@ internal class NodeContext
597
634
private int _nodeId ;
598
635
599
636
/// <summary>
600
- /// The process id
637
+ /// The node process.
601
638
/// </summary>
602
- private int _processId ;
639
+ private readonly Process _process ;
603
640
604
641
/// <summary>
605
642
/// An array used to store the header byte for each packet when read.
@@ -631,14 +668,14 @@ internal class NodeContext
631
668
private Task _packetWriteDrainTask = Task . CompletedTask ;
632
669
633
670
/// <summary>
634
- /// Event indicating the node has terminated .
671
+ /// Delegate called when the context terminates .
635
672
/// </summary>
636
- private ManualResetEvent _nodeTerminated ;
673
+ private NodeContextTerminateDelegate _terminateDelegate ;
637
674
638
675
/// <summary>
639
- /// Delegate called when the context terminates .
676
+ /// Tracks the state of the packet sent to terminate the node .
640
677
/// </summary>
641
- private NodeContextTerminateDelegate _terminateDelegate ;
678
+ private ExitPacketState _exitPacketState ;
642
679
643
680
/// <summary>
644
681
/// Per node read buffers
@@ -648,20 +685,18 @@ internal class NodeContext
648
685
/// <summary>
649
686
/// Constructor.
650
687
/// </summary>
651
- public NodeContext ( int nodeId , int processId ,
688
+ public NodeContext ( int nodeId , Process process ,
652
689
Stream nodePipe ,
653
690
INodePacketFactory factory , NodeContextTerminateDelegate terminateDelegate )
654
691
{
655
692
_nodeId = nodeId ;
656
- _processId = processId ;
693
+ _process = process ;
657
694
_clientToServerStream = nodePipe ;
658
695
_serverToClientStream = nodePipe ;
659
696
_packetFactory = factory ;
660
697
_headerByte = new byte [ 5 ] ; // 1 for the packet type, 4 for the body length
661
-
662
698
_readBufferMemoryStream = new MemoryStream ( ) ;
663
699
_writeBufferMemoryStream = new MemoryStream ( ) ;
664
- _nodeTerminated = new ManualResetEvent ( false ) ;
665
700
_terminateDelegate = terminateDelegate ;
666
701
_sharedReadBuffer = InterningBinaryReader . CreateSharedBuffer ( ) ;
667
702
}
@@ -749,6 +784,10 @@ public async Task RunPacketReadLoopAsync()
749
784
/// <param name="packet">The packet to send.</param>
750
785
public void SendData ( INodePacket packet )
751
786
{
787
+ if ( IsExitPacket ( packet ) )
788
+ {
789
+ _exitPacketState = ExitPacketState . ExitPacketQueued ;
790
+ }
752
791
_packetWriteQueue . Add ( packet ) ;
753
792
DrainPacketQueue ( ) ;
754
793
}
@@ -816,6 +855,10 @@ private void SendDataCore(INodePacket packet)
816
855
int lengthToWrite = Math . Min ( writeStreamLength - i , MaxPacketWriteSize ) ;
817
856
_serverToClientStream . Write ( writeStreamBuffer , i , lengthToWrite ) ;
818
857
}
858
+ if ( IsExitPacket ( packet ) )
859
+ {
860
+ _exitPacketState = ExitPacketState . ExitPacketSent ;
861
+ }
819
862
}
820
863
catch ( IOException e )
821
864
{
@@ -828,6 +871,11 @@ private void SendDataCore(INodePacket packet)
828
871
}
829
872
}
830
873
874
+ private static bool IsExitPacket ( INodePacket packet )
875
+ {
876
+ return packet is NodeBuildComplete buildCompletePacket && ! buildCompletePacket . PrepareForReuse ;
877
+ }
878
+
831
879
/// <summary>
832
880
/// Avoid having a BinaryWriter just to write a 4-byte int
833
881
/// </summary>
@@ -842,7 +890,7 @@ private void WriteInt32(MemoryStream stream, int value)
842
890
/// <summary>
843
891
/// Closes the node's context, disconnecting it from the node.
844
892
/// </summary>
845
- public void Close ( )
893
+ private void Close ( )
846
894
{
847
895
_clientToServerStream . Dispose ( ) ;
848
896
if ( ! object . ReferenceEquals ( _clientToServerStream , _serverToClientStream ) )
@@ -852,6 +900,52 @@ public void Close()
852
900
_terminateDelegate ( _nodeId ) ;
853
901
}
854
902
903
+ /// <summary>
904
+ /// Waits for the child node process to exit.
905
+ /// </summary>
906
+ public async Task WaitForExitAsync ( ILoggingService loggingService )
907
+ {
908
+ if ( _exitPacketState == ExitPacketState . ExitPacketQueued )
909
+ {
910
+ // Wait up to 100ms until all remaining packets are sent.
911
+ // We don't need to wait long, just long enough for the Task to start running on the ThreadPool.
912
+ await Task . WhenAny ( _packetWriteDrainTask , Task . Delay ( 100 ) ) ;
913
+ }
914
+ if ( _exitPacketState == ExitPacketState . ExitPacketSent )
915
+ {
916
+ CommunicationsUtilities . Trace ( "Waiting for node with pid = {0} to exit" , _process . Id ) ;
917
+
918
+ // .NET 5 introduces a real WaitForExitAsyc.
919
+ // This is a poor man's implementation that uses polling.
920
+ int timeout = TimeoutForWaitForExit ;
921
+ int delay = 5 ;
922
+ while ( timeout > 0 )
923
+ {
924
+ bool exited = _process . WaitForExit ( milliseconds : 0 ) ;
925
+ if ( exited )
926
+ {
927
+ return ;
928
+ }
929
+ timeout -= delay ;
930
+ await Task . Delay ( delay ) . ConfigureAwait ( false ) ;
931
+
932
+ // Double delay up to 500ms.
933
+ delay = Math . Min ( delay * 2 , 500 ) ;
934
+ }
935
+ }
936
+
937
+ // Kill the child and do a blocking wait.
938
+ loggingService ? . LogWarning (
939
+ BuildEventContext . Invalid ,
940
+ null ,
941
+ BuildEventFileInfo . Empty ,
942
+ "KillingProcessWithPid" ,
943
+ _process . Id ) ;
944
+ CommunicationsUtilities . Trace ( "Killing node with pid = {0}" , _process . Id ) ;
945
+
946
+ _process . KillTree ( timeout : 5000 ) ;
947
+ }
948
+
855
949
#if FEATURE_APM
856
950
/// <summary>
857
951
/// Completes the asynchronous packet write to the node.
@@ -873,17 +967,16 @@ private bool ProcessHeaderBytesRead(int bytesRead)
873
967
{
874
968
if ( bytesRead != _headerByte . Length )
875
969
{
876
- CommunicationsUtilities . Trace ( _nodeId , "COMMUNICATIONS ERROR (HRC) Node: {0} Process: {1} Bytes Read: {2} Expected: {3}" , _nodeId , _processId , bytesRead , _headerByte . Length ) ;
970
+ CommunicationsUtilities . Trace ( _nodeId , "COMMUNICATIONS ERROR (HRC) Node: {0} Process: {1} Bytes Read: {2} Expected: {3}" , _nodeId , _process . Id , bytesRead , _headerByte . Length ) ;
877
971
try
878
972
{
879
- Process childProcess = Process . GetProcessById ( _processId ) ;
880
- if ( childProcess ? . HasExited != false )
973
+ if ( _process . HasExited )
881
974
{
882
- CommunicationsUtilities . Trace ( _nodeId , " Child Process {0} has exited." , _processId ) ;
975
+ CommunicationsUtilities . Trace ( _nodeId , " Child Process {0} has exited." , _process . Id ) ;
883
976
}
884
977
else
885
978
{
886
- CommunicationsUtilities . Trace ( _nodeId , " Child Process {0} is still running." , _processId ) ;
979
+ CommunicationsUtilities . Trace ( _nodeId , " Child Process {0} is still running." , _process . Id ) ;
887
980
}
888
981
}
889
982
catch ( Exception e ) when ( ! ExceptionHandling . IsCriticalException ( e ) )
0 commit comments