Changed logging system to be event based.

Implemented LRUCache for instantiating services without starting.

Service state changed back to enumerator.

Namespace refactoring.
This commit is contained in:
2021-04-19 01:34:45 -05:00
parent cac5ca054c
commit 35a2765559
26 changed files with 404 additions and 498 deletions

View File

@@ -0,0 +1,16 @@
using System;
using System.Runtime.Serialization;
namespace GameServiceWarden.Core.Module.Exceptions
{
[System.Serializable]
public class ModuleLoadException : Exception
{
public ModuleLoadException() { }
public ModuleLoadException(string message) : base(message) { }
public ModuleLoadException(string message, Exception inner) : base(message, inner) { }
protected ModuleLoadException(
SerializationInfo info,
StreamingContext context) : base(info, context) { }
}
}

View File

@@ -0,0 +1,13 @@
namespace GameServiceWarden.Core.Module.Exceptions
{
[System.Serializable]
public class ServiceInitializationException : System.Exception
{
public ServiceInitializationException() { }
public ServiceInitializationException(string message) : base(message) { }
public ServiceInitializationException(string message, System.Exception inner) : base(message, inner) { }
protected ServiceInitializationException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
}
}

View File

@@ -0,0 +1,9 @@
using GameServiceWarden.API.Games;
namespace GameServiceWarden.Core.Module
{
public interface IServiceManagerActionExecuter
{
void ExecuteAction(ServiceManagerAction action);
}
}

View File

@@ -0,0 +1,9 @@
using GameServiceWarden.API.Games;
namespace GameServiceWarden.Core.Module
{
public interface IServiceManagerMonitor
{
void Present(ServiceManagerState state);
}
}

View File

@@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
namespace GameServiceWarden.Core.Module
{
public class LRUCache<K, V>
{
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<K, Node> valueDictionary;
private Action<V> cleanupAction;
public LRUCache(int size = 100, Action<V> cleanup = null)
{
this.size = size;
valueDictionary = new Dictionary<K, Node>(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<V> 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();
}
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Reflection;
using System.Runtime.Loader;
namespace GameServiceWarden.Core.Module
{
class ModuleLoadContext : AssemblyLoadContext
{
private readonly 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;
}
}
}

View File

@@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using GameServiceWarden.Core.Module.Exceptions;
using GameServiceWarden.API.Module;
namespace GameServiceWarden.Core.Module
{
public class ModuleLoader //Gateway
{
/// <summary>
/// Loads an extension module.
/// </summary>
/// <param name="path">The path to the module.</param>
/// <returns>An </<see cref="IEnumerable{IServiceModule}"/> from the given module.</returns>
/// <exception cref="NoServiceableFoundException">When the module requested to be loaded does not contain any public <see cref="IService"/> classes.</exception>
public IEnumerable<IServiceModule> LoadModules(string path)
{
return instantiateServiceable(loadAssembly(path));
}
/// <summary>
/// Loads all module for each given path to modules file.
/// </summary>
/// <param name="paths">The paths to load modules for.</param>
/// <returns>A <see cref="Dictionary{string, IEnumerable{IServiceModule}}"/> where the key is a <see cref="string"/> that is the associated path.</returns>
public Dictionary<string, IEnumerable<IServiceModule>> LoadAllModules(IEnumerable<string> paths)
{
Dictionary<string, IEnumerable<IServiceModule>> res = new Dictionary<string, IEnumerable<IServiceModule>>();
foreach (string path in paths)
{
res.Add(path, LoadModules(path));
}
return res;
}
private Assembly loadAssembly(string path)
{
ModuleLoadContext moduleLoadContext = new ModuleLoadContext(path);
return moduleLoadContext.LoadFromAssemblyPath(path);
}
private IEnumerable<IServiceModule> instantiateServiceable(Assembly assembly)
{
int serviceableCount = 0;
foreach (Type type in assembly.GetExportedTypes())
{
if (typeof(IServiceModule).IsAssignableFrom(type))
{
IServiceModule res = Activator.CreateInstance(type) as IServiceModule;
if (res != null)
{
serviceableCount++;
yield return res;
}
}
}
if (serviceableCount == 0)
{
List<string> typeNames = new List<string>();
foreach (Type type in assembly.GetExportedTypes())
{
typeNames.Add(type.FullName);
}
string types = String.Join(',', typeNames);
throw new ModuleLoadException(
$"No public classes in {assembly} from {assembly.Location} implemented {typeof(IService).FullName}." +
$"Detected types: {types}");
}
}
}
}

View File

@@ -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;
/// <summary>
/// The name of the service itself, independent of the name of the module this service is using.
/// </summary>
public string ServiceName { get { return serviceName; } }
private readonly string serviceName;
private volatile ServiceState state;
private readonly IService service;
private string moduleName;
private readonly string assemblyName;
/// <summary>
/// Name of module this service uses.
/// </summary>
private readonly IReadOnlyDictionary<string, IServiceConfigurable> configurables;
public event EventHandler<ServiceState> ServiceStateChangeEvent;
public event EventHandler<string> 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<string, IServiceConfigurable> tempConfigurables = new Dictionary<string, IServiceConfigurable>();
foreach (IServiceConfigurable configurable in service.Configurables)
{
tempConfigurables.Add(configurable.OptionName, configurable);
}
this.configurables = new ReadOnlyDictionary<string, IServiceConfigurable>(tempConfigurables);
}
/// <summary>
/// Starts this service.
/// </summary>
/// <exception cref="InvalidOperationException">Is thrown when the service is already running.</exception>
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();
}
/// <summary>
/// Stops the service.
/// </summary>
/// <exception cref="InvalidOperationException">Is thrown when the is not running.</exception>
public void Stop()
{
if (state != ServiceState.Running) throw new InvalidOperationException("Service instance not running.");
Logger.Log($"\"{ServiceName}\" is stopping.");
service.ElegantShutdown();
}
/// <summary>
/// Sends a command to this service to execute.
/// </summary>
/// <param name="command">The command to execute.</param>
/// <exception cref="InvalidOperationException">Is thrown when the service is not running.</exception>
public void ExecuteCommand(string command)
{
Logger.Log($"\"{ServiceName}\" is executing command \"{command}\".", LogLevel.DEBUG);
if (state != ServiceState.Running) throw new InvalidOperationException("Service instance not running.");
service.ExecuteCommand(command);
}
/// <summary>
/// Gets the possible <see cref="IServiceConfigurable"/>'s names for this service.
/// </summary>
/// <returns>A <see cref="ISet{string}"/> returned where the string is the option's name.</returns>
public ISet<string> GetConfigurableOptions()
{
return new HashSet<string>(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;
}
/// <returns>The name of assembly this module is contained in.</returns>
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);
}
}
}

View File

@@ -0,0 +1,262 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using GameServiceWarden.API.Games;
using GameServiceWarden.Core.Persistence;
using GameServiceWarden.API.Module;
using System.Text;
namespace GameServiceWarden.Core.Module
{
public class ServiceManager : IServiceManagerActionExecuter
{
private IServiceManagerMonitor managerMonitor;
private readonly ConcurrentDictionary<string, ServiceDescriptor> running;
private readonly IPersistent<IReadOnlyDictionary<string, string>> services;
private readonly LRUCache<string, ServiceDescriptor> descriptorCache;
private readonly IReadOnlyPersistent<IReadOnlyDictionary<string, IServiceModule>> modules;
public ServiceManager(IServiceManagerMonitor actionMonitor, IPersistent<IReadOnlyDictionary<string, string>> services, IReadOnlyPersistent<IReadOnlyDictionary<string, IServiceModule>> 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<string, ServiceDescriptor>();
this.descriptorCache = new LRUCache<string, ServiceDescriptor>(100);
}
public void CreateService(string serviceName, string assemblyName, string moduleName)
{
if (!this.modules.ContainsKey(assemblyName)) throw new KeyNotFoundException($"No file \"{assemblyName}\" found.");
IReadOnlyDictionary<string, IServiceModule> assemblyModules = this.modules[assemblyName];
if (services.ContainsKey(serviceName)) throw new ArgumentException($"Service of Name \"{serviceName}\" already exists.");
Dictionary<string, string> data = new Dictionary<string, string>();
data[ServiceDescriptor.ASSEMBLY_PROPERTY] = assemblyName;
data[ServiceDescriptor.MODULE_PROPERTY] = moduleName;
services.AddToPersistence(serviceName, data);
ServiceManagerState managerState = new ServiceManagerState();
managerState.delta = true;
managerState.subtract = false;
managerState.services = new List<string>();
managerState.services.Add(serviceName);
managerMonitor.Present(managerState);
}
public void DeleteService(string serviceName)
{
if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found.");
if (running.ContainsKey(serviceName)) running[serviceName].Stop();
services.Delete(serviceName);
ServiceManagerState managerState = new ServiceManagerState();
managerState.delta = true;
managerState.subtract = true;
managerState.services = new List<string>();
managerState.services.Add(serviceName);
managerMonitor.Present(managerState);
}
public IEnumerable<string> GetModuleNames()
{
ServiceManagerState managerState = new ServiceManagerState();
managerState.modules = modules.Keys.ToImmutableArray();
managerMonitor.Present(managerState);
return modules.Keys;
}
public IEnumerable<string> GetServiceNames()
{
ServiceManagerState managerState = new ServiceManagerState();
managerState.services = services.Keys.ToImmutableArray();
managerMonitor.Present(managerState);
return services.Keys;
}
public IEnumerable<string> GetRunningServiceNames() {
ServiceManagerState managerState = new ServiceManagerState();
managerState.running = running.Keys.ToImmutableArray();
managerMonitor.Present(managerState);
return running.Keys;
}
private IEnumerable<string> GetServiceOptions(string serviceName)
{
if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found.");
IReadOnlyDictionary<string, string> 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.");
IReadOnlyDictionary<string, string> 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<string, IReadOnlyDictionary<string, string>> GetOptions() {
ServiceManagerState managerState = new ServiceManagerState();
Dictionary<string, IReadOnlyDictionary<string, string>> serviceOptions = new Dictionary<string, IReadOnlyDictionary<string, string>>();
foreach (string service in GetServiceNames())
{
Dictionary<string, string> optionsOfService = new Dictionary<string, string>();
foreach (string option in GetServiceOptions(service))
{
optionsOfService.Add(option, GetServiceOptionValue(service, option));
}
serviceOptions.Add(service, optionsOfService);
}
managerState.serviceOptions = serviceOptions;
managerMonitor.Present(managerState);
return serviceOptions;
}
public bool SetServiceOptionValue(string serviceName, string optionName, string value)
{
if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found.");
IReadOnlyDictionary<string, string> 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 (service.SetConfigurableValue(optionName, value)) {
managerState.delta = true;
Dictionary<string, IReadOnlyDictionary<string, string>> changedOption = new Dictionary<string, IReadOnlyDictionary<string, string>>();
Dictionary<string, string> options = new Dictionary<string, string>();
options[optionName] = GetServiceOptionValue(serviceName, optionName);
changedOption[serviceName] = options;
managerState.serviceOptions = changedOption;
}
managerMonitor.Present(managerState);
return managerState.delta;
}
public void StartService(string serviceName)
{
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.");
IReadOnlyDictionary<string, string> 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)
{
if (!running.ContainsKey(serviceName)) throw new InvalidOperationException($"Service under name \"{serviceName}\" is not running.");
running[serviceName].Stop();
}
public void ExecuteCommand(string serviceName, string command)
{
if (!running.ContainsKey(serviceName)) throw new InvalidOperationException($"Service under name \"{serviceName}\" is not running.");
running[serviceName].ExecuteCommand(command);
}
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<string, string> info = services[serviceName];
ServiceDescriptor service = descriptorCache.Use(serviceName, () => GenerateDescriptor(serviceName, info[ServiceDescriptor.ASSEMBLY_PROPERTY], info[ServiceDescriptor.MODULE_PROPERTY]));
return service.GetLogBuffer();
}
public Dictionary<string, byte[]> GetLogBuffer() {
Dictionary<string, byte[]> logs = new Dictionary<string, byte[]>();
foreach (string service in running.Keys)
{
logs.Add(service, running[service].GetLogBuffer());
}
ServiceManagerState managerState = new ServiceManagerState();
managerState.logs = logs;
managerMonitor.Present(managerState);
return logs;
}
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 ServiceState.Running:
if (running.TryAdd(serviceInfo.ServiceName, serviceInfo)) {
managerChange.delta = true;
managerChange.running = new List<string>();
managerChange.running.Add(serviceInfo.ServiceName);
}
break;
case ServiceState.Stopped:
ServiceDescriptor removed;
if (running.TryRemove(serviceInfo.ServiceName, out removed)) {
removed.ServiceStateChangeEvent -= OnServiceStateChange;
removed.LogUpdateEvent -= OnLogUpdated;
Dictionary<string, string> removedInfo = new Dictionary<string, string>();
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<string>();
managerChange.running.Add(serviceInfo.ServiceName);
}
break;
}
managerMonitor.Present(managerChange);
}
void OnLogUpdated(object sender, string update) {
ServiceDescriptor service = (ServiceDescriptor)sender;
ServiceManagerState state = new ServiceManagerState();
state.delta = true;
Dictionary<string, string> logUpdate = new Dictionary<string, string>();
logUpdate[service.ServiceName] = update;
managerMonitor.Present(state);
}
public void ExecuteAction(ServiceManagerAction action)
{
switch (action.action)
{
case ServiceManagerAction.Type.View:
GetServiceNames();
GetRunningServiceNames();
GetModuleNames();
GetLogBuffer();
GetOptions();
break;
case ServiceManagerAction.Type.CreateService:
CreateService(action.serviceName, action.assemblyName, action.moduleName);
break;
case ServiceManagerAction.Type.DeleteService:
DeleteService(action.serviceName);
break;
case ServiceManagerAction.Type.ExecuteCommand:
ExecuteCommand(action.serviceName, action.command);
break;
case ServiceManagerAction.Type.SetServiceOption:
SetServiceOptionValue(action.serviceName, action.option, action.value);
break;
case ServiceManagerAction.Type.Start:
StartService(action.serviceName);
break;
case ServiceManagerAction.Type.Stop:
StopService(action.serviceName);
break;
}
}
}
}