Skip to content

Commit a044083

Browse files
authored
Merge pull request #2850 from erri120/misc/cli-server-exception
Remove timeout and cancel exception from CliServer
2 parents b8bc335 + 8edd9d8 commit a044083

File tree

2 files changed

+27
-57
lines changed

2 files changed

+27
-57
lines changed

src/NexusMods.SingleProcess/CliServer.cs

+27-52
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections.Concurrent;
12
using System.Net;
23
using System.Net.Sockets;
34
using Microsoft.Extensions.Hosting;
@@ -12,18 +13,15 @@ namespace NexusMods.SingleProcess;
1213
/// A long-running service that listens for incoming connections from clients and executes them as if they ran
1314
/// on as CLI command.
1415
/// </summary>
15-
public class CliServer : IHostedService, IDisposable
16+
public sealed class CliServer : IHostedService, IDisposable
1617
{
1718
private readonly CancellationTokenSource _cancellationTokenSource = new();
1819
private CancellationToken Token => _cancellationTokenSource.Token;
1920

2021
private bool _started;
2122

2223
private TcpListener? _tcpListener;
23-
private Task? _listenerTask;
24-
25-
private readonly List<Task> _runningClients = [];
26-
private readonly CliSettings _settings;
24+
private readonly ConcurrentDictionary<Guid, Task> _runningClients = [];
2725

2826
private readonly IServiceProvider _serviceProvider;
2927
private readonly ILogger<CliServer> _logger;
@@ -45,7 +43,7 @@ public CliServer(
4543
_configurator = configurator;
4644
_syncFile = syncFile;
4745

48-
_settings = settingsManager.Get<CliSettings>();
46+
settingsManager.Get<CliSettings>();
4947
}
5048

5149
/// <summary>
@@ -65,7 +63,7 @@ private Task StartTcpListenerAsync()
6563
{
6664
_tcpListener = new TcpListener(IPAddress.Loopback, 0);
6765
_tcpListener.Start();
68-
_listenerTask = Task.Run(async () => await StartListeningAsync(), _cancellationTokenSource.Token);
66+
Task.Run(async () => await StartListeningAsync(), _cancellationTokenSource.Token);
6967
var port = ((IPEndPoint)_tcpListener.LocalEndpoint).Port;
7068

7169
if (!_syncFile.TrySetMain(port))
@@ -84,83 +82,60 @@ private async Task StartListeningAsync()
8482
{
8583
try
8684
{
87-
CleanClosedConnections();
88-
89-
// Create a timeout token, and combine it with the main cancellation token
90-
var timeout = new CancellationTokenSource(delay: _settings.ListenTimeout);
91-
var combined = CancellationTokenSource.CreateLinkedTokenSource(Token, timeout.Token);
92-
93-
var found = await _tcpListener!.AcceptTcpClientAsync(combined.Token);
85+
var found = await _tcpListener!.AcceptTcpClientAsync(Token);
9486
found.NoDelay = true; // Disable Nagle's algorithm to reduce delay.
95-
_runningClients.Add(Task.Run(() => HandleClientAsync(found), Token));
9687

97-
_logger.LogInformation("Accepted TCP connection from {RemoteEndPoint}",
98-
((IPEndPoint)found.Client.RemoteEndPoint!).Port
99-
);
88+
var id = Guid.NewGuid();
89+
var task = Task.Run(() => HandleClientAsync(id, found), Token);
90+
_ = _runningClients.GetOrAdd(id, task);
91+
92+
_logger.LogInformation("Accepted TCP connection from {RemoteEndPoint}", ((IPEndPoint)found.Client.RemoteEndPoint!).Port);
10093
}
10194
catch (OperationCanceledException)
10295
{
103-
// The cancellation could be from the timeout, or the main cancellation token, if it's the
104-
// timeout, then we should just continue, if it's the main cancellation token, then we should stop
105-
if (!Token.IsCancellationRequested)
106-
continue;
10796
_logger.LogInformation("TCP listener was cancelled, stopping");
10897
return;
10998
}
11099
catch (Exception ex)
111100
{
112101
_logger.LogError(ex, "Got an exception while accepting a client connection");
113102
}
114-
115103
}
116104
}
117-
105+
118106
/// <summary>
119107
/// Handle a client connection
120108
/// </summary>
121-
/// <param name="client"></param>
122-
/// <returns></returns>
123-
/// <exception cref="NotImplementedException"></exception>
124-
private async Task HandleClientAsync(TcpClient client)
109+
private async Task HandleClientAsync(Guid id, TcpClient client)
125110
{
126-
var stream = client.GetStream();
111+
try
112+
{
113+
var stream = client.GetStream();
127114

128-
var (arguments, renderer) = await ProxiedRenderer.Create(_serviceProvider, stream);
129-
await _configurator.RunAsync(arguments, renderer, Token);
130-
client.Dispose();
131-
}
132-
133-
/// <summary>
134-
/// Clears up any closed connections in the <see cref="_runningClients"/> dictionary
135-
/// </summary>
136-
/// <exception cref="NotImplementedException"></exception>
137-
private void CleanClosedConnections()
138-
{
139-
// Snapshot the dictionary before we modify it
140-
foreach(var task in _runningClients.ToArray())
115+
var (arguments, renderer) = await ProxiedRenderer.Create(_serviceProvider, stream);
116+
await _configurator.RunAsync(arguments, renderer, Token);
117+
}
118+
finally
141119
{
142-
if (task.IsCompleted)
143-
_runningClients.Remove(task);
120+
client.Dispose();
121+
_runningClients.Remove(id, out _);
144122
}
145123
}
146-
124+
147125
/// <inheritdoc />
148-
public Task StartAsync(CancellationToken cancellationToken)
149-
{
150-
return Task.CompletedTask;
151-
}
126+
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
152127

153128
/// <inheritdoc />
154129
public async Task StopAsync(CancellationToken cancellationToken)
155130
{
156131
if (!_started) return;
157-
132+
158133
// Ditch this value and don't wait on it because it otherwise blocks the shutdown even when *no-one* is
159134
// waiting on the token
160135
_ = _cancellationTokenSource.CancelAsync();
161-
136+
162137
_tcpListener?.Stop();
163-
await Task.WhenAll(_runningClients);
138+
await Task.WhenAll(_runningClients.Values.ToArray());
164139
_started = false;
165140
}
166141

src/NexusMods.SingleProcess/CliSettings.cs

-5
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,6 @@ private static CliSettings CreateDefault(IServiceProvider provider)
4646
};
4747
}
4848

49-
/// <summary>
50-
/// The amount of time the TCPListener will pause waiting for new connections before checking if it should exit.
51-
/// </summary>
52-
public TimeSpan ListenTimeout { get; set; } = TimeSpan.FromSeconds(1);
53-
5449
/// <summary>
5550
/// If true the CLI backend will be started, otherwise it will not be started, and CLI commands will not be available.
5651
/// </summary>

0 commit comments

Comments
 (0)