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