diff --git a/src/NTwain/Internals/ExceptionExtensions.cs b/src/NTwain/Internals/ExceptionExtensions.cs new file mode 100644 index 0000000..f186b38 --- /dev/null +++ b/src/NTwain/Internals/ExceptionExtensions.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace NTwain.Internals +{ + static class ExceptionExtensions + { + /// + /// Rethrows the specified excetion while keeping stack trace + /// if not null. + /// + /// The ex. + public static void TryRethrow(this Exception ex) + { + if (ex != null) + { + typeof(Exception).GetMethod("PrepForRemoting", + BindingFlags.NonPublic | BindingFlags.Instance)?.Invoke(ex, new object[0]); + throw ex; + } + } + + } +} diff --git a/src/NTwain/Threading/ActionItem.cs b/src/NTwain/Threading/ActionItem.cs index 310bdd6..32a947f 100644 --- a/src/NTwain/Threading/ActionItem.cs +++ b/src/NTwain/Threading/ActionItem.cs @@ -8,6 +8,8 @@ namespace NTwain.Threading private ManualResetEventSlim waiter; private Action action; + public Exception Error { get; private set; } + public ActionItem(Action action) { this.action = action; @@ -27,7 +29,7 @@ namespace NTwain.Threading } catch (Exception ex) { - // TODO: do something + Error = ex; } finally { diff --git a/src/NTwain/Threading/DispatcherLoop.cs b/src/NTwain/Threading/DispatcherLoop.cs new file mode 100644 index 0000000..68e7e75 --- /dev/null +++ b/src/NTwain/Threading/DispatcherLoop.cs @@ -0,0 +1,110 @@ +using NTwain.Data.Win32; +using NTwain.Internals; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security; +using System.Text; +using System.Threading; + +namespace NTwain.Threading +{ + /// + /// Provides an internal message pump on non-Windows using a background thread. + /// + class DispatcherLoop : IThreadContext + { + readonly TwainSession session; + BlockingCollection actionQueue; + CancellationTokenSource stopToken; + + Thread loopThread; + + public DispatcherLoop(TwainSession session) + { + this.session = session; + } + + public void Start() + { + if (loopThread != null) return; + + Debug.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId}: {nameof(WinMsgLoop)}.{nameof(Start)}()"); + + actionQueue = new BlockingCollection(); + stopToken = new CancellationTokenSource(); + + // startWaiter ensures thread is running before this method returns + using (var startWaiter = new ManualResetEventSlim()) + { + loopThread = new Thread(new ThreadStart(() => + { + Debug.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId}: starting msg pump..."); + + startWaiter.Set(); + + try + { + while (actionQueue.TryTake(out ActionItem work, -1, stopToken.Token)) + { + work.DoAction(); + } + } + catch (OperationCanceledException) { } + finally + { + // clear queue + actionQueue.Dispose(); + + loopThread = null; + } + })); + loopThread.IsBackground = true; + loopThread.TrySetApartmentState(ApartmentState.STA); + loopThread.Start(); + startWaiter.Wait(); + } + } + + public void Stop() + { + if (!stopToken.IsCancellationRequested) stopToken.Cancel(); + } + + bool IsSameThread() + { + return loopThread == Thread.CurrentThread || loopThread == null; + } + + + public void Invoke(Action action) + { + if (IsSameThread()) + { + // ok + action(); + } + else + { + // queue up work + using (var waiter = new ManualResetEventSlim()) + { + var work = new ActionItem(waiter, action); + actionQueue.TryAdd(work); + waiter.Wait(); + work.Error.TryRethrow(); + } + } + } + + public void BeginInvoke(Action action) + { + actionQueue.TryAdd(new ActionItem(action)); + } + } +} diff --git a/src/NTwain/Threading/UIThreadContext.cs b/src/NTwain/Threading/UIThreadContext.cs index c58b19f..770b186 100644 --- a/src/NTwain/Threading/UIThreadContext.cs +++ b/src/NTwain/Threading/UIThreadContext.cs @@ -1,4 +1,5 @@ -using System; +using NTwain.Internals; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -68,18 +69,7 @@ namespace NTwain.Threading } }, null); - if (error != null) { Rethrow(error); } - } - - /// - /// Rethrows the specified excetion while keeping stack trace. - /// - /// The ex. - static void Rethrow(Exception ex) - { - typeof(Exception).GetMethod("PrepForRemoting", - BindingFlags.NonPublic | BindingFlags.Instance)?.Invoke(ex, new object[0]); - throw ex; + error.TryRethrow(); } void IThreadContext.Start() diff --git a/src/NTwain/Threading/WinMsgLoop.cs b/src/NTwain/Threading/WinMsgLoop.cs index 4176a3a..36992a4 100644 --- a/src/NTwain/Threading/WinMsgLoop.cs +++ b/src/NTwain/Threading/WinMsgLoop.cs @@ -1,4 +1,5 @@ using NTwain.Data.Win32; +using NTwain.Internals; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -160,7 +161,7 @@ namespace NTwain.Threading loopThread.Start(); startWaiter.Wait(); - if (startErr != null) Rethrow(startErr); + startErr.TryRethrow(); } } @@ -203,9 +204,11 @@ namespace NTwain.Threading // queue up work using (var waiter = new ManualResetEventSlim()) { - actionQueue.Enqueue(new ActionItem(waiter, action)); + var work = new ActionItem(waiter, action); + actionQueue.Enqueue(work); UnsafeNativeMethods.PostMessageW(hWnd, dequeMsg, IntPtr.Zero, IntPtr.Zero); waiter.Wait(); + work.Error.TryRethrow(); } } } @@ -222,17 +225,6 @@ namespace NTwain.Threading } - /// - /// Rethrows the specified excetion while keeping stack trace. - /// - /// The ex. - static void Rethrow(Exception ex) - { - typeof(Exception).GetMethod("PrepForRemoting", - BindingFlags.NonPublic | BindingFlags.Instance)?.Invoke(ex, new object[0]); - throw ex; - } - [SuppressUnmanagedCodeSecurity] internal static class UnsafeNativeMethods { diff --git a/src/NTwain/TwainSession.cs b/src/NTwain/TwainSession.cs index 6fed785..bb06e02 100644 --- a/src/NTwain/TwainSession.cs +++ b/src/NTwain/TwainSession.cs @@ -44,6 +44,8 @@ namespace NTwain { case PlatformID.MacOSX: case PlatformID.Unix: + _internalContext = new DispatcherLoop(this); + break; default: _internalContext = new WinMsgLoop(this); _callback32Delegate = new Callback32(Handle32BitCallback);