diff --git a/Ductus.FluentDocker.Tests/AmbientContext/AmbientContext.cs b/Ductus.FluentDocker.Tests/AmbientContext/AmbientContext.cs new file mode 100644 index 00000000..9a402e85 --- /dev/null +++ b/Ductus.FluentDocker.Tests/AmbientContext/AmbientContext.cs @@ -0,0 +1,243 @@ +using System; +using System.Threading; +using Ductus.FluentDocker.AmbientContex; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Ductus.FluentDocker.Tests.AmbientContext +{ + [TestClass] + public class AmbientContext + { + [TestMethod] + public void Test1000000Nested() + { + for (var i = 0; i < 1000000; i++) + { + var thrVar = new ThreadVariable(); + using (thrVar.Use("my value")) + { + var x = thrVar.Current; + x = thrVar.Current; + x = thrVar.Current; + using (thrVar.Use("my value 2")) + { + var x2 = thrVar.Current; + x2 = thrVar.Current; + x2 = thrVar.Current; + } + } + } + } + + [TestMethod] + public void TestSimple() + { + var thrVar = new ThreadVariable(); + ShouldBeEmpty(thrVar); + using (thrVar.Use("my value")) + { + ShouldEqual(thrVar, "my value"); + } + + ShouldBeEmpty(thrVar); + } + + private static void ShouldEqual(ThreadVariable thrVar, T expected) + { + Assert.IsTrue(thrVar.HasCurrent); + Assert.AreEqual(thrVar.CurrentOrDefault, expected); + Assert.AreEqual(thrVar.Current, expected); + } + + private static void ShouldBeEmpty(ThreadVariable thrVar) + { + Assert.IsFalse(thrVar.HasCurrent); + Assert.ThrowsException(() => Assert.IsNull(thrVar.Current)); + Assert.AreEqual(thrVar.CurrentOrDefault, default(T)); + } + + [TestMethod] + public void TestFallback() + { + var thrVar = new ThreadVariable("default"); + ShouldEqual(thrVar, "default"); + using (thrVar.Use("my value")) + { + ShouldEqual(thrVar, "my value"); + } + + ShouldEqual(thrVar, "default"); + } + + [TestMethod] + public void TestValueTypeSupport() + { + var thrVar = new ThreadVariable(); + ShouldBeEmpty(thrVar); + Assert.IsFalse(thrVar.CurrentOrDefault); + using (thrVar.Use(true)) + { + ShouldEqual(thrVar, true); + } + + ShouldBeEmpty(thrVar); + Assert.IsFalse(thrVar.CurrentOrDefault); + } + + [TestMethod] + public void TestNullableSupport() + { + var thrVar = new ThreadVariable(); + ShouldBeEmpty(thrVar); + Assert.IsFalse(thrVar.CurrentOrDefault.HasValue); + using (thrVar.Use(true)) + { + ShouldEqual(thrVar, true); + } + + ShouldBeEmpty(thrVar); + Assert.IsFalse(thrVar.CurrentOrDefault.HasValue); + } + + [TestMethod] + public void TestNested() + { + var thrVar = new ThreadVariable(); + ShouldBeEmpty(thrVar); + using (thrVar.Use("my value")) + { + ShouldEqual(thrVar, "my value"); + using (thrVar.Use("my value 2")) + { + ShouldEqual(thrVar, "my value 2"); + } + + ShouldEqual(thrVar, "my value"); + } + + ShouldBeEmpty(thrVar); + } + + [TestMethod] + public void TestMultipleValues() + { + var thrVar1 = new ThreadVariable(); + var thrVar2 = new ThreadVariable(); + var thrVar3 = new ThreadVariable(); + + IDisposable scope1 = thrVar1.Use(1); + IDisposable scope2 = thrVar2.Use(2); + IDisposable scope3 = thrVar3.Use(3); + + Assert.AreEqual(thrVar1.Current, 1); + Assert.AreEqual(thrVar2.Current, 2); + Assert.AreEqual(thrVar3.Current, 3); + + scope1.Dispose(); + scope2.Dispose(); + scope3.Dispose(); + + ShouldBeEmpty(thrVar1); + ShouldBeEmpty(thrVar1); + ShouldBeEmpty(thrVar1); + + Assert.IsNull(thrVar1.CurrentOrDefault); + Assert.IsNull(thrVar2.CurrentOrDefault); + Assert.IsNull(thrVar3.CurrentOrDefault); + + Assert.IsFalse(thrVar1.CurrentOrDefault.HasValue); + Assert.IsFalse(thrVar2.CurrentOrDefault.HasValue); + Assert.IsFalse(thrVar3.CurrentOrDefault.HasValue); + } + + [TestMethod] + public void TestDisposeInIncorrectOrder() + { + var thrVar = new ThreadVariable(); + + ShouldBeEmpty(thrVar); + using (thrVar.Use(1)) // outer scope + { + ShouldEqual(thrVar, 1); + IDisposable middle = thrVar.Use(2); + ShouldEqual(thrVar, 2); + IDisposable inner = thrVar.Use(3); + ShouldEqual(thrVar, 3); + middle.Dispose(); + ShouldEqual(thrVar, 1); + + /* When disposing 'inner', usually the value is + * recovered to 'middle'.Value. + * But 'middle' is already disposed, so it should + * go back to 'outer' instead. + * + * Internally 'middle'.Dispose() should also + * dispose inner scopes, e.g. 'inner', so that + * 'inner'.Dispose() just does nothing. + */ + inner.Dispose(); + ShouldEqual(thrVar, 1); + } + + ShouldBeEmpty(thrVar); + } + + [TestMethod] + public void TestMultithread() + { + var thrVar = new ThreadVariable(); + var letWi1 = new AutoResetEvent(false); + var letWi2 = new AutoResetEvent(false); + + WaitCallback W2 = state => + { + Console.WriteLine("WI 2 Startup"); + + ShouldBeEmpty(thrVar); + Console.WriteLine("WI 2 is empty"); + + using (thrVar.Use("B")) + { + Console.WriteLine("WI 2 Set"); + ShouldEqual(thrVar, "B"); + Console.WriteLine("WI 2 is B"); + + letWi1.Set(); // goto 001 + + // wait + letWi2.WaitOne(); // 002 + } + + Console.WriteLine("WI 2 Disposed"); + ShouldBeEmpty(thrVar); + Console.WriteLine("WI 2 is empty"); + Console.WriteLine("WI 2 End"); + letWi1.Set(); // goto 003 + }; + // start + Console.WriteLine("WI 1 Startup"); + using (thrVar.Use("A")) + { + Console.WriteLine("WI 1 Set"); + ShouldEqual(thrVar, "A"); + Console.WriteLine("WI 1 is A"); + ThreadPool.QueueUserWorkItem(W2); + + // wait + letWi1.WaitOne(); // 001 + ShouldEqual(thrVar, "A"); + Console.WriteLine("WI 1 is still A"); + letWi2.Set(); // goto 002 + + // wait + letWi1.WaitOne(); // 003 + ShouldEqual(thrVar, "A"); + Console.WriteLine("WI 1 is still A"); + } + + Console.WriteLine("WI 1 Disposed"); + ShouldBeEmpty(thrVar); + Console.WriteLine("WI 1 is empty"); + } + } +} diff --git a/Ductus.FluentDocker/AmbientContext/DataReceivedContext.cs b/Ductus.FluentDocker/AmbientContext/DataReceivedContext.cs new file mode 100644 index 00000000..943caf3f --- /dev/null +++ b/Ductus.FluentDocker/AmbientContext/DataReceivedContext.cs @@ -0,0 +1,17 @@ +using System; +using Ductus.FluentDocker.AmbientContex; +using Ductus.FluentDocker.Executors.ProcessDataReceived; + +namespace Ductus.FluentDocker.AmbientContext +{ + public class DataReceivedContext + { + private static readonly ThreadVariable DataReceivedThreadVariable = new ThreadVariable(new DataReceived()); + public static DataReceived DataReceived => DataReceivedThreadVariable.Current; + + public static IDisposable UseProcessManager(DataReceived processManager) + { + return DataReceivedThreadVariable.Use(processManager); + } + } +} diff --git a/Ductus.FluentDocker/AmbientContext/ThreadVariable.cs b/Ductus.FluentDocker/AmbientContext/ThreadVariable.cs new file mode 100644 index 00000000..314626c7 --- /dev/null +++ b/Ductus.FluentDocker/AmbientContext/ThreadVariable.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections.Generic; +using System.Threading; + +namespace Ductus.FluentDocker.AmbientContex +{ + /// + ///

Minimod.ThreadVariable, Version 1.0.0, Copyright © Lars Corneliussen 2011

+ /// Makes thread variables () available + /// only inside a specific scope. It also manages nested usage of scopes on + /// the same variable declaration. + ///
+ /// + /// Author: Lars Corneliussen, 2008, http://startbigthinksmall.wordpress.com + /// Licensed under the Apache License, Version 2.0; you may not use this file except in compliance with the License. + /// http://www.apache.org/licenses/LICENSE-2.0 + /// + /// + /// + /// + /// + /// You can store a static instance of the thread variable and use it in any thread. + /// One possibilty is to make it public, the other one is to wrap it, if you want more + /// control. + /// + /// + /// IsSuperUser = new ThreadVariable(); + /// } + /// + /// .... + /// + /// using(SomeThreadVars.IsSuperUser.Use(true)) + /// { + /// SomeThreadVars.IsSuperUser.Current.ShouldBeTrue(); + /// } + /// ]]> + /// + /// + /// The type of the values to store. + public class ThreadVariable + { + /// + /// Storing the default value for a struct, null for a class or a + /// custom value given in as default to the constructor of + /// . + /// + private readonly T _fallback; + + /// + /// just to avoid comparing fallback with default(T) + /// + private readonly bool _fallbackDefined = false; + + /// + /// Stores the values (wrapped in ) + /// for n within the current thread. + /// + [ThreadStatic] + private static Dictionary, ThreadVariableValueScope> _values; + + /// + /// Initializes a threadsafe reusable instance. + /// + public ThreadVariable() + { + } + + /// + /// Initializes a threadsafe reusable instance with + /// as fallback value to and . + /// + /// + /// Defaults as 0 for or null for any are allowed, + /// but will then still be true and both + /// and will return them as + /// valid values. + /// + /// + /// The value and will return if + /// there is no value provided to the current calling thread. + /// + public ThreadVariable(T fallback) + { + _fallback = fallback; + _fallbackDefined = true; + } + + /// + /// Returns and creates a new dictionary if it doesn't exist. + /// + private static Dictionary, ThreadVariableValueScope> EnsuredValues + { + get + { + if (_values == null) + _values = new Dictionary, ThreadVariableValueScope>(1); + return _values; + } + } + + /// + /// Returns a scope for the . The value is held in the + /// current thread, until the scope is disposed. + /// + /// + /// Within the scope, the value will be available via and + /// , will always + /// be true. + /// + public IDisposable Use(T value) + { + ThreadVariableValueScope old; + var v = EnsuredValues; + v.TryGetValue(this, out old); + return v[this] = new ThreadVariableValueScope(this, value, old); + } + + /// + /// Returns the value of the current inner most scope. Throws an + /// , if no scope is available + /// in the calling context. + /// + /// + /// Is thrown when the calling context hasn't provided any value to + /// the current scope. + /// + public T Current + { + get + { + ThreadVariableValueScope current; + EnsuredValues.TryGetValue(this, out current); + if (current == null) + { + if (_fallbackDefined) + return _fallback; + + throw new InvalidOperationException("There is currently no value available."); + } + + return current.value; + } + } + + + /// + /// Returns the value of the current inner most scope. If the calling context + /// doesn't have any value, default(T) is returned. + /// + /// + /// default(T) is usually + /// + public T CurrentOrDefault + { + get { return HasCurrent ? Current : default(T); } + } + + /// + /// Gets a value indicating wether the current calling context has provided any values. + /// + public bool HasCurrent + { + get { return _fallbackDefined || EnsuredValues.ContainsKey(this); } + } + + + /// + /// Disposes the scope and it's other + /// nested scopes. + /// + private void DisposeScope(ThreadVariableValueScope scopeToDispose) + { + // 1) The programmer simply did an error if he tried to dispose a scope on a different thread + // 2) If the garbage collection takes care of disposal, it will get an error and ignore it + if (Thread.CurrentThread.ManagedThreadId != scopeToDispose.threadId) + throw new InvalidOperationException("The thread variable value scope " + + "has to be disposed on the same thread it was created on!"); + + /* Let's say we have three scopes: outer, middle and inner. + * + * When disposing 'inner', usually the value is + * recovered to 'middle'. + * But if 'middle' is disposed before 'inner', + * 'inner' should be disposed automatically, too. + * + * Else, disposing 'inner' afterwards, would end in + * setting the current value to 'middle' - which + * already is disposed + */ + + /* start on the current scope wich is always scopeToDispose + * or one of its inner scopes - actually the inner most one */ + var v = EnsuredValues; + ThreadVariableValueScope innerMost; + v.TryGetValue(this, out innerMost); + + // dispose all inner scopes + while (innerMost != scopeToDispose) + { + innerMost.MarkAsDisposed(); + innerMost = innerMost.previous; + } + + // mark current one as disposed + scopeToDispose.MarkAsDisposed(); + + // remove, or recover previous value + if (scopeToDispose.previous == null) + v.Remove(this); + else + v[this] = scopeToDispose.previous; + } + + + /// + /// Inner helper class wrapping the current value, it's precedor as well + /// as some meta data. + /// + private class ThreadVariableValueScope : IDisposable + { + private readonly ThreadVariable _key; + private bool _isDisposed; + + /* These values are accessed from ThreadVariable and therefor + * marked as internal. It's all private, so i drop properties + * wich just would make it slower. */ + internal readonly T value; + internal readonly ThreadVariableValueScope previous; + internal readonly int threadId; + + /* the previous value is stored in order to avoid the overhead of an stack or linked list */ + + public ThreadVariableValueScope(ThreadVariable key, T value, ThreadVariableValueScope previous) + { + threadId = Thread.CurrentThread.ManagedThreadId; + _key = key; + this.value = value; + this.previous = previous; + } + + internal void MarkAsDisposed() + { + _isDisposed = true; + + // Dispose() will then not be called by the GarbageCollector + GC.SuppressFinalize(this); + } + + void IDisposable.Dispose() + { + if (_isDisposed) + return; + + _key.DisposeScope(this); + } + } + } +} diff --git a/Ductus.FluentDocker/Builders/CompositeBuilder.cs b/Ductus.FluentDocker/Builders/CompositeBuilder.cs index 96dccb2c..a0d5b4f9 100644 --- a/Ductus.FluentDocker/Builders/CompositeBuilder.cs +++ b/Ductus.FluentDocker/Builders/CompositeBuilder.cs @@ -17,6 +17,7 @@ namespace Ductus.FluentDocker.Builders [Experimental(TargetVersion = "3.0.0")] public sealed class CompositeBuilder : BaseBuilder { + private readonly DockerComposeFileConfig _config = new DockerComposeFileConfig(); internal CompositeBuilder(IBuilder parent, string composeFile = null) : base(parent) diff --git a/Ductus.FluentDocker/Commands/Compose.cs b/Ductus.FluentDocker/Commands/Compose.cs index c6bc4979..bdd0fcd4 100644 --- a/Ductus.FluentDocker/Commands/Compose.cs +++ b/Ductus.FluentDocker/Commands/Compose.cs @@ -415,7 +415,8 @@ public static CommandResponse> ComposeUp(this DockerUri host, bool noStart = false, string[] services = null /*all*/, IDictionary env = null, - ICertificatePaths certificates = null, params string[] composeFile) + ICertificatePaths certificates = null, + params string[] composeFile) { if (forceRecreate && noRecreate) { @@ -463,7 +464,8 @@ public static CommandResponse> ComposeUp(this DockerUri host, return new ProcessExecutor>( "docker-compose".ResolveBinary(), - $"{args} up {options}", cwd.NeedCwd ? cwd.Cwd : null).ExecutionEnvironment(env).Execute(); + $"{args} up {options}", cwd.NeedCwd ? cwd.Cwd : null).ExecutionEnvironment(env) + .Execute(); } public static CommandResponse> ComposeRm(this DockerUri host, string altProjectName = null, @@ -471,7 +473,8 @@ public static CommandResponse> ComposeRm(this DockerUri host, stri bool removeVolumes = false, string[] services = null /*all*/, IDictionary env = null, - ICertificatePaths certificates = null, params string[] composeFile) + ICertificatePaths certificates = null, + params string[] composeFile) { var cwd = WorkingDirectory(composeFile); var args = $"{host.RenderBaseArgs(certificates)}"; diff --git a/Ductus.FluentDocker/Ductus.FluentDocker.csproj b/Ductus.FluentDocker/Ductus.FluentDocker.csproj index 0a75ba53..4d20e19a 100644 --- a/Ductus.FluentDocker/Ductus.FluentDocker.csproj +++ b/Ductus.FluentDocker/Ductus.FluentDocker.csproj @@ -35,7 +35,7 @@ - + diff --git a/Ductus.FluentDocker/Executors/ProcessDataReceived/DataReceived.cs b/Ductus.FluentDocker/Executors/ProcessDataReceived/DataReceived.cs new file mode 100644 index 00000000..4c816307 --- /dev/null +++ b/Ductus.FluentDocker/Executors/ProcessDataReceived/DataReceived.cs @@ -0,0 +1,12 @@ +namespace Ductus.FluentDocker.Executors.ProcessDataReceived +{ + public class DataReceived + { + public delegate void OutputDataReceivedEventHandler(object sender, ProcessDataReceivedArgs ars); + public OutputDataReceivedEventHandler OutputDataReceived; + + public delegate void ErrorDataReceivedEventHandler(object sender, ProcessDataReceivedArgs ars); + public ErrorDataReceivedEventHandler ErrorDataReceived; + + } +} diff --git a/Ductus.FluentDocker/Executors/ProcessDataReceived/ProcessDataReceivedArgs.cs b/Ductus.FluentDocker/Executors/ProcessDataReceived/ProcessDataReceivedArgs.cs new file mode 100644 index 00000000..17979511 --- /dev/null +++ b/Ductus.FluentDocker/Executors/ProcessDataReceived/ProcessDataReceivedArgs.cs @@ -0,0 +1,10 @@ +using System; + +namespace Ductus.FluentDocker.Executors.ProcessDataReceived +{ + public class ProcessDataReceivedArgs:EventArgs + { + public string ProcessIdentifier { get; set; } + public string Data { get; set; } + } +} diff --git a/Ductus.FluentDocker/Executors/ProcessExecutor.cs b/Ductus.FluentDocker/Executors/ProcessExecutor.cs index 02fcd140..b4ef4633 100644 --- a/Ductus.FluentDocker/Executors/ProcessExecutor.cs +++ b/Ductus.FluentDocker/Executors/ProcessExecutor.cs @@ -1,7 +1,10 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Runtime.CompilerServices; using System.Text; +using Ductus.FluentDocker.AmbientContext; using Ductus.FluentDocker.Common; +using Ductus.FluentDocker.Executors.ProcessDataReceived; using Ductus.FluentDocker.Extensions; using Ductus.FluentDocker.Model.Containers; @@ -30,7 +33,7 @@ public ProcessExecutor(string command, string arguments, string workingdir = nul public IDictionary Env { get; } = new Dictionary(); - public CommandResponse Execute() + public CommandResponse Execute([CallerMemberName] string caller = "") { var startInfo = new ProcessStartInfo { @@ -60,16 +63,28 @@ public CommandResponse Execute() var output = new StringBuilder(); var err = new StringBuilder(); + var dataReceivedContext = DataReceivedContext.DataReceived; + process.OutputDataReceived += (sender, args) => { if (!string.IsNullOrEmpty(args.Data)) output.AppendLine(args.Data); + + if (dataReceivedContext == null) return; + + var processDataReceivedArgs = new ProcessDataReceivedArgs {Data = args.Data, ProcessIdentifier = caller}; + dataReceivedContext.OutputDataReceived?.Invoke(sender, processDataReceivedArgs); }; process.ErrorDataReceived += (sender, args) => { if (!string.IsNullOrEmpty(args.Data)) err.AppendLine(args.Data); + + if (dataReceivedContext == null) return; + + var processDataReceivedArgs = new ProcessDataReceivedArgs {Data = args.Data, ProcessIdentifier = caller}; + dataReceivedContext.ErrorDataReceived?.Invoke(sender, processDataReceivedArgs); }; if (!process.Start()) diff --git a/Ductus.FluentDocker/Model/Containers/ImageConfig.cs b/Ductus.FluentDocker/Model/Containers/ImageConfig.cs index 3a8a0c58..4cccfa73 100644 --- a/Ductus.FluentDocker/Model/Containers/ImageConfig.cs +++ b/Ductus.FluentDocker/Model/Containers/ImageConfig.cs @@ -19,6 +19,7 @@ public sealed class ImageConfig public ContainerConfig Config { get; set; } public string Architecture { get; set; } public string Os { get; set; } + public string OsVersion { get; set; } public long Size { get; set; } public long VirtualSize { get; set; } public GraphDriver GraphDriver { get; set; } diff --git a/Ductus.FluentDocker/Services/Impl/BuilderCompositeService.cs b/Ductus.FluentDocker/Services/Impl/BuilderCompositeService.cs index a8a85c6b..d7823134 100644 --- a/Ductus.FluentDocker/Services/Impl/BuilderCompositeService.cs +++ b/Ductus.FluentDocker/Services/Impl/BuilderCompositeService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; using System.Linq; namespace Ductus.FluentDocker.Services.Impl diff --git a/Ductus.FluentDocker/Services/Impl/DockerComposeCompositeService.cs b/Ductus.FluentDocker/Services/Impl/DockerComposeCompositeService.cs index 2cf2223c..a75e0197 100644 --- a/Ductus.FluentDocker/Services/Impl/DockerComposeCompositeService.cs +++ b/Ductus.FluentDocker/Services/Impl/DockerComposeCompositeService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; using System.Linq; using Ductus.FluentDocker.Commands; using Ductus.FluentDocker.Common; @@ -136,7 +137,8 @@ public override void Start() false, _config.Services, _config.EnvironmentNameValue, - host.Certificates, _config.ComposeFilePath.ToArray()); + host.Certificates, + _config.ComposeFilePath.ToArray()); if (!result.Success) { @@ -213,8 +215,14 @@ public override void Remove(bool force = false) State = ServiceRunningState.Removing; var host = Hosts.First(); - var result = host.Host.ComposeRm(_config.AlternativeServiceName, force, - !_config.KeepVolumes, _config.Services, _config.EnvironmentNameValue, host.Certificates, _config.ComposeFilePath.ToArray()); + var result = host.Host.ComposeRm( + _config.AlternativeServiceName, + force, + !_config.KeepVolumes, + _config.Services, + _config.EnvironmentNameValue, + host.Certificates, + _config.ComposeFilePath.ToArray()); if (!result.Success) {