-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Insert SSH 3.11.41, add more unit tests and logging to tests (#473)
Fix for https://github.com/devdiv-microsoft/basis-planning/issues/1618 Insert SSH 3.11.41 that has the fix. Add a unit test for client connecting to host when the tunnel has multiple port. Add more logging to unit tests.
- Loading branch information
1 parent
464f85e
commit 3679192
Showing
5 changed files
with
281 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
using System.Diagnostics; | ||
using System.Globalization; | ||
using System.Net; | ||
using System.Net.Sockets; | ||
using System.Text; | ||
using Microsoft.DevTunnels.Management; | ||
|
||
namespace Microsoft.DevTunnels.Test; | ||
public sealed class TcpListeners : IAsyncDisposable | ||
{ | ||
private const int MaxAttempts = 10; | ||
|
||
private readonly TraceSource trace; | ||
private readonly CancellationTokenSource cts = new(); | ||
private readonly List<TcpListener> listeners = new(); | ||
private readonly List<Task> listenerTasks = new(); | ||
|
||
public TcpListeners(int count, TraceSource trace) | ||
{ | ||
Requires.Argument(count > 0, nameof(count), "Count must be greater than 0."); | ||
this.trace = trace.WithName("TcpListeners"); | ||
Ports = new int[count]; | ||
for (int index = 0; index < count; index++) | ||
{ | ||
TcpListener listener = null; | ||
int port; | ||
int attempt = 0; | ||
while (true) | ||
{ | ||
try | ||
{ | ||
port = TcpUtils.GetAvailableTcpPort(canReuseAddress: false); | ||
listener = new TcpListener(IPAddress.Loopback, port); | ||
listener.Start(); | ||
break; | ||
} | ||
catch (SocketException ex) | ||
{ | ||
listener?.Stop(); | ||
if (++attempt >= MaxAttempts) | ||
{ | ||
throw new InvalidOperationException("Failed to find available port", ex); | ||
} | ||
} | ||
catch | ||
{ | ||
listener?.Stop(); | ||
throw; | ||
} | ||
} | ||
|
||
Ports[index] = port; | ||
this.listeners.Add(listener); | ||
this.listenerTasks.Add(AcceptConnectionsAsync(listener, port)); | ||
} | ||
|
||
this.trace.Info("Listening on ports: {0}", string.Join(", ", Ports)); | ||
} | ||
|
||
public int Port { get; } | ||
|
||
public int[] Ports { get; } | ||
|
||
public async ValueTask DisposeAsync() | ||
{ | ||
cts.Cancel(); | ||
StopListeners(); | ||
await Task.WhenAll(this.listenerTasks); | ||
this.listenerTasks.Clear(); | ||
} | ||
|
||
private async Task AcceptConnectionsAsync(TcpListener listener, int port) | ||
{ | ||
var tasks = new List<Task>(); | ||
TaskCompletionSource allTasksCompleted = null; | ||
try | ||
{ | ||
while (!cts.IsCancellationRequested) | ||
{ | ||
var tcpClient = await listener.AcceptTcpClientAsync(cts.Token); | ||
var task = Task.Run(() => RunClientAsync(tcpClient, port)); | ||
lock (tasks) | ||
{ | ||
tasks.Add(task); | ||
} | ||
|
||
_ = task.ContinueWith( | ||
(t) => | ||
{ | ||
lock (tasks) | ||
{ | ||
tasks.Remove(t); | ||
if (tasks.Count == 0) | ||
{ | ||
allTasksCompleted?.TrySetResult(); | ||
} | ||
} | ||
}); | ||
} | ||
} | ||
catch (OperationCanceledException) when (this.cts.IsCancellationRequested) | ||
{ | ||
// Ignore | ||
} | ||
catch (SocketException) when (this.cts.IsCancellationRequested) | ||
{ | ||
// Ignore | ||
} | ||
catch (Exception ex) | ||
{ | ||
this.trace.Error($"Error accepting TCP client for port {port}: ${ex}"); | ||
} | ||
|
||
lock (tasks) | ||
{ | ||
if (tasks.Count == 0) | ||
{ | ||
return; | ||
} | ||
|
||
allTasksCompleted = new TaskCompletionSource(); | ||
} | ||
|
||
await allTasksCompleted.Task; | ||
} | ||
|
||
private async Task RunClientAsync(TcpClient tcpClient, int port) | ||
{ | ||
try | ||
{ | ||
using var disposable = tcpClient; | ||
|
||
this.trace.Info($"Accepted client connection to TCP port {port}"); | ||
await using var stream = tcpClient.GetStream(); | ||
|
||
var bytes = Encoding.UTF8.GetBytes(port.ToString(CultureInfo.InvariantCulture)); | ||
await stream.WriteAsync(bytes); | ||
|
||
} | ||
catch (OperationCanceledException) when (this.cts.IsCancellationRequested) | ||
{ | ||
// Ignore | ||
} | ||
catch (SocketException) when (this.cts.IsCancellationRequested) | ||
{ | ||
// Ignore | ||
} | ||
catch (Exception ex) | ||
{ | ||
this.trace.Error($"Error handling TCP client on listener running on port {port}: ${ex}"); | ||
} | ||
} | ||
|
||
private void StopListeners() | ||
{ | ||
foreach (var listener in this.listeners) | ||
{ | ||
listener.Stop(); | ||
} | ||
|
||
this.listeners.Clear(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,32 @@ | ||
using System.Net; | ||
using System.Globalization; | ||
using System.Net; | ||
using System.Net.Sockets; | ||
using System.Text; | ||
|
||
namespace Microsoft.DevTunnels.Test; | ||
|
||
internal static class TcpUtils | ||
{ | ||
public static int GetAvailableTcpPort() | ||
public static int GetAvailableTcpPort(bool canReuseAddress = true) | ||
{ | ||
// Get any available local tcp port | ||
var l = new TcpListener(IPAddress.Loopback, 0); | ||
if (!canReuseAddress) | ||
{ | ||
l.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, false); | ||
} | ||
|
||
l.Start(); | ||
int port = ((IPEndPoint)l.LocalEndpoint).Port; | ||
l.Stop(); | ||
return port; | ||
} | ||
|
||
public static async Task<int> ReadIntToEndAsync(this Stream stream, CancellationToken cancellation) | ||
{ | ||
var buffer = new byte[1024]; | ||
var length = await stream.ReadAsync(buffer, cancellation); | ||
var text = Encoding.UTF8.GetString(buffer, 0, length); | ||
return int.Parse(text, CultureInfo.InvariantCulture); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
using System.Diagnostics; | ||
using System.Text; | ||
using Xunit.Abstractions; | ||
|
||
namespace Microsoft.DevTunnels.Test; | ||
|
||
internal sealed class XunitTraceListener : TraceListener | ||
{ | ||
private readonly ITestOutputHelper output; | ||
private readonly StringBuilder currentLine = new (); | ||
private readonly DateTimeOffset loggingStart = DateTimeOffset.UtcNow; | ||
private DateTimeOffset? messageStart; | ||
|
||
public XunitTraceListener(ITestOutputHelper output) | ||
{ | ||
this.output = output; | ||
} | ||
|
||
public override void Write(string message) | ||
{ | ||
this.messageStart ??= DateTimeOffset.UtcNow; | ||
this.currentLine.Append(message); | ||
} | ||
|
||
public override void WriteLine(string message) | ||
{ | ||
var messageTime = (this.messageStart ?? DateTimeOffset.UtcNow) - this.loggingStart; | ||
this.output.WriteLine($"{messageTime} {this.currentLine}{message}"); | ||
this.currentLine.Clear(); | ||
this.messageStart = null; | ||
} | ||
} |