diff --git a/samples/WinConsole32/Program.cs b/samples/WinConsole32/Program.cs index 2851060..9abe79d 100644 --- a/samples/WinConsole32/Program.cs +++ b/samples/WinConsole32/Program.cs @@ -36,27 +36,30 @@ namespace WinConsole32 Console.WriteLine($"Default data source = {defaultSrc}"); Console.WriteLine(); - twain.ShowUserSelect(); - Console.WriteLine($"Selected data source = {twain.DefaultSource}"); - Console.WriteLine(); - - var targetSrc = defaultSrc.HasValue ? defaultSrc : firstSrc; - - if (targetSrc.HasValue) + sts = twain.ShowUserSelect(); + if (sts.IsSuccess) { - TestThisSource(twain, targetSrc); - } - else - { - Console.WriteLine("No data source to test."); + Console.WriteLine($"Selected data source = {twain.DefaultSource}"); Console.WriteLine(); - } - Console.WriteLine("---------------------------------------------"); - Console.WriteLine("Test in progress, press Enter to stop testing"); - Console.WriteLine("---------------------------------------------"); - Console.ReadLine(); - twain.TryStepdown(STATE.S1); + var targetSrc = defaultSrc.HasValue ? defaultSrc : firstSrc; + + if (targetSrc.HasValue) + { + TestThisSource(twain, targetSrc); + } + else + { + Console.WriteLine("No data source to test."); + Console.WriteLine(); + } + + Console.WriteLine("---------------------------------------------"); + Console.WriteLine("Test in progress, press Enter to stop testing"); + Console.WriteLine("---------------------------------------------"); + Console.ReadLine(); + twain.TryStepdown(STATE.S1); + } } else { @@ -115,7 +118,7 @@ namespace WinConsole32 TW_SETUPFILEXFER setup = new() { - FileName = targetName, + FileName = Path.Combine("Images", targetName), Format = format, }; e.SetupFileTransfer(ref setup); @@ -171,9 +174,11 @@ namespace WinConsole32 { watch.Stop(); var elapsed = watch.Elapsed; - Console.WriteLine($"Session source disabled, took {elapsed}."); + Console.WriteLine($"Session source disabled, took {elapsed}, will retest in 3 sec..."); - //TestThisSource(twain, e); + Thread.Sleep(3000); + if (twain.State > STATE.S3) + TestThisSource(twain, e); } private static void TestThisSource(TwainAppSession twain, TW_IDENTITY_LEGACY source) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 7df73d3..e981ddc 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ 4.0.0.0 - alpha.7 + alpha.8 4.0.0.0 diff --git a/src/NTwain/MessagePumpThread.cs b/src/NTwain/MessagePumpThread.cs index 1bdba63..5076352 100644 --- a/src/NTwain/MessagePumpThread.cs +++ b/src/NTwain/MessagePumpThread.cs @@ -9,8 +9,7 @@ using System.Windows.Forms; namespace NTwain { /// - /// For use under Windows to host a message pump in non-winform/wpf apps. - /// This is highly experimental. + /// For use under Windows to host a message pump. /// class MessagePumpThread { diff --git a/src/NTwain/TwainAppSession.Callbacks.cs b/src/NTwain/TwainAppSession.Callbacks.cs index bd6c8f4..9ba2aae 100644 --- a/src/NTwain/TwainAppSession.Callbacks.cs +++ b/src/NTwain/TwainAppSession.Callbacks.cs @@ -7,125 +7,133 @@ using System.Runtime.InteropServices; namespace NTwain { - // this file contains callback methods + // this file contains callback methods - partial class TwainAppSession - { - - delegate ushort LegacyIDCallbackDelegate( - ref TW_IDENTITY_LEGACY origin, ref TW_IDENTITY_LEGACY dest, - DG dg, DAT dat, MSG msg, IntPtr twnull - ); - delegate ushort BotchedLinuxCallbackDelegate - ( - ref TW_IDENTITY origin, ref TW_IDENTITY dest, - DG dg, DAT dat, MSG msg, IntPtr twnull - ); - delegate ushort OSXCallbackDelegate - ( - ref TW_IDENTITY_MACOSX origin, ref TW_IDENTITY_MACOSX dest, - DG dg, DAT dat, MSG msg, IntPtr twnull - ); - - // these are kept around while a callback ptr is registered so they - // don't get gc'd - readonly LegacyIDCallbackDelegate _legacyCallbackDelegate; - readonly OSXCallbackDelegate _osxCallbackDelegate; - - /// - /// Try to registers callbacks for after opening the source. - /// - internal void RegisterCallback() + partial class TwainAppSession { - IntPtr cbPtr = IntPtr.Zero; - if (TWPlatform.IsMacOSX) - { - cbPtr = Marshal.GetFunctionPointerForDelegate(_osxCallbackDelegate); - } - else - { - cbPtr = Marshal.GetFunctionPointerForDelegate(_legacyCallbackDelegate); - } + delegate ushort LegacyIDCallbackDelegate( + ref TW_IDENTITY_LEGACY origin, ref TW_IDENTITY_LEGACY dest, + DG dg, DAT dat, MSG msg, IntPtr twnull + ); + delegate ushort BotchedLinuxCallbackDelegate + ( + ref TW_IDENTITY origin, ref TW_IDENTITY dest, + DG dg, DAT dat, MSG msg, IntPtr twnull + ); + delegate ushort OSXCallbackDelegate + ( + ref TW_IDENTITY_MACOSX origin, ref TW_IDENTITY_MACOSX dest, + DG dg, DAT dat, MSG msg, IntPtr twnull + ); - var rc = TWRC.FAILURE; + // these are kept around while a callback ptr is registered so they + // don't get gc'd + readonly LegacyIDCallbackDelegate _legacyCallbackDelegate; + readonly OSXCallbackDelegate _osxCallbackDelegate; - // per the spec (pg 8-10), apps for 2.2 or higher uses callback2 so try this first - if (_appIdentity.ProtocolMajor > 2 || (_appIdentity.ProtocolMajor >= 2 && _appIdentity.ProtocolMinor >= 2)) - { - var cb2 = new TW_CALLBACK2 { CallBackProc = cbPtr }; - rc = DGControl.Callback2.RegisterCallback(ref _appIdentity, ref _currentDS, ref cb2); - } - if (rc != TWRC.SUCCESS) - { - // always try old callback - var cb = new TW_CALLBACK { CallBackProc = cbPtr }; - DGControl.Callback.RegisterCallback(ref _appIdentity, ref _currentDS, ref cb); - } - } + /// + /// Try to registers callbacks for after opening the source. + /// + internal void RegisterCallback() + { + IntPtr cbPtr = IntPtr.Zero; - private ushort LegacyCallbackHandler - ( - ref TW_IDENTITY_LEGACY origin, ref TW_IDENTITY_LEGACY dest, - DG dg, DAT dat, MSG msg, IntPtr twnull - ) - { - Debug.WriteLine($"Legacy callback got {msg}"); - HandleSourceMsg(msg); - return (ushort)TWRC.SUCCESS; - } - - private ushort OSXCallbackHandler - ( - ref TW_IDENTITY_MACOSX origin, ref TW_IDENTITY_MACOSX dest, - DG dg, DAT dat, MSG msg, IntPtr twnull - ) - { - Debug.WriteLine($"OSX callback got {msg}"); - HandleSourceMsg(msg); - return (ushort)TWRC.SUCCESS; - } - - - private void HandleSourceMsg(MSG msg, [CallerMemberName] string? caller = null) - { - Debug.WriteLine($"[thread {Environment.CurrentManagedThreadId}] {nameof(HandleSourceMsg)} called by {caller} at state {State} with {msg}."); - - // the reason we post these to the background is - // if they're coming from UI message loop then - // this needs to return asap - - switch (msg) - { - case MSG.XFERREADY: - // some sources spam this even during transfer so we gate it - if (!_inTransfer) - { - _inTransfer = true; - _xferReady.Set(); - //_bgPendingMsgs.Add(msg); - } - break; - case MSG.CLOSEDSOK: - case MSG.CLOSEDSREQ: - _closeDsRequested = true; - if (!_inTransfer) - { - // this should be done on ui thread (or same one that enabled the ds) - DisableSource(); - } - break; - case MSG.DEVICEEVENT: - if (DeviceEvent != null && DGControl.DeviceEvent.Get(ref _appIdentity, ref _currentDS, out TW_DEVICEEVENT de) == TWRC.SUCCESS) - { - try + if (TWPlatform.IsMacOSX) { - DeviceEvent.Invoke(this, de); + cbPtr = Marshal.GetFunctionPointerForDelegate(_osxCallbackDelegate); } - catch { } - } - break; - } + else + { + cbPtr = Marshal.GetFunctionPointerForDelegate(_legacyCallbackDelegate); + } + + var rc = TWRC.FAILURE; + + // per the spec (pg 8-10), apps for 2.2 or higher uses callback2 so try this first + if (_appIdentity.ProtocolMajor > 2 || (_appIdentity.ProtocolMajor >= 2 && _appIdentity.ProtocolMinor >= 2)) + { + var cb2 = new TW_CALLBACK2 { CallBackProc = cbPtr }; + rc = DGControl.Callback2.RegisterCallback(ref _appIdentity, ref _currentDS, ref cb2); + } + if (rc != TWRC.SUCCESS) + { + // always try old callback + var cb = new TW_CALLBACK { CallBackProc = cbPtr }; + DGControl.Callback.RegisterCallback(ref _appIdentity, ref _currentDS, ref cb); + } + } + + private ushort LegacyCallbackHandler + ( + ref TW_IDENTITY_LEGACY origin, ref TW_IDENTITY_LEGACY dest, + DG dg, DAT dat, MSG msg, IntPtr twnull + ) + { + Debug.WriteLine($"Legacy callback got {msg}"); + HandleSourceMsg(msg); + return (ushort)TWRC.SUCCESS; + } + + private ushort OSXCallbackHandler + ( + ref TW_IDENTITY_MACOSX origin, ref TW_IDENTITY_MACOSX dest, + DG dg, DAT dat, MSG msg, IntPtr twnull + ) + { + Debug.WriteLine($"OSX callback got {msg}"); + HandleSourceMsg(msg); + return (ushort)TWRC.SUCCESS; + } + + + private void HandleSourceMsg(MSG msg, [CallerMemberName] string? caller = null) + { + Debug.WriteLine($"[thread {Environment.CurrentManagedThreadId}] {nameof(HandleSourceMsg)} called by {caller} at state {State} with {msg}."); + + // the reason we post these to the background is + // if they're coming from UI message loop then + // this needs to return asap + + switch (msg) + { + case MSG.XFERREADY: + if (_transferInCallbackThread) + { + EnterTransferRoutine(); + } + else + { + // use bg thread to process transfer. + // some sources spam this even during transfer so we gate it + if (!_inTransfer) + { + _inTransfer = true; + _xferReady.Set(); + //_bgPendingMsgs.Add(msg); + } + } + break; + case MSG.CLOSEDSOK: + case MSG.CLOSEDSREQ: + _closeDsRequested = true; + if (!_inTransfer) + { + // this should be done on ui thread (or same one that enabled the ds) + DisableSource(); + } + break; + case MSG.DEVICEEVENT: + if (DeviceEvent != null && DGControl.DeviceEvent.Get(ref _appIdentity, ref _currentDS, out TW_DEVICEEVENT de) == TWRC.SUCCESS) + { + try + { + DeviceEvent.Invoke(this, de); + } + catch { } + } + break; + } + } } - } } diff --git a/src/NTwain/TwainAppSession.Xfers.cs b/src/NTwain/TwainAppSession.Xfers.cs index 8ed36a6..319d6de 100644 --- a/src/NTwain/TwainAppSession.Xfers.cs +++ b/src/NTwain/TwainAppSession.Xfers.cs @@ -2,9 +2,11 @@ using NTwain.Native; using NTwain.Triplets; using System; +using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; +using System.Threading; namespace NTwain { @@ -161,7 +163,7 @@ namespace NTwain break; } } - HandleXferCode(ref sts, ref pending); + HandleXferCode(ref sts, ref pending, isEnd: false); } } catch (Exception ex) @@ -176,7 +178,7 @@ namespace NTwain } while (sts.RC == TWRC.SUCCESS && pending.Count != 0); } - HandleXferCode(ref sts, ref pending); + HandleXferCode(ref sts, ref pending, isEnd: true); if (State >= STATE.S5) { @@ -185,13 +187,17 @@ namespace NTwain _inTransfer = false; } - private void HandleXferCode(ref STS sts, ref TW_PENDINGXFERS pending) + private void HandleXferCode(ref STS sts, ref TW_PENDINGXFERS pending, bool isEnd) { switch (sts.RC) { case TWRC.SUCCESS: case TWRC.XFERDONE: // ok to keep going + if (isEnd) + { + //DGControl.PendingXfers.EndXfer(ref _appIdentity, ref _currentDS, ref pending); + } break; case TWRC.CANCEL: // might eventually have option to cancel this or all like transfer ready @@ -210,6 +216,18 @@ namespace NTwain // TODO: raise error event switch (sts.STATUS.ConditionCode) { + case TWCC.SEQERROR: + if (isEnd) + { + // special break down to state 5 + pending = TW_PENDINGXFERS.DONTCARE(); + sts = WrapInSTS(DGControl.PendingXfers.EndXfer(ref _appIdentity, ref _currentDS, ref pending)); + State = STATE.S6; + pending = TW_PENDINGXFERS.DONTCARE(); + sts = WrapInSTS(DGControl.PendingXfers.Reset(ref _appIdentity, ref _currentDS, ref pending)); + State = STATE.S5; + } + break; case TWCC.DAMAGEDCORNER: case TWCC.DOCTOODARK: case TWCC.DOCTOOLIGHT: @@ -245,6 +263,7 @@ namespace NTwain // and just start it var sts = WrapInSTS(DGAudio.AudioFileXfer.Get(ref _appIdentity, ref _currentDS)); + if (sts.RC == TWRC.XFERDONE) { State = STATE.S7; @@ -475,7 +494,11 @@ namespace NTwain // get what will be transferred DGControl.SetupFileXfer.Get(ref _appIdentity, ref _currentDS, out TW_SETUPFILEXFER fileSetup); // and just start it + + int tries = 0; + RETRY: var sts = WrapInSTS(DGImage.ImageFileXfer.Get(ref _appIdentity, ref _currentDS)); + if (sts.RC == TWRC.XFERDONE) { State = STATE.S7; @@ -494,6 +517,23 @@ namespace NTwain State = pending.Count == 0 ? STATE.S5 : STATE.S6; } } + else + { + // sometimes it errors only due to timing so wait a bit and try again + if (sts.RC == TWRC.FAILURE && (sts.ConditionCode == TWCC.None || sts.ConditionCode == TWCC.SEQERROR)) + { + if (tries++ < 3) + { + Debug.WriteLine($"Using fileXfer timing workaround try {tries}."); + Thread.Sleep(500); + goto RETRY; + } + } + else + { + Debugger.Break(); + } + } return sts; } diff --git a/src/NTwain/TwainAppSession.cs b/src/NTwain/TwainAppSession.cs index d8084b0..0e6a9cd 100644 --- a/src/NTwain/TwainAppSession.cs +++ b/src/NTwain/TwainAppSession.cs @@ -9,334 +9,344 @@ using System.Threading.Tasks; namespace NTwain { - // this file contains initialization/cleanup things. + // this file contains initialization/cleanup things. - public partial class TwainAppSession : IDisposable - { - /// - /// Creates TWAIN session with current app info. - /// - public TwainAppSession() - : this(new TW_IDENTITY_LEGACY(Environment.GetCommandLineArgs()[0])) { } - - /// - /// Creates TWAIN session with explicit app info. - /// - /// - public TwainAppSession(TW_IDENTITY_LEGACY appId) + public partial class TwainAppSession : IDisposable { + /// + /// Creates TWAIN session with current app info. + /// + public TwainAppSession() + : this(new TW_IDENTITY_LEGACY(Environment.GetCommandLineArgs()[0])) { } + + /// + /// Creates TWAIN session with explicit app info. + /// + /// + public TwainAppSession(TW_IDENTITY_LEGACY appId) + { #if WINDOWS || NETFRAMEWORK - DSM.DsmLoader.TryLoadCustomDSM(); + DSM.DsmLoader.TryLoadCustomDSM(); #endif - _appIdentity = appId; + _appIdentity = appId; - _legacyCallbackDelegate = LegacyCallbackHandler; - _osxCallbackDelegate = OSXCallbackHandler; + _legacyCallbackDelegate = LegacyCallbackHandler; + _osxCallbackDelegate = OSXCallbackHandler; - StartTransferThread(); - } + StartTransferThread(); + } - internal IntPtr _hwnd; - internal TW_USERINTERFACE _userInterface; // kept around for disable to use + internal IntPtr _hwnd; + internal TW_USERINTERFACE _userInterface; // kept around for disable to use #if WINDOWS || NETFRAMEWORK - MessagePumpThread? _selfPump; - TW_EVENT _procEvent; // kept here so the alloc/free only happens once + MessagePumpThread? _selfPump; + TW_EVENT _procEvent; // kept here so the alloc/free only happens once #endif - // test threads a bit - //readonly BlockingCollection _bgPendingMsgs = new(); - SynchronizationContext? _pumpThreadMarshaller; - bool _closeDsRequested; - bool _inTransfer; - readonly AutoResetEvent _xferReady = new(false); - private bool disposedValue; + // test threads a bit + //readonly BlockingCollection _bgPendingMsgs = new(); + SynchronizationContext? _pumpThreadMarshaller; + bool _closeDsRequested; + bool _inTransfer; + readonly AutoResetEvent _xferReady = new(false); + private bool disposedValue; + bool _transferInCallbackThread = true; - void StartTransferThread() - { - Thread t = new(TransferLoopLoop) - { - IsBackground = true - }; -#if WINDOWS || NETFRAMEWORK - t.SetApartmentState(ApartmentState.STA); // just in case -#endif - t.Start(); - } - - private void TransferLoopLoop(object? obj) - { - while (!disposedValue) - { - try + void StartTransferThread() { - _xferReady.WaitOne(); - } - catch (ObjectDisposedException) { break; } - try - { - EnterTransferRoutine(); - } - catch { } - } - } - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - // this will end the bg thread - _xferReady.Dispose(); - //_bgPendingMsgs.CompleteAdding(); - } -#if WINDOWS || NETFRAMEWORK - if (_procEvent.pEvent != IntPtr.Zero) Marshal.FreeHGlobal(_procEvent.pEvent); -#endif - disposedValue = true; - } - } - - // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources - // ~TwainSession() - // { - // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - // Dispose(disposing: false); - // } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - -#if WINDOWS || NETFRAMEWORK - /// - /// Loads and opens the TWAIN data source manager in a self-hosted message queue thread. - /// Highly experimental and only use if necessary. Must close with - /// if used. - /// - /// - public async Task OpenDSMAsync() - { - if (_selfPump == null) - { - var pump = new MessagePumpThread(); - var sts = await pump.AttachAsync(this); - if (sts.IsSuccess) - { - _selfPump = pump; - } - return sts; - } - return new STS { RC = TWRC.FAILURE, STATUS = new TW_STATUS { ConditionCode = TWCC.SEQERROR } }; - } - - /// - /// Closes the TWAIN data source manager if opened with . - /// - /// - /// - public async Task CloseDSMAsync() - { - if (_selfPump == null) throw new InvalidOperationException($"Cannot close if not opened with {nameof(OpenDSMAsync)}()."); - - var sts = await _selfPump.DetatchAsync(); - if (sts.IsSuccess) - { - _selfPump = null; - } - return sts; - } -#endif - - /// - /// Loads and opens the TWAIN data source manager. - /// - /// Required if on Windows. - /// Context for TWAIN to invoke certain actions on the thread that the hwnd lives on. - /// - public STS OpenDSM(IntPtr hwnd, SynchronizationContext uiThreadMarshaller) - { - var rc = DGControl.Parent.OpenDSM(ref _appIdentity, hwnd); - if (rc == TWRC.SUCCESS) - { - _hwnd = hwnd; - _pumpThreadMarshaller = uiThreadMarshaller; - State = STATE.S3; - // get default source - if (DGControl.Identity.GetDefault(ref _appIdentity, out TW_IDENTITY_LEGACY ds) == TWRC.SUCCESS) - { - _defaultDS = ds; - try - { - DefaultSourceChanged?.Invoke(this, _defaultDS); - } - catch { } - } - - // determine memory mgmt routines used - if (((DG)AppIdentity.SupportedGroups & DG.DSM2) == DG.DSM2) - { - DGControl.EntryPoint.Get(ref _appIdentity, out _entryPoint); - } - } - return WrapInSTS(rc, true); - } - - - /// - /// Closes the TWAIN data source manager. - /// - /// - /// - public STS CloseDSM() - { -#if WINDOWS || NETFRAMEWORK - if (_selfPump != null) throw new InvalidOperationException($"Cannot close if opened with {nameof(OpenDSMAsync)}()."); -#endif - return CloseDSMReal(); - } - - /// - /// Closes the TWAIN data source manager. - /// - /// - internal STS CloseDSMReal() - { - var rc = DGControl.Parent.CloseDSM(ref _appIdentity, _hwnd); - if (rc == TWRC.SUCCESS) - { - State = STATE.S2; - _entryPoint = default; - _defaultDS = default; - try - { - DefaultSourceChanged?.Invoke(this, _defaultDS); - } - catch { } - _hwnd = IntPtr.Zero; - _pumpThreadMarshaller = null; - } - return WrapInSTS(rc, true); - } - - /// - /// Wraps a return code with additional status if not successful. - /// Use this right after an API call to get its condition code. - /// - /// - /// true to get status for dsm operation error, false to get status for ds operation error, - /// - public STS WrapInSTS(TWRC rc, bool dsmOnly = false) - { - if (rc != TWRC.FAILURE) return new STS { RC = rc }; - var sts = new STS { RC = rc, STATUS = GetLastStatus(dsmOnly) }; - if (sts.STATUS.ConditionCode == TWCC.BADDEST) - { - // TODO: the current ds is bad, should assume we're back in S3? - // needs the dest parameter to find out. - } - else if (sts.STATUS.ConditionCode == TWCC.BUMMER) - { - // TODO: notify with critical event to end the twain stuff - } - return sts; - } - - /// - /// Gets the last status code if an operation did not return success. - /// This can only be done once after an error. - /// - /// true to get status for dsm operation error, false to get status for ds operation error, - /// - public TW_STATUS GetLastStatus(bool dsmOnly = false) - { - if (dsmOnly) - { - DGControl.Status.GetForDSM(ref _appIdentity, out TW_STATUS status); - return status; - } - else - { - DGControl.Status.GetForDS(ref _appIdentity, ref _currentDS, out TW_STATUS status); - return status; - } - } - - /// - /// Tries to get string representation of a previously gotten status - /// from if possible. - /// - /// - /// - public string? GetStatusText(TW_STATUS status) - { - if (DGControl.StatusUtf8.Get(ref _appIdentity, status, out TW_STATUSUTF8 extendedStatus) == TWRC.SUCCESS) - { - return extendedStatus.Read(this); - } - return null; - } - - /// - /// Tries to bring the TWAIN session down to some state. - /// - /// - /// The final state. - public STATE TryStepdown(STATE targetState) - { - int tries = 0; - while (State > targetState) - { - var oldState = State; - - switch (oldState) - { - // todo: finish - case STATE.S7: - case STATE.S6: - break; - case STATE.S5: - DisableSource(); - break; - case STATE.S4: - CloseSource(); - break; - case STATE.S3: -#if WINDOWS || NETFRAMEWORK - if (_selfPump != null) + Thread t = new(TransferLoopLoop) { - try - { - _ = CloseDSMAsync(); - } - catch (InvalidOperationException) { } + IsBackground = true + }; +#if WINDOWS || NETFRAMEWORK + t.SetApartmentState(ApartmentState.STA); // just in case +#endif + t.Start(); + } + + private void TransferLoopLoop(object? obj) + { + while (!disposedValue) + { + try + { + _xferReady.WaitOne(); + } + catch (ObjectDisposedException) { break; } + try + { + EnterTransferRoutine(); + } + catch { } + } + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // this will end the bg thread + _xferReady.Dispose(); + //_bgPendingMsgs.CompleteAdding(); + } +#if WINDOWS || NETFRAMEWORK + if (_procEvent.pEvent != IntPtr.Zero) Marshal.FreeHGlobal(_procEvent.pEvent); +#endif + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~TwainSession() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + +#if WINDOWS || NETFRAMEWORK + /// + /// Loads and opens the TWAIN data source manager in a self-hosted message queue thread. + /// Must close with + /// if used. + /// + /// + public async Task OpenDSMAsync() + { + if (_selfPump == null) + { + _transferInCallbackThread = true; + var pump = new MessagePumpThread(); + var sts = await pump.AttachAsync(this); + if (sts.IsSuccess) + { + _selfPump = pump; + } + return sts; + } + return new STS { RC = TWRC.FAILURE, STATUS = new TW_STATUS { ConditionCode = TWCC.SEQERROR } }; + } + + /// + /// Closes the TWAIN data source manager if opened with . + /// + /// + /// + public async Task CloseDSMAsync() + { + if (_selfPump == null) throw new InvalidOperationException($"Cannot close if not opened with {nameof(OpenDSMAsync)}()."); + + var sts = await _selfPump.DetatchAsync(); + if (sts.IsSuccess) + { + _selfPump = null; + } + return sts; + } +#endif + + /// + /// Loads and opens the TWAIN data source manager. + /// + /// Required if on Windows. + /// Context for TWAIN to invoke certain actions on the thread that the hwnd lives on. + /// + public STS OpenDSM(IntPtr hwnd, SynchronizationContext uiThreadMarshaller) + { + var rc = DGControl.Parent.OpenDSM(ref _appIdentity, hwnd); + if (rc == TWRC.SUCCESS) + { + _transferInCallbackThread = true; + _hwnd = hwnd; + _pumpThreadMarshaller = uiThreadMarshaller; + State = STATE.S3; + // get default source + if (DGControl.Identity.GetDefault(ref _appIdentity, out TW_IDENTITY_LEGACY ds) == TWRC.SUCCESS) + { + _defaultDS = ds; + try + { + DefaultSourceChanged?.Invoke(this, _defaultDS); + } + catch { } + } + + // determine memory mgmt routines used + if (((DG)AppIdentity.SupportedGroups & DG.DSM2) == DG.DSM2) + { + DGControl.EntryPoint.Get(ref _appIdentity, out _entryPoint); + } + } + return WrapInSTS(rc, true); + } + + + /// + /// Closes the TWAIN data source manager. + /// + /// + /// + public STS CloseDSM() + { +#if WINDOWS || NETFRAMEWORK + if (_selfPump != null) throw new InvalidOperationException($"Cannot close if opened with {nameof(OpenDSMAsync)}()."); +#endif + return CloseDSMReal(); + } + + /// + /// Closes the TWAIN data source manager. + /// + /// + internal STS CloseDSMReal() + { + var rc = DGControl.Parent.CloseDSM(ref _appIdentity, _hwnd); + if (rc == TWRC.SUCCESS) + { + State = STATE.S2; + _entryPoint = default; + _defaultDS = default; + try + { + DefaultSourceChanged?.Invoke(this, _defaultDS); + } + catch { } + _hwnd = IntPtr.Zero; + _pumpThreadMarshaller = null; + } + return WrapInSTS(rc, true); + } + + /// + /// Wraps a return code with additional status if not successful. + /// Use this right after an API call to get its condition code. + /// + /// + /// true to get status for dsm operation error, false to get status for ds operation error, + /// + public STS WrapInSTS(TWRC rc, bool dsmOnly = false) + { + if (rc != TWRC.FAILURE) return new STS { RC = rc }; + var sts = new STS { RC = rc, STATUS = GetLastStatus(dsmOnly) }; + if (sts.STATUS.ConditionCode == TWCC.BADDEST) + { + // TODO: the current ds is bad, should assume we're back in S3? + // needs the dest parameter to find out. + } + else if (sts.STATUS.ConditionCode == TWCC.BUMMER) + { + // TODO: notify with critical event to end the twain stuff + } + return sts; + } + + /// + /// Gets the last status code if an operation did not return success. + /// This can only be done once after an error. + /// + /// true to get status for dsm operation error, false to get status for ds operation error, + /// + public TW_STATUS GetLastStatus(bool dsmOnly = false) + { + if (dsmOnly) + { + DGControl.Status.GetForDSM(ref _appIdentity, out TW_STATUS status); + return status; } else { - CloseDSM(); + DGControl.Status.GetForDS(ref _appIdentity, ref _currentDS, out TW_STATUS status); + return status; } -#else - CloseDSM(); -#endif - break; - case STATE.S2: - // can't really go lower - if (targetState < STATE.S2) - { - return State; - } - break; } - if (oldState == State) + + /// + /// Tries to get string representation of a previously gotten status + /// from if possible. + /// + /// + /// + public string? GetStatusText(TW_STATUS status) { - // didn't work - if (tries++ > 5) break; + if (DGControl.StatusUtf8.Get(ref _appIdentity, status, out TW_STATUSUTF8 extendedStatus) == TWRC.SUCCESS) + { + return extendedStatus.Read(this); + } + return null; + } + + /// + /// Tries to bring the TWAIN session down to some state. + /// + /// + /// The final state. + public STATE TryStepdown(STATE targetState) + { + int tries = 0; + while (State > targetState) + { + var oldState = State; + + switch (oldState) + { + // todo: finish + case STATE.S7: + var pending = TW_PENDINGXFERS.DONTCARE(); + DGControl.PendingXfers.EndXfer(ref _appIdentity, ref _currentDS, ref pending); + _state = STATE.S6; + break; + case STATE.S6: + pending = TW_PENDINGXFERS.DONTCARE(); + DGControl.PendingXfers.Reset(ref _appIdentity, ref _currentDS, ref pending); + _state = STATE.S5; + break; + case STATE.S5: + DisableSource(); + break; + case STATE.S4: + CloseSource(); + break; + case STATE.S3: +#if WINDOWS || NETFRAMEWORK + if (_selfPump != null) + { + try + { + _ = CloseDSMAsync(); + } + catch (InvalidOperationException) { } + } + else + { + CloseDSM(); + } +#else + CloseDSM(); +#endif + break; + case STATE.S2: + // can't really go lower + if (targetState < STATE.S2) + { + return State; + } + break; + } + if (oldState == State) + { + // didn't work + if (tries++ > 5) break; + } + } + return State; } - } - return State; } - } }