changeset 210:5dc21f6a3222 v2

Code review for RunnableComponent Added StaApartment class based on System.Windows.Forms.Application message loop
author cin
date Mon, 20 Mar 2017 17:44:18 +0300
parents a867536c68fc
children 3eb3255d8cc5
files Implab.Fx.Test/Implab.Fx.Test.csproj Implab.Fx.Test/StaApartmentTests.cs Implab.Fx/Implab.Fx.csproj Implab.Fx/StaApartment.cs Implab.Test/Implab.Test.csproj Implab/Components/RunnableComponent.cs
diffstat 6 files changed, 347 insertions(+), 56 deletions(-) [+]
line wrap: on
line diff
--- a/Implab.Fx.Test/Implab.Fx.Test.csproj	Wed Nov 16 03:06:08 2016 +0300
+++ b/Implab.Fx.Test/Implab.Fx.Test.csproj	Mon Mar 20 17:44:18 2017 +0300
@@ -80,6 +80,7 @@
     <Compile Include="Sample\OverlayForm.Designer.cs">
       <DependentUpon>OverlayForm.cs</DependentUpon>
     </Compile>
+    <Compile Include="StaApartmentTests.cs" />
   </ItemGroup>
   <ItemGroup>
     <EmbeddedResource Include="Sample\MainForm.resx">
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Implab.Fx.Test/StaApartmentTests.cs	Mon Mar 20 17:44:18 2017 +0300
@@ -0,0 +1,52 @@
+using System;
+using System.Reflection;
+using System.Threading;
+using Implab.Parallels;
+using Implab.Components;
+
+#if MONO
+
+using NUnit.Framework;
+using TestClassAttribute = NUnit.Framework.TestFixtureAttribute;
+using TestMethodAttribute = NUnit.Framework.TestAttribute;
+using AssertFailedException = NUnit.Framework.AssertionException;
+#else
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+#endif
+namespace Implab.Fx.Test {
+    [TestClass]
+    public class StaApartmentTests {
+        [TestMethod]
+        public void CreateDestroyApartment() {
+            var apartment = new StaApartment();
+            try {
+                Assert.IsNotNull(apartment.SyncContext);
+                Assert.Fail();
+            } catch (InvalidOperationException) {
+                // OK
+            }
+
+            apartment.Start().Join();
+            Assert.AreEqual(apartment.State, ExecutionState.Running);
+
+            Assert.IsNotNull(apartment.SyncContext);
+            apartment.Stop().Join();
+            
+            Assert.IsTrue(apartment.State == ExecutionState.Disposed);
+        }
+
+        [TestMethod]
+        public void InvokeInApartment() {
+            var apartment = new StaApartment();
+
+            apartment.Start().Join();
+
+            var apType = apartment.Invoke(() => { return Thread.CurrentThread.GetApartmentState(); }).Join();
+            Assert.AreEqual(apType, ApartmentState.STA);
+
+            apartment.Stop().Join();
+        }
+    }
+}
--- a/Implab.Fx/Implab.Fx.csproj	Wed Nov 16 03:06:08 2016 +0300
+++ b/Implab.Fx/Implab.Fx.csproj	Mon Mar 20 17:44:18 2017 +0300
@@ -70,6 +70,7 @@
     <Compile Include="PromiseHelpers.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="ControlBoundPromise.cs" />
