Network WIP

This commit is contained in:
Ben Olden-Cooligan 2020-02-02 14:24:05 -05:00
parent 0baa9f0188
commit d7f4ed83b1
18 changed files with 252 additions and 167 deletions

View File

@ -50,24 +50,24 @@ namespace NAPS2.Server
}
}
new Thread(() => ServerDiscovery.ListenForBroadcast(port)) { IsBackground = true }.Start();
new Thread(() => Discovery.ListenForBroadcast(port)) { IsBackground = true }.Start();
// Listen for requests
using var host = new ServiceHost(typeof(ScanService));
// // Listen for requests
// using var host = new ServiceHost(typeof(ScanService));
var serverIcon = new ServerNotifyIcon(port, () => form.Close());
host.Opened += (sender, eventArgs) => serverIcon.Show();
// host.Description.Behaviors.Add(new ServiceFactoryBehavior(() => kernel.Get<ScanService>()));
var binding = new NetTcpBinding
{
ReceiveTimeout = TimeSpan.FromHours(1),
SendTimeout = TimeSpan.FromHours(1),
Security =
{
Mode = SecurityMode.None
}
};
host.AddServiceEndpoint(typeof(IScanService), binding, $"net.tcp://0.0.0.0:{port}/NAPS2.Server");
host.Open();
// host.Opened += (sender, eventArgs) => serverIcon.Show();
// // host.Description.Behaviors.Add(new ServiceFactoryBehavior(() => kernel.Get<ScanService>()));
// var binding = new NetTcpBinding
// {
// ReceiveTimeout = TimeSpan.FromHours(1),
// SendTimeout = TimeSpan.FromHours(1),
// Security =
// {
// Mode = SecurityMode.None
// }
// };
// host.AddServiceEndpoint(typeof(IScanService), binding, $"net.tcp://0.0.0.0:{port}/NAPS2.Server");
// host.Open();
try
{
Application.Run(form);

View File

@ -1,28 +0,0 @@
using System;
using System.ServiceModel;
using NAPS2.Logging;
namespace NAPS2.Remoting.Network
{
public class ClientContext : IDisposable
{
public IScanService Service { get; set; }
public IScanCallback Callback { get; set; }
public void Dispose()
{
try
{
((IDisposable)Service)?.Dispose();
}
catch (CommunicationObjectFaultedException)
{
}
catch (Exception e)
{
Log.ErrorException("Error cleaning up client", e);
}
}
}
}

View File

@ -1,26 +0,0 @@
using System;
using System.ServiceModel;
using NAPS2.Scan;
namespace NAPS2.Remoting.Network
{
public class ClientContextFactory
{
public ClientContext Create(ScanProxyConfig proxyConfig)
{
// TODO: Validate IP
var uri = new Uri($"net.tcp://{proxyConfig.Ip}:{proxyConfig.Port}/NAPS2.Server");
var callback = new ScanCallback();
var instanceContext = new InstanceContext(callback);
var channelFactory = new DuplexChannelFactory<IScanService>(instanceContext,
new NetTcpBinding
{
MaxReceivedMessageSize = 1024 * 1024 * 1024,
Security = { Mode = SecurityMode.None }
},
new EndpointAddress(uri));
var channel = channelFactory.CreateChannel();
return new ClientContext { Service = channel, Callback = callback };
}
}
}

View File

@ -1,15 +0,0 @@
using System;
using System.ServiceModel;
using NAPS2.Recovery;
namespace NAPS2.Remoting.Network
{
[ServiceContract]
public interface IScanCallback
{
[OperationContract(IsOneWay = true)]
void ImageReceived(byte[] imageData, RecoveryIndexImage indexImage);
event Action<byte[], RecoveryIndexImage> ImageCallback;
}
}

View File

@ -1,23 +0,0 @@
using System.Collections.Generic;
using System.ServiceModel;
using System.Threading.Tasks;
using NAPS2.Scan;
namespace NAPS2.Remoting.Network
{
[ServiceContract(CallbackContract = typeof(IScanCallback))]
public interface IScanService
{
[OperationContract]
List<string> GetSupportedDriverNames();
[OperationContract]
List<ScanDevice> GetDeviceList(ScanProfile scanProfile);
[OperationContract]
Task<int> Scan(ScanProfile scanProfile, ScanParams scanParams);
[OperationContract(IsOneWay = true)]
void CancelScan();
}
}

View File

@ -9,16 +9,17 @@ using NAPS2.Util;
namespace NAPS2.Remoting.Network
{
public class ServerDiscovery
public class Discovery
{
private const int DISCOVERY_PORT = 33277;
public const int DEFAULT_DISCOVERY_PORT = 33277;
private static readonly byte[] MagicBroadcastBytes = { 0x7f, 0x87, 0x00, 0x8b, 0x08, 0x87, 0x5d, 0xd3, 0x64, 0x1a };
private static readonly byte[] MagicResponseBytes = { 0xf4, 0x38, 0xb9, 0xa3, 0xf7, 0x37, 0xaf, 0x35, 0x41, 0xc7 };
public static void ListenForBroadcast(int serverPort)
{
var fallback = new ExpFallback(100, 60 * 1000);
var udpClient = new UdpClient(DISCOVERY_PORT) { Client = { ReceiveTimeout = 0 } };
var udpClient = new UdpClient(DEFAULT_DISCOVERY_PORT) { Client = { ReceiveTimeout = 0 } };
IPEndPoint remoteEndpoint = null;
while (true)
{
@ -45,7 +46,7 @@ namespace NAPS2.Remoting.Network
public static void SendBroadcast(Action<string, IPEndPoint> callback)
{
var udpClient = new UdpClient { Client = { ReceiveTimeout = 1000 } };
udpClient.Send(MagicBroadcastBytes, MagicBroadcastBytes.Length, new IPEndPoint(IPAddress.Broadcast, DISCOVERY_PORT));
udpClient.Send(MagicBroadcastBytes, MagicBroadcastBytes.Length, new IPEndPoint(IPAddress.Broadcast, DEFAULT_DISCOVERY_PORT));
var fallback = new ExpFallback(100, 60 * 1000);
IPEndPoint remoteEndpoint = null;

View File

@ -0,0 +1,49 @@
syntax = "proto3";
package NAPS2.Remoting.Network;
import "NAPS2.Sdk/Remoting/Error.proto";
import "NAPS2.Sdk/Serialization/SerializedImage.proto";
service NetworkScanService {
rpc GetCapabilities (GetCapabilitiesRequest) returns (GetCapabilitiesResponse) {}
rpc GetDeviceList (GetDeviceListRequest) returns (GetDeviceListResponse) {}
rpc Scan (ScanRequest) returns (stream ScanResponse) {}
}
message GetCapabilitiesRequest {
}
message GetCapabilitiesResponse {
NAPS2.Remoting.Error error = 1;
repeated string supportedDrivers = 2;
}
message GetDeviceListRequest {
string optionsXml = 1;
}
message GetDeviceListResponse {
NAPS2.Remoting.Error error = 1;
string deviceListXml = 2;
}
message ScanRequest {
string optionsXml = 1;
}
message ScanResponse {
oneof event {
NAPS2.Remoting.Error error = 1;
NAPS2.Serialization.SerializedImage image = 2;
ProgressEvent progress = 3;
PageStartEvent pageStart = 4;
}
}
message ProgressEvent {
double value = 1;
}
message PageStartEvent {
}

View File

@ -3,16 +3,56 @@ using System.Collections.Generic;
using System.ServiceModel;
using System.Threading;
using System.Threading.Tasks;
using Grpc.Core;
using NAPS2.Scan;
using NAPS2.Scan.Internal;
using NAPS2.Serialization;
namespace NAPS2.Remoting.Network
{
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession,
IncludeExceptionDetailInFaults = true,
ConcurrencyMode = ConcurrencyMode.Multiple)]
public class ScanService : IScanService
internal class ScanService : NetworkScanService.NetworkScanServiceBase
{
public override Task<GetCapabilitiesResponse> GetCapabilities(GetCapabilitiesRequest request, ServerCallContext context)
{
return base.GetCapabilities(request, context);
}
public override async Task<GetDeviceListResponse> GetDeviceList(GetDeviceListRequest request, ServerCallContext context)
{
try
{
var controller = new ScanController();
var options = request.OptionsXml.FromXml<ScanOptions>();
var devices = await controller.GetDeviceList(options);
return new GetDeviceListResponse
{
DeviceListXml = devices.ToXml()
};
}
catch (Exception e)
{
return new GetDeviceListResponse
{
Error = RemotingHelper.ToError(e)
};
}
}
public override async Task Scan(ScanRequest request, IServerStreamWriter<ScanResponse> responseStream, ServerCallContext context)
{
try
{
// TODO
}
catch (Exception e)
{
await responseStream.WriteAsync(new ScanResponse
{
Error = RemotingHelper.ToError(e)
});
}
}
// private readonly IRemoteScanController remoteScanController;
//
// private CancellationTokenSource scanCts = new CancellationTokenSource();

View File

@ -19,8 +19,10 @@
<PackageReference Include="BouncyCastle" Version="1.8.4" />
<PackageReference Include="Grpc.Core" Version="2.26.0" />
<PackageReference Include="Grpc.Tools" Version="2.26.0" />
<Reference Include="System.ServiceModel" />
</ItemGroup>
<ItemGroup>
<ProtoBuf Include="../NAPS2.Sdk.Network/**/*.proto" Access="Internal" ProtoRoot=".." />
</ItemGroup>
</Project>

View File

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
namespace NAPS2.Remoting.Network
{
public class NetworkScanClient
{
public async Task<IEnumerable<DiscoveredServer>> DiscoverServers(int timeout = 5000, CancellationToken cancellationToken = default)
{
var servers = new List<DiscoveredServer>();
await DiscoverServers(servers.Add, timeout, cancellationToken);
return servers;
}
public async Task DiscoverServers(Action<DiscoveredServer> callback, int timeout = 5000, CancellationToken cancellationToken = default)
{
var timeoutCts = new CancellationTokenSource();
timeoutCts.CancelAfter(timeout);
var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
// TODO
}
}
public class DiscoveredServer
{
internal DiscoveredServer(string name, IPEndPoint ipEndPoint)
{
Name = name;
IPEndPoint = ipEndPoint;
}
public string Name { get; }
public IPEndPoint IPEndPoint { get; }
}
}

View File

@ -0,0 +1,22 @@
using System;
namespace NAPS2.Remoting.Network
{
public class NetworkScanServer : IDisposable
{
public void Start()
{
}
public void Kill()
{
Dispose();
}
public void Dispose()
{
}
}
}

View File

@ -0,0 +1,11 @@
namespace NAPS2.Remoting.Network
{
public class NetworkScanServerOptions
{
public int Port { get; set; }
public bool AllowDiscovery { get; set; } = true;
public int DiscoveryPort { get; set; }
}
}

View File

@ -1,15 +0,0 @@
using System;
using NAPS2.Recovery;
namespace NAPS2.Remoting.Network
{
public class ScanCallback : IScanCallback
{
public void ImageReceived(byte[] imageData, RecoveryIndexImage indexImage)
{
ImageCallback?.Invoke(imageData, indexImage);
}
public event Action<byte[], RecoveryIndexImage> ImageCallback;
}
}

View File

@ -31,7 +31,7 @@ namespace NAPS2.Remoting
}
}
private static Error ToError(Exception e) =>
public static Error ToError(Exception e) =>
new Error
{
Type = e.GetType().FullName,

View File

@ -0,0 +1,32 @@
using System.Threading.Tasks;
using Grpc.Core;
namespace NAPS2.Remoting
{
public class SequencedWriter<T>
{
private readonly IServerStreamWriter<T> serverStreamWriter;
private Task lastWriteTask = Task.CompletedTask;
public SequencedWriter(IServerStreamWriter<T> serverStreamWriter)
{
this.serverStreamWriter = serverStreamWriter;
}
public void Write(T item)
{
lock (this)
{
lastWriteTask = lastWriteTask.ContinueWith(t => serverStreamWriter.WriteAsync(item)).Unwrap();
}
}
public Task WaitForCompletion()
{
lock (this)
{
return lastWriteTask;
}
}
}
}

View File

@ -86,48 +86,44 @@ namespace NAPS2.Remoting.Worker
},
err => new GetDeviceListResponse { Error = err });
public override Task Scan(ScanRequest request, IServerStreamWriter<ScanResponse> responseStream, ServerCallContext context)
public override async Task Scan(ScanRequest request, IServerStreamWriter<ScanResponse> responseStream, ServerCallContext context)
{
// gRPC doesn't allow multiple pending writes. However, we don't want to write synchronously because that will slow down the scanning process.
// Instead, we can use some async magic (chained tasks) to only write one at a time.
Task lastWriteTask = Task.CompletedTask;
Task WriteSequenced(ScanResponse response)
var sequencedWriter = new SequencedWriter<ScanResponse>(responseStream);
try
{
lastWriteTask = lastWriteTask.ContinueWith(t => responseStream.WriteAsync(response)).Unwrap();
return lastWriteTask;
}
return RemotingHelper.WrapAction(
async () =>
{
var scanEvents = new ScanEvents(
() => WriteSequenced(new ScanResponse
var scanEvents = new ScanEvents(
() => sequencedWriter.Write(new ScanResponse
{
PageStart = new PageStartEvent()
}),
progress => sequencedWriter.Write(new ScanResponse
{
Progress = new ProgressEvent
{
PageStart = new PageStartEvent()
}),
progress => WriteSequenced(new ScanResponse
Value = progress
}
})
);
await remoteScanController.Scan(request.OptionsXml.FromXml<ScanOptions>(),
context.CancellationToken, scanEvents,
(image, postProcessingContext) =>
sequencedWriter.Write(new ScanResponse
{
Progress = new ProgressEvent
{
Value = progress
}
})
);
await remoteScanController.Scan(request.OptionsXml.FromXml<ScanOptions>(), context.CancellationToken, scanEvents,
(image, postProcessingContext) =>
WriteSequenced(new ScanResponse
{
Image = SerializedImageHelper.Serialize(imageContext, image, new SerializedImageHelper.SerializeOptions
Image = SerializedImageHelper.Serialize(imageContext, image,
new SerializedImageHelper.SerializeOptions
{
TransferOwnership = true,
IncludeThumbnail = true,
RenderedFilePath = postProcessingContext.TempPath
})
}));
// It's important that we wait for writes to complete, otherwise the channel might close first.
await lastWriteTask;
}, err => WriteSequenced(new ScanResponse { Error = err }).Wait());
}));
await sequencedWriter.WaitForCompletion();
}
catch (Exception e)
{
sequencedWriter.Write(new ScanResponse { Error = RemotingHelper.ToError(e) });
await sequencedWriter.WaitForCompletion();
}
}
public override Task<SendMapiEmailResponse> SendMapiEmail(SendMapiEmailRequest request, ServerCallContext context) =>