Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[iOS] Add basic classes to support the usage of tunnels. #66

Merged
merged 10 commits into from
Apr 23, 2020
6 changes: 4 additions & 2 deletions src/Microsoft.DotNet.XHarness.CLI/iOS/iOSTestCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ protected override async Task<ExitCode> InvokeInternal()
var processManager = new ProcessManager(_arguments.XcodeRoot, _arguments.MlaunchPath);
var deviceLoader = new HardwareDeviceLoader(processManager);
var simulatorLoader = new SimulatorLoader(processManager);
var tunnelBore = new TunnelBore(processManager);

var logs = new Logs(_arguments.OutputDirectory);

Expand All @@ -61,7 +62,7 @@ protected override async Task<ExitCode> InvokeInternal()

foreach (TestTarget target in _arguments.TestTargets)
{
var exitCodeForRun = await RunTest(target, logs, processManager, deviceLoader, simulatorLoader, cts.Token);
var exitCodeForRun = await RunTest(target, logs, processManager, deviceLoader, simulatorLoader, tunnelBore, cts.Token);

if (exitCodeForRun != ExitCode.SUCCESS)
{
Expand All @@ -77,6 +78,7 @@ private async Task<ExitCode> RunTest(TestTarget target,
ProcessManager processManager,
IHardwareDeviceLoader deviceLoader,
ISimulatorLoader simulatorLoader,
ITunnelBore tunnelBore,
CancellationToken cancellationToken = default)
{
_log.LogInformation($"Starting test for {target.AsString()}{ (_arguments.DeviceName != null ? " targeting " + _arguments.DeviceName : null) }..");
Expand Down Expand Up @@ -143,7 +145,7 @@ private async Task<ExitCode> RunTest(TestTarget target,
processManager,
deviceLoader,
simulatorLoader,
new SimpleListenerFactory(),
new SimpleListenerFactory(tunnelBore),
new CrashSnapshotReporterFactory(processManager),
new CaptureLogFactory(),
new DeviceLogCapturerFactory(processManager),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,4 +318,21 @@ public VerbosityArgument()

public override string AsCommandLineArgument() => "-v";
}

/// <summary>
/// Create a tcp tunnel with the iOS device from the host.
/// </summary>
public sealed class TcpTunnelArgument : MlaunchArgument
{
readonly int port;

public TcpTunnelArgument(int port)
{
if (port <= 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

impossible for this to be null or > 65535?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

int cannot be null, if is too large, the exception will be raised later

throw new ArgumentOutOfRangeException(nameof(port));
this.port = port;
}

public override string AsCommandLineArgument() => $"--tcp-tunnel={port}:{port}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,27 @@ public interface ISimpleListenerFactory
ILog listenerLog,
bool isSimulator,
bool autoExit,
bool xmlOutput);
bool xmlOutput,
bool useTcpTunnel);
}

public class SimpleListenerFactory : ISimpleListenerFactory
{

public ITunnelBore TunnelBore { get; private set; }

public SimpleListenerFactory(ITunnelBore tunnelBore)
{
TunnelBore = tunnelBore ?? throw new ArgumentNullException(nameof(tunnelBore));
}

public (ListenerTransport transport, ISimpleListener listener, string listenerTempFile) Create(RunMode mode,
ILog log,
ILog listenerLog,
bool isSimulator,
bool autoExit,
bool xmlOutput)
bool xmlOutput,
bool useTcpTunnel)
{
string listenerTempFile = null;
ISimpleListener listener;
Expand All @@ -57,7 +66,7 @@ public class SimpleListenerFactory : ISimpleListenerFactory
listener = new SimpleHttpListener(log, listenerLog, autoExit, xmlOutput);
break;
case ListenerTransport.Tcp:
listener = new SimpleTcpListener(log, listenerLog, autoExit, xmlOutput);
listener = new SimpleTcpListener(log, listenerLog, autoExit, xmlOutput, useTcpTunnel);
break;
default:
throw new NotImplementedException("Unknown type of listener");
Expand Down
116 changes: 106 additions & 10 deletions src/Microsoft.DotNet.XHarness.iOS.Shared/Listeners/SimpleTcpListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,64 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.XHarness.iOS.Shared.Logging;

namespace Microsoft.DotNet.XHarness.iOS.Shared.Listeners
{
public class SimpleTcpListener : SimpleListener
public class SimpleTcpListener : SimpleListener, ITunnelListener
{
readonly bool autoExit;
const int _timeOutInit = 100;
const int _timeOutIincrement = 250;
readonly bool _autoExit;
readonly bool _useTcpTunnel = true;

byte[] buffer = new byte[16 * 1024];
TcpListener server;
TcpClient client;

public SimpleTcpListener(ILog log, ILog testLog, bool autoExit, bool xmlOutput) : base(log, testLog, xmlOutput)
public TaskCompletionSource<bool> TunnelHoleThrough { get; private set; } = new TaskCompletionSource<bool>();

public SimpleTcpListener(ILog log, ILog testLog, bool autoExit, bool xmlOutput, bool tunnel = false) : base(log, testLog, xmlOutput)
{
this.autoExit = autoExit;
_autoExit = autoExit;
_useTcpTunnel = tunnel;
}

public SimpleTcpListener(int port, ILog log, ILog testLog, bool autoExit, bool xmlOutput, bool tunnel = false) : this(log, testLog, autoExit, xmlOutput, tunnel)
=> Port = port;

protected override void Stop()
{
server.Stop();
client?.Close();
client?.Dispose();
server?.Stop();
}

public override void Initialize()
{
if (_useTcpTunnel && Port != 0)
return;

server = new TcpListener(Address, Port);
server.Start();

if (Port == 0)
Port = ((IPEndPoint)server.LocalEndpoint).Port;

if (_useTcpTunnel)
{
// close the listener. We have a port. This is not the best
// way to find a free port, but there is nothing we can do
// better than this.

server.Stop();
}
}

protected override void Start()
void StartNetworkTcp()
{
bool processed;

Expand All @@ -44,18 +71,18 @@ protected override void Start()
do
{
Log.WriteLine("Test log server listening on: {0}:{1}", Address, Port);
using (TcpClient client = server.AcceptTcpClient())
using (client = server.AcceptTcpClient())
{
client.ReceiveBufferSize = buffer.Length;
processed = Processing(client);
}
} while (!autoExit || !processed);
} while (!_autoExit || !processed);
}
catch (Exception e)
{
var se = e as SocketException;
if (se == null || se.SocketErrorCode != SocketError.Interrupted)
Console.WriteLine("[{0}] : {1}", DateTime.Now, e);
Log.WriteLine("[{0}] : {1}", DateTime.Now, e);
}
finally
{
Expand All @@ -70,6 +97,76 @@ protected override void Start()
}
}

void StartTcpTunnel()
{
if (!TunnelHoleThrough.Task.Result)
{ // do nothing until the tunnel is ready
throw new InvalidOperationException("Tcp tunnel could not be initialized.");
}
bool processed;
try
{
int timeout = _timeOutInit; ;
var watch = new System.Diagnostics.Stopwatch();
watch.Start();
while (true)
{
try
{
client = new TcpClient("localhost", Port);
Log.WriteLine("Test log server listening on: {0}:{1}", Address, Port);
// let the device know we are ready!
var stream = client.GetStream();
var ping = Encoding.UTF8.GetBytes("ping");
stream.Write(ping, 0, ping.Length);
break;

}
catch (SocketException ex)
{
if (timeout == _timeOutInit && watch.ElapsedMilliseconds > 20000)
{
timeout = _timeOutIincrement; // Switch to a 250ms timeout after 20 seconds
}
else if (watch.ElapsedMilliseconds > 120000)
{
// Give up after 2 minutes.
throw ex;
}
Log.WriteLine($"Could not connet to tcp tunnel. Rerrying in {timeout} milliseconds.");
Thread.Sleep(timeout);
}
}
do
{
client.ReceiveBufferSize = buffer.Length;
processed = Processing(client);
} while (!_autoExit || !processed);
}
catch (Exception e)
{
var se = e as SocketException;
if (se == null || se.SocketErrorCode != SocketError.Interrupted)
Log.WriteLine("[{0}] : {1}", DateTime.Now, e);
}
finally
{
Finished();
}
}

protected override void Start()
{
if (_useTcpTunnel)
{
StartTcpTunnel();
}
else
{
StartNetworkTcp();
}
}

bool Processing(TcpClient client)
{
Connected(client.Client.RemoteEndPoint.ToString());
Expand All @@ -95,4 +192,3 @@ bool Processing(TcpClient client)
}
}
}

96 changes: 96 additions & 0 deletions src/Microsoft.DotNet.XHarness.iOS.Shared/Listeners/TcpTunnel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.XHarness.iOS.Shared.Execution;
using Microsoft.DotNet.XHarness.iOS.Shared.Execution.Mlaunch;
using Microsoft.DotNet.XHarness.iOS.Shared.Logging;

namespace Microsoft.DotNet.XHarness.iOS.Shared.Listeners
{

// interface to be implemented by those listeners that can use a tcp tunnel
public interface ITunnelListener : ISimpleListener
{
TaskCompletionSource<bool> TunnelHoleThrough { get; }
}

// interface implemented by a tcp tunnel between the host and the device.
public interface ITcpTunnel : IAsyncDisposable
{
public void Open(string device, ITunnelListener simpleListener, TimeSpan timeout, ILog mainLog);
public Task Close();
public Task<bool> Started { get; }
}

// represents a tunnel created between a device and a host. This tunnel allows communication between
// the host and the device via the usb cable.
public class TcpTunnel : ITcpTunnel
{
readonly object _processExecutionLock = new object();
readonly IProcessManager _processManager;

Task<ProcessExecutionResult> _tcpTunnelExecutionTask = null;
CancellationTokenSource _cancellationToken;

public TaskCompletionSource<bool> startedCompletionSource { get; private set; } = new TaskCompletionSource<bool>();
public Task<bool> Started => startedCompletionSource.Task;
public int Port { get; private set; }

public TcpTunnel(IProcessManager processManager)
{
_processManager = processManager ?? throw new ArgumentNullException(nameof(processManager));
}

public void Open(string device, ITunnelListener simpleListener, TimeSpan timeout, ILog mainLog)
{
if (device == null)
throw new ArgumentNullException(nameof(device));
if (simpleListener == null)
throw new ArgumentNullException(nameof(simpleListener));
if (mainLog == null)
throw new ArgumentNullException(nameof(mainLog));

lock (_processExecutionLock)
{
// launch app, but do not await for the result, since we need to create the tunnel
var tcpArgs = new MlaunchArguments {
new TcpTunnelArgument (simpleListener.Port),
new VerbosityArgument (),
new DeviceNameArgument (device),
};

// use a cancelation token, later will be used to kill the tcp tunnel process
_cancellationToken = new CancellationTokenSource();
mainLog.WriteLine($"Starting tcp tunnel between mac port: {simpleListener.Port} and devie port {simpleListener.Port}.");
Port = simpleListener.Port;
var tunnelbackLog = new CallbackLog((line) =>
{
mainLog.WriteLine($"The tcp tunnel output is {line}");
if (line.Contains("Tcp tunnel started on device"))
{
mainLog.Write($"Tcp tunnel created on port {simpleListener.Port}");
startedCompletionSource.TrySetResult(true);
simpleListener.TunnelHoleThrough.TrySetResult(true);
}
});
// do not await since we are going to be running the process in parallel
_tcpTunnelExecutionTask = _processManager.ExecuteCommandAsync(tcpArgs, tunnelbackLog, timeout, cancellationToken: _cancellationToken.Token);
}
}

public async Task Close()
{
if (_cancellationToken == null)
throw new InvalidOperationException("Cannot close tunnel that was not opened.");
// cancel process and wait for it to terminate, else we might want to start a second tunnel to the same device
// which is going to give problems.
_cancellationToken.Cancel();
await _tcpTunnelExecutionTask;
}

public async ValueTask DisposeAsync()
{
await Close();
}
}
}
Loading