+    <Compile Include="StaApartment.cs" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\Implab\Implab.csproj">
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Implab.Fx/StaApartment.cs	Mon Mar 20 17:44:18 2017 +0300
@@ -0,0 +1,188 @@
+using Implab.Components;
+using Implab.Diagnostics;
+using Implab.Parallels;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace Implab.Fx {
+    public class StaApartment : RunnableComponent {
+        readonly Thread m_worker;
+        SynchronizationContext m_syncContext;
+        readonly Promise m_threadStarted;
+        readonly Promise m_threadTerminated;
+
+        public StaApartment() : base(true) {
+            m_threadStarted = new Promise();
+            m_threadTerminated = new Promise();
+
+            m_worker = new Thread(WorkerEntry);
+            m_worker.SetApartmentState(ApartmentState.STA);
+            m_worker.IsBackground = true;
+            m_worker.Name = "STA managed aparment";
+        }
+
+        public SynchronizationContext SyncContext {
+            get {
+                if (m_syncContext == null)
+                    throw new InvalidOperationException();
+                return m_syncContext;
+            }
+        }
+
+        public IPromise Invoke(Action<ICancellationToken> action) {
+            Safe.ArgumentNotNull(action, "action");
+
+            if (m_syncContext == null)
+                throw new InvalidOperationException();
+            var p = new Promise();
+            var lop = TraceContext.Instance.CurrentOperation;
+
+            m_syncContext.Post(x => {
+                TraceContext.Instance.EnterLogicalOperation(lop, false);
+                try {
+                    if (p.CancelOperationIfRequested())
+                        return;
+
+                    action(p);
+                    p.Resolve();
+                } catch (Exception e) {
+                    p.Reject(e);
+                } finally {
+                    TraceContext.Instance.Leave();
+                }
+            }, null);
+
+            return p;
+        }
+
+        public IPromise<T> Invoke<T>(Func<ICancellationToken, T> action) {
+            Safe.ArgumentNotNull(action, "action");
+
+            if (m_syncContext == null)
+                throw new InvalidOperationException();
+            var p = new Promise<T>();
+            var lop = TraceContext.Instance.CurrentOperation;
+
+            m_syncContext.Post(x => {
+                TraceContext.Instance.EnterLogicalOperation(lop, false);
+                try {
+                    if (p.CancelOperationIfRequested())
+                        return;
+                    p.Resolve(action(p));
+                } catch (Exception e) {
+                    p.Reject(e);
+                } finally {
+                    TraceContext.Instance.Leave();
+                }
+            }, null);
+
+            return p;
+        }
+
+        public IPromise Invoke(Action action) {
+            Safe.ArgumentNotNull(action, "action");
+
+            if (m_syncContext == null)
+                throw new InvalidOperationException();
+            var p = new Promise();
+            var lop = TraceContext.Instance.CurrentOperation;
+
+            m_syncContext.Post(x => {
+                TraceContext.Instance.EnterLogicalOperation(lop, false);
+                try {
+                    if (p.CancelOperationIfRequested())
+                        return;
+                    action();
+                    p.Resolve();
+                } catch (Exception e) {
+                    p.Reject(e);
+                } finally {
+                    TraceContext.Instance.Leave();
+                }
+            }, null);
+
+            return p;
+        }
+
+        public IPromise<T> Invoke<T>(Func<T> action) {
+            Safe.ArgumentNotNull(action, "action");
+
+            if (m_syncContext == null)
+                throw new InvalidOperationException();
+            var p = new Promise<T>();
+            var lop = TraceContext.Instance.CurrentOperation;
+
+            m_syncContext.Post(x => {
+                TraceContext.Instance.EnterLogicalOperation(lop, false);
+                try {
+                    if (p.CancelOperationIfRequested())
+                        return;
+                    p.Resolve(action());
+                } catch (Exception e) {
+                    p.Reject(e);
+                } finally {
+                    TraceContext.Instance.Leave();
+                }
+            }, null);
+
+            return p;
+        }
+
+
+        /// <summary>
+        /// Starts the apartment thread
+        /// </summary>
+        /// <returns>Promise which will be fullfiled when the syncronization
+        /// context will be ready to accept tasks.</returns>
+        protected override IPromise OnStart() {
+            m_worker.Start();
+            return m_threadStarted;
+        }
+
+        /// <summary>
+        /// Posts quit message to the message loop of the apartment
+        /// </summary>
+        /// <returns>Promise</returns>
+        protected override IPromise OnStop() {
+            m_syncContext.Post(x => Application.ExitThread(), null);
+            return m_threadTerminated;
+        }
+
+        void WorkerEntry() {
+            m_syncContext = new WindowsFormsSynchronizationContext();
+            SynchronizationContext.SetSynchronizationContext(m_syncContext);
+
+            m_threadStarted.Resolve();
+
+            Application.OleRequired();
+            Application.Run();
+
+            try {
+                OnShutdown();
+                m_threadTerminated.Resolve();
+            } catch(Exception err) {
+                m_threadTerminated.Reject(err);
+            }
+        }
+
+        /// <summary>
+        /// Called from the STA apartment after the message loop is terminated, override this
+        /// method to handle apartment cleanup.
+        /// </summary>
+        protected virtual void OnShutdown() {
+        }
+
+        protected override void Dispose(bool disposing) {
+            if (disposing) {
+                if (!m_threadTerminated.IsResolved)
+                    m_syncContext.Post(x => Application.ExitThread(), null);
+            }
+            base.Dispose(disposing);
+        }
+    }
+}
--- a/Implab.Test/Implab.Test.csproj	Wed Nov 16 03:06:08 2016 +0300
+++ b/Implab.Test/Implab.Test.csproj	Mon Mar 20 17:44:18 2017 +0300
@@ -63,6 +63,9 @@
   <ItemGroup>
     <Compile Include="AsyncTests.cs" />
     <Compile Include="CancelationTests.cs" />
