Skip to content

Commit

Permalink
Improved AdminPanel UI to manage connect servers
Browse files Browse the repository at this point in the history
  • Loading branch information
sven-n committed Sep 24, 2024
1 parent 4a021d8 commit 94b44e8
Show file tree
Hide file tree
Showing 12 changed files with 453 additions and 41 deletions.
40 changes: 40 additions & 0 deletions src/Dapr/AdminPanel.Host/DockerConnectServerInstanceManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using MUnique.OpenMU.Interfaces;

namespace MUnique.OpenMU.AdminPanel.Host;

/// <summary>
/// An implementation of <see cref="IConnectServerInstanceManager"/>.
/// </summary>
public class DockerConnectServerInstanceManager : IConnectServerInstanceManager
{
private readonly IServerProvider _serverProvider;

/// <summary>
/// Initializes a new instance of the <see cref="DockerGameServerInstanceManager"/> class.
/// </summary>
/// <param name="serverProvider">The server provider.</param>
public DockerConnectServerInstanceManager(IServerProvider serverProvider)
{
this._serverProvider = serverProvider;
}

/// <inheritdoc />
public async ValueTask InitializeConnectServerAsync(byte serverId)
{
// TODO: Implement this... by starting a new docker container

}

/// <inheritdoc />
public async ValueTask RemoveConnectServerAsync(byte serverId)
{
var connectServers = this._serverProvider.Servers
.Where(server => server.Type == ServerType.ConnectServer)
.FirstOrDefault(server => server.Id == serverId);
if (connectServers is not null)
{
await connectServers.ShutdownAsync().ConfigureAwait(false);
// TODO: Remove the docker container
}
}
}
7 changes: 6 additions & 1 deletion src/Dapr/AdminPanel.Host/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
// </copyright>

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using MUnique.OpenMU.AdminPanel.Host;
using MUnique.OpenMU.Dapr.Common;
using MUnique.OpenMU.Interfaces;
using MUnique.OpenMU.PlugIns;
using MUnique.OpenMU.Web.AdminPanel;

Expand All @@ -15,7 +18,9 @@

services.AddPeristenceProvider(true)
.AddPlugInManager(plugInConfigurations)
.AddManageableServerRegistry();
.AddManageableServerRegistry()
.AddSingleton<IGameServerInstanceManager, DockerGameServerInstanceManager>()
.AddSingleton<IConnectServerInstanceManager, DockerConnectServerInstanceManager>();

builder.AddAdminPanel();

Expand Down
23 changes: 23 additions & 0 deletions src/Interfaces/IConnectServerInstanceManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// <copyright file="IConnectServerInstanceManager.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.Interfaces;

/// <summary>
/// Interface for an instance which manages connect servers.
/// </summary>
public interface IConnectServerInstanceManager
{
/// <summary>
/// Initializes a connect server with the specified definition.
/// </summary>
/// <param name="connectServerDefinitionId">The connect server definition identifier.</param>
ValueTask InitializeConnectServerAsync(Guid connectServerDefinitionId);

/// <summary>
/// Removes the connect server instance of the specified definition.
/// </summary>
/// <param name="connectServerDefinitionId">The connect server definition identifier.</param>
ValueTask RemoveConnectServerAsync(Guid connectServerDefinitionId);
}
70 changes: 47 additions & 23 deletions src/Startup/ConnectServerContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ namespace MUnique.OpenMU.Startup;
/// <summary>
/// A container which keeps all <see cref="Interfaces.IConnectServer"/>s in one <see cref="IHostedService"/>.
/// </summary>
public class ConnectServerContainer : ServerContainerBase, IEnumerable<IConnectServer>
public class ConnectServerContainer : ServerContainerBase, IEnumerable<IConnectServer>, IConnectServerInstanceManager
{
private readonly IList<IManageableServer> _servers;
private readonly IPersistenceContextProvider _persistenceContextProvider;
private readonly ConnectServerFactory _connectServerFactory;
private readonly IList<IConnectServer> _connectServers = new List<IConnectServer>();
private readonly IDictionary<GameClientDefinition, IGameServerStateObserver> _observers = new Dictionary<GameClientDefinition, IGameServerStateObserver>();
private readonly Dictionary<GameClientDefinition, MulticastConnectionServerStateObserver> _observers = new();

/// <summary>
/// Initializes a new instance of the <see cref="ConnectServerContainer" /> class.
Expand All @@ -40,6 +40,31 @@ public ConnectServerContainer(IList<IManageableServer> servers, IPersistenceCont
this._connectServerFactory = connectServerFactory;
}

/// <inheritdoc />
public async ValueTask InitializeConnectServerAsync(Guid connectServerDefinitionId)
{
using var persistenceContext = this._persistenceContextProvider.CreateNewConfigurationContext();
var definition = await persistenceContext
.GetByIdAsync<ConnectServerDefinition>(connectServerDefinitionId)
.ConfigureAwait(false);

var newConnectServer = this.InitializeConnectServer(definition ?? throw new InvalidOperationException($"ConnectServerDefinition with id {connectServerDefinitionId} was not found."));
if(this._observers.TryGetValue(definition.Client!, out var observer))
{
observer.PullRegistrations(newConnectServer);
}
}

/// <inheritdoc />
public async ValueTask RemoveConnectServerAsync(Guid connectServerDefinitionId)
{
var connectServer = this._connectServers
.FirstOrDefault(server => server.ConfigurationId == connectServerDefinitionId)
?? throw new InvalidOperationException($"ConnectServer with Definition with id {connectServerDefinitionId} was not found.");
await connectServer.StopAsync(default).ConfigureAwait(false);
this._servers.Remove(connectServer);
}

/// <summary>
/// Gets the observer.
/// </summary>
Expand All @@ -50,7 +75,6 @@ public IGameServerStateObserver GetObserver(GameClientDefinition gameClient)
if (!this._observers.TryGetValue(gameClient, out var observer))
{
// In this case, most probably the game server gets started before the connection server.
// As a workaround, we just create a new multicast observer, on which the connect server will be added later.
observer = new MulticastConnectionServerStateObserver();
this._observers[gameClient] = observer;
}
Expand Down Expand Up @@ -83,27 +107,9 @@ protected override async Task StartListenersAsync(CancellationToken cancellation
protected override async Task StartInnerAsync(CancellationToken cancellationToken)
{
using var persistenceContext = this._persistenceContextProvider.CreateNewConfigurationContext();
foreach (var connectServerDefinition in await persistenceContext.GetAsync<ConnectServerDefinition>().ConfigureAwait(false))
foreach (var connectServerDefinition in await persistenceContext.GetAsync<ConnectServerDefinition>(cancellationToken).ConfigureAwait(false))
{
var connectServer = this._connectServerFactory.CreateConnectServer(connectServerDefinition);
this._servers.Add(connectServer);
this._connectServers.Add(connectServer);
var client = connectServerDefinition.Client!;
if (this._observers.TryGetValue(client, out var observer))
{
if (observer is not MulticastConnectionServerStateObserver multicastObserver)
{
multicastObserver = new MulticastConnectionServerStateObserver();
multicastObserver.AddObserver(observer);
this._observers[client] = multicastObserver;
}

multicastObserver.AddObserver(connectServer);
}
else
{
this._observers[client] = connectServer;
}
this.InitializeConnectServer(connectServerDefinition);
}
}

Expand All @@ -118,4 +124,22 @@ protected override async Task StopInnerAsync(CancellationToken cancellationToken

this._connectServers.Clear();
}

private IConnectServer InitializeConnectServer(ConnectServerDefinition connectServerDefinition)
{
var connectServer = this._connectServerFactory.CreateConnectServer(connectServerDefinition);
this._servers.Add(connectServer);
this._connectServers.Add(connectServer);
var client = connectServerDefinition.Client!;
if (!this._observers.TryGetValue(client, out var observer))
{
// we're now always creating a multicast observer, because we want to support
// creation of connect servers during runtime.
observer = new MulticastConnectionServerStateObserver();
this._observers[client] = observer;
}

observer.AddObserver(connectServer);
return connectServer;
}
}
45 changes: 43 additions & 2 deletions src/Startup/MulticastConnectionServerStateObserver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace MUnique.OpenMU.Startup;

using System.Collections.Concurrent;
using System.Net;
using MUnique.OpenMU.Interfaces;

Expand All @@ -14,8 +15,13 @@ namespace MUnique.OpenMU.Startup;
/// <seealso cref="MUnique.OpenMU.Interfaces.IGameServerStateObserver" />
internal class MulticastConnectionServerStateObserver : IGameServerStateObserver
{
private readonly IList<IGameServerStateObserver> _observers = new List<IGameServerStateObserver>();

private readonly MemorizingObserver _memorizingObserver = new();
private readonly List<IGameServerStateObserver> _observers = new();

public MulticastConnectionServerStateObserver()
{
this._observers.Add(this._memorizingObserver);
}
/// <summary>
/// Adds the observer which wants to get notified about changes.
/// </summary>
Expand Down Expand Up @@ -48,4 +54,39 @@ public void CurrentConnectionsChanged(ushort serverId, int currentConnections)
this._observers[i].CurrentConnectionsChanged(serverId, currentConnections);
}
}

/// <summary>
/// Pulls the registrations.
/// </summary>
/// <param name="observer">The observer.</param>
public void PullRegistrations(IGameServerStateObserver observer)
{
foreach (var (serverInfo, endpoint) in this._memorizingObserver.ServerInfos.Values)
{
observer.RegisterGameServer(serverInfo, endpoint);
}
}

private class MemorizingObserver : IGameServerStateObserver
{
public ConcurrentDictionary<ushort, (ServerInfo, IPEndPoint)> ServerInfos { get; } = new();

public void RegisterGameServer(ServerInfo gameServer, IPEndPoint publicEndPoint)
{
this.ServerInfos.TryAdd(gameServer.Id, (gameServer, publicEndPoint));
}

public void UnregisterGameServer(ushort gameServerId)
{
this.ServerInfos.TryRemove(gameServerId, out _);
}

public void CurrentConnectionsChanged(ushort serverId, int currentConnections)
{
if (this.ServerInfos.TryGetValue(serverId, out var tuple))
{
tuple.Item1.CurrentConnections = currentConnections;
}
}
}
}
1 change: 1 addition & 0 deletions src/Startup/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ private async Task<IHost> CreateHostAsync(string[] args)
.AddSingleton<IChatServer>(s => s.GetService<ChatServer>()!)
.AddSingleton<ConnectServerFactory>()
.AddSingleton<ConnectServerContainer>()
.AddSingleton<IConnectServerInstanceManager>(provider => provider.GetService<ConnectServerContainer>()!)
.AddSingleton<GameServerContainer>()
.AddSingleton<IGameServerInstanceManager>(provider => provider.GetService<GameServerContainer>()!)
.AddScoped<IMapFactory, JavascriptMapFactory>()
Expand Down
47 changes: 36 additions & 11 deletions src/Web/AdminPanel/Components/ServerItem.razor
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
<button type="button" class="btn-warning" title="Stop" @onclick="this.OnPauseClickAsync">
<span class="oi oi-media-pause"></span>
</button>
@if (this.Server.Type == ServerType.GameServer)
@if (this.Server.Type is ServerType.GameServer or ServerType.ConnectServer)
{
<button type="button" class="btn-secondary" title="Remove" disabled>
<span class="oi oi-trash"></span>
Expand All @@ -65,9 +65,9 @@
<button type="button" class="btn-secondary" title="Stop" disabled>
<span class="oi oi-media-pause"></span>
</button>
@if (this.Server.Type == ServerType.GameServer)
@if (this.Server.Type is ServerType.GameServer or ServerType.ConnectServer)
{
<button type="button" class="btn-danger" title="Remove" @onclick="this.OnDeleteClickAsync">
<button type="button" class="btn-danger" title="Remove" @onclick="this.OnDeleteServerClickAsync">
<span class="oi oi-trash"></span>
</button>
}
Expand All @@ -80,7 +80,7 @@
<button type="button" class="btn-secondary" title="Stop" disabled>
<span class="oi oi-media-pause"></span>
</button>
@if (this.Server.Type == ServerType.GameServer)
@if (this.Server.Type is ServerType.GameServer or ServerType.ConnectServer)
{
<button type="button" class="btn-secondary" title="Remove" disabled>
<span class="oi oi-trash"></span>
Expand All @@ -106,7 +106,13 @@
/// Gets or sets the <see cref="IGameServerInstanceManager"/>.
/// </summary>
[Inject]
public IGameServerInstanceManager InstanceManager { get; set; } = null!;
public IGameServerInstanceManager GameServerInstanceManager { get; set; } = null!;

/// <summary>
/// Gets or sets the <see cref="IConnectServerInstanceManager"/>.
/// </summary>
[Inject]
public IConnectServerInstanceManager ConnectServerInstanceManager { get; set; } = null!;

/// <summary>
/// Gets or sets the <see cref="IPersistenceContextProvider"/>.
Expand Down Expand Up @@ -178,23 +184,42 @@
await this.Server.StartAsync(default);
}

private async Task OnDeleteClickAsync()
private async Task OnDeleteServerClickAsync()
{
if (this.Server.Type is not (ServerType.GameServer or ServerType.ConnectServer))
{
return;
}

var dialogResult = await this.ModalService.ShowQuestionAsync(
"Remove Game Server",
"Remove Server",
"The server will be deleted from the database. Are you sure to proceed?");
if (!dialogResult)
{
return;
}

await this.Server.StopAsync(default);
await this.InstanceManager.RemoveGameServerAsync((byte)this.Server.Id);
if (this.Server.Type == ServerType.GameServer)
{
await this.GameServerInstanceManager.RemoveGameServerAsync((byte)this.Server.Id);
await this.DeleteAsync<GameServerDefinition>(s => s.ServerID == this.Server.Id);
}
else
{
await this.ConnectServerInstanceManager.RemoveConnectServerAsync(this.Server.ConfigurationId);
await this.DeleteAsync<ConnectServerDefinition>(s => s.ConfigurationId == this.Server.ConfigurationId);
}
}

private async ValueTask DeleteAsync<T>(Predicate<T> predicate)
where T : class
{
var gameConfiguration = await this.DataSource.GetOwnerAsync().ConfigureAwait(false);
using var context = this.ContextProvider.CreateNewTypedContext<GameServerDefinition>(true, gameConfiguration);
var definitions = await context.GetAsync<GameServerDefinition>().ConfigureAwait(false);
var definition = definitions.FirstOrDefault(def => def.ServerID == this.Server.Id);
using var context = this.ContextProvider.CreateNewTypedContext<T>(true, gameConfiguration);
var definitions = await context.GetAsync<T>().ConfigureAwait(false);
var definition = definitions.FirstOrDefault(def => predicate(def));

if (definition is not null)
{
await context.DeleteAsync(definition).ConfigureAwait(false);
Expand Down
24 changes: 24 additions & 0 deletions src/Web/AdminPanel/Pages/CreateConnectServerConfig.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@page "/create-connect-server"

@using MUnique.OpenMU.Persistence

<h1>Create Connect Server Definition</h1>


@if (this._viewModel is null)
{
<span class="spinner-border" role="status" aria-hidden="true"></span>
<span class="sr-only">Loading...</span>
return;
}

@if (this._initState is { })
{
<span class="spinner-border" role="status" aria-hidden="true"></span>
<span class="sr-only">@this._initState</span>
return;
}


<AutoForm Model="this._viewModel" OnValidSubmit="this.OnSaveButtonClickAsync"></AutoForm>

Loading

0 comments on commit 94b44e8

Please sign in to comment.