// ---------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. All rights reserved. // // ---------------------------------------------------------------------------- // // //This file contains code for running an external program, as an MSBuild task. //It is essentially a replacement for the Exec task, and this implementation //solves a problem with the MSBuild Exec task implementation. The MSBuild //version does not pump messages while it waits for the external program. //This means that Exec is practically useless for building from within Visual //Studio, because Visual Studio invokes the MSBuild engine (and therefore //all of its tasks) on the VS GUI thread. // //The key problem is that we need to meet all of these needs: // //Receive stdout and stderr messages and call IBuildEngine.LogMessage on //the same thread that called ITask.Execute(). // //Receive notification of the termination of child process. // //Receive and dispatch USER32 (GUI) messages, on the same thread that //called ITask.Execute(). // //I tried a lot of different approaches, and none of them were satisfying. //In the end, the simplest and most reliable approach was to use P/Invoke //to directly call USER32!GetMessage, TranslateMessage, and DispatchMessage //directly. The background (worker) threads wake up the main thread by //posting WM_NULL to the main thread. Rather than communicating data by //passing values in the wParam or lParam, we use a CLR monitor lock to //synchronize access to a shared data structure. The worker threads lock //that data structure, modify it (queue messages), then unlock and post //a message to the main thread. Whenever the main thread returns from //calling GetMessage, it locks the shared data structure and checks to //see if it needs to do any work. This is simple and reliable. // //Since we're P/Invoking directly to Win32, some of the fancy CLR wiring //will be disabled while we're waiting for GetMessage or DispatchMessage. //Fine -- I tried forty different ways of implementing this, and they are //all severe pains in the neck, and I am sick of coddling the broken //threading model and incomplete managed API. Directly calling into USER32 //is the least painful option of all. // // using System; using System.Runtime.InteropServices; using System.IO; using System.Threading; using System.Collections.Generic; using System.Diagnostics; using System.Text; using System.Text.RegularExpressions; using Microsoft.Build.Framework; using Windows; using System.ComponentModel; namespace Microsoft.Singularity.BuildTasks { public abstract class ExecTaskBase : TaskBase { /// // // This method executes a program and redirects its output to the MSBuild // logger. // // This method pumps STA messages while the program is executing. This is // necessary in order to keep the VS IDE responsive, since the IDE (to my // great horror) invokes MSBuild on its UI thread. // // protected bool ExecuteProgram(string exepath, IEnumerable args) { if (_showDetailedProgramArguments) { LogMessage("Arguments:"); foreach (string arg in args) { LogMessage(" " + arg); } } // Now build the single-string command line that we will execute. StringBuilder buffer = new StringBuilder(); // buffer.Append(AddQuotesIfNecessary(sgc_exe)); foreach (string arg in args) { buffer.Append(' '); bool needs_quotes = Util.NeedsQuotes(arg); if (needs_quotes) buffer.Append("\""); buffer.Append(arg); if (needs_quotes) buffer.Append("\""); } string args_single_string = buffer.ToString(); return ExecuteProgram(exepath, args_single_string); } bool _showDetailedProgramArguments; public bool ShowDetailedProgramArguments { get { return _showDetailedProgramArguments; } set { _showDetailedProgramArguments = value; } } bool _showCommandLine; public bool ShowCommandLine { get { return _showCommandLine; } set { _showCommandLine = value; } } ProcessPriorityClass _processPriorityClass = ProcessPriorityClass.Normal; public ProcessPriorityClass ProcessPriorityClass { get { return _processPriorityClass; } set { if (!Enum.IsDefined(typeof(ProcessPriorityClass), value)) throw new InvalidEnumArgumentException("ProcessPriorityClass", (int)value, typeof(ProcessPriorityClass)); _processPriorityClass = value; } } protected bool ExecuteProgram(string exepath, string args) { if (String.IsNullOrEmpty(exepath)) { LogError("Internal error: No program path was specified for task."); return false; } try { if (_showCommandLine) LogMessage(MessageImportance.High, exepath + " " + args); // Create the process and execute it. this.messages = new Queue(); using (Process process = new Process()) using (AutoResetEvent pushEvent = new AutoResetEvent(false)) { this.process = process; this.notifyMainEvent = pushEvent; process.StartInfo.UseShellExecute = false; process.StartInfo.FileName = exepath; process.StartInfo.Arguments = args; process.StartInfo.CreateNoWindow = true; process.StartInfo.RedirectStandardInput = true; process.StartInfo.RedirectStandardOutput = true; process.StartInfo.RedirectStandardError = true; Closure stdout_pump = this.PumpStdout; Closure stderr_pump = this.PumpStderr; Closure exit_watcher = this.WaitForProcessExit; process.Start(); process.PriorityClass = _processPriorityClass; process.StandardInput.Close(); IAsyncResult stdout_result = stdout_pump.BeginInvoke(null, null); IAsyncResult stderr_result = stderr_pump.BeginInvoke(null, null); IAsyncResult exit_result = exit_watcher.BeginInvoke(null, null); IntPtr pushEventHandle = pushEvent.SafeWaitHandle.DangerousGetHandle(); // Dequeue lines from stdout/stderr in batches. string[] dequeued_lines = new string[0x20]; bool quit = false; while (!quit) { Kernel32.WaitResult waitResult; unsafe { waitResult = User32.MsgWaitForMultipleObjects(1, &pushEventHandle, false, User32.InfiniteTimeout, User32.QS_ALLEVENTS); } switch (waitResult) { case Kernel32.WaitResult.Object0: { // One of the worker threads has changed shared state. bool has_more_lines; do { int dequeued_line_count = 0; lock (SharedStateLock) { // Acknowledge signal from worker threads. Debug.Assert(notifyingMain, "Expected notifyingMain to be true"); for (;;) { if (messages.Count == 0) { has_more_lines = false; break; } if (dequeued_line_count == dequeued_lines.Length) { has_more_lines = true; break; } string line = messages.Dequeue(); dequeued_lines[dequeued_line_count] = line; dequeued_line_count++; } if (!has_more_lines && processExited && stdoutDone && stderrDone) { quit = true; } // If we are not going to run the lock-loop again (no more lines available), then allow // worker threads to signal pushEvent again. if (!has_more_lines) notifyingMain = false; } for (int i = 0; i < dequeued_line_count; i++) { string line = dequeued_lines[i]; dequeued_lines[i] = null; LogMessage(MessageImportance.High, line); } } while (has_more_lines); break; } case Kernel32.WaitResult.Object1: { // A USER32 message is ready. unsafe { MSG msg; if (User32.PeekMessageW(& msg, IntPtr.Zero, 0, 0, User32.PM_REMOVE)) { User32.TranslateMessage(&msg); User32.DispatchMessageW(&msg); } else { Debug.WriteLine("Received WM_QUIT, or PeekMessage failed"); break; } } break; } case Kernel32.WaitResult.Timeout: // Should never happen Debug.Fail("MsgWaitForMultipleObjects returned WAIT_TIMEOUT?!"); break; case Kernel32.WaitResult.Failed: break; default: Debug.Fail("Unrecognized return value from MsgWaitForMultipleObjects"); break; } } // Synchronize with the termination of the worker routines. // This is equivalent to Thread.Join(), but we used async delegates, not threads. // EndInvoke is guaranteed to wait for the async operation to complete. exit_watcher.EndInvoke(exit_result); stdout_pump.EndInvoke(stdout_result); stderr_pump.EndInvoke(stderr_result); process.StandardOutput.Close(); process.StandardInput.Close(); process.WaitForExit(); if (process.ExitCode == 0) { LogMessage("Process exited successfully."); return true; } else { LogError("The compiler terminated with an error code."); return false; } } } finally { this.process = null; this.notifyMainEvent = null; this.messages = null; } } private object SharedStateLock { get { return this; } } public Process process; public bool processExited; public bool stdoutDone; public bool stderrDone; public bool notifyingMain; public AutoResetEvent notifyMainEvent; public Queue messages; void WriteLine(string line) { lock (SharedStateLock) { messages.Enqueue(line); NotifyMainThreadLocked(); } } void PumpStream(StreamReader reader) { if (reader == null) return; for (;;) { string line; try { line = reader.ReadLine(); } catch (Exception ex) { WriteLine("exception from reader: " + ex.Message); break; } if (line == null) break; lock (SharedStateLock) { WriteLine(line); } } } public void PumpStdout() { PumpStream(process.StandardOutput); lock (SharedStateLock) { stdoutDone = true; NotifyMainThreadLocked(); } } public void PumpStderr() { PumpStream(process.StandardError); lock (SharedStateLock) { stderrDone = true; NotifyMainThreadLocked(); } } internal void NotifyMainThreadLocked() { if (notifyingMain) return; notifyingMain = true; notifyMainEvent.Set(); } public void WaitForProcessExit() { process.WaitForExit(); lock (SharedStateLock) { this.processExited = true; NotifyMainThreadLocked(); } } static readonly Regex diagnosticRegex = new Regex(@"(? [a-zA-Z0-9._-]+?):(?.*)$", RegexOptions.IgnorePatternWhitespace); void ParseAndLogProcessOutputLine(string line) { Match match = diagnosticRegex.Match(line); if (match.Success) { string msg = match.Groups["msg"].Value; string filename = match.Groups["filename"].Value; // BuildMessageEventArgs e = new BuildMessageEventArgs(line, null, null, MessageImportance.Normal); Console.WriteLine("msg ({0})", msg); Console.WriteLine("filename ({0})", filename); LogMessage(MessageImportance.High, line); } else { LogMessage(MessageImportance.High, line); } } } delegate void Closure(); class InvalidTaskParametersException : Exception { public InvalidTaskParametersException() : base("One or more parameters for this task are not valid.") { } public InvalidTaskParametersException(string msg) : base(msg) { } } }