From 6467c178c31813c50d7cadfc05024763469c9427 Mon Sep 17 00:00:00 2001 From: Harrison Deng Date: Thu, 24 Dec 2020 16:33:17 -0600 Subject: [PATCH] Basic service entity completed and tested. added UML to guide actual implementation for Host. ModuleLoader, ServiceGateway, are written but untested. --- .../GameServiceWarden.Host.csproj | 12 ++ .../Logging/FileLogReceiver.cs | 14 ++ .../Logging/ILogRecievable.cs | 21 +++ .../Logging/LogLevel.cs | 10 ++ src/GameServiceWarden.Host/Logging/Logger.cs | 46 ++++++ .../CorruptedServiceInfoException.cs | 13 ++ .../Exceptions/NoServiceableFoundException.cs | 17 ++ .../Exceptions/NotServiceableTypeException.cs | 15 ++ .../Modules/ModuleLoadContext.cs | 33 ++++ .../Modules/ModuleLoader.cs | 84 ++++++++++ .../Modules/ServiceGateway.cs | 72 +++++++++ .../Modules/ServiceInfo.cs | 150 ++++++++++++++++++ .../Modules/ServiceManager.cs | 80 ++++++++++ .../Preferences/GeneralPreferences.cs | 46 ++++++ .../Preferences/IPersistable.cs | 8 + src/GameServiceWarden.Host/Program.cs | 12 ++ src/GameServiceWarden.Host/UMLSketch.drawio | 58 +++++++ .../GameServiceWarden.ModuleAPI.csproj | 7 + .../IConfigurable.cs | 9 ++ .../IGameService.cs | 15 ++ .../IGameServiceModule.cs | 28 ++++ .../ServiceState.cs | 12 ++ .../GameServiceWarden.Host.Tests.csproj | 21 +++ .../Modules/FakeConfigurable.cs | 21 +++ .../Modules/FakeService.cs | 32 ++++ .../Modules/ServiceInfoTest.cs | 141 ++++++++++++++++ 26 files changed, 977 insertions(+) create mode 100644 src/GameServiceWarden.Host/GameServiceWarden.Host.csproj create mode 100644 src/GameServiceWarden.Host/Logging/FileLogReceiver.cs create mode 100644 src/GameServiceWarden.Host/Logging/ILogRecievable.cs create mode 100644 src/GameServiceWarden.Host/Logging/LogLevel.cs create mode 100644 src/GameServiceWarden.Host/Logging/Logger.cs create mode 100644 src/GameServiceWarden.Host/Modules/Exceptions/CorruptedServiceInfoException.cs create mode 100644 src/GameServiceWarden.Host/Modules/Exceptions/NoServiceableFoundException.cs create mode 100644 src/GameServiceWarden.Host/Modules/Exceptions/NotServiceableTypeException.cs create mode 100644 src/GameServiceWarden.Host/Modules/ModuleLoadContext.cs create mode 100644 src/GameServiceWarden.Host/Modules/ModuleLoader.cs create mode 100644 src/GameServiceWarden.Host/Modules/ServiceGateway.cs create mode 100644 src/GameServiceWarden.Host/Modules/ServiceInfo.cs create mode 100644 src/GameServiceWarden.Host/Modules/ServiceManager.cs create mode 100644 src/GameServiceWarden.Host/Preferences/GeneralPreferences.cs create mode 100644 src/GameServiceWarden.Host/Preferences/IPersistable.cs create mode 100644 src/GameServiceWarden.Host/Program.cs create mode 100644 src/GameServiceWarden.Host/UMLSketch.drawio create mode 100644 src/GameServiceWarden.ModuleAPI/GameServiceWarden.ModuleAPI.csproj create mode 100644 src/GameServiceWarden.ModuleAPI/IConfigurable.cs create mode 100644 src/GameServiceWarden.ModuleAPI/IGameService.cs create mode 100644 src/GameServiceWarden.ModuleAPI/IGameServiceModule.cs create mode 100644 src/GameServiceWarden.ModuleAPI/ServiceState.cs create mode 100644 tests/GameServiceWarden.Host.Tests/GameServiceWarden.Host.Tests.csproj create mode 100644 tests/GameServiceWarden.Host.Tests/Modules/FakeConfigurable.cs create mode 100644 tests/GameServiceWarden.Host.Tests/Modules/FakeService.cs create mode 100644 tests/GameServiceWarden.Host.Tests/Modules/ServiceInfoTest.cs diff --git a/src/GameServiceWarden.Host/GameServiceWarden.Host.csproj b/src/GameServiceWarden.Host/GameServiceWarden.Host.csproj new file mode 100644 index 0000000..60aa430 --- /dev/null +++ b/src/GameServiceWarden.Host/GameServiceWarden.Host.csproj @@ -0,0 +1,12 @@ + + + + + + + + Exe + netcoreapp3.1 + + + diff --git a/src/GameServiceWarden.Host/Logging/FileLogReceiver.cs b/src/GameServiceWarden.Host/Logging/FileLogReceiver.cs new file mode 100644 index 0000000..246a42a --- /dev/null +++ b/src/GameServiceWarden.Host/Logging/FileLogReceiver.cs @@ -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) + { + + } + } +} \ No newline at end of file diff --git a/src/GameServiceWarden.Host/Logging/ILogRecievable.cs b/src/GameServiceWarden.Host/Logging/ILogRecievable.cs new file mode 100644 index 0000000..6ee8067 --- /dev/null +++ b/src/GameServiceWarden.Host/Logging/ILogRecievable.cs @@ -0,0 +1,21 @@ +using System; + +namespace GameServiceWarden.Host.Logging +{ + public interface ILogReceiver + { + /// + /// The severity of the messages this log should receive. + /// + /// The severity of the logs. + LogLevel Level { get; } + + /// + /// Logs the message. + /// + /// The message to be logged. + /// The time at which this message was requested to be logged. + /// The severity of this message. + void LogMessage(string message, DateTime time, LogLevel level); + } +} \ No newline at end of file diff --git a/src/GameServiceWarden.Host/Logging/LogLevel.cs b/src/GameServiceWarden.Host/Logging/LogLevel.cs new file mode 100644 index 0000000..4e6ec2e --- /dev/null +++ b/src/GameServiceWarden.Host/Logging/LogLevel.cs @@ -0,0 +1,10 @@ +namespace GameServiceWarden.Host.Logging +{ + public enum LogLevel + { + FATAL, + INFO, + WARNING, + DEBUG, + } +} \ No newline at end of file diff --git a/src/GameServiceWarden.Host/Logging/Logger.cs b/src/GameServiceWarden.Host/Logging/Logger.cs new file mode 100644 index 0000000..b4237a3 --- /dev/null +++ b/src/GameServiceWarden.Host/Logging/Logger.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; + +namespace GameServiceWarden.Host.Logging +{ + public class Logger { + private HashSet listeners = new HashSet(); + + /// + /// Logs the message to listeners that are listening to the set severity of the message or greater. + /// + /// The message to log. + /// The level of severity, by default, info. + public void Log(string message, LogLevel level = LogLevel.INFO) { + foreach (ILogReceiver listener in listeners) + { + if (level <= listener.Level) { + listener.LogMessage(message, DateTime.Now, level); + } + } + } + + /// + /// Adds a log listener. + /// + /// The listener to add. + public void AddLogListener(ILogReceiver listener) { + listeners.Add(listener); + } + + /// + /// Removes a log listener. + /// + /// The listener to remove. + public void RemoveLogListener(ILogReceiver listener) { + listeners.Remove(listener); + } + + /// + /// Called when all listeners should perform any flushing they need. + /// + public static void FlushListeners() { + + } + } +} \ No newline at end of file diff --git a/src/GameServiceWarden.Host/Modules/Exceptions/CorruptedServiceInfoException.cs b/src/GameServiceWarden.Host/Modules/Exceptions/CorruptedServiceInfoException.cs new file mode 100644 index 0000000..efff62c --- /dev/null +++ b/src/GameServiceWarden.Host/Modules/Exceptions/CorruptedServiceInfoException.cs @@ -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) { } + } +} \ No newline at end of file diff --git a/src/GameServiceWarden.Host/Modules/Exceptions/NoServiceableFoundException.cs b/src/GameServiceWarden.Host/Modules/Exceptions/NoServiceableFoundException.cs new file mode 100644 index 0000000..ab39402 --- /dev/null +++ b/src/GameServiceWarden.Host/Modules/Exceptions/NoServiceableFoundException.cs @@ -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) { } + } +} \ No newline at end of file diff --git a/src/GameServiceWarden.Host/Modules/Exceptions/NotServiceableTypeException.cs b/src/GameServiceWarden.Host/Modules/Exceptions/NotServiceableTypeException.cs new file mode 100644 index 0000000..59c4433 --- /dev/null +++ b/src/GameServiceWarden.Host/Modules/Exceptions/NotServiceableTypeException.cs @@ -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) { } + } +} \ No newline at end of file diff --git a/src/GameServiceWarden.Host/Modules/ModuleLoadContext.cs b/src/GameServiceWarden.Host/Modules/ModuleLoadContext.cs new file mode 100644 index 0000000..cbbde50 --- /dev/null +++ b/src/GameServiceWarden.Host/Modules/ModuleLoadContext.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/src/GameServiceWarden.Host/Modules/ModuleLoader.cs b/src/GameServiceWarden.Host/Modules/ModuleLoader.cs new file mode 100644 index 0000000..b3b67ae --- /dev/null +++ b/src/GameServiceWarden.Host/Modules/ModuleLoader.cs @@ -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 + { + /// + /// Loads an extension module. + /// + /// The path to the module. + /// An from the given module. + /// When the module requested to be loaded does not contain any public classes. + public IEnumerable LoadModules(string path) + { + return instantiateServiceable(loadAssembly(path)); + } + + /// + /// Loads all module for each given path to modules file. + /// + /// The paths to load modules for. + /// A where the key is a that is the associated path. + public Dictionary> LoadAllModules(IEnumerable paths) + { + Dictionary> res = new Dictionary>(); + foreach (string path in paths) + { + res.Add(path, LoadModules(path)); + } + return res; + } + + /// + /// Loads all module for each given path to modules file. + /// + /// The paths to load modules for. + /// A where the key is a that is the associated path. + public Dictionary> LoadAllModules(params string[] paths) + { + return LoadAllModules(paths); + } + + private Assembly loadAssembly(string path) + { + ModuleLoadContext moduleLoadContext = new ModuleLoadContext(path); + return moduleLoadContext.LoadFromAssemblyPath(path); + } + + private IEnumerable 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 typeNames = new List(); + 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}"); + } + } + } +} \ No newline at end of file diff --git a/src/GameServiceWarden.Host/Modules/ServiceGateway.cs b/src/GameServiceWarden.Host/Modules/ServiceGateway.cs new file mode 100644 index 0000000..77decd6 --- /dev/null +++ b/src/GameServiceWarden.Host/Modules/ServiceGateway.cs @@ -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 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 GetAllServiceInfoPaths() + { + string[] files = Directory.GetFiles(dataDirectory); + foreach (string filePath in files) + { + if (Path.GetExtension(filePath).Equals(EXTENSION)) + { + yield return filePath; + } + } + } + } +} \ No newline at end of file diff --git a/src/GameServiceWarden.Host/Modules/ServiceInfo.cs b/src/GameServiceWarden.Host/Modules/ServiceInfo.cs new file mode 100644 index 0000000..5cd8231 --- /dev/null +++ b/src/GameServiceWarden.Host/Modules/ServiceInfo.cs @@ -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 + { + + /// + /// The name of the service itself, independent of the name of the module this service is using. + /// + public string ServiceName { get { return serviceName; } set { Interlocked.Exchange(ref serviceName, value); } } + + /// + /// The services console output stream. + /// + 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 configurables = new Dictionary(); + 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); + } + } + + /// + /// Starts this service. + /// + /// Is thrown when the service is already running. + 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)); + } + } + + /// + /// Stops the service. + /// + /// Is thrown when the is not running. + public void Stop() + { + lock (controlLock) + { + if (state != ServiceState.Running) throw new InvalidOperationException("Service instance not running."); + this.service.ElegantShutdown(); + ServiceConsoleStream.Close(); + ServiceConsoleStream = null; + } + } + + /// + /// Sends a command to this service to execute. + /// + /// The command to execute. + /// Is thrown when the service is not running. + public void ExecuteCommand(string command) + { + lock (controlLock) + { + if (state != ServiceState.Running) throw new InvalidOperationException("Service instance not running."); + service.ExecuteCommand(command); + } + } + + /// + /// Gets the possible 's for this service. + /// + /// A is returned where the string is the option name and the configurable is what handles actually changing the values. + public IReadOnlyDictionary GetConfigurables() + { + return new ReadOnlyDictionary(this.configurables); + } + + /// The that this service is currently in. + public ServiceState GetServiceState() + { + lock (controlLock) + { + return state; + } + } + + /// The name of the module this service uses. + public string GetModuleName() + { + return moduleName; + } + + /// The name of assembly this module is contained in. + 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); + } + } +} \ No newline at end of file diff --git a/src/GameServiceWarden.Host/Modules/ServiceManager.cs b/src/GameServiceWarden.Host/Modules/ServiceManager.cs new file mode 100644 index 0000000..67cd648 --- /dev/null +++ b/src/GameServiceWarden.Host/Modules/ServiceManager.cs @@ -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 services = new Dictionary(); + private Dictionary> modules = new Dictionary>(); + + public void AddModule(string assemblyName, IGameServiceModule module) + { + if (!modules.ContainsKey(assemblyName)) modules.Add(assemblyName, new Dictionary()); + 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 GetServiceNames() + { + string[] names = new string[services.Count]; + services.Keys.CopyTo(names, 0); + return names; + } + + public IEnumerable 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; + } + } +} \ No newline at end of file diff --git a/src/GameServiceWarden.Host/Preferences/GeneralPreferences.cs b/src/GameServiceWarden.Host/Preferences/GeneralPreferences.cs new file mode 100644 index 0000000..28f375b --- /dev/null +++ b/src/GameServiceWarden.Host/Preferences/GeneralPreferences.cs @@ -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); + } + } + } +} \ No newline at end of file diff --git a/src/GameServiceWarden.Host/Preferences/IPersistable.cs b/src/GameServiceWarden.Host/Preferences/IPersistable.cs new file mode 100644 index 0000000..04804bc --- /dev/null +++ b/src/GameServiceWarden.Host/Preferences/IPersistable.cs @@ -0,0 +1,8 @@ +namespace GameServiceWarden.Host.Preferences +{ + public interface IPersistable + { + public void Save(); + public void Load(); + } +} \ No newline at end of file diff --git a/src/GameServiceWarden.Host/Program.cs b/src/GameServiceWarden.Host/Program.cs new file mode 100644 index 0000000..c9f3241 --- /dev/null +++ b/src/GameServiceWarden.Host/Program.cs @@ -0,0 +1,12 @@ +using System; + +namespace GameServiceWarden.Host +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + } + } +} diff --git a/src/GameServiceWarden.Host/UMLSketch.drawio b/src/GameServiceWarden.Host/UMLSketch.drawio new file mode 100644 index 0000000..0d72182 --- /dev/null +++ b/src/GameServiceWarden.Host/UMLSketch.drawio @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/GameServiceWarden.ModuleAPI/GameServiceWarden.ModuleAPI.csproj b/src/GameServiceWarden.ModuleAPI/GameServiceWarden.ModuleAPI.csproj new file mode 100644 index 0000000..9f5c4f4 --- /dev/null +++ b/src/GameServiceWarden.ModuleAPI/GameServiceWarden.ModuleAPI.csproj @@ -0,0 +1,7 @@ + + + + netstandard2.0 + + + diff --git a/src/GameServiceWarden.ModuleAPI/IConfigurable.cs b/src/GameServiceWarden.ModuleAPI/IConfigurable.cs new file mode 100644 index 0000000..7081f48 --- /dev/null +++ b/src/GameServiceWarden.ModuleAPI/IConfigurable.cs @@ -0,0 +1,9 @@ +namespace GameServiceWarden.ModuleAPI +{ + public interface IConfigurable + { + string OptionName { get; } + bool SetValue(string value); + string GetValue(); + } +} \ No newline at end of file diff --git a/src/GameServiceWarden.ModuleAPI/IGameService.cs b/src/GameServiceWarden.ModuleAPI/IGameService.cs new file mode 100644 index 0000000..2859b10 --- /dev/null +++ b/src/GameServiceWarden.ModuleAPI/IGameService.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace GameServiceWarden.ModuleAPI +{ + public interface IGameService + { + event EventHandler StateChangeEvent; + IReadOnlyCollection Configurables{ get; } + void InitializeService(TextWriter stream); + void ElegantShutdown(); + void ExecuteCommand(string command); + } +} \ No newline at end of file diff --git a/src/GameServiceWarden.ModuleAPI/IGameServiceModule.cs b/src/GameServiceWarden.ModuleAPI/IGameServiceModule.cs new file mode 100644 index 0000000..05052d8 --- /dev/null +++ b/src/GameServiceWarden.ModuleAPI/IGameServiceModule.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace GameServiceWarden.ModuleAPI +{ + public interface IGameServiceModule + { + /// + /// The name of the game service this module handles. + /// + string Name { get; } + + /// + /// Description of the game service this module handles. + /// + string Description { get; } + + /// + /// The authors responsible for creating this module. + /// + IEnumerable Authors { get; } + + /// + /// Creates an instance of a the service to be used. + /// + /// The responsible for the instance of the game service. + IGameService CreateGameService(); + } +} \ No newline at end of file diff --git a/src/GameServiceWarden.ModuleAPI/ServiceState.cs b/src/GameServiceWarden.ModuleAPI/ServiceState.cs new file mode 100644 index 0000000..6a1c453 --- /dev/null +++ b/src/GameServiceWarden.ModuleAPI/ServiceState.cs @@ -0,0 +1,12 @@ +using System; + +namespace GameServiceWarden.ModuleAPI +{ + public enum ServiceState + { + Stopped, + Running, + Error, + PendingRestart + } +} \ No newline at end of file diff --git a/tests/GameServiceWarden.Host.Tests/GameServiceWarden.Host.Tests.csproj b/tests/GameServiceWarden.Host.Tests/GameServiceWarden.Host.Tests.csproj new file mode 100644 index 0000000..deb112d --- /dev/null +++ b/tests/GameServiceWarden.Host.Tests/GameServiceWarden.Host.Tests.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + + + diff --git a/tests/GameServiceWarden.Host.Tests/Modules/FakeConfigurable.cs b/tests/GameServiceWarden.Host.Tests/Modules/FakeConfigurable.cs new file mode 100644 index 0000000..5d57485 --- /dev/null +++ b/tests/GameServiceWarden.Host.Tests/Modules/FakeConfigurable.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/tests/GameServiceWarden.Host.Tests/Modules/FakeService.cs b/tests/GameServiceWarden.Host.Tests/Modules/FakeService.cs new file mode 100644 index 0000000..0e8fd86 --- /dev/null +++ b/tests/GameServiceWarden.Host.Tests/Modules/FakeService.cs @@ -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 Configurables { get; set; } = new HashSet(); + + public event EventHandler 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); + } + } +} \ No newline at end of file diff --git a/tests/GameServiceWarden.Host.Tests/Modules/ServiceInfoTest.cs b/tests/GameServiceWarden.Host.Tests/Modules/ServiceInfoTest.cs new file mode 100644 index 0000000..5da54b4 --- /dev/null +++ b/tests/GameServiceWarden.Host.Tests/Modules/ServiceInfoTest.cs @@ -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 configurables = new HashSet(); + 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(serviceInfo.ServiceConsoleStream); + serviceInfo.Dispose(); + } + } +} \ No newline at end of file