+    <Compile Include="Mock\MockPollingComponent.cs" />
+    <Compile Include="Mock\MockRunnableComponent.cs" />
+    <Compile Include="PollingComponentTests.cs" />
     <Compile Include="PromiseHelper.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="RunnableComponentTests.cs" />
@@ -73,6 +76,9 @@
       <Name>Implab</Name>
     </ProjectReference>
   </ItemGroup>
+  <ItemGroup>
+    <Folder Include="Implab.Format.Test\" />
+  </ItemGroup>
   <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
        Other similar extension points exist, see Microsoft.Common.targets.
--- a/Implab/Components/RunnableComponent.cs	Wed Nov 16 03:06:08 2016 +0300
+++ b/Implab/Components/RunnableComponent.cs	Mon Mar 20 17:44:18 2017 +0300
@@ -15,52 +15,90 @@
         }
 
         class StateMachine {
-            static readonly ExecutionState[,] _transitions;
+            public static readonly ExecutionState[,] ReusableTransitions;
+            public static readonly ExecutionState[,] NonreusableTransitions;
+
+            class StateBuilder {
+                readonly ExecutionState[,] m_states;
+
+                public ExecutionState[,] States {
+                    get { return m_states; }
+                }
+                public StateBuilder(ExecutionState[,] states) {
+                    m_states = states;
+                }
+
+                public StateBuilder() {
+                    m_states = new ExecutionState[(int)ExecutionState.Last + 1, (int)Commands.Last + 1];
+                }
+
+                public StateBuilder Edge(ExecutionState s1, ExecutionState s2, Commands cmd) {
+                    m_states[(int)s1, (int)cmd] = s2;
+                    return this;
+                }
+
+                public StateBuilder Clone() {
+                    return new StateBuilder((ExecutionState[,])m_states.Clone());
+                }
+            }
 
             static StateMachine() {
-                _transitions = new ExecutionState[(int)ExecutionState.Last + 1, (int)Commands.Last + 1];
+                ReusableTransitions = new ExecutionState[(int)ExecutionState.Last + 1, (int)Commands.Last + 1];
 
-                Edge(ExecutionState.Created, ExecutionState.Initializing, Commands.Init);
-                Edge(ExecutionState.Created, ExecutionState.Disposed, Commands.Dispose);
+                var common = new StateBuilder()
+                    .Edge(ExecutionState.Created, ExecutionState.Initializing, Commands.Init)
+                    .Edge(ExecutionState.Created, ExecutionState.Disposed, Commands.Dispose)
 
-                Edge(ExecutionState.Initializing, ExecutionState.Ready, Commands.Ok);
-                Edge(ExecutionState.Initializing, ExecutionState.Failed, Commands.Fail);
+                    .Edge(ExecutionState.Initializing, ExecutionState.Ready, Commands.Ok)
+                    .Edge(ExecutionState.Initializing, ExecutionState.Failed, Commands.Fail)
 
-                Edge(ExecutionState.Ready, ExecutionState.Starting, Commands.Start);
-                Edge(ExecutionState.Ready, ExecutionState.Disposed, Commands.Dispose);
+                    .Edge(ExecutionState.Ready, ExecutionState.Starting, Commands.Start)
+                    .Edge(ExecutionState.Ready, ExecutionState.Disposed, Commands.Dispose)
+
+                    .Edge(ExecutionState.Starting, ExecutionState.Running, Commands.Ok)
+                    .Edge(ExecutionState.Starting, ExecutionState.Failed, Commands.Fail)
+                    .Edge(ExecutionState.Starting, ExecutionState.Stopping, Commands.Stop)
+                    .Edge(ExecutionState.Starting, ExecutionState.Disposed, Commands.Dispose)
 
-                Edge(ExecutionState.Starting, ExecutionState.Running, Commands.Ok);
-                Edge(ExecutionState.Starting, ExecutionState.Failed, Commands.Fail);
-                Edge(ExecutionState.Starting, ExecutionState.Stopping, Commands.Stop);
-                Edge(ExecutionState.Starting, ExecutionState.Disposed, Commands.Dispose);
+                    .Edge(ExecutionState.Running, ExecutionState.Failed, Commands.Fail)
+                    .Edge(ExecutionState.Running, ExecutionState.Stopping, Commands.Stop)
+                    .Edge(ExecutionState.Running, ExecutionState.Disposed, Commands.Dispose)
+
+                    .Edge(ExecutionState.Failed, ExecutionState.Disposed, Commands.Dispose)
+                    .Edge(ExecutionState.Failed, ExecutionState.Initializing, Commands.Reset)
+
+                    .Edge(ExecutionState.Stopping, ExecutionState.Failed, Commands.Fail)
+                    .Edge(ExecutionState.Stopping, ExecutionState.Disposed, Commands.Dispose)
+
+                    .Edge(ExecutionState.Disposed, ExecutionState.Disposed, Commands.Dispose);
 
-                Edge(ExecutionState.Running, ExecutionState.Failed, Commands.Fail);
-                Edge(ExecutionState.Running, ExecutionState.Stopping, Commands.Stop);
-                Edge(ExecutionState.Running, ExecutionState.Disposed, Commands.Dispose);
+                var reusable = common
+                    .Clone()
+                    .Edge(ExecutionState.Stopping, ExecutionState.Ready, Commands.Ok);
+
+                var nonreusable = common
+                    .Clone()
+                    .Edge(ExecutionState.Stopping, ExecutionState.Disposed, Commands.Ok);
+                
+                NonreusableTransitions = nonreusable.States;
+                ReusableTransitions = reusable.States;
 
-                Edge(ExecutionState.Stopping, ExecutionState.Failed, Commands.Fail);
-                Edge(ExecutionState.Stopping, ExecutionState.Ready, Commands.Ok);
-                Edge(ExecutionState.Stopping, ExecutionState.Disposed, Commands.Dispose);
-
-                Edge(ExecutionState.Failed, ExecutionState.Disposed, Commands.Dispose);
-                Edge(ExecutionState.Failed, ExecutionState.Initializing, Commands.Reset);
             }
 
-            static void Edge(ExecutionState s1, ExecutionState s2, Commands cmd) {
-                _transitions[(int)s1, (int)cmd] = s2;
-            }
+            readonly ExecutionState[,] m_states;
 
             public ExecutionState State {
                 get;
                 private set;
             }
 
-            public StateMachine(ExecutionState initial) {
+            public StateMachine(ExecutionState[,] states, ExecutionState initial) {
                 State = initial;
+                m_states = states;
             }
 
             public bool Move(Commands cmd) {
-                var next = _transitions[(int)State, (int)cmd];
+                var next = m_states[(int)State, (int)cmd];
                 if (next == ExecutionState.Undefined)
                     return false;
                 State = next;
@@ -81,9 +119,11 @@
         /// <param name="initialized">If set, the component initial state is <see cref="ExecutionState.Ready"/> and the component is ready to start, otherwise initialization is required.</param>
         /// <param name="reusable">If set, the component may start after it has been stopped, otherwise the component is disposed after being stopped.</param>
         protected RunnableComponent(bool initialized, bool reusable) {
-            m_stateMachine = new StateMachine(initialized ? ExecutionState.Ready : ExecutionState.Created);
+            m_stateMachine = new StateMachine(
+                reusable ? StateMachine.ReusableTransitions : StateMachine.NonreusableTransitions,
+                initialized ? ExecutionState.Ready : ExecutionState.Created
+            );
             m_reusable = reusable;
-            DisposeTimeout = 10000;
         }
 
         /// <summary>
@@ -93,14 +133,6 @@
         protected RunnableComponent(bool initialized) : this(initialized, false) {
         }
 
-        /// <summary>
-        /// Gets or sets the timeout to wait for the pending operation to complete. If the pending operation doesn't finish than the component will be disposed anyway.
-        /// </summary>
-        protected int DisposeTimeout {
-            get;
-            set;
-        }
-
         void ThrowInvalidCommand(Commands cmd) {
             if (m_stateMachine.State == ExecutionState.Disposed)
                 throw new ObjectDisposedException(ToString());
@@ -155,21 +187,43 @@
 
                 ret = m_pending;
                 m_pending = pending;
-                m_lastError = error;
-                
+                m_lastError = error;
+
             }
-            if(prev != current)
+            if (prev != current)
                 OnStateChanged(prev, current, error);
             return ret;
         }
 
+        /// <summary>
+        /// Handles the state of the component change event, raises the <see cref="StateChanged"/>  event, handles
+        /// the transition to the <see cref="ExecutionState.Disposed"/> state (calls <see cref="Dispose(bool)"/> method).
+        /// </summary>
+        /// <param name="previous">The previous state</param>
+        /// <param name="current">The current state</param>
+        /// <param name="error">The last error if any.</param>
+        /// <remarks>
+        /// <para>
+        /// If the previous state and the current state are same this method isn't called, such situiation is treated
+        /// as the component hasn't changed it's state.
+        /// </para>
+        /// <para>
+        /// When overriding this method ensure the call is made to the base implementation, otherwise it will lead to 
+        /// the wrong behavior of the component.
+        /// </para>
+        /// </remarks>
         protected virtual void OnStateChanged(ExecutionState previous, ExecutionState current, Exception error) {
-            var h = StateChanged;
-            if (h != null)
-                h(this, new StateChangeEventArgs {
+            StateChanged.DispatchEvent(
+                this,
+                new StateChangeEventArgs {
                     State = current,
                     LastError = error
-                });
+                }
+            );
+            if (current == ExecutionState.Disposed) {
+                GC.SuppressFinalize(this);
+                Dispose(true);
+            }
         }
 
         /// <summary>
@@ -278,8 +332,7 @@
         }
 
         public IPromise Stop() {
-            var pending = InvokeAsync(Commands.Stop, OnStop, StopPending);
-            return m_reusable ? pending : pending.Then(Dispose);
+            return InvokeAsync(Commands.Stop, OnStop, StopPending);
         }
 
         protected virtual IPromise OnStop() {
@@ -336,16 +389,7 @@
         /// </para></remarks>
         [SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification = "Dipose(bool) and GC.SuppessFinalize are called")]
         public void Dispose() {
-            IPromise pending;
-
-            lock (m_stateMachine) {
-                if (m_stateMachine.State == ExecutionState.Disposed)
-                    return;
-                Move(Commands.Dispose, null, null);
-            }
-
-            GC.SuppressFinalize(this);
-            Dispose(true);
+            Move(Commands.Dispose, null, null);
         }
 
         ~RunnableComponent() {
@@ -360,7 +404,6 @@
         /// <param name="disposing">true if this method is called during normal dispose process.</param>
         /// <param name="pending">The operation which is currenty pending</param>
         protected virtual void Dispose(bool disposing) {
-
         }
 
     }