diff --git a/src/Main/ICSharpCode.SharpDevelop.BuildWorker/BuildJob.cs b/src/Main/ICSharpCode.SharpDevelop.BuildWorker/BuildJob.cs new file mode 100644 index 0000000000..3002226b46 --- /dev/null +++ b/src/Main/ICSharpCode.SharpDevelop.BuildWorker/BuildJob.cs @@ -0,0 +1,90 @@ +// +// +// +// +// $Revision$ +// + +using System; +using System.Collections.Generic; +using System.Text; + +namespace ICSharpCode.SharpDevelop.BuildWorker +{ + /// + /// The settings used to start a build. + /// + [Serializable] + public class BuildJob + { + /// + /// The value of IntPtr.Size on the host. The build worker will report an error if its IntPtr.Size + /// doesn't match. This is a safety feature to prevent compiling half of a solution using 32-bit MSBuild + /// and the other half using 64-bit MSBuild. + /// + public int IntPtrSize { get; set; } + + + public string ProjectFileName { get; set; } + public string Target { get; set; } + + EventTypes eventMask = EventTypes.All; + + /// + /// Gets/Sets the mask that controls which events are reported back to the host. + /// + public EventTypes EventMask { + get { return eventMask; } + set { eventMask = value; } + } + + Dictionary properties = new Dictionary(); + + public Dictionary Properties { + get { return properties; } + } + + List additionalImports = new List(); + + public IList AdditionalImports { + get { return additionalImports; } + } + + HashSet interestingTaskNames = new HashSet(StringComparer.OrdinalIgnoreCase); + + public ICollection InterestingTaskNames { + get { return interestingTaskNames; } + } + + public override string ToString() + { + StringBuilder b = new StringBuilder(); + b.AppendLine("BuildJob:"); + b.AppendLine(" ProjectFileName = " + ProjectFileName); + b.AppendLine(" Target = " + Target); + b.AppendLine(" " + Properties.Count + " Properties:"); + foreach (KeyValuePair pair in Properties) { + b.AppendLine(" " + pair.Key + " = " + pair.Value); + } + b.AppendLine(" " + AdditionalImports.Count + " Additional Imports:"); + foreach (string import in AdditionalImports) { + b.AppendLine(" " + import); + } + b.AppendLine(" " + InterestingTaskNames.Count + " Interesting Task Names:"); + foreach (string name in InterestingTaskNames) { + b.AppendLine(" " + name); + } + return b.ToString(); + } + + [NonSerialized] + internal Action CancelCallback; + + public void Cancel() + { + if (CancelCallback != null) { + CancelCallback(); + } + } + } +} diff --git a/src/Main/ICSharpCode.SharpDevelop.BuildWorker/Configuration/AssemblyInfo.cs b/src/Main/ICSharpCode.SharpDevelop.BuildWorker/Configuration/AssemblyInfo.cs new file mode 100644 index 0000000000..3d3864f779 --- /dev/null +++ b/src/Main/ICSharpCode.SharpDevelop.BuildWorker/Configuration/AssemblyInfo.cs @@ -0,0 +1,17 @@ +// +// +// +// +// $Revision$ +// + +using System; +using System.Reflection; +using System.Runtime.CompilerServices; + +[assembly: AssemblyTitle("SharpDevelop Build Worker")] +[assembly: AssemblyDescription("Runs MSBuild")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: CLSCompliant(true)] diff --git a/src/Main/ICSharpCode.SharpDevelop.BuildWorker/EventSource.cs b/src/Main/ICSharpCode.SharpDevelop.BuildWorker/EventSource.cs new file mode 100644 index 0000000000..7818baf8e4 --- /dev/null +++ b/src/Main/ICSharpCode.SharpDevelop.BuildWorker/EventSource.cs @@ -0,0 +1,131 @@ +// +// +// +// +// $Revision$ +// + +using System; +using Microsoft.Build.Framework; + +namespace ICSharpCode.SharpDevelop.BuildWorker +{ + [Flags] + public enum EventTypes + { + None = 0, + Message = 0x0001, + Error = 0x0002, + Warning = 0x0004, + BuildStarted = 0x0008, + BuildFinished = 0x0010, + ProjectStarted = 0x0020, + ProjectFinished = 0x0040, + TargetStarted = 0x0080, + TargetFinished = 0x0100, + TaskStarted = 0x0200, + TaskFinished = 0x0400, + Custom = 0x0800, + Unknown = 0x1000, + All = 0x1fff + } + + class EventSource : IEventSource + { + public event BuildMessageEventHandler MessageRaised; + public event BuildErrorEventHandler ErrorRaised; + public event BuildWarningEventHandler WarningRaised; + public event BuildStartedEventHandler BuildStarted; + public event BuildFinishedEventHandler BuildFinished; + public event ProjectStartedEventHandler ProjectStarted; + public event ProjectFinishedEventHandler ProjectFinished; + public event TargetStartedEventHandler TargetStarted; + public event TargetFinishedEventHandler TargetFinished; + public event TaskStartedEventHandler TaskStarted; + public event TaskFinishedEventHandler TaskFinished; + public event CustomBuildEventHandler CustomEventRaised; + public event BuildStatusEventHandler StatusEventRaised; + + public event AnyEventHandler AnyEventRaised; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")] + public void RaiseEvent(BuildEventArgs e) + { + if (e is BuildStatusEventArgs) { + if (e is TaskStartedEventArgs) { + if (TaskStarted != null) + TaskStarted(this, (TaskStartedEventArgs)e); + } else if (e is TaskFinishedEventArgs) { + if (TaskFinished != null) + TaskFinished(this, (TaskFinishedEventArgs)e); + } else if (e is TargetStartedEventArgs) { + if (TargetStarted != null) + TargetStarted(this, (TargetStartedEventArgs)e); + } else if (e is TargetFinishedEventArgs) { + if (TargetFinished != null) + TargetFinished(this, (TargetFinishedEventArgs)e); + } else if (e is ProjectStartedEventArgs) { + if (ProjectStarted != null) + ProjectStarted(this, (ProjectStartedEventArgs)e); + } else if (e is ProjectFinishedEventArgs) { + if (ProjectFinished != null) + ProjectFinished(this, (ProjectFinishedEventArgs)e); + } else if (e is BuildStartedEventArgs) { + if (BuildStarted != null) + BuildStarted(this, (BuildStartedEventArgs)e); + } else if (e is BuildFinishedEventArgs) { + if (BuildFinished != null) + BuildFinished(this, (BuildFinishedEventArgs)e); + } + if (StatusEventRaised != null) + StatusEventRaised(this, (BuildStatusEventArgs)e); + } else if (e is BuildMessageEventArgs) { + if (MessageRaised != null) + MessageRaised(this, (BuildMessageEventArgs)e); + } else if (e is BuildWarningEventArgs) { + if (WarningRaised != null) + WarningRaised(this, (BuildWarningEventArgs)e); + } else if (e is BuildErrorEventArgs) { + if (ErrorRaised != null) + ErrorRaised(this, (BuildErrorEventArgs)e); + } else if (e is CustomBuildEventArgs) { + if (CustomEventRaised != null) + CustomEventRaised(this, (CustomBuildEventArgs)e); + } + + if (AnyEventRaised != null) + AnyEventRaised(this, e); + } + + public static EventTypes GetEventType(BuildEventArgs e) + { + if (e is TaskStartedEventArgs) { + return EventTypes.TaskStarted; + } else if (e is TaskFinishedEventArgs) { + return EventTypes.TaskFinished; + } else if (e is TargetStartedEventArgs) { + return EventTypes.TargetStarted; + } else if (e is TargetFinishedEventArgs) { + return EventTypes.TargetFinished; + } else if (e is BuildMessageEventArgs) { + return EventTypes.Message; + } else if (e is BuildWarningEventArgs) { + return EventTypes.Warning; + } else if (e is BuildErrorEventArgs) { + return EventTypes.Error; + } else if (e is ProjectStartedEventArgs) { + return EventTypes.ProjectStarted; + } else if (e is ProjectFinishedEventArgs) { + return EventTypes.ProjectFinished; + } else if (e is BuildStartedEventArgs) { + return EventTypes.BuildStarted; + } else if (e is BuildFinishedEventArgs) { + return EventTypes.BuildFinished; + } else if (e is CustomBuildEventArgs) { + return EventTypes.Custom; + } else { + return EventTypes.Unknown; + } + } + } +} diff --git a/src/Main/ICSharpCode.SharpDevelop.BuildWorker/ICSharpCode.SharpDevelop.BuildWorker.csproj b/src/Main/ICSharpCode.SharpDevelop.BuildWorker/ICSharpCode.SharpDevelop.BuildWorker.csproj new file mode 100644 index 0000000000..ba871edb97 --- /dev/null +++ b/src/Main/ICSharpCode.SharpDevelop.BuildWorker/ICSharpCode.SharpDevelop.BuildWorker.csproj @@ -0,0 +1,74 @@ + + + {C3CBC8E3-81D8-4C5B-9941-DCCD12D50B1F} + Debug + AnyCPU + Exe + ICSharpCode.SharpDevelop.BuildWorker + ICSharpCode.SharpDevelop.BuildWorker + v3.5 + False + False + 4 + false + ..\..\..\bin\ + True + ..\ICSharpCode.SharpDevelop.snk + False + File + False + -Microsoft.Globalization#CA1303 + + + true + Full + False + True + DEBUG;TRACE + + + False + None + True + False + TRACE + + + False + Auto + 4194304 + x86 + 4096 + + + + + + + + 3.5 + + + + + + + Configuration\GlobalAssemblyInfo.cs + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Main/ICSharpCode.SharpDevelop.BuildWorker/Interprocess/HostProcess.cs b/src/Main/ICSharpCode.SharpDevelop.BuildWorker/Interprocess/HostProcess.cs new file mode 100644 index 0000000000..30f9d45384 --- /dev/null +++ b/src/Main/ICSharpCode.SharpDevelop.BuildWorker/Interprocess/HostProcess.cs @@ -0,0 +1,112 @@ +// +// +// +// +// $Revision$ +// + +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using System.Threading; + +namespace ICSharpCode.SharpDevelop.BuildWorker.Interprocess +{ + /// + /// Used in the worker process to refer to the host. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable")] + public class HostProcess + { + readonly object workerObject; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1720:AvoidTypeNamesInParameters")] + public HostProcess(object workerObject) + { + if (workerObject == null) + throw new ArgumentNullException("workerObject"); + this.workerObject = workerObject; + } + + TcpClient client; + PacketReceiver receiver; + PacketSender sender; + + ManualResetEvent shutdownEvent; + bool connectionIsLost; + + internal const int SendKeepAliveInterval = 10000; + + public void WorkerProcessMain(string argument, string passwordBase64) + { + int port = int.Parse(argument, CultureInfo.InvariantCulture); + + client = new TcpClient(); + client.Connect(new IPEndPoint(IPAddress.Loopback, port)); + Stream stream = client.GetStream(); + receiver = new PacketReceiver(); + sender = new PacketSender(stream); + shutdownEvent = new ManualResetEvent(false); + receiver.ConnectionLost += OnConnectionLost; + receiver.PacketReceived += OnPacketReceived; + sender.WriteFailed += OnConnectionLost; + + // send password + sender.Send(Convert.FromBase64String(passwordBase64)); + + receiver.StartReceive(stream); + while (!shutdownEvent.WaitOne(SendKeepAliveInterval, false)) { + Program.Log("Sending keep-alive packet"); + sender.Send(new byte[0]); + } + + Program.Log("Closing client (end of WorkerProcessMain)"); + client.Close(); + shutdownEvent.Close(); + } + + public void Disconnect() + { + client.Close(); + } + + public void CallMethodOnHost(string methodName, params object[] args) + { + Program.Log("CallMethodOnHost: " + methodName); + sender.Send(WorkerProcess.SerializeObject(new WorkerProcess.MethodCall(methodName, args))); + } + + void OnConnectionLost(object sender, EventArgs e) + { + lock (this) { + if (connectionIsLost) + return; + Program.Log("OnConnectionLost"); + connectionIsLost = true; + shutdownEvent.Set(); + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + void OnPacketReceived(object sender, PacketReceivedEventArgs e) + { + Program.Log("OnPacketReceived"); + if (e.Packet.Length != 0) { + try { + WorkerProcess.MethodCall mc = (WorkerProcess.MethodCall)WorkerProcess.DeserializeObject(e.Packet); + mc.CallOn(workerObject); + } catch (TargetInvocationException ex) { + Program.Log(ex.ToString()); + CallMethodOnHost("ReportException", ex.InnerException.ToString()); + } catch (Exception ex) { + Program.Log(ex.ToString()); + CallMethodOnHost("ReportException", ex.ToString()); + } + } + } + } +} diff --git a/src/Main/ICSharpCode.SharpDevelop.BuildWorker/Interprocess/IHostObject.cs b/src/Main/ICSharpCode.SharpDevelop.BuildWorker/Interprocess/IHostObject.cs new file mode 100644 index 0000000000..df1e484c6c --- /dev/null +++ b/src/Main/ICSharpCode.SharpDevelop.BuildWorker/Interprocess/IHostObject.cs @@ -0,0 +1,16 @@ +// +// +// +// +// $Revision$ +// + +using System; + +namespace ICSharpCode.SharpDevelop.BuildWorker.Interprocess +{ + public interface IHostObject + { + void ReportException(string exceptionText); + } +} diff --git a/src/Main/ICSharpCode.SharpDevelop.BuildWorker/Interprocess/PacketReceiver.cs b/src/Main/ICSharpCode.SharpDevelop.BuildWorker/Interprocess/PacketReceiver.cs new file mode 100644 index 0000000000..ad9700d731 --- /dev/null +++ b/src/Main/ICSharpCode.SharpDevelop.BuildWorker/Interprocess/PacketReceiver.cs @@ -0,0 +1,148 @@ +// +// +// +// +// $Revision$ +// + +using System; +using System.Net; +using System.Net.Sockets; +using System.Diagnostics; +using System.IO; +using System.Runtime; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters; +using System.Runtime.Serialization.Formatters.Binary; + +namespace ICSharpCode.SharpDevelop.BuildWorker.Interprocess +{ + public sealed class PacketReceiver + { + Stream stream; + byte[] buffer = new byte[10000]; + int bufferReadOffset; + + public void StartReceive(Stream stream) + { + if (stream == null) + throw new ArgumentNullException("stream"); + if (this.stream != null) + throw new InvalidOperationException("StartReceive can be called only once."); + this.stream = stream; + try { + stream.BeginRead(buffer, 0, buffer.Length, OnReceive, null); + } catch (ObjectDisposedException) { + OnConnectionLost(); + } catch (IOException) { + OnConnectionLost(); + } + } + + int maxPacketSize = int.MaxValue - 20000; + + /// + /// Gets/Sets the maximum allowed packet size in bytes. + /// + public int MaxPacketSize { + get { return maxPacketSize; } + set { + if (value < 1) + throw new ArgumentOutOfRangeException("value", value, "MaxPacketSize must be >0"); + maxPacketSize = value; + } + } + + void OnReceive(IAsyncResult ar) + { + int bytes; + try { + bytes = stream.EndRead(ar); + } catch (ObjectDisposedException) { + OnConnectionLost(); + return; + } catch (IOException) { + OnConnectionLost(); + return; + } + if (bytes == 0) { + // 0 bytes read indicates the end of the stream + OnConnectionLost(); + return; + } + bufferReadOffset += bytes; + int packetStart = 0; + int packetSize = -1; + while (bufferReadOffset >= packetStart + 4) { + packetSize = BitConverter.ToInt32(buffer, packetStart); + if (packetSize < 4) + throw new ProtocolViolationException("packetSize must be > 4"); + if (packetSize - 4 > MaxPacketSize) + throw new ProtocolViolationException("packetSize must be smaller than MaxPacketSize"); + if (bufferReadOffset >= packetStart + packetSize) { + //Debug.WriteLine("receiving packet of size " + packetSize); + byte[] packet = new byte[packetSize - 4]; + Array.Copy(buffer, packetStart + 4, packet, 0, packet.Length); + OnPacketReceived(packet); + packetStart += packetSize; + } else { + break; + } + } + if (packetStart != 0) { + // copy half-received packet to the beginning of the buffer + int copyAmount = bufferReadOffset - packetStart; + for (int i = 0; i < copyAmount; i++) { + buffer[i] = buffer[i + packetStart]; + } + bufferReadOffset = copyAmount; + } + if (packetSize > buffer.Length) { + Debug.WriteLine("resizing receive buffer for packet of size " + packetSize); + Array.Resize(ref buffer, Math.Max(packetSize, buffer.Length * 2)); + } + if (bufferReadOffset >= buffer.Length) { + // should never happen - the buffer now is large enough to contain the packet, + // and we would have already processed the packet if received it completely + throw new InvalidOperationException("trying to read 0 bytes from socket"); + } + try { + stream.BeginRead(buffer, bufferReadOffset, buffer.Length - bufferReadOffset, OnReceive, null); + } catch (ObjectDisposedException) { + OnConnectionLost(); + } catch (IOException) { + OnConnectionLost(); + } + } + + void OnConnectionLost() + { + if (ConnectionLost != null) + ConnectionLost(this, EventArgs.Empty); + } + + void OnPacketReceived(byte[] packet) + { + if (PacketReceived != null) + PacketReceived(this, new PacketReceivedEventArgs(packet)); + } + + public event EventHandler ConnectionLost; + public event EventHandler PacketReceived; + } + + public class PacketReceivedEventArgs : EventArgs + { + byte[] packet; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] + public byte[] Packet { + get { return packet; } + } + + public PacketReceivedEventArgs(byte[] packet) + { + this.packet = packet; + } + } +} diff --git a/src/Main/ICSharpCode.SharpDevelop.BuildWorker/Interprocess/PacketSender.cs b/src/Main/ICSharpCode.SharpDevelop.BuildWorker/Interprocess/PacketSender.cs new file mode 100644 index 0000000000..61b5570bf8 --- /dev/null +++ b/src/Main/ICSharpCode.SharpDevelop.BuildWorker/Interprocess/PacketSender.cs @@ -0,0 +1,70 @@ +// +// +// +// +// $Revision$ +// + +using System; +using System.IO; +using System.Threading; + +namespace ICSharpCode.SharpDevelop.BuildWorker.Interprocess +{ + /// + /// Description of PacketSender. + /// + public sealed class PacketSender + { + Stream targetStream; + public PacketSender(Stream stream) + { + if (stream == null) + throw new ArgumentNullException("stream"); + targetStream = stream; + } + + public void Send(byte[] packet) + { + if (packet == null) + throw new ArgumentNullException("packet"); + Send(new MemoryStream(packet, false)); + } + + public void Send(Stream stream) + { + if (stream == null) + throw new ArgumentNullException("stream"); + byte[] buffer = new byte[4 + stream.Length]; + unchecked { + buffer[0] = (byte)buffer.Length; + buffer[1] = (byte)(buffer.Length >> 8); + buffer[2] = (byte)(buffer.Length >> 16); + buffer[3] = (byte)(buffer.Length >> 24); + } + int pos = 4; + int c; + do { + c = stream.Read(buffer, pos, buffer.Length - pos); + pos += c; + } while (c > 0); + try { + targetStream.Write(buffer, 0, buffer.Length); + } catch (IOException ex) { + ICSharpCode.SharpDevelop.BuildWorker.Program.Log(ex.ToString()); + OnWriteFailed(); + } catch (ObjectDisposedException ex) { + ICSharpCode.SharpDevelop.BuildWorker.Program.Log(ex.ToString()); + OnWriteFailed(); + } + } + + void OnWriteFailed() + { + if (WriteFailed != null) + WriteFailed(this, EventArgs.Empty); + } + + public event EventHandler WriteFailed; + } +} diff --git a/src/Main/ICSharpCode.SharpDevelop.BuildWorker/Interprocess/WorkerProcess.cs b/src/Main/ICSharpCode.SharpDevelop.BuildWorker/Interprocess/WorkerProcess.cs new file mode 100644 index 0000000000..8d10b78fcf --- /dev/null +++ b/src/Main/ICSharpCode.SharpDevelop.BuildWorker/Interprocess/WorkerProcess.cs @@ -0,0 +1,270 @@ +// +// +// +// +// $Revision$ +// + +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters; +using System.Runtime.Serialization.Formatters.Binary; +using System.Security.Cryptography; +using System.Threading; + +namespace ICSharpCode.SharpDevelop.BuildWorker.Interprocess +{ + /// + /// Manages a worker process that communicates with the host using a local TCP connection. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable")] + public sealed class WorkerProcess + { + readonly object hostObject; + + public WorkerProcess(IHostObject hostObject) + { + if (hostObject == null) + throw new ArgumentNullException("hostObject"); + this.hostObject = hostObject; + } + + TcpListener listener; + Process process; + + TcpClient client; + PacketSender sender; + PacketReceiver receiver; + + // We use TCP, so we'll have to deal with "attackers" trying to hijack our connection. + // There are two lines of defense: + // 1. the only local connections are accepted + // 2. the client must authenticate with a random password + // We're not protecting against DOS - we're only vulnerable to local attackers during a short + // time frame. The password is only to defend against deserializing arbitrary objects from + // unauthenticated clients. + // We probably should use Named Pipes instead of TCP. + byte[] password; + bool clientAuthenticated; + + public void Start(ProcessStartInfo info) + { + if (info == null) + throw new ArgumentNullException("info"); + + password = new byte[16]; + new RNGCryptoServiceProvider().GetBytes(password); + + listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + string argument = ((IPEndPoint)listener.LocalEndpoint).Port.ToString(CultureInfo.InvariantCulture); + + string oldArguments = info.Arguments; + info.Arguments += " " + argument + " " + Convert.ToBase64String(password); + process = Process.Start(info); + // "manual process start" - useful for profiling the build worker + //System.Windows.Forms.MessageBox.Show(info.Arguments); + //process = Process.GetProcessesByName("ICSharpCode.SharpDevelop.BuildWorker")[0]; + + info.Arguments = oldArguments; + + SetTimeout(); + listener.BeginAcceptTcpClient(OnAcceptTcpClient, null); + } + + Timer currentTimer; + + void SetTimeout() + { + lock (this) { + ClearTimeout(); + currentTimer = new Timer(OnTimeout, null, HostProcess.SendKeepAliveInterval * 5 / 2, -1); + } + } + + void ClearTimeout() + { + lock (this) { + if (currentTimer != null) { + currentTimer.Dispose(); + currentTimer = null; + } + } + } + + void OnTimeout(object state) + { + Program.Log("OnTimeout"); + Kill(); + } + + void OnAcceptTcpClient(IAsyncResult ar) + { + clientAuthenticated = false; + SetTimeout(); + try { + client = listener.EndAcceptTcpClient(ar); + } catch (SocketException) { + // error connecting + } + listener.Stop(); + listener = null; + + if (client == null) { + OnConnectionLost(null, null); + } else { + Stream stream = client.GetStream(); + receiver = new PacketReceiver(); + sender = new PacketSender(stream); + receiver.ConnectionLost += OnConnectionLost; + receiver.PacketReceived += OnPacketReceived; + + receiver.StartReceive(stream); + OnReady(); + } + } + + public void Shutdown() + { + Program.Log("Shutdown"); + OnWorkerLost(); + if (client != null) { + client.Close(); + } + } + + void OnConnectionLost(object sender, EventArgs e) + { + Program.Log("OnConnectionLost"); + SetTimeout(); + OnWorkerLost(); + } + + void OnPacketReceived(object sender, PacketReceivedEventArgs e) + { + SetTimeout(); + if (e.Packet.Length != 0) { + if (clientAuthenticated) { + MethodCall mc = (MethodCall)DeserializeObject(e.Packet); + mc.CallOn(hostObject); + } else { + if (ArrayEquals(e.Packet, password)) { + clientAuthenticated = true; + } else { + Kill(); + throw new InvalidOperationException("Worker process authentication failed."); + } + } + } + } + + static bool ArrayEquals(byte[] a, byte[] b) + { + if (a.Length != b.Length) + return false; + for (int i = 0; i < a.Length; i++) { + if (a[i] != b[i]) + return false; + } + return true; + } + + public void Kill() + { + Program.Log("Kill"); + ClearTimeout(); + OnWorkerLost(); + if (client != null) { + client.Close(); + client = null; + } + if (process != null) { + try { + if (!process.HasExited) { + process.Kill(); + } + } catch (InvalidOperationException) { + // may occur when the worker process crashes + } + process = null; + } + } + + int workerIsLost; + + void OnReady() + { + if (workerIsLost == 1) + return; + Program.Log("OnReady"); + if (Ready != null) + Ready(this, EventArgs.Empty); + } + + void OnWorkerLost() + { + if (Interlocked.Exchange(ref workerIsLost, 1) == 1) + return; + Program.Log("OnWorkerLost"); + if (WorkerLost != null) + WorkerLost(this, EventArgs.Empty); + } + + /// + /// Occurs when the worker process is ready to execute a job. + /// + public event EventHandler Ready; + + /// + /// Occurs when the connection to the worker process broke. + /// + public event EventHandler WorkerLost; + + public void CallMethodOnWorker(string methodName, params object[] args) + { + Debug.WriteLine("CallMethodOnWorker: " + methodName); + sender.Send(SerializeObject(new MethodCall(methodName, args))); + } + + internal static MemoryStream SerializeObject(object obj) + { + MemoryStream ms = new MemoryStream(); + BinaryFormatter bf = new BinaryFormatter(); + bf.Serialize(ms, obj); + ms.Position = 0; + return ms; + } + + internal static object DeserializeObject(byte[] packet) + { + BinaryFormatter bf = new BinaryFormatter(); + return bf.Deserialize(new MemoryStream(packet, false)); + } + + [Serializable] + internal class MethodCall + { + public readonly string MethodName; + public readonly object[] Arguments; + + public MethodCall(string methodName, object[] arguments) + { + this.MethodName = methodName; + this.Arguments = arguments; + } + + public void CallOn(object target) + { + target.GetType().InvokeMember(MethodName, + BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Instance, + null, target, Arguments, CultureInfo.InvariantCulture); + } + } + } +} diff --git a/src/Main/ICSharpCode.SharpDevelop.BuildWorker/Program.cs b/src/Main/ICSharpCode.SharpDevelop.BuildWorker/Program.cs new file mode 100644 index 0000000000..4bc9d0ebc4 --- /dev/null +++ b/src/Main/ICSharpCode.SharpDevelop.BuildWorker/Program.cs @@ -0,0 +1,479 @@ +// +// +// +// +// $Revision$ +// + +// activate this define to see the build worker window +//#define WORKERDEBUG + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.Serialization; +using System.Threading; + +using ICSharpCode.SharpDevelop.BuildWorker.Interprocess; +using Microsoft.Build.BuildEngine; +using Microsoft.Build.Framework; + +namespace ICSharpCode.SharpDevelop.BuildWorker +{ + class Program + { + static HostProcess host; + BuildJob currentJob; + bool requestCancellation; + + [STAThread] + internal static void Main(string[] args) + { + AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(AppDomain_CurrentDomain_UnhandledException); + + if (args.Length == 3 && args[0] == "worker") { + try { + host = new HostProcess(new Program()); + host.WorkerProcessMain(args[1], args[2]); + } catch (Exception ex) { + ShowMessageBox(ex.ToString()); + } + } else { + Console.WriteLine("ICSharpCode.SharpDevelop.BuildWorker.exe is used to compile " + + "MSBuild projects inside SharpDevelop."); + Console.WriteLine("If you want to compile projects on the command line, use " + + "MSBuild.exe (part of the .NET Framework)"); + } + } + + static void AppDomain_CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) + { + ShowMessageBox(e.ExceptionObject.ToString()); + } + + #if RELEASE && WORKERDEBUG + #error WORKERDEBUG must not be defined if RELEASE is defined + #endif + + internal static ProcessStartInfo CreateStartInfo() + { + ProcessStartInfo info = new ProcessStartInfo(typeof(Program).Assembly.Location); + info.WorkingDirectory = Path.GetDirectoryName(info.FileName); + info.Arguments = "worker"; + info.UseShellExecute = false; + #if RELEASE || !WORKERDEBUG + info.CreateNoWindow = true; + #endif + return info; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1300:SpecifyMessageBoxOptions")] + internal static void ShowMessageBox(string text) + { + System.Windows.Forms.MessageBox.Show(text, "SharpDevelop Build Worker Process"); + } + + [Conditional("DEBUG")] + internal static void Log(string text) + { + Debug.WriteLine(text); + #if WORKERDEBUG + DateTime now = DateTime.Now; + Console.WriteLine(now.ToString() + "," + now.Millisecond.ToString("d3") + " " + text); + #endif + } + + // Called with CallMethodOnWorker + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void StartBuild(BuildJob job) + { + if (job == null) + throw new ArgumentNullException("job"); + lock (this) { + if (currentJob != null) + throw new InvalidOperationException("Already running a job"); + currentJob = job; + requestCancellation = false; + } + #if WORKERDEBUG + Console.Title = "BuildWorker - " + Path.GetFileName(job.ProjectFileName); + #endif + Program.Log("Got job:"); + Program.Log(job.ToString()); + Program.Log("Start build thread"); + Thread thread = new Thread(RunThread); + thread.Name = "Build thread"; + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + } + + // Called with CallMethodOnWorker + //[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + // TODO: make use of CancelBuild + public void CancelBuild() + { + lock (this) { + requestCancellation = true; + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + void RunThread() + { + Program.Log("In build thread"); + bool success = false; + try { + success = DoBuild(); + } catch (Exception ex) { + host.CallMethodOnHost("ReportException", ex.ToString()); + } finally { + Program.Log("BuildDone"); + + #if WORKERDEBUG + Console.Title = "BuildWorker - no job"; + DisplayEventCounts(); + #endif + + lock (this) { + currentJob = null; + } + // in the moment we call BuildDone, we can get the next job + host.CallMethodOnHost("BuildDone", success); + } + } + + Engine CreateEngine() + { + Engine engine = new Engine(ToolsetDefinitionLocations.Registry + | ToolsetDefinitionLocations.ConfigurationFile); + + engine.RegisterLogger(new ForwardingLogger(this)); + //engine.RegisterLogger(new ConsoleLogger(LoggerVerbosity.Diagnostic)); + + return engine; + } + + EventSource hostEventSource; + + internal void BuildInProcess(BuildSettings settings, BuildJob job) + { + lock (this) { + if (currentJob != null) + throw new InvalidOperationException("Already running a job"); + currentJob = job; + requestCancellation = false; + hostEventSource = new EventSource(); + } + job.CancelCallback = delegate { + lock (this) { + if (currentJob == job) { + requestCancellation = true; + } + } + }; + bool success = false; + try { + foreach (ILogger logger in settings.Logger) { + logger.Initialize(hostEventSource); + } + success = DoBuild(); + foreach (ILogger logger in settings.Logger) { + logger.Shutdown(); + } + } finally { + lock (this) { + currentJob = null; + } + if (settings.BuildDoneCallback != null) + settings.BuildDoneCallback(success); + } + } + + Engine engine; + Dictionary lastJobProperties; + + // Fix for SD2-1533 - Project configurations get confused + // Whenever the global properties change, we have to create a new Engine + // to ensure MSBuild doesn't cache old paths + bool GlobalPropertiesChanged(Dictionary newJobProperties) + { + Debug.Assert(newJobProperties != null); + if (lastJobProperties == null || lastJobProperties.Count != newJobProperties.Count) { + Log("Recreating engine: Number of build properties changed"); + return true; + } + foreach (KeyValuePair pair in lastJobProperties) { + string val; + if (!newJobProperties.TryGetValue(pair.Key, out val)) { + Log("Recreating engine: Build property removed: " + pair.Key); + return true; + } + if (val != pair.Value) { + Log("Recreating engine: Build property changed: " + pair.Key); + return true; + } + } + return false; + } + + bool DoBuild() + { + if (currentJob.IntPtrSize != IntPtr.Size) + throw new ApplicationException("Incompatible IntPtr.Size between host and worker"); + + if (engine == null || GlobalPropertiesChanged(currentJob.Properties)) { + engine = CreateEngine(); + lastJobProperties = currentJob.Properties; + engine.GlobalProperties.Clear(); + foreach (KeyValuePair pair in currentJob.Properties) { + engine.GlobalProperties.SetProperty(pair.Key, pair.Value); + } + } + + Log("Loading " + currentJob.ProjectFileName); + Project project = LoadProject(engine, currentJob.ProjectFileName); + if (project == null) + return false; + + if (string.IsNullOrEmpty(currentJob.Target)) { + Log("Building default target in " + currentJob.ProjectFileName); + return engine.BuildProject(project); + } else { + Log("Building target '" + currentJob.Target + "' in " + currentJob.ProjectFileName); + return engine.BuildProject(project, currentJob.Target.Split(';')); + } + } + + Project LoadProject(Engine engine, string fileName) + { + Project project = engine.CreateNewProject(); + try { + project.Load(fileName); + + /* No longer necessary as we stopped using BuildingInsideVisualStudio + // When we set BuildingInsideVisualStudio, MSBuild tries to build all projects + // every time because in Visual Studio, the host compiler does the change detection + // We override the property '_ComputeNonExistentFileProperty' which is responsible + // for recompiling each time - our _ComputeNonExistentFileProperty does nothing, + // which re-enables the MSBuild's usual change detection + project.Targets.AddNewTarget("_ComputeNonExistentFileProperty"); + */ + + foreach (string additionalImport in currentJob.AdditionalImports) { + project.AddNewImport(additionalImport, null); + } + + return project; + } catch (ArgumentException ex) { + ReportError(ex.Message); + } catch (InvalidProjectFileException ex) { + ReportError(new BuildErrorEventArgs(ex.ErrorSubcategory, ex.ErrorCode, ex.ProjectFile, + ex.LineNumber, ex.ColumnNumber, ex.EndLineNumber, ex.EndColumnNumber, + ex.BaseMessage, ex.HelpKeyword, ex.Source)); + } + return null; + } + + void ReportError(string message) + { + ReportError(new BuildErrorEventArgs(null, null, null, -1, -1, -1, -1, + message, null, "SharpDevelopBuildWorker")); + } + + void ReportError(BuildErrorEventArgs e) + { + HostReportEvent(e); + } + + void HostReportEvent(BuildEventArgs e) + { + if (host != null) { + host.CallMethodOnHost("ReportEvent", e); + } else { + // enable error reporting for in-process builds + EventSource eventSource = hostEventSource; + if (eventSource != null) { + eventSource.RaiseEvent(e); + } + } + } + + sealed class ForwardingLogger : ILogger + { + Program program; + + public ForwardingLogger(Program program) + { + this.program = program; + } + + IEventSource eventSource; + + public LoggerVerbosity Verbosity { get; set; } + public string Parameters { get; set; } + + public void Initialize(IEventSource eventSource) + { + this.eventSource = eventSource; + EventTypes eventMask = program.currentJob.EventMask; + if ((eventMask & EventTypes.Message) != 0) + eventSource.MessageRaised += OnEvent; + if ((eventMask & EventTypes.Error) != 0) + eventSource.ErrorRaised += OnEvent; + if ((eventMask & EventTypes.Warning) != 0) + eventSource.WarningRaised += OnEvent; + if ((eventMask & EventTypes.BuildStarted) != 0) + eventSource.BuildStarted += OnEvent; + if ((eventMask & EventTypes.BuildFinished) != 0) + eventSource.BuildFinished += OnEvent; + if ((eventMask & EventTypes.ProjectStarted) != 0) + eventSource.ProjectStarted += OnEvent; + if ((eventMask & EventTypes.ProjectFinished) != 0) + eventSource.ProjectFinished += OnEvent; + if ((eventMask & EventTypes.TargetStarted) != 0) + eventSource.TargetStarted += OnEvent; + if ((eventMask & EventTypes.TargetFinished) != 0) + eventSource.TargetFinished += OnEvent; + if ((eventMask & EventTypes.TaskStarted) != 0) + eventSource.TaskStarted += OnEvent; + else + eventSource.TaskStarted += OnTaskStarted; + if ((eventMask & EventTypes.TaskFinished) != 0) + eventSource.TaskFinished += OnEvent; + else + eventSource.TaskFinished += OnTaskFinished; + if ((eventMask & EventTypes.Custom) != 0) + eventSource.CustomEventRaised += OnEvent; + if ((eventMask & EventTypes.Unknown) != 0) + eventSource.AnyEventRaised += OnUnknownEventRaised; + if (eventMask != EventTypes.All) + eventSource.AnyEventRaised += OnAnyEvent; + + #if WORKERDEBUG + eventSource.AnyEventRaised += CountEvent; + #endif + } + + public void Shutdown() + { + EventTypes eventMask = program.currentJob.EventMask; + if ((eventMask & EventTypes.Message) != 0) + eventSource.MessageRaised -= OnEvent; + if ((eventMask & EventTypes.Error) != 0) + eventSource.ErrorRaised -= OnEvent; + if ((eventMask & EventTypes.Warning) != 0) + eventSource.WarningRaised -= OnEvent; + if ((eventMask & EventTypes.BuildStarted) != 0) + eventSource.BuildStarted -= OnEvent; + if ((eventMask & EventTypes.BuildFinished) != 0) + eventSource.BuildFinished -= OnEvent; + if ((eventMask & EventTypes.ProjectStarted) != 0) + eventSource.ProjectStarted -= OnEvent; + if ((eventMask & EventTypes.ProjectFinished) != 0) + eventSource.ProjectFinished -= OnEvent; + if ((eventMask & EventTypes.TargetStarted) != 0) + eventSource.TargetStarted -= OnEvent; + if ((eventMask & EventTypes.TargetFinished) != 0) + eventSource.TargetFinished -= OnEvent; + if ((eventMask & EventTypes.TaskStarted) != 0) + eventSource.TaskStarted -= OnEvent; + else + eventSource.TaskStarted -= OnTaskStarted; + if ((eventMask & EventTypes.TaskFinished) != 0) + eventSource.TaskFinished -= OnEvent; + else + eventSource.TaskFinished -= OnTaskFinished; + if ((eventMask & EventTypes.Custom) != 0) + eventSource.CustomEventRaised -= OnEvent; + if ((eventMask & EventTypes.Unknown) != 0) + eventSource.AnyEventRaised -= OnUnknownEventRaised; + if (eventMask != EventTypes.All) + eventSource.AnyEventRaised -= OnAnyEvent; + + #if WORKERDEBUG + eventSource.AnyEventRaised -= CountEvent; + #endif + } + + // registered for AnyEventRaised to support build cancellation. + // is not registered if all events should be forwarded, in that case, OnEvent + // already handles build cancellation + void OnAnyEvent(object sender, BuildEventArgs e) + { + if (program.requestCancellation) + throw new BuildCancelException(); + } + + // used for all events that should be forwarded + void OnEvent(object sender, BuildEventArgs e) + { + if (program.requestCancellation) + throw new BuildCancelException(); + program.HostReportEvent(e); + } + + // registered for AnyEventRaised to forward unknown events + void OnUnknownEventRaised(object sender, BuildEventArgs e) + { + if (EventSource.GetEventType(e) == EventTypes.Unknown) + OnEvent(sender, e); + } + + // registered when only specific tasks should be forwarded + void OnTaskStarted(object sender, TaskStartedEventArgs e) + { + if (program.currentJob.InterestingTaskNames.Contains(e.TaskName)) + OnEvent(sender, e); + } + + // registered when only specific tasks should be forwarded + void OnTaskFinished(object sender, TaskFinishedEventArgs e) + { + if (program.currentJob.InterestingTaskNames.Contains(e.TaskName)) + OnEvent(sender, e); + } + + + #if WORKERDEBUG + void CountEvent(object sender, BuildEventArgs e) + { + Program.CountEvent(EventSource.GetEventType(e)); + } + #endif + } + + #if WORKERDEBUG + static Dictionary eventCounts = new Dictionary(); + + static void CountEvent(EventTypes e) + { + if (eventCounts.ContainsKey(e)) + eventCounts[e] += 1; + else + eventCounts[e] = 1; + } + + static void DisplayEventCounts() + { + foreach (var pair in eventCounts) { + Console.WriteLine(" " + pair.Key.ToString() + ": " + pair.Value.ToString()); + } + } + #endif + + [Serializable] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1064:ExceptionsShouldBePublic")] + sealed class BuildCancelException : Exception + { + public BuildCancelException() + { + } + + BuildCancelException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } + } +} diff --git a/src/Main/ICSharpCode.SharpDevelop.BuildWorker/WorkerManager.cs b/src/Main/ICSharpCode.SharpDevelop.BuildWorker/WorkerManager.cs new file mode 100644 index 0000000000..b28bd949f7 --- /dev/null +++ b/src/Main/ICSharpCode.SharpDevelop.BuildWorker/WorkerManager.cs @@ -0,0 +1,274 @@ +// +// +// +// +// $Revision$ +// + +using Microsoft.Build.Framework; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Diagnostics; +using ICSharpCode.SharpDevelop.BuildWorker.Interprocess; + +namespace ICSharpCode.SharpDevelop.BuildWorker +{ + /// + /// Manages the list of running child worker processes. + /// + public static class WorkerManager + { + /// + /// Delegate executed on the host when an exception occurs. + /// + public static Action ShowError { get; set; } + + const int MaxWorkerProcessCount = 16; + + static readonly object lockObject = new object(); + static Queue outstandingBuildRuns = new Queue(); + static int workerProcessCount; + static List freeWorkerProcesses = new List(); + + static WorkerProcessHost DequeueFreeWorkerProcess() + { + WorkerProcessHost h = freeWorkerProcesses[freeWorkerProcesses.Count - 1]; + freeWorkerProcesses.RemoveAt(freeWorkerProcesses.Count - 1); + return h; + } + + public static void StartBuild(BuildJob job, BuildSettings settings) + { + if (job == null) + throw new ArgumentNullException("job"); + if (settings == null) + throw new ArgumentNullException("settings"); + + BuildRun buildRun = new BuildRun(job, settings); + lock (lockObject) { + if (freeWorkerProcesses.Count > 0) { + DequeueFreeWorkerProcess().StartBuild(buildRun); + } else { + outstandingBuildRuns.Enqueue(buildRun); + if (workerProcessCount < MaxWorkerProcessCount) { + workerProcessCount++; + (new WorkerProcessHost()).Start(); + } + } + } + } + + static Program inProcessBuildWorker; + + public static void RunBuildInProcess(BuildJob job, BuildSettings settings) + { + if (job == null) + throw new ArgumentNullException("job"); + if (settings == null) + throw new ArgumentNullException("settings"); + lock (lockObject) { + if (inProcessBuildWorker == null) + inProcessBuildWorker = new Program(); + } + inProcessBuildWorker.BuildInProcess(settings, job); + } + + static readonly object timerLock = new object(); + static Timer lastBuildDoneTimer; + + static void SetLastBuildDoneTimer() + { + lock (timerLock) { + if (lastBuildDoneTimer != null) { + lastBuildDoneTimer.Dispose(); + lastBuildDoneTimer = null; + } + lastBuildDoneTimer = new Timer(LastBuildDoneTimerCallback, null, 60000, 20000); + } + } + + static void ClearLastBuildDoneTimer() + { + lock (timerLock) { + if (lastBuildDoneTimer != null) { + lastBuildDoneTimer.Dispose(); + lastBuildDoneTimer = null; + } + } + } + + static void LastBuildDoneTimerCallback(object state) + { + lock (lockObject) { + if (freeWorkerProcesses.Count > 0) { + Debug.WriteLine("WorkerManager: shutting down free worker"); + DequeueFreeWorkerProcess().Shutdown(); + } else { + ClearLastBuildDoneTimer(); + } + } + } + + sealed class BuildRun + { + internal BuildJob job; + internal BuildSettings settings; + EventSource eventSource = new EventSource(); + + public BuildRun(BuildJob job, BuildSettings settings) + { + this.job = job; + this.settings = settings; + foreach (ILogger logger in settings.Logger) { + logger.Initialize(eventSource); + } + } + + public void RaiseError(string message) + { + Debug.WriteLine(message); + RaiseEvent(new BuildErrorEventArgs(null, null, null, -1, -1, -1, -1, message, null, "SharpDevelopBuildWorkerManager")); + } + + public void RaiseEvent(BuildEventArgs e) + { + eventSource.RaiseEvent(e); + } + + public void Done(bool success) + { + SetLastBuildDoneTimer(); + try { + foreach (ILogger logger in settings.Logger) { + logger.Shutdown(); + } + } finally { + if (settings.BuildDoneCallback != null) + settings.BuildDoneCallback(success); + } + } + } + + sealed class WorkerProcessHost : IHostObject + { + WorkerProcess process; + + internal void Start() + { + process = new WorkerProcess(this); + process.Ready += OnReady; + process.WorkerLost += OnWorkerLost; + + process.Start(Program.CreateStartInfo()); + } + + BuildRun currentBuildRun; + + // runs in lock(lockObject) + internal void StartBuild(BuildRun nextBuildRun) + { + Debug.Assert(currentBuildRun == null); + currentBuildRun = nextBuildRun; + process.CallMethodOnWorker("StartBuild", currentBuildRun.job); + } + + void OnReady(object sender, EventArgs e) + { + BuildRun nextBuildRun = null; + lock (lockObject) { + if (outstandingBuildRuns.Count > 0) + nextBuildRun = outstandingBuildRuns.Dequeue(); + else + freeWorkerProcesses.Add(this); + } + if (nextBuildRun != null) { + StartBuild(nextBuildRun); + } + } + + void OnWorkerLost(object sender, EventArgs e) + { + BuildRun buildRun; + lock (lockObject) { + workerProcessCount--; + freeWorkerProcesses.Remove(this); + if (workerProcessCount == 0 && currentBuildRun == null) { + // error starting worker => we must + // cancel all outstanding build runs to prevent them from waiting + // for a worker becoming ready when all workers are dead + while (workerProcessCount == 0 && outstandingBuildRuns.Count > 0) { + BuildRun r = outstandingBuildRuns.Dequeue(); + Monitor.Exit(lockObject); + r.RaiseError("Error starting worker process."); + r.Done(false); + Monitor.Enter(lockObject); + } + } + buildRun = Interlocked.Exchange(ref currentBuildRun, null); + } + if (buildRun != null) { + buildRun.RaiseError("Worker process lost during build"); + buildRun.Done(false); + } + } + + internal void Shutdown() + { + process.Shutdown(); + } + + // Called with CallMethodOnHost + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void ReportEvent(BuildEventArgs e) + { + BuildRun buildRun = currentBuildRun; + if (buildRun != null) { + buildRun.RaiseEvent(e); + } + } + + // Called with CallMethodOnHost + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void BuildDone(bool success) + { + BuildRun buildRun; + lock (lockObject) { + buildRun = Interlocked.Exchange(ref currentBuildRun, null); + } + if (buildRun != null) { + // OnReady must be called before buildRun.Done - the callback + // might trigger another build, and if this worker process + // isn't marked as ready, a new process will be created even + // though this one could do the work. + OnReady(null, null); + buildRun.Done(success); + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + public void ReportException(string exceptionText) + { + // shutdown worker if it produced an exception + try { + process.Shutdown(); + } catch {} + + if (ShowError != null) + ShowError(new Exception(exceptionText)); + else + Program.ShowMessageBox(exceptionText); + } + } + } + + public sealed class BuildSettings + { + List logger = new List(); + public Action BuildDoneCallback { get; set; } + + public ICollection Logger { + get { return logger; } + } + } +} diff --git a/src/Main/ICSharpCode.SharpDevelop.BuildWorker/app.config b/src/Main/ICSharpCode.SharpDevelop.BuildWorker/app.config new file mode 100644 index 0000000000..aa701ee4ae --- /dev/null +++ b/src/Main/ICSharpCode.SharpDevelop.BuildWorker/app.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + +