398 lines
16 KiB
C#
398 lines
16 KiB
C#
|
// ----------------------------------------------------------------------------
|
||
|
//
|
||
|
// 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
|
||
|
{
|
||
|
///
|
||
|
//<summary>
|
||
|
// 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.
|
||
|
//</summary>
|
||
|
//
|
||
|
|
||
|
protected bool ExecuteProgram(string exepath, IEnumerable<String> 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<String>();
|
||
|
|
||
|
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<String> 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(@"(?<filename> [a-zA-Z0-9._-]+?):(?<msg>.*)$", 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)
|
||
|
{
|
||
|
}
|
||
|
|
||
|
}
|
||
|
}
|