diff --git a/EchoTcpServer/EchoServer.cs b/EchoTcpServer/EchoServer.cs new file mode 100644 index 0000000..e1288f5 --- /dev/null +++ b/EchoTcpServer/EchoServer.cs @@ -0,0 +1,71 @@ +using EchoTcpServer.Networking; + +namespace EchoTcpServer; + +public class EchoServer +{ + private readonly ITcpListener _listener; + private readonly Action _log; + private CancellationTokenSource _cts = new(); + + public EchoServer(ITcpListener listener, Action? log = null) + { + _listener = listener; + _log = log ?? Console.WriteLine; + } + + public async Task StartAsync() + { + _listener.Start(); + _log("Server started."); + + while (!_cts.Token.IsCancellationRequested) + { + try + { + var stream = await _listener.AcceptClientStreamAsync(); + _log("Client connected."); + _ = Task.Run(() => HandleClientAsync(stream, _cts.Token)); + } + catch (ObjectDisposedException) + { + break; + } + } + + _log("Server shutdown."); + } + + public async Task HandleClientAsync(Stream stream, CancellationToken token) + { + using (stream) + { + try + { + byte[] buffer = new byte[8192]; + int bytesRead; + + while (!token.IsCancellationRequested + && (bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, token)) > 0) + { + await stream.WriteAsync(buffer, 0, bytesRead, token); + _log($"Echoed {bytesRead} bytes."); + } + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + _log($"Error: {ex.Message}"); + } + } + + _log("Client disconnected."); + } + + public void Stop() + { + _cts.Cancel(); + _listener.Stop(); + _cts.Dispose(); + _log("Server stopped."); + } +} diff --git a/EchoTcpServer/EchoServer.csproj b/EchoTcpServer/EchoServer.csproj index 2150e37..46e738e 100644 --- a/EchoTcpServer/EchoServer.csproj +++ b/EchoTcpServer/EchoServer.csproj @@ -5,6 +5,7 @@ net8.0 enable enable + EchoTcpServer diff --git a/EchoTcpServer/Networking/ITcpListener.cs b/EchoTcpServer/Networking/ITcpListener.cs new file mode 100644 index 0000000..9952cc0 --- /dev/null +++ b/EchoTcpServer/Networking/ITcpListener.cs @@ -0,0 +1,8 @@ +namespace EchoTcpServer.Networking; + +public interface ITcpListener +{ + void Start(); + void Stop(); + Task AcceptClientStreamAsync(); +} diff --git a/EchoTcpServer/Networking/TcpListenerWrapper.cs b/EchoTcpServer/Networking/TcpListenerWrapper.cs new file mode 100644 index 0000000..67b84a7 --- /dev/null +++ b/EchoTcpServer/Networking/TcpListenerWrapper.cs @@ -0,0 +1,23 @@ +using System.Net; +using System.Net.Sockets; + +namespace EchoTcpServer.Networking; + +public class TcpListenerWrapper : ITcpListener +{ + private readonly TcpListener _listener; + + public TcpListenerWrapper(int port) + { + _listener = new TcpListener(IPAddress.Any, port); + } + + public void Start() => _listener.Start(); + public void Stop() => _listener.Stop(); + + public async Task AcceptClientStreamAsync() + { + var client = await _listener.AcceptTcpClientAsync(); + return client.GetStream(); + } +} diff --git a/EchoTcpServer/Program.cs b/EchoTcpServer/Program.cs index 5966c57..ab9890f 100644 --- a/EchoTcpServer/Program.cs +++ b/EchoTcpServer/Program.cs @@ -1,173 +1,16 @@ -using System; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; +using EchoTcpServer; +using EchoTcpServer.Networking; -/// -/// This program was designed for test purposes only -/// Not for a review -/// -public class EchoServer -{ - private readonly int _port; - private TcpListener _listener; - private CancellationTokenSource _cancellationTokenSource; +var server = new EchoServer(new TcpListenerWrapper(5000)); +_ = Task.Run(() => server.StartAsync()); +using var sender = new UdpTimedSender("127.0.0.1", 60000); +Console.WriteLine("Press any key to stop sending..."); +sender.StartSending(5000); - public EchoServer(int port) - { - _port = port; - _cancellationTokenSource = new CancellationTokenSource(); - } +Console.WriteLine("Press 'q' to quit..."); +while (Console.ReadKey(intercept: true).Key != ConsoleKey.Q) { } - public async Task StartAsync() - { - _listener = new TcpListener(IPAddress.Any, _port); - _listener.Start(); - Console.WriteLine($"Server started on port {_port}."); - - while (!_cancellationTokenSource.Token.IsCancellationRequested) - { - try - { - TcpClient client = await _listener.AcceptTcpClientAsync(); - Console.WriteLine("Client connected."); - - _ = Task.Run(() => HandleClientAsync(client, _cancellationTokenSource.Token)); - } - catch (ObjectDisposedException) - { - // Listener has been closed - break; - } - } - - Console.WriteLine("Server shutdown."); - } - - private async Task HandleClientAsync(TcpClient client, CancellationToken token) - { - using (NetworkStream stream = client.GetStream()) - { - try - { - byte[] buffer = new byte[8192]; - int bytesRead; - - while (!token.IsCancellationRequested && (bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, token)) > 0) - { - // Echo back the received message - await stream.WriteAsync(buffer, 0, bytesRead, token); - Console.WriteLine($"Echoed {bytesRead} bytes to the client."); - } - } - catch (Exception ex) when (!(ex is OperationCanceledException)) - { - Console.WriteLine($"Error: {ex.Message}"); - } - finally - { - client.Close(); - Console.WriteLine("Client disconnected."); - } - } - } - - public void Stop() - { - _cancellationTokenSource.Cancel(); - _listener.Stop(); - _cancellationTokenSource.Dispose(); - Console.WriteLine("Server stopped."); - } - - public static async Task Main(string[] args) - { - EchoServer server = new EchoServer(5000); - - // Start the server in a separate task - _ = Task.Run(() => server.StartAsync()); - - string host = "127.0.0.1"; // Target IP - int port = 60000; // Target Port - int intervalMilliseconds = 5000; // Send every 3 seconds - - using (var sender = new UdpTimedSender(host, port)) - { - Console.WriteLine("Press any key to stop sending..."); - sender.StartSending(intervalMilliseconds); - - Console.WriteLine("Press 'q' to quit..."); - while (Console.ReadKey(intercept: true).Key != ConsoleKey.Q) - { - // Just wait until 'q' is pressed - } - - sender.StopSending(); - server.Stop(); - Console.WriteLine("Sender stopped."); - } - } -} - - -public class UdpTimedSender : IDisposable -{ - private readonly string _host; - private readonly int _port; - private readonly UdpClient _udpClient; - private Timer _timer; - - public UdpTimedSender(string host, int port) - { - _host = host; - _port = port; - _udpClient = new UdpClient(); - } - - public void StartSending(int intervalMilliseconds) - { - if (_timer != null) - throw new InvalidOperationException("Sender is already running."); - - _timer = new Timer(SendMessageCallback, null, 0, intervalMilliseconds); - } - - ushort i = 0; - - private void SendMessageCallback(object state) - { - try - { - //dummy data - Random rnd = new Random(); - byte[] samples = new byte[1024]; - rnd.NextBytes(samples); - i++; - - byte[] msg = (new byte[] { 0x04, 0x84 }).Concat(BitConverter.GetBytes(i)).Concat(samples).ToArray(); - var endpoint = new IPEndPoint(IPAddress.Parse(_host), _port); - - _udpClient.Send(msg, msg.Length, endpoint); - Console.WriteLine($"Message sent to {_host}:{_port} "); - } - catch (Exception ex) - { - Console.WriteLine($"Error sending message: {ex.Message}"); - } - } - - public void StopSending() - { - _timer?.Dispose(); - _timer = null; - } - - public void Dispose() - { - StopSending(); - _udpClient.Dispose(); - } -} \ No newline at end of file +sender.StopSending(); +server.Stop(); +Console.WriteLine("Sender stopped."); diff --git a/EchoTcpServer/UdpTimedSender.cs b/EchoTcpServer/UdpTimedSender.cs new file mode 100644 index 0000000..aabf3a6 --- /dev/null +++ b/EchoTcpServer/UdpTimedSender.cs @@ -0,0 +1,64 @@ +using System.Net; +using System.Net.Sockets; + +namespace EchoTcpServer; + +public class UdpTimedSender : IDisposable +{ + private readonly string _host; + private readonly int _port; + private readonly UdpClient _udpClient; + private readonly Random _rnd = new(); + private Timer? _timer; + private ushort _sequenceNumber; + + public UdpTimedSender(string host, int port) + { + _host = host; + _port = port; + _udpClient = new UdpClient(); + } + + public void StartSending(int intervalMilliseconds) + { + if (_timer != null) + throw new InvalidOperationException("Sender is already running."); + + _timer = new Timer(SendMessageCallback, null, 0, intervalMilliseconds); + } + + private void SendMessageCallback(object? state) + { + try + { + byte[] samples = new byte[1024]; + _rnd.NextBytes(samples); + _sequenceNumber++; + + byte[] msg = new byte[] { 0x04, 0x84 } + .Concat(BitConverter.GetBytes(_sequenceNumber)) + .Concat(samples) + .ToArray(); + + var endpoint = new IPEndPoint(IPAddress.Parse(_host), _port); + _udpClient.Send(msg, msg.Length, endpoint); + Console.WriteLine($"Message sent to {_host}:{_port}"); + } + catch (Exception ex) + { + Console.WriteLine($"Error sending message: {ex.Message}"); + } + } + + public void StopSending() + { + _timer?.Dispose(); + _timer = null; + } + + public void Dispose() + { + StopSending(); + _udpClient.Dispose(); + } +} diff --git a/EchoTcpServerTests/EchoServerTests.cs b/EchoTcpServerTests/EchoServerTests.cs new file mode 100644 index 0000000..b81c47f --- /dev/null +++ b/EchoTcpServerTests/EchoServerTests.cs @@ -0,0 +1,159 @@ +using Moq; +using EchoTcpServer; +using EchoTcpServer.Networking; + +namespace EchoTcpServerTests; + +[TestFixture] +public class EchoServerTests +{ + private Mock _listenerMock = null!; + private List _logs = null!; + private EchoServer _server = null!; + + [SetUp] + public void SetUp() + { + _listenerMock = new Mock(); + _logs = new List(); + _server = new EchoServer(_listenerMock.Object, msg => _logs.Add(msg)); + } + + [Test] + public void Stop_StopsListenerAndLogs() + { + _server.Stop(); + + _listenerMock.Verify(l => l.Stop(), Times.Once); + Assert.That(_logs, Contains.Item("Server stopped.")); + } + + [Test] + public async Task StartAsync_StartsListenerAndLogsShutdown() + { + _listenerMock + .Setup(l => l.AcceptClientStreamAsync()) + .ThrowsAsync(new ObjectDisposedException("listener")); + + await _server.StartAsync(); + + _listenerMock.Verify(l => l.Start(), Times.Once); + Assert.That(_logs, Contains.Item("Server started.")); + Assert.That(_logs, Contains.Item("Server shutdown.")); + } + + [Test] + public async Task StartAsync_AcceptsClientAndLogs() + { + _listenerMock.SetupSequence(l => l.AcceptClientStreamAsync()) + .ReturnsAsync(new MemoryStream(new byte[] { 1, 2, 3 })) + .ThrowsAsync(new ObjectDisposedException("listener")); + + await _server.StartAsync(); + + _listenerMock.Verify(l => l.AcceptClientStreamAsync(), Times.Exactly(2)); + Assert.That(_logs, Contains.Item("Client connected.")); + } + + [Test] + public async Task HandleClientAsync_EchoesInputToOutput() + { + var inputData = new byte[] { 0x41, 0x42, 0x43 }; // "ABC" + var inputStream = new MemoryStream(inputData); + var outputStream = new MemoryStream(); + var stream = new DuplexStream(inputStream, outputStream); + + await _server.HandleClientAsync(stream, CancellationToken.None); + + Assert.That(outputStream.ToArray(), Is.EqualTo(inputData)); + } + + [Test] + public async Task HandleClientAsync_LogsEchoedBytes() + { + var stream = new DuplexStream(new MemoryStream(new byte[] { 1, 2 }), new MemoryStream()); + + await _server.HandleClientAsync(stream, CancellationToken.None); + + Assert.That(_logs.Any(m => m.StartsWith("Echoed")), Is.True); + Assert.That(_logs, Contains.Item("Client disconnected.")); + } + + [Test] + public async Task HandleClientAsync_PreCancelledToken_DoesNotRead() + { + var cts = new CancellationTokenSource(); + cts.Cancel(); + + var mockStream = new Mock(); + mockStream.Setup(s => s.CanRead).Returns(true); + + await _server.HandleClientAsync(mockStream.Object, cts.Token); + + mockStream.Verify( + s => s.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + + [Test] + public async Task HandleClientAsync_StreamException_LogsError() + { + var mockStream = new Mock(); + mockStream + .Setup(s => s.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ThrowsAsync(new IOException("Connection reset by peer")); + + await _server.HandleClientAsync(mockStream.Object, CancellationToken.None); + + Assert.That(_logs.Any(m => m.StartsWith("Error:")), Is.True); + } + + [Test] + public async Task HandleClientAsync_EmptyStream_LogsDisconnected() + { + var emptyStream = new DuplexStream(new MemoryStream(Array.Empty()), new MemoryStream()); + + await _server.HandleClientAsync(emptyStream, CancellationToken.None); + + Assert.That(_logs, Contains.Item("Client disconnected.")); + } +} + +/// +/// Reads from one stream, writes to another — simulates a bidirectional client connection. +/// +internal sealed class DuplexStream : Stream +{ + private readonly Stream _read; + private readonly Stream _write; + + public DuplexStream(Stream read, Stream write) + { + _read = read; + _write = write; + } + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => true; + public override long Length => throw new NotSupportedException(); + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override void Flush() => _write.Flush(); + public override int Read(byte[] buffer, int offset, int count) => _read.Read(buffer, offset, count); + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken ct) + => _read.ReadAsync(buffer, offset, count, ct); + + public override void Write(byte[] buffer, int offset, int count) => _write.Write(buffer, offset, count); + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken ct) + => _write.WriteAsync(buffer, offset, count, ct); + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); +} diff --git a/EchoTcpServerTests/EchoTcpServerTests.csproj b/EchoTcpServerTests/EchoTcpServerTests.csproj new file mode 100644 index 0000000..b46e32b --- /dev/null +++ b/EchoTcpServerTests/EchoTcpServerTests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + false + true + + + + + + + + + + + + + + + + + + + + diff --git a/EchoTcpServerTests/UdpTimedSenderTests.cs b/EchoTcpServerTests/UdpTimedSenderTests.cs new file mode 100644 index 0000000..616b8b7 --- /dev/null +++ b/EchoTcpServerTests/UdpTimedSenderTests.cs @@ -0,0 +1,51 @@ +using EchoTcpServer; + +namespace EchoTcpServerTests; + +[TestFixture] +public class UdpTimedSenderTests +{ + [Test] + public void StartSending_WhenAlreadyRunning_ThrowsInvalidOperationException() + { + using var sender = new UdpTimedSender("127.0.0.1", 19999); + sender.StartSending(60_000); + + Assert.Throws(() => sender.StartSending(60_000)); + } + + [Test] + public void StopSending_WithoutStarting_DoesNotThrow() + { + using var sender = new UdpTimedSender("127.0.0.1", 19999); + + Assert.DoesNotThrow(() => sender.StopSending()); + } + + [Test] + public void StopSending_AfterStart_AllowsSecondStart() + { + using var sender = new UdpTimedSender("127.0.0.1", 19999); + sender.StartSending(60_000); + sender.StopSending(); + + Assert.DoesNotThrow(() => sender.StartSending(60_000)); + } + + [Test] + public void Dispose_WithoutStarting_DoesNotThrow() + { + var sender = new UdpTimedSender("127.0.0.1", 19999); + + Assert.DoesNotThrow(() => sender.Dispose()); + } + + [Test] + public void Dispose_WhileSending_DoesNotThrow() + { + var sender = new UdpTimedSender("127.0.0.1", 19999); + sender.StartSending(60_000); + + Assert.DoesNotThrow(() => sender.Dispose()); + } +} diff --git a/NetSdrClient.sln b/NetSdrClient.sln index 42431fb..959be4e 100644 --- a/NetSdrClient.sln +++ b/NetSdrClient.sln @@ -9,24 +9,68 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetSdrClientAppTests", "Net EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EchoServer", "EchoTcpServer\EchoServer.csproj", "{9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EchoTcpServerTests", "EchoTcpServerTests\EchoTcpServerTests.csproj", "{E6BC0BE9-A629-48C5-A08A-8373A21434F5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EchoTcpServer", "EchoTcpServer", "{DBDFFC83-AD19-3622-0FF4-C4288E16DE63}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Debug|x64.ActiveCfg = Debug|Any CPU + {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Debug|x64.Build.0 = Debug|Any CPU + {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Debug|x86.ActiveCfg = Debug|Any CPU + {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Debug|x86.Build.0 = Debug|Any CPU {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Release|Any CPU.ActiveCfg = Release|Any CPU {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Release|Any CPU.Build.0 = Release|Any CPU + {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Release|x64.ActiveCfg = Release|Any CPU + {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Release|x64.Build.0 = Release|Any CPU + {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Release|x86.ActiveCfg = Release|Any CPU + {3CA739B1-888F-4B84-8E76-387640E3B3E9}.Release|x86.Build.0 = Release|Any CPU {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Debug|x64.ActiveCfg = Debug|Any CPU + {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Debug|x64.Build.0 = Debug|Any CPU + {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Debug|x86.ActiveCfg = Debug|Any CPU + {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Debug|x86.Build.0 = Debug|Any CPU {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Release|Any CPU.ActiveCfg = Release|Any CPU {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Release|Any CPU.Build.0 = Release|Any CPU + {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Release|x64.ActiveCfg = Release|Any CPU + {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Release|x64.Build.0 = Release|Any CPU + {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Release|x86.ActiveCfg = Release|Any CPU + {D0155366-89B4-4BA4-90E2-2ECC8C1EB915}.Release|x86.Build.0 = Release|Any CPU {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Debug|x64.ActiveCfg = Debug|Any CPU + {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Debug|x64.Build.0 = Debug|Any CPU + {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Debug|x86.ActiveCfg = Debug|Any CPU + {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Debug|x86.Build.0 = Debug|Any CPU {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|Any CPU.ActiveCfg = Release|Any CPU {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|Any CPU.Build.0 = Release|Any CPU + {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|x64.ActiveCfg = Release|Any CPU + {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|x64.Build.0 = Release|Any CPU + {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|x86.ActiveCfg = Release|Any CPU + {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|x86.Build.0 = Release|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Debug|x64.ActiveCfg = Debug|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Debug|x64.Build.0 = Debug|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Debug|x86.ActiveCfg = Debug|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Debug|x86.Build.0 = Debug|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Release|Any CPU.Build.0 = Release|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Release|x64.ActiveCfg = Release|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Release|x64.Build.0 = Release|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Release|x86.ActiveCfg = Release|Any CPU + {E6BC0BE9-A629-48C5-A08A-8373A21434F5}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE