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