mirror of
https://github.com/cyanfish/naps2.git
synced 2024-09-11 15:26:55 +03:00
Escl: Fix port handling and server restarts
Ports are now static per SharedDevice where possible, falling back to random ports if an error occurs.
This commit is contained in:
parent
062a056b6c
commit
29b3c57207
@ -11,16 +11,11 @@ public class EsclServer : IEsclServer
|
||||
|
||||
public void AddDevice(EsclDeviceConfig deviceConfig)
|
||||
{
|
||||
if (deviceConfig.Port == 0)
|
||||
{
|
||||
deviceConfig.Port = Port++;
|
||||
}
|
||||
var advertiser = new MdnsAdvertiser();
|
||||
_devices[deviceConfig] = (advertiser, new CancellationTokenSource());
|
||||
if (_started)
|
||||
{
|
||||
StartServer(deviceConfig);
|
||||
Task.Run(() => advertiser.AdvertiseDevice(deviceConfig));
|
||||
Task.Run(() => StartServerAndAdvertise(deviceConfig, advertiser));
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,10 +31,7 @@ public class EsclServer : IEsclServer
|
||||
_devices.Remove(deviceConfig);
|
||||
}
|
||||
|
||||
// TODO: Better port handling
|
||||
public int Port { get; set; } = 9898;
|
||||
|
||||
public void Start()
|
||||
public Task Start()
|
||||
{
|
||||
if (_started)
|
||||
{
|
||||
@ -48,24 +40,36 @@ public class EsclServer : IEsclServer
|
||||
_started = true;
|
||||
_cts = new CancellationTokenSource();
|
||||
|
||||
var tasks = new List<Task>();
|
||||
foreach (var device in _devices.Keys)
|
||||
{
|
||||
StartServer(device);
|
||||
Task.Run(() => _devices[device].advertiser.AdvertiseDevice(device));
|
||||
tasks.Add(Task.Run(() => StartServerAndAdvertise(device, _devices[device].advertiser)));
|
||||
}
|
||||
return Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
private void StartServer(EsclDeviceConfig deviceConfig)
|
||||
private async Task StartServerAndAdvertise(EsclDeviceConfig deviceConfig, MdnsAdvertiser advertiser)
|
||||
{
|
||||
// TODO: Auto free port?
|
||||
var url = $"http://+:{deviceConfig.Port}/";
|
||||
var cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cts!.Token, _devices[deviceConfig].cts.Token).Token;
|
||||
// Try to run the server with the port specified in the EsclDeviceConfig first. If that fails, try random ports
|
||||
// instead, and store the actually-used port back in EsclDeviceConfig so it can be advertised correctly.
|
||||
await PortFinder.RunWithSpecifiedOrRandomPort(deviceConfig.Port, async port =>
|
||||
{
|
||||
await StartServer(deviceConfig, port, cancelToken);
|
||||
deviceConfig.Port = port;
|
||||
}, cancelToken);
|
||||
advertiser.AdvertiseDevice(deviceConfig);
|
||||
}
|
||||
|
||||
private async Task StartServer(EsclDeviceConfig deviceConfig, int port, CancellationToken cancelToken)
|
||||
{
|
||||
var url = $"http://+:{port}/";
|
||||
var serverState = new EsclServerState();
|
||||
var server = new WebServer(o => o
|
||||
.WithMode(HttpListenerMode.EmbedIO)
|
||||
.WithUrlPrefix(url))
|
||||
.WithWebApi("/eSCL", m => m.WithController(() => new EsclApiController(deviceConfig, serverState)));
|
||||
server.StateChanged += ServerOnStateChanged;
|
||||
server.RunAsync(CancellationTokenSource.CreateLinkedTokenSource(_cts!.Token, _devices[deviceConfig].cts.Token).Token);
|
||||
await server.StartAsync(cancelToken);
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
@ -84,10 +88,6 @@ public class EsclServer : IEsclServer
|
||||
_cts = null;
|
||||
}
|
||||
|
||||
private void ServerOnStateChanged(object sender, WebServerStateChangedEventArgs e)
|
||||
{
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_started)
|
||||
|
43
NAPS2.Escl.Server/PortFinder.cs
Normal file
43
NAPS2.Escl.Server/PortFinder.cs
Normal file
@ -0,0 +1,43 @@
|
||||
namespace NAPS2.Escl.Server;
|
||||
|
||||
internal static class PortFinder
|
||||
{
|
||||
private const int MAX_PORT_TRIES = 5;
|
||||
private const int RANDOM_PORT_MIN = 10001;
|
||||
private const int RANDOM_PORT_MAX = 19999;
|
||||
|
||||
public static async Task RunWithSpecifiedOrRandomPort(int defaultPort, Func<int, Task> portTaskFunc,
|
||||
CancellationToken cancelToken)
|
||||
{
|
||||
int port = defaultPort;
|
||||
int retries = 0;
|
||||
var random = new Random();
|
||||
if (port == 0)
|
||||
{
|
||||
port = RandomPort(random);
|
||||
}
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await portTaskFunc(port);
|
||||
break;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
if (cancelToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
retries++;
|
||||
port = RandomPort(random);
|
||||
if (retries > MAX_PORT_TRIES)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int RandomPort(Random random) => random.Next(RANDOM_PORT_MIN, RANDOM_PORT_MAX + 1);
|
||||
}
|
30
NAPS2.Escl.Server/WebServerExtensions.cs
Normal file
30
NAPS2.Escl.Server/WebServerExtensions.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using EmbedIO;
|
||||
|
||||
namespace NAPS2.Escl.Server;
|
||||
|
||||
internal static class WebServerExtensions
|
||||
{
|
||||
public static async Task StartAsync(this WebServer server, CancellationToken cancelToken = default)
|
||||
{
|
||||
var startedTcs = new TaskCompletionSource<bool>();
|
||||
server.StateChanged += (_, args) =>
|
||||
{
|
||||
if (args.NewState == WebServerState.Listening)
|
||||
{
|
||||
startedTcs.TrySetResult(true);
|
||||
}
|
||||
};
|
||||
_ = server.RunAsync(cancelToken).ContinueWith(t =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
{
|
||||
startedTcs.TrySetException(t.Exception!);
|
||||
}
|
||||
else
|
||||
{
|
||||
startedTcs.TrySetCanceled();
|
||||
}
|
||||
});
|
||||
await startedTcs.Task;
|
||||
}
|
||||
}
|
@ -18,11 +18,12 @@ public class AdvertiseTests
|
||||
{
|
||||
Version = "2.6",
|
||||
MakeAndModel = "HP Blah",
|
||||
SerialNumber = "123abc"
|
||||
SerialNumber = "123abc",
|
||||
Uuid = Guid.NewGuid().ToString("D")
|
||||
},
|
||||
CreateJob = _ => job
|
||||
});
|
||||
server.Start();
|
||||
await server.Start();
|
||||
if (Debugger.IsAttached)
|
||||
{
|
||||
for (int i = 0; i < 100; i++)
|
||||
|
@ -11,32 +11,28 @@ public class ClientServerTests
|
||||
[Fact]
|
||||
public async Task ClientServer()
|
||||
{
|
||||
// TODO: Any better way to prevent port collisions when running tests?
|
||||
#if NET6_0_OR_GREATER
|
||||
int port = 9802;
|
||||
#else
|
||||
int port = 9801;
|
||||
#endif
|
||||
var job = Substitute.For<IEsclScanJob>();
|
||||
using var server = new EsclServer { Port = port };
|
||||
server.AddDevice(new EsclDeviceConfig
|
||||
using var server = new EsclServer();
|
||||
var deviceConfig = new EsclDeviceConfig
|
||||
{
|
||||
Capabilities = new EsclCapabilities
|
||||
{
|
||||
Version = "2.0",
|
||||
MakeAndModel = "HP Blah",
|
||||
SerialNumber = "123abc"
|
||||
SerialNumber = "123abc",
|
||||
Uuid = Guid.NewGuid().ToString("D")
|
||||
},
|
||||
CreateJob = _ => job
|
||||
});
|
||||
server.Start();
|
||||
};
|
||||
server.AddDevice(deviceConfig);
|
||||
await server.Start();
|
||||
var client = new EsclClient(new EsclService
|
||||
{
|
||||
IpV4 = IPAddress.Loopback,
|
||||
IpV6 = IPAddress.IPv6Loopback,
|
||||
Host = IPAddress.IPv6Loopback.ToString(),
|
||||
RemoteEndpoint = IPAddress.IPv6Loopback,
|
||||
Port = 9801,
|
||||
Port = deviceConfig.Port,
|
||||
RootUrl = "eSCL",
|
||||
Tls = false
|
||||
});
|
||||
|
@ -4,7 +4,6 @@ public interface IEsclServer : IDisposable
|
||||
{
|
||||
void AddDevice(EsclDeviceConfig deviceConfig);
|
||||
void RemoveDevice(EsclDeviceConfig deviceConfig);
|
||||
int Port { get; set; }
|
||||
void Start();
|
||||
Task Start();
|
||||
void Stop();
|
||||
}
|
@ -10,6 +10,8 @@ namespace NAPS2.EtoForms.Ui;
|
||||
|
||||
public class SharedDeviceForm : EtoDialogBase
|
||||
{
|
||||
private const int BASE_PORT = 9801;
|
||||
|
||||
private readonly IScanPerformer _scanPerformer;
|
||||
private readonly ErrorOutput _errorOutput;
|
||||
|
||||
@ -114,6 +116,8 @@ public class SharedDeviceForm : EtoDialogBase
|
||||
}
|
||||
}
|
||||
|
||||
private int Port { get; set; }
|
||||
|
||||
private Driver DeviceDriver
|
||||
{
|
||||
get => _twainDriver.Checked ? Driver.Twain
|
||||
@ -151,6 +155,7 @@ public class SharedDeviceForm : EtoDialogBase
|
||||
|
||||
_displayName.Text = SharedDevice?.Name ?? "";
|
||||
CurrentDevice ??= SharedDevice?.Device;
|
||||
Port = SharedDevice?.Port ?? 0;
|
||||
|
||||
DeviceDriver = SharedDevice?.Device.Driver ?? ScanOptionsValidator.SystemDefaultDriver;
|
||||
|
||||
@ -198,10 +203,22 @@ public class SharedDeviceForm : EtoDialogBase
|
||||
SharedDevice = new SharedDevice
|
||||
{
|
||||
Name = _displayName.Text,
|
||||
Device = CurrentDevice!
|
||||
Device = CurrentDevice!,
|
||||
Port = Port == 0 ? NextPort() : Port
|
||||
};
|
||||
}
|
||||
|
||||
private int NextPort()
|
||||
{
|
||||
var devices = Config.Get(c => c.SharedDevices);
|
||||
int port = BASE_PORT;
|
||||
while (devices.Any(x => x.Port == port))
|
||||
{
|
||||
port++;
|
||||
}
|
||||
return port;
|
||||
}
|
||||
|
||||
private void Ok_Click(object? sender, EventArgs e)
|
||||
{
|
||||
if (_displayName.Text == "")
|
||||
|
@ -25,8 +25,8 @@ public class ScanServer : IDisposable
|
||||
|
||||
public void SetDefaultIcon(byte[] iconPng) => _defaultIconPng = iconPng;
|
||||
|
||||
public void RegisterDevice(ScanDevice device, string? displayName = null) =>
|
||||
RegisterDevice(new SharedDevice { Device = device, Name = displayName ?? device.Name });
|
||||
public void RegisterDevice(ScanDevice device, string? displayName = null, int port = 0) =>
|
||||
RegisterDevice(new SharedDevice { Device = device, Name = displayName ?? device.Name, Port = port });
|
||||
|
||||
public void RegisterDevice(SharedDevice sharedDevice)
|
||||
{
|
||||
@ -35,8 +35,8 @@ public class ScanServer : IDisposable
|
||||
_esclServer.AddDevice(esclDeviceConfig);
|
||||
}
|
||||
|
||||
public void UnregisterDevice(ScanDevice device, string? displayName = null) =>
|
||||
UnregisterDevice(new SharedDevice { Device = device, Name = displayName ?? device.Name });
|
||||
public void UnregisterDevice(ScanDevice device, string? displayName = null, int port = 0) =>
|
||||
UnregisterDevice(new SharedDevice { Device = device, Name = displayName ?? device.Name, Port = port });
|
||||
|
||||
public void UnregisterDevice(SharedDevice sharedDevice)
|
||||
{
|
||||
@ -49,6 +49,7 @@ public class ScanServer : IDisposable
|
||||
{
|
||||
return new EsclDeviceConfig
|
||||
{
|
||||
Port = device.Port,
|
||||
Capabilities = new EsclCapabilities
|
||||
{
|
||||
MakeAndModel = device.Name,
|
||||
|
@ -8,6 +8,7 @@ public record SharedDevice
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
public required ScanDevice Device { get; init; }
|
||||
public required int Port { get; init; }
|
||||
|
||||
public string Uuid
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user