diff --git a/MTSC.UnitTests/E2ETests.cs b/MTSC.UnitTests/E2ETests.cs index 9a0b65b..e59474b 100644 --- a/MTSC.UnitTests/E2ETests.cs +++ b/MTSC.UnitTests/E2ETests.cs @@ -35,7 +35,7 @@ namespace MTSC.UnitTests public TestContext TestContext { get; set; } static ServerSide.Server Server { get; set; } - [AssemblyInitialize] + [ClassInitialize] public static void InitializeServer(TestContext testContext) { ServicePointManager.ServerCertificateValidationCallback += (s, e, o, p) => true; diff --git a/MTSC.UnitTests/Models/ServiceOnInitialization.cs b/MTSC.UnitTests/Models/ServiceOnInitialization.cs new file mode 100644 index 0000000..d15fae8 --- /dev/null +++ b/MTSC.UnitTests/Models/ServiceOnInitialization.cs @@ -0,0 +1,36 @@ +using MTSC.ServerSide; +using MTSC.ServerSide.Handlers; +using System; + +namespace MTSC.UnitTests.Models; + +public sealed class ServiceOnInitialization : IHandler, IRunOnInitialization +{ + public bool RanOnInitialization { get; private set; } + + public void OnInitialization(Server server) + { + if (this.RanOnInitialization) + { + throw new InvalidOperationException("Initialization procedure already ran"); + } + + this.RanOnInitialization = true; + } + + public void ClientRemoved(Server server, ClientData client) + { + } + + public bool HandleClient(Server server, ClientData client) => true; + + public bool HandleReceivedMessage(Server server, ClientData client, Message message) => false; + + public bool HandleSendMessage(Server server, ClientData client, ref Message message) => false; + + public bool PreHandleReceivedMessage(Server server, ClientData client, ref Message message) => false; + + public void Tick(Server server) + { + } +} diff --git a/MTSC.UnitTests/Models/ServiceOnStartup.cs b/MTSC.UnitTests/Models/ServiceOnStartup.cs new file mode 100644 index 0000000..92f3d2c --- /dev/null +++ b/MTSC.UnitTests/Models/ServiceOnStartup.cs @@ -0,0 +1,30 @@ +using MTSC.ServerSide; +using MTSC.ServerSide.Handlers; + +namespace MTSC.UnitTests.Models; + +public sealed class ServiceOnStartup : IHandler, IRunOnStartup +{ + public int RanOnStartup { get; set; } = 0; + + public void OnStartup(Server server) + { + this.RanOnStartup += 1; + } + + public void ClientRemoved(Server server, ClientData client) + { + } + + public bool HandleClient(Server server, ClientData client) => true; + + public bool HandleReceivedMessage(Server server, ClientData client, Message message) => false; + + public bool HandleSendMessage(Server server, ClientData client, ref Message message) => false; + + public bool PreHandleReceivedMessage(Server server, ClientData client, ref Message message) => false; + + public void Tick(Server server) + { + } +} diff --git a/MTSC.UnitTests/ServerTests.cs b/MTSC.UnitTests/ServerTests.cs index 1d35ef8..86edfc0 100644 --- a/MTSC.UnitTests/ServerTests.cs +++ b/MTSC.UnitTests/ServerTests.cs @@ -1,7 +1,11 @@ using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; using MTSC.ServerSide; +using MTSC.UnitTests.Models; +using System; using System.Threading; +using System.Threading.Tasks; namespace MTSC.UnitTests { @@ -34,5 +38,51 @@ namespace MTSC.UnitTests runningTask.Wait(1000); runningTask.IsCompleted.Should().BeTrue(); } + + [TestMethod] + public async Task Stop_Start_RunsInitializationOnlyOnce() + { + var serviceOnInitialization = new ServiceOnInitialization(); + + var server = new Server(256) + .AddHandler(serviceOnInitialization); + server.RunAsync(); + var cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + while(cancellationToken.IsCancellationRequested is false) + { + if (server.Running) + { + break; + } + } + + server.Stop(); + server.RunAsync(); + while (cancellationToken.IsCancellationRequested is false) + { + if (server.Running) + { + break; + } + } + + serviceOnInitialization.RanOnInitialization.Should().BeTrue(); + } + + [TestMethod] + public async Task Stop_Start_RunsOnStartupEveryTime() + { + var serviceOnStartup = new ServiceOnStartup(); + + var server = new Server(256) + .AddHandler(serviceOnStartup); + var cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(2)); + await server.RunAsync(cancellationToken.Token); + + cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(2)); + await server.RunAsync(cancellationToken.Token); + + serviceOnStartup.RanOnStartup.Should().Be(2); + } } } diff --git a/MTSC/MTSC.csproj b/MTSC/MTSC.csproj index 7d4285f..799936d 100644 --- a/MTSC/MTSC.csproj +++ b/MTSC/MTSC.csproj @@ -5,13 +5,13 @@ netstandard2.0 - 5.5 + 5.5.1 latest Alexandru-Victor Macocian MTSC Modular TCP Server and Client - 5.5.0.0 - 5.5.0.0 + 5.5.1.0 + 5.5.1.0 true AnyCPU;x64 https://github.com/AlexMacocian/MTSC diff --git a/MTSC/ServerSide/Handlers/HttpRoutingHandler.cs b/MTSC/ServerSide/Handlers/HttpRoutingHandler.cs index 7c84908..677ca25 100644 --- a/MTSC/ServerSide/Handlers/HttpRoutingHandler.cs +++ b/MTSC/ServerSide/Handlers/HttpRoutingHandler.cs @@ -16,7 +16,7 @@ using static MTSC.Common.Http.HttpMessage; namespace MTSC.ServerSide.Handlers { - public sealed class HttpRoutingHandler : IHandler, IRunOnStartup + public sealed class HttpRoutingHandler : IHandler, IRunOnInitialization { private class FragmentedMessage { @@ -188,7 +188,7 @@ namespace MTSC.ServerSide.Handlers } } - void IRunOnStartup.OnStartup(Server server) + void IRunOnInitialization.OnInitialization(Server server) { foreach (var routes in this.moduleDictionary.Values) { diff --git a/MTSC/ServerSide/Handlers/IRunOnInitialization.cs b/MTSC/ServerSide/Handlers/IRunOnInitialization.cs new file mode 100644 index 0000000..71565e5 --- /dev/null +++ b/MTSC/ServerSide/Handlers/IRunOnInitialization.cs @@ -0,0 +1,9 @@ +namespace MTSC.ServerSide.Handlers; + +/// +/// Implement this interface in handlers that need to run a procedure on server initialization. +/// +public interface IRunOnInitialization +{ + void OnInitialization(Server server); +} diff --git a/MTSC/ServerSide/Handlers/IRunOnStartup.cs b/MTSC/ServerSide/Handlers/IRunOnStartup.cs index 800b35e..2fb1e9c 100644 --- a/MTSC/ServerSide/Handlers/IRunOnStartup.cs +++ b/MTSC/ServerSide/Handlers/IRunOnStartup.cs @@ -1,10 +1,9 @@ -namespace MTSC.ServerSide.Handlers +namespace MTSC.ServerSide.Handlers; + +/// +/// Implement this interface in handlers that need to run a procedure on server startup. +/// +public interface IRunOnStartup { - /// - /// Implement this interface in handlers that need to run a procedure on server startup. - /// - public interface IRunOnStartup - { - void OnStartup(Server server); - } + void OnStartup(Server server); } diff --git a/MTSC/ServerSide/Handlers/WebsocketRoutingHandler.cs b/MTSC/ServerSide/Handlers/WebsocketRoutingHandler.cs index 7d2a4bd..c820408 100644 --- a/MTSC/ServerSide/Handlers/WebsocketRoutingHandler.cs +++ b/MTSC/ServerSide/Handlers/WebsocketRoutingHandler.cs @@ -10,7 +10,7 @@ using System.Text; namespace MTSC.ServerSide.Handlers { - public class WebsocketRoutingHandler : IHandler, IRunOnStartup + public class WebsocketRoutingHandler : IHandler, IRunOnInitialization { private const string WebsocketHeaderAcceptKey = "Sec-WebSocket-Accept"; private const string WebsocketHeaderKey = "Sec-WebSocket-Key"; @@ -302,7 +302,7 @@ namespace MTSC.ServerSide.Handlers } } - void IRunOnStartup.OnStartup(Server server) + void IRunOnInitialization.OnInitialization(Server server) { foreach((var routeType, _) in this.moduleDictionary.Values) { diff --git a/MTSC/ServerSide/Listeners/TcpSocketListener.cs b/MTSC/ServerSide/Listeners/TcpSocketListener.cs index 27fd79b..9ec3901 100644 --- a/MTSC/ServerSide/Listeners/TcpSocketListener.cs +++ b/MTSC/ServerSide/Listeners/TcpSocketListener.cs @@ -10,17 +10,20 @@ namespace MTSC.ServerSide.Listeners { private readonly ConcurrentQueue acceptedSockets = new(); - private Socket socket; + private volatile Socket socket; public bool Active => this.socket is not null; public EndPoint LocalEndpoint { get => this.socket?.LocalEndPoint; } public void Initialize(int port, IPAddress ipAddress) { - this.socket?.Close(); - this.socket?.Dispose(); - this.socket = new Socket(SocketType.Stream, ProtocolType.Tcp); - this.socket.Bind(new IPEndPoint(ipAddress, port)); + lock (this) + { + this.socket?.Close(); + this.socket?.Dispose(); + this.socket = new Socket(SocketType.Stream, ProtocolType.Tcp); + this.socket.Bind(new IPEndPoint(ipAddress, port)); + } } public Socket AcceptSocket() @@ -40,24 +43,32 @@ namespace MTSC.ServerSide.Listeners public void Start() { - this.socket.Listen(50); - var socket = this.socket; - Task.Run(async() => + lock (this) { - while(socket is not null) + while(this.socket is not Socket) { - var clientSocket = await this.socket.AcceptAsync(); - this.acceptedSockets.Enqueue(clientSocket); } - }); - + + this.socket.Listen(50); + Task.Run(async () => + { + while (this.socket is not null) + { + var clientSocket = await this.socket.AcceptAsync(); + this.acceptedSockets.Enqueue(clientSocket); + } + }); + } } public void Stop() { - this.socket?.Close(); - this.socket?.Dispose(); - this.socket = null; + lock (this) + { + this.socket?.Close(); + this.socket?.Dispose(); + this.socket = null; + } } } } diff --git a/MTSC/ServerSide/Server.cs b/MTSC/ServerSide/Server.cs index aa85c5b..3a2db13 100644 --- a/MTSC/ServerSide/Server.cs +++ b/MTSC/ServerSide/Server.cs @@ -30,7 +30,7 @@ namespace MTSC.ServerSide { #region Fields private CancellationToken cancellationToken; - private bool running; + private bool initialized; private X509Certificate2 certificate; private readonly BackgroundServicesHolder backgroundServicesHolder; private readonly ProducerConsumerQueue addQueue = new(); @@ -106,11 +106,11 @@ namespace MTSC.ServerSide /// public IReadOnlyCollection Clients { get => this.clients.AsReadOnly(); } /// - /// for configuring and retrieving services. Will be initialized at server startup from the . + /// for configuring and retrieving services. Will be populated at server startup from the . /// - public IServiceManager ServiceManager { get; private set; } + public IServiceManager ServiceManager { get; private set; } = new ServiceManager(); /// - /// used to create the at server startup. + /// used to populate the at server startup. /// public IServiceCollection ServiceCollection { get; } = new ServiceCollection(); #endregion @@ -423,50 +423,62 @@ namespace MTSC.ServerSide /// Cancellation token used to cancel the server. public void Run(CancellationToken cancellationToken = default) { - if (this.running) + lock (this) { - return; - } - - this.ServiceManager?.Dispose(); - this.ServiceManager = this.ServiceCollection.BuildSlimServiceProvider() as IServiceManager; - this.Listener?.Stop(); - if (this.logger is null) - { - // Try to get a logger, in case it exists. If not, keep it null. - try + if (this.Running) { - this.logger = this.ServiceManager.GetService>(); + return; } - catch + + /* + * Run only once when the server is first initializing. + * This gives the handlers an opportunity to schedule tasks, register services, etc. + */ + if (this.initialized is false) { + this.initialized = true; + this.ServiceManager = this.ServiceCollection.BuildSlimServiceProvider(this.ServiceManager) as IServiceManager; + + foreach (var toBeRunOnInitialization in this.handlers.OfType()) + { + toBeRunOnInitialization.OnInitialization(this); + } + + // Try to get a logger, in case it exists. If not, keep it null. try { - this.logger = this.ServiceManager.GetService(); + this.logger = this.ServiceManager.GetService>(); } catch { + try + { + this.logger = this.ServiceManager.GetService(); + } + catch + { + } } } - } - this.Listener.Initialize(this.Port, this.IPAddress); - this.Listener.Start(); - this.cancellationToken = cancellationToken; - this.running = true; - this.Log("Server started on: " + this.Listener.LocalEndpoint.ToString()); - foreach(var toBeRunOnStartup in this.handlers.OfType()) - { - toBeRunOnStartup.OnStartup(this); - } + this.Listener?.Stop(); + this.Listener.Initialize(this.Port, this.IPAddress); + this.Listener.Start(); + this.cancellationToken = cancellationToken; + this.Log("Server starting on: " + this.Listener.LocalEndpoint.ToString()); + foreach (var toBeRunOnStartup in this.handlers.OfType()) + { + toBeRunOnStartup.OnStartup(this); + } - /* - * Initialize background services. - */ - this.backgroundServicesHolder.Initialize(); + /* + * Initialize background services. + */ + this.backgroundServicesHolder.Initialize(); + } DateTime startLoopTime; - while (this.running) + while (this.Listener?.Active is true) { startLoopTime = DateTime.Now; @@ -476,7 +488,6 @@ namespace MTSC.ServerSide */ if (this.cancellationToken.IsCancellationRequested) { - this.running = false; break; } @@ -586,7 +597,7 @@ namespace MTSC.ServerSide } } - this.Listener.Stop(); + this.Listener?.Stop(); foreach (var client in this.Clients) { try @@ -598,8 +609,6 @@ namespace MTSC.ServerSide this.HandleException(e); } } - - this.Listener = null; } /// /// Runs the server async. @@ -613,10 +622,7 @@ namespace MTSC.ServerSide /// public void Stop() { - if (this.running) - { - this.running = false; - } + this.Listener?.Stop(); } #endregion #region Private Methods