Basic service entity completed and tested.
added UML to guide actual implementation for Host. ModuleLoader, ServiceGateway, are written but untested.
This commit is contained in:
parent
f1a4e32866
commit
6467c178c3
12
src/GameServiceWarden.Host/GameServiceWarden.Host.csproj
Normal file
12
src/GameServiceWarden.Host/GameServiceWarden.Host.csproj
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\GameServiceWarden.ModuleAPI\GameServiceWarden.ModuleAPI.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
14
src/GameServiceWarden.Host/Logging/FileLogReceiver.cs
Normal file
14
src/GameServiceWarden.Host/Logging/FileLogReceiver.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace GameServiceWarden.Host.Logging
|
||||||
|
{
|
||||||
|
public class FileLogReceiver : ILogReceiver
|
||||||
|
{
|
||||||
|
public LogLevel Level => LogLevel.INFO;
|
||||||
|
|
||||||
|
public void LogMessage(string message, DateTime time, LogLevel level)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
src/GameServiceWarden.Host/Logging/ILogRecievable.cs
Normal file
21
src/GameServiceWarden.Host/Logging/ILogRecievable.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace GameServiceWarden.Host.Logging
|
||||||
|
{
|
||||||
|
public interface ILogReceiver
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The severity of the messages this log should receive.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The severity of the logs.</value>
|
||||||
|
LogLevel Level { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs the message.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The message to be logged.</param>
|
||||||
|
/// <param name="time">The time at which this message was requested to be logged.</param>
|
||||||
|
/// <param name="level">The severity of this message.</param>
|
||||||
|
void LogMessage(string message, DateTime time, LogLevel level);
|
||||||
|
}
|
||||||
|
}
|
10
src/GameServiceWarden.Host/Logging/LogLevel.cs
Normal file
10
src/GameServiceWarden.Host/Logging/LogLevel.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace GameServiceWarden.Host.Logging
|
||||||
|
{
|
||||||
|
public enum LogLevel
|
||||||
|
{
|
||||||
|
FATAL,
|
||||||
|
INFO,
|
||||||
|
WARNING,
|
||||||
|
DEBUG,
|
||||||
|
}
|
||||||
|
}
|
46
src/GameServiceWarden.Host/Logging/Logger.cs
Normal file
46
src/GameServiceWarden.Host/Logging/Logger.cs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace GameServiceWarden.Host.Logging
|
||||||
|
{
|
||||||
|
public class Logger {
|
||||||
|
private HashSet<ILogReceiver> listeners = new HashSet<ILogReceiver>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs the message to listeners that are listening to the set severity of the message or greater.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The message to log.</param>
|
||||||
|
/// <param name="level">The level of severity, by default, info.</param>
|
||||||
|
public void Log(string message, LogLevel level = LogLevel.INFO) {
|
||||||
|
foreach (ILogReceiver listener in listeners)
|
||||||
|
{
|
||||||
|
if (level <= listener.Level) {
|
||||||
|
listener.LogMessage(message, DateTime.Now, level);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a log listener.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="listener">The listener to add.</param>
|
||||||
|
public void AddLogListener(ILogReceiver listener) {
|
||||||
|
listeners.Add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a log listener.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="listener">The listener to remove.</param>
|
||||||
|
public void RemoveLogListener(ILogReceiver listener) {
|
||||||
|
listeners.Remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when all listeners should perform any flushing they need.
|
||||||
|
/// </summary>
|
||||||
|
public static void FlushListeners() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
namespace GameServiceWarden.Host.Modules.Exceptions
|
||||||
|
{
|
||||||
|
[System.Serializable]
|
||||||
|
public class CorruptedServiceInfoException : System.Exception
|
||||||
|
{
|
||||||
|
public CorruptedServiceInfoException() { }
|
||||||
|
public CorruptedServiceInfoException(string message) : base(message) { }
|
||||||
|
public CorruptedServiceInfoException(string message, System.Exception inner) : base(message, inner) { }
|
||||||
|
protected CorruptedServiceInfoException(
|
||||||
|
System.Runtime.Serialization.SerializationInfo info,
|
||||||
|
System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
|
||||||
|
namespace GameServiceWarden.Host.Modules.Exceptions
|
||||||
|
{
|
||||||
|
[Serializable]
|
||||||
|
public class NoServiceableFoundException : Exception
|
||||||
|
{
|
||||||
|
public NoServiceableFoundException(string message) : base(message) { }
|
||||||
|
public NoServiceableFoundException(string message, Exception inner) : base(message, inner) { }
|
||||||
|
protected NoServiceableFoundException(
|
||||||
|
SerializationInfo info,
|
||||||
|
StreamingContext context) : base(info, context) { }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace GameServiceWarden.Host.Modules.Exceptions
|
||||||
|
{
|
||||||
|
[System.Serializable]
|
||||||
|
public class NotServiceableTypeException : Exception
|
||||||
|
{
|
||||||
|
public NotServiceableTypeException() { }
|
||||||
|
public NotServiceableTypeException(string message) : base(message) { }
|
||||||
|
public NotServiceableTypeException(string message, System.Exception inner) : base(message, inner) { }
|
||||||
|
protected NotServiceableTypeException(
|
||||||
|
System.Runtime.Serialization.SerializationInfo info,
|
||||||
|
System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
|
||||||
|
}
|
||||||
|
}
|
33
src/GameServiceWarden.Host/Modules/ModuleLoadContext.cs
Normal file
33
src/GameServiceWarden.Host/Modules/ModuleLoadContext.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.Loader;
|
||||||
|
|
||||||
|
namespace GameServiceWarden.Host.Modules
|
||||||
|
{
|
||||||
|
class ModuleLoadContext : AssemblyLoadContext
|
||||||
|
{
|
||||||
|
private AssemblyDependencyResolver dependencyResolver;
|
||||||
|
|
||||||
|
public ModuleLoadContext(string path) {
|
||||||
|
dependencyResolver = new AssemblyDependencyResolver(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Assembly Load(AssemblyName assemblyName)
|
||||||
|
{
|
||||||
|
string assemblyPath = dependencyResolver.ResolveAssemblyToPath(assemblyName);
|
||||||
|
if (assemblyPath != null) {
|
||||||
|
return LoadFromAssemblyPath(assemblyPath);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
|
||||||
|
{
|
||||||
|
String libraryPath = dependencyResolver.ResolveUnmanagedDllToPath(unmanagedDllName);
|
||||||
|
if (libraryPath != null) {
|
||||||
|
return LoadUnmanagedDllFromPath(libraryPath);
|
||||||
|
}
|
||||||
|
return IntPtr.Zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
84
src/GameServiceWarden.Host/Modules/ModuleLoader.cs
Normal file
84
src/GameServiceWarden.Host/Modules/ModuleLoader.cs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using GameServiceWarden.Host.Modules.Exceptions;
|
||||||
|
using GameServiceWarden.ModuleAPI;
|
||||||
|
|
||||||
|
namespace GameServiceWarden.Host.Modules
|
||||||
|
{
|
||||||
|
public class ModuleLoader //Gateway
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Loads an extension module.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The path to the module.</param>
|
||||||
|
/// <returns>An </<see cref="IEnumerable{IGameServiceModule}"/> from the given module.</returns>
|
||||||
|
/// <exception cref="NoServiceableFoundException">When the module requested to be loaded does not contain any public <see cref="IGameServiceable"/> classes.</exception>
|
||||||
|
public IEnumerable<IGameServiceModule> LoadModules(string path)
|
||||||
|
{
|
||||||
|
return instantiateServiceable(loadAssembly(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads all module for each given path to modules file.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="paths">The paths to load modules for.</param>
|
||||||
|
/// <returns>A <see cref="Dictionary{string, IEnumerable{IGameServiceModule}}"/> where the key is a <see cref="string"/> that is the associated path.</returns>
|
||||||
|
public Dictionary<string, IEnumerable<IGameServiceModule>> LoadAllModules(IEnumerable<string> paths)
|
||||||
|
{
|
||||||
|
Dictionary<string, IEnumerable<IGameServiceModule>> res = new Dictionary<string, IEnumerable<IGameServiceModule>>();
|
||||||
|
foreach (string path in paths)
|
||||||
|
{
|
||||||
|
res.Add(path, LoadModules(path));
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads all module for each given path to modules file.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="paths">The paths to load modules for.</param>
|
||||||
|
/// <returns>A <see cref="Dictionary{string, IEnumerable{IGameServiceModule}}"/> where the key is a <see cref="string"/> that is the associated path.</returns>
|
||||||
|
public Dictionary<string, IEnumerable<IGameServiceModule>> LoadAllModules(params string[] paths)
|
||||||
|
{
|
||||||
|
return LoadAllModules(paths);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Assembly loadAssembly(string path)
|
||||||
|
{
|
||||||
|
ModuleLoadContext moduleLoadContext = new ModuleLoadContext(path);
|
||||||
|
return moduleLoadContext.LoadFromAssemblyPath(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<IGameServiceModule> instantiateServiceable(Assembly assembly)
|
||||||
|
{
|
||||||
|
int serviceableCount = 0;
|
||||||
|
foreach (Type type in assembly.GetExportedTypes())
|
||||||
|
{
|
||||||
|
if (typeof(IGameServiceModule).IsAssignableFrom(type))
|
||||||
|
{
|
||||||
|
IGameServiceModule res = Activator.CreateInstance(type) as IGameServiceModule;
|
||||||
|
if (res != null)
|
||||||
|
{
|
||||||
|
serviceableCount++;
|
||||||
|
yield return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serviceableCount == 0)
|
||||||
|
{
|
||||||
|
List<string> typeNames = new List<string>();
|
||||||
|
foreach (Type type in assembly.GetExportedTypes())
|
||||||
|
{
|
||||||
|
typeNames.Add(type.FullName);
|
||||||
|
}
|
||||||
|
string types = String.Join(',', typeNames);
|
||||||
|
|
||||||
|
throw new NoServiceableFoundException(
|
||||||
|
$"No public classes in {assembly} from {assembly.Location} implemented {typeof(IGameService).FullName}." +
|
||||||
|
$"Detected types: {types}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
72
src/GameServiceWarden.Host/Modules/ServiceGateway.cs
Normal file
72
src/GameServiceWarden.Host/Modules/ServiceGateway.cs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using GameServiceWarden.Host.Modules.Exceptions;
|
||||||
|
|
||||||
|
namespace GameServiceWarden.Host.Modules
|
||||||
|
{
|
||||||
|
public class ServiceGateway
|
||||||
|
{
|
||||||
|
private string dataDirectory;
|
||||||
|
private const string SERVICE_NAME = "Service Name";
|
||||||
|
private const string ASSEMBLY_NAME = "Assembly Name";
|
||||||
|
private const string MODULE_NAME = "Module Name";
|
||||||
|
private const string EXTENSION = ".sin"; //Service info
|
||||||
|
|
||||||
|
public ServiceGateway(string dataDirectory)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(dataDirectory)) Directory.CreateDirectory(dataDirectory);
|
||||||
|
this.dataDirectory = dataDirectory;
|
||||||
|
}
|
||||||
|
public void SaveService(string serviceName, string assemblyName, string moduleName)
|
||||||
|
{
|
||||||
|
string serviceInfoPath = dataDirectory + Path.DirectorySeparatorChar + serviceName + EXTENSION;
|
||||||
|
using (StreamWriter writer = File.CreateText(serviceInfoPath))
|
||||||
|
{
|
||||||
|
writer.WriteLine($"{SERVICE_NAME} : {serviceName}");
|
||||||
|
writer.WriteLine($"{ASSEMBLY_NAME} : {assemblyName}");
|
||||||
|
writer.WriteLine($"{MODULE_NAME} : {moduleName}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetServiceName(string path)
|
||||||
|
{
|
||||||
|
return GetServiceInfoValue(path, SERVICE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetServiceModuleName(string path)
|
||||||
|
{
|
||||||
|
return GetServiceInfoValue(path, MODULE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetServiceAssemblyName(string path)
|
||||||
|
{
|
||||||
|
return GetServiceInfoValue(path, ASSEMBLY_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetServiceInfoValue(string path, string key)
|
||||||
|
{
|
||||||
|
IEnumerable<string> lines = File.ReadAllLines(path);
|
||||||
|
foreach (string line in lines)
|
||||||
|
{
|
||||||
|
if (line.StartsWith($"{key}: "))
|
||||||
|
{
|
||||||
|
return line.Substring(key.Length + 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new CorruptedServiceInfoException($"\"{path}\" is corrupted. Could not find value for: {key}.");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> GetAllServiceInfoPaths()
|
||||||
|
{
|
||||||
|
string[] files = Directory.GetFiles(dataDirectory);
|
||||||
|
foreach (string filePath in files)
|
||||||
|
{
|
||||||
|
if (Path.GetExtension(filePath).Equals(EXTENSION))
|
||||||
|
{
|
||||||
|
yield return filePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
150
src/GameServiceWarden.Host/Modules/ServiceInfo.cs
Normal file
150
src/GameServiceWarden.Host/Modules/ServiceInfo.cs
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using GameServiceWarden.Host.Preferences;
|
||||||
|
using GameServiceWarden.ModuleAPI;
|
||||||
|
|
||||||
|
namespace GameServiceWarden.Host.Modules
|
||||||
|
{
|
||||||
|
public class ServiceInfo : IDisposable //entity
|
||||||
|
{
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the service itself, independent of the name of the module this service is using.
|
||||||
|
/// </summary>
|
||||||
|
public string ServiceName { get { return serviceName; } set { Interlocked.Exchange(ref serviceName, value); } }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The services console output stream.
|
||||||
|
/// </summary>
|
||||||
|
private volatile string serviceName; //thread-safe(?)
|
||||||
|
public Stream ServiceConsoleStream { get; private set; } // Thread safe.
|
||||||
|
private object controlLock = new object();
|
||||||
|
private volatile ServiceState state;
|
||||||
|
private readonly IGameService service;
|
||||||
|
private readonly string assemblyName;
|
||||||
|
private readonly string moduleName;
|
||||||
|
private readonly Dictionary<string, IConfigurable> configurables = new Dictionary<string, IConfigurable>();
|
||||||
|
private bool disposed;
|
||||||
|
|
||||||
|
public ServiceInfo(IGameService service, string moduleName, string assemblyName)
|
||||||
|
{
|
||||||
|
this.service = service ?? throw new ArgumentNullException("serviceable");
|
||||||
|
this.moduleName = moduleName ?? throw new ArgumentNullException("moduleName");
|
||||||
|
this.assemblyName = assemblyName ?? throw new ArgumentNullException("assemblyName");
|
||||||
|
this.service.StateChangeEvent += OnServiceStateChange;
|
||||||
|
|
||||||
|
foreach (IConfigurable configurable in service.Configurables)
|
||||||
|
{
|
||||||
|
configurables.Add(configurable.OptionName, configurable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts this service.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="InvalidOperationException">Is thrown when the service is already running.</exception>
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
lock (controlLock)
|
||||||
|
{
|
||||||
|
if (state != ServiceState.Stopped) throw new InvalidOperationException("Service instance already running.");
|
||||||
|
DateTimeFormatInfo format = new DateTimeFormatInfo();
|
||||||
|
format.TimeSeparator = "-";
|
||||||
|
format.DateSeparator = "_";
|
||||||
|
ServiceConsoleStream = new MemoryStream(8 * 1024 * 1024); // 8 MB
|
||||||
|
ServiceConsoleStream = Stream.Synchronized(ServiceConsoleStream);
|
||||||
|
service.InitializeService(new StreamWriter(ServiceConsoleStream));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops the service.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="InvalidOperationException">Is thrown when the is not running.</exception>
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
lock (controlLock)
|
||||||
|
{
|
||||||
|
if (state != ServiceState.Running) throw new InvalidOperationException("Service instance not running.");
|
||||||
|
this.service.ElegantShutdown();
|
||||||
|
ServiceConsoleStream.Close();
|
||||||
|
ServiceConsoleStream = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a command to this service to execute.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="command">The command to execute.</param>
|
||||||
|
/// <exception cref="InvalidOperationException">Is thrown when the service is not running.</exception>
|
||||||
|
public void ExecuteCommand(string command)
|
||||||
|
{
|
||||||
|
lock (controlLock)
|
||||||
|
{
|
||||||
|
if (state != ServiceState.Running) throw new InvalidOperationException("Service instance not running.");
|
||||||
|
service.ExecuteCommand(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the possible <see cref="IConfigurable"/>'s for this service.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A <see cref="IReadOnlyDictionary{string, IConfigurable}"/> is returned where the string is the option name and the configurable is what handles actually changing the values.</returns>
|
||||||
|
public IReadOnlyDictionary<string, IConfigurable> GetConfigurables()
|
||||||
|
{
|
||||||
|
return new ReadOnlyDictionary<string, IConfigurable>(this.configurables);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <returns>The <see cref="ServiceState"/> that this service is currently in.</returns>
|
||||||
|
public ServiceState GetServiceState()
|
||||||
|
{
|
||||||
|
lock (controlLock)
|
||||||
|
{
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <returns>The name of the module this service uses.</returns>
|
||||||
|
public string GetModuleName()
|
||||||
|
{
|
||||||
|
return moduleName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <returns>The name of assembly this module is contained in.</returns>
|
||||||
|
public string GetAssemblyName()
|
||||||
|
{
|
||||||
|
return assemblyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnServiceStateChange(object sender, ServiceState current)
|
||||||
|
{
|
||||||
|
lock (controlLock)
|
||||||
|
{
|
||||||
|
this.state = current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!disposed)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
ServiceConsoleStream?.Dispose();
|
||||||
|
}
|
||||||
|
//No unmanaged code, therefore, no finalizer.
|
||||||
|
disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(disposing: true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
80
src/GameServiceWarden.Host/Modules/ServiceManager.cs
Normal file
80
src/GameServiceWarden.Host/Modules/ServiceManager.cs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using GameServiceWarden.ModuleAPI;
|
||||||
|
|
||||||
|
namespace GameServiceWarden.Host.Modules
|
||||||
|
{
|
||||||
|
public class ServiceManager
|
||||||
|
{
|
||||||
|
private Dictionary<string, ServiceInfo> services = new Dictionary<string, ServiceInfo>();
|
||||||
|
private Dictionary<string, Dictionary<string, IGameServiceModule>> modules = new Dictionary<string, Dictionary<string, IGameServiceModule>>();
|
||||||
|
|
||||||
|
public void AddModule(string assemblyName, IGameServiceModule module)
|
||||||
|
{
|
||||||
|
if (!modules.ContainsKey(assemblyName)) modules.Add(assemblyName, new Dictionary<string, IGameServiceModule>());
|
||||||
|
modules[assemblyName][module.Name] = module;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveModule(string assemblyName, string moduleName)
|
||||||
|
{
|
||||||
|
if (!modules.ContainsKey(assemblyName) || !modules[assemblyName].ContainsKey(moduleName)) throw new KeyNotFoundException($"No module registered from {assemblyName} named {moduleName}.");
|
||||||
|
modules[assemblyName].Remove(moduleName);
|
||||||
|
if (modules[assemblyName].Count == 0) modules.Remove(assemblyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CreateService(string serviceName, string assemblyName, string moduleName)
|
||||||
|
{
|
||||||
|
if (!modules.ContainsKey(assemblyName) || modules[assemblyName].ContainsKey(moduleName)) throw new KeyNotFoundException($"No module registered from \"{assemblyName}\" named \"{moduleName}\".");
|
||||||
|
if (services.ContainsKey(serviceName)) throw new ArgumentException($"Service of Name \"{serviceName}\" already exists.");
|
||||||
|
|
||||||
|
services.Add(serviceName, new ServiceInfo(modules[assemblyName][moduleName].CreateGameService(), moduleName, assemblyName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyCollection<string> GetServiceNames()
|
||||||
|
{
|
||||||
|
string[] names = new string[services.Count];
|
||||||
|
services.Keys.CopyTo(names, 0);
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> GetServiceOptions(string serviceName)
|
||||||
|
{
|
||||||
|
if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found.");
|
||||||
|
ServiceInfo serviceInfo = services[serviceName];
|
||||||
|
return serviceInfo.GetConfigurables().Keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SetServiceOptionValue(string serviceName, string optionName, string value)
|
||||||
|
{
|
||||||
|
if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found.");
|
||||||
|
if (!services[serviceName].GetConfigurables().ContainsKey(optionName)) throw new KeyNotFoundException($"Option \"{optionName}\" for service \"{serviceName}\" not found.");
|
||||||
|
IConfigurable configurable = services[serviceName].GetConfigurables()[optionName];
|
||||||
|
return configurable.SetValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartService(string serviceName)
|
||||||
|
{
|
||||||
|
if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found.");
|
||||||
|
services[serviceName].Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopService(string serviceName)
|
||||||
|
{
|
||||||
|
if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found.");
|
||||||
|
services[serviceName].Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ExecuteCommand(string serviceName, string command)
|
||||||
|
{
|
||||||
|
if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found.");
|
||||||
|
services[serviceName].ExecuteCommand(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream GetServiceConsoleStream(string serviceName)
|
||||||
|
{
|
||||||
|
if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found.");
|
||||||
|
return services[serviceName].ServiceConsoleStream;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
46
src/GameServiceWarden.Host/Preferences/GeneralPreferences.cs
Normal file
46
src/GameServiceWarden.Host/Preferences/GeneralPreferences.cs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
|
namespace GameServiceWarden.Host.Preferences
|
||||||
|
{
|
||||||
|
[Serializable]
|
||||||
|
public class GeneralPreferences : IPersistable
|
||||||
|
{
|
||||||
|
//XML serialization invariants.
|
||||||
|
private readonly XmlSerializer xmlSerializer;
|
||||||
|
private readonly string APP_DATA_DIR;
|
||||||
|
|
||||||
|
//Preferences stored.
|
||||||
|
public int Port = 8080;
|
||||||
|
public string ListeningIP = IPAddress.Any.ToString();
|
||||||
|
public string ModuleDataPath;
|
||||||
|
|
||||||
|
public GeneralPreferences()
|
||||||
|
{
|
||||||
|
APP_DATA_DIR = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "/" + Assembly.GetAssembly(GetType()).GetName().Name + "/";
|
||||||
|
xmlSerializer = new XmlSerializer(GetType());
|
||||||
|
|
||||||
|
this.ModuleDataPath = APP_DATA_DIR + "modules/";
|
||||||
|
Load();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save()
|
||||||
|
{
|
||||||
|
using (FileStream writer = new FileStream(APP_DATA_DIR + GetType().Name + ".xml", FileMode.OpenOrCreate))
|
||||||
|
{
|
||||||
|
xmlSerializer.Serialize(writer, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Load()
|
||||||
|
{
|
||||||
|
using (FileStream reader = new FileStream(APP_DATA_DIR + GetType().Name + ".xml", FileMode.Open))
|
||||||
|
{
|
||||||
|
xmlSerializer.Deserialize(reader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
src/GameServiceWarden.Host/Preferences/IPersistable.cs
Normal file
8
src/GameServiceWarden.Host/Preferences/IPersistable.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace GameServiceWarden.Host.Preferences
|
||||||
|
{
|
||||||
|
public interface IPersistable
|
||||||
|
{
|
||||||
|
public void Save();
|
||||||
|
public void Load();
|
||||||
|
}
|
||||||
|
}
|
12
src/GameServiceWarden.Host/Program.cs
Normal file
12
src/GameServiceWarden.Host/Program.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace GameServiceWarden.Host
|
||||||
|
{
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
static void Main(string[] args)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Hello World!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
58
src/GameServiceWarden.Host/UMLSketch.drawio
Normal file
58
src/GameServiceWarden.Host/UMLSketch.drawio
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<mxfile host="65bd71144e" modified="2020-12-24T22:32:37.030Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Code/1.52.1 Chrome/83.0.4103.122 Electron/9.3.5 Safari/537.36" etag="XRoGvs6ajEFQR45Yu_qq" version="13.10.0" type="embed">
|
||||||
|
<diagram id="LHR7ubqCPd17_LyHkaH9" name="Structure">
|
||||||
|
<mxGraphModel dx="1009" dy="418" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0"/>
|
||||||
|
<mxCell id="1" parent="0"/>
|
||||||
|
<mxCell id="v9q6W0nyI9kZyF3peKlB-5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;jumpStyle=none;dashed=1;endArrow=block;endFill=0;exitX=1;exitY=0;exitDx=0;exitDy=0;" parent="1" source="v9q6W0nyI9kZyF3peKlB-1" target="v9q6W0nyI9kZyF3peKlB-4" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="v9q6W0nyI9kZyF3peKlB-1" value="<table border="1" width="100%" cellpadding="4" style="width: 100% ; height: 100% ; border-collapse: collapse"><tbody><tr><th align="center">ServiceInfo (entity)</th></tr><tr><td align="center">- serviceName: string<br>- controlLock: object<br>- state: ServiceState<br>- service: IGameService<br>- serviceConsoleStream: Stream<br>- moduleName: string<br>- assemblyName: string<br>- Dictionary&lt;string, IConfigurable&gt;<br>- disposed: bool</td></tr><tr><td align="center">+ Start(): void<br>+ Stop(): void<br>+ GetConfigurables(): IReadOnlyDictionary&lt;string, IConfigurable&gt;<br>+ GetServiceState(): ServiceState<br>+ getModuleName(): string<br>+ GetassemblyName(): string<br>+ SetServiceName(name: string): void // Implemented as property<br>+ GetServiceName(): string // Implemented as property<br>+ GetServiceConsoleStream(): Stream // Implemented as property<br>- OnServiceStateChange(curr: ServiceState,&nbsp;prev: ServiceState): void<br># Dispose(disposing: bool): void<br>+ Dispose(): void<br></td></tr></tbody></table>" style="text;html=1;fillColor=none;overflow=fill;strokeColor=#f0f0f0;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="950" y="950" width="400" height="410" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;fontStyle=1" parent="1" source="v9q6W0nyI9kZyF3peKlB-2" target="v9q6W0nyI9kZyF3peKlB-1" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="v9q6W0nyI9kZyF3peKlB-2" value="<table border="1" width="100%" cellpadding="4" style="width: 100% ; height: 100% ; border-collapse: collapse"><tbody><tr><th align="center">ServiceManager (Use-case)</th></tr><tr><td align="center">- services: Dictionary&lt;string, Service&gt;<br>- modules: Dictionary&lt;string, Dictionary&lt;string, IGameServiceModule&gt;&gt;</td></tr><tr><td align="center">+ AddModule(assemblyName: string, module: IGameServiceModule): void<br>+ RemoveModule(assemblyName: string, moduleName string): void<br>+ CreateService(serviceName: string, assemblyName: string, moduleName: string): void<br>+ GetServiceNames(): IReadOnlyCollection&lt;string&gt;<br>+ GetServiceOptions(serviceName: string): IEnumerable&lt;string&gt;<br>+ SetServiceOptionValue(serviceName: string, optionName: string, string: value): bool<br>+ StartService(serviceName: string): void<br>+ StopService(serviceName: string): void<br>+ ExecuteCommand(serviceName: string, command: string): void<br>+ GetServiceConsoleStream(): Stream</td></tr></tbody></table>" style="text;html=1;fillColor=none;overflow=fill;strokeColor=#f0f0f0;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="640" y="610" width="490" height="280" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="v9q6W0nyI9kZyF3peKlB-4" value="IDisposable" style="shape=ext;double=1;rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="1410" y="820" width="120" height="80" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="v9q6W0nyI9kZyF3peKlB-13" value="<table border="1" width="100%" cellpadding="4" style="width: 100% ; height: 100% ; border-collapse: collapse"><tbody><tr><th align="center">ModuleLoader (Gateway)</th></tr><tr><td align="center"></td></tr><tr><td align="center">- InstantiateServiceables(assembly: Assembly): void<br>- LoadAssembly(path: string): void<br>+ LoadModules(path: string): IEnumerable&lt;IGameServiceModule&gt;<br>+ LoadAllModules(path: string[]): IEnumerable&lt;IGameServiceModule&gt;<br>+ LoadAllModules(path: IEnumerable&lt;string&gt;): IEnumerable&lt;IGameServiceModule&gt;<br></td></tr></tbody></table>" style="text;html=1;fillColor=none;overflow=fill;strokeColor=#f0f0f0;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="110" y="280" width="490" height="250" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="titdvn9p0HDrujjw1N2D-4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.75;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;dashed=1;endArrow=block;endFill=0;" parent="1" source="v9q6W0nyI9kZyF3peKlB-16" target="v9q6W0nyI9kZyF3peKlB-27" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="v9q6W0nyI9kZyF3peKlB-16" target="v9q6W0nyI9kZyF3peKlB-2" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="v9q6W0nyI9kZyF3peKlB-16" target="v9q6W0nyI9kZyF3peKlB-13" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.75;exitDx=0;exitDy=0;entryX=1;entryY=0.25;entryDx=0;entryDy=0;" parent="1" source="v9q6W0nyI9kZyF3peKlB-16" target="v9q6W0nyI9kZyF3peKlB-23" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="v9q6W0nyI9kZyF3peKlB-16" value="<table border="1" width="100%" cellpadding="4" style="width: 100% ; height: 100% ; border-collapse: collapse"><tbody><tr><th align="center">ServiceController (Controller)</th></tr><tr><td align="center">- moduleLoader: ModuleLoader<br>- serviceManager: serviceManager</td></tr><tr><td align="center">+ LoadModulesInDirectory(directory: string): void<br>+ SaveServices(serviceDataDir: string): void<br>+ LoadServices(serviceDataDir: string): void</td></tr></tbody></table>" style="text;html=1;fillColor=none;overflow=fill;strokeColor=#f0f0f0;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="640" y="280" width="432.5" height="250" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="v9q6W0nyI9kZyF3peKlB-23" value="<table border="1" width="100%" cellpadding="4" style="width: 100% ; height: 100% ; border-collapse: collapse"><tbody><tr><th align="center">ServiceGateway (Gateway)</th></tr><tr><td align="center">- dataDirectory: string</td></tr><tr><td align="center">+ SaveService(name: string, assemblyName: string, moduleName: string): void<br>+ GetServiceName(path: string): string<br>+ GetServiceModuleName(path: string): string<br>+ GetAllServiceInfoPaths() IEnumerable&lt;string&gt;</td></tr></tbody></table>" style="text;html=1;fillColor=none;overflow=fill;strokeColor=#f0f0f0;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="110" y="545" width="490" height="230" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="v9q6W0nyI9kZyF3peKlB-27" value="<table border="1" width="100%" cellpadding="4" style="width: 100% ; height: 100% ; border-collapse: collapse"><tbody><tr><th align="center">ICommandable &lt;I&gt;</th></tr><tr><td align="center"></td></tr><tr><td align="center">+ GetPrefix(): string<br>+ Validate(string input): bool<br>+ Execute(string input): void<br>+ Help(): string</td></tr></tbody></table>" style="text;html=1;fillColor=none;overflow=fill;strokeColor=#f0f0f0;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="900" y="50" width="250" height="170" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="titdvn9p0HDrujjw1N2D-5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;dashed=1;endArrow=block;endFill=0;" parent="1" source="titdvn9p0HDrujjw1N2D-1" target="v9q6W0nyI9kZyF3peKlB-27" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="HWr5KmjBEDiam6OlhXxB-2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.25;exitY=0;exitDx=0;exitDy=0;entryX=1;entryY=1;entryDx=0;entryDy=0;endArrow=open;endFill=0;" parent="1" source="titdvn9p0HDrujjw1N2D-1" target="v9q6W0nyI9kZyF3peKlB-27" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="titdvn9p0HDrujjw1N2D-1" value="<table border="1" width="100%" cellpadding="4" style="width: 100% ; height: 100% ; border-collapse: collapse"><tbody><tr><th align="center">CommandFork (Controller)</th></tr><tr><td align="center">- commands: Dictionary&lt;string, ICommandable&gt;</td></tr><tr><td align="center"><br></td></tr></tbody></table>" style="text;html=1;fillColor=none;overflow=fill;strokeColor=#f0f0f0;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="1120" y="280" width="280" height="140" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>
|
@ -0,0 +1,7 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
9
src/GameServiceWarden.ModuleAPI/IConfigurable.cs
Normal file
9
src/GameServiceWarden.ModuleAPI/IConfigurable.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace GameServiceWarden.ModuleAPI
|
||||||
|
{
|
||||||
|
public interface IConfigurable
|
||||||
|
{
|
||||||
|
string OptionName { get; }
|
||||||
|
bool SetValue(string value);
|
||||||
|
string GetValue();
|
||||||
|
}
|
||||||
|
}
|
15
src/GameServiceWarden.ModuleAPI/IGameService.cs
Normal file
15
src/GameServiceWarden.ModuleAPI/IGameService.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace GameServiceWarden.ModuleAPI
|
||||||
|
{
|
||||||
|
public interface IGameService
|
||||||
|
{
|
||||||
|
event EventHandler<ServiceState> StateChangeEvent;
|
||||||
|
IReadOnlyCollection<IConfigurable> Configurables{ get; }
|
||||||
|
void InitializeService(TextWriter stream);
|
||||||
|
void ElegantShutdown();
|
||||||
|
void ExecuteCommand(string command);
|
||||||
|
}
|
||||||
|
}
|
28
src/GameServiceWarden.ModuleAPI/IGameServiceModule.cs
Normal file
28
src/GameServiceWarden.ModuleAPI/IGameServiceModule.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace GameServiceWarden.ModuleAPI
|
||||||
|
{
|
||||||
|
public interface IGameServiceModule
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the game service this module handles.
|
||||||
|
/// </summary>
|
||||||
|
string Name { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Description of the game service this module handles.
|
||||||
|
/// </summary>
|
||||||
|
string Description { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The authors responsible for creating this module.
|
||||||
|
/// </summary>
|
||||||
|
IEnumerable<string> Authors { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an instance of a the service to be used.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The <see cref="IGameService"/> responsible for the instance of the game service.</returns>
|
||||||
|
IGameService CreateGameService();
|
||||||
|
}
|
||||||
|
}
|
12
src/GameServiceWarden.ModuleAPI/ServiceState.cs
Normal file
12
src/GameServiceWarden.ModuleAPI/ServiceState.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace GameServiceWarden.ModuleAPI
|
||||||
|
{
|
||||||
|
public enum ServiceState
|
||||||
|
{
|
||||||
|
Stopped,
|
||||||
|
Running,
|
||||||
|
Error,
|
||||||
|
PendingRestart
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
|
||||||
|
<PackageReference Include="coverlet.collector" Version="1.2.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\GameServiceWarden.Host\GameServiceWarden.Host.csproj" />
|
||||||
|
<ProjectReference Include="..\..\src\GameServiceWarden.ModuleAPI\GameServiceWarden.ModuleAPI.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
@ -0,0 +1,21 @@
|
|||||||
|
using GameServiceWarden.ModuleAPI;
|
||||||
|
|
||||||
|
namespace GameServiceWarden.Host.Tests.Modules
|
||||||
|
{
|
||||||
|
public class FakeConfigurable : IConfigurable
|
||||||
|
{
|
||||||
|
private string value;
|
||||||
|
public string OptionName => "FakeOption";
|
||||||
|
|
||||||
|
public string GetValue()
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SetValue(string value)
|
||||||
|
{
|
||||||
|
this.value = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
tests/GameServiceWarden.Host.Tests/Modules/FakeService.cs
Normal file
32
tests/GameServiceWarden.Host.Tests/Modules/FakeService.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using GameServiceWarden.ModuleAPI;
|
||||||
|
|
||||||
|
namespace GameServiceWarden.Host.Tests.Modules
|
||||||
|
{
|
||||||
|
public class FakeService : IGameService
|
||||||
|
{
|
||||||
|
public IReadOnlyCollection<IConfigurable> Configurables { get; set; } = new HashSet<IConfigurable>();
|
||||||
|
|
||||||
|
public event EventHandler<ServiceState> StateChangeEvent;
|
||||||
|
|
||||||
|
public ServiceState CurrentState { get; private set; } = ServiceState.Stopped;
|
||||||
|
|
||||||
|
public void ElegantShutdown()
|
||||||
|
{
|
||||||
|
CurrentState = ServiceState.Stopped;
|
||||||
|
StateChangeEvent?.Invoke(this, CurrentState);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ExecuteCommand(string command)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InitializeService(TextWriter stream)
|
||||||
|
{
|
||||||
|
CurrentState = ServiceState.Running;
|
||||||
|
StateChangeEvent?.Invoke(this, CurrentState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
141
tests/GameServiceWarden.Host.Tests/Modules/ServiceInfoTest.cs
Normal file
141
tests/GameServiceWarden.Host.Tests/Modules/ServiceInfoTest.cs
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using GameServiceWarden.Host.Modules;
|
||||||
|
using GameServiceWarden.ModuleAPI;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace GameServiceWarden.Host.Tests.Modules
|
||||||
|
{
|
||||||
|
// Testing convention from: https://docs.microsoft.com/en-us/dotnet/core/testing/unit-testing-best-practices
|
||||||
|
// Fakes are generic test objects,
|
||||||
|
// mocks are the objects being asserted upon,
|
||||||
|
// stubs are objects used as part of the test.
|
||||||
|
public class ServiceInfoTest
|
||||||
|
{
|
||||||
|
//MethodTested_ScenarioTested_ExpectedBehavior
|
||||||
|
[Fact]
|
||||||
|
public void Start_FromStopped_StateIsRunning()
|
||||||
|
{
|
||||||
|
//Arrange, Act, Assert
|
||||||
|
IGameService stubGameService = new FakeService();
|
||||||
|
ServiceInfo serviceInfo = new ServiceInfo(stubGameService, "FakeModule", "FakeAssembly");
|
||||||
|
serviceInfo.Start();
|
||||||
|
Assert.Equal(ServiceState.Running, serviceInfo.GetServiceState());
|
||||||
|
serviceInfo.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Stop_FromStart_Stopped()
|
||||||
|
{
|
||||||
|
IGameService stubService = new FakeService();
|
||||||
|
ServiceInfo serviceInfo = new ServiceInfo(stubService, "FakeModule", "FakeAssembly");
|
||||||
|
serviceInfo.Start();
|
||||||
|
serviceInfo.Stop();
|
||||||
|
Assert.Equal(ServiceState.Stopped, serviceInfo.GetServiceState());
|
||||||
|
serviceInfo.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetConfigurables_ServiceStopped_ReturnsConfigurables()
|
||||||
|
{
|
||||||
|
//Given
|
||||||
|
FakeService stubService = new FakeService();
|
||||||
|
FakeConfigurable stubConfigurable = new FakeConfigurable();
|
||||||
|
HashSet<IConfigurable> configurables = new HashSet<IConfigurable>();
|
||||||
|
configurables.Add(stubConfigurable);
|
||||||
|
stubService.Configurables = configurables;
|
||||||
|
ServiceInfo serviceInfo = new ServiceInfo(stubService, "FakeModule", "FakeAssembly");
|
||||||
|
//When
|
||||||
|
serviceInfo.Start();
|
||||||
|
//Then
|
||||||
|
Assert.Contains(stubConfigurable, serviceInfo.GetConfigurables().Values);
|
||||||
|
serviceInfo.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetServiceState_ServiceNotStarted_ReturnsStoppedState()
|
||||||
|
{
|
||||||
|
//Given
|
||||||
|
IGameService stubService = new FakeService();
|
||||||
|
ServiceInfo serviceInfo = new ServiceInfo(stubService, "FakeModule", "FakeAssembly");
|
||||||
|
//Then
|
||||||
|
Assert.Equal(ServiceState.Stopped, serviceInfo.GetServiceState());
|
||||||
|
serviceInfo.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetServiceState_ServiceStarted_ReturnsRunningState()
|
||||||
|
{
|
||||||
|
//Given
|
||||||
|
IGameService stubService = new FakeService();
|
||||||
|
ServiceInfo serviceInfo = new ServiceInfo(stubService, "FakeModule", "FakeAssembly");
|
||||||
|
//When
|
||||||
|
serviceInfo.Start();
|
||||||
|
//Then
|
||||||
|
Assert.Equal(ServiceState.Running, serviceInfo.GetServiceState());
|
||||||
|
serviceInfo.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetModuleName_ServiceNotStarted_ReturnsSetName()
|
||||||
|
{
|
||||||
|
//Given
|
||||||
|
const string MODULE_NAME = "FakeModule";
|
||||||
|
IGameService stubService = new FakeService();
|
||||||
|
ServiceInfo serviceInfo = new ServiceInfo(stubService, MODULE_NAME, "FakeAssembly");
|
||||||
|
//Then
|
||||||
|
Assert.Equal(MODULE_NAME, serviceInfo.GetModuleName());
|
||||||
|
serviceInfo.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetAssemblyName_ServiceNotStarted_ReturnsSetAssemblyName()
|
||||||
|
{
|
||||||
|
//Given
|
||||||
|
const string ASSEMBLY_NAME = "FakeAssembly";
|
||||||
|
IGameService stubService = new FakeService();
|
||||||
|
ServiceInfo serviceInfo = new ServiceInfo(stubService, "FakeModule", ASSEMBLY_NAME);
|
||||||
|
//Then
|
||||||
|
Assert.Equal(ASSEMBLY_NAME, serviceInfo.GetAssemblyName());
|
||||||
|
serviceInfo.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetAndGetServiceName_ServiceNotStartedSingleThread_ServiceNameUpdated()
|
||||||
|
{
|
||||||
|
//Given
|
||||||
|
const string SERVICE_NAME = "Service";
|
||||||
|
IGameService stubService = new FakeService();
|
||||||
|
ServiceInfo serviceInfo = new ServiceInfo(stubService, "FakeModule", "FakeAssemblyName");
|
||||||
|
//When
|
||||||
|
serviceInfo.ServiceName = SERVICE_NAME;
|
||||||
|
//Then
|
||||||
|
Assert.Equal(SERVICE_NAME, serviceInfo.ServiceName);
|
||||||
|
serviceInfo.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ServiceConsoleStream_ServiceNotStarted_NullReturned()
|
||||||
|
{
|
||||||
|
//Given
|
||||||
|
IGameService stubService = new FakeService();
|
||||||
|
ServiceInfo serviceInfo = new ServiceInfo(stubService, "FakeModule", "FakeAssembly");
|
||||||
|
//Then
|
||||||
|
Assert.Null(serviceInfo.ServiceConsoleStream);
|
||||||
|
serviceInfo.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ServiceConsoleStream_ServiceStarted_StreamReturned()
|
||||||
|
{
|
||||||
|
//Given
|
||||||
|
IGameService stubService = new FakeService();
|
||||||
|
ServiceInfo serviceInfo = new ServiceInfo(stubService, "FakeModule", "FakeAssembly");
|
||||||
|
//When
|
||||||
|
serviceInfo.Start();
|
||||||
|
//Then
|
||||||
|
Assert.IsAssignableFrom<Stream>(serviceInfo.ServiceConsoleStream);
|
||||||
|
serviceInfo.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user