-
-
Save jnm2/a8a39b67a584ad555360102407049ae2 to your computer and use it in GitHub Desktop.
| using System.Collections.Concurrent; | |
| using System.Reflection; | |
| using System.Threading; | |
| using System.Xml.Linq; | |
| public void RunSimultaneously(params Action[] actions) | |
| { | |
| if (actions.Length == 0) return; | |
| var bufferAccessAsyncLocal = default(AsyncLocal<TaskOutputBuffer>); | |
| using (InterceptProcessRunner(Context, original => | |
| { | |
| var alreadyInstalledBufferedProcessRunner = original as BufferedOutputProcessRunner; | |
| if (alreadyInstalledBufferedProcessRunner != null) | |
| { | |
| bufferAccessAsyncLocal = alreadyInstalledBufferedProcessRunner.BufferAccess; | |
| return original; | |
| } | |
| else | |
| { | |
| bufferAccessAsyncLocal = new AsyncLocal<TaskOutputBuffer>(); | |
| return new BufferedOutputProcessRunner(original, bufferAccessAsyncLocal); | |
| } | |
| })) | |
| { | |
| var originalConsole = default(IConsole); | |
| using (InterceptConsole(Context, original => | |
| { | |
| originalConsole = original; | |
| var alreadyInstalledBufferedConsole = original as BufferedOutputConsole; | |
| return alreadyInstalledBufferedConsole != null && alreadyInstalledBufferedConsole.BufferAccess == bufferAccessAsyncLocal ? original : | |
| new BufferedOutputConsole(originalConsole, bufferAccessAsyncLocal); | |
| })) | |
| { | |
| var outputBuffers = new BlockingCollection<TaskOutputBuffer>(); | |
| var actionOrderLock = new object(); | |
| var nextActionIndex = 0; | |
| var tasks = new System.Threading.Tasks.Task[actions.Length]; | |
| for (var i = 0; i < tasks.Length; i++) | |
| tasks[i] = System.Threading.Tasks.Task.Run(() => | |
| { | |
| using (var outputBuffer = new TaskOutputBuffer()) | |
| { | |
| // Ensure that outputBuffers.Add is called in the same order as actions, even if tasks start out of order | |
| Action action; | |
| lock (actionOrderLock) | |
| { | |
| action = actions[nextActionIndex]; | |
| nextActionIndex++; | |
| outputBuffers.Add(outputBuffer); | |
| } | |
| bufferAccessAsyncLocal.Value = outputBuffer; | |
| try | |
| { | |
| action.Invoke(); | |
| } | |
| catch (Exception ex) | |
| { | |
| Error(ex.Message); | |
| throw; | |
| } | |
| } | |
| }); | |
| for (var i = 0; i < actions.Length; i++) | |
| outputBuffers.Take().WriteOutputUntilCompleted(originalConsole); | |
| System.Threading.Tasks.Task.WaitAll(tasks); // throw exceptions, if any | |
| } | |
| } | |
| } | |
| private sealed class TaskOutputBuffer : IDisposable | |
| { | |
| private readonly BlockingCollection<Action<IConsole>> outputActions = new BlockingCollection<Action<IConsole>>(); | |
| public void AppendAction(Action<IConsole> outputAction) | |
| { | |
| outputActions.Add(outputAction); | |
| } | |
| public void Dispose() | |
| { | |
| outputActions.CompleteAdding(); | |
| } | |
| public void WriteOutputUntilCompleted(IConsole console) | |
| { | |
| Action<IConsole> action; | |
| while (outputActions.TryTake(out action, Timeout.Infinite)) | |
| action.Invoke(console); | |
| } | |
| } | |
| private static Tuple<FieldInfo, T> FindSingleFieldWithValue<T>(object instance) | |
| { | |
| var fields = instance.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance); | |
| var found = (Tuple<FieldInfo, T>)null; | |
| foreach (var field in fields) | |
| { | |
| var fieldValue = field.GetValue(instance); | |
| if (!(fieldValue is T)) continue; | |
| if (found != null) return null; | |
| found = Tuple.Create(field, (T)fieldValue); | |
| } | |
| return found; | |
| } | |
| private static IDisposable InterceptProcessRunner(ICakeContext context, Func<IProcessRunner, IProcessRunner> replacementProvider) | |
| { | |
| var processRunnerField = FindSingleFieldWithValue<IProcessRunner>(context); | |
| if (processRunnerField == null) | |
| throw new InvalidOperationException(context + " does not have exactly one field containing an IProcessRunner instance."); | |
| var fieldInfo = processRunnerField.Item1; | |
| var originalValue = processRunnerField.Item2; | |
| var replacement = replacementProvider.Invoke(originalValue); | |
| if (replacement == originalValue) return null; | |
| fieldInfo.SetValue(context, replacement); | |
| return On.Dispose(() => fieldInfo.SetValue(context, originalValue)); | |
| } | |
| private static IDisposable InterceptConsole(ICakeContext context, Func<IConsole, IConsole> replacementProvider) | |
| { | |
| var log = context.Log; | |
| FieldInfo consoleFieldInfo; | |
| IConsole originalConsole; | |
| for (;;) | |
| { | |
| var consoleField = FindSingleFieldWithValue<IConsole>(log); | |
| if (consoleField != null) | |
| { | |
| consoleFieldInfo = consoleField.Item1; | |
| originalConsole = consoleField.Item2; | |
| break; | |
| } | |
| var decoratedLogField = FindSingleFieldWithValue<ICakeLog>(log); | |
| if (decoratedLogField == null) | |
| throw new InvalidOperationException(log + " does not have exactly one field containing an IConsole instance or exactly one field containing an ICakeLog instance."); | |
| log = decoratedLogField.Item2; | |
| } | |
| var replacement = replacementProvider.Invoke(originalConsole); | |
| if (replacement == originalConsole) return null; | |
| consoleFieldInfo.SetValue(log, replacement); | |
| return On.Dispose(() => consoleFieldInfo.SetValue(log, originalConsole)); | |
| } | |
| private sealed class BufferedOutputConsole : IConsole | |
| { | |
| private readonly IConsole internalConsole; | |
| private readonly AsyncLocal<TaskOutputBuffer> bufferAccess; | |
| internal AsyncLocal<TaskOutputBuffer> BufferAccess { get { return bufferAccess; } } | |
| public BufferedOutputConsole(IConsole internalConsole, AsyncLocal<TaskOutputBuffer> bufferAccess) | |
| { | |
| this.internalConsole = internalConsole; | |
| this.bufferAccess = bufferAccess; | |
| } | |
| public void Write(string format, params object[] arg) | |
| { | |
| var outputBuffer = bufferAccess.Value; | |
| if (outputBuffer != null) | |
| bufferAccess.Value.AppendAction(console => console.Write(format, arg)); | |
| else | |
| internalConsole.Write(format, arg); | |
| } | |
| public void WriteLine(string format, params object[] arg) | |
| { | |
| var outputBuffer = bufferAccess.Value; | |
| if (outputBuffer != null) | |
| bufferAccess.Value.AppendAction(console => console.WriteLine(format, arg)); | |
| else | |
| internalConsole.WriteLine(format, arg); | |
| } | |
| public void WriteError(string format, params object[] arg) | |
| { | |
| var outputBuffer = bufferAccess.Value; | |
| if (outputBuffer != null) | |
| bufferAccess.Value.AppendAction(console => console.WriteError(format, arg)); | |
| else | |
| internalConsole.WriteError(format, arg); | |
| } | |
| public void WriteErrorLine(string format, params object[] arg) | |
| { | |
| var outputBuffer = bufferAccess.Value; | |
| if (outputBuffer != null) | |
| bufferAccess.Value.AppendAction(console => console.WriteErrorLine(format, arg)); | |
| else | |
| internalConsole.WriteErrorLine(format, arg); | |
| } | |
| public ConsoleColor ForegroundColor | |
| { | |
| get | |
| { | |
| throw new NotImplementedException(); | |
| } | |
| set | |
| { | |
| var outputBuffer = bufferAccess.Value; | |
| if (outputBuffer != null) | |
| bufferAccess.Value.AppendAction(console => console.ForegroundColor = value); | |
| else | |
| internalConsole.ForegroundColor = value; | |
| } | |
| } | |
| public ConsoleColor BackgroundColor | |
| { | |
| get | |
| { | |
| throw new NotImplementedException(); | |
| } | |
| set | |
| { | |
| var outputBuffer = bufferAccess.Value; | |
| if (outputBuffer != null) | |
| bufferAccess.Value.AppendAction(console => console.BackgroundColor = value); | |
| else | |
| internalConsole.BackgroundColor = value; | |
| } | |
| } | |
| public void ResetColor() | |
| { | |
| var outputBuffer = bufferAccess.Value; | |
| if (outputBuffer != null) | |
| bufferAccess.Value.AppendAction(console => console.ResetColor()); | |
| else | |
| internalConsole.ResetColor(); | |
| } | |
| } | |
| private sealed class BufferedOutputProcessRunner : IProcessRunner | |
| { | |
| private readonly IProcessRunner internalRunner; | |
| private readonly AsyncLocal<TaskOutputBuffer> bufferAccess; | |
| internal AsyncLocal<TaskOutputBuffer> BufferAccess { get { return bufferAccess; } } | |
| public BufferedOutputProcessRunner(IProcessRunner internalRunner, AsyncLocal<TaskOutputBuffer> bufferAccess) | |
| { | |
| this.internalRunner = internalRunner; | |
| this.bufferAccess = bufferAccess; | |
| } | |
| public IProcess Start(FilePath filePath, ProcessSettings settings) | |
| { | |
| var outputBuffer = bufferAccess.Value; | |
| if (outputBuffer == null) return internalRunner.Start(filePath, settings); | |
| settings.RedirectStandardOutput = true; | |
| var wrapper = internalRunner.Start(filePath, settings); | |
| outputBuffer.AppendAction(console => | |
| { | |
| foreach (var line in wrapper.GetStandardOutput()) | |
| console.WriteLine(line.Replace("{", "{{").Replace("}", "}}")); | |
| }); | |
| return wrapper; | |
| } | |
| } | |
| public static class On | |
| { | |
| public static IDisposable Dispose(Action action) | |
| { | |
| return new OnDisposeAction(action); | |
| } | |
| private sealed class OnDisposeAction : IDisposable | |
| { | |
| private Action action; | |
| public OnDisposeAction(Action action) | |
| { | |
| this.action = action; | |
| } | |
| public void Dispose() | |
| { | |
| var action = System.Threading.Interlocked.Exchange(ref this.action, null); | |
| if (action != null) action.Invoke(); | |
| } | |
| } | |
| } |
@pitermarx I was reluctant, but I've just had to update it yet again for compatibility with https://github.com/agc93/Cake.BuildSystems.Module TFS integration. I am strongly considering it ASAP, when my other commitments are met.
Until it is available as a nuget package, you can include using http://cakebuild.net/docs/fundamentals/preprocessor-directives
#load "local:?path=tools/RunSimultaneously.cake"
Hey, have you ever seen this kind of error using this gist?
2020-12-07T22:07:59.9572751Z Error: System.AggregateException: One or more errors occurred. ---> System.InvalidOperationException: No process is associated with this object.
2020-12-07T22:07:59.9573390Z at System.Diagnostics.Process.EnsureState(State state)
2020-12-07T22:07:59.9573751Z at System.Diagnostics.Process.get_HasExited()
2020-12-07T22:07:59.9574150Z at Cake.Core.IO.ProcessWrapper.d__15.MoveNext()
2020-12-07T22:07:59.9574908Z at Submission#0.BufferedOutputProcessRunner.<>c__DisplayClass5_0.b__0(IConsole console) in C:\vsts-agent-1_work\8\s\core\build\run-simultaneously.cake:line 290
2020-12-07T22:07:59.9575971Z at Submission#0.TaskOutputBuffer.WriteOutputUntilCompleted(IConsole console) in C:\vsts-agent-1_work\8\s\core\build\run-simultaneously.cake:line 99
2020-12-07T22:07:59.9576856Z at Submission#0.RunSimultaneously(Action[] actions) in C:\vsts-agent-1_work\8\s\core\build\run-simultaneously.cake:line 73
2020-12-07T22:07:59.9577663Z at Submission#0.<>b__0_9() in C:\vsts-agent-1_work\8\s\core\build\azure-pipelines-pr-unit.cake:line 35
2020-12-07T22:07:59.9578262Z at Cake.Core.CakeTaskBuilderExtensions.<>c__DisplayClass32_0.b__0(ICakeContext x)
2020-12-07T22:07:59.9578610Z at Cake.Core.CakeTask.d__43.MoveNext()
AWESOME WORK! Could you make this a cake addin?