1 // Copyright (c) Microsoft. All rights reserved.
2 // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3 
4 using System;
5 using System.Collections;
6 using System.Collections.Generic;
7 using System.Diagnostics;
8 using System.IO;
9 using System.Threading;
10 
11 using Microsoft.Build.Framework;
12 using Microsoft.Build.BuildEngine.Shared;
13 using System.Security.AccessControl;
14 
15 namespace Microsoft.Build.BuildEngine
16 {
17     /// <summary>
18     /// This class hosts a node class in the child process. It uses shared memory to communicate
19     /// with the local node provider.
20     /// Wraps a Node.
21     /// </summary>
22     public class LocalNode
23     {
24         #region Static Constructors
25         /// <summary>
26         /// Hook up an unhandled exception handler, in case our error handling paths are leaky
27         /// </summary>
LocalNode()28         static LocalNode()
29         {
30             AppDomain currentDomain = AppDomain.CurrentDomain;
31             currentDomain.UnhandledException += new UnhandledExceptionEventHandler(UnhandledExceptionHandler);
32         }
33         #endregion
34 
35         #region Static Methods
36 
37         /// <summary>
38         /// Dump any unhandled exceptions to a file so they can be diagnosed
39         /// </summary>
UnhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e)40         private static void UnhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e)
41         {
42             Exception ex = (Exception)e.ExceptionObject;
43             DumpExceptionToFile(ex);
44         }
45 
46         /// <summary>
47         /// Dump the exception information to a file
48         /// </summary>
DumpExceptionToFile(Exception ex)49         internal static void DumpExceptionToFile(Exception ex)
50         {
51                 // Lock as multiple threads may throw simultaneously
52                 lock (dumpFileLocker)
53                 {
54                     if (dumpFileName == null)
55                     {
56                         Guid guid = Guid.NewGuid();
57                         string tempPath = Path.GetTempPath();
58 
59                         // For some reason we get Watson buckets because GetTempPath gives us a folder here that doesn't exist.
60                         // Either because %TMP% is misdefined, or because they deleted the temp folder during the build.
61                         if (!Directory.Exists(tempPath))
62                         {
63                             // If this throws, no sense catching it, we can't log it now, and we're here
64                             // because we're a child node with no console to log to, so die
65                             Directory.CreateDirectory(tempPath);
66                         }
67 
68                         dumpFileName = Path.Combine(tempPath, "MSBuild_" + guid.ToString());
69 
70                         using (StreamWriter writer = new StreamWriter(dumpFileName, true /*append*/))
71                         {
72                             writer.WriteLine("UNHANDLED EXCEPTIONS FROM CHILD NODE:");
73                             writer.WriteLine("===================");
74                         }
75                     }
76 
77                     using (StreamWriter writer = new StreamWriter(dumpFileName, true /*append*/))
78                     {
79                         writer.WriteLine(DateTime.Now.ToLongTimeString());
80                         writer.WriteLine(ex.ToString());
81                         writer.WriteLine("===================");
82                     }
83                 }
84         }
85 
86 #endregion
87 
88         #region Constructors
89 
90         /// <summary>
91         /// Creates an instance of this class.
92         /// </summary>
LocalNode(int nodeNumberIn)93         internal LocalNode(int nodeNumberIn)
94         {
95             this.nodeNumber = nodeNumberIn;
96 
97             engineCallback = new LocalNodeCallback(communicationThreadExitEvent, this);
98         }
99 
100         #endregion
101 
102         #region Communication Methods
103 
104         /// <summary>
105         /// This method causes the reader and writer threads to start and create the shared memory structures
106         /// </summary>
StartCommunicationThreads()107         void StartCommunicationThreads()
108         {
109             // The writer thread should be created before the
110             // reader thread because some LocalCallDescriptors
111             // assume the shared memory for the writer thread
112             // has already been created. The method will both
113             // instantiate the shared memory for the writer
114             // thread and also start the writer thread itself.
115             // We will verifyThrow in the method if the
116             // sharedMemory was not created correctly.
117             engineCallback.StartWriterThread(nodeNumber);
118 
119             // Create the shared memory buffer
120             this.sharedMemory =
121                   new SharedMemory
122                   (
123                         // Generate the name for the shared memory region
124                         LocalNodeProviderGlobalNames.NodeInputMemoryName(nodeNumber),
125                         SharedMemoryType.ReadOnly,
126                         // Reuse an existing shared memory region as it should have already
127                         // been created by the parent node side
128                         true
129                   );
130 
131             ErrorUtilities.VerifyThrow(this.sharedMemory.IsUsable,
132                 "Failed to create shared memory for local node input.");
133 
134 
135             // Start the thread that will be processing the calls from the parent engine
136             ThreadStart threadState = new ThreadStart(this.SharedMemoryReaderThread);
137             readerThread = new Thread(threadState);
138             readerThread.Name = "MSBuild Child<-Parent Reader";
139             readerThread.Start();
140 
141         }
142 
143         /// <summary>
144         /// This method causes the reader and writer threads to exit and dispose of the shared memory structures
145         /// </summary>
StopCommunicationThreads()146         void StopCommunicationThreads()
147         {
148             communicationThreadExitEvent.Set();
149 
150             // Wait for communication threads to exit
151             Thread writerThread = engineCallback.GetWriterThread();
152             // The threads may not exist if the child has timed out before the parent has told the node
153             // to start up its communication threads. This can happen if the node is started with /nodemode:x
154             // and no parent is running, or if the parent node has spawned a new process and then crashed
155             // before establishing communication with the child node.
156             if(writerThread != null)
157             {
158               writerThread.Join();
159             }
160 
161             if (readerThread != null)
162             {
163                 readerThread.Join();
164             }
165 
166             // Make sure the exit event is not set
167             communicationThreadExitEvent.Reset();
168         }
169 
170         #endregion
171 
172         #region Startup Methods
173 
174         /// <summary>
175         /// Create global events necessary for handshaking with the parent
176         /// </summary>
177         /// <param name="nodeNumber"></param>
178         /// <returns>True if events created successfully and false otherwise</returns>
CreateGlobalEvents(int nodeNumber)179         private static bool CreateGlobalEvents(int nodeNumber)
180         {
181             bool createdNew = false;
182             if (NativeMethods.IsUserAdministrator())
183             {
184                 EventWaitHandleSecurity mSec = new EventWaitHandleSecurity();
185 
186                 // Add a rule that grants the access only to admins and systems
187                 mSec.SetSecurityDescriptorSddlForm(NativeMethods.ADMINONLYSDDL);
188 
189                 // Create an initiation event to allow the parent side  to prove to the child that we have the same level of privilege as it does.
190                 // this is done by having the parent set this event which means it needs to have administrative permissions to do so.
191                 globalInitiateActivationEvent = new EventWaitHandle(false, EventResetMode.ManualReset, LocalNodeProviderGlobalNames.NodeInitiateActivationEventName(nodeNumber), out createdNew, mSec);
192             }
193             else
194             {
195                 // Create an initiation event to allow the parent side  to prove to the child that we have the same level of privilege as it does.
196                 // this is done by having the parent set this event which means it has atleast the same permissions as the child process
197                 globalInitiateActivationEvent = new EventWaitHandle(false, EventResetMode.ManualReset, LocalNodeProviderGlobalNames.NodeInitiateActivationEventName(nodeNumber), out createdNew);
198             }
199 
200             // This process must be the creator of the event to prevent squating by a lower privilaged attacker
201             if (!createdNew)
202             {
203                 return false;
204             }
205 
206             // Informs the parent process that the child process has been created.
207             globalNodeActive = new EventWaitHandle(false, EventResetMode.ManualReset, LocalNodeProviderGlobalNames.NodeActiveEventName(nodeNumber));
208             globalNodeActive.Set();
209 
210             // Indicate to the parent process, this node is currently is ready to start to recieve requests
211             globalNodeInUse = new EventWaitHandle(false, EventResetMode.ManualReset, LocalNodeProviderGlobalNames.NodeInUseEventName(nodeNumber));
212 
213             // Used by the parent process to inform the child process to shutdown due to the child process
214             // not recieving the initialization command.
215             globalNodeErrorShutdown = new EventWaitHandle(false, EventResetMode.ManualReset, LocalNodeProviderGlobalNames.NodeErrorShutdownEventName(nodeNumber));
216 
217             // Inform the parent process the node has started its communication threads.
218             globalNodeActivate = new EventWaitHandle(false, EventResetMode.ManualReset, LocalNodeProviderGlobalNames.NodeActivedEventName(nodeNumber));
219 
220             return true;
221         }
222 
223         /// <summary>
224         /// This function starts local node when process is launched and shuts it down on time out
225         /// Called by msbuild.exe.
226         /// </summary>
StartLocalNodeServer(int nodeNumber)227         public static void StartLocalNodeServer(int nodeNumber)
228         {
229             // Create global events necessary for handshaking with the parent
230             if (!CreateGlobalEvents(nodeNumber))
231             {
232                 return;
233             }
234 
235             LocalNode localNode = new LocalNode(nodeNumber);
236 
237             WaitHandle[] waitHandles = new WaitHandle[4];
238             waitHandles[0] = shutdownEvent;
239             waitHandles[1] = globalNodeErrorShutdown;
240             waitHandles[2] = inUseEvent;
241             waitHandles[3] = globalInitiateActivationEvent;
242 
243             // This is necessary to make build.exe finish promptly. Dont remove.
244             if (!Engine.debugMode)
245             {
246                 // Create null streams for the current input/output/error streams
247                 Console.SetOut(new StreamWriter(Stream.Null));
248                 Console.SetError(new StreamWriter(Stream.Null));
249                 Console.SetIn(new StreamReader(Stream.Null));
250             }
251 
252             bool continueRunning = true;
253 
254             while (continueRunning)
255             {
256                 int eventType = WaitHandle.WaitAny(waitHandles, inactivityTimeout, false);
257 
258                 if (eventType == 0 || eventType == 1 || eventType == WaitHandle.WaitTimeout)
259                 {
260                     continueRunning = false;
261                     localNode.ShutdownNode(eventType != 1 ?
262                                            Node.NodeShutdownLevel.PoliteShutdown :
263                                            Node.NodeShutdownLevel.ErrorShutdown, true, true);
264                 }
265                 else if (eventType == 2)
266                 {
267                     // reset the event as we do not want it to go into this state again when we are done with this if statement.
268                     inUseEvent.Reset();
269                     // The parent knows at this point the child process has been launched
270                     globalNodeActivate.Reset();
271                     // Set the global inuse event so other parent processes know this node is now initialized
272                     globalNodeInUse.Set();
273                     // Make a copy of the parents handle to protect ourselves in case the parent dies,
274                     // this is to prevent a parent from reserving a node another parent is trying to use.
275                     globalNodeReserveHandle =
276                         new EventWaitHandle(false, EventResetMode.ManualReset, LocalNodeProviderGlobalNames.NodeReserveEventName(nodeNumber));
277                     WaitHandle[] waitHandlesActive = new WaitHandle[3];
278                     waitHandlesActive[0] = shutdownEvent;
279                     waitHandlesActive[1] = globalNodeErrorShutdown;
280                     waitHandlesActive[2] = notInUseEvent;
281 
282                     eventType = WaitHandle.WaitTimeout;
283                     while (eventType == WaitHandle.WaitTimeout && continueRunning == true)
284                     {
285                         eventType = WaitHandle.WaitAny(waitHandlesActive, parentCheckInterval, false);
286 
287                         if (eventType == 0 || /* nice shutdown due to shutdownEvent */
288                             eventType == 1 || /* error shutdown due to globalNodeErrorShutdown */
289                             eventType == WaitHandle.WaitTimeout && !localNode.IsParentProcessAlive())
290                         {
291                             continueRunning = false;
292                             // If the exit is not triggered by running of shutdown method
293                             if (eventType != 0)
294                             {
295                                 localNode.ShutdownNode(Node.NodeShutdownLevel.ErrorShutdown, true, true);
296                             }
297                         }
298                         else if (eventType == 2)
299                         {
300                             // Trigger a collection before the node goes idle to insure that
301                             // the memory is released to the system as soon as possible
302                             GC.Collect();
303                             // Change the current directory to a safe one so that the directory
304                             // last used by the build can be safely deleted. We must have read
305                             // access to the safe directory so use SystemDirectory for this purpose.
306                             Directory.SetCurrentDirectory(Environment.SystemDirectory);
307                             notInUseEvent.Reset();
308                             globalNodeInUse.Reset();
309                         }
310                     }
311 
312                     ErrorUtilities.VerifyThrow(localNode.node == null,
313                                                "Expected either node to be null or continueRunning to be false.");
314 
315                     // Stop the communication threads and release the shared memory object so that the next parent can create it
316                     localNode.StopCommunicationThreads();
317                     // Close the local copy of the reservation handle (this allows another parent to reserve
318                     // the node)
319                     globalNodeReserveHandle.Close();
320                     globalNodeReserveHandle = null;
321                 }
322                 else if (eventType == 3)
323                 {
324                     globalInitiateActivationEvent.Reset();
325                     localNode.StartCommunicationThreads();
326                     globalNodeActivate.Set();
327                 }
328             }
329             // Stop the communication threads and release the shared memory object so that the next parent can create it
330             localNode.StopCommunicationThreads();
331 
332             globalNodeActive.Close();
333             globalNodeInUse.Close();
334          }
335 
336         #endregion
337 
338         #region Methods
339 
340         /// <summary>
341         /// This method is run in its own thread, it is responsible for reading messages sent from the parent process
342         /// through the shared memory region.
343         /// </summary>
SharedMemoryReaderThread()344         private void SharedMemoryReaderThread()
345         {
346             // Create an array of event to the node thread responds
347             WaitHandle[] waitHandles = new WaitHandle[2];
348             waitHandles[0] = communicationThreadExitEvent;
349             waitHandles[1] = sharedMemory.ReadFlag;
350 
351             bool continueExecution = true;
352 
353             try
354             {
355                 while (continueExecution)
356                 {
357                     // Wait for the next work item or an exit command
358                     int eventType = WaitHandle.WaitAny(waitHandles);
359 
360                     if (eventType == 0)
361                     {
362                         // Exit node event
363                         continueExecution = false;
364                     }
365                     else
366                     {
367                         // Read the list of LocalCallDescriptors from sharedMemory,
368                         // this will be null if a large object is being read from shared
369                         // memory and will continue to be null until the large object has
370                         // been completly sent.
371                         IList localCallDescriptorList = sharedMemory.Read();
372 
373                         if (localCallDescriptorList != null)
374                         {
375                             foreach (LocalCallDescriptor callDescriptor in localCallDescriptorList)
376                             {
377                                 // Execute the command method which relates to running on a child node
378                                 callDescriptor.NodeAction(node, this);
379 
380                                 if ((callDescriptor.IsReply) && (callDescriptor is LocalReplyCallDescriptor))
381                                 {
382                                     // Process the reply from the parent so it can be looked in a hashtable based
383                                     // on the call descriptor who requested the reply.
384                                     engineCallback.PostReplyFromParent((LocalReplyCallDescriptor) callDescriptor);
385                                 }
386                             }
387                         }
388                     }
389                 }
390             }
391             catch (Exception e)
392             {
393                 // Will rethrow the exception if necessary
394                 ReportFatalCommunicationError(e);
395             }
396 
397             // Dispose of the shared memory buffer
398             if (sharedMemory != null)
399             {
400                 sharedMemory.Dispose();
401                 sharedMemory = null;
402             }
403         }
404 
405         /// <summary>
406         /// This method will shutdown the node being hosted by the child process and notify the parent process if requested,
407         /// </summary>
408         /// <param name="shutdownLevel">What kind of shutdown is causing the child node to shutdown</param>
409         /// <param name="exitProcess">should the child process exit as part of the shutdown process</param>
410         /// <param name="noParentNotification">Indicates if the parent process should be notified the child node is being shutdown</param>
ShutdownNode(Node.NodeShutdownLevel shutdownLevel, bool exitProcess, bool noParentNotification)411         internal void ShutdownNode(Node.NodeShutdownLevel shutdownLevel, bool exitProcess, bool noParentNotification)
412         {
413             if (node != null)
414             {
415                 try
416                 {
417                     node.ShutdownNode(shutdownLevel);
418 
419                     if (!noParentNotification)
420                     {
421                         // Write the last event out directly
422                         LocalCallDescriptorForShutdownComplete callDescriptor =
423 
424                             new LocalCallDescriptorForShutdownComplete(shutdownLevel, node.TotalTaskTime);
425                         // Post the message indicating that the shutdown is complete
426                         engineCallback.PostMessageToParent(callDescriptor, true);
427                      }
428                 }
429                 catch (Exception e)
430                 {
431                      if (shutdownLevel != Node.NodeShutdownLevel.ErrorShutdown)
432                     {
433                         ReportNonFatalCommunicationError(e);
434                     }
435                 }
436             }
437 
438             // If the shutdownLevel is not a build complete message, then this means there was a politeshutdown or an error shutdown, null the node out
439             // as either it is no longer needed due to the node goign idle or there was a error and it is now in a bad state.
440             if (shutdownLevel != Node.NodeShutdownLevel.BuildCompleteSuccess &&
441                 shutdownLevel != Node.NodeShutdownLevel.BuildCompleteFailure)
442             {
443                 node = null;
444                 notInUseEvent.Set();
445             }
446 
447             if (exitProcess)
448             {
449                 // Even if we completed a build, if we are goign to exit the process we need to null out the node and set the notInUseEvent, this is
450                 // accomplished by calling this method again with the ErrorShutdown handle
451                 if ( shutdownLevel == Node.NodeShutdownLevel.BuildCompleteSuccess || shutdownLevel == Node.NodeShutdownLevel.BuildCompleteFailure )
452                 {
453                     ShutdownNode(Node.NodeShutdownLevel.ErrorShutdown, false, true);
454                 }
455                 // Signal all the communication threads to exit
456                 shutdownEvent.Set();
457             }
458         }
459 
460         /// <summary>
461         /// This methods activates the local node
462         /// </summary>
Activate( Hashtable environmentVariables, LoggerDescription[] nodeLoggers, int nodeId, BuildPropertyGroup parentGlobalProperties, ToolsetDefinitionLocations toolsetSearchLocations, int parentId, string parentStartupDirectory )463         internal void Activate
464         (
465             Hashtable environmentVariables,
466             LoggerDescription[] nodeLoggers,
467             int nodeId,
468             BuildPropertyGroup parentGlobalProperties,
469             ToolsetDefinitionLocations toolsetSearchLocations,
470             int parentId,
471             string parentStartupDirectory
472         )
473         {
474             ErrorUtilities.VerifyThrow(node == null, "Expected node to be null on activation.");
475 
476             this.parentProcessId = parentId;
477 
478             engineCallback.Reset();
479 
480             inUseEvent.Set();
481 
482             // Clear the environment so that we dont have extra variables laying around, this
483             // may be a performance hog but needs to be done
484             IDictionary variableDictionary = Environment.GetEnvironmentVariables();
485             foreach (string variableName in variableDictionary.Keys)
486             {
487                 Environment.SetEnvironmentVariable(variableName, null);
488             }
489 
490             foreach(string key in environmentVariables.Keys)
491             {
492                 Environment.SetEnvironmentVariable(key,(string)environmentVariables[key]);
493             }
494 
495             // Host the msbuild engine and system
496             node = new Node(nodeId, nodeLoggers, engineCallback, parentGlobalProperties, toolsetSearchLocations, parentStartupDirectory);
497 
498 
499             // Write the initialization complete event out directly
500             LocalCallDescriptorForInitializationComplete callDescriptor =
501                 new LocalCallDescriptorForInitializationComplete(Process.GetCurrentProcess().Id);
502 
503             // Post the message indicating that the initialization is complete
504             engineCallback.PostMessageToParent(callDescriptor, true);
505         }
506 
507         /// <summary>
508         /// This method checks is the parent process has not exited
509         /// </summary>
510         /// <returns>True if the parent process is still alive</returns>
IsParentProcessAlive()511         private bool IsParentProcessAlive()
512         {
513             bool isParentAlive = true;
514             try
515             {
516                 // Check if the parent is still there
517                 if (Process.GetProcessById(parentProcessId).HasExited)
518                 {
519                     isParentAlive = false;
520                 }
521             }
522             catch (ArgumentException)
523             {
524                 isParentAlive = false;
525             }
526 
527             if (!isParentAlive)
528             {
529                 // No logging's going to reach the parent at this point:
530                 // indicate on the console what's going on
531                 string message = ResourceUtilities.FormatResourceString("ParentProcessUnexpectedlyDied", node.NodeId);
532                 Console.WriteLine(message);
533             }
534 
535             return isParentAlive;
536         }
537 
538         /// <summary>
539         /// Any error occuring in the shared memory transport is considered to be fatal
540         /// </summary>
541         /// <param name="originalException"></param>
542         /// <exception cref="Exception">Re-throws exception passed in</exception>
ReportFatalCommunicationError(Exception originalException)543         internal void ReportFatalCommunicationError(Exception originalException)
544         {
545             try
546             {
547                 DumpExceptionToFile(originalException);
548             }
549             finally
550             {
551                 if (node != null)
552                 {
553                     node.ReportFatalCommunicationError(originalException, null);
554                 }
555             }
556         }
557 
558         /// <summary>
559         /// This function is used to report exceptions which don't indicate breakdown
560         /// of communication with the parent
561         /// </summary>
562         /// <param name="originalException"></param>
ReportNonFatalCommunicationError(Exception originalException)563         internal void ReportNonFatalCommunicationError(Exception originalException)
564         {
565 
566             if (node != null)
567             {
568                 try
569                 {
570                     DumpExceptionToFile(originalException);
571                 }
572                 finally
573                 {
574                     node.ReportUnhandledError(originalException);
575                 }
576             }
577             else
578             {
579                 // Since there is no node object report rethrow the exception
580                 ReportFatalCommunicationError(originalException);
581             }
582         }
583 
584         #endregion
585         #region Properties
586         internal static string DumpFileName
587         {
588             get
589             {
590                 return dumpFileName;
591             }
592         }
593         #endregion
594 
595         #region Member data
596 
597         private Node node;
598         private SharedMemory sharedMemory;
599         private LocalNodeCallback engineCallback;
600         private int parentProcessId;
601         private int nodeNumber;
602         private Thread readerThread;
603         private static object dumpFileLocker = new Object();
604 
605         // Public named events
606         // If this event is set the node host process is currently running
607         private static EventWaitHandle globalNodeActive;
608         // If this event is set the node is currently running a build
609         private static EventWaitHandle globalNodeInUse;
610         // If this event exists the node is reserved for use by a particular parent engine
611         // the node keeps a handle to this event during builds to prevent it from being used
612         // by another parent engine if the original dies
613         private static EventWaitHandle globalNodeReserveHandle;
614         // If this event is set the node will immediatelly exit. The event is used by the
615         // parent engine to cause the node to exit if communication is lost.
616         private static EventWaitHandle globalNodeErrorShutdown;
617         // This event is used to cause the child to create the shared memory structures to start communication
618         // with the parent
619         private static EventWaitHandle globalInitiateActivationEvent;
620         // This event is used to indicate to the parent that shared memory buffers have been created and are ready for
621         // use
622         private static EventWaitHandle globalNodeActivate;
623         // Private local events
624         private static ManualResetEvent communicationThreadExitEvent = new ManualResetEvent(false);
625         private static ManualResetEvent shutdownEvent = new ManualResetEvent(false);
626         private static ManualResetEvent notInUseEvent = new ManualResetEvent(false);
627 
628         /// <summary>
629         /// Indicates the node is now in use. This means the node has recieved an activate command with initialization
630         /// data from the parent procss
631         /// </summary>
632         private static ManualResetEvent inUseEvent    = new ManualResetEvent(false);
633 
634         /// <summary>
635         /// Randomly generated file name for all exceptions thrown by this node that need to be dumped to a file.
636         /// (There may be more than one exception, if they occur on different threads.)
637         /// </summary>
638         private static string dumpFileName = null;
639 
640         // Timeouts && Constants
641         private const int inactivityTimeout   = 60 * 1000; // 60 seconds of inactivity to exit
642         private const int parentCheckInterval = 5 * 1000; // Check if the parent process is there every 5 seconds
643 
644         #endregion
645 
646     }
647 }
648 
649