Implementing call monitor endpoint
This commit is contained in:
154
src/FritzCallMonitor/CallMonitorClient.cs
Normal file
154
src/FritzCallMonitor/CallMonitorClient.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AMWD.Net.Api.Fritz.CallMonitor.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace AMWD.Net.Api.Fritz.CallMonitor
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a client for monitoring call events from a FRITZ!Box.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The FRITZ!Box has a built-in realtime call monitoring feature that can be accessed via TCP on port 1012.
|
||||
/// </remarks>
|
||||
public class CallMonitorClient : IDisposable
|
||||
{
|
||||
private bool _isDisposed;
|
||||
|
||||
private ILogger? _logger;
|
||||
private readonly ReconnectTcpClient _client;
|
||||
private readonly CancellationTokenSource _disposeCts;
|
||||
|
||||
private Task _monitorTask = Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CallMonitorClient"/> class.
|
||||
/// </summary>
|
||||
/// <param name="host">The hostname or IP address of the FRITZ!Box to monitor.</param>
|
||||
/// <param name="port">The port to connect to (Default: 1012).</param>
|
||||
/// <exception cref="ArgumentNullException">The hostname is not set.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">The port is not in valid range of 1 to 65535.</exception>
|
||||
public CallMonitorClient(string host, int port = 1012)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
throw new ArgumentNullException(nameof(host));
|
||||
|
||||
if (port <= ushort.MinValue || ushort.MaxValue < port)
|
||||
throw new ArgumentOutOfRangeException(nameof(port));
|
||||
|
||||
_disposeCts = new CancellationTokenSource();
|
||||
_client = new ReconnectTcpClient(host, port) { OnConnected = OnConnected };
|
||||
|
||||
// Start the client in the background
|
||||
_client.StartAsync(_disposeCts.Token).Forget();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a call monitoring event is raised.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The event provides details using the <see cref="CallMonitorEventArgs"/> parameter.
|
||||
/// </remarks>
|
||||
public event EventHandler<CallMonitorEventArgs>? OnEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a logger instance.
|
||||
/// </summary>
|
||||
public ILogger? Logger
|
||||
{
|
||||
get => _logger;
|
||||
set
|
||||
{
|
||||
_logger = value;
|
||||
_client.Logger = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases all resources used by the current instance of the <see cref="CallMonitorClient"/>.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed)
|
||||
return;
|
||||
|
||||
_isDisposed = true;
|
||||
|
||||
_disposeCts.Cancel();
|
||||
|
||||
try
|
||||
{
|
||||
_monitorTask.Wait();
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
|
||||
_client.Dispose();
|
||||
_disposeCts.Dispose();
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private Task OnConnected(ReconnectTcpClient client)
|
||||
{
|
||||
_monitorTask = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var stream = client.GetStream();
|
||||
if (stream == null)
|
||||
return;
|
||||
|
||||
string? buffer = null;
|
||||
byte[] rawBuffer = new byte[4096];
|
||||
while (!_disposeCts.IsCancellationRequested && client.IsConnected)
|
||||
{
|
||||
try
|
||||
{
|
||||
int bytesRead = await stream.ReadAsync(rawBuffer, 0, rawBuffer.Length, _disposeCts.Token);
|
||||
string data = Encoding.UTF8.GetString(rawBuffer, 0, bytesRead);
|
||||
|
||||
if (buffer != null)
|
||||
{
|
||||
data = buffer + data;
|
||||
buffer = null;
|
||||
}
|
||||
|
||||
if (!data.EndsWith("\n"))
|
||||
{
|
||||
buffer = data;
|
||||
continue;
|
||||
}
|
||||
|
||||
string[] lines = data.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (string line in lines)
|
||||
{
|
||||
var eventArgs = CallMonitorEventArgs.Parse(line);
|
||||
if (eventArgs == null)
|
||||
continue;
|
||||
|
||||
Task.Run(() => OnEvent?.Invoke(this, eventArgs), _disposeCts.Token).Forget();
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (_disposeCts.IsCancellationRequested)
|
||||
{
|
||||
// Client was stopped or disposed.
|
||||
return;
|
||||
}
|
||||
catch (Exception ex) when (!_disposeCts.IsCancellationRequested)
|
||||
{
|
||||
Logger?.LogError(ex, "Error while reading from the call monitor stream.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/FritzCallMonitor/Enums/EventType.cs
Normal file
34
src/FritzCallMonitor/Enums/EventType.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace AMWD.Net.Api.Fritz.CallMonitor
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the types of events that can occur during a call lifecycle on a FRITZ!Box.
|
||||
/// </summary>
|
||||
public enum EventType
|
||||
{
|
||||
/// <summary>
|
||||
/// A call is incoming to the Fritz!Box.
|
||||
/// </summary>
|
||||
[EnumMember(Value = "RING")]
|
||||
Ring = 1,
|
||||
|
||||
/// <summary>
|
||||
/// A call is connected - the parties are now talking.
|
||||
/// </summary>
|
||||
[EnumMember(Value = "CONNECT")]
|
||||
Connect = 2,
|
||||
|
||||
/// <summary>
|
||||
/// A call is disconnected - one party has hung up.
|
||||
/// </summary>
|
||||
[EnumMember(Value = "DISCONNECT")]
|
||||
Disconnect = 3,
|
||||
|
||||
/// <summary>
|
||||
/// A call is outgoing from the Fritz!Box.
|
||||
/// </summary>
|
||||
[EnumMember(Value = "CALL")]
|
||||
Call = 4,
|
||||
}
|
||||
}
|
||||
98
src/FritzCallMonitor/EventArgs/CallMonitorEventArgs.cs
Normal file
98
src/FritzCallMonitor/EventArgs/CallMonitorEventArgs.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace AMWD.Net.Api.Fritz.CallMonitor
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides data for call monitoring events, including details about the call type, identifiers, and optional metadata.
|
||||
/// </summary>
|
||||
public class CallMonitorEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the timestamp of the event.
|
||||
/// </summary>
|
||||
public DateTimeOffset? Timestamp { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of event.
|
||||
/// </summary>
|
||||
public EventType? Event { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the connection ID.
|
||||
/// </summary>
|
||||
public int? ConnectionId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the line / port of signaled.
|
||||
/// </summary>
|
||||
public int? LinePort { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the external number displayed in the FRITZ!Box.
|
||||
/// </summary>
|
||||
public string? CallerNumber { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the internal number registered in the FRITZ!Box.
|
||||
/// </summary>
|
||||
public string? CalleeNumber { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the duarion of the call (only on <see cref="EventType.Disconnect"/> event).
|
||||
/// </summary>
|
||||
public TimeSpan? Duration { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a line from the call monitor output into a <see cref="CallMonitorEventArgs"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="line">The line from the call monitor output.</param>
|
||||
/// <returns><see langword="null"/> when parsing fails, otherwise a new instance of the <see cref="CallMonitorEventArgs"/>.</returns>
|
||||
internal static CallMonitorEventArgs? Parse(string line)
|
||||
{
|
||||
string[] columns = line.Trim().Split(';');
|
||||
|
||||
if (!DateTimeOffset.TryParseExact(columns[0], "dd.MM.yy HH:mm:ss", null, DateTimeStyles.None, out var timestamp))
|
||||
return null;
|
||||
|
||||
if (!Enum.TryParse<EventType>(columns[1], true, out var eventType))
|
||||
return null;
|
||||
|
||||
if (!int.TryParse(columns[2], out int connectionId))
|
||||
return null;
|
||||
|
||||
var args = new CallMonitorEventArgs
|
||||
{
|
||||
Timestamp = timestamp,
|
||||
Event = eventType,
|
||||
ConnectionId = connectionId
|
||||
};
|
||||
|
||||
switch (eventType)
|
||||
{
|
||||
case EventType.Ring:
|
||||
args.CallerNumber = columns[3];
|
||||
args.CalleeNumber = columns[4];
|
||||
break;
|
||||
|
||||
case EventType.Connect:
|
||||
args.LinePort = int.TryParse(columns[3], out int connectLinePort) ? connectLinePort : null;
|
||||
args.CallerNumber = columns[4];
|
||||
break;
|
||||
|
||||
case EventType.Disconnect:
|
||||
if (int.TryParse(columns[3], out int durationSeconds))
|
||||
args.Duration = TimeSpan.FromSeconds(durationSeconds);
|
||||
break;
|
||||
|
||||
case EventType.Call:
|
||||
args.LinePort = int.TryParse(columns[3], out int callLinePort) ? callLinePort : null;
|
||||
args.CalleeNumber = columns[4];
|
||||
args.CallerNumber = columns[5];
|
||||
break;
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/FritzCallMonitor/Utils/Extensions.cs
Normal file
21
src/FritzCallMonitor/Utils/Extensions.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace AMWD.Net.Api.Fritz.CallMonitor.Utils
|
||||
{
|
||||
internal static class Extensions
|
||||
{
|
||||
public static async void Forget(this Task task, ILogger? logger = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await task.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogError(ex, "An error occurred in a fire-and-forget task.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
222
src/FritzCallMonitor/Utils/ReconnectTcpClient.cs
Normal file
222
src/FritzCallMonitor/Utils/ReconnectTcpClient.cs
Normal file
@@ -0,0 +1,222 @@
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AMWD.Net.Api.Fritz.CallMonitor.Wrappers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace AMWD.Net.Api.Fritz.CallMonitor.Utils
|
||||
{
|
||||
internal class ReconnectTcpClient : IDisposable
|
||||
{
|
||||
private bool _isDisposed;
|
||||
private readonly SemaphoreSlim _connectLock = new(1, 1);
|
||||
|
||||
private readonly string _host;
|
||||
private readonly int _port;
|
||||
|
||||
private TcpClientWrapper? _tcpClient;
|
||||
private readonly TcpClientWrapperFactory _tcpClientFactory = new();
|
||||
|
||||
private CancellationTokenSource? _stopCts;
|
||||
private Task _monitorTask = Task.CompletedTask;
|
||||
|
||||
public ReconnectTcpClient(string host, int port)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
throw new ArgumentNullException(nameof(host), "The host is required.");
|
||||
|
||||
if (port <= ushort.MinValue || ushort.MaxValue < port)
|
||||
throw new ArgumentOutOfRangeException(nameof(port), $"The port must be between {ushort.MinValue + 1} and {ushort.MaxValue}.");
|
||||
|
||||
_host = host;
|
||||
_port = port;
|
||||
}
|
||||
|
||||
public virtual bool IsConnected => _tcpClient?.Connected ?? false;
|
||||
|
||||
public virtual ILogger? Logger { get; set; }
|
||||
|
||||
public virtual Func<ReconnectTcpClient, Task>? OnConnected { get; set; }
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
if (_isDisposed)
|
||||
return;
|
||||
|
||||
_isDisposed = true;
|
||||
|
||||
StopAsyncInternally(CancellationToken.None).Wait();
|
||||
|
||||
_connectLock.Dispose();
|
||||
}
|
||||
|
||||
public virtual NetworkStreamWrapper? GetStream()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return _tcpClient?.GetStream();
|
||||
}
|
||||
|
||||
public virtual async Task StartAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
_stopCts = new CancellationTokenSource();
|
||||
|
||||
using (var combinedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _stopCts.Token))
|
||||
{
|
||||
await ConnectWithRetryAsync(combinedTokenSource.Token).ConfigureAwait(false);
|
||||
if (combinedTokenSource.IsCancellationRequested)
|
||||
return;
|
||||
}
|
||||
|
||||
_monitorTask = Task.Run(() => MonitorConnectionAsync(_stopCts.Token), _stopCts.Token);
|
||||
}
|
||||
|
||||
public virtual Task StopAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return StopAsyncInternally(cancellationToken);
|
||||
}
|
||||
|
||||
private async Task StopAsyncInternally(CancellationToken cancellationToken)
|
||||
{
|
||||
var stopTask = Task.Run(async () =>
|
||||
{
|
||||
_stopCts?.Cancel();
|
||||
try
|
||||
{
|
||||
await _monitorTask.ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
|
||||
_monitorTask = Task.CompletedTask;
|
||||
|
||||
_stopCts?.Dispose();
|
||||
_stopCts = null;
|
||||
|
||||
_tcpClient?.Dispose();
|
||||
_tcpClient = null;
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
await Task.WhenAny(stopTask, Task.Delay(Timeout.Infinite, cancellationToken)).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
}
|
||||
|
||||
private async Task ConnectWithRetryAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _connectLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (_isDisposed || IsConnected)
|
||||
return;
|
||||
|
||||
int delay = 250;
|
||||
while (!cancellationToken.IsCancellationRequested && !_isDisposed)
|
||||
{
|
||||
try
|
||||
{
|
||||
_tcpClient?.Dispose();
|
||||
|
||||
_tcpClient = _tcpClientFactory.Create();
|
||||
_tcpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
var connectTask = _tcpClient.ConnectAsync(_host, _port, cancellationToken);
|
||||
#else
|
||||
var connectTask = _tcpClient.ConnectAsync(_host, _port);
|
||||
#endif
|
||||
var completedTask = await Task.WhenAny(connectTask, Task.Delay(1000, cancellationToken)).ConfigureAwait(false);
|
||||
if (completedTask != connectTask)
|
||||
throw new TimeoutException("Connection attempt timed out.");
|
||||
|
||||
if (OnConnected != null)
|
||||
await OnConnected(this).ConfigureAwait(false);
|
||||
|
||||
return;
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Client was stopped or disposed.
|
||||
return;
|
||||
}
|
||||
catch (Exception ex) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
delay *= 2;
|
||||
|
||||
// Limit the delay to a maximum of 1 minute.
|
||||
if (delay > 60 * 1000)
|
||||
delay = 60 * 1000;
|
||||
|
||||
Logger?.LogWarning(ex, $"Failed to connect to {_host}:{_port}. Retrying in {delay}ms...");
|
||||
try
|
||||
{
|
||||
await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_connectLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MonitorConnectionAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] buffer = new byte[1];
|
||||
while (!cancellationToken.IsCancellationRequested && !_isDisposed)
|
||||
{
|
||||
if (!IsConnected)
|
||||
{
|
||||
await ConnectWithRetryAsync(cancellationToken).ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
var stream = _tcpClient?.GetStream();
|
||||
if (stream != null && stream.CanRead)
|
||||
{
|
||||
bool disconnected = false;
|
||||
try
|
||||
{
|
||||
// Attempt to read zero bytes to check if the connection is still alive.
|
||||
// Should return immediately if the connection is still active.
|
||||
// So the timeout of 1sec is more than enough.
|
||||
var readTask = stream.ReadAsync(buffer, 0, 0, cancellationToken);
|
||||
var completedTask = await Task.WhenAny(readTask, Task.Delay(1000, cancellationToken)).ConfigureAwait(false);
|
||||
if (completedTask != readTask)
|
||||
continue; // Timeout
|
||||
}
|
||||
catch
|
||||
{
|
||||
disconnected = true;
|
||||
}
|
||||
|
||||
if (disconnected || !IsConnected)
|
||||
await ConnectWithRetryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Check for an active connection every 5 seconds.
|
||||
await Task.Delay(5000, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{ }
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (_isDisposed)
|
||||
throw new ObjectDisposedException(GetType().FullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/FritzCallMonitor/Wrappers/NetworkStreamWrapper.cs
Normal file
32
src/FritzCallMonitor/Wrappers/NetworkStreamWrapper.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AMWD.Net.Api.Fritz.CallMonitor.Wrappers
|
||||
{
|
||||
/// <inheritdoc cref="NetworkStream"/>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
internal class NetworkStreamWrapper : IDisposable
|
||||
{
|
||||
private readonly NetworkStream _networkStream;
|
||||
|
||||
public NetworkStreamWrapper(NetworkStream networkStream)
|
||||
{
|
||||
_networkStream = networkStream;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Stream.Dispose()"/>
|
||||
public virtual void Dispose()
|
||||
=> _networkStream.Dispose();
|
||||
|
||||
/// <inheritdoc cref="NetworkStream.CanRead"/>
|
||||
public virtual bool CanRead =>
|
||||
_networkStream.CanRead;
|
||||
|
||||
/// <inheritdoc cref="Stream.ReadAsync(byte[], int, int, CancellationToken)"/>
|
||||
public virtual Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
=> _networkStream.ReadAsync(buffer, offset, count, cancellationToken);
|
||||
}
|
||||
}
|
||||
25
src/FritzCallMonitor/Wrappers/SocketWrapper.cs
Normal file
25
src/FritzCallMonitor/Wrappers/SocketWrapper.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace AMWD.Net.Api.Fritz.CallMonitor.Wrappers
|
||||
{
|
||||
/// <inheritdoc cref="Socket"/>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
internal class SocketWrapper : IDisposable
|
||||
{
|
||||
private readonly Socket _socket;
|
||||
|
||||
public SocketWrapper(Socket socket)
|
||||
{
|
||||
_socket = socket;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Socket.Dispose()"/>
|
||||
public virtual void Dispose()
|
||||
=> _socket.Dispose();
|
||||
|
||||
/// <inheritdoc cref="Socket.SetSocketOption(SocketOptionLevel, SocketOptionName, bool)"/>
|
||||
public virtual void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, bool optionValue)
|
||||
=> _socket.SetSocketOption(optionLevel, optionName, optionValue);
|
||||
}
|
||||
}
|
||||
42
src/FritzCallMonitor/Wrappers/TcpClientWrapper.cs
Normal file
42
src/FritzCallMonitor/Wrappers/TcpClientWrapper.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AMWD.Net.Api.Fritz.CallMonitor.Wrappers
|
||||
{
|
||||
/// <inheritdoc cref="TcpClient"/>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
internal class TcpClientWrapper : IDisposable
|
||||
{
|
||||
private readonly TcpClient _tcpClient;
|
||||
|
||||
public TcpClientWrapper()
|
||||
{
|
||||
_tcpClient = new TcpClient();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="TcpClient.Dispose()"/>
|
||||
public virtual void Dispose()
|
||||
=> _tcpClient.Dispose();
|
||||
|
||||
/// <inheritdoc cref="TcpClient.Client"/>
|
||||
public virtual SocketWrapper Client => new(_tcpClient.Client);
|
||||
|
||||
/// <inheritdoc cref="TcpClient.Connected"/>
|
||||
public virtual bool Connected => _tcpClient.Connected;
|
||||
|
||||
/// <inheritdoc cref="TcpClient.ConnectAsync(string, int)"/>
|
||||
public virtual Task ConnectAsync(string host, int port)
|
||||
=> _tcpClient.ConnectAsync(host, port);
|
||||
|
||||
/// <inheritdoc cref="TcpClient.GetStream()"/>
|
||||
public virtual NetworkStreamWrapper GetStream()
|
||||
=> new(_tcpClient.GetStream());
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
public virtual Task ConnectAsync(string host, int port, CancellationToken cancellationToken)
|
||||
=> _tcpClient.ConnectAsync(host, port, cancellationToken).AsTask();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
18
src/FritzCallMonitor/Wrappers/TcpClientWrapperFactory.cs
Normal file
18
src/FritzCallMonitor/Wrappers/TcpClientWrapperFactory.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace AMWD.Net.Api.Fritz.CallMonitor.Wrappers
|
||||
{
|
||||
/// <summary>
|
||||
/// Factory for creating instances of <see cref="TcpClientWrapper"/>.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
internal class TcpClientWrapperFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new instance of <see cref="TcpClientWrapper"/>.
|
||||
/// </summary>
|
||||
public virtual TcpClientWrapper Create()
|
||||
{
|
||||
var client = new TcpClientWrapper();
|
||||
return client;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user