diff --git a/NAPS2.Lib.Common/Modules/CommonModule.cs b/NAPS2.Lib.Common/Modules/CommonModule.cs index 5216c8b21..e256ffbf9 100644 --- a/NAPS2.Lib.Common/Modules/CommonModule.cs +++ b/NAPS2.Lib.Common/Modules/CommonModule.cs @@ -10,6 +10,7 @@ using NAPS2.Ocr; using NAPS2.Operation; using NAPS2.Scan; using NAPS2.Images; +using NAPS2.ImportExport.Email.Mapi; using NAPS2.Scan.Sane; using NAPS2.Scan.Twain; using NAPS2.Scan.Wia; @@ -36,6 +37,7 @@ namespace NAPS2.DI.Modules Bind().To(); Bind().To(); Bind().To(); + Bind().To(); Bind().ToMethod(ctx => OcrEngineManager.Default); Bind().ToSelf().InSingletonScope(); @@ -50,6 +52,7 @@ namespace NAPS2.DI.Modules Bind().To().InSingletonScope().Named(TwainScanDriver.DRIVER_NAME); Bind().To().InSingletonScope().Named(SaneScanDriver.DRIVER_NAME); Bind().To().InSingletonScope().Named(ProxiedScanDriver.DRIVER_NAME); + Bind().To(); // Config Bind().ToSelf().InSingletonScope(); diff --git a/NAPS2.Sdk.Tests/NAPS2.Sdk.Tests.csproj b/NAPS2.Sdk.Tests/NAPS2.Sdk.Tests.csproj index d5b712ff3..d3a6a8d2a 100644 --- a/NAPS2.Sdk.Tests/NAPS2.Sdk.Tests.csproj +++ b/NAPS2.Sdk.Tests/NAPS2.Sdk.Tests.csproj @@ -41,6 +41,12 @@ ..\packages\Castle.Core.4.3.1\lib\net45\Castle.Core.dll + + ..\packages\Grpc.Core.1.19.0\lib\net45\Grpc.Core.dll + + + ..\packages\Grpc.Core.Api.1.19.0\lib\net45\Grpc.Core.Api.dll + ..\packages\Moq.4.10.1\lib\net45\Moq.dll @@ -52,6 +58,9 @@ + + ..\packages\System.Interactive.Async.3.2.0\lib\net46\System.Interactive.Async.dll + ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.0\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll @@ -98,6 +107,7 @@ + @@ -125,6 +135,8 @@ + + \ No newline at end of file diff --git a/NAPS2.Sdk.Tests/Worker/WorkerChannelTests.cs b/NAPS2.Sdk.Tests/Worker/WorkerChannelTests.cs new file mode 100644 index 000000000..cfc26aa70 --- /dev/null +++ b/NAPS2.Sdk.Tests/Worker/WorkerChannelTests.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Grpc.Core; +using Moq; +using NAPS2.Images; +using NAPS2.ImportExport.Email.Mapi; +using NAPS2.Scan; +using NAPS2.Scan.Twain; +using NAPS2.Worker; +using Xunit; + +namespace NAPS2.Sdk.Tests.Worker +{ + public class WorkerChannelTests + { + private Channel Start() + { + var twainWrapper = new Mock(); + var thumbnailRenderer = new ThumbnailRenderer(); + var mapiWrapper = new Mock(); + Server server = new Server + { + Services = { GrpcWorkerService.BindService(new GrpcWorkerServiceImpl(twainWrapper.Object, thumbnailRenderer, mapiWrapper.Object)) }, + Ports = { new ServerPort("localhost", 0, ServerCredentials.Insecure) } + }; + server.Start(); + var client = new GrpcWorkerServiceAdapter(server.Ports.First().BoundPort); + return new Channel + { + Server = server, + Client = client, + TwainWrapper = twainWrapper, + ThumbnailRenderer = thumbnailRenderer, + MapiWrapper = mapiWrapper + }; + } + + [Fact] + public void Init() + { + using (var channel = Start()) + { + // TODO: When init actually does something, do a better test + channel.Client.Init(""); + } + } + + [Fact] + public void TwainGetDeviceList() + { + using (var channel = Start()) + { + channel.TwainWrapper + .Setup(tw => tw.GetDeviceList(TwainImpl.OldDsm)) + .Returns(new List { new ScanDevice("test_id", "test_name") }); + + var deviceList = channel.Client.TwainGetDeviceList(TwainImpl.OldDsm); + + Assert.Single(deviceList); + Assert.Equal("test_id", deviceList[0].ID); + Assert.Equal("test_name", deviceList[0].Name); + channel.TwainWrapper.Verify(tw => tw.GetDeviceList(TwainImpl.OldDsm)); + channel.TwainWrapper.VerifyNoOtherCalls(); + } + } + + private class Channel : IDisposable + { + public Server Server { get; set; } + + public GrpcWorkerServiceAdapter Client { get; set; } + + public Mock TwainWrapper { get; set; } + + public ThumbnailRenderer ThumbnailRenderer { get; set; } + + public Mock MapiWrapper { get; set; } + + public void Dispose() + { + Server.KillAsync(); + } + } + } +} diff --git a/NAPS2.Sdk.Tests/packages.config b/NAPS2.Sdk.Tests/packages.config index 82ba4fad5..03b362c81 100644 --- a/NAPS2.Sdk.Tests/packages.config +++ b/NAPS2.Sdk.Tests/packages.config @@ -2,7 +2,10 @@ + + + diff --git a/NAPS2.Sdk/Images/Storage/GdiConverters.cs b/NAPS2.Sdk/Images/Storage/GdiConverters.cs index 3aca5c25b..6754c3769 100644 --- a/NAPS2.Sdk/Images/Storage/GdiConverters.cs +++ b/NAPS2.Sdk/Images/Storage/GdiConverters.cs @@ -38,6 +38,7 @@ namespace NAPS2.Images.Storage // TODO: Better format choice? var format = convertParams.Lossless ? ImageFormat.Png : ImageFormat.Jpeg; input.Bitmap.Save(stream, format); + stream.Seek(0, SeekOrigin.Begin); return new MemoryStreamStorage(stream); } } diff --git a/NAPS2.Sdk/ImportExport/Email/Mapi/IMapiWrapper.cs b/NAPS2.Sdk/ImportExport/Email/Mapi/IMapiWrapper.cs new file mode 100644 index 000000000..e773e6c5c --- /dev/null +++ b/NAPS2.Sdk/ImportExport/Email/Mapi/IMapiWrapper.cs @@ -0,0 +1,7 @@ +namespace NAPS2.ImportExport.Email.Mapi +{ + public interface IMapiWrapper + { + MapiSendMailReturnCode SendEmail(EmailMessage message); + } +} \ No newline at end of file diff --git a/NAPS2.Sdk/ImportExport/Email/Mapi/MapiEmailProvider.cs b/NAPS2.Sdk/ImportExport/Email/Mapi/MapiEmailProvider.cs index bf91ee648..cd5cd462e 100644 --- a/NAPS2.Sdk/ImportExport/Email/Mapi/MapiEmailProvider.cs +++ b/NAPS2.Sdk/ImportExport/Email/Mapi/MapiEmailProvider.cs @@ -14,10 +14,10 @@ namespace NAPS2.ImportExport.Email.Mapi public class MapiEmailProvider : IEmailProvider { private readonly IWorkerServiceFactory workerServiceFactory; - private readonly MapiWrapper mapiWrapper; + private readonly IMapiWrapper mapiWrapper; private readonly ErrorOutput errorOutput; - public MapiEmailProvider(IWorkerServiceFactory workerServiceFactory, MapiWrapper mapiWrapper, ErrorOutput errorOutput) + public MapiEmailProvider(IWorkerServiceFactory workerServiceFactory, IMapiWrapper mapiWrapper, ErrorOutput errorOutput) { this.workerServiceFactory = workerServiceFactory; this.mapiWrapper = mapiWrapper; diff --git a/NAPS2.Sdk/ImportExport/Email/Mapi/MapiWrapper.cs b/NAPS2.Sdk/ImportExport/Email/Mapi/MapiWrapper.cs index 2375fbaa2..dc2750a2b 100644 --- a/NAPS2.Sdk/ImportExport/Email/Mapi/MapiWrapper.cs +++ b/NAPS2.Sdk/ImportExport/Email/Mapi/MapiWrapper.cs @@ -6,7 +6,7 @@ using NAPS2.Util; namespace NAPS2.ImportExport.Email.Mapi { - public class MapiWrapper + public class MapiWrapper : IMapiWrapper { private readonly SystemEmailClients systemEmailClients; diff --git a/NAPS2.Sdk/NAPS2.Sdk.csproj b/NAPS2.Sdk/NAPS2.Sdk.csproj index 255e74bff..bcaca85ce 100644 --- a/NAPS2.Sdk/NAPS2.Sdk.csproj +++ b/NAPS2.Sdk/NAPS2.Sdk.csproj @@ -188,6 +188,7 @@ + @@ -255,6 +256,7 @@ + @@ -290,6 +292,7 @@ + @@ -342,11 +345,7 @@ - - - - True True @@ -453,7 +452,6 @@ - diff --git a/NAPS2.Sdk/Scan/ScanDriverBase.cs b/NAPS2.Sdk/Scan/ScanDriverBase.cs index fe493a118..81b5bbad2 100644 --- a/NAPS2.Sdk/Scan/ScanDriverBase.cs +++ b/NAPS2.Sdk/Scan/ScanDriverBase.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.ServiceModel; using System.Threading; using System.Threading.Tasks; using NAPS2.Config; @@ -12,7 +11,6 @@ using NAPS2.Lang.Resources; using NAPS2.Logging; using NAPS2.Util; using NAPS2.WinForms; -using NAPS2.Worker; namespace NAPS2.Scan { @@ -138,10 +136,11 @@ namespace NAPS2.Scan { error = e; } - catch (FaultException e) - { - error = e.Detail.Exception; - } + // TODO + //catch (FaultException e) + //{ + // error = e.Detail.Exception; + //} catch (Exception e) { error = new ScanDriverUnknownException(e); diff --git a/NAPS2.Sdk/Scan/Twain/ITwainWrapper.cs b/NAPS2.Sdk/Scan/Twain/ITwainWrapper.cs new file mode 100644 index 000000000..d7011471b --- /dev/null +++ b/NAPS2.Sdk/Scan/Twain/ITwainWrapper.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using NAPS2.Images; + +namespace NAPS2.Scan.Twain +{ + public interface ITwainWrapper + { + List GetDeviceList(TwainImpl twainImpl); + + void Scan(IntPtr dialogParent, ScanDevice scanDevice, ScanProfile scanProfile, ScanParams scanParams, + CancellationToken cancelToken, ScannedImageSink sink, Action runBackgroundOcr); + } +} diff --git a/NAPS2.Sdk/Scan/Twain/TwainScanDriver.cs b/NAPS2.Sdk/Scan/Twain/TwainScanDriver.cs index a75b2a424..a30d659f7 100644 --- a/NAPS2.Sdk/Scan/Twain/TwainScanDriver.cs +++ b/NAPS2.Sdk/Scan/Twain/TwainScanDriver.cs @@ -15,7 +15,7 @@ namespace NAPS2.Scan.Twain { public const string DRIVER_NAME = "twain"; - private readonly TwainWrapper twainWrapper; + private readonly ITwainWrapper twainWrapper; private readonly ScannedImageHelper scannedImageHelper; public TwainScanDriver() : base(ErrorOutput.Default, new AutoSaver()) @@ -24,7 +24,7 @@ namespace NAPS2.Scan.Twain scannedImageHelper = new ScannedImageHelper(); } - public TwainScanDriver(TwainWrapper twainWrapper, ScannedImageHelper scannedImageHelper, ErrorOutput errorOutput, AutoSaver autoSaver) : base(errorOutput, autoSaver) + public TwainScanDriver(ITwainWrapper twainWrapper, ScannedImageHelper scannedImageHelper, ErrorOutput errorOutput, AutoSaver autoSaver) : base(errorOutput, autoSaver) { this.twainWrapper = twainWrapper; this.scannedImageHelper = scannedImageHelper; @@ -63,13 +63,11 @@ namespace NAPS2.Scan.Twain { using (var worker = WorkerManager.Factory.Create()) { - worker.Callback.ImageCallback += (img, tempPath) => + await worker.Service.TwainScan(scanDevice, scanProfile, scanParams, dialogParent, cancelToken, (img, tempPath) => { if (tempPath != null) scannedImageHelper.RunBackgroundOcr(img, scanParams, tempPath); sink.PutImage(img); - }; - cancelToken.Register(worker.Service.CancelTwainScan); - await worker.Service.TwainScan(scanDevice, scanProfile, scanParams, dialogParent); + }); } } else diff --git a/NAPS2.Sdk/Scan/Twain/TwainWrapper.cs b/NAPS2.Sdk/Scan/Twain/TwainWrapper.cs index 9782d2cee..ba111adad 100644 --- a/NAPS2.Sdk/Scan/Twain/TwainWrapper.cs +++ b/NAPS2.Sdk/Scan/Twain/TwainWrapper.cs @@ -18,7 +18,7 @@ using NAPS2.Util; namespace NAPS2.Scan.Twain { - public class TwainWrapper + public class TwainWrapper : ITwainWrapper { private static readonly TWIdentity TwainAppId = TWIdentity.CreateFromAssembly(DataGroups.Image | DataGroups.Control, Assembly.GetEntryAssembly()); diff --git a/NAPS2.Sdk/Worker/GrpcHelper.cs b/NAPS2.Sdk/Worker/GrpcHelper.cs new file mode 100644 index 000000000..5aee4f03e --- /dev/null +++ b/NAPS2.Sdk/Worker/GrpcHelper.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using NAPS2.Util; + +namespace NAPS2.Worker +{ + public static class GrpcHelper + { + public static void HandleErrors(Error error) + { + if (!string.IsNullOrEmpty(error?.Xml)) + { + var knownExceptionTypes = new Type[] { }; // TODO + var exception = error.Xml.FromXml(); + exception.PreserveStackTrace(); + throw exception; + } + } + + public static Task WrapFunc(Func action, Func error) => + Task.Run(() => + { + try + { + return action(); + } + catch (Exception e) + { + return error(new Error { Xml = e.ToXml() }); + } + }); + + public static Task WrapFunc(Func> action, Func error) => + Task.Run(async () => + { + try + { + return await action(); + } + catch (Exception e) + { + return error(new Error { Xml = e.ToXml() }); + } + }); + + public static Task WrapAction(Action action, Action error) => + Task.Run(() => + { + try + { + action(); + } + catch (Exception e) + { + error(new Error { Xml = e.ToXml() }); + } + }); + } +} diff --git a/NAPS2.Sdk/Worker/GrpcWorkerServiceAdapter.cs b/NAPS2.Sdk/Worker/GrpcWorkerServiceAdapter.cs index 610050d1d..9d4deb1d8 100644 --- a/NAPS2.Sdk/Worker/GrpcWorkerServiceAdapter.cs +++ b/NAPS2.Sdk/Worker/GrpcWorkerServiceAdapter.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Grpc.Core; using NAPS2.Images; @@ -12,38 +13,83 @@ using NAPS2.Util; namespace NAPS2.Worker { - class GrpcWorkerServiceAdapter : IWorkerService + public class GrpcWorkerServiceAdapter { - private int port; + private GrpcWorkerService.GrpcWorkerServiceClient client; public GrpcWorkerServiceAdapter(int port) { - this.port = port; + this.client = new GrpcWorkerService.GrpcWorkerServiceClient(new Channel("localhost", port, ChannelCredentials.Insecure)); + } + public void Init(string recoveryFolderPath) + { + var req = new InitRequest { RecoveryFolderPath = recoveryFolderPath }; + var resp = client.Init(req); + GrpcHelper.HandleErrors(resp.Error); } - public WiaConfiguration Wia10NativeUI(string scanDevice, IntPtr hwnd) => throw new NotImplementedException(); + public WiaConfiguration Wia10NativeUI(string scanDevice, IntPtr hwnd) + { + var req = new Wia10NativeUiRequest + { + DeviceId = scanDevice, + Hwnd = (ulong)hwnd + }; + var resp = client.Wia10NativeUi(req); + GrpcHelper.HandleErrors(resp.Error); + return resp.WiaConfigurationXml.FromXml(); + } public List TwainGetDeviceList(TwainImpl twainImpl) { - var client = new GrpcWorkerService.GrpcWorkerServiceClient(new Channel("localhost", port, ChannelCredentials.Insecure)); - var response = client.TwainGetDeviceList(new TwainGetDeviceListRequest { TwainImpl = twainImpl.ToXml() }); - return response.DeviceListXml.FromXml>(); + var req = new TwainGetDeviceListRequest { TwainImpl = twainImpl.ToXml() }; + var resp = client.TwainGetDeviceList(req); + GrpcHelper.HandleErrors(resp.Error); + return resp.DeviceListXml.FromXml>(); } - public Task TwainScan(ScanDevice scanDevice, ScanProfile scanProfile, ScanParams scanParams, IntPtr hwnd) => throw new NotImplementedException(); - - public void CancelTwainScan() + public async Task TwainScan(ScanDevice scanDevice, ScanProfile scanProfile, ScanParams scanParams, IntPtr hwnd, CancellationToken cancelToken, Action imageCallback) { - throw new NotImplementedException(); + var req = new TwainScanRequest + { + ScanDeviceXml = scanDevice.ToXml(), + ScanProfileXml = scanProfile.ToXml(), + ScanParamsXml = scanParams.ToXml(), + Hwnd = (ulong)hwnd + }; + var streamingCall = client.TwainScan(req, cancellationToken: cancelToken); + while (await streamingCall.ResponseStream.MoveNext()) + { + var resp = streamingCall.ResponseStream.Current; + GrpcHelper.HandleErrors(resp.Error); + // TODO + //var scannedImage = new ScannedImage(image); + //if (thumbnail != null) + //{ + // scannedImage.SetThumbnail(StorageManager.MemoryStorageFactory.Decode(new MemoryStream(thumbnail), ".bmp")); + //} + //ImageCallback?.Invoke(scannedImage, tempImageFilePath); + } } - public MapiSendMailReturnCode SendMapiEmail(EmailMessage message) => throw new NotImplementedException(); - - public byte[] RenderThumbnail(ScannedImage.Snapshot snapshot, int size) => throw new NotImplementedException(); - - public void Init(string recoveryFolderPath) + public MapiSendMailReturnCode SendMapiEmail(EmailMessage message) { - //throw new NotImplementedException(); + var req = new SendMapiEmailRequest { EmailMessageXml = message.ToXml() }; + var resp = client.SendMapiEmail(req); + GrpcHelper.HandleErrors(resp.Error); + return resp.ReturnCodeXml.FromXml(); + } + + public byte[] RenderThumbnail(ScannedImage.Snapshot snapshot, int size) + { + var req = new RenderThumbnailRequest + { + SnapshotXml = snapshot.ToXml(), + Size = size + }; + var resp = client.RenderThumbnail(req); + GrpcHelper.HandleErrors(resp.Error); + return resp.Thumbnail.ToByteArray(); } } } \ No newline at end of file diff --git a/NAPS2.Sdk/Worker/GrpcWorkerServiceImpl.cs b/NAPS2.Sdk/Worker/GrpcWorkerServiceImpl.cs index 6af2a0c2c..88de7869d 100644 --- a/NAPS2.Sdk/Worker/GrpcWorkerServiceImpl.cs +++ b/NAPS2.Sdk/Worker/GrpcWorkerServiceImpl.cs @@ -1,40 +1,144 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Google.Protobuf; using Grpc.Core; using NAPS2.Images; +using NAPS2.Images.Storage; using NAPS2.ImportExport.Email; using NAPS2.ImportExport.Email.Mapi; using NAPS2.Scan; using NAPS2.Scan.Twain; using NAPS2.Scan.Wia; +using NAPS2.Scan.Wia.Native; using NAPS2.Util; namespace NAPS2.Worker { public class GrpcWorkerServiceImpl : GrpcWorkerService.GrpcWorkerServiceBase { - private readonly TwainWrapper twainWrapper; + private readonly ITwainWrapper twainWrapper; private readonly ThumbnailRenderer thumbnailRenderer; - private readonly MapiWrapper mapiWrapper; + private readonly IMapiWrapper mapiWrapper; private CancellationTokenSource twainScanCts = new CancellationTokenSource(); - public GrpcWorkerServiceImpl(TwainWrapper twainWrapper, ThumbnailRenderer thumbnailRenderer, MapiWrapper mapiWrapper) + public GrpcWorkerServiceImpl(ITwainWrapper twainWrapper, ThumbnailRenderer thumbnailRenderer, IMapiWrapper mapiWrapper) { this.twainWrapper = twainWrapper; this.thumbnailRenderer = thumbnailRenderer; this.mapiWrapper = mapiWrapper; } - public override async Task TwainGetDeviceList(TwainGetDeviceListRequest request, ServerCallContext context) + public override Task Init(InitRequest request, ServerCallContext context) => + GrpcHelper.WrapFunc( + () => new InitResponse(), + err => new InitResponse { Error = err }); + + public override Task Wia10NativeUi(Wia10NativeUiRequest request, ServerCallContext context) => + GrpcHelper.WrapFunc( + () => + { + try + { + using (var deviceManager = new WiaDeviceManager(WiaVersion.Wia10)) + using (var device = deviceManager.FindDevice(request.DeviceId)) + { + var item = device.PromptToConfigure((IntPtr)request.Hwnd); + return new Wia10NativeUiResponse + { + WiaConfigurationXml = new WiaConfiguration + { + DeviceProps = device.Properties.SerializeEditable(), + ItemProps = item.Properties.SerializeEditable(), + ItemName = item.Name() + }.ToXml() + }; + } + } + catch (WiaException e) + { + WiaScanErrors.ThrowDeviceError(e); + throw new InvalidOperationException(); + } + }, + err => new Wia10NativeUiResponse { Error = err }); + + public override Task TwainGetDeviceList(TwainGetDeviceListRequest request, ServerCallContext context) => + GrpcHelper.WrapFunc( + () => new TwainGetDeviceListResponse + { + DeviceListXml = twainWrapper.GetDeviceList(request.TwainImpl.FromXml()).ToXml() + }, + err => new TwainGetDeviceListResponse { Error = err }); + + public override Task TwainScan(TwainScanRequest request, IServerStreamWriter responseStream, ServerCallContext context) => + GrpcHelper.WrapAction( + () => + { + var imagePathDict = new Dictionary(); + twainWrapper.Scan( + (IntPtr)request.Hwnd, + request.ScanDeviceXml.FromXml(), + request.ScanProfileXml.FromXml(), + request.ScanParamsXml.FromXml(), + context.CancellationToken, + new WorkerImageSink(responseStream, imagePathDict), + (img, _, path) => imagePathDict.Add(img, path)); + }, err => responseStream.WriteAsync(new TwainScanResponse { Error = err })); + + public override Task SendMapiEmail(SendMapiEmailRequest request, ServerCallContext context) => + GrpcHelper.WrapFunc( + () => new SendMapiEmailResponse + { + ReturnCodeXml = mapiWrapper.SendEmail(request.EmailMessageXml.FromXml()).ToXml() + }, + err => new SendMapiEmailResponse { Error = err }); + + public override Task RenderThumbnail(RenderThumbnailRequest request, ServerCallContext context) => + GrpcHelper.WrapFunc( + async () => + { + var thumbnail = await thumbnailRenderer.Render(request.SnapshotXml.FromXml(), request.Size); + var stream = StorageManager.Convert(thumbnail, new StorageConvertParams { Lossless = true }).Stream; + return new RenderThumbnailResponse + { + Thumbnail = ByteString.FromStream(stream) + }; + }, + err => new RenderThumbnailResponse { Error = err }); + + private class WorkerImageSink : ScannedImageSink { - return await Task.Run(() => new TwainGetDeviceListResponse + private readonly IServerStreamWriter callback; + private readonly Dictionary imagePathDict; + + public WorkerImageSink(IServerStreamWriter callback, Dictionary imagePathDict) { - DeviceListXml = twainWrapper.GetDeviceList(request.TwainImpl.FromXml()).ToXml() - }); + this.callback = callback; + this.imagePathDict = imagePathDict; + } + + public override void PutImage(ScannedImage image) + { + // TODO: Ideally this shouldn't be inheriting ScannedImageSink, some other cleaner mechanism + + // TODO + MemoryStream stream = null; + var thumb = image.GetThumbnail(); + if (thumb != null) + { + stream = StorageManager.Convert(thumb, new StorageConvertParams { Lossless = true }).Stream; + } + //callback.WriteAsync(new TwainScanResponse + //{ + // RecoveryIndexImageXml = image. + //}); + //callback.TwainImageReceived(image.RecoveryIndexImage, stream?.ToArray(), imagePathDict.Get(image)); + } } } } \ No newline at end of file diff --git a/NAPS2.Sdk/Worker/IWorkerCallback.cs b/NAPS2.Sdk/Worker/IWorkerCallback.cs deleted file mode 100644 index d80740054..000000000 --- a/NAPS2.Sdk/Worker/IWorkerCallback.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.ServiceModel; -using NAPS2.Recovery; -using NAPS2.Images; - -namespace NAPS2.Worker -{ - [ServiceContract] - public interface IWorkerCallback - { - [OperationContract(IsOneWay = true)] - void TwainImageReceived(RecoveryIndexImage image, byte[] thumbnail, string tempImageFilePath); - - event Action ImageCallback; - } -} diff --git a/NAPS2.Sdk/Worker/IWorkerService.cs b/NAPS2.Sdk/Worker/IWorkerService.cs deleted file mode 100644 index 9ee160233..000000000 --- a/NAPS2.Sdk/Worker/IWorkerService.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.ServiceModel; -using System.Threading.Tasks; -using NAPS2.ImportExport.Email; -using NAPS2.ImportExport.Email.Mapi; -using NAPS2.Scan; -using NAPS2.Images; -using NAPS2.Scan.Wia; - -namespace NAPS2.Worker -{ - /// - /// The WCF service interface for NAPS2.Worker.exe. - /// - [ServiceContract(CallbackContract = typeof(IWorkerCallback))] - public interface IWorkerService - { - [OperationContract] - void Init(string recoveryFolderPath); - - [FaultContract(typeof(ScanDriverExceptionDetail))] - [OperationContract] - WiaConfiguration Wia10NativeUI(string scanDevice, IntPtr hwnd); - - [OperationContract] - List TwainGetDeviceList(TwainImpl twainImpl); - - [FaultContract(typeof(ScanDriverExceptionDetail))] - [OperationContract] - Task TwainScan(ScanDevice scanDevice, ScanProfile scanProfile, ScanParams scanParams, IntPtr hwnd); - - [OperationContract] - void CancelTwainScan(); - - [OperationContract] - MapiSendMailReturnCode SendMapiEmail(EmailMessage message); - - [OperationContract] - byte[] RenderThumbnail(ScannedImage.Snapshot snapshot, int size); - } -} diff --git a/NAPS2.Sdk/Worker/ScanDriverExceptionDetail.cs b/NAPS2.Sdk/Worker/ScanDriverExceptionDetail.cs deleted file mode 100644 index 4e135de11..000000000 --- a/NAPS2.Sdk/Worker/ScanDriverExceptionDetail.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.Serialization; -using NAPS2.Scan.Exceptions; -using NAPS2.Util; - -namespace NAPS2.Worker -{ - public class ScanDriverExceptionDetail - { - public ScanDriverExceptionDetail() - { - } - - public ScanDriverExceptionDetail(ScanDriverException e) - { - var stream = new MemoryStream(); - new NetDataContractSerializer().Serialize(stream, e); - SerializedException = stream.ToArray(); - } - - public byte[] SerializedException { get; set; } - - public ScanDriverException Exception => (ScanDriverException)new NetDataContractSerializer().Deserialize(new MemoryStream(SerializedException)); - - public void Throw() - { - var exception = Exception; - exception.PreserveStackTrace(); - throw exception; - } - } -} diff --git a/NAPS2.Sdk/Worker/WorkerCallback.cs b/NAPS2.Sdk/Worker/WorkerCallback.cs deleted file mode 100644 index 06b3ad944..000000000 --- a/NAPS2.Sdk/Worker/WorkerCallback.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NAPS2.Recovery; -using NAPS2.Images; - -namespace NAPS2.Worker -{ - public class WorkerCallback : IWorkerCallback - { - public event Action ImageCallback; - - public void TwainImageReceived(RecoveryIndexImage image, byte[] thumbnail, string tempImageFilePath) - { - // TODO - //var scannedImage = new ScannedImage(image); - //if (thumbnail != null) - //{ - // scannedImage.SetThumbnail(StorageManager.MemoryStorageFactory.Decode(new MemoryStream(thumbnail), ".bmp")); - //} - //ImageCallback?.Invoke(scannedImage, tempImageFilePath); - } - } -} diff --git a/NAPS2.Sdk/Worker/WorkerContext.cs b/NAPS2.Sdk/Worker/WorkerContext.cs index 217d38657..fc41a5d11 100644 --- a/NAPS2.Sdk/Worker/WorkerContext.cs +++ b/NAPS2.Sdk/Worker/WorkerContext.cs @@ -12,25 +12,12 @@ namespace NAPS2.Worker /// public class WorkerContext : IDisposable { - public IWorkerService Service { get; set; } - - public IWorkerCallback Callback { get; set; } + public GrpcWorkerServiceAdapter Service { get; set; } public Process Process { get; set; } public void Dispose() { - try - { - ((IDisposable) Service)?.Dispose(); - } - catch (CommunicationObjectFaultedException) - { - } - catch (Exception e) - { - Log.ErrorException("Error cleaning up worker", e); - } try { Process.Kill(); diff --git a/NAPS2.Sdk/Worker/WorkerManager.cs b/NAPS2.Sdk/Worker/WorkerManager.cs index 16334637b..f423e072e 100644 --- a/NAPS2.Sdk/Worker/WorkerManager.cs +++ b/NAPS2.Sdk/Worker/WorkerManager.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; -using System.ServiceModel; using System.Threading.Tasks; using NAPS2.Platform; @@ -16,7 +15,6 @@ namespace NAPS2.Worker /// public static class WorkerManager { - public const string PIPE_NAME_FORMAT = "net.pipe://localhost/NAPS2.Worker/{0}"; public const string WORKER_EXE_NAME = "NAPS2.Worker.exe"; public static readonly string[] SearchDirs = { @@ -86,18 +84,7 @@ namespace NAPS2.Worker Task.Factory.StartNew(() => { var (proc, port) = StartWorkerProcess(); - //var pipeName = string.Format(PIPE_NAME_FORMAT, proc.Id); - //var callback = new WorkerCallback(); - //var instanceContext = new InstanceContext(callback); - //var channelFactory = new DuplexChannelFactory(instanceContext, - // new NetNamedPipeBinding - // { - // SendTimeout = TimeSpan.FromHours(24), - // MaxReceivedMessageSize = int.MaxValue - // }, - // new EndpointAddress(pipeName)); - //var channel = channelFactory.CreateChannel(); - _workerQueue.Add(new WorkerContext { Service = new GrpcWorkerServiceAdapter(port), Callback = null, Process = proc }); + _workerQueue.Add(new WorkerContext { Service = new GrpcWorkerServiceAdapter(port), Process = proc }); }); } diff --git a/NAPS2.Sdk/Worker/WorkerService.cs b/NAPS2.Sdk/Worker/WorkerService.cs deleted file mode 100644 index 09cb6d239..000000000 --- a/NAPS2.Sdk/Worker/WorkerService.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.ServiceModel; -using System.Threading; -using System.Threading.Tasks; -using NAPS2.ImportExport.Email; -using NAPS2.ImportExport.Email.Mapi; -using NAPS2.Scan; -using NAPS2.Scan.Exceptions; -using NAPS2.Images; -using NAPS2.Scan.Twain; -using NAPS2.Scan.Wia; -using NAPS2.Scan.Wia.Native; - -namespace NAPS2.Worker -{ - /// - /// The WCF service implementation for NAPS2.Worker.exe. - /// - [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, - IncludeExceptionDetailInFaults = true, - ConcurrencyMode = ConcurrencyMode.Multiple)] - public class WorkerService : IWorkerService - { - private readonly TwainWrapper twainWrapper; - private readonly ThumbnailRenderer thumbnailRenderer; - private readonly MapiWrapper mapiWrapper; - - private CancellationTokenSource twainScanCts = new CancellationTokenSource(); - - public WorkerService(TwainWrapper twainWrapper, ThumbnailRenderer thumbnailRenderer, MapiWrapper mapiWrapper) - { - this.twainWrapper = twainWrapper; - this.thumbnailRenderer = thumbnailRenderer; - this.mapiWrapper = mapiWrapper; - } - - public void Init(string recoveryFolderPath) - { - Callback = OperationContext.Current.GetCallbackChannel(); - // TODO - //RecoveryImage.RecoveryFolder = new DirectoryInfo(recoveryFolderPath); - } - - public WiaConfiguration Wia10NativeUI(string deviceId, IntPtr hwnd) - { - try - { - try - { - using (var deviceManager = new WiaDeviceManager(WiaVersion.Wia10)) - using (var device = deviceManager.FindDevice(deviceId)) - { - var item = device.PromptToConfigure(hwnd); - return new WiaConfiguration - { - DeviceProps = device.Properties.SerializeEditable(), - ItemProps = item.Properties.SerializeEditable(), - ItemName = item.Name() - }; - } - } - catch (WiaException e) - { - WiaScanErrors.ThrowDeviceError(e); - throw new InvalidOperationException(); - } - } - catch (ScanDriverException e) - { - throw new FaultException(new ScanDriverExceptionDetail(e)); - } - } - - public List TwainGetDeviceList(TwainImpl twainImpl) - { - return twainWrapper.GetDeviceList(twainImpl); - } - - public async Task TwainScan(ScanDevice scanDevice, ScanProfile scanProfile, ScanParams scanParams, IntPtr hwnd) - { - try - { - await Task.Factory.StartNew(() => - { - var imagePathDict = new Dictionary(); - twainWrapper.Scan(hwnd, scanDevice, scanProfile, scanParams, twainScanCts.Token, - new WorkerImageSink(Callback, imagePathDict), (img, _, path) => imagePathDict.Add(img, path)); - }, TaskCreationOptions.LongRunning); - } - catch (ScanDriverException e) - { - throw new FaultException(new ScanDriverExceptionDetail(e)); - } - } - - public void CancelTwainScan() - { - twainScanCts.Cancel(); - } - - public MapiSendMailReturnCode SendMapiEmail(EmailMessage message) - { - return mapiWrapper.SendEmail(message); - } - - public byte[] RenderThumbnail(ScannedImage.Snapshot snapshot, int size) - { - var stream = new MemoryStream(); - // TODO - //using (var bitmap = Task.Factory.StartNew(() => thumbnailRenderer.RenderThumbnail(snapshot, size)).Unwrap().Result) - //{ - // bitmap.Save(stream, ImageFormat.Png); - //} - return stream.ToArray(); - } - - public void Dispose() - { - } - - public IWorkerCallback Callback { get; set; } - - private class WorkerImageSink : ScannedImageSink - { - private readonly IWorkerCallback callback; - private readonly Dictionary imagePathDict; - - public WorkerImageSink(IWorkerCallback callback, Dictionary imagePathDict) - { - this.callback = callback; - this.imagePathDict = imagePathDict; - } - - public override void PutImage(ScannedImage image) - { - // TODO: Ideally this shouldn't be inheriting ScannedImageSink, some other cleaner mechanism - - // TODO - //MemoryStream stream = null; - //var thumb = image.GetThumbnail(); - //if (thumb != null) - //{ - // stream = new MemoryStream(); - // thumb.Save(stream, ImageFormat.Png); - //} - //callback.TwainImageReceived(image.RecoveryIndexImage, stream?.ToArray(), imagePathDict.Get(image)); - } - } - } -} diff --git a/NAPS2.Sdk/Worker/worker.proto b/NAPS2.Sdk/Worker/worker.proto index bf5a5b76a..18cade260 100644 --- a/NAPS2.Sdk/Worker/worker.proto +++ b/NAPS2.Sdk/Worker/worker.proto @@ -6,6 +6,9 @@ service GrpcWorkerService { rpc Init (InitRequest) returns (InitResponse) {} rpc Wia10NativeUi (Wia10NativeUiRequest) returns (Wia10NativeUiResponse) {} rpc TwainGetDeviceList (TwainGetDeviceListRequest) returns (TwainGetDeviceListResponse) {} + rpc TwainScan (TwainScanRequest) returns (stream TwainScanResponse) {} + rpc SendMapiEmail (SendMapiEmailRequest) returns (SendMapiEmailResponse) {} + rpc RenderThumbnail (RenderThumbnailRequest) returns (RenderThumbnailResponse) {} } message Error { @@ -39,4 +42,35 @@ message TwainGetDeviceListResponse { string deviceListXml = 2; } -// TODO more. Use streaming for scan callback/cancel. +message TwainScanRequest { + string scanDeviceXml = 1; + string scanProfileXml = 2; + string scanParamsXml = 3; + fixed64 hwnd = 4; +} + +message TwainScanResponse { + Error error = 1; + string recoveryIndexImageXml = 2; + bytes thumbnail = 3; + string tempImageFilePath = 4; +} + +message SendMapiEmailRequest { + string emailMessageXml = 1; +} + +message SendMapiEmailResponse { + Error error = 1; + string returnCodeXml = 2; +} + +message RenderThumbnailRequest { + string snapshotXml = 1; + int32 size = 2; +} + +message RenderThumbnailResponse { + Error error = 1; + bytes thumbnail = 2; +} diff --git a/NAPS2.sln.DotSettings b/NAPS2.sln.DotSettings index e7829ead0..c4637d0a5 100644 --- a/NAPS2.sln.DotSettings +++ b/NAPS2.sln.DotSettings @@ -6,6 +6,9 @@ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <data><IncludeFilters /><ExcludeFilters /></data> <data /> + True + True True True + True True \ No newline at end of file