Mercurial > pub > ImplabNet
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) { - } }