From 35a2765559ac80c0a660c56205985282708d21e9 Mon Sep 17 00:00:00 2001 From: Harrison Deng Date: Mon, 19 Apr 2021 01:34:45 -0500 Subject: [PATCH] Changed logging system to be event based. Implemented LRUCache for instantiating services without starting. Service state changed back to enumerator. Namespace refactoring. --- src/GameServiceWarden.API.Module/IService.cs | 6 +- .../IServiceModule.cs | 3 +- .../ServiceRunState.cs | 1 + .../Games/ServiceManagerState.cs | 2 +- .../Games/ServiceDescriptor.cs | 264 ------------------ .../Exceptions/ModuleLoadException.cs | 2 +- .../ServiceInitializationException.cs | 2 +- .../{Games => Module}/IServiceExecuter.cs | 2 +- .../IServiceManagerMonitor.cs | 2 +- src/GameServiceWarden.Core/Module/LRUCache.cs | 77 +++++ .../Modules => Module}/ModuleLoadContext.cs | 2 +- .../ModuleLoader.cs} | 6 +- .../Module/ServiceDescriptor.cs | 156 +++++++++++ .../{Games => Module}/ServiceManager.cs | 98 ++++--- .../Persistence/IPersistent.cs | 2 +- .../ServiceDescriptorPersistence.cs | 41 ++- .../Persistence/ServiceModules.cs | 4 +- .../UI/IPCController.cs | 2 +- src/GameServiceWarden.Core/UI/IPCPresenter.cs | 2 +- .../Modules/Games/FakePersistence.cs | 2 +- .../Modules/Games/FakeService.cs | 21 +- .../Games/FakeServiceManagerMonitor.cs | 2 +- .../Modules/Games/FakeServiceModule.cs | 2 +- .../Modules/Games/ServiceDescriptorTest.cs | 39 ++- .../Modules/Games/ServiceManagerTest.cs | 133 ++------- .../ServiceDescriptorPersistenceTest.cs | 29 +- 26 files changed, 404 insertions(+), 498 deletions(-) delete mode 100644 src/GameServiceWarden.Core/Games/ServiceDescriptor.cs rename src/GameServiceWarden.Core/{Games/Modules => Module}/Exceptions/ModuleLoadException.cs (89%) rename src/GameServiceWarden.Core/{Games/Modules => Module}/Exceptions/ServiceInitializationException.cs (90%) rename src/GameServiceWarden.Core/{Games => Module}/IServiceExecuter.cs (79%) rename src/GameServiceWarden.Core/{Games => Module}/IServiceManagerMonitor.cs (78%) create mode 100644 src/GameServiceWarden.Core/Module/LRUCache.cs rename src/GameServiceWarden.Core/{Games/Modules => Module}/ModuleLoadContext.cs (95%) rename src/GameServiceWarden.Core/{Games/Modules/ServiceModuleLoader.cs => Module/ModuleLoader.cs} (94%) create mode 100644 src/GameServiceWarden.Core/Module/ServiceDescriptor.cs rename src/GameServiceWarden.Core/{Games => Module}/ServiceManager.cs (67%) diff --git a/src/GameServiceWarden.API.Module/IService.cs b/src/GameServiceWarden.API.Module/IService.cs index 349535a..864c29d 100644 --- a/src/GameServiceWarden.API.Module/IService.cs +++ b/src/GameServiceWarden.API.Module/IService.cs @@ -6,10 +6,12 @@ namespace GameServiceWarden.API.Module { public interface IService { - event EventHandler StateChangeEvent; + event EventHandler StateChangeEvent; + event EventHandler UpdateLogEvent; IReadOnlyCollection Configurables{ get; } - void InitializeService(Stream stream); + void InitializeService(); void ElegantShutdown(); + byte[] GetLogBuffer(); void ExecuteCommand(string command); } } \ No newline at end of file diff --git a/src/GameServiceWarden.API.Module/IServiceModule.cs b/src/GameServiceWarden.API.Module/IServiceModule.cs index f3f920f..a54423c 100644 --- a/src/GameServiceWarden.API.Module/IServiceModule.cs +++ b/src/GameServiceWarden.API.Module/IServiceModule.cs @@ -23,8 +23,7 @@ namespace GameServiceWarden.API.Module /// Creates an instance of a the service to be used. /// /// The workspace directory. All service required files should be stored here. Expect the directory to be created. - /// Whether or not this game service is new. That is, the workspace can be assumed empty. /// The responsible for the instance of the game service. - IService InstantiateService(string workspace, bool clean); + IService InstantiateService(string workspace); } } \ No newline at end of file diff --git a/src/GameServiceWarden.API.Module/ServiceRunState.cs b/src/GameServiceWarden.API.Module/ServiceRunState.cs index 13d71c5..cde3981 100644 --- a/src/GameServiceWarden.API.Module/ServiceRunState.cs +++ b/src/GameServiceWarden.API.Module/ServiceRunState.cs @@ -4,5 +4,6 @@ namespace GameServiceWarden.API.Module { Stopped, Running, + RestartPending } } \ No newline at end of file diff --git a/src/GameServiceWarden.API/Games/ServiceManagerState.cs b/src/GameServiceWarden.API/Games/ServiceManagerState.cs index 083b9a2..2358d4b 100644 --- a/src/GameServiceWarden.API/Games/ServiceManagerState.cs +++ b/src/GameServiceWarden.API/Games/ServiceManagerState.cs @@ -9,7 +9,7 @@ namespace GameServiceWarden.API.Games public ICollection services; public ICollection running; public ICollection modules; - public IReadOnlyDictionary logs; + public IReadOnlyDictionary logs; public IReadOnlyDictionary> serviceOptions; } } \ No newline at end of file diff --git a/src/GameServiceWarden.Core/Games/ServiceDescriptor.cs b/src/GameServiceWarden.Core/Games/ServiceDescriptor.cs deleted file mode 100644 index a3336df..0000000 --- a/src/GameServiceWarden.Core/Games/ServiceDescriptor.cs +++ /dev/null @@ -1,264 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Globalization; -using System.IO; -using System.IO.Pipes; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using GameServiceWarden.Core.Games.Modules.Exceptions; -using GameServiceWarden.Core.Logging; -using GameServiceWarden.API.Module; -using System.Net.Sockets; - -//TODO Update UML -namespace GameServiceWarden.Core.Games -{ - public class ServiceDescriptor //entity - { - private const string LOG_DISTRIBUTOR_PREFIX = "log_dist_"; - private const int TIMEOUT = 1000; - /// - /// The name of the service itself, independent of the name of the module this service is using. - /// - public string ServiceName { get { return serviceName; } } - private readonly string serviceName; - private readonly Guid runningUID; - private volatile bool running; - private readonly IService service; - /// - /// The services log output pipe name. - /// - public string ServiceLogPipeName { get { return (runningUID.ToString() + ".pipe"); } } - private string moduleName; - private readonly string assemblyName; - private volatile NamedPipeServerStream logReceiver; - private volatile NamedPipeClientStream logSender; - private ConcurrentStack logStreamListeners; - private Task logUpdateTask; - private Task listenTask; - private volatile CancellationTokenSource stopToken; - private NamedPipeServerStream acceptingPipe; - - /// - /// Name of module this service uses. - /// - private readonly IReadOnlyDictionary configurables; - public event EventHandler ServiceStateChangeEvent; - public ServiceDescriptor(IService service, string serviceName, string moduleName, string assemblyName) - { - this.service = service ?? throw new ArgumentNullException("service"); - this.moduleName = moduleName ?? throw new ArgumentNullException("moduleName"); - this.assemblyName = assemblyName ?? throw new ArgumentNullException("assemblyName"); - this.serviceName = serviceName ?? throw new ArgumentNullException("serviceName"); - this.service.StateChangeEvent += OnServiceStateChange; - runningUID = Guid.NewGuid(); - - - Dictionary tempConfigurables = new Dictionary(); - foreach (IServiceConfigurable configurable in service.Configurables) - { - tempConfigurables.Add(configurable.OptionName, configurable); - } - this.configurables = new ReadOnlyDictionary(tempConfigurables); - - logStreamListeners = new ConcurrentStack(); - } - - /// - /// Starts this service. - /// - /// Is thrown when the service is already running. - public void Start() - { - Logger.Log($"\"{ServiceName}\" is starting."); - if (running) throw new InvalidOperationException("Service instance already running."); - logReceiver = new NamedPipeServerStream(LOG_DISTRIBUTOR_PREFIX + ServiceLogPipeName, PipeDirection.In, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); - Task waitForConnection = logReceiver.WaitForConnectionAsync(); - logSender = new NamedPipeClientStream(".", LOG_DISTRIBUTOR_PREFIX + ServiceLogPipeName, PipeDirection.Out); - logSender.Connect(); - waitForConnection.Wait(); - byte[] idToken = Guid.NewGuid().ToByteArray(); - ValueTask sendTokenTask = logSender.WriteAsync(idToken); - byte[] receivedToken = new byte[idToken.Length]; - logReceiver.Read(receivedToken); - - if (!sendTokenTask.AsTask().Wait(500) || !sendTokenTask.IsCompletedSuccessfully) - { - throw new ServiceInitializationException("Error while sending identification token."); - } - if (!idToken.SequenceEqual(receivedToken)) - { - throw new ServiceInitializationException("Wrong distributor identification token."); - } - CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(TIMEOUT); - Task initializationTask = Task.Run(() => service.InitializeService(logSender), cancellationTokenSource.Token); - initializationTask.Wait(); - cancellationTokenSource.Dispose(); - stopToken = new CancellationTokenSource(); - listenTask = AcceptLogConnections(); - logUpdateTask = BroadcastLog(); - } - - /// - /// Stops the service. - /// - /// Is thrown when the is not running. - public void Stop() - { - if (!running) throw new InvalidOperationException("Service instance not running."); - Logger.Log($"\"{ServiceName}\" is stopping."); - service.ElegantShutdown(); - stopToken.Cancel(); // Doesn't work on Linux(?) - acceptingPipe.Dispose(); //Handles Linux case - logSender.Dispose(); //Makes sure logging client is disposed - logReceiver.Dispose(); //Closes receiver (Linux doesn't respond to cancellations, needed to dispose either way). - - NamedPipeServerStream terminatingPipe; - while (logStreamListeners.TryPop(out terminatingPipe)) - { - terminatingPipe.Dispose(); // Required before waiting since this is under listenTask. - } - try - { - if (!listenTask.Wait(TIMEOUT)) { - throw new TimeoutException($"Could not stop \"{ServiceName}\" accepting task within {TIMEOUT}ms."); - } - } - catch (AggregateException e) - { - e.Handle((exception) => exception is TaskCanceledException || (exception is SocketException && exception.Message.Equals("Operation canceled"))); //Task cancel for Windows, Socket for operation cancellation. - } - try - { - if (!logUpdateTask.Wait(TIMEOUT)) { - throw new TimeoutException($"Could not stop \"{ServiceName}\" broadcast within{TIMEOUT}ms."); - } - } - catch (AggregateException e) - { - e.Handle((exception) => exception is TaskCanceledException || (exception is SocketException && exception.Message.Equals("Operation canceled"))); //Same as above. - } - stopToken.Dispose(); - } - - /// - /// 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) - { - Logger.Log($"\"{ServiceName}\" is executing command \"{command}\".", LogLevel.DEBUG); - if (!running) throw new InvalidOperationException("Service instance not running."); - service.ExecuteCommand(command); - } - - /// - /// Gets the possible 's names for this service. - /// - /// A returned where the string is the option's name. - public ISet GetConfigurableOptions() - { - return new HashSet(this.configurables.Keys); - } - - public bool SetConfigurableValue(string configurationName, string value) - { - if (!this.configurables.ContainsKey(configurationName)) throw new KeyNotFoundException($"Unable to find option with name \"{configurationName}\"."); - return this.configurables[configurationName].SetValue(value); - } - - public string GetConfigurableValue(string configurationName) - { - if (!this.configurables.ContainsKey(configurationName)) throw new KeyNotFoundException($"Unable to find option with name \"{configurationName}\"."); - return this.configurables[configurationName].GetValue(); - } - - public bool GetServiceState() - { - return running; - } - - public string GetServiceName() - { - return serviceName; - } - - public string GetModuleName() - { - return moduleName; - } - - /// The name of assembly this module is contained in. - public string GetAssemblyName() - { - return assemblyName; - } - - private void OnServiceStateChange(object sender, bool running) - { - this.running = running; - Logger.Log($"The service \"{ServiceName}\" is changing states to {(running ? "running" : "stopped")}.", LogLevel.DEBUG); - ServiceStateChangeEvent?.Invoke(this, running); - } - - private async Task AcceptLogConnections() - { - Logger.Log($"\"{ServiceName}\" is now accepting log listeners."); - while (running) - { - NamedPipeServerStream pipe = new NamedPipeServerStream(ServiceLogPipeName, PipeDirection.Out, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); - acceptingPipe = pipe; - await pipe.WaitForConnectionAsync(stopToken.Token); - Logger.Log($"A log listener has connected. Currently broadcasting to {logStreamListeners.Count + 1} listener(s).", LogLevel.DEBUG); - logStreamListeners.Push(pipe); - } - Logger.Log($"\"{ServiceName}\" stopped accepting log listeners."); - } - - private async Task BroadcastLog() - { - Stack completeStack = new Stack(); - Stack<(Task, CancellationTokenSource)> writeTasks = new Stack<(Task, CancellationTokenSource)>(); - byte[] buffer = new byte[1024 * 8]; - int fill; - Logger.Log($"\"{ServiceName}\" is now listening to the service log and broadcasting."); - while (running && (fill = await logReceiver.ReadAsync(buffer, 0, buffer.Length, stopToken.Token)) > 0) - { - Logger.Log($"Broadcasting {fill} bytes.", LogLevel.DEBUG); - NamedPipeServerStream pipe; - while (logStreamListeners.TryPop(out pipe)) - { - if (!pipe.IsConnected) - { - pipe.Dispose(); - Logger.Log($"\"{ServiceName}\" detected a disconnected log listener. Removing from list of listener(s).", LogLevel.DEBUG); - } - else - { - CancellationTokenSource cancelToken = new CancellationTokenSource(1000); - writeTasks.Push((pipe.WriteAsync(buffer, 0, fill, cancelToken.Token), cancelToken)); - completeStack.Push(pipe); - } - } - NamedPipeServerStream completePipe; - while (completeStack.TryPop(out completePipe)) - { - logStreamListeners.Push(completePipe); - } - (Task, CancellationTokenSource) taskAndCancel; - while (writeTasks.TryPop(out taskAndCancel)) - { - await taskAndCancel.Item1; - taskAndCancel.Item2.Dispose(); - } - Logger.Log($"\"{ServiceName}\" broadcasted to {logStreamListeners.Count} listener(s).", LogLevel.DEBUG); - } - Logger.Log($"\"{ServiceName}\" stopped listening to service log."); - } - } -} \ No newline at end of file diff --git a/src/GameServiceWarden.Core/Games/Modules/Exceptions/ModuleLoadException.cs b/src/GameServiceWarden.Core/Module/Exceptions/ModuleLoadException.cs similarity index 89% rename from src/GameServiceWarden.Core/Games/Modules/Exceptions/ModuleLoadException.cs rename to src/GameServiceWarden.Core/Module/Exceptions/ModuleLoadException.cs index f84e5a4..a3394c7 100644 --- a/src/GameServiceWarden.Core/Games/Modules/Exceptions/ModuleLoadException.cs +++ b/src/GameServiceWarden.Core/Module/Exceptions/ModuleLoadException.cs @@ -1,7 +1,7 @@ using System; using System.Runtime.Serialization; -namespace GameServiceWarden.Core.Games.Modules.Exceptions +namespace GameServiceWarden.Core.Module.Exceptions { [System.Serializable] public class ModuleLoadException : Exception diff --git a/src/GameServiceWarden.Core/Games/Modules/Exceptions/ServiceInitializationException.cs b/src/GameServiceWarden.Core/Module/Exceptions/ServiceInitializationException.cs similarity index 90% rename from src/GameServiceWarden.Core/Games/Modules/Exceptions/ServiceInitializationException.cs rename to src/GameServiceWarden.Core/Module/Exceptions/ServiceInitializationException.cs index 3ce2335..ea46ac3 100644 --- a/src/GameServiceWarden.Core/Games/Modules/Exceptions/ServiceInitializationException.cs +++ b/src/GameServiceWarden.Core/Module/Exceptions/ServiceInitializationException.cs @@ -1,4 +1,4 @@ -namespace GameServiceWarden.Core.Games.Modules.Exceptions +namespace GameServiceWarden.Core.Module.Exceptions { [System.Serializable] public class ServiceInitializationException : System.Exception diff --git a/src/GameServiceWarden.Core/Games/IServiceExecuter.cs b/src/GameServiceWarden.Core/Module/IServiceExecuter.cs similarity index 79% rename from src/GameServiceWarden.Core/Games/IServiceExecuter.cs rename to src/GameServiceWarden.Core/Module/IServiceExecuter.cs index 7fd75ff..a08c34f 100644 --- a/src/GameServiceWarden.Core/Games/IServiceExecuter.cs +++ b/src/GameServiceWarden.Core/Module/IServiceExecuter.cs @@ -1,6 +1,6 @@ using GameServiceWarden.API.Games; -namespace GameServiceWarden.Core.Games +namespace GameServiceWarden.Core.Module { public interface IServiceManagerActionExecuter { diff --git a/src/GameServiceWarden.Core/Games/IServiceManagerMonitor.cs b/src/GameServiceWarden.Core/Module/IServiceManagerMonitor.cs similarity index 78% rename from src/GameServiceWarden.Core/Games/IServiceManagerMonitor.cs rename to src/GameServiceWarden.Core/Module/IServiceManagerMonitor.cs index a75a5d6..29e9f85 100644 --- a/src/GameServiceWarden.Core/Games/IServiceManagerMonitor.cs +++ b/src/GameServiceWarden.Core/Module/IServiceManagerMonitor.cs @@ -1,6 +1,6 @@ using GameServiceWarden.API.Games; -namespace GameServiceWarden.Core.Games +namespace GameServiceWarden.Core.Module { public interface IServiceManagerMonitor { diff --git a/src/GameServiceWarden.Core/Module/LRUCache.cs b/src/GameServiceWarden.Core/Module/LRUCache.cs new file mode 100644 index 0000000..2a227e1 --- /dev/null +++ b/src/GameServiceWarden.Core/Module/LRUCache.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; + +namespace GameServiceWarden.Core.Module +{ + public class LRUCache + { + private class Node + { + public K key; + public V value; + public Node front; + public Node back; + } + + public int Size {get { return size; } } + public int Length {get { return valueDictionary.Count; } } + private readonly int size; + private Node top; + private Node bottom; + private Dictionary valueDictionary; + private Action cleanupAction; + + public LRUCache(int size = 100, Action cleanup = null) + { + this.size = size; + valueDictionary = new Dictionary(size); + this.cleanupAction = cleanup; + } + + private void MoveToTop(K key) { + Node node = valueDictionary[key]; + if (node != null && top != node) { + node.front.back = node.back; + node.back = top; + node.front = null; + top = node; + } + } + + private Node AddToTop(K key, V value) { + Node node = new Node(); + node.front = null; + node.back = top; + node.value = value; + node.key = key; + top = node; + if (bottom == null) { + bottom = node; + } else if (valueDictionary.Count == Size) { + valueDictionary.Remove(bottom.key); + cleanupAction(bottom.value); + bottom = bottom.front; + } + valueDictionary[key] = node; + return node; + } + + public V Use(K key, Func generate) { + if (generate == null) throw new ArgumentNullException("generate"); + Node value = null; + if (valueDictionary.ContainsKey(key)) { + value = valueDictionary[key]; + MoveToTop(key); + } else { + value = AddToTop(key, generate()); + } + return value.value; + } + + public void Clear() { + top = null; + bottom = null; + valueDictionary.Clear(); + } + } +} \ No newline at end of file diff --git a/src/GameServiceWarden.Core/Games/Modules/ModuleLoadContext.cs b/src/GameServiceWarden.Core/Module/ModuleLoadContext.cs similarity index 95% rename from src/GameServiceWarden.Core/Games/Modules/ModuleLoadContext.cs rename to src/GameServiceWarden.Core/Module/ModuleLoadContext.cs index 57cd064..29d8f84 100644 --- a/src/GameServiceWarden.Core/Games/Modules/ModuleLoadContext.cs +++ b/src/GameServiceWarden.Core/Module/ModuleLoadContext.cs @@ -2,7 +2,7 @@ using System; using System.Reflection; using System.Runtime.Loader; -namespace GameServiceWarden.Core.Games.Modules +namespace GameServiceWarden.Core.Module { class ModuleLoadContext : AssemblyLoadContext { diff --git a/src/GameServiceWarden.Core/Games/Modules/ServiceModuleLoader.cs b/src/GameServiceWarden.Core/Module/ModuleLoader.cs similarity index 94% rename from src/GameServiceWarden.Core/Games/Modules/ServiceModuleLoader.cs rename to src/GameServiceWarden.Core/Module/ModuleLoader.cs index 76a6d05..d6bacf0 100644 --- a/src/GameServiceWarden.Core/Games/Modules/ServiceModuleLoader.cs +++ b/src/GameServiceWarden.Core/Module/ModuleLoader.cs @@ -1,12 +1,12 @@ using System; using System.Collections.Generic; using System.Reflection; -using GameServiceWarden.Core.Games.Modules.Exceptions; +using GameServiceWarden.Core.Module.Exceptions; using GameServiceWarden.API.Module; -namespace GameServiceWarden.Core.Games.Modules +namespace GameServiceWarden.Core.Module { - public class ServiceModuleLoader //Gateway + public class ModuleLoader //Gateway { /// /// Loads an extension module. diff --git a/src/GameServiceWarden.Core/Module/ServiceDescriptor.cs b/src/GameServiceWarden.Core/Module/ServiceDescriptor.cs new file mode 100644 index 0000000..8669c76 --- /dev/null +++ b/src/GameServiceWarden.Core/Module/ServiceDescriptor.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.IO; +using System.IO.Pipes; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using GameServiceWarden.Core.Module.Exceptions; +using GameServiceWarden.Core.Logging; +using GameServiceWarden.API.Module; +using System.Net.Sockets; + +//TODO Update UML +namespace GameServiceWarden.Core.Module +{ + public class ServiceDescriptor //entity + { + public const string MODULE_PROPERTY = "SERVICE_MODULE"; + public const string ASSEMBLY_PROPERTY = "SERVICE_ASSEMBLY"; + private const int TIMEOUT = 1000; + /// + /// The name of the service itself, independent of the name of the module this service is using. + /// + public string ServiceName { get { return serviceName; } } + private readonly string serviceName; + private volatile ServiceState state; + private readonly IService service; + private string moduleName; + private readonly string assemblyName; + /// + /// Name of module this service uses. + /// + private readonly IReadOnlyDictionary configurables; + public event EventHandler ServiceStateChangeEvent; + public event EventHandler LogUpdateEvent; + public ServiceDescriptor(IService service, string serviceName, string moduleName, string assemblyName) + { + this.service = service ?? throw new ArgumentNullException("service"); + this.moduleName = moduleName ?? throw new ArgumentNullException("moduleName"); + this.assemblyName = assemblyName ?? throw new ArgumentNullException("assemblyName"); + this.serviceName = serviceName ?? throw new ArgumentNullException("serviceName"); + this.service.StateChangeEvent += OnServiceStateChange; + this.service.UpdateLogEvent += OnLogUpdate; + + Dictionary tempConfigurables = new Dictionary(); + foreach (IServiceConfigurable configurable in service.Configurables) + { + tempConfigurables.Add(configurable.OptionName, configurable); + } + this.configurables = new ReadOnlyDictionary(tempConfigurables); + } + + /// + /// Starts this service. + /// + /// Is thrown when the service is already running. + public void Start() + { + Logger.Log($"\"{ServiceName}\" is starting."); + if (state == ServiceState.Running) throw new InvalidOperationException("Service instance already running."); + CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(TIMEOUT); + Task initializationTask = Task.Run(() => service.InitializeService(), cancellationTokenSource.Token); + try { + initializationTask.Wait(); + } catch (AggregateException a) { + a.Handle((e) => e is TaskCanceledException); + } + cancellationTokenSource.Dispose(); + } + + /// + /// Stops the service. + /// + /// Is thrown when the is not running. + public void Stop() + { + if (state != ServiceState.Running) throw new InvalidOperationException("Service instance not running."); + Logger.Log($"\"{ServiceName}\" is stopping."); + service.ElegantShutdown(); + } + + /// + /// 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) + { + Logger.Log($"\"{ServiceName}\" is executing command \"{command}\".", LogLevel.DEBUG); + if (state != ServiceState.Running) throw new InvalidOperationException("Service instance not running."); + service.ExecuteCommand(command); + } + + /// + /// Gets the possible 's names for this service. + /// + /// A returned where the string is the option's name. + public ISet GetConfigurableOptions() + { + return new HashSet(this.configurables.Keys); + } + + public bool SetConfigurableValue(string configurationName, string value) + { + if (!this.configurables.ContainsKey(configurationName)) throw new KeyNotFoundException($"Unable to find option with name \"{configurationName}\"."); + return this.configurables[configurationName].SetValue(value); + } + + public string GetConfigurableValue(string configurationName) + { + if (!this.configurables.ContainsKey(configurationName)) throw new KeyNotFoundException($"Unable to find option with name \"{configurationName}\"."); + return this.configurables[configurationName].GetValue(); + } + + public ServiceState GetServiceState() + { + return state; + } + + public string GetServiceName() + { + return serviceName; + } + + public string GetModuleName() + { + return moduleName; + } + + /// The name of assembly this module is contained in. + public string GetAssemblyName() + { + return assemblyName; + } + + public byte[] GetLogBuffer() { + if (state == ServiceState.Stopped) throw new InvalidOperationException("Cannot get log of service that is not running."); + return service.GetLogBuffer(); + } + + private void OnServiceStateChange(object sender, ServiceState state) + { + this.state = state; + Logger.Log($"The service \"{ServiceName}\" is changing states to {this.state}.", LogLevel.DEBUG); + ServiceStateChangeEvent?.Invoke(this, this.state); + } + + private void OnLogUpdate(object sender, string log) { + LogUpdateEvent?.Invoke(this, log); + } + } +} \ No newline at end of file diff --git a/src/GameServiceWarden.Core/Games/ServiceManager.cs b/src/GameServiceWarden.Core/Module/ServiceManager.cs similarity index 67% rename from src/GameServiceWarden.Core/Games/ServiceManager.cs rename to src/GameServiceWarden.Core/Module/ServiceManager.cs index 600bf31..8685a5e 100644 --- a/src/GameServiceWarden.Core/Games/ServiceManager.cs +++ b/src/GameServiceWarden.Core/Module/ServiceManager.cs @@ -9,22 +9,25 @@ using System.Linq; using GameServiceWarden.API.Games; using GameServiceWarden.Core.Persistence; using GameServiceWarden.API.Module; +using System.Text; -namespace GameServiceWarden.Core.Games +namespace GameServiceWarden.Core.Module { public class ServiceManager : IServiceManagerActionExecuter { private IServiceManagerMonitor managerMonitor; private readonly ConcurrentDictionary running; - private readonly IPersistent services; + private readonly IPersistent> services; + private readonly LRUCache descriptorCache; private readonly IReadOnlyPersistent> modules; - public ServiceManager(IServiceManagerMonitor actionMonitor, IPersistent services, IReadOnlyPersistent> modules) + public ServiceManager(IServiceManagerMonitor actionMonitor, IPersistent> services, IReadOnlyPersistent> modules) { this.services = services ?? throw new ArgumentNullException("services"); this.modules = modules ?? throw new ArgumentNullException("modules"); this.managerMonitor = actionMonitor ?? throw new ArgumentNullException("actionMonitor"); this.running = new ConcurrentDictionary(); + this.descriptorCache = new LRUCache(100); } public void CreateService(string serviceName, string assemblyName, string moduleName) @@ -32,8 +35,10 @@ namespace GameServiceWarden.Core.Games if (!this.modules.ContainsKey(assemblyName)) throw new KeyNotFoundException($"No file \"{assemblyName}\" found."); IReadOnlyDictionary assemblyModules = this.modules[assemblyName]; if (services.ContainsKey(serviceName)) throw new ArgumentException($"Service of Name \"{serviceName}\" already exists."); - - services.AddToPersistence(serviceName, new ServiceDescriptor(assemblyModules[moduleName].InstantiateService(services.GetPathForKey(serviceName), true), serviceName, moduleName, assemblyName)); + Dictionary data = new Dictionary(); + data[ServiceDescriptor.ASSEMBLY_PROPERTY] = assemblyName; + data[ServiceDescriptor.MODULE_PROPERTY] = moduleName; + services.AddToPersistence(serviceName, data); ServiceManagerState managerState = new ServiceManagerState(); managerState.delta = true; managerState.subtract = false; @@ -45,7 +50,7 @@ namespace GameServiceWarden.Core.Games public void DeleteService(string serviceName) { if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found."); - if (services[serviceName].GetServiceState()) services[serviceName].Stop(); + if (running.ContainsKey(serviceName)) running[serviceName].Stop(); services.Delete(serviceName); ServiceManagerState managerState = new ServiceManagerState(); managerState.delta = true; @@ -81,15 +86,18 @@ namespace GameServiceWarden.Core.Games private IEnumerable GetServiceOptions(string serviceName) { if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found."); - ServiceDescriptor serviceInfo = services[serviceName]; - return serviceInfo.GetConfigurableOptions(); + IReadOnlyDictionary info = services[serviceName]; + ServiceDescriptor service = descriptorCache.Use(serviceName, () => GenerateDescriptor(serviceName, info[ServiceDescriptor.ASSEMBLY_PROPERTY], info[ServiceDescriptor.MODULE_PROPERTY])); + return service.GetConfigurableOptions(); } private string GetServiceOptionValue(string serviceName, string optionName) { if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found."); - if (!services[serviceName].GetConfigurableOptions().Contains(optionName)) throw new KeyNotFoundException($"Option \"{optionName}\" for service \"{serviceName}\" not found."); - return services[serviceName].GetConfigurableValue(optionName); + IReadOnlyDictionary info = services[serviceName]; + ServiceDescriptor service = descriptorCache.Use(serviceName, () => GenerateDescriptor(serviceName, info[ServiceDescriptor.ASSEMBLY_PROPERTY], info[ServiceDescriptor.MODULE_PROPERTY])); + if (!service.GetConfigurableOptions().Contains(optionName)) throw new KeyNotFoundException($"Option \"{optionName}\" for service \"{serviceName}\" not found."); + return service.GetConfigurableValue(optionName); } public IReadOnlyDictionary> GetOptions() { @@ -112,9 +120,11 @@ namespace GameServiceWarden.Core.Games 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].GetConfigurableOptions().Contains(optionName)) throw new KeyNotFoundException($"Option \"{optionName}\" for service \"{serviceName}\" not found."); + IReadOnlyDictionary info = services[serviceName]; + ServiceDescriptor service = descriptorCache.Use(serviceName, () => GenerateDescriptor(serviceName, info[ServiceDescriptor.ASSEMBLY_PROPERTY], info[ServiceDescriptor.MODULE_PROPERTY])); + if (!service.GetConfigurableOptions().Contains(optionName)) throw new KeyNotFoundException($"Option \"{optionName}\" for service \"{serviceName}\" not found."); ServiceManagerState managerState = new ServiceManagerState(); - if (services[serviceName].SetConfigurableValue(optionName, value)) { + if (service.SetConfigurableValue(optionName, value)) { managerState.delta = true; Dictionary> changedOption = new Dictionary>(); Dictionary options = new Dictionary(); @@ -130,9 +140,11 @@ namespace GameServiceWarden.Core.Games { if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found."); if (running.ContainsKey(serviceName)) throw new InvalidOperationException($"Service under name \"{serviceName}\" is already running."); - ServiceDescriptor info = services[serviceName]; - info.ServiceStateChangeEvent += OnServiceStateChange; - info.Start(); + IReadOnlyDictionary info = services[serviceName]; + ServiceDescriptor service = descriptorCache.Use(serviceName, () => GenerateDescriptor(serviceName, info[ServiceDescriptor.ASSEMBLY_PROPERTY], info[ServiceDescriptor.MODULE_PROPERTY])); + service.ServiceStateChangeEvent += OnServiceStateChange; + service.LogUpdateEvent += OnLogUpdated; + service.Start(); } public void StopService(string serviceName) @@ -147,52 +159,55 @@ namespace GameServiceWarden.Core.Games running[serviceName].ExecuteCommand(command); } - private string GetServiceLogPipeName(string serviceName) - { - if (!running.ContainsKey(serviceName)) throw new InvalidOperationException($"Service under name \"{serviceName}\" is not running."); - return running[serviceName].ServiceLogPipeName; + private byte[] GetServiceLogBuffer(string serviceName) { + if (!running.ContainsKey(serviceName)) throw new InvalidOperationException($"Service under name \"{serviceName}\" is not running and therefore, does not have a log."); + IReadOnlyDictionary info = services[serviceName]; + ServiceDescriptor service = descriptorCache.Use(serviceName, () => GenerateDescriptor(serviceName, info[ServiceDescriptor.ASSEMBLY_PROPERTY], info[ServiceDescriptor.MODULE_PROPERTY])); + return service.GetLogBuffer(); } - public IReadOnlyDictionary GetLogPipeNames() { - ServiceManagerState managerState = new ServiceManagerState(); - Dictionary logPipeNames = new Dictionary(); - foreach (string service in GetRunningServiceNames()) + public Dictionary GetLogBuffer() { + Dictionary logs = new Dictionary(); + foreach (string service in running.Keys) { - logPipeNames.Add(service, GetServiceLogPipeName(service)); + logs.Add(service, running[service].GetLogBuffer()); } - managerState.logs = logPipeNames; + ServiceManagerState managerState = new ServiceManagerState(); + managerState.logs = logs; managerMonitor.Present(managerState); - return logPipeNames; + return logs; } - private void OnServiceStateChange(object sender, bool state) { + private ServiceDescriptor GenerateDescriptor(string name, string assembly, string module) { + return new ServiceDescriptor(modules[assembly][module].InstantiateService(services.GetPathForKey(name)), name, module, assembly); + } + + private void OnServiceStateChange(object sender, ServiceState state) { ServiceDescriptor serviceInfo = (ServiceDescriptor)sender; ServiceManagerState managerChange = new ServiceManagerState(); switch (state) { - case true: + case ServiceState.Running: if (running.TryAdd(serviceInfo.ServiceName, serviceInfo)) { managerChange.delta = true; managerChange.running = new List(); managerChange.running.Add(serviceInfo.ServiceName); - Dictionary logAdded = new Dictionary(); - logAdded.Add(serviceInfo.ServiceName, GetServiceLogPipeName(serviceInfo.ServiceName)); - managerChange.logs = logAdded; } break; - case false: + case ServiceState.Stopped: ServiceDescriptor removed; if (running.TryRemove(serviceInfo.ServiceName, out removed)) { removed.ServiceStateChangeEvent -= OnServiceStateChange; - services[serviceInfo.ServiceName] = removed; + removed.LogUpdateEvent -= OnLogUpdated; + Dictionary removedInfo = new Dictionary(); + removedInfo[ServiceDescriptor.ASSEMBLY_PROPERTY] = removed.GetAssemblyName(); + removedInfo[ServiceDescriptor.MODULE_PROPERTY] = removed.GetModuleName(); + services[serviceInfo.ServiceName] = removedInfo; managerChange.delta = true; managerChange.subtract = true; managerChange.running = new List(); managerChange.running.Add(serviceInfo.ServiceName); - Dictionary logRemoved = new Dictionary(); - logRemoved.Add(serviceInfo.ServiceName, null); - managerChange.logs = logRemoved; } break; } @@ -200,6 +215,15 @@ namespace GameServiceWarden.Core.Games managerMonitor.Present(managerChange); } + void OnLogUpdated(object sender, string update) { + ServiceDescriptor service = (ServiceDescriptor)sender; + ServiceManagerState state = new ServiceManagerState(); + state.delta = true; + Dictionary logUpdate = new Dictionary(); + logUpdate[service.ServiceName] = update; + managerMonitor.Present(state); + } + public void ExecuteAction(ServiceManagerAction action) { switch (action.action) @@ -208,7 +232,7 @@ namespace GameServiceWarden.Core.Games GetServiceNames(); GetRunningServiceNames(); GetModuleNames(); - GetLogPipeNames(); + GetLogBuffer(); GetOptions(); break; diff --git a/src/GameServiceWarden.Core/Persistence/IPersistent.cs b/src/GameServiceWarden.Core/Persistence/IPersistent.cs index be00f79..f235c5d 100644 --- a/src/GameServiceWarden.Core/Persistence/IPersistent.cs +++ b/src/GameServiceWarden.Core/Persistence/IPersistent.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using GameServiceWarden.Core.Games; +using GameServiceWarden.Core.Module; namespace GameServiceWarden.Core.Persistence { diff --git a/src/GameServiceWarden.Core/Persistence/ServiceDescriptorPersistence.cs b/src/GameServiceWarden.Core/Persistence/ServiceDescriptorPersistence.cs index 4c24768..d963af7 100644 --- a/src/GameServiceWarden.Core/Persistence/ServiceDescriptorPersistence.cs +++ b/src/GameServiceWarden.Core/Persistence/ServiceDescriptorPersistence.cs @@ -3,39 +3,36 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; -using GameServiceWarden.Core.Games; +using GameServiceWarden.Core.Module; using GameServiceWarden.API.Module; +using System.Collections.ObjectModel; namespace GameServiceWarden.Core.Persistence { - public class ServiceDescriptorPersistence : IPersistent + public class ServiceDescriptorPersistence : IPersistent> { - private readonly IReadOnlyPersistent> modules; private readonly string mapDirectory; - private const string ASSEMBLY_NAME = "Assembly Name"; - private const string MODULE_NAME = "Module Name"; private const string EXTENSION = ".sin"; - public ServiceDescriptorPersistence(string mapDirectory, IReadOnlyPersistent> modules) + public ServiceDescriptorPersistence(string mapDirectory) { this.mapDirectory = mapDirectory; - this.modules = modules; } - public ServiceDescriptor this[string key] + public IReadOnlyDictionary this[string key] { set { - SaveService(key, value.GetAssemblyName(), value.GetModuleName()); + SaveService(key, value[ServiceDescriptor.ASSEMBLY_PROPERTY], value[ServiceDescriptor.MODULE_PROPERTY]); } get { if (!ContainsKey(key)) throw new KeyNotFoundException(); - string assemblyName = GetServiceInfoValue(key, ASSEMBLY_NAME); - string moduleName = GetServiceInfoValue(key, MODULE_NAME); - IService service = modules[assemblyName][moduleName].InstantiateService(GetPathForKey(key), false); - return new ServiceDescriptor(service, key, moduleName, assemblyName); + Dictionary info = new Dictionary(); + info[ServiceDescriptor.ASSEMBLY_PROPERTY] = GetServiceInfoValue(key, ServiceDescriptor.ASSEMBLY_PROPERTY); + info[ServiceDescriptor.MODULE_PROPERTY] = GetServiceInfoValue(key, ServiceDescriptor.MODULE_PROPERTY); + return new ReadOnlyDictionary(info); } } @@ -51,10 +48,10 @@ namespace GameServiceWarden.Core.Persistence } } - public IEnumerable Values { + public IEnumerable> Values { get { IEnumerable keys = Keys; - List res = new List(); + List> res = new List>(); foreach (string key in keys) { res.Add(this[key]); @@ -75,7 +72,7 @@ namespace GameServiceWarden.Core.Persistence } } - public void AddToPersistence(string key, ServiceDescriptor value) + public void AddToPersistence(string key, IReadOnlyDictionary value) { if (key == null) throw new ArgumentNullException(); if (ContainsKey(key)) throw new ArgumentException(); @@ -96,13 +93,13 @@ namespace GameServiceWarden.Core.Persistence return Directory.Exists(GetPathForKey(key)); } - public IEnumerator> GetEnumerator() + public IEnumerator>> GetEnumerator() { IEnumerable keys = Keys; - List> result = new List>(); + List>> result = new List>>(); foreach (string key in keys) { - result.Add(new KeyValuePair(key, this[key])); + result.Add(new KeyValuePair>(key, this[key])); } return result.GetEnumerator(); } @@ -127,7 +124,7 @@ namespace GameServiceWarden.Core.Persistence return true; } - public bool TryLoadValue(string key, [MaybeNullWhen(false)] out ServiceDescriptor value) + public bool TryLoadValue(string key, [MaybeNullWhen(false)] out IReadOnlyDictionary value) { try { value = this[key]; @@ -152,8 +149,8 @@ namespace GameServiceWarden.Core.Persistence Directory.CreateDirectory(serviceInfoPath); using (StreamWriter writer = File.CreateText(Path.Combine(serviceInfoPath, key + EXTENSION))) { - writer.WriteLine($"{ASSEMBLY_NAME}: {assemblyName}"); - writer.WriteLine($"{MODULE_NAME}: {moduleName}"); + writer.WriteLine($"{ServiceDescriptor.ASSEMBLY_PROPERTY}: {assemblyName}"); + writer.WriteLine($"{ServiceDescriptor.MODULE_PROPERTY}: {moduleName}"); } } diff --git a/src/GameServiceWarden.Core/Persistence/ServiceModules.cs b/src/GameServiceWarden.Core/Persistence/ServiceModules.cs index 0ee9153..bf7c4f0 100644 --- a/src/GameServiceWarden.Core/Persistence/ServiceModules.cs +++ b/src/GameServiceWarden.Core/Persistence/ServiceModules.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.IO; -using GameServiceWarden.Core.Games.Modules; +using GameServiceWarden.Core.Module; using GameServiceWarden.API.Module; namespace GameServiceWarden.Core.Persistence @@ -12,7 +12,7 @@ namespace GameServiceWarden.Core.Persistence { private readonly string mapDirectory; - private readonly ServiceModuleLoader loader = new ServiceModuleLoader(); + private readonly ModuleLoader loader = new ModuleLoader(); public ServiceModules(string mapDirectory) { diff --git a/src/GameServiceWarden.Core/UI/IPCController.cs b/src/GameServiceWarden.Core/UI/IPCController.cs index b8fdba6..374f1cd 100644 --- a/src/GameServiceWarden.Core/UI/IPCController.cs +++ b/src/GameServiceWarden.Core/UI/IPCController.cs @@ -2,7 +2,7 @@ using System.Diagnostics; using System.Text.Json; using GameServiceWarden.API.Communicable; using GameServiceWarden.API.Communicable.Requests; -using GameServiceWarden.Core.Games; +using GameServiceWarden.Core.Module; using GameServiceWarden.Core.Logging; namespace GameServiceWarden.Core.UI diff --git a/src/GameServiceWarden.Core/UI/IPCPresenter.cs b/src/GameServiceWarden.Core/UI/IPCPresenter.cs index 0ceef56..adccc3b 100644 --- a/src/GameServiceWarden.Core/UI/IPCPresenter.cs +++ b/src/GameServiceWarden.Core/UI/IPCPresenter.cs @@ -2,7 +2,7 @@ using System.Text.Json; using System.Threading.Tasks; using GameServiceWarden.API.Communicable; using GameServiceWarden.API.Games; -using GameServiceWarden.Core.Games; +using GameServiceWarden.Core.Module; namespace GameServiceWarden.Core.UI { diff --git a/tests/GameServiceWarden.Core.Tests/Modules/Games/FakePersistence.cs b/tests/GameServiceWarden.Core.Tests/Modules/Games/FakePersistence.cs index e1a41fb..004af4b 100644 --- a/tests/GameServiceWarden.Core.Tests/Modules/Games/FakePersistence.cs +++ b/tests/GameServiceWarden.Core.Tests/Modules/Games/FakePersistence.cs @@ -2,7 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; -using GameServiceWarden.Core.Games; +using GameServiceWarden.Core.Module; using GameServiceWarden.Core.Persistence; namespace GameServiceWarden.Core.Tests.Modules diff --git a/tests/GameServiceWarden.Core.Tests/Modules/Games/FakeService.cs b/tests/GameServiceWarden.Core.Tests/Modules/Games/FakeService.cs index 3def652..9c2a169 100644 --- a/tests/GameServiceWarden.Core.Tests/Modules/Games/FakeService.cs +++ b/tests/GameServiceWarden.Core.Tests/Modules/Games/FakeService.cs @@ -10,9 +10,11 @@ namespace GameServiceWarden.Core.Tests.Modules.Games { public IReadOnlyCollection Configurables { get; set; } - public event EventHandler StateChangeEvent; - public ServiceState CurrentState { get; private set; } = ServiceState.Stopped; + public event EventHandler StateChangeEvent; + public event EventHandler UpdateLogEvent; + public ServiceState CurrentState { get; private set; } = ServiceState.Stopped; + private MemoryStream memoryStream; private StreamWriter consoleWriter; private Stack taskStack = new Stack(); @@ -29,7 +31,7 @@ namespace GameServiceWarden.Core.Tests.Modules.Games public void ElegantShutdown() { CurrentState = ServiceState.Stopped; - StateChangeEvent?.Invoke(this, false); + StateChangeEvent?.Invoke(this, ServiceState.Stopped); Task task; while(taskStack.TryPop(out task)) { if (task.IsCompleted) { @@ -44,13 +46,20 @@ namespace GameServiceWarden.Core.Tests.Modules.Games { taskStack.Push(consoleWriter.WriteLineAsync(command)); taskStack.Push(consoleWriter.FlushAsync()); + UpdateLogEvent?.Invoke(this, command); } - public void InitializeService(Stream stream) + public void InitializeService() { CurrentState = ServiceState.Running; - this.consoleWriter = new StreamWriter(stream); - StateChangeEvent?.Invoke(this, true); + memoryStream = new MemoryStream(); + this.consoleWriter = new StreamWriter(memoryStream); + StateChangeEvent?.Invoke(this, ServiceState.Running); + } + + public byte[] GetLogBuffer() + { + return memoryStream.ToArray(); } } } \ No newline at end of file diff --git a/tests/GameServiceWarden.Core.Tests/Modules/Games/FakeServiceManagerMonitor.cs b/tests/GameServiceWarden.Core.Tests/Modules/Games/FakeServiceManagerMonitor.cs index c5b45c1..35faa5c 100644 --- a/tests/GameServiceWarden.Core.Tests/Modules/Games/FakeServiceManagerMonitor.cs +++ b/tests/GameServiceWarden.Core.Tests/Modules/Games/FakeServiceManagerMonitor.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using GameServiceWarden.API.Games; -using GameServiceWarden.Core.Games; +using GameServiceWarden.Core.Module; namespace GameServiceWarden.Core.Tests.Modules.Games { diff --git a/tests/GameServiceWarden.Core.Tests/Modules/Games/FakeServiceModule.cs b/tests/GameServiceWarden.Core.Tests/Modules/Games/FakeServiceModule.cs index 74c66a8..2e0da7f 100644 --- a/tests/GameServiceWarden.Core.Tests/Modules/Games/FakeServiceModule.cs +++ b/tests/GameServiceWarden.Core.Tests/Modules/Games/FakeServiceModule.cs @@ -17,7 +17,7 @@ namespace GameServiceWarden.Core.Tests.Modules.Games public IEnumerable Authors { get; private set; } = new string[] { "FakeAuthor", "FakeAuthor2" }; - public IService InstantiateService(string workspace, bool clean) + public IService InstantiateService(string workspace) { return new FakeService(configurables); } diff --git a/tests/GameServiceWarden.Core.Tests/Modules/Games/ServiceDescriptorTest.cs b/tests/GameServiceWarden.Core.Tests/Modules/Games/ServiceDescriptorTest.cs index bba0d43..5dd11c1 100644 --- a/tests/GameServiceWarden.Core.Tests/Modules/Games/ServiceDescriptorTest.cs +++ b/tests/GameServiceWarden.Core.Tests/Modules/Games/ServiceDescriptorTest.cs @@ -1,10 +1,11 @@ using System.Collections.Generic; using System.IO; -using GameServiceWarden.Core.Games; +using GameServiceWarden.Core.Module; using GameServiceWarden.Core.Logging; using GameServiceWarden.API.Module; using Xunit; using Xunit.Abstractions; +using System.Text; namespace GameServiceWarden.Core.Tests.Modules.Games { @@ -34,7 +35,7 @@ namespace GameServiceWarden.Core.Tests.Modules.Games IService stubService = new FakeService(); ServiceDescriptor serviceInfo = new ServiceDescriptor(stubService, SERVICE_NAME, "FakeModule", "FakeAssembly"); serviceInfo.Start(); - Assert.True(serviceInfo.GetServiceState()); + Assert.Equal(ServiceState.Running, serviceInfo.GetServiceState()); serviceInfo.Stop(); } @@ -46,7 +47,7 @@ namespace GameServiceWarden.Core.Tests.Modules.Games ServiceDescriptor serviceInfo = new ServiceDescriptor(stubService, SERVICE_NAME, "FakeModule", "FakeAssembly"); serviceInfo.Start(); serviceInfo.Stop(); - Assert.False(serviceInfo.GetServiceState()); + Assert.Equal(ServiceState.Stopped, serviceInfo.GetServiceState()); } [Fact] @@ -89,7 +90,7 @@ namespace GameServiceWarden.Core.Tests.Modules.Games IService stubService = new FakeService(); ServiceDescriptor serviceInfo = new ServiceDescriptor(stubService, SERVICE_NAME, "FakeModule", "FakeAssembly"); //Then - Assert.False(serviceInfo.GetServiceState()); + Assert.Equal(ServiceState.Stopped, serviceInfo.GetServiceState()); } [Fact] @@ -102,7 +103,7 @@ namespace GameServiceWarden.Core.Tests.Modules.Games //When serviceInfo.Start(); //Then - Assert.True(serviceInfo.GetServiceState()); + Assert.Equal(ServiceState.Running, serviceInfo.GetServiceState()); serviceInfo.Stop(); } @@ -143,28 +144,24 @@ namespace GameServiceWarden.Core.Tests.Modules.Games } [Fact] - public void ServiceLogPipeName_ServiceNotStarted_PipeNameReturned() + public void GetLogBuffer_CommandWritten_CommandLogged() { //Given - const string SERVICE_NAME = "ServiceLogPipeName_ServiceNotStarted_PipeNameReturned"; + const string CMD = "hello"; + const string SERVICE_NAME = "GetLogBuffer_CommandWritten_CommandLogged"; IService stubService = new FakeService(); ServiceDescriptor serviceInfo = new ServiceDescriptor(stubService, SERVICE_NAME, "FakeModule", "FakeAssembly"); - //Then - Assert.NotNull(serviceInfo.ServiceLogPipeName); - } - - [Fact] - public void ServiceLogPipeName_ServiceStarted_StreamReturned() - { - //Given - const string SERVICE_NAME = "ServiceLogPipeName_ServiceStarted_StreamReturned"; - IService stubService = new FakeService(); - ServiceDescriptor serviceInfo = new ServiceDescriptor(stubService, SERVICE_NAME, "FakeModule", "FakeAssembly"); - //When serviceInfo.Start(); + //When + serviceInfo.ExecuteCommand(CMD); //Then - Assert.NotNull(serviceInfo.ServiceLogPipeName); - serviceInfo.Stop(); + using (MemoryStream mem = new MemoryStream(serviceInfo.GetLogBuffer())) + { + using (StreamReader reader = new StreamReader(mem)) + { + Assert.Equal(CMD, reader.ReadLine()); + } + } } } } \ No newline at end of file diff --git a/tests/GameServiceWarden.Core.Tests/Modules/Games/ServiceManagerTest.cs b/tests/GameServiceWarden.Core.Tests/Modules/Games/ServiceManagerTest.cs index ec97782..a553bfc 100644 --- a/tests/GameServiceWarden.Core.Tests/Modules/Games/ServiceManagerTest.cs +++ b/tests/GameServiceWarden.Core.Tests/Modules/Games/ServiceManagerTest.cs @@ -4,11 +4,12 @@ using System.IO; using System.IO.Pipes; using System.Threading; using System.Threading.Tasks; -using GameServiceWarden.Core.Games; +using GameServiceWarden.Core.Module; using GameServiceWarden.Core.Logging; using GameServiceWarden.API.Module; using Xunit; using Xunit.Abstractions; +using System.Text; [assembly: CollectionBehavior(DisableTestParallelization = true)] namespace GameServiceWarden.Core.Tests.Modules.Games @@ -28,7 +29,7 @@ namespace GameServiceWarden.Core.Tests.Modules.Games const string ASSEMBLY_NAME = "FakeAssembly"; const string FAKE_SERVICE_NAME = "CreateService_NewManager_NewServiceCreated"; FakePersistence> stubPersistentModuleDictionary = new FakePersistence>(); - FakePersistence stubPersistentServiceDictionary = new FakePersistence(); + FakePersistence> stubPersistentServiceDictionary = new FakePersistence>(); FakeServiceManagerMonitor stubMonitor = new FakeServiceManagerMonitor(); ServiceManager serviceManager = new ServiceManager(stubMonitor, stubPersistentServiceDictionary, stubPersistentModuleDictionary); Dictionary stubAssemblyModulesDictionary = new Dictionary(); @@ -48,7 +49,7 @@ namespace GameServiceWarden.Core.Tests.Modules.Games const string ASSEMBLY_NAME = "FakeAssembly"; const string FAKE_SERVICE_NAME = "CreateService_OneService_ServiceDeleted"; FakePersistence> stubPersistentModuleDictionary = new FakePersistence>(); - FakePersistence stubPersistentServiceDictionary = new FakePersistence(); + FakePersistence> stubPersistentServiceDictionary = new FakePersistence>(); FakeServiceManagerMonitor stubMonitor = new FakeServiceManagerMonitor(); ServiceManager serviceManager = new ServiceManager(stubMonitor, stubPersistentServiceDictionary, stubPersistentModuleDictionary); Dictionary stubAssemblyModulesDictionary = new Dictionary(); @@ -71,7 +72,7 @@ namespace GameServiceWarden.Core.Tests.Modules.Games const string ASSEMBLY_NAME = "FakeAssembly"; const string FAKE_SERVICE_PREFIX = "GetServiceNames_MultipleServices_AllCorrectNames_"; FakePersistence> stubPersistentModuleDictionary = new FakePersistence>(); - FakePersistence stubPersistentServiceDictionary = new FakePersistence(); + FakePersistence> stubPersistentServiceDictionary = new FakePersistence>(); FakeServiceManagerMonitor stubMonitor = new FakeServiceManagerMonitor(); ServiceManager serviceManager = new ServiceManager(stubMonitor, stubPersistentServiceDictionary, stubPersistentModuleDictionary); Dictionary stubAssemblyModulesDictionary = new Dictionary(); @@ -97,7 +98,7 @@ namespace GameServiceWarden.Core.Tests.Modules.Games const string ASSEMBLY_NAME = "FakeAssembly"; const string FAKE_SERVICE_NAME = "GetServiceOptions_ThreeOptionService_CorrectOptions"; FakePersistence> stubPersistentModuleDictionary = new FakePersistence>(); - FakePersistence stubPersistentServiceDictionary = new FakePersistence(); + FakePersistence> stubPersistentServiceDictionary = new FakePersistence>(); FakeServiceManagerMonitor stubMonitor = new FakeServiceManagerMonitor(); ServiceManager serviceManager = new ServiceManager(stubMonitor, stubPersistentServiceDictionary, stubPersistentModuleDictionary); Dictionary stubAssemblyModulesDictionary = new Dictionary(); @@ -122,7 +123,7 @@ namespace GameServiceWarden.Core.Tests.Modules.Games const string ASSEMBLY_NAME = "FakeAssembly"; const string FAKE_SERVICE_NAME = "SetandGetServiceOptionValue_OneOption_OptionChanged"; FakePersistence> stubPersistentModuleDictionary = new FakePersistence>(); - FakePersistence stubPersistentServiceDictionary = new FakePersistence(); + FakePersistence> stubPersistentServiceDictionary = new FakePersistence>(); FakeServiceManagerMonitor stubMonitor = new FakeServiceManagerMonitor(); ServiceManager serviceManager = new ServiceManager(stubMonitor, stubPersistentServiceDictionary, stubPersistentModuleDictionary); Dictionary stubAssemblyModulesDictionary = new Dictionary(); @@ -145,7 +146,7 @@ namespace GameServiceWarden.Core.Tests.Modules.Games const string ASSEMBLY_NAME = "FakeAssembly"; const string FAKE_SERVICE_NAME = "GetServiceState_NotRunning_ReturnsNotRunningState"; FakePersistence> stubPersistentModuleDictionary = new FakePersistence>(); - FakePersistence stubPersistentServiceDictionary = new FakePersistence(); + FakePersistence> stubPersistentServiceDictionary = new FakePersistence>(); FakeServiceManagerMonitor stubMonitor = new FakeServiceManagerMonitor(); ServiceManager serviceManager = new ServiceManager(stubMonitor, stubPersistentServiceDictionary, stubPersistentModuleDictionary); Dictionary stubAssemblyModulesDictionary = new Dictionary(); @@ -165,7 +166,7 @@ namespace GameServiceWarden.Core.Tests.Modules.Games const string ASSEMBLY_NAME = "FakeAssembly"; const string FAKE_SERVICE_NAME = "StartService_NotStarted_SuccessfulStart"; FakePersistence> stubPersistentModuleDictionary = new FakePersistence>(); - FakePersistence stubPersistentServiceDictionary = new FakePersistence(); + FakePersistence> stubPersistentServiceDictionary = new FakePersistence>(); FakeServiceManagerMonitor stubMonitor = new FakeServiceManagerMonitor(); ServiceManager serviceManager = new ServiceManager(stubMonitor, stubPersistentServiceDictionary, stubPersistentModuleDictionary); Dictionary stubAssemblyModulesDictionary = new Dictionary(); @@ -187,7 +188,7 @@ namespace GameServiceWarden.Core.Tests.Modules.Games const string ASSEMBLY_NAME = "FakeAssembly"; const string FAKE_SERVICE_NAME = "StopService_ServiceStartedThenStopped_StateUpdated"; FakePersistence> stubPersistentModuleDictionary = new FakePersistence>(); - FakePersistence stubPersistentServiceDictionary = new FakePersistence(); + FakePersistence> stubPersistentServiceDictionary = new FakePersistence>(); FakeServiceManagerMonitor stubMonitor = new FakeServiceManagerMonitor(); ServiceManager serviceManager = new ServiceManager(stubMonitor, stubPersistentServiceDictionary, stubPersistentModuleDictionary); Dictionary stubAssemblyModulesDictionary = new Dictionary(); @@ -203,14 +204,14 @@ namespace GameServiceWarden.Core.Tests.Modules.Games } [Fact] - public void ExecuteCommand_CommandExecutedBeforeConnected_CommandLogged() + public void ExecuteCommand_ServiceStarted_CommandLogged() { //Given const string ASSEMBLY_NAME = "FakeAssembly"; - const string FAKE_SERVICE_NAME = "ExecuteCommand_CommandExecutedBeforeConnected_CommandLogged"; + const string FAKE_SERVICE_NAME = "ExecuteCommand_ServiceStarted_CommandLogged"; const string COMMAND = "TEST"; FakePersistence> stubPersistentModuleDictionary = new FakePersistence>(); - FakePersistence stubPersistentServiceDictionary = new FakePersistence(); + FakePersistence> stubPersistentServiceDictionary = new FakePersistence>(); FakeServiceManagerMonitor stubMonitor = new FakeServiceManagerMonitor(); ServiceManager serviceManager = new ServiceManager(stubMonitor, stubPersistentServiceDictionary, stubPersistentModuleDictionary); Dictionary stubAssemblyModulesDictionary = new Dictionary(); @@ -220,116 +221,16 @@ namespace GameServiceWarden.Core.Tests.Modules.Games //When serviceManager.CreateService(FAKE_SERVICE_NAME, ASSEMBLY_NAME, stubServiceModule.Name); serviceManager.StartService(FAKE_SERVICE_NAME); - string pipeName = serviceManager.GetLogPipeNames()[FAKE_SERVICE_NAME]; - NamedPipeClientStream clientStream = new NamedPipeClientStream(".", pipeName, PipeDirection.In); - serviceManager.ExecuteCommand(FAKE_SERVICE_NAME, COMMAND); - clientStream.Connect(1000); - Thread.Sleep(1000); - //Then - byte[] buffer = new byte[1024 * 8]; - CancellationTokenSource cancelToken = new CancellationTokenSource(2000); - ValueTask task = clientStream.ReadAsync(buffer, cancelToken.Token); - Assert.False(task.AsTask().Wait(1000)); - serviceManager.StopService(FAKE_SERVICE_NAME); - cancelToken.Dispose(); - } - - [Fact] - public void ExecuteCommand_CommandExecutedAfterConnected_CommandLogged() - { - //Given - const string ASSEMBLY_NAME = "FakeAssembly"; - const string FAKE_SERVICE_NAME = "ExecuteCommand_CommandExecutedAfterConnected_CommandLogged"; - const string COMMAND = "TEST"; - FakePersistence> stubPersistentModuleDictionary = new FakePersistence>(); - FakePersistence stubPersistentServiceDictionary = new FakePersistence(); - FakeServiceManagerMonitor stubMonitor = new FakeServiceManagerMonitor(); - ServiceManager serviceManager = new ServiceManager(stubMonitor, stubPersistentServiceDictionary, stubPersistentModuleDictionary); - Dictionary stubAssemblyModulesDictionary = new Dictionary(); - IServiceModule stubServiceModule = new FakeServiceModule(); - stubAssemblyModulesDictionary.Add(stubServiceModule.Name, stubServiceModule); - stubPersistentModuleDictionary.AddToPersistence(ASSEMBLY_NAME, stubAssemblyModulesDictionary); - //When - serviceManager.CreateService(FAKE_SERVICE_NAME, ASSEMBLY_NAME, stubServiceModule.Name); - serviceManager.StartService(FAKE_SERVICE_NAME); - string pipeName = serviceManager.GetLogPipeNames()[FAKE_SERVICE_NAME]; - NamedPipeClientStream clientStream = new NamedPipeClientStream(".", pipeName, PipeDirection.In); - clientStream.Connect(1000); - Thread.Sleep(1000); serviceManager.ExecuteCommand(FAKE_SERVICE_NAME, COMMAND); //Then - using (StreamReader reader = new StreamReader(clientStream)) + using (MemoryStream mem = new MemoryStream(serviceManager.GetLogBuffer()[FAKE_SERVICE_NAME])) { - CancellationTokenSource cancelToken = new CancellationTokenSource(2000); - string message = null; - Task task = Task.Run(() => message = reader.ReadLine(), cancelToken.Token); - Assert.True(task.Wait(1000)); - Assert.True(COMMAND.Equals(message), $"Received message \"{message}\" when expecting \"{COMMAND}\""); - cancelToken.Dispose(); - } - serviceManager.StopService(FAKE_SERVICE_NAME); - } - -[Fact] - public void ExecuteCommand_CommandExecutedAfterMultipleLogListenersConnected_CommandLogged() - { - //Given - const string ASSEMBLY_NAME = "FakeAssembly"; - const string FAKE_SERVICE_NAME = "ExecuteCommand_CommandExecutedAfterMultipleLogListenersConnected_CommandLogged"; - const string COMMAND = "TEST"; - FakePersistence> stubPersistentModuleDictionary = new FakePersistence>(); - FakePersistence stubPersistentServiceDictionary = new FakePersistence(); - FakeServiceManagerMonitor stubMonitor = new FakeServiceManagerMonitor(); - ServiceManager serviceManager = new ServiceManager(stubMonitor, stubPersistentServiceDictionary, stubPersistentModuleDictionary); - Dictionary stubAssemblyModulesDictionary = new Dictionary(); - IServiceModule stubServiceModule = new FakeServiceModule(); - stubAssemblyModulesDictionary.Add(stubServiceModule.Name, stubServiceModule); - stubPersistentModuleDictionary.AddToPersistence(ASSEMBLY_NAME, stubAssemblyModulesDictionary); - //When - serviceManager.CreateService(FAKE_SERVICE_NAME, ASSEMBLY_NAME, stubServiceModule.Name); - serviceManager.StartService(FAKE_SERVICE_NAME); - string pipeName = serviceManager.GetLogPipeNames()[FAKE_SERVICE_NAME]; - NamedPipeClientStream[] clientStreams = new NamedPipeClientStream[5]; - for (int i = 0; i < clientStreams.Length; i++) - { - clientStreams[i] = new NamedPipeClientStream(".", pipeName, PipeDirection.In); - clientStreams[i].Connect(1000); - } - Thread.Sleep(1000); - serviceManager.ExecuteCommand(FAKE_SERVICE_NAME, COMMAND); - //Then - for (int i = 0; i < clientStreams.Length; i++) - { - using (StreamReader reader = new StreamReader(clientStreams[i])) + using (StreamReader reader = new StreamReader(mem)) { - string message = null; - Task clientTask = Task.Run(() => message = reader.ReadLine()); - Assert.True(clientTask.Wait(1000)); - Assert.True(COMMAND.Equals(message), $"Received message \"{message}\" when expecting \"{COMMAND}\""); + Assert.Equal(COMMAND, reader.ReadLine()); } } - Task task = Task.Run(() => serviceManager.StopService(FAKE_SERVICE_NAME)); - Assert.True(task.Wait(5000)); //TODO FIX WHY THIS IS HAPPENING!!!!! - } - - [Fact] - public void GetServiceConsoleStream_ServiceStopped_ExceptionThrown() - { - //Given - const string ASSEMBLY_NAME = "FakeAssembly"; - const string FAKE_SERVICE_NAME = "GetServiceConsoleStream_ServiceStopped_ExceptionThrown"; - FakePersistence> stubPersistentModuleDictionary = new FakePersistence>(); - FakePersistence stubPersistentServiceDictionary = new FakePersistence(); - FakeServiceManagerMonitor stubMonitor = new FakeServiceManagerMonitor(); - ServiceManager serviceManager = new ServiceManager(stubMonitor, stubPersistentServiceDictionary, stubPersistentModuleDictionary); - Dictionary stubAssemblyModulesDictionary = new Dictionary(); - IServiceModule stubServiceModule = new FakeServiceModule(); - stubAssemblyModulesDictionary.Add(stubServiceModule.Name, stubServiceModule); - stubPersistentModuleDictionary.AddToPersistence(ASSEMBLY_NAME, stubAssemblyModulesDictionary); - //When - serviceManager.CreateService(FAKE_SERVICE_NAME, ASSEMBLY_NAME, stubServiceModule.Name); - //Then - Assert.Throws(() => serviceManager.GetLogPipeNames()[FAKE_SERVICE_NAME]); + serviceManager.StopService(FAKE_SERVICE_NAME); } } } \ No newline at end of file diff --git a/tests/GameServiceWarden.Core.Tests/Persistence/ServiceDescriptorPersistenceTest.cs b/tests/GameServiceWarden.Core.Tests/Persistence/ServiceDescriptorPersistenceTest.cs index ff90df5..eb5630e 100644 --- a/tests/GameServiceWarden.Core.Tests/Persistence/ServiceDescriptorPersistenceTest.cs +++ b/tests/GameServiceWarden.Core.Tests/Persistence/ServiceDescriptorPersistenceTest.cs @@ -1,7 +1,7 @@ using System.Collections; using System.Collections.Generic; using System.IO; -using GameServiceWarden.Core.Games; +using GameServiceWarden.Core.Module; using GameServiceWarden.Core.Persistence; using GameServiceWarden.Core.Tests.Modules; using GameServiceWarden.Core.Tests.Modules.Games; @@ -28,7 +28,7 @@ namespace GameServiceWarden.Core.Tests.Persistence stubAssemblyDict[MODULE_NAME] = stubServiceModule; stubModulesPersistence[ASSEMBLY_NAME] = stubAssemblyDict; - ServiceDescriptorPersistence persistedServices = new ServiceDescriptorPersistence(TEST_DIR, stubModulesPersistence); + ServiceDescriptorPersistence persistedServices = new ServiceDescriptorPersistence(TEST_DIR); //Then Assert.True(persistedServices.GetPathForKey(SERVICE_NAME).Equals(Path.Combine(TEST_DIR, SERVICE_NAME))); @@ -49,11 +49,14 @@ namespace GameServiceWarden.Core.Tests.Persistence stubAssemblyDict[MODULE_NAME] = stubServiceModule; stubModulesPersistence[ASSEMBLY_NAME] = stubAssemblyDict; - ServiceDescriptorPersistence persistedServiceInfos = new ServiceDescriptorPersistence(TEST_DIR, stubModulesPersistence); + ServiceDescriptorPersistence persistedServiceInfos = new ServiceDescriptorPersistence(TEST_DIR); - ServiceDescriptor stubServiceInfo = new ServiceDescriptor(stubModulesPersistence[ASSEMBLY_NAME][MODULE_NAME].InstantiateService(persistedServiceInfos.GetPathForKey(SERVICE_NAME), true), SERVICE_NAME, MODULE_NAME, ASSEMBLY_NAME); + ServiceDescriptor stubServiceInfo = new ServiceDescriptor(stubModulesPersistence[ASSEMBLY_NAME][MODULE_NAME].InstantiateService(persistedServiceInfos.GetPathForKey(SERVICE_NAME)), SERVICE_NAME, MODULE_NAME, ASSEMBLY_NAME); //When - persistedServiceInfos[SERVICE_NAME] = stubServiceInfo; + Dictionary info = new Dictionary(); + info[ServiceDescriptor.MODULE_PROPERTY] = MODULE_NAME; + info[ServiceDescriptor.ASSEMBLY_PROPERTY] = ASSEMBLY_NAME; + persistedServiceInfos[SERVICE_NAME] = info; //Then Assert.True(Directory.Exists(TEST_DIR)); Assert.True(Directory.Exists(persistedServiceInfos.GetPathForKey(SERVICE_NAME))); @@ -79,15 +82,19 @@ namespace GameServiceWarden.Core.Tests.Persistence stubAssemblyDict[MODULE_NAME] = stubServiceModule; stubModulesPersistence[ASSEMBLY_NAME] = stubAssemblyDict; - ServiceDescriptorPersistence persistedServices = new ServiceDescriptorPersistence(TEST_DIR, stubModulesPersistence); + ServiceDescriptorPersistence persistedServices = new ServiceDescriptorPersistence(TEST_DIR); - ServiceDescriptor stubServiceInfo = new ServiceDescriptor(stubModulesPersistence[ASSEMBLY_NAME][MODULE_NAME].InstantiateService(persistedServices.GetPathForKey(SERVICE_NAME), true), SERVICE_NAME, MODULE_NAME, ASSEMBLY_NAME); - persistedServices[SERVICE_NAME] = stubServiceInfo; + Dictionary info = new Dictionary(); + info[ServiceDescriptor.MODULE_PROPERTY] = MODULE_NAME; + info[ServiceDescriptor.ASSEMBLY_PROPERTY] = ASSEMBLY_NAME; + persistedServices[SERVICE_NAME] = info; //When - ServiceDescriptor loadedService = persistedServices[SERVICE_NAME]; + IReadOnlyDictionary loadedServiceInfo = persistedServices[SERVICE_NAME]; + string loadedAssemblyName = loadedServiceInfo[ServiceDescriptor.ASSEMBLY_PROPERTY]; + string loadedModuleName = loadedServiceInfo[ServiceDescriptor.MODULE_PROPERTY]; //Then - Assert.True(loadedService.GetModuleName().Equals(MODULE_NAME)); - Assert.True(loadedService.GetAssemblyName().Equals(ASSEMBLY_NAME)); + Assert.True(loadedModuleName.Equals(MODULE_NAME)); + Assert.True(loadedAssemblyName.Equals(ASSEMBLY_NAME)); Directory.Delete(TEST_DIR, true); }