Implemented IPC system with minimal testing.

Large naming refactoring.

Added some more tests.
This commit is contained in:
Harrison Deng 2021-04-08 21:36:08 -05:00
parent 56259ac419
commit dfc54fdc00
64 changed files with 2231 additions and 1437 deletions

View File

@ -1,3 +1,3 @@
#!/bin/bash
dotnet build -v d src/GameServiceWarden.ModuleAPI
dotnet build -v d src/GameServiceWarden.API.Module
dotnet build -v d src/GameServiceWarden.Core

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace GameServiceWarden.API.Module
{
public interface IService
{
event EventHandler<bool> StateChangeEvent;
IReadOnlyCollection<IServiceConfigurable> Configurables{ get; }
void InitializeService(Stream stream);
void ElegantShutdown();
void ExecuteCommand(string command);
}
}

View File

@ -1,9 +1,9 @@
namespace GameServiceWarden.ModuleAPI
namespace GameServiceWarden.API.Module
{
public interface IGameConfigurable
public interface IServiceConfigurable
{
string OptionName { get; }
bool SetValue(string value);
string GetValue();
bool SetValue(string value);
}
}

View File

@ -1,8 +1,8 @@
using System.Collections.Generic;
namespace GameServiceWarden.ModuleAPI
namespace GameServiceWarden.API.Module
{
public interface IGameServiceModule
public interface IServiceModule
{
/// <summary>
/// The name of the game service this module handles.
@ -24,7 +24,7 @@ namespace GameServiceWarden.ModuleAPI
/// </summary>
/// <param name="workspace">The workspace directory. All service required files should be stored here. Expect the directory to be created.</param>
/// <param name="clean">Whether or not this game service is new. That is, the <code>workspace</code> can be assumed empty.</param>
/// <returns>The <see cref="IGameService"/> responsible for the instance of the game service.</returns>
IGameService InstantiateGameService(string workspace, bool clean);
/// <returns>The <see cref="IService"/> responsible for the instance of the game service.</returns>
IService InstantiateService(string workspace, bool clean);
}
}

View File

@ -1,10 +1,8 @@
namespace GameServiceWarden.ModuleAPI
namespace GameServiceWarden.API.Module
{
public enum ServiceState
{
Stopped,
Running,
Error,
PendingRestart
}
}

View File

@ -0,0 +1,12 @@
using System;
namespace GameServiceWarden.API.Communicable
{
public enum CommunicableType : uint
{
Disconnect,
Connect,
Service,
UnexpectedCommunication
}
}

View File

@ -0,0 +1,9 @@
using System;
namespace GameServiceWarden.API.Communicable
{
public interface ICommunicable
{
CommunicableType Type { get; }
}
}

View File

@ -0,0 +1,13 @@
namespace GameServiceWarden.API.Communicable.Requests
{
public struct ConnectRequest : ICommunicable
{
public string requestedIdentifier;
public string programName;
public string programAuthor;
public string versionNumber;
public string details;
public CommunicableType Type => CommunicableType.Connect;
}
}

View File

@ -0,0 +1,7 @@
namespace GameServiceWarden.API.Communicable
{
public struct DisconnectRequest
{
string reason;
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.IO.Pipes;
namespace GameServiceWarden.API.Communicable.Requests
{
public static class RequestHeader
{
public static void Decode(byte[] header, out CommunicableType type, out uint length) {
type = (CommunicableType) BitConverter.ToUInt32(header, 0);
length = BitConverter.ToUInt32(header, sizeof(uint));
}
}
}

View File

@ -0,0 +1,11 @@
using GameServiceWarden.API.Games;
namespace GameServiceWarden.API.Communicable.Requests
{
public struct ServiceRequest : ICommunicable
{
public ServiceManagerAction serviceManagerAction;
public CommunicableType Type => CommunicableType.Service;
}
}

View File

@ -0,0 +1,14 @@
using System.Security.Cryptography.X509Certificates;
namespace GameServiceWarden.API.Communicable.Responses
{
public struct ConnectResponse : ICommunicable
{
public string identifier;
public bool nameTaken;
public bool invalidName;
public string errorMsg;
public CommunicableType Type => CommunicableType.Connect;
}
}

View File

@ -0,0 +1,7 @@
namespace GameServiceWarden.API.Communicable
{
public struct DisconnectResponse
{
public string reason;
}
}

View File

@ -0,0 +1,14 @@
using System;
namespace GameServiceWarden.API.Communicable.Responses
{
public static class ResponseHeader
{
public static byte[] Encode(CommunicableType type, uint length) {
byte[] res = new byte[sizeof(uint) + sizeof(uint)];
BitConverter.GetBytes((uint)type).CopyTo(res, 0);
BitConverter.GetBytes(length).CopyTo(res, sizeof(uint));
return res;
}
}
}

View File

@ -0,0 +1,11 @@
using GameServiceWarden.API.Games;
namespace GameServiceWarden.API.Communicable.Responses
{
public struct ServiceResponse : ICommunicable
{
public ServiceManagerState gameServiceDelta;
public CommunicableType Type => CommunicableType.Service;
}
}

View File

@ -0,0 +1,10 @@
namespace GameServiceWarden.API.Communicable.Responses
{
public struct UnexpectedRequestResponse : ICommunicable
{
public CommunicableType origin;
public string message;
public CommunicableType Type => CommunicableType.UnexpectedCommunication;
}
}

View File

@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,25 @@
namespace GameServiceWarden.API.Games
{
public struct ServiceManagerAction
{
public enum Type
{
Start,
Stop,
CreateService,
DeleteService,
ExecuteCommand,
SetServiceOption,
View
}
public string assemblyName;
public string moduleName;
public string serviceName;
public string option;
public string command;
public string value;
public Type action;
}
}

View File

@ -0,0 +1,15 @@
using System.Collections.Generic;
namespace GameServiceWarden.API.Games
{
public struct ServiceManagerState
{
public bool delta;
public bool subtract;
public ICollection<string> services;
public ICollection<string> running;
public ICollection<string> modules;
public IReadOnlyDictionary<string, string> logs;
public IReadOnlyDictionary<string, IReadOnlyDictionary<string, string>> serviceOptions;
}
}

View File

@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\GameServiceWarden.ModuleAPI\GameServiceWarden.ModuleAPI.csproj" />
<ProjectReference Include="..\GameServiceWarden.API.Module\GameServiceWarden.API.Module.csproj" />
<ProjectReference Include="..\GameServiceWarden.API\GameServiceWarden.API.csproj" />
</ItemGroup>
<PropertyGroup>

View File

@ -1,13 +0,0 @@
using GameServiceWarden.ModuleAPI;
namespace GameServiceWarden.Core.Games
{
public struct ServiceAction
{
public string AssemblyName { get; set; }
public string ModuleName { get; set; }
public string ServiceName { get; set; }
public IGameServiceModule Module { get; set; }
public GameServiceActions Action { get; set; }
}
}

View File

@ -1,16 +0,0 @@
namespace GameServiceWarden.Core.Games
{
public enum GameServiceActions
{
Start,
Stop,
AddModule,
RemoveModule,
CreateService,
DeleteService,
ExecuteCommand,
SetServiceOption,
View
}
}

View File

@ -1,72 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace GameServiceWarden.Core.Games
{
public class GameServiceGateway
{
private readonly 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 GameServiceGateway(string dataDirectory)
{
if (!Directory.Exists(dataDirectory)) Directory.CreateDirectory(dataDirectory);
this.dataDirectory = dataDirectory;
}
public void SaveService(string serviceName, string assemblyName, string moduleName)
{
string serviceInfoPath = dataDirectory + Path.DirectorySeparatorChar + serviceName + EXTENSION;
using (StreamWriter writer = File.CreateText(serviceInfoPath))
{
writer.WriteLine($"{SERVICE_NAME} : {serviceName}");
writer.WriteLine($"{ASSEMBLY_NAME} : {assemblyName}");
writer.WriteLine($"{MODULE_NAME} : {moduleName}");
}
}
public string GetServiceName(string path)
{
return GetServiceInfoValue(path, SERVICE_NAME);
}
public string GetServiceModuleName(string path)
{
return GetServiceInfoValue(path, MODULE_NAME);
}
public string GetServiceAssemblyName(string path)
{
return GetServiceInfoValue(path, ASSEMBLY_NAME);
}
private string GetServiceInfoValue(string path, string key)
{
IEnumerable<string> lines = File.ReadAllLines(path);
foreach (string line in lines)
{
if (line.StartsWith($"{key}: "))
{
return line.Substring(key.Length + 2);
}
}
throw new FormatException($"\"{path}\" is corrupted. Could not find value for: {key}.");
}
public IEnumerable<string> GetAllServiceInfoPaths()
{
string[] files = Directory.GetFiles(dataDirectory);
foreach (string filePath in files)
{
if (Path.GetExtension(filePath).Equals(EXTENSION))
{
yield return filePath;
}
}
}
}
}

View File

@ -1,161 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Threading;
using GameServiceWarden.ModuleAPI;
namespace GameServiceWarden.Core.Games
{
public class GameServiceInfo : IDisposable //entity
{
/// <summary>
/// The name of the service itself, independent of the name of the module this service is using.
/// </summary>
public string ServiceName { get { return serviceName; } set { Interlocked.Exchange(ref serviceName, value); } }
/// <summary>
/// The services console output stream.
/// </summary>
private volatile string serviceName; //thread-safe(?)
public Stream ServiceConsoleStream { get; private set; } // Thread safe.
private readonly object controlLock = new object();
private volatile ServiceState state;
private readonly IGameService service;
private readonly string assemblyName;
/// <summary>
/// Name of module this service uses.
/// </summary>
public string ModuleName { get; private set; }
private readonly IReadOnlyDictionary<string, IGameConfigurable> configurables;
private bool disposed;
public GameServiceInfo(IGameService service, 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.service.StateChangeEvent += OnServiceStateChange;
Dictionary<string, IGameConfigurable> tempConfigurables = new Dictionary<string, IGameConfigurable>();
foreach (IGameConfigurable configurable in service.Configurables)
{
tempConfigurables.Add(configurable.OptionName, configurable);
}
this.configurables = new ReadOnlyDictionary<string, IGameConfigurable>(tempConfigurables);
}
/// <summary>
/// Starts this service.
/// </summary>
/// <exception cref="InvalidOperationException">Is thrown when the service is already running.</exception>
public void Start()
{
lock (controlLock)
{
if (state != ServiceState.Stopped) throw new InvalidOperationException("Service instance already running.");
DateTimeFormatInfo format = new DateTimeFormatInfo();
format.TimeSeparator = "-";
format.DateSeparator = "_";
ServiceConsoleStream = new MemoryStream(8 * 1024 * 1024); // 8 MB
ServiceConsoleStream = Stream.Synchronized(ServiceConsoleStream);
service.InitializeService(new StreamWriter(ServiceConsoleStream));
}
}
/// <summary>
/// Stops the service.
/// </summary>
/// <exception cref="InvalidOperationException">Is thrown when the is not running.</exception>
public void Stop()
{
lock (controlLock)
{
if (state != ServiceState.Running) throw new InvalidOperationException("Service instance not running.");
this.service.ElegantShutdown();
ServiceConsoleStream.Close();
ServiceConsoleStream = null;
}
}
/// <summary>
/// Sends a command to this service to execute.
/// </summary>
/// <param name="command">The command to execute.</param>
/// <exception cref="InvalidOperationException">Is thrown when the service is not running.</exception>
public void ExecuteCommand(string command)
{
lock (controlLock)
{
if (state != ServiceState.Running) throw new InvalidOperationException("Service instance not running.");
service.ExecuteCommand(command);
}
}
/// <summary>
/// Gets the possible <see cref="IGameConfigurable"/>'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();
}
/// <returns>The <see cref="ServiceState"/> that this service is currently in.</returns>
public ServiceState GetServiceState()
{
lock (controlLock)
{
return state;
}
}
/// <returns>The name of assembly this module is contained in.</returns>
public string GetAssemblyName()
{
return assemblyName;
}
private void OnServiceStateChange(object sender, ServiceState current)
{
lock (controlLock)
{
this.state = current;
}
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
ServiceConsoleStream?.Dispose();
}
//No unmanaged code, therefore, no finalizer.
disposed = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View File

@ -1,103 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using GameServiceWarden.Core.Persistence;
using GameServiceWarden.ModuleAPI;
namespace GameServiceWarden.Core.Games
{
public class GameServiceManager
{
private readonly IDictionary<string, GameServiceInfo> running;
private readonly IPersistent<GameServiceInfo> services;
private readonly IReadOnlyPersistent<IReadOnlyDictionary<string, IGameServiceModule>> modules;
public GameServiceManager(IPersistent<GameServiceInfo> services, IReadOnlyPersistent<IReadOnlyDictionary<string, IGameServiceModule>> modules)
{
this.services = services;
this.modules = modules;
this.running = new Dictionary<string, GameServiceInfo>();
}
public void CreateService(string serviceName, string assemblyName, string moduleName)
{
if (!this.modules.ContainsKey(assemblyName)) throw new KeyNotFoundException($"No file \"{assemblyName}\" found.");
IReadOnlyDictionary<string, IGameServiceModule> assemblyModules = this.modules[assemblyName];
if (services.ContainsKey(serviceName)) throw new ArgumentException($"Service of Name \"{serviceName}\" already exists.");
services.AddToPersistence(serviceName, new GameServiceInfo(assemblyModules[moduleName].InstantiateGameService(services.GetPathForKey(serviceName), true), moduleName, assemblyName));
}
public void DeleteService(string serviceName)
{
if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found.");
if (services[serviceName].GetServiceState() == ServiceState.Running) services[serviceName].Stop();
services.Delete(serviceName);
}
public IEnumerable<string> GetModuleNames()
{
return modules.Keys;
}
public IEnumerable<string> GetServiceNames()
{
return services.Keys;
}
public IEnumerable<string> GetServiceOptions(string serviceName)
{
if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found.");
GameServiceInfo serviceInfo = services[serviceName];
return serviceInfo.GetConfigurableOptions();
}
public 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);
}
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.");
return services[serviceName].SetConfigurableValue(optionName, value);
}
public ServiceState GetServiceState(string serviceName)
{
if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found.");
return services[serviceName].GetServiceState();
}
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.");
running.Add(serviceName, services[serviceName]);
running[serviceName].Start();
}
public void StopService(string serviceName)
{
if (!running.ContainsKey(serviceName)) throw new InvalidOperationException($"Service under name \"{serviceName}\" is not running.");
running[serviceName].Stop();
services[serviceName] = running[serviceName];
running.Remove(serviceName);
}
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);
}
public Stream GetServiceConsoleStream(string serviceName)
{
if (!running.ContainsKey(serviceName)) throw new InvalidOperationException($"Service under name \"{serviceName}\" is not running.");
return running[serviceName].ServiceConsoleStream;
}
}
}

View File

@ -1,7 +0,0 @@
namespace GameServiceWarden.Core.Games
{
public interface IGameServiceExecutioner
{
void ExecuteAction(ServiceAction action);
}
}

View File

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

View File

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

View File

@ -0,0 +1,13 @@
namespace GameServiceWarden.Core.Games.Modules.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

@ -4,11 +4,11 @@ using System.Runtime.Loader;
namespace GameServiceWarden.Core.Games.Modules
{
class GameModuleLoadContext : AssemblyLoadContext
class ModuleLoadContext : AssemblyLoadContext
{
private readonly AssemblyDependencyResolver dependencyResolver;
public GameModuleLoadContext(string path) {
public ModuleLoadContext(string path) {
dependencyResolver = new AssemblyDependencyResolver(path);
}

View File

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

View File

@ -0,0 +1,235 @@
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;
//TODO Update UML
namespace GameServiceWarden.Core.Games
{
public class ServiceDescriptor //entity
{
private const string DISTRIBUTOR_SUFFIX = "_dist";
private const int INIT_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 bool running;
private readonly IService service;
/// <summary>
/// The services log output pipe name.
/// </summary>
public string ServiceLogPipeName { get { return (serviceName); } }
private string moduleName;
private readonly string assemblyName;
private NamedPipeServerStream logReceiver;
private NamedPipeClientStream logSender;
private ConcurrentStack<NamedPipeServerStream> logStreamListeners;
private Task logUpdateTask;
private Task acceptingTask;
private CancellationTokenSource stopToken;
/// <summary>
/// Name of module this service uses.
/// </summary>
private readonly IReadOnlyDictionary<string, IServiceConfigurable> configurables;
public event EventHandler<bool> 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;
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);
logStreamListeners = new ConcurrentStack<NamedPipeServerStream>();
}
/// <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 (running) throw new InvalidOperationException("Service instance already running.");
logReceiver = new NamedPipeServerStream(ServiceLogPipeName + DISTRIBUTOR_SUFFIX, PipeDirection.In, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
Task waitForConnection = logReceiver.WaitForConnectionAsync();
logSender = new NamedPipeClientStream(".", ServiceLogPipeName + DISTRIBUTOR_SUFFIX, 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.IsCompletedSuccessfully) {
throw new ServiceInitializationException("Error while sending identification token.");
}
if (!idToken.SequenceEqual(receivedToken)) {
throw new ServiceInitializationException("Wrong distributor identification token.");
}
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(INIT_TIMEOUT);
Task initializationTask = Task.Run(() => service.InitializeService(logSender), cancellationTokenSource.Token);
initializationTask.Wait();
cancellationTokenSource.Dispose();
stopToken = new CancellationTokenSource();
acceptingTask = AcceptLogConnections();
logUpdateTask = BroadcastLog();
}
/// <summary>
/// Stops the service.
/// </summary>
/// <exception cref="InvalidOperationException">Is thrown when the is not running.</exception>
public void Stop()
{
Logger.Log("\"{ServiceName}\" is stopping.");
if (!running) throw new InvalidOperationException("Service instance not running.");
service.ElegantShutdown();
stopToken.Cancel();
try {
acceptingTask.Wait();
} catch (AggregateException e) {
e.Handle((exception) => exception is TaskCanceledException);
}
try
{
logUpdateTask.Wait();
}
catch (AggregateException e)
{
e.Handle((exception) => exception is TaskCanceledException);
}
logSender.Dispose();
logReceiver.Dispose();
logStreamListeners.Clear();
stopToken.Dispose();
}
/// <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 (!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 bool GetServiceState()
{
return running;
}
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;
}
private void OnServiceStateChange(object sender, bool running)
{
this.running = running;
Logger.Log($"{ServiceName}'s state changed 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);
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<NamedPipeServerStream> completeStack = new Stack<NamedPipeServerStream>();
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 ((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.");
}
}
}

View File

@ -0,0 +1,239 @@
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;
namespace GameServiceWarden.Core.Games
{
public class ServiceManager : IServiceManagerActionExecuter
{
private IServiceManagerMonitor managerMonitor;
private readonly ConcurrentDictionary<string, ServiceDescriptor> running;
private readonly IPersistent<ServiceDescriptor> services;
private readonly IReadOnlyPersistent<IReadOnlyDictionary<string, IServiceModule>> modules;
public ServiceManager(IServiceManagerMonitor actionMonitor, IPersistent<ServiceDescriptor> 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>();
}
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.");
services.AddToPersistence(serviceName, new ServiceDescriptor(assemblyModules[moduleName].InstantiateService(services.GetPathForKey(serviceName), true), serviceName, moduleName, assemblyName));
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 (services[serviceName].GetServiceState()) services[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.");
ServiceDescriptor serviceInfo = services[serviceName];
return serviceInfo.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);
}
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.");
if (!services[serviceName].GetConfigurableOptions().Contains(optionName)) throw new KeyNotFoundException($"Option \"{optionName}\" for service \"{serviceName}\" not found.");
ServiceManagerState managerState = new ServiceManagerState();
if (services[serviceName].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.");
ServiceDescriptor info = services[serviceName];
info.ServiceStateChangeEvent += OnServiceStateChange;
info.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 string GetServiceLogPipeName(string serviceName)
{
if (!running.ContainsKey(serviceName)) throw new InvalidOperationException($"Service under name \"{serviceName}\" is not running.");
return running[serviceName].ServiceLogPipeName;
}
public IReadOnlyDictionary<string, string> GetLogPipeNames() {
ServiceManagerState managerState = new ServiceManagerState();
Dictionary<string, string> logPipeNames = new Dictionary<string, string>();
foreach (string service in GetRunningServiceNames())
{
logPipeNames.Add(service, GetServiceLogPipeName(service));
}
managerState.logs = logPipeNames;
managerMonitor.Present(managerState);
return logPipeNames;
}
private void OnServiceStateChange(object sender, bool state) {
ServiceDescriptor serviceInfo = (ServiceDescriptor)sender;
ServiceManagerState managerChange = new ServiceManagerState();
switch (state)
{
case true:
if (running.TryAdd(serviceInfo.ServiceName, serviceInfo)) {
managerChange.delta = true;
managerChange.running = new List<string>();
managerChange.running.Add(serviceInfo.ServiceName);
Dictionary<string, string> logAdded = new Dictionary<string, string>();
logAdded.Add(serviceInfo.ServiceName, GetServiceLogPipeName(serviceInfo.ServiceName));
managerChange.logs = logAdded;
}
break;
case false:
ServiceDescriptor removed;
if (running.Remove(serviceInfo.ServiceName, out removed)) {
removed.ServiceStateChangeEvent -= OnServiceStateChange;
services[serviceInfo.ServiceName] = removed;
managerChange.delta = true;
managerChange.subtract = true;
managerChange.running = new List<string>();
managerChange.running.Add(serviceInfo.ServiceName);
Dictionary<string, string> logRemoved = new Dictionary<string, string>();
logRemoved.Add(serviceInfo.ServiceName, null);
managerChange.logs = logRemoved;
}
break;
}
managerMonitor.Present(managerChange);
}
public void ExecuteAction(ServiceManagerAction action)
{
switch (action.action)
{
//TODO FINISH MOVING THIS!!!!
case ServiceManagerAction.Type.View:
GetServiceNames();
GetRunningServiceNames();
GetModuleNames();
GetLogPipeNames();
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;
}
}
}
}

View File

@ -1,19 +0,0 @@
using System;
namespace GameServiceWarden.Core.Logging
{
public class FileLogReceiver : ILogReceiver
{
public LogLevel Level => LogLevel.INFO;
public void Flush()
{
throw new NotImplementedException();
}
public void LogMessage(string message, DateTime time, LogLevel level)
{
throw new NotImplementedException();
}
}
}

View File

@ -4,6 +4,8 @@ namespace GameServiceWarden.Core.Logging
{
public interface ILogReceiver
{
string Identifier { get; }
/// <summary>
/// The severity of the messages this log should receive.
/// </summary>

View File

@ -1,6 +1,6 @@
namespace GameServiceWarden.Core.Logging
{
public enum LogLevel
public enum LogLevel : int
{
FATAL,
INFO,

View File

@ -3,16 +3,16 @@ using System.Collections.Generic;
namespace GameServiceWarden.Core.Logging
{
public class Logger {
private readonly HashSet<ILogReceiver> listeners = new HashSet<ILogReceiver>();
public static class Logger {
private static readonly Dictionary<string, ILogReceiver> listeners = new Dictionary<string, ILogReceiver>();
/// <summary>
/// Logs the message to listeners that are listening to the set severity of the message or greater.
/// </summary>
/// <param name="message">The message to log.</param>
/// <param name="level">The level of severity, by default, info.</param>
public void Log(string message, LogLevel level = LogLevel.INFO) {
foreach (ILogReceiver listener in listeners)
public static void Log(string message, LogLevel level = LogLevel.INFO) {
foreach (ILogReceiver listener in listeners.Values)
{
if (level <= listener.Level) {
listener.LogMessage(message, DateTime.Now, level);
@ -24,24 +24,24 @@ namespace GameServiceWarden.Core.Logging
/// Adds a log listener.
/// </summary>
/// <param name="listener">The listener to add.</param>
public void AddLogListener(ILogReceiver listener) {
listeners.Add(listener);
public static void AddLogListener(ILogReceiver listener) {
listeners[listener.Identifier] = listener;
}
/// <summary>
/// Removes a log listener.
/// </summary>
/// <param name="listener">The listener to remove.</param>
public void RemoveLogListener(ILogReceiver listener) {
listeners.Remove(listener);
public static void RemoveLogListener(ILogReceiver listener) {
listeners.Remove(listener.Identifier);
}
/// <summary>
/// Called when all listeners should perform any flushing they need.
/// </summary>
public void FlushListeners()
public static void FlushListeners()
{
foreach (ILogReceiver listener in listeners)
foreach (ILogReceiver listener in listeners.Values)
{
listener.Flush();
}

View File

@ -1,6 +1,6 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using GameServiceWarden.ModuleAPI;
using GameServiceWarden.API.Module;
namespace GameServiceWarden.Core.Persistence
{

View File

@ -4,39 +4,38 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using GameServiceWarden.Core.Games;
using GameServiceWarden.ModuleAPI;
using GameServiceWarden.API.Module;
namespace GameServiceWarden.Core.Persistence
{
public class PersistedGameServiceInfos : IPersistent<GameServiceInfo>
public class ServiceDescriptorPersistence : IPersistent<ServiceDescriptor>
{
private readonly IReadOnlyPersistent<IReadOnlyDictionary<string, IGameServiceModule>> modules;
private readonly IReadOnlyPersistent<IReadOnlyDictionary<string, IServiceModule>> 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 PersistedGameServiceInfos(string mapDirectory, IReadOnlyPersistent<IReadOnlyDictionary<string, IGameServiceModule>> modules)
public ServiceDescriptorPersistence(string mapDirectory, IReadOnlyPersistent<IReadOnlyDictionary<string, IServiceModule>> modules)
{
this.mapDirectory = mapDirectory;
this.modules = modules;
}
public GameServiceInfo this[string key]
public ServiceDescriptor this[string key]
{
set
{
value.ServiceName = key;
SaveService(key, value.GetAssemblyName(), value.ModuleName);
SaveService(key, value.GetAssemblyName(), value.GetModuleName());
}
get
{
if (!ContainsKey(key)) throw new KeyNotFoundException();
string assemblyName = GetServiceInfoValue(key, ASSEMBLY_NAME);
string moduleName = GetServiceInfoValue(key, MODULE_NAME);
IGameService service = modules[assemblyName][moduleName].InstantiateGameService(GetPathForKey(key), false);
return new GameServiceInfo(service, moduleName, assemblyName);
IService service = modules[assemblyName][moduleName].InstantiateService(GetPathForKey(key), false);
return new ServiceDescriptor(service, key, moduleName, assemblyName);
}
}
@ -52,10 +51,10 @@ namespace GameServiceWarden.Core.Persistence
}
}
public IEnumerable<GameServiceInfo> Values {
public IEnumerable<ServiceDescriptor> Values {
get {
IEnumerable<string> keys = Keys;
List<GameServiceInfo> res = new List<GameServiceInfo>();
List<ServiceDescriptor> res = new List<ServiceDescriptor>();
foreach (string key in keys)
{
res.Add(this[key]);
@ -76,7 +75,7 @@ namespace GameServiceWarden.Core.Persistence
}
}
public void AddToPersistence(string key, GameServiceInfo value)
public void AddToPersistence(string key, ServiceDescriptor value)
{
if (key == null) throw new ArgumentNullException();
if (ContainsKey(key)) throw new ArgumentException();
@ -97,13 +96,13 @@ namespace GameServiceWarden.Core.Persistence
return Directory.Exists(GetPathForKey(key));
}
public IEnumerator<KeyValuePair<string, GameServiceInfo>> GetEnumerator()
public IEnumerator<KeyValuePair<string, ServiceDescriptor>> GetEnumerator()
{
IEnumerable<string> keys = Keys;
List<KeyValuePair<string, GameServiceInfo>> result = new List<KeyValuePair<string, GameServiceInfo>>();
List<KeyValuePair<string, ServiceDescriptor>> result = new List<KeyValuePair<string, ServiceDescriptor>>();
foreach (string key in keys)
{
result.Add(new KeyValuePair<string, GameServiceInfo>(key, this[key]));
result.Add(new KeyValuePair<string, ServiceDescriptor>(key, this[key]));
}
return result.GetEnumerator();
}
@ -128,7 +127,7 @@ namespace GameServiceWarden.Core.Persistence
return true;
}
public bool TryLoadValue(string key, [MaybeNullWhen(false)] out GameServiceInfo value)
public bool TryLoadValue(string key, [MaybeNullWhen(false)] out ServiceDescriptor value)
{
try {
value = this[key];

View File

@ -4,33 +4,33 @@ using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using GameServiceWarden.Core.Games.Modules;
using GameServiceWarden.ModuleAPI;
using GameServiceWarden.API.Module;
namespace GameServiceWarden.Core.Persistence
{
public class GameServiceModules : IReadOnlyPersistent<IReadOnlyDictionary<string, IGameServiceModule>>
public class ServiceModules : IReadOnlyPersistent<IReadOnlyDictionary<string, IServiceModule>>
{
private readonly string mapDirectory;
private readonly GameServiceModuleLoader loader = new GameServiceModuleLoader();
private readonly ServiceModuleLoader loader = new ServiceModuleLoader();
public GameServiceModules(string mapDirectory)
public ServiceModules(string mapDirectory)
{
this.mapDirectory = mapDirectory;
}
public IReadOnlyDictionary<string, IGameServiceModule> this[string key]
public IReadOnlyDictionary<string, IServiceModule> this[string key]
{
get
{
if (!ContainsKey(key)) throw new KeyNotFoundException($"Key \"{key}\" not found.");
Dictionary<string, IGameServiceModule> res = new Dictionary<string, IGameServiceModule>();
IEnumerable<IGameServiceModule> modules = loader.LoadModules(GetPathForKey(key));
foreach (IGameServiceModule module in modules)
Dictionary<string, IServiceModule> res = new Dictionary<string, IServiceModule>();
IEnumerable<IServiceModule> modules = loader.LoadModules(GetPathForKey(key));
foreach (IServiceModule module in modules)
{
res.Add(module.Name, module);
}
return new ReadOnlyDictionary<string, IGameServiceModule>(res);
return new ReadOnlyDictionary<string, IServiceModule>(res);
}
}
@ -57,7 +57,7 @@ namespace GameServiceWarden.Core.Persistence
}
}
public IEnumerable<IReadOnlyDictionary<string, IGameServiceModule>> Values
public IEnumerable<IReadOnlyDictionary<string, IServiceModule>> Values
{
get
{
@ -92,12 +92,12 @@ namespace GameServiceWarden.Core.Persistence
return File.Exists(path) && Path.GetExtension(path).ToLower().Equals("dll");
}
public IEnumerator<KeyValuePair<string, IReadOnlyDictionary<string, IGameServiceModule>>> GetEnumerator()
public IEnumerator<KeyValuePair<string, IReadOnlyDictionary<string, IServiceModule>>> GetEnumerator()
{
IEnumerable<string> keys = Keys;
foreach (string key in keys)
{
yield return new KeyValuePair<string, IReadOnlyDictionary<string, IGameServiceModule>>(key, this[key]);
yield return new KeyValuePair<string, IReadOnlyDictionary<string, IServiceModule>>(key, this[key]);
}
}
@ -106,7 +106,7 @@ namespace GameServiceWarden.Core.Persistence
return mapDirectory + Path.DirectorySeparatorChar + key;
}
public bool TryLoadValue(string key, [MaybeNullWhen(false)] out IReadOnlyDictionary<string, IGameServiceModule> value)
public bool TryLoadValue(string key, [MaybeNullWhen(false)] out IReadOnlyDictionary<string, IServiceModule> value)
{
try
{

View File

@ -6,7 +6,7 @@ namespace GameServiceWarden.Core
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}

View File

@ -0,0 +1,38 @@
using System.Diagnostics;
using System.Text.Json;
using GameServiceWarden.API.Communicable;
using GameServiceWarden.API.Communicable.Requests;
using GameServiceWarden.Core.Games;
using GameServiceWarden.Core.Logging;
namespace GameServiceWarden.Core.UI
{
public class IPCController
{
private IPCMediator mediator;
private IServiceManagerActionExecuter serviceExecutioner;
public IPCController(IPCMediator mediator, IServiceManagerActionExecuter serviceExecutioner)
{
this.mediator = mediator;
this.serviceExecutioner = serviceExecutioner;
}
public void Process() {
Logger.Log("Beginning to process interface requests.");
mediator.Open();
(string, CommunicableType, byte[]) action;
while (mediator.RequestQueue.TryTake(out action))
{
switch (action.Item2)
{
case CommunicableType.Service:
ServiceRequest request = JsonSerializer.Deserialize<ServiceRequest>(action.Item3);
serviceExecutioner.ExecuteAction(request.serviceManagerAction);
break;
}
}
mediator.Close();
}
}
}

View File

@ -0,0 +1,274 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipes;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using GameServiceWarden.API.Communicable;
using GameServiceWarden.API.Communicable.Requests;
using GameServiceWarden.API.Communicable.Responses;
using GameServiceWarden.Core.Logging;
namespace GameServiceWarden.Core.UI
{
public class IPCMediator
{
private const int CONNECT_TIMEOUT = 1000;
private readonly string name;
private readonly ConcurrentDictionary<string, (NamedPipeServerStream, Task)> pipes;
public BlockingCollection<(string, CommunicableType, byte[])> RequestQueue { get; private set; }
private volatile bool active;
public bool IsRunning { get => active; }
private Task acceptTask;
private CancellationTokenSource stopAcceptingToken;
public IPCMediator(string name)
{
this.name = name;
pipes = new ConcurrentDictionary<string, (NamedPipeServerStream, Task)>();
RequestQueue = new BlockingCollection<(string, CommunicableType, byte[])>(new ConcurrentQueue<(string, CommunicableType, byte[])>());
}
public void Open()
{
if (active) throw new InvalidOperationException("IPC already opened.");
active = true;
acceptTask = AcceptConnections();
}
public void Close()
{
if (!active) throw new InvalidOperationException("IPC not open.");
Logger.Log("Closing IPC mediator.");
active = false;
stopAcceptingToken.Cancel();
try
{
acceptTask.Wait();
}
catch (AggregateException e)
{
e.Handle((exception) => exception is TaskCanceledException);
}
InitiateDisconnectAll("Closing IPC system.").Wait();
RequestQueue.CompleteAdding();
stopAcceptingToken.Dispose();
}
public async Task ReplyAll(CommunicableType type, byte[] data)
{
IEnumerable<string> identifiers = pipes.Keys;
Stack<Task> replyTasks = new Stack<Task>();
foreach (string identifier in identifiers)
{
replyTasks.Push(Reply(identifier, type, data));
}
Task replyTask;
while (replyTasks.TryPop(out replyTask))
{
await replyTask;
}
}
public async Task Reply(string identifier, CommunicableType type, byte[] data)
{
CancellationTokenSource cancel = new CancellationTokenSource(1000);
byte[] header = ResponseHeader.Encode(type, (uint)data.Length);
await pipes[identifier].Item1.WriteAsync(header, 0, header.Length, cancel.Token);
await pipes[identifier].Item1.WriteAsync(data, cancel.Token);
cancel.Dispose();
}
public async Task InitiateDisconnect(string identifier, string reason)
{
Logger.Log($"Disconnecting \"{identifier}\". Reason: \"{reason}\"");
DisconnectResponse response;
response.reason = reason;
await Reply(identifier, CommunicableType.Disconnect, JsonSerializer.SerializeToUtf8Bytes(response));
(NamedPipeServerStream, Task) pipeAndTask = pipes[identifier];
pipeAndTask.Item1.Close();
await pipeAndTask.Item2;
Logger.Log($"Successfully disconnected \"{identifier}\".");
}
public async Task InitiateDisconnectAll(string reason) {
Logger.Log($"Disconnecting all of {pipes.Count} interfaces.");
foreach (string id in pipes.Keys)
{
await InitiateDisconnect(id, reason);
}
}
private async Task AcceptConnections()
{
List<Task> connectionTasks = new List<Task>();
Logger.Log("Accepting pipe connections.");
while (active)
{
NamedPipeServerStream pipe = new NamedPipeServerStream(name, PipeDirection.InOut, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
await pipe.WaitForConnectionAsync(stopAcceptingToken.Token);
connectionTasks.Add(OnConnection(pipe));
for (int i = 0; i < connectionTasks.Count; i++)
{
if (connectionTasks[i].IsCompleted) {
Task connectionTask = connectionTasks[i];
connectionTasks.RemoveAt(i);
connectionTask.Wait();
}
}
}
foreach (Task task in connectionTasks)
{
task.Wait();
}
Logger.Log("Stopped accepting pipe connections.");
}
private async Task OnConnection(NamedPipeServerStream pipe)
{
Logger.Log("Interface attempting to connect.", LogLevel.DEBUG);
byte[] headerBuffer = new byte[sizeof(uint) * 2];
int headerFill = 0;
CancellationTokenSource headerCancel = new CancellationTokenSource(CONNECT_TIMEOUT);
try
{
int readLength;
do
{
readLength = await pipe.ReadAsync(headerBuffer, headerFill, headerBuffer.Length - headerFill, headerCancel.Token);
headerFill += readLength;
} while (readLength != 0 && headerFill != headerBuffer.Length);
}
catch (AggregateException e)
{
e.Handle((exception) => exception is TaskCanceledException);
Logger.Log($"Interface did not send header data within {CONNECT_TIMEOUT}ms.", LogLevel.DEBUG);
} finally {
await pipe.DisposeAsync();
headerCancel.Dispose();
}
if (headerFill != headerBuffer.Length) {
Logger.Log($"Interface failed to send header data.", LogLevel.DEBUG);
return;
}
CommunicableType comType;
uint bodyLength;
RequestHeader.Decode(headerBuffer, out comType, out bodyLength); //TODO do exception check.
byte[] bodyBuffer = new byte[bodyLength];
int bodyFill = 0;
CancellationTokenSource bodyCancel = new CancellationTokenSource(CONNECT_TIMEOUT);
try
{
int readLength = 0;
do
{
readLength = await pipe.ReadAsync(bodyBuffer, bodyFill, bodyBuffer.Length - bodyFill, bodyCancel.Token);
bodyFill += readLength;
} while (readLength != 0 && bodyFill != headerBuffer.Length);
}
catch (AggregateException e)
{
e.Handle((exception) => exception is TaskCanceledException);
Logger.Log($"Interface failed to send body data within {CONNECT_TIMEOUT}.", LogLevel.DEBUG);
} finally {
await pipe.DisposeAsync();
bodyCancel.Dispose();
}
if (bodyFill != bodyBuffer.Length) {
Logger.Log($"Interface failed to send body data.", LogLevel.DEBUG);
return;
}
ConnectRequest request = JsonSerializer.Deserialize<ConnectRequest>(bodyBuffer);
ConnectResponse response = new ConnectResponse();
bool requestAccepted = false;
if (string.IsNullOrWhiteSpace(request.requestedIdentifier)) {
response.invalidName = true;
response.errorMsg = $"The requested identifier \"{request.requestedIdentifier}\" is null or whitespace.";
Logger.Log(response.errorMsg, LogLevel.DEBUG);
} else if (pipes.ContainsKey(request.requestedIdentifier)) {
response.invalidName = true;
response.nameTaken = true;
response.errorMsg = $"Interface requested identifier \"{request.requestedIdentifier}\" is taken.";
Logger.Log(response.errorMsg, LogLevel.DEBUG);
} else {
requestAccepted = true;
response.identifier = request.requestedIdentifier;
}
CancellationTokenSource cancelResponse = new CancellationTokenSource(CONNECT_TIMEOUT);
try
{
await pipe.WriteAsync(JsonSerializer.SerializeToUtf8Bytes(response), cancelResponse.Token);
}
catch (AggregateException e)
{
e.Handle((exception) => exception is TaskCanceledException);
Logger.Log($"Interface did not receive response within {CONNECT_TIMEOUT}ms.", LogLevel.DEBUG);
}
if (!requestAccepted) {
cancelResponse.Dispose();
await pipe.DisposeAsync();
Logger.Log($"Interface failed to connect.");
return;
}
Logger.Log($"Interface \"{response.identifier}\" connected.");
pipes[request.requestedIdentifier] = (pipe, Listen(response.identifier, pipe));
}
private async Task Listen(string identifier, NamedPipeServerStream pipe)
{
Logger.Log($"Started listening to interface \"{identifier}\".", LogLevel.DEBUG);
byte[] buffer = new byte[1024];
byte[] headerBuffer = new byte[sizeof(uint) * 2];
byte[] bodyBuffer = null;
int bodyFill = 0;
int headerFill = 0;
int readLength = 0;
CommunicableType? comType = null;
while ((readLength = await pipe.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
for (int i = 0; i < readLength; i++)
{
if (bodyBuffer == null)
{
headerBuffer[headerFill] = buffer[i];
headerFill++;
if (headerFill == headerBuffer.Length)
{
uint length;
CommunicableType type;
RequestHeader.Decode(headerBuffer, out type, out length);
bodyBuffer = new byte[length];
headerFill = 0;
comType = type;
}
}
else
{
bodyBuffer[bodyFill] = buffer[i];
bodyFill++;
if (bodyFill == bodyBuffer.Length)
{
RequestQueue.Add((identifier, comType.Value, bodyBuffer));
bodyFill = 0;
bodyBuffer = null;
}
}
}
}
Logger.Log($"Pipe for interface \"{identifier}\" has closed.", LogLevel.DEBUG);
(NamedPipeServerStream, Task) removedPipe;
pipes.Remove(identifier, out removedPipe);
await removedPipe.Item1.DisposeAsync();
Logger.Log($"Stopped listening to interface \"{identifier}\".", LogLevel.DEBUG);
}
}
}

View File

@ -0,0 +1,18 @@
using System.Text.Json;
using System.Threading.Tasks;
using GameServiceWarden.API.Communicable;
using GameServiceWarden.API.Games;
using GameServiceWarden.Core.Games;
namespace GameServiceWarden.Core.UI
{
public class IPCPresenter : IServiceManagerMonitor
{
private IPCMediator mediator;
public void Present(ServiceManagerState state)
{
Task replyTask = mediator.ReplyAll(CommunicableType.Service, JsonSerializer.SerializeToUtf8Bytes(state));
}
}
}

View File

@ -1,10 +0,0 @@
namespace GameServiceWarden.Core.UI
{
public interface ITextCommand
{
string Prefix { get; }
string Help{ get; }
bool Validate(string input);
void Execute(string input);
}
}

View File

@ -1,561 +1,540 @@
<mxfile host="65bd71144e" pages="2">
<diagram id="LHR7ubqCPd17_LyHkaH9" name="Structure">
<mxGraphModel dx="574" dy="229" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<mxGraphModel dx="1051" dy="506" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="dmd0HlDYcxYugIlahWj0-10" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.75;exitY=0;exitDx=0;exitDy=0;dashed=1;endArrow=block;endFill=0;sketch=1;" parent="1" source="dmd0HlDYcxYugIlahWj0-5" target="dmd0HlDYcxYugIlahWj0-9" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-5" value="GameServiceInfo" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;sketch=1;" parent="1" vertex="1">
<mxGeometry x="762" y="1030" width="401" height="386" as="geometry">
<mxCell id="dmd0HlDYcxYugIlahWj0-5" value="ServiceDescriptor" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;" parent="1" vertex="1">
<mxGeometry x="695" y="1100" width="510" height="410" as="geometry">
<mxRectangle x="762" y="1030" width="130" height="26" as="alternateBounds"/>
</mxGeometry>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-6" value="- serviceName: string&#10;- controlLock: object&#10;- state: ServiceState&#10;- service: IGameService&#10;- serviceConsoleStream: Stream&#10;- moduleName: string&#10;- assemblyName: string&#10;- Dictionary&lt;string, IConfigurable&gt;&#10;- disposed: bool" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="dmd0HlDYcxYugIlahWj0-5" vertex="1">
<mxGeometry y="26" width="401" height="140" as="geometry"/>
<mxCell id="dmd0HlDYcxYugIlahWj0-6" value="+ ServiceName: string property&#10;- serviceName: string&#10;- running: bool&#10;- service: IService&#10;- ServiceLogPipeName: string property&#10;- moduleName: string&#10;- assemblyName: string&#10;- logStreamListeners: ConcurrentStack&lt;NamedPipeServerStream&gt;&#10;- logUpdateTask: Task&#10;- acceptingTask: Task&#10;- stopToken: CancellationTokenSource&#10;- configurables: IReadOnlyDictionary&lt;string, IServiceConfigurable&gt;&#10;+ ServiceStateChangeEvent: event EventHandler&lt;bool&gt;" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="dmd0HlDYcxYugIlahWj0-5" vertex="1">
<mxGeometry y="26" width="510" height="194" as="geometry"/>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-7" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;sketch=1;" parent="dmd0HlDYcxYugIlahWj0-5" vertex="1">
<mxGeometry y="166" width="401" height="8" as="geometry"/>
<mxCell id="dmd0HlDYcxYugIlahWj0-7" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="dmd0HlDYcxYugIlahWj0-5" vertex="1">
<mxGeometry y="220" width="510" height="8" as="geometry"/>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-8" value="+ Start(): void&#10;+ Stop(): void&#10;+ GetConfigurableOptions(): ISet&lt;string&gt;&#10;+ SetConfigurableValue(configurationName: string, value: string): bool&#10;+ GetConfigurableValue(configurationName: string): string&#10;+ GetServiceState(): ServiceState&#10;+ getModuleName(): string //Implemented as property&#10;+ GetassemblyName(): string&#10;+ SetServiceName(name: string): void // Implemented as property&#10;+ GetServiceName(): string // Implemented as property&#10;+ GetServiceConsoleStream(): Stream // Implemented as property&#10;- OnServiceStateChange(curr: ServiceState, prev: ServiceState): void&#10;# Dispose(disposing: bool): void&#10;+ Dispose(): voide" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="dmd0HlDYcxYugIlahWj0-5" vertex="1">
<mxGeometry y="174" width="401" height="212" as="geometry"/>
<mxCell id="dmd0HlDYcxYugIlahWj0-8" value="+ Start(): void&#10;+ Stop(): void&#10;+ ExecuteCommand(command: string): void&#10;+ GetConfigurableOptions(): ISet&lt;string&gt;&#10;+ SetConfigurableValue(configurationName: string, value: string): bool&#10;+ GetConfigurableValue(configurationName: string): string&#10;+ GetServiceState(): bool&#10;+ GetModuleName(): string &#10;+ GetassemblyName(): string&#10;- OnServiceStateChange(sender: object, running: bool): void&#10;- AcceptLogConnections(): Task&#10;- BroadcastLog(): Task" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="dmd0HlDYcxYugIlahWj0-5" vertex="1">
<mxGeometry y="228" width="510" height="182" as="geometry"/>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-9" value="«interface»&lt;br&gt;&lt;span&gt;IDisposable&lt;/span&gt;" style="html=1;sketch=1;" parent="1" vertex="1">
<mxGeometry x="1250" y="980" width="111" height="50" as="geometry"/>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-15" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="1" source="dmd0HlDYcxYugIlahWj0-11" target="dmd0HlDYcxYugIlahWj0-5" edge="1">
<mxCell id="dmd0HlDYcxYugIlahWj0-15" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;" parent="1" source="dmd0HlDYcxYugIlahWj0-11" target="dmd0HlDYcxYugIlahWj0-5" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="wwlaSBDwwZOn0hO83bWU-6" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=block;endFill=0;sketch=1;" parent="1" source="dmd0HlDYcxYugIlahWj0-11" target="wwlaSBDwwZOn0hO83bWU-2" edge="1">
<mxCell id="wwlaSBDwwZOn0hO83bWU-6" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=block;endFill=0;" parent="1" source="dmd0HlDYcxYugIlahWj0-11" target="wwlaSBDwwZOn0hO83bWU-2" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="wwlaSBDwwZOn0hO83bWU-18" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="1" source="dmd0HlDYcxYugIlahWj0-11" target="wwlaSBDwwZOn0hO83bWU-12" edge="1">
<mxCell id="fdKXkHfjRXYybK0fejAG-2" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;" parent="1" source="dmd0HlDYcxYugIlahWj0-11" target="zFFzFwxISwJASp9ezwbr-1" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="fdKXkHfjRXYybK0fejAG-2" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="1" source="dmd0HlDYcxYugIlahWj0-11" target="zFFzFwxISwJASp9ezwbr-1" edge="1">
<mxCell id="SI3d9EEbteElKQB4Ic5T-1" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;" parent="1" source="dmd0HlDYcxYugIlahWj0-11" target="fdKXkHfjRXYybK0fejAG-9" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="SI3d9EEbteElKQB4Ic5T-1" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="1" source="dmd0HlDYcxYugIlahWj0-11" target="fdKXkHfjRXYybK0fejAG-9" edge="1">
<mxCell id="29" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;" parent="1" source="dmd0HlDYcxYugIlahWj0-11" target="24" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="SI3d9EEbteElKQB4Ic5T-14" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="1" source="dmd0HlDYcxYugIlahWj0-11" target="SI3d9EEbteElKQB4Ic5T-10" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-11" value="GameServiceManager" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;sketch=1;" parent="1" vertex="1">
<mxGeometry x="669.5" y="640" width="561" height="300" as="geometry">
<mxCell id="dmd0HlDYcxYugIlahWj0-11" value="ServiceManager" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;" parent="1" vertex="1">
<mxGeometry x="669.5" y="640" width="561" height="360" as="geometry">
<mxRectangle x="25" y="490" width="120" height="26" as="alternateBounds"/>
</mxGeometry>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-12" value="- services: IPersistentDictionary&lt;GameServiceInfo&gt;&#10;- modules: IReadOnlyPersistentDictionary&lt;string, IReadOnlyDictionary&lt;string, IGameServiceModule&gt;&gt;&#10;- running: IDictionary&lt;string, GameServiceInfo&gt;" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="dmd0HlDYcxYugIlahWj0-11" vertex="1">
<mxGeometry y="26" width="561" height="54" as="geometry"/>
<mxCell id="dmd0HlDYcxYugIlahWj0-12" value="- managerMonitor: IServiceManagerMonitor&#10;- running: ConcurrentDictionary&lt;string, ServiceDescriptor&gt;&#10;- services: IPersistent&lt;GameServiceInfo&gt;&#10;- modules: IReadOnlyPersistent&lt;IReadOnlyDictionary&lt;string, IServiceModule&gt;&gt;" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="dmd0HlDYcxYugIlahWj0-11" vertex="1">
<mxGeometry y="26" width="561" height="74" as="geometry"/>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-13" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;sketch=1;" parent="dmd0HlDYcxYugIlahWj0-11" vertex="1">
<mxGeometry y="80" width="561" height="8" as="geometry"/>
<mxCell id="dmd0HlDYcxYugIlahWj0-13" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="dmd0HlDYcxYugIlahWj0-11" vertex="1">
<mxGeometry y="100" width="561" height="8" as="geometry"/>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-14" value="+ CreateService(serviceName: string, assemblyName: string, moduleName: string): void&#10;+ DeleteService(serviceName: string): void&#10;+ GetModuleNames(): IEnumerable&lt;string&gt;&#10;+ GetServiceNames(): IEnumerable&lt;string&gt;&#10;+ GetServiceOptions(serviceName: string): IEnumerable&lt;string&gt;&#10;+ GetServiceOptionValue(serviceName: string, optionName: string): IEnumerable&lt;string&gt;&#10;+ SetServiceOptionValue(serviceName: string, optionName: string, string: value): bool&#10;+ GetServiceState(serviceName: string): ServiceState&#10;+ StartService(serviceName: string): void&#10;+ StopService(serviceName: string): void&#10;+ ExecuteCommand(serviceName: string, command: string): void&#10;+ GetServiceConsoleStream(): Stream&#10;+ ExecuteServiceAction(serviceAction: serviceAction): void" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="dmd0HlDYcxYugIlahWj0-11" vertex="1">
<mxGeometry y="88" width="561" height="212" as="geometry"/>
<mxCell id="dmd0HlDYcxYugIlahWj0-14" value="+ CreateService(serviceName: string, assemblyName: string, moduleName: string): void&#10;+ DeleteService(serviceName: string): void&#10;+ GetModuleNames(): IEnumerable&lt;string&gt;&#10;+ GetServiceNames(): IEnumerable&lt;string&gt;&#10;+ GetRunningServiceNames(): IEnumerable&lt;string&gt;&#10;- GetServiceOptions(serviceName: string): IEnumerable&lt;string&gt;&#10;- GetServiceOptionValue(serviceName: string, optionName: string): IEnumerable&lt;string&gt;&#10;+ GetOptions(): IReadOnlyDictionary&lt;string, IReadOnlyDictionary&lt;string, string&gt;&gt;&#10;+ SetServiceOptionValue(serviceName: string, optionName: string, string: value): bool&#10;+ StartService(serviceName: string): void&#10;+ StopService(serviceName: string): void&#10;+ ExecuteCommand(serviceName: string, command: string): void&#10;- GetServiceLogPipeName(serviceName: string): string&#10;+ GetLogPipeNames(): IReadOnlyDictionary&lt;string, string&gt;&#10;- OnServiceStateChange(sender: object, state: bool): void" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="dmd0HlDYcxYugIlahWj0-11" vertex="1">
<mxGeometry y="108" width="561" height="252" as="geometry"/>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-38" value="&lt;&lt;Interface&gt;&gt;&#10;ITextCommand" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;sketch=1;" parent="1" vertex="1">
<mxGeometry x="1285" y="138" width="181" height="114" as="geometry"/>
<mxCell id="wwlaSBDwwZOn0hO83bWU-9" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=block;endFill=0;" parent="1" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1679.5" y="202" as="sourcePoint"/>
</mxGeometry>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-40" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;sketch=1;" parent="dmd0HlDYcxYugIlahWj0-38" vertex="1">
<mxGeometry y="40" width="181" height="8" as="geometry"/>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-41" value="+ GetPrefix(): string&#10;+ Validate(input: string): bool&#10;+ Execute(input: string): void&#10;+ Help(): string" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="dmd0HlDYcxYugIlahWj0-38" vertex="1">
<mxGeometry y="48" width="181" height="66" as="geometry"/>
</mxCell>
<mxCell id="2br9O0FZKGLhbr8u3XJU-1" value="ConsoleView" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;sketch=1;" parent="1" vertex="1">
<mxGeometry x="850" y="30" width="200" height="104" as="geometry"/>
</mxCell>
<mxCell id="2br9O0FZKGLhbr8u3XJU-2" value="- mainController: ITextCommand&#10;- mainPresenter: ITextOutput" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="2br9O0FZKGLhbr8u3XJU-1" vertex="1">
<mxGeometry y="26" width="200" height="44" as="geometry"/>
</mxCell>
<mxCell id="2br9O0FZKGLhbr8u3XJU-3" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;sketch=1;" parent="2br9O0FZKGLhbr8u3XJU-1" vertex="1">
<mxGeometry y="70" width="200" height="8" as="geometry"/>
</mxCell>
<mxCell id="2br9O0FZKGLhbr8u3XJU-4" value="+ Interact()" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="2br9O0FZKGLhbr8u3XJU-1" vertex="1">
<mxGeometry y="78" width="200" height="26" as="geometry"/>
</mxCell>
<mxCell id="wwlaSBDwwZOn0hO83bWU-9" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=block;endFill=0;sketch=1;" parent="1" source="qpeZJq-dxPH0P0VpmRa_-7" target="dmd0HlDYcxYugIlahWj0-38" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="wwlaSBDwwZOn0hO83bWU-10" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="1" source="qpeZJq-dxPH0P0VpmRa_-7" target="dmd0HlDYcxYugIlahWj0-38" edge="1">
<mxCell id="wwlaSBDwwZOn0hO83bWU-10" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;dashed=1;endArrow=open;endFill=0;" parent="1" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="950" y="162" as="targetPoint"/>
<mxPoint x="1814.5" y="172" as="sourcePoint"/>
</mxGeometry>
</mxCell>
<mxCell id="qpeZJq-dxPH0P0VpmRa_-7" value="MainController" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;sketch=1;" parent="1" vertex="1">
<mxGeometry x="1560" y="247" width="270" height="60" as="geometry"/>
</mxCell>
<mxCell id="qpeZJq-dxPH0P0VpmRa_-8" value="+ commands: Dictionary&lt;string, ITextCommand&gt;" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="qpeZJq-dxPH0P0VpmRa_-7" vertex="1">
<mxGeometry y="26" width="270" height="26" as="geometry"/>
</mxCell>
<mxCell id="qpeZJq-dxPH0P0VpmRa_-9" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;sketch=1;" parent="qpeZJq-dxPH0P0VpmRa_-7" vertex="1">
<mxGeometry y="52" width="270" height="8" as="geometry"/>
</mxCell>
<mxCell id="K1k0_LUP-qlT_3mlrptx-1" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="1" source="2br9O0FZKGLhbr8u3XJU-2" target="dmd0HlDYcxYugIlahWj0-38" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="wwlaSBDwwZOn0hO83bWU-7" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="1" source="K1k0_LUP-qlT_3mlrptx-2" target="wwlaSBDwwZOn0hO83bWU-2" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="V3nv0dmUtDNsDw_gxP-z-3" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="1" source="K1k0_LUP-qlT_3mlrptx-2" target="dmd0HlDYcxYugIlahWj0-38" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="K1k0_LUP-qlT_3mlrptx-2" value="ConsoleGameServiceController" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;sketch=1;" parent="1" vertex="1">
<mxGeometry x="1230.5" y="318" width="290" height="60" as="geometry"/>
</mxCell>
<mxCell id="K1k0_LUP-qlT_3mlrptx-3" value="- service: IGameServiceActionExecuter" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="K1k0_LUP-qlT_3mlrptx-2" vertex="1">
<mxGeometry y="26" width="290" height="26" as="geometry"/>
</mxCell>
<mxCell id="K1k0_LUP-qlT_3mlrptx-4" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;sketch=1;" parent="K1k0_LUP-qlT_3mlrptx-2" vertex="1">
<mxGeometry y="52" width="290" height="8" as="geometry"/>
</mxCell>
<mxCell id="wwlaSBDwwZOn0hO83bWU-16" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="1" source="wwlaSBDwwZOn0hO83bWU-2" target="wwlaSBDwwZOn0hO83bWU-12" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="wwlaSBDwwZOn0hO83bWU-2" value="&lt;&lt;Interface&gt;&gt;&#10;IGameServiceActionExecuter" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;sketch=1;" parent="1" vertex="1">
<mxGeometry x="1275" y="480" width="250" height="74" as="geometry"/>
</mxCell>
<mxCell id="wwlaSBDwwZOn0hO83bWU-4" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;sketch=1;" parent="wwlaSBDwwZOn0hO83bWU-2" vertex="1">
<mxGeometry y="40" width="250" height="8" as="geometry"/>
</mxCell>
<mxCell id="wwlaSBDwwZOn0hO83bWU-5" value="+ ExecuteAction(action: ServiceAction): void" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="wwlaSBDwwZOn0hO83bWU-2" vertex="1">
<mxGeometry y="48" width="250" height="26" as="geometry"/>
</mxCell>
<mxCell id="SI3d9EEbteElKQB4Ic5T-15" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="1" source="wwlaSBDwwZOn0hO83bWU-12" target="SI3d9EEbteElKQB4Ic5T-10" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="wwlaSBDwwZOn0hO83bWU-12" value="&lt;&lt;DS&gt;&gt;&#10;GameServiceAction" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;sketch=1;" parent="1" vertex="1">
<mxGeometry x="1440" y="606" width="161" height="128" as="geometry"/>
</mxCell>
<mxCell id="wwlaSBDwwZOn0hO83bWU-13" value="+ AssemblyName: string&#10;+ ModuleName: string&#10;+ ServiceName: string&#10;+ Module: IModule&#10;+ Action: ServiceActions" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="wwlaSBDwwZOn0hO83bWU-12" vertex="1">
<mxGeometry y="40" width="161" height="80" as="geometry"/>
</mxCell>
<mxCell id="wwlaSBDwwZOn0hO83bWU-14" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;sketch=1;" parent="wwlaSBDwwZOn0hO83bWU-12" vertex="1">
<mxGeometry y="120" width="161" height="8" as="geometry"/>
</mxCell>
<mxCell id="wwlaSBDwwZOn0hO83bWU-19" value="&lt;&lt;Interface&gt;&gt;&#10;ITextOutput" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;sketch=1;" parent="1" vertex="1">
<mxGeometry x="340" y="137" width="310" height="110" as="geometry"/>
</mxCell>
<mxCell id="wwlaSBDwwZOn0hO83bWU-21" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;sketch=1;" parent="wwlaSBDwwZOn0hO83bWU-19" vertex="1">
<mxGeometry y="40" width="310" height="8" as="geometry"/>
</mxCell>
<mxCell id="wwlaSBDwwZOn0hO83bWU-22" value="+ GetPresenterName(): string&#10;+ Event TextOutput(output: string): void&#10;+ OnOutputReceived(sender: object, output: string): void" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="wwlaSBDwwZOn0hO83bWU-19" vertex="1">
<mxGeometry y="48" width="310" height="62" as="geometry"/>
</mxCell>
<mxCell id="V3nv0dmUtDNsDw_gxP-z-2" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="1" source="2br9O0FZKGLhbr8u3XJU-2" target="wwlaSBDwwZOn0hO83bWU-19" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="fdKXkHfjRXYybK0fejAG-3" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="1" source="HUSvFZX5SimreebZp30a-5" target="wwlaSBDwwZOn0hO83bWU-19" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="fdKXkHfjRXYybK0fejAG-4" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=block;endFill=0;sketch=1;" parent="1" source="HUSvFZX5SimreebZp30a-5" target="wwlaSBDwwZOn0hO83bWU-19" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="HUSvFZX5SimreebZp30a-5" value="MainPresenter" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;sketch=1;" parent="1" vertex="1">
<mxGeometry x="20" y="214" width="270" height="60" as="geometry"/>
</mxCell>
<mxCell id="HUSvFZX5SimreebZp30a-6" value="- textPresenters: Dictionary&lt;string, ITextOutput&gt;" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="HUSvFZX5SimreebZp30a-5" vertex="1">
<mxGeometry y="26" width="270" height="26" as="geometry"/>
</mxCell>
<mxCell id="HUSvFZX5SimreebZp30a-7" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;sketch=1;" parent="HUSvFZX5SimreebZp30a-5" vertex="1">
<mxGeometry y="52" width="270" height="8" as="geometry"/>
</mxCell>
<mxCell id="HUSvFZX5SimreebZp30a-16" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=block;endFill=0;sketch=1;" parent="1" source="HUSvFZX5SimreebZp30a-11" target="wwlaSBDwwZOn0hO83bWU-19" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="fdKXkHfjRXYybK0fejAG-1" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=block;endFill=0;sketch=1;" parent="1" source="HUSvFZX5SimreebZp30a-11" target="zFFzFwxISwJASp9ezwbr-1" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="HUSvFZX5SimreebZp30a-11" value="ServicePresenter" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;sketch=1;" parent="1" vertex="1">
<mxGeometry x="415" y="344" width="160" height="34" as="geometry"/>
</mxCell>
<mxCell id="HUSvFZX5SimreebZp30a-13" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;sketch=1;" parent="HUSvFZX5SimreebZp30a-11" vertex="1">
<mxGeometry y="26" width="160" height="8" as="geometry"/>
</mxCell>
<mxCell id="SI3d9EEbteElKQB4Ic5T-5" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="1" source="zFFzFwxISwJASp9ezwbr-1" target="fdKXkHfjRXYybK0fejAG-9" edge="1">
<mxCell id="K1k0_LUP-qlT_3mlrptx-1" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;" parent="1" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="495" y="660"/>
<mxPoint x="407" y="660"/>
</Array>
<mxPoint x="1414.5" y="133" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="zFFzFwxISwJASp9ezwbr-1" value="&lt;&lt;Interface&gt;&gt;&#10;IGameServiceOutputMonitor" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;sketch=1;" parent="1" vertex="1">
<mxGeometry x="355" y="464" width="280" height="74" as="geometry"/>
<mxCell id="30" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;" parent="1" source="wwlaSBDwwZOn0hO83bWU-2" target="24" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="zFFzFwxISwJASp9ezwbr-3" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;sketch=1;" parent="zFFzFwxISwJASp9ezwbr-1" vertex="1">
<mxGeometry y="40" width="280" height="8" as="geometry"/>
<mxCell id="wwlaSBDwwZOn0hO83bWU-2" value="&lt;&lt;Interface&gt;&gt;&#10;IServiceManagerActionExecuter" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;" parent="1" vertex="1">
<mxGeometry x="1386" y="406" width="305" height="74" as="geometry"/>
</mxCell>
<mxCell id="zFFzFwxISwJASp9ezwbr-4" value="+ ServicesChanged(ServicesResult results): void" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="zFFzFwxISwJASp9ezwbr-1" vertex="1">
<mxGeometry y="48" width="280" height="26" as="geometry"/>
<mxCell id="wwlaSBDwwZOn0hO83bWU-4" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="wwlaSBDwwZOn0hO83bWU-2" vertex="1">
<mxGeometry y="40" width="305" height="8" as="geometry"/>
</mxCell>
<mxCell id="fdKXkHfjRXYybK0fejAG-9" value="&lt;&lt;DS&gt;&gt;&#10;GameServicesActionResults" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;sketch=1;" parent="1" vertex="1">
<mxGeometry x="225" y="830" width="365" height="138" as="geometry"/>
<mxCell id="wwlaSBDwwZOn0hO83bWU-5" value="+ ExecuteAction(action: ServiceManagerAction): void" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="wwlaSBDwwZOn0hO83bWU-2" vertex="1">
<mxGeometry y="48" width="305" height="26" as="geometry"/>
</mxCell>
<mxCell id="fdKXkHfjRXYybK0fejAG-10" value="+ Services: ICollection&lt;string&gt;&#10;+ Running: ICollection&lt;string&gt;&#10;+ Errors: ICollection&lt;string&gt;&#10;+ Consoles: IDictionary&lt;string, Stream&gt;&#10;+ ServiceOptions: IDictionary&lt;string, IDictionary&lt;string, string&gt;&gt;" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="fdKXkHfjRXYybK0fejAG-9" vertex="1">
<mxGeometry y="40" width="365" height="90" as="geometry"/>
<mxCell id="V3nv0dmUtDNsDw_gxP-z-2" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;" parent="1" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="850" y="78" as="sourcePoint"/>
</mxGeometry>
</mxCell>
<mxCell id="fdKXkHfjRXYybK0fejAG-11" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;sketch=1;" parent="fdKXkHfjRXYybK0fejAG-9" vertex="1">
<mxGeometry y="130" width="365" height="8" as="geometry"/>
<mxCell id="fdKXkHfjRXYybK0fejAG-3" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;dashed=1;endArrow=open;endFill=0;" parent="1" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="340" y="192" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="SI3d9EEbteElKQB4Ic5T-10" value="&lt;&lt;Enum&gt;&gt;&#10;GameServiceActions" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;sketch=1;" parent="1" vertex="1">
<mxGeometry x="882.5" y="408" width="160" height="198" as="geometry"/>
<mxCell id="fdKXkHfjRXYybK0fejAG-4" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=block;endFill=0;" parent="1" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="340" y="192" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="SI3d9EEbteElKQB4Ic5T-11" value="+ Start&#10;+ Stop&#10;+ AddModule&#10;+ RemoveModule&#10;+ CreateService&#10;+ DeleteService&#10;+ ExecuteCommand&#10;+ SetServiceOption&#10;+ RestoreService&#10;+ View" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;sketch=1;" parent="SI3d9EEbteElKQB4Ic5T-10" vertex="1">
<mxGeometry y="40" width="160" height="150" as="geometry"/>
<mxCell id="fdKXkHfjRXYybK0fejAG-1" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=block;endFill=0;" parent="1" source="15" target="zFFzFwxISwJASp9ezwbr-1" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="495" y="378" as="sourcePoint"/>
</mxGeometry>
</mxCell>
<mxCell id="SI3d9EEbteElKQB4Ic5T-12" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;sketch=1;" parent="SI3d9EEbteElKQB4Ic5T-10" vertex="1">
<mxGeometry y="190" width="160" height="8" as="geometry"/>
<mxCell id="SI3d9EEbteElKQB4Ic5T-5" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;" parent="1" source="zFFzFwxISwJASp9ezwbr-1" target="fdKXkHfjRXYybK0fejAG-9" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="270" y="610" as="sourcePoint"/>
</mxGeometry>
</mxCell>
<mxCell id="5" value="Use" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="SI3d9EEbteElKQB4Ic5T-5" vertex="1" connectable="0">
<mxGeometry x="0.1309" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="zFFzFwxISwJASp9ezwbr-1" value="&lt;&lt;Interface&gt;&gt;&#10;IServiceManagerMonitor" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;" parent="1" vertex="1">
<mxGeometry x="278" y="480" width="295" height="74" as="geometry"/>
</mxCell>
<mxCell id="zFFzFwxISwJASp9ezwbr-3" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="zFFzFwxISwJASp9ezwbr-1" vertex="1">
<mxGeometry y="40" width="295" height="8" as="geometry"/>
</mxCell>
<mxCell id="zFFzFwxISwJASp9ezwbr-4" value="+ Present(state: ServiceManagerState): void" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="zFFzFwxISwJASp9ezwbr-1" vertex="1">
<mxGeometry y="48" width="295" height="26" as="geometry"/>
</mxCell>
<mxCell id="fdKXkHfjRXYybK0fejAG-9" value="&lt;&lt;DS&gt;&gt;&#10;ServiceManagerState" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;" parent="1" vertex="1">
<mxGeometry x="20" y="860" width="485" height="218" as="geometry"/>
</mxCell>
<mxCell id="fdKXkHfjRXYybK0fejAG-10" value="+ delta: bool&#10;+ subtract: bool&#10;+ services: ICollection&lt;string&gt;&#10;+ running: ICollection&lt;string&gt;&#10;+ modules: ICollection&lt;string&gt;&#10;+ logs: IReadOnlyDictionary&lt;string, string&gt;&#10;+ serviceOptions: IReadOnlyDictionary&lt;string, IReadOnlyDictionary&lt;string, string&gt;&gt;" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="fdKXkHfjRXYybK0fejAG-9" vertex="1">
<mxGeometry y="40" width="485" height="170" as="geometry"/>
</mxCell>
<mxCell id="fdKXkHfjRXYybK0fejAG-11" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="fdKXkHfjRXYybK0fejAG-9" vertex="1">
<mxGeometry y="210" width="485" height="8" as="geometry"/>
</mxCell>
<mxCell id="SI3d9EEbteElKQB4Ic5T-10" value="&lt;&lt;Enum&gt;&gt;&#10;ServiceManagerAction.Type" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;" parent="1" vertex="1">
<mxGeometry x="1336" y="610" width="180" height="168" as="geometry"/>
</mxCell>
<mxCell id="SI3d9EEbteElKQB4Ic5T-11" value="+ Start&#10;+ Stop&#10;+ CreateService&#10;+ DeleteService&#10;+ ExecuteCommand&#10;+ SetServiceOption&#10;+ View" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="SI3d9EEbteElKQB4Ic5T-10" vertex="1">
<mxGeometry y="40" width="180" height="120" as="geometry"/>
</mxCell>
<mxCell id="SI3d9EEbteElKQB4Ic5T-12" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="SI3d9EEbteElKQB4Ic5T-10" vertex="1">
<mxGeometry y="160" width="180" height="8" as="geometry"/>
</mxCell>
<mxCell id="6" value="IPCMediator" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;" parent="1" vertex="1">
<mxGeometry x="685" y="122" width="520" height="320" as="geometry"/>
</mxCell>
<mxCell id="7" value="- CONNECT_TIMEOUT: int&#10;- name: string&#10;- pipesAndTasks: ConcurrentDictionary&lt;string, (NamedPipeServerStream, Task)&gt;&#10;+ RequestQueue: default property of BlockingCollection&lt;(string, CommunicableType, Byte[])&gt;&#10;- active: bool&#10;+ IsRunning: bool property&#10;- acceptTask: Task&#10;- stopToken: CancellationTokenSource" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="6" vertex="1">
<mxGeometry y="26" width="520" height="124" as="geometry"/>
</mxCell>
<mxCell id="8" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="6" vertex="1">
<mxGeometry y="150" width="520" height="8" as="geometry"/>
</mxCell>
<mxCell id="9" value="+ Open(): void&#10;+ Close(): void&#10;+ ReplyAll(type: CommunicableType, data: byte[]): Task&#10;+ Reply(identifier: string, type: CommunicableType, data: byte[]): Task&#10;+ InitiateDisconnect(identifier: string, reason: string): Task&#10;+ InitiateDisconnectAll(reason: string): Task&#10;- AcceptConnections(): Task&#10;- OnConnection(pipe: NamedPipeServerStream): Task&#10;- Listen(identifier: string, pipe: NamedPipeServerStream): Task" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="6" vertex="1">
<mxGeometry y="158" width="520" height="162" as="geometry"/>
</mxCell>
<mxCell id="20" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;" parent="1" source="10" target="6" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="21" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;" parent="1" source="10" target="wwlaSBDwwZOn0hO83bWU-2" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="10" value="IPCController" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;" parent="1" vertex="1">
<mxGeometry x="1378.5" y="230" width="320" height="104" as="geometry"/>
</mxCell>
<mxCell id="11" value="- mediator: IPCMediator&#10;- serviceExecuter: IServiceManagerActionExecuter" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="10" vertex="1">
<mxGeometry y="26" width="320" height="44" as="geometry"/>
</mxCell>
<mxCell id="12" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="10" vertex="1">
<mxGeometry y="70" width="320" height="8" as="geometry"/>
</mxCell>
<mxCell id="13" value="+ Process(): void" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="10" vertex="1">
<mxGeometry y="78" width="320" height="26" as="geometry"/>
</mxCell>
<mxCell id="19" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;endArrow=open;endFill=0;dashed=1;" parent="1" source="15" target="6" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="15" value="IPCPresenter" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;" parent="1" vertex="1">
<mxGeometry x="300" y="239" width="250" height="86" as="geometry"/>
</mxCell>
<mxCell id="16" value="- mediator: IPCMediator" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="15" vertex="1">
<mxGeometry y="26" width="250" height="26" as="geometry"/>
</mxCell>
<mxCell id="17" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="15" vertex="1">
<mxGeometry y="52" width="250" height="8" as="geometry"/>
</mxCell>
<mxCell id="18" value="+ Present(state: ServiceManagerState): void" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="15" vertex="1">
<mxGeometry y="60" width="250" height="26" as="geometry"/>
</mxCell>
<mxCell id="28" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;" parent="1" source="24" target="SI3d9EEbteElKQB4Ic5T-10" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="24" value="&lt;&lt;DS&gt;&gt;&#10;ServiceManagerAction" style="swimlane;fontStyle=1;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=40;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;" parent="1" vertex="1">
<mxGeometry x="1096" y="460" width="220" height="132" as="geometry"/>
</mxCell>
<mxCell id="25" value="+ assemblyName: string&#10;+ moduleName: string&#10;+ serviceName: string&#10;+ module: string&#10;+ action: ServiceManagerAction.Type" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="24" vertex="1">
<mxGeometry y="40" width="220" height="84" as="geometry"/>
</mxCell>
<mxCell id="26" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="24" vertex="1">
<mxGeometry y="124" width="220" height="8" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="gj0qHRc3eh050ABAey3g" name="Data-Flow">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGraphModel dx="1216" dy="740" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGraphModel dx="1024" dy="592" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
&#xa; &#xa; &#xa;&#xa;&#xa;
<root>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-0"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-1" parent="jVG6p58vlRYGO9X4wXeX-0"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-21" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.3333333333333333;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;endArrow=open;endFill=0;fillColor=#1ba1e2;strokeColor=#006EAF;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-2" target="jVG6p58vlRYGO9X4wXeX-3" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-21" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.3333333333333333;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;endArrow=open;endFill=0;fillColor=#1ba1e2;strokeColor=#006EAF;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-2" target="jVG6p58vlRYGO9X4wXeX-3" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-2" value="Actor" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-2" value="Actor" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="10" y="300" width="30" height="60" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-12" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=open;endFill=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;fillColor=#1ba1e2;strokeColor=#006EAF;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-3" target="jVG6p58vlRYGO9X4wXeX-4" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-12" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=open;endFill=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;fillColor=#1ba1e2;strokeColor=#006EAF;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-3" target="jVG6p58vlRYGO9X4wXeX-4" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-22" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.25;exitDx=0;exitDy=0;entryX=0.75;entryY=0.1;entryDx=0;entryDy=0;entryPerimeter=0;endArrow=open;endFill=0;fillColor=#1ba1e2;strokeColor=#006EAF;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-3" target="jVG6p58vlRYGO9X4wXeX-2" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-22" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.25;exitDx=0;exitDy=0;entryX=0.75;entryY=0.1;entryDx=0;entryDy=0;entryPerimeter=0;endArrow=open;endFill=0;fillColor=#1ba1e2;strokeColor=#006EAF;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-3" target="jVG6p58vlRYGO9X4wXeX-2" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="345FJoVc2gbAayMsQlD7-0" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.25;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-3" target="jVG6p58vlRYGO9X4wXeX-4" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="345FJoVc2gbAayMsQlD7-0" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.25;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-3" target="jVG6p58vlRYGO9X4wXeX-4" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="345FJoVc2gbAayMsQlD7-6" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.25;exitY=0;exitDx=0;exitDy=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-3" target="28FAlPysTx9DMYvLwa-2-7" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="345FJoVc2gbAayMsQlD7-6" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.25;exitY=0;exitDx=0;exitDy=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-3" target="28FAlPysTx9DMYvLwa-2-7" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-3" value="Console View" style="whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-3" value="Console View" style="whiteSpace=wrap;html=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="80" y="300" width="120" height="60" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-13" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=open;endFill=0;fillColor=#1ba1e2;strokeColor=#006EAF;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-4" target="jVG6p58vlRYGO9X4wXeX-5" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-13" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=open;endFill=0;fillColor=#1ba1e2;strokeColor=#006EAF;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-4" target="jVG6p58vlRYGO9X4wXeX-5" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="345FJoVc2gbAayMsQlD7-2" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.75;exitDx=0;exitDy=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-4" target="jVG6p58vlRYGO9X4wXeX-5" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="345FJoVc2gbAayMsQlD7-2" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.75;exitDx=0;exitDy=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-4" target="jVG6p58vlRYGO9X4wXeX-5" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-4" value="string command (request)" style="whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-4" value="string command (request)" style="whiteSpace=wrap;html=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="260" y="482.5" width="120" height="60" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-8" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;dashed=1;endArrow=block;endFill=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-5" target="jVG6p58vlRYGO9X4wXeX-7" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-8" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;dashed=1;endArrow=block;endFill=0;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-5" target="jVG6p58vlRYGO9X4wXeX-7" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-3" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.75;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;endArrow=open;endFill=0;dashed=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-5" target="jVG6p58vlRYGO9X4wXeX-7" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-3" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.75;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;endArrow=open;endFill=0;dashed=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-5" target="jVG6p58vlRYGO9X4wXeX-7" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-18" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=open;endFill=0;strokeColor=#006EAF;fillColor=#1ba1e2;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-5" target="jVG6p58vlRYGO9X4wXeX-9" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-18" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=open;endFill=0;strokeColor=#006EAF;fillColor=#1ba1e2;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-5" target="jVG6p58vlRYGO9X4wXeX-9" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-5" value="MainController" style="whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-5" value="MainController" style="whiteSpace=wrap;html=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="420" y="482.5" width="120" height="60" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-6" value="http://www.plainionist.net/Implementing-Clean-Architecture-Controller-Presenter/" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-6" value="http://www.plainionist.net/Implementing-Clean-Architecture-Controller-Presenter/" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry y="840" width="480" height="20" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-7" value="&amp;lt;&amp;lt;Interface&amp;gt;&amp;gt;&lt;br&gt;ICommand" style="whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-7" value="&amp;lt;&amp;lt;Interface&amp;gt;&amp;gt;&lt;br&gt;ICommand" style="whiteSpace=wrap;html=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="420" y="372.5" width="120" height="60" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-10" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;dashed=1;endArrow=block;endFill=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-9" target="jVG6p58vlRYGO9X4wXeX-7" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-10" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;dashed=1;endArrow=block;endFill=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-9" target="jVG6p58vlRYGO9X4wXeX-7" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-8" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.25;entryDx=0;entryDy=0;endArrow=open;endFill=0;strokeColor=#006EAF;fillColor=#1ba1e2;exitX=1;exitY=0.5;exitDx=0;exitDy=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-9" target="UY-EM7-1ECCvWtENr50b-1" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-8" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.25;entryDx=0;entryDy=0;endArrow=open;endFill=0;strokeColor=#006EAF;fillColor=#1ba1e2;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-9" target="UY-EM7-1ECCvWtENr50b-1" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxPoint x="809.9999999999998" y="512.5000000000002" as="sourcePoint"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxGeometry>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="345FJoVc2gbAayMsQlD7-3" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.25;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-9" target="UY-EM7-1ECCvWtENr50b-2" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="345FJoVc2gbAayMsQlD7-3" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.25;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-9" target="UY-EM7-1ECCvWtENr50b-2" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="345FJoVc2gbAayMsQlD7-4" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.75;exitDx=0;exitDy=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-9" target="UY-EM7-1ECCvWtENr50b-1" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="345FJoVc2gbAayMsQlD7-4" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.75;exitDx=0;exitDy=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;" parent="jVG6p58vlRYGO9X4wXeX-1" source="jVG6p58vlRYGO9X4wXeX-9" target="UY-EM7-1ECCvWtENr50b-1" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-9" value="ServiceController" style="whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="jVG6p58vlRYGO9X4wXeX-9" value="ServiceController" style="whiteSpace=wrap;html=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="575" y="482.5" width="120" height="60" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-4" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=none;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-1" target="UY-EM7-1ECCvWtENr50b-2" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-4" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=none;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-1" target="UY-EM7-1ECCvWtENr50b-2" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-5" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;dashed=1;endArrow=block;endFill=0;strokeColor=#f0f0f0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-1" target="UY-EM7-1ECCvWtENr50b-2" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-5" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;dashed=1;endArrow=block;endFill=0;strokeColor=#f0f0f0;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-1" target="UY-EM7-1ECCvWtENr50b-2" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-7" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=1.008;entryY=0.625;entryDx=0;entryDy=0;entryPerimeter=0;endArrow=open;endFill=0;strokeColor=#f0f0f0;dashed=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-1" target="UY-EM7-1ECCvWtENr50b-1" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-7" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=1.008;entryY=0.625;entryDx=0;entryDy=0;entryPerimeter=0;endArrow=open;endFill=0;strokeColor=#f0f0f0;dashed=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-1" target="UY-EM7-1ECCvWtENr50b-1" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<Array as="points">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxPoint x="960" y="338"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxPoint x="960" y="580"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</Array>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxGeometry>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-16" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;endArrow=open;endFill=0;dashed=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-1" target="UY-EM7-1ECCvWtENr50b-11" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-16" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;endArrow=open;endFill=0;dashed=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-1" target="UY-EM7-1ECCvWtENr50b-11" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="345FJoVc2gbAayMsQlD7-7" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.25;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-1" target="UY-EM7-1ECCvWtENr50b-10" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="345FJoVc2gbAayMsQlD7-7" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.25;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-1" target="UY-EM7-1ECCvWtENr50b-10" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<Array as="points">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxPoint x="960" y="338"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxPoint x="960" y="83"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</Array>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxGeometry>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-1" value="ServiceManager" style="whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-1" value="ServiceManager" style="whiteSpace=wrap;html=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="800" y="307.5" width="120" height="60" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-9" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;endArrow=open;endFill=0;fillColor=#1ba1e2;strokeColor=#006EAF;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-5" target="tM_Gde3HH8YiZ2frBV5J-0" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-9" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;endArrow=open;endFill=0;fillColor=#1ba1e2;strokeColor=#006EAF;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-5" target="tM_Gde3HH8YiZ2frBV5J-0" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-12" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=block;endFill=0;strokeColor=#f0f0f0;dashed=1;exitX=1;exitY=0.75;exitDx=0;exitDy=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-5" target="UY-EM7-1ECCvWtENr50b-11" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-12" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=block;endFill=0;strokeColor=#f0f0f0;dashed=1;exitX=1;exitY=0.75;exitDx=0;exitDy=0;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-5" target="UY-EM7-1ECCvWtENr50b-11" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-13" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.75;entryDx=0;entryDy=0;endArrow=open;endFill=0;strokeColor=#f0f0f0;dashed=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-5" target="UY-EM7-1ECCvWtENr50b-10" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-13" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.75;entryDx=0;entryDy=0;endArrow=open;endFill=0;strokeColor=#f0f0f0;dashed=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-5" target="UY-EM7-1ECCvWtENr50b-10" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="tM_Gde3HH8YiZ2frBV5J-3" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;dashed=1;endArrow=block;endFill=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-5" target="tM_Gde3HH8YiZ2frBV5J-1" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="tM_Gde3HH8YiZ2frBV5J-3" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;dashed=1;endArrow=block;endFill=0;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-5" target="tM_Gde3HH8YiZ2frBV5J-1" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-5" value="ServicePresenter" style="whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-5" value="ServicePresenter" style="whiteSpace=wrap;html=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="575" y="122.5" width="120" height="60" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-8" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;endArrow=open;endFill=0;fillColor=#1ba1e2;strokeColor=#006EAF;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-7" target="jVG6p58vlRYGO9X4wXeX-3" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-8" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;endArrow=open;endFill=0;fillColor=#1ba1e2;strokeColor=#006EAF;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-7" target="jVG6p58vlRYGO9X4wXeX-3" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="tM_Gde3HH8YiZ2frBV5J-5" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-7" target="tM_Gde3HH8YiZ2frBV5J-0" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="tM_Gde3HH8YiZ2frBV5J-5" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-7" target="tM_Gde3HH8YiZ2frBV5J-0" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-7" value="String Output" style="whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="28FAlPysTx9DMYvLwa-2-7" value="String Output" style="whiteSpace=wrap;html=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="260" y="122.5" width="120" height="60" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-1" value="ServiceAction &amp;lt;DS&amp;gt;" style="whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-1" value="ServiceAction &amp;lt;DS&amp;gt;" style="whiteSpace=wrap;html=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="800" y="542.5" width="120" height="60" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-6" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;endArrow=open;endFill=0;strokeColor=#f0f0f0;dashed=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="UY-EM7-1ECCvWtENr50b-2" target="UY-EM7-1ECCvWtENr50b-1" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-6" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;endArrow=open;endFill=0;strokeColor=#f0f0f0;dashed=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="UY-EM7-1ECCvWtENr50b-2" target="UY-EM7-1ECCvWtENr50b-1" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-20" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.25;exitDx=0;exitDy=0;entryX=1;entryY=0.75;entryDx=0;entryDy=0;endArrow=open;endFill=0;strokeColor=#006EAF;fillColor=#1ba1e2;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="UY-EM7-1ECCvWtENr50b-1" target="28FAlPysTx9DMYvLwa-2-1" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-20" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.25;exitDx=0;exitDy=0;entryX=1;entryY=0.75;entryDx=0;entryDy=0;endArrow=open;endFill=0;strokeColor=#006EAF;fillColor=#1ba1e2;" parent="jVG6p58vlRYGO9X4wXeX-1" source="UY-EM7-1ECCvWtENr50b-1" target="28FAlPysTx9DMYvLwa-2-1" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-2" value="&amp;lt;&amp;lt;Interface&amp;gt;&amp;gt;&lt;br&gt;IServiceManipulator" style="whiteSpace=wrap;html=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-2" value="&amp;lt;&amp;lt;Interface&amp;gt;&amp;gt;&lt;br&gt;IServiceManipulator" style="whiteSpace=wrap;html=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="800" y="432.5" width="120" height="60" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-22" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.25;entryDx=0;entryDy=0;endArrow=open;endFill=0;strokeColor=#006EAF;fillColor=#1ba1e2;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="UY-EM7-1ECCvWtENr50b-10" target="28FAlPysTx9DMYvLwa-2-5" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-22" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.25;entryDx=0;entryDy=0;endArrow=open;endFill=0;strokeColor=#006EAF;fillColor=#1ba1e2;" parent="jVG6p58vlRYGO9X4wXeX-1" source="UY-EM7-1ECCvWtENr50b-10" target="28FAlPysTx9DMYvLwa-2-5" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-10" value="ServicesResult &amp;lt;DS&amp;gt;" style="whiteSpace=wrap;html=1;fillColor=none;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-10" value="ServicesResult &amp;lt;DS&amp;gt;" style="whiteSpace=wrap;html=1;fillColor=none;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="800" y="67.5" width="120" height="60" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-14" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;endArrow=open;endFill=0;strokeColor=#f0f0f0;dashed=1;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="UY-EM7-1ECCvWtENr50b-11" target="UY-EM7-1ECCvWtENr50b-10" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-14" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;endArrow=open;endFill=0;strokeColor=#f0f0f0;dashed=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="UY-EM7-1ECCvWtENr50b-11" target="UY-EM7-1ECCvWtENr50b-10" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-21" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.25;exitDx=0;exitDy=0;entryX=1;entryY=0.75;entryDx=0;entryDy=0;endArrow=open;endFill=0;strokeColor=#006EAF;fillColor=#1ba1e2;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-1" target="UY-EM7-1ECCvWtENr50b-10" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-21" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.25;exitDx=0;exitDy=0;entryX=1;entryY=0.75;entryDx=0;entryDy=0;endArrow=open;endFill=0;strokeColor=#006EAF;fillColor=#1ba1e2;" parent="jVG6p58vlRYGO9X4wXeX-1" source="28FAlPysTx9DMYvLwa-2-1" target="UY-EM7-1ECCvWtENr50b-10" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-11" value="&amp;lt;&amp;lt;Interface&amp;gt;&amp;gt;&lt;br&gt;IServicesMonitor" style="whiteSpace=wrap;html=1;fillColor=none;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-11" value="&amp;lt;&amp;lt;Interface&amp;gt;&amp;gt;&lt;br&gt;IServicesMonitor" style="whiteSpace=wrap;html=1;fillColor=none;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="800" y="167.5" width="120" height="60" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-70" value="" style="line;strokeWidth=2;direction=south;html=1;fillColor=none;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-70" value="" style="line;strokeWidth=2;direction=south;html=1;fillColor=none;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="220" y="50" width="10" height="570" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-71" value="" style="line;strokeWidth=2;direction=south;html=1;fillColor=none;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-71" value="" style="line;strokeWidth=2;direction=south;html=1;fillColor=none;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="760" y="50" width="10" height="570" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-73" value="Page 191 (Chapter 22) of Clean Architecture" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="UY-EM7-1ECCvWtENr50b-73" value="Page 191 (Chapter 22) of Clean Architecture" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry y="870" width="480" height="20" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="tM_Gde3HH8YiZ2frBV5J-2" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;dashed=1;endArrow=block;endFill=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="tM_Gde3HH8YiZ2frBV5J-0" target="tM_Gde3HH8YiZ2frBV5J-1" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="tM_Gde3HH8YiZ2frBV5J-2" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;dashed=1;endArrow=block;endFill=0;" parent="jVG6p58vlRYGO9X4wXeX-1" source="tM_Gde3HH8YiZ2frBV5J-0" target="tM_Gde3HH8YiZ2frBV5J-1" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="tM_Gde3HH8YiZ2frBV5J-4" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.75;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" source="tM_Gde3HH8YiZ2frBV5J-0" target="tM_Gde3HH8YiZ2frBV5J-1" edge="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="tM_Gde3HH8YiZ2frBV5J-4" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.75;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;dashed=1;endArrow=open;endFill=0;" parent="jVG6p58vlRYGO9X4wXeX-1" source="tM_Gde3HH8YiZ2frBV5J-0" target="tM_Gde3HH8YiZ2frBV5J-1" edge="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="tM_Gde3HH8YiZ2frBV5J-0" value="MainPresenter" style="html=1;dashed=0;whitespace=wrap;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="tM_Gde3HH8YiZ2frBV5J-0" value="MainPresenter" style="html=1;dashed=0;whitespace=wrap;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="420" y="122.5" width="110" height="60" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxCell id="tM_Gde3HH8YiZ2frBV5J-1" value="&amp;lt;&amp;lt;Interface&amp;gt;&amp;gt;&lt;br&gt;IConsoleOutput" style="html=1;dashed=0;whitespace=wrap;sketch=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxCell id="tM_Gde3HH8YiZ2frBV5J-1" value="&amp;lt;&amp;lt;Interface&amp;gt;&amp;gt;&lt;br&gt;IConsoleOutput" style="html=1;dashed=0;whitespace=wrap;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa;&#xa;&#xa;
<mxGeometry x="420" y="20" width="110" height="50" as="geometry"/>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxCell>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</root>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</mxGraphModel>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
&#xa; &#xa; &#xa;&#xa;&#xa;
</diagram>
</mxfile>

View File

@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace GameServiceWarden.ModuleAPI
{
public interface IGameService
{
event EventHandler<ServiceState> StateChangeEvent;
IReadOnlyCollection<IGameConfigurable> Configurables{ get; }
void InitializeService(TextWriter stream);
void ElegantShutdown();
void ExecuteCommand(string command);
}
}

View File

@ -15,7 +15,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\GameServiceWarden.Core\GameServiceWarden.Core.csproj" />
<ProjectReference Include="..\..\src\GameServiceWarden.ModuleAPI\GameServiceWarden.ModuleAPI.csproj" />
<ProjectReference Include="..\..\src\GameServiceWarden.API.Module\GameServiceWarden.API.Module.csproj" />
</ItemGroup>
</Project>

View File

@ -1,47 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using GameServiceWarden.ModuleAPI;
namespace GameServiceWarden.Core.Tests.Modules.Games
{
public class FakeGameService : IGameService
{
public IReadOnlyCollection<IGameConfigurable> Configurables { get; set; }
public event EventHandler<ServiceState> StateChangeEvent;
public ServiceState CurrentState { get; private set; } = ServiceState.Stopped;
private TextWriter consoleStream;
public FakeGameService(params IGameConfigurable[] configurables)
{
HashSet<IGameConfigurable> modifiable = new HashSet<IGameConfigurable>();
foreach (IGameConfigurable configurable in configurables)
{
modifiable.Add(configurable);
}
this.Configurables = modifiable;
}
public void ElegantShutdown()
{
CurrentState = ServiceState.Stopped;
StateChangeEvent?.Invoke(this, CurrentState);
}
public void ExecuteCommand(string command)
{
consoleStream.WriteLine(command);
consoleStream.Flush();
}
public void InitializeService(TextWriter stream)
{
CurrentState = ServiceState.Running;
StateChangeEvent?.Invoke(this, CurrentState);
this.consoleStream = stream;
}
}
}

View File

@ -36,7 +36,7 @@ namespace GameServiceWarden.Core.Tests.Modules
backing.Add(item);
}
public void Add(string key, GameServiceInfo value)
public void Add(string key, ServiceDescriptor value)
{
throw new System.NotImplementedException();
}

View File

@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using GameServiceWarden.API.Module;
namespace GameServiceWarden.Core.Tests.Modules.Games
{
public class FakeService : IService
{
public IReadOnlyCollection<IServiceConfigurable> Configurables { get; set; }
public event EventHandler<bool> StateChangeEvent;
public ServiceState CurrentState { get; private set; } = ServiceState.Stopped;
private StreamWriter consoleWriter;
private Stack<Task> taskStack = new Stack<Task>();
public FakeService(params IServiceConfigurable[] configurables)
{
HashSet<IServiceConfigurable> modifiable = new HashSet<IServiceConfigurable>();
foreach (IServiceConfigurable configurable in configurables)
{
modifiable.Add(configurable);
}
this.Configurables = modifiable;
}
public void ElegantShutdown()
{
CurrentState = ServiceState.Stopped;
StateChangeEvent?.Invoke(this, false);
Task task;
while(taskStack.TryPop(out task)) {
if (task.IsCompleted) {
task.Wait();
} else {
throw new InvalidOperationException("A task was not completed.");
}
}
}
public void ExecuteCommand(string command)
{
taskStack.Push(consoleWriter.WriteLineAsync(command));
taskStack.Push(consoleWriter.FlushAsync());
}
public void InitializeService(Stream stream)
{
CurrentState = ServiceState.Running;
this.consoleWriter = new StreamWriter(stream);
StateChangeEvent?.Invoke(this, true);
}
}
}

View File

@ -1,13 +1,13 @@
using GameServiceWarden.ModuleAPI;
using GameServiceWarden.API.Module;
namespace GameServiceWarden.Core.Tests.Modules.Games
{
public class FakeGameConfigurable : IGameConfigurable
public class FakeServiceConfigurable : IServiceConfigurable
{
private string value;
public string OptionName { get; private set; }
public FakeGameConfigurable(string optionName)
public FakeServiceConfigurable(string optionName)
{
this.OptionName = optionName;
}

View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using GameServiceWarden.API.Games;
using GameServiceWarden.Core.Games;
namespace GameServiceWarden.Core.Tests.Modules.Games
{
public class FakeServiceManagerMonitor : IServiceManagerMonitor
{
public List<ServiceManagerState> states = new List<ServiceManagerState>();
public ServiceManagerState this[int i]
{
get
{
return states[i];
}
}
public void Present(ServiceManagerState state)
{
states.Add(state);
}
public ServiceManagerState GetLastState()
{
return states[states.Count - 1];
}
}
}

View File

@ -1,25 +1,25 @@
using System.Collections.Generic;
using GameServiceWarden.ModuleAPI;
using GameServiceWarden.API.Module;
namespace GameServiceWarden.Core.Tests.Modules.Games
{
public class FakeGameServiceModule : IGameServiceModule
public class FakeServiceModule : IServiceModule
{
public string Name => "FakeModule";
public string Description => "A fake module for testing.";
private IGameConfigurable[] configurables;
public FakeGameServiceModule(params IGameConfigurable[] configurables)
private IServiceConfigurable[] configurables;
public FakeServiceModule(params IServiceConfigurable[] configurables)
{
this.configurables = configurables;
}
public IEnumerable<string> Authors { get; private set; } = new string[] { "FakeAuthor", "FakeAuthor2" };
public IGameService InstantiateGameService(string workspace, bool clean)
public IService InstantiateService(string workspace, bool clean)
{
return new FakeGameService(configurables);
return new FakeService(configurables);
}
}
}

View File

@ -1,155 +0,0 @@
using System.Collections.Generic;
using System.IO;
using GameServiceWarden.Core.Games;
using GameServiceWarden.ModuleAPI;
using Xunit;
namespace GameServiceWarden.Core.Tests.Modules.Games
{
// 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 GameServiceInfoTest
{
//MethodTested_ScenarioTested_ExpectedBehavior
[Fact]
public void Start_FromStopped_StateIsRunning()
{
//Arrange, Act, Assert
IGameService stubGameService = new FakeGameService();
GameServiceInfo serviceInfo = new GameServiceInfo(stubGameService, "FakeModule", "FakeAssembly");
serviceInfo.Start();
Assert.Equal(ServiceState.Running, serviceInfo.GetServiceState());
serviceInfo.Dispose();
}
[Fact]
public void Stop_FromStart_Stopped()
{
IGameService stubService = new FakeGameService();
GameServiceInfo serviceInfo = new GameServiceInfo(stubService, "FakeModule", "FakeAssembly");
serviceInfo.Start();
serviceInfo.Stop();
Assert.Equal(ServiceState.Stopped, serviceInfo.GetServiceState());
serviceInfo.Dispose();
}
[Fact]
public void GetConfigurableOptions_ServiceStopped_ReturnsConfigurables()
{
//Given
FakeGameService stubService = new FakeGameService();
FakeGameConfigurable stubConfigurable = new FakeGameConfigurable("Option");
HashSet<IGameConfigurable> configurables = new HashSet<IGameConfigurable>();
configurables.Add(stubConfigurable);
stubService.Configurables = configurables;
GameServiceInfo serviceInfo = new GameServiceInfo(stubService, "FakeModule", "FakeAssembly");
//Then
Assert.Contains<string>(stubConfigurable.OptionName, serviceInfo.GetConfigurableOptions());
serviceInfo.Dispose();
}
[Fact]
public void SetAndGetConfigurationValue_ServiceStopped_AppropriateValueReturned()
{
//Given
FakeGameService stubService = new FakeGameService();
FakeGameConfigurable stubConfigurable = new FakeGameConfigurable("Option");
HashSet<IGameConfigurable> configurables = new HashSet<IGameConfigurable>();
configurables.Add(stubConfigurable);
stubService.Configurables = configurables;
GameServiceInfo serviceInfo = new GameServiceInfo(stubService, "FakeModule", "FakeAssembly");
//When
serviceInfo.SetConfigurableValue(stubConfigurable.OptionName, "success");
//Then
Assert.True("success".Equals(serviceInfo.GetConfigurableValue(stubConfigurable.OptionName)));
}
[Fact]
public void GetServiceState_ServiceNotStarted_ReturnsStoppedState()
{
//Given
IGameService stubService = new FakeGameService();
GameServiceInfo serviceInfo = new GameServiceInfo(stubService, "FakeModule", "FakeAssembly");
//Then
Assert.Equal(ServiceState.Stopped, serviceInfo.GetServiceState());
serviceInfo.Dispose();
}
[Fact]
public void GetServiceState_ServiceStarted_ReturnsRunningState()
{
//Given
IGameService stubService = new FakeGameService();
GameServiceInfo serviceInfo = new GameServiceInfo(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 FakeGameService();
GameServiceInfo serviceInfo = new GameServiceInfo(stubService, MODULE_NAME, "FakeAssembly");
//Then
Assert.Equal(MODULE_NAME, serviceInfo.ModuleName);
serviceInfo.Dispose();
}
[Fact]
public void GetAssemblyName_ServiceNotStarted_ReturnsSetAssemblyName()
{
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
IGameService stubService = new FakeGameService();
GameServiceInfo serviceInfo = new GameServiceInfo(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 FakeGameService();
GameServiceInfo serviceInfo = new GameServiceInfo(stubService, "FakeModule", "FakeAssemblyName");
//When
serviceInfo.ServiceName = SERVICE_NAME;
//Then
Assert.True(SERVICE_NAME.Equals(serviceInfo.ServiceName));
serviceInfo.Dispose();
}
[Fact]
public void ServiceConsoleStream_ServiceNotStarted_NullReturned()
{
//Given
IGameService stubService = new FakeGameService();
GameServiceInfo serviceInfo = new GameServiceInfo(stubService, "FakeModule", "FakeAssembly");
//Then
Assert.Null(serviceInfo.ServiceConsoleStream);
serviceInfo.Dispose();
}
[Fact]
public void ServiceConsoleStream_ServiceStarted_StreamReturned()
{
//Given
IGameService stubService = new FakeGameService();
GameServiceInfo serviceInfo = new GameServiceInfo(stubService, "FakeModule", "FakeAssembly");
//When
serviceInfo.Start();
//Then
Assert.IsAssignableFrom<Stream>(serviceInfo.ServiceConsoleStream);
serviceInfo.Dispose();
}
}
}

View File

@ -1,252 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using GameServiceWarden.Core.Games;
using GameServiceWarden.ModuleAPI;
using Xunit;
namespace GameServiceWarden.Core.Tests.Modules.Games
{
public class GameServiceManagerTest
{
[Fact]
public void CreateService_NewManager_NewServiceCreated()
{
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string FAKE_SERVICE_NAME = "FakeService";
FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>> stubPersistentModuleDictionary = new FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>>();
FakePersistence<GameServiceInfo> stubPersistentServiceDictionary = new FakePersistence<GameServiceInfo>();
GameServiceManager serviceManager = new GameServiceManager(stubPersistentServiceDictionary, stubPersistentModuleDictionary);
Dictionary<string, IGameServiceModule> stubAssemblyModulesDictionary = new Dictionary<string, IGameServiceModule>();
IGameServiceModule stubGameServiceModule = new FakeGameServiceModule();
stubAssemblyModulesDictionary.Add(stubGameServiceModule.Name, stubGameServiceModule);
stubPersistentModuleDictionary.AddToPersistence(ASSEMBLY_NAME, stubAssemblyModulesDictionary);
//When
serviceManager.CreateService(FAKE_SERVICE_NAME, ASSEMBLY_NAME, stubGameServiceModule.Name);
//Then
Assert.Contains<string>(FAKE_SERVICE_NAME, serviceManager.GetServiceNames());
}
[Fact]
public void CreateService_OneService_ServiceDeleted()
{
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string FAKE_SERVICE_NAME = "FakeService";
FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>> stubPersistentModuleDictionary = new FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>>();
FakePersistence<GameServiceInfo> stubPersistentServiceDictionary = new FakePersistence<GameServiceInfo>();
GameServiceManager serviceManager = new GameServiceManager(stubPersistentServiceDictionary, stubPersistentModuleDictionary);
Dictionary<string, IGameServiceModule> stubAssemblyModulesDictionary = new Dictionary<string, IGameServiceModule>();
IGameServiceModule stubGameServiceModule = new FakeGameServiceModule();
stubAssemblyModulesDictionary.Add(stubGameServiceModule.Name, stubGameServiceModule);
stubPersistentModuleDictionary.AddToPersistence(ASSEMBLY_NAME, stubAssemblyModulesDictionary);
//When
serviceManager.CreateService(FAKE_SERVICE_NAME, ASSEMBLY_NAME, stubGameServiceModule.Name);
serviceManager.DeleteService(FAKE_SERVICE_NAME);
//Then
Assert.DoesNotContain<string>(FAKE_SERVICE_NAME, serviceManager.GetServiceNames());
}
[Fact]
public void GetServiceNames_MultipleServices_AllCorrectNames()
{
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string FAKE_SERVICE_PREFIX = "FakeService_";
FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>> stubPersistentModuleDictionary = new FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>>();
FakePersistence<GameServiceInfo> stubPersistentServiceDictionary = new FakePersistence<GameServiceInfo>();
GameServiceManager serviceManager = new GameServiceManager(stubPersistentServiceDictionary, stubPersistentModuleDictionary);
Dictionary<string, IGameServiceModule> stubAssemblyModulesDictionary = new Dictionary<string, IGameServiceModule>();
IGameServiceModule stubGameServiceModule = new FakeGameServiceModule();
stubAssemblyModulesDictionary.Add(stubGameServiceModule.Name, stubGameServiceModule);
stubPersistentModuleDictionary.AddToPersistence(ASSEMBLY_NAME, stubAssemblyModulesDictionary);
//When
for (int i = 0; i < 100; i++)
{
serviceManager.CreateService(FAKE_SERVICE_PREFIX + i, ASSEMBLY_NAME, stubGameServiceModule.Name);
}
//Then
for (int i = 0; i < 100; i++)
{
Assert.Contains<string>(FAKE_SERVICE_PREFIX + i, serviceManager.GetServiceNames());
}
}
[Fact]
public void GetServiceOptions_ThreeOptionService_CorrectOptions()
{
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string FAKE_SERVICE_NAME = "FakeService";
FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>> stubPersistentModuleDictionary = new FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>>();
FakePersistence<GameServiceInfo> stubPersistentServiceDictionary = new FakePersistence<GameServiceInfo>();
GameServiceManager serviceManager = new GameServiceManager(stubPersistentServiceDictionary, stubPersistentModuleDictionary);
Dictionary<string, IGameServiceModule> stubAssemblyModulesDictionary = new Dictionary<string, IGameServiceModule>();
IGameServiceModule stubGameServiceModule = new FakeGameServiceModule(
new FakeGameConfigurable("A"),
new FakeGameConfigurable("B"),
new FakeGameConfigurable("C")
);
stubAssemblyModulesDictionary.Add(stubGameServiceModule.Name, stubGameServiceModule);
stubPersistentModuleDictionary.AddToPersistence(ASSEMBLY_NAME, stubAssemblyModulesDictionary);
//When
serviceManager.CreateService(FAKE_SERVICE_NAME, ASSEMBLY_NAME, stubGameServiceModule.Name);
//Then
Assert.Contains<string>("A", serviceManager.GetServiceOptions(FAKE_SERVICE_NAME));
Assert.Contains<string>("B", serviceManager.GetServiceOptions(FAKE_SERVICE_NAME));
Assert.Contains<string>("C", serviceManager.GetServiceOptions(FAKE_SERVICE_NAME));
}
[Fact]
public void SetandGetServiceOptionValue_OneOption_OptionChanged()
{
const string ASSEMBLY_NAME = "FakeAssembly";
const string FAKE_SERVICE_NAME = "FakeService";
FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>> stubPersistentModuleDictionary = new FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>>();
FakePersistence<GameServiceInfo> stubPersistentServiceDictionary = new FakePersistence<GameServiceInfo>();
GameServiceManager serviceManager = new GameServiceManager(stubPersistentServiceDictionary, stubPersistentModuleDictionary);
Dictionary<string, IGameServiceModule> stubAssemblyModulesDictionary = new Dictionary<string, IGameServiceModule>();
IGameServiceModule stubGameServiceModule = new FakeGameServiceModule(
new FakeGameConfigurable("A")
);
stubAssemblyModulesDictionary.Add(stubGameServiceModule.Name, stubGameServiceModule);
stubPersistentModuleDictionary.AddToPersistence(ASSEMBLY_NAME, stubAssemblyModulesDictionary);
//When
serviceManager.CreateService(FAKE_SERVICE_NAME, ASSEMBLY_NAME, stubGameServiceModule.Name);
serviceManager.SetServiceOptionValue(FAKE_SERVICE_NAME, "A", "Test");
//Then
Assert.True("Test".Equals(serviceManager.GetServiceOptionValue(FAKE_SERVICE_NAME, "A")));
}
[Fact]
public void GetServiceState_NotRunning_ReturnsNotRunningState()
{
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string FAKE_SERVICE_NAME = "FakeService";
FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>> stubPersistentModuleDictionary = new FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>>();
FakePersistence<GameServiceInfo> stubPersistentServiceDictionary = new FakePersistence<GameServiceInfo>();
GameServiceManager serviceManager = new GameServiceManager(stubPersistentServiceDictionary, stubPersistentModuleDictionary);
Dictionary<string, IGameServiceModule> stubAssemblyModulesDictionary = new Dictionary<string, IGameServiceModule>();
IGameServiceModule stubGameServiceModule = new FakeGameServiceModule();
stubAssemblyModulesDictionary.Add(stubGameServiceModule.Name, stubGameServiceModule);
stubPersistentModuleDictionary.AddToPersistence(ASSEMBLY_NAME, stubAssemblyModulesDictionary);
//When
serviceManager.CreateService(FAKE_SERVICE_NAME, ASSEMBLY_NAME, stubGameServiceModule.Name);
//Then
Assert.Equal<ServiceState>(ServiceState.Stopped, serviceManager.GetServiceState(FAKE_SERVICE_NAME));
}
[Fact]
public void GetServiceState_Running_ReturnsNotRunningState()
{
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string FAKE_SERVICE_NAME = "FakeService";
FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>> stubPersistentModuleDictionary = new FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>>();
FakePersistence<GameServiceInfo> stubPersistentServiceDictionary = new FakePersistence<GameServiceInfo>();
GameServiceManager serviceManager = new GameServiceManager(stubPersistentServiceDictionary, stubPersistentModuleDictionary);
Dictionary<string, IGameServiceModule> stubAssemblyModulesDictionary = new Dictionary<string, IGameServiceModule>();
IGameServiceModule stubGameServiceModule = new FakeGameServiceModule();
stubAssemblyModulesDictionary.Add(stubGameServiceModule.Name, stubGameServiceModule);
stubPersistentModuleDictionary.AddToPersistence(ASSEMBLY_NAME, stubAssemblyModulesDictionary);
//When
serviceManager.CreateService(FAKE_SERVICE_NAME, ASSEMBLY_NAME, stubGameServiceModule.Name);
serviceManager.StartService(FAKE_SERVICE_NAME);
//Then
Assert.Equal<ServiceState>(ServiceState.Running, serviceManager.GetServiceState(FAKE_SERVICE_NAME));
}
[Fact]
public void StartService_NotStarted_SuccessfulStart()
{
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string FAKE_SERVICE_NAME = "FakeService";
FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>> stubPersistentModuleDictionary = new FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>>();
FakePersistence<GameServiceInfo> stubPersistentServiceDictionary = new FakePersistence<GameServiceInfo>();
GameServiceManager serviceManager = new GameServiceManager(stubPersistentServiceDictionary, stubPersistentModuleDictionary);
Dictionary<string, IGameServiceModule> stubAssemblyModulesDictionary = new Dictionary<string, IGameServiceModule>();
IGameServiceModule stubGameServiceModule = new FakeGameServiceModule();
stubAssemblyModulesDictionary.Add(stubGameServiceModule.Name, stubGameServiceModule);
stubPersistentModuleDictionary.AddToPersistence(ASSEMBLY_NAME, stubAssemblyModulesDictionary);
//When
serviceManager.CreateService(FAKE_SERVICE_NAME, ASSEMBLY_NAME, stubGameServiceModule.Name);
serviceManager.StartService(FAKE_SERVICE_NAME);
//Then
Assert.Equal<ServiceState>(ServiceState.Running, serviceManager.GetServiceState(FAKE_SERVICE_NAME));
}
[Fact]
public void StopService_Stopped_StateUpdated()
{
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string FAKE_SERVICE_NAME = "FakeService";
FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>> stubPersistentModuleDictionary = new FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>>();
FakePersistence<GameServiceInfo> stubPersistentServiceDictionary = new FakePersistence<GameServiceInfo>();
GameServiceManager serviceManager = new GameServiceManager(stubPersistentServiceDictionary, stubPersistentModuleDictionary);
Dictionary<string, IGameServiceModule> stubAssemblyModulesDictionary = new Dictionary<string, IGameServiceModule>();
IGameServiceModule stubGameServiceModule = new FakeGameServiceModule();
stubAssemblyModulesDictionary.Add(stubGameServiceModule.Name, stubGameServiceModule);
stubPersistentModuleDictionary.AddToPersistence(ASSEMBLY_NAME, stubAssemblyModulesDictionary);
//When
serviceManager.CreateService(FAKE_SERVICE_NAME, ASSEMBLY_NAME, stubGameServiceModule.Name);
serviceManager.StartService(FAKE_SERVICE_NAME);
serviceManager.StopService(FAKE_SERVICE_NAME);
//Then
Assert.Equal<ServiceState>(ServiceState.Stopped, serviceManager.GetServiceState(FAKE_SERVICE_NAME));
}
[Fact]
public void ExecuteCommand_ServiceStarted_CommandLogged()
{
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string FAKE_SERVICE_NAME = "FakeService";
FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>> stubPersistentModuleDictionary = new FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>>();
FakePersistence<GameServiceInfo> stubPersistentServiceDictionary = new FakePersistence<GameServiceInfo>();
GameServiceManager serviceManager = new GameServiceManager(stubPersistentServiceDictionary, stubPersistentModuleDictionary);
Dictionary<string, IGameServiceModule> stubAssemblyModulesDictionary = new Dictionary<string, IGameServiceModule>();
IGameServiceModule stubGameServiceModule = new FakeGameServiceModule();
stubAssemblyModulesDictionary.Add(stubGameServiceModule.Name, stubGameServiceModule);
stubPersistentModuleDictionary.AddToPersistence(ASSEMBLY_NAME, stubAssemblyModulesDictionary);
//When
serviceManager.CreateService(FAKE_SERVICE_NAME, ASSEMBLY_NAME, stubGameServiceModule.Name);
serviceManager.StartService(FAKE_SERVICE_NAME);
serviceManager.ExecuteCommand(FAKE_SERVICE_NAME, "Test");
//Then
Stream stream = serviceManager.GetServiceConsoleStream(FAKE_SERVICE_NAME);
stream.Position = 0;
using (StreamReader reader = new StreamReader(stream))
{
Assert.True("Test".Equals(reader.ReadLine()));
}
}
[Fact]
public void GetServiceConsoleStream_ServiceStopped_ExceptionThrown()
{
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string FAKE_SERVICE_NAME = "FakeService";
FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>> stubPersistentModuleDictionary = new FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>>();
FakePersistence<GameServiceInfo> stubPersistentServiceDictionary = new FakePersistence<GameServiceInfo>();
GameServiceManager serviceManager = new GameServiceManager(stubPersistentServiceDictionary, stubPersistentModuleDictionary);
Dictionary<string, IGameServiceModule> stubAssemblyModulesDictionary = new Dictionary<string, IGameServiceModule>();
IGameServiceModule stubGameServiceModule = new FakeGameServiceModule();
stubAssemblyModulesDictionary.Add(stubGameServiceModule.Name, stubGameServiceModule);
stubPersistentModuleDictionary.AddToPersistence(ASSEMBLY_NAME, stubAssemblyModulesDictionary);
//When
serviceManager.CreateService(FAKE_SERVICE_NAME, ASSEMBLY_NAME, stubGameServiceModule.Name);
//Then
Action action = delegate()
{
serviceManager.GetServiceConsoleStream(FAKE_SERVICE_NAME);
};
Assert.Throws<InvalidOperationException>(action);
}
}
}

View File

@ -0,0 +1,170 @@
using System.Collections.Generic;
using System.IO;
using GameServiceWarden.Core.Games;
using GameServiceWarden.Core.Logging;
using GameServiceWarden.API.Module;
using Xunit;
using Xunit.Abstractions;
namespace GameServiceWarden.Core.Tests.Modules.Games
{
// 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.
[CollectionDefinition("Service", DisableParallelization = true)]
public class ServiceDescriptorTest
{
private readonly ITestOutputHelper output;
private readonly XUnitLogger logger;
public ServiceDescriptorTest(ITestOutputHelper output)
{
this.output = output;
logger = new XUnitLogger(output);
Logger.AddLogListener(logger);
}
//MethodTested_ScenarioTested_ExpectedBehavior
[Fact]
public void Start_FromStopped_StateIsRunning()
{
//Arrange, Act, Assert
const string SERVICE_NAME = "Start_FromStopped_StateIsRunning";
IService stubService = new FakeService();
ServiceDescriptor serviceInfo = new ServiceDescriptor(stubService, SERVICE_NAME, "FakeModule", "FakeAssembly");
serviceInfo.Start();
Assert.Equal(true, serviceInfo.GetServiceState());
serviceInfo.Stop();
}
[Fact]
public void Stop_FromStart_Stopped()
{
const string SERVICE_NAME = "Stop_FromStart_Stopped";
IService stubService = new FakeService();
ServiceDescriptor serviceInfo = new ServiceDescriptor(stubService, SERVICE_NAME, "FakeModule", "FakeAssembly");
serviceInfo.Start();
serviceInfo.Stop();
Assert.Equal(false, serviceInfo.GetServiceState());
}
[Fact]
public void GetConfigurableOptions_ServiceStopped_ReturnsConfigurables()
{
//Given
const string SERVICE_NAME = "GetConfigurableOptions_ServiceStopped_ReturnsConfigurables";
FakeService stubService = new FakeService();
FakeServiceConfigurable stubConfigurable = new FakeServiceConfigurable("Option");
HashSet<IServiceConfigurable> configurables = new HashSet<IServiceConfigurable>();
configurables.Add(stubConfigurable);
stubService.Configurables = configurables;
ServiceDescriptor serviceInfo = new ServiceDescriptor(stubService, SERVICE_NAME, "FakeModule", "FakeAssembly");
//Then
Assert.Contains<string>(stubConfigurable.OptionName, serviceInfo.GetConfigurableOptions());
}
[Fact]
public void SetAndGetConfigurationValue_ServiceStopped_AppropriateValueReturned()
{
//Given
const string SERVICE_NAME = "SetAndGetConfigurationValue_ServiceStopped_AppropriateValueReturned";
FakeService stubService = new FakeService();
FakeServiceConfigurable stubConfigurable = new FakeServiceConfigurable("Option");
HashSet<IServiceConfigurable> configurables = new HashSet<IServiceConfigurable>();
configurables.Add(stubConfigurable);
stubService.Configurables = configurables;
ServiceDescriptor serviceInfo = new ServiceDescriptor(stubService, SERVICE_NAME, "FakeModule", "FakeAssembly");
//When
serviceInfo.SetConfigurableValue(stubConfigurable.OptionName, "success");
//Then
Assert.True("success".Equals(serviceInfo.GetConfigurableValue(stubConfigurable.OptionName)));
}
[Fact]
public void GetServiceState_ServiceNotStarted_ReturnsStoppedState()
{
//Given
const string SERVICE_NAME = "GetServiceState_ServiceNotStarted_ReturnsStoppedState";
IService stubService = new FakeService();
ServiceDescriptor serviceInfo = new ServiceDescriptor(stubService, SERVICE_NAME, "FakeModule", "FakeAssembly");
//Then
Assert.Equal(false, serviceInfo.GetServiceState());
}
[Fact]
public void GetServiceState_ServiceStarted_ReturnsRunningState()
{
//Given
const string SERVICE_NAME = "GetServiceState_ServiceStarted_ReturnsRunningState";
IService stubService = new FakeService();
ServiceDescriptor serviceInfo = new ServiceDescriptor(stubService, SERVICE_NAME, "FakeModule", "FakeAssembly");
//When
serviceInfo.Start();
//Then
Assert.Equal(true, serviceInfo.GetServiceState());
serviceInfo.Stop();
}
[Fact]
public void GetModuleName_ServiceNotStarted_ReturnsSetName()
{
//Given
const string SERVICE_NAME = "GetModuleName_ServiceNotStarted_ReturnsSetName";
const string MODULE_NAME = "FakeModule";
IService stubService = new FakeService();
ServiceDescriptor serviceInfo = new ServiceDescriptor(stubService, SERVICE_NAME, MODULE_NAME, "FakeAssembly");
//Then
Assert.Equal(MODULE_NAME, serviceInfo.GetModuleName());
}
[Fact]
public void GetAssemblyName_ServiceNotStarted_ReturnsSetAssemblyName()
{
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string SERVICE_NAME = "GetAssemblyName_ServiceNotStarted_ReturnsSetAssemblyName";
IService stubService = new FakeService();
ServiceDescriptor serviceInfo = new ServiceDescriptor(stubService, SERVICE_NAME, "FakeModule", ASSEMBLY_NAME);
//Then
Assert.Equal(ASSEMBLY_NAME, serviceInfo.GetAssemblyName());
}
[Fact]
public void GetServiceName_ServiceNotStartedSingleThread_ServiceNameReturned()
{
//Given
const string SERVICE_NAME = "GetServiceName_ServiceNotStartedSingleThread_ServiceNameReturned";
IService stubService = new FakeService();
//When
ServiceDescriptor serviceInfo = new ServiceDescriptor(stubService, SERVICE_NAME, "FakeModule", "FakeAssemblyName");
//Then
Assert.True(SERVICE_NAME.Equals(serviceInfo.ServiceName));
}
[Fact]
public void ServiceLogPipeName_ServiceNotStarted_NullReturned()
{
//Given
const string SERVICE_NAME = "ServiceLogPipeName_ServiceNotStarted_NullReturned";
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();
//Then
Assert.NotNull(serviceInfo.ServiceLogPipeName);
serviceInfo.Stop();
}
}
}

View File

@ -0,0 +1,337 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipes;
using System.Threading;
using System.Threading.Tasks;
using GameServiceWarden.Core.Games;
using GameServiceWarden.Core.Logging;
using GameServiceWarden.API.Module;
using Xunit;
using Xunit.Abstractions;
namespace GameServiceWarden.Core.Tests.Modules.Games
{
[CollectionDefinition("Service")]
public class ServiceManagerTest
{
private readonly ITestOutputHelper output;
private readonly XUnitLogger logger;
public ServiceManagerTest(ITestOutputHelper output)
{
this.output = output;
logger = new XUnitLogger(output);
Logger.AddLogListener(logger);
}
[Fact]
public void CreateService_NewManager_NewServiceCreated()
{
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string FAKE_SERVICE_NAME = "CreateService_NewManager_NewServiceCreated";
FakePersistence<IReadOnlyDictionary<string, IServiceModule>> stubPersistentModuleDictionary = new FakePersistence<IReadOnlyDictionary<string, IServiceModule>>();
FakePersistence<ServiceDescriptor> stubPersistentServiceDictionary = new FakePersistence<ServiceDescriptor>();
FakeServiceManagerMonitor stubMonitor = new FakeServiceManagerMonitor();
ServiceManager serviceManager = new ServiceManager(stubMonitor, stubPersistentServiceDictionary, stubPersistentModuleDictionary);
Dictionary<string, IServiceModule> stubAssemblyModulesDictionary = new Dictionary<string, IServiceModule>();
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.Contains<string>(FAKE_SERVICE_NAME, stubMonitor.GetLastState().services);
}
[Fact]
public void CreateService_OneService_ServiceDeleted()
{
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string FAKE_SERVICE_NAME = "CreateService_OneService_ServiceDeleted";
FakePersistence<IReadOnlyDictionary<string, IServiceModule>> stubPersistentModuleDictionary = new FakePersistence<IReadOnlyDictionary<string, IServiceModule>>();
FakePersistence<ServiceDescriptor> stubPersistentServiceDictionary = new FakePersistence<ServiceDescriptor>();
FakeServiceManagerMonitor stubMonitor = new FakeServiceManagerMonitor();
ServiceManager serviceManager = new ServiceManager(stubMonitor, stubPersistentServiceDictionary, stubPersistentModuleDictionary);
Dictionary<string, IServiceModule> stubAssemblyModulesDictionary = new Dictionary<string, IServiceModule>();
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.DeleteService(FAKE_SERVICE_NAME);
//Then
Assert.True(stubMonitor.GetLastState().delta);
Assert.True(stubMonitor.GetLastState().subtract);
Assert.Contains(FAKE_SERVICE_NAME, stubMonitor.GetLastState().services);
}
[Fact]
public void GetServiceNames_MultipleServices_AllCorrectNames()
{
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string FAKE_SERVICE_PREFIX = "GetServiceNames_MultipleServices_AllCorrectNames_";
FakePersistence<IReadOnlyDictionary<string, IServiceModule>> stubPersistentModuleDictionary = new FakePersistence<IReadOnlyDictionary<string, IServiceModule>>();
FakePersistence<ServiceDescriptor> stubPersistentServiceDictionary = new FakePersistence<ServiceDescriptor>();
FakeServiceManagerMonitor stubMonitor = new FakeServiceManagerMonitor();
ServiceManager serviceManager = new ServiceManager(stubMonitor, stubPersistentServiceDictionary, stubPersistentModuleDictionary);
Dictionary<string, IServiceModule> stubAssemblyModulesDictionary = new Dictionary<string, IServiceModule>();
IServiceModule stubServiceModule = new FakeServiceModule();
stubAssemblyModulesDictionary.Add(stubServiceModule.Name, stubServiceModule);
stubPersistentModuleDictionary.AddToPersistence(ASSEMBLY_NAME, stubAssemblyModulesDictionary);
//When
for (int i = 0; i < 100; i++)
{
serviceManager.CreateService(FAKE_SERVICE_PREFIX + i, ASSEMBLY_NAME, stubServiceModule.Name);
}
//Then
for (int i = 0; i < 100; i++)
{
Assert.Contains<string>(FAKE_SERVICE_PREFIX + i, serviceManager.GetServiceNames());
}
}
[Fact]
public void GetServiceOptions_ThreeOptionService_CorrectOptions()
{
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string FAKE_SERVICE_NAME = "GetServiceOptions_ThreeOptionService_CorrectOptions";
FakePersistence<IReadOnlyDictionary<string, IServiceModule>> stubPersistentModuleDictionary = new FakePersistence<IReadOnlyDictionary<string, IServiceModule>>();
FakePersistence<ServiceDescriptor> stubPersistentServiceDictionary = new FakePersistence<ServiceDescriptor>();
FakeServiceManagerMonitor stubMonitor = new FakeServiceManagerMonitor();
ServiceManager serviceManager = new ServiceManager(stubMonitor, stubPersistentServiceDictionary, stubPersistentModuleDictionary);
Dictionary<string, IServiceModule> stubAssemblyModulesDictionary = new Dictionary<string, IServiceModule>();
IServiceModule stubServiceModule = new FakeServiceModule(
new FakeServiceConfigurable("A"),
new FakeServiceConfigurable("B"),
new FakeServiceConfigurable("C")
);
stubAssemblyModulesDictionary.Add(stubServiceModule.Name, stubServiceModule);
stubPersistentModuleDictionary.AddToPersistence(ASSEMBLY_NAME, stubAssemblyModulesDictionary);
//When
serviceManager.CreateService(FAKE_SERVICE_NAME, ASSEMBLY_NAME, stubServiceModule.Name);
//Then
Assert.Contains<string>("A", serviceManager.GetOptions()[FAKE_SERVICE_NAME].Keys);
Assert.Contains<string>("B", serviceManager.GetOptions()[FAKE_SERVICE_NAME].Keys);
Assert.Contains<string>("C", serviceManager.GetOptions()[FAKE_SERVICE_NAME].Keys);
}
[Fact]
public void SetandGetServiceOptionValue_OneOption_OptionChanged()
{
const string ASSEMBLY_NAME = "FakeAssembly";
const string FAKE_SERVICE_NAME = "SetandGetServiceOptionValue_OneOption_OptionChanged";
FakePersistence<IReadOnlyDictionary<string, IServiceModule>> stubPersistentModuleDictionary = new FakePersistence<IReadOnlyDictionary<string, IServiceModule>>();
FakePersistence<ServiceDescriptor> stubPersistentServiceDictionary = new FakePersistence<ServiceDescriptor>();
FakeServiceManagerMonitor stubMonitor = new FakeServiceManagerMonitor();
ServiceManager serviceManager = new ServiceManager(stubMonitor, stubPersistentServiceDictionary, stubPersistentModuleDictionary);
Dictionary<string, IServiceModule> stubAssemblyModulesDictionary = new Dictionary<string, IServiceModule>();
IServiceModule stubServiceModule = new FakeServiceModule(
new FakeServiceConfigurable("A")
);
stubAssemblyModulesDictionary.Add(stubServiceModule.Name, stubServiceModule);
stubPersistentModuleDictionary.AddToPersistence(ASSEMBLY_NAME, stubAssemblyModulesDictionary);
//When
serviceManager.CreateService(FAKE_SERVICE_NAME, ASSEMBLY_NAME, stubServiceModule.Name);
serviceManager.SetServiceOptionValue(FAKE_SERVICE_NAME, "A", "Test");
//Then
Assert.True("Test".Equals(serviceManager.GetOptions()[FAKE_SERVICE_NAME]["A"]));
}
[Fact]
public void GetServiceState_NotRunning_ReturnsNotRunningState()
{
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string FAKE_SERVICE_NAME = "GetServiceState_NotRunning_ReturnsNotRunningState";
FakePersistence<IReadOnlyDictionary<string, IServiceModule>> stubPersistentModuleDictionary = new FakePersistence<IReadOnlyDictionary<string, IServiceModule>>();
FakePersistence<ServiceDescriptor> stubPersistentServiceDictionary = new FakePersistence<ServiceDescriptor>();
FakeServiceManagerMonitor stubMonitor = new FakeServiceManagerMonitor();
ServiceManager serviceManager = new ServiceManager(stubMonitor, stubPersistentServiceDictionary, stubPersistentModuleDictionary);
Dictionary<string, IServiceModule> stubAssemblyModulesDictionary = new Dictionary<string, IServiceModule>();
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.DoesNotContain(FAKE_SERVICE_NAME, serviceManager.GetRunningServiceNames());
}
[Fact]
public void StartService_NotStarted_SuccessfulStart()
{
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string FAKE_SERVICE_NAME = "StartService_NotStarted_SuccessfulStart";
FakePersistence<IReadOnlyDictionary<string, IServiceModule>> stubPersistentModuleDictionary = new FakePersistence<IReadOnlyDictionary<string, IServiceModule>>();
FakePersistence<ServiceDescriptor> stubPersistentServiceDictionary = new FakePersistence<ServiceDescriptor>();
FakeServiceManagerMonitor stubMonitor = new FakeServiceManagerMonitor();
ServiceManager serviceManager = new ServiceManager(stubMonitor, stubPersistentServiceDictionary, stubPersistentModuleDictionary);
Dictionary<string, IServiceModule> stubAssemblyModulesDictionary = new Dictionary<string, IServiceModule>();
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);
//Then
Assert.Contains(FAKE_SERVICE_NAME, serviceManager.GetRunningServiceNames());
serviceManager.StopService(FAKE_SERVICE_NAME);
}
[Fact]
public void StopService_Stopped_StateUpdated()
{
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string FAKE_SERVICE_NAME = "StopService_Stopped_StateUpdated";
FakePersistence<IReadOnlyDictionary<string, IServiceModule>> stubPersistentModuleDictionary = new FakePersistence<IReadOnlyDictionary<string, IServiceModule>>();
FakePersistence<ServiceDescriptor> stubPersistentServiceDictionary = new FakePersistence<ServiceDescriptor>();
FakeServiceManagerMonitor stubMonitor = new FakeServiceManagerMonitor();
ServiceManager serviceManager = new ServiceManager(stubMonitor, stubPersistentServiceDictionary, stubPersistentModuleDictionary);
Dictionary<string, IServiceModule> stubAssemblyModulesDictionary = new Dictionary<string, IServiceModule>();
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);
serviceManager.StopService(FAKE_SERVICE_NAME);
//Then
Assert.DoesNotContain(FAKE_SERVICE_NAME, serviceManager.GetRunningServiceNames());
}
[Fact]
public void ExecuteCommand_CommandExecutedBeforeConnected_CommandLogged()
{
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string FAKE_SERVICE_NAME = "ExecuteCommand_CommandExecutedBeforeConnected_CommandLogged";
const string COMMAND = "TEST";
FakePersistence<IReadOnlyDictionary<string, IServiceModule>> stubPersistentModuleDictionary = new FakePersistence<IReadOnlyDictionary<string, IServiceModule>>();
FakePersistence<ServiceDescriptor> stubPersistentServiceDictionary = new FakePersistence<ServiceDescriptor>();
FakeServiceManagerMonitor stubMonitor = new FakeServiceManagerMonitor();
ServiceManager serviceManager = new ServiceManager(stubMonitor, stubPersistentServiceDictionary, stubPersistentModuleDictionary);
Dictionary<string, IServiceModule> stubAssemblyModulesDictionary = new Dictionary<string, IServiceModule>();
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);
serviceManager.ExecuteCommand(FAKE_SERVICE_NAME, COMMAND);
clientStream.Connect(1000);
Thread.Sleep(1000);
//Then
byte[] buffer = new byte[1024 * 8];
CancellationTokenSource cancelToken = new CancellationTokenSource();
ValueTask<int> task = clientStream.ReadAsync(buffer, cancelToken.Token);
Assert.False(task.AsTask().Wait(1000));
serviceManager.StopService(FAKE_SERVICE_NAME);
}
[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<IReadOnlyDictionary<string, IServiceModule>> stubPersistentModuleDictionary = new FakePersistence<IReadOnlyDictionary<string, IServiceModule>>();
FakePersistence<ServiceDescriptor> stubPersistentServiceDictionary = new FakePersistence<ServiceDescriptor>();
FakeServiceManagerMonitor stubMonitor = new FakeServiceManagerMonitor();
ServiceManager serviceManager = new ServiceManager(stubMonitor, stubPersistentServiceDictionary, stubPersistentModuleDictionary);
Dictionary<string, IServiceModule> stubAssemblyModulesDictionary = new Dictionary<string, IServiceModule>();
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))
{
CancellationTokenSource cancelToken = new CancellationTokenSource();
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}\"");
}
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<IReadOnlyDictionary<string, IServiceModule>> stubPersistentModuleDictionary = new FakePersistence<IReadOnlyDictionary<string, IServiceModule>>();
FakePersistence<ServiceDescriptor> stubPersistentServiceDictionary = new FakePersistence<ServiceDescriptor>();
FakeServiceManagerMonitor stubMonitor = new FakeServiceManagerMonitor();
ServiceManager serviceManager = new ServiceManager(stubMonitor, stubPersistentServiceDictionary, stubPersistentModuleDictionary);
Dictionary<string, IServiceModule> stubAssemblyModulesDictionary = new Dictionary<string, IServiceModule>();
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]))
{
CancellationTokenSource cancelToken = new CancellationTokenSource();
string message = null;
Task task = Task.Run(() => message = reader.ReadLine(), cancelToken.Token);
Assert.True(task.Wait(10000));
Assert.True(COMMAND.Equals(message), $"Received message \"{message}\" when expecting \"{COMMAND}\"");
}
}
serviceManager.StopService(FAKE_SERVICE_NAME);
}
[Fact]
public void GetServiceConsoleStream_ServiceStopped_ExceptionThrown()
{
//Given
const string ASSEMBLY_NAME = "FakeAssembly";
const string FAKE_SERVICE_NAME = "GetServiceConsoleStream_ServiceStopped_ExceptionThrown";
FakePersistence<IReadOnlyDictionary<string, IServiceModule>> stubPersistentModuleDictionary = new FakePersistence<IReadOnlyDictionary<string, IServiceModule>>();
FakePersistence<ServiceDescriptor> stubPersistentServiceDictionary = new FakePersistence<ServiceDescriptor>();
FakeServiceManagerMonitor stubMonitor = new FakeServiceManagerMonitor();
ServiceManager serviceManager = new ServiceManager(stubMonitor, stubPersistentServiceDictionary, stubPersistentModuleDictionary);
Dictionary<string, IServiceModule> stubAssemblyModulesDictionary = new Dictionary<string, IServiceModule>();
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<KeyNotFoundException>(() => serviceManager.GetLogPipeNames()[FAKE_SERVICE_NAME]);
}
}
}

View File

@ -1,95 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using System.IO;
using GameServiceWarden.Core.Games;
using GameServiceWarden.Core.Persistence;
using GameServiceWarden.Core.Tests.Modules;
using GameServiceWarden.Core.Tests.Modules.Games;
using GameServiceWarden.ModuleAPI;
using Xunit;
namespace GameServiceWarden.Core.Tests.Persistence
{
public class PersistedGameServiceInfosTest
{
//MethodTested_ScenarioTested_ExpectedBehavior
[Fact]
public void GetPathForKey_PathGen_ExpectedPathResult()
{
//Given
const string TEST_DIR = "services";
const string MODULE_NAME = "fake_module";
const string ASSEMBLY_NAME = "fake_assembly";
const string SERVICE_NAME = "fake_service";
FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>> stubModulesPersistence = new FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>>();
Dictionary<string, IGameServiceModule> stubAssemblyDict = new Dictionary<string, IGameServiceModule>();
FakeGameServiceModule stubGameServiceModule = new FakeGameServiceModule();
stubAssemblyDict[MODULE_NAME] = stubGameServiceModule;
stubModulesPersistence[ASSEMBLY_NAME] = stubAssemblyDict;
PersistedGameServiceInfos persistedGameServices = new PersistedGameServiceInfos(TEST_DIR, stubModulesPersistence);
//Then
Assert.True(persistedGameServices.GetPathForKey(SERVICE_NAME).Equals(Path.Combine(TEST_DIR, SERVICE_NAME)));
}
[Fact]
public void Save_SavingService_FileCreated()
{
//Given
const string TEST_DIR = "services";
const string MODULE_NAME = "fake_module";
const string ASSEMBLY_NAME = "fake_assembly";
const string SERVICE_NAME = "fake_service";
FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>> stubModulesPersistence = new FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>>();
Dictionary<string, IGameServiceModule> stubAssemblyDict = new Dictionary<string, IGameServiceModule>();
FakeGameServiceModule stubGameServiceModule = new FakeGameServiceModule();
stubAssemblyDict[MODULE_NAME] = stubGameServiceModule;
stubModulesPersistence[ASSEMBLY_NAME] = stubAssemblyDict;
PersistedGameServiceInfos persistedGameServiceInfos = new PersistedGameServiceInfos(TEST_DIR, stubModulesPersistence);
GameServiceInfo stubGameServiceInfo = new GameServiceInfo(stubModulesPersistence[ASSEMBLY_NAME][MODULE_NAME].InstantiateGameService(persistedGameServiceInfos.GetPathForKey(SERVICE_NAME), true), MODULE_NAME, ASSEMBLY_NAME);
//When
persistedGameServiceInfos[SERVICE_NAME] = stubGameServiceInfo;
//Then
Assert.True(Directory.Exists(TEST_DIR));
Assert.True(Directory.Exists(persistedGameServiceInfos.GetPathForKey(SERVICE_NAME)));
string[] files = Directory.GetFiles(persistedGameServiceInfos.GetPathForKey(SERVICE_NAME));
Assert.True(files.Length == 1);
Assert.StartsWith(SERVICE_NAME, Path.GetFileName(files[0]));
Directory.Delete(TEST_DIR, true);
}
[Fact]
public void Save_ReadingService_MetadataRead()
{
//Given
const string TEST_DIR = "services";
const string MODULE_NAME = "fake_module";
const string ASSEMBLY_NAME = "fake_assembly";
const string SERVICE_NAME = "fake_service";
FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>> stubModulesPersistence = new FakePersistence<IReadOnlyDictionary<string, IGameServiceModule>>();
Dictionary<string, IGameServiceModule> stubAssemblyDict = new Dictionary<string, IGameServiceModule>();
FakeGameServiceModule stubGameServiceModule = new FakeGameServiceModule();
stubAssemblyDict[MODULE_NAME] = stubGameServiceModule;
stubModulesPersistence[ASSEMBLY_NAME] = stubAssemblyDict;
PersistedGameServiceInfos persistedGameServices = new PersistedGameServiceInfos(TEST_DIR, stubModulesPersistence);
GameServiceInfo stubGameServiceInfo = new GameServiceInfo(stubModulesPersistence[ASSEMBLY_NAME][MODULE_NAME].InstantiateGameService(persistedGameServices.GetPathForKey(SERVICE_NAME), true), MODULE_NAME, ASSEMBLY_NAME);
persistedGameServices[SERVICE_NAME] = stubGameServiceInfo;
//When
GameServiceInfo loadedService = persistedGameServices[SERVICE_NAME];
//Then
Assert.True(loadedService.ModuleName.Equals(MODULE_NAME));
Assert.True(loadedService.GetAssemblyName().Equals(ASSEMBLY_NAME));
Directory.Delete(TEST_DIR, true);
}
}
}

View File

@ -0,0 +1,95 @@
using System.Collections;
using System.Collections.Generic;
using System.IO;
using GameServiceWarden.Core.Games;
using GameServiceWarden.Core.Persistence;
using GameServiceWarden.Core.Tests.Modules;
using GameServiceWarden.Core.Tests.Modules.Games;
using GameServiceWarden.API.Module;
using Xunit;
namespace GameServiceWarden.Core.Tests.Persistence
{
public class ServiceDescriptorPersistenceTest
{
//MethodTested_ScenarioTested_ExpectedBehavior
[Fact]
public void GetPathForKey_PathGen_ExpectedPathResult()
{
//Given
const string TEST_DIR = "services";
const string MODULE_NAME = "fake_module";
const string ASSEMBLY_NAME = "fake_assembly";
const string SERVICE_NAME = "fake_service";
FakePersistence<IReadOnlyDictionary<string, IServiceModule>> stubModulesPersistence = new FakePersistence<IReadOnlyDictionary<string, IServiceModule>>();
Dictionary<string, IServiceModule> stubAssemblyDict = new Dictionary<string, IServiceModule>();
FakeServiceModule stubServiceModule = new FakeServiceModule();
stubAssemblyDict[MODULE_NAME] = stubServiceModule;
stubModulesPersistence[ASSEMBLY_NAME] = stubAssemblyDict;
ServiceDescriptorPersistence persistedServices = new ServiceDescriptorPersistence(TEST_DIR, stubModulesPersistence);
//Then
Assert.True(persistedServices.GetPathForKey(SERVICE_NAME).Equals(Path.Combine(TEST_DIR, SERVICE_NAME)));
}
[Fact]
public void Save_SavingService_FileCreated()
{
//Given
const string TEST_DIR = "services";
const string MODULE_NAME = "fake_module";
const string ASSEMBLY_NAME = "fake_assembly";
const string SERVICE_NAME = "fake_service";
FakePersistence<IReadOnlyDictionary<string, IServiceModule>> stubModulesPersistence = new FakePersistence<IReadOnlyDictionary<string, IServiceModule>>();
Dictionary<string, IServiceModule> stubAssemblyDict = new Dictionary<string, IServiceModule>();
FakeServiceModule stubServiceModule = new FakeServiceModule();
stubAssemblyDict[MODULE_NAME] = stubServiceModule;
stubModulesPersistence[ASSEMBLY_NAME] = stubAssemblyDict;
ServiceDescriptorPersistence persistedServiceInfos = new ServiceDescriptorPersistence(TEST_DIR, stubModulesPersistence);
ServiceDescriptor stubServiceInfo = new ServiceDescriptor(stubModulesPersistence[ASSEMBLY_NAME][MODULE_NAME].InstantiateService(persistedServiceInfos.GetPathForKey(SERVICE_NAME), true), SERVICE_NAME, MODULE_NAME, ASSEMBLY_NAME);
//When
persistedServiceInfos[SERVICE_NAME] = stubServiceInfo;
//Then
Assert.True(Directory.Exists(TEST_DIR));
Assert.True(Directory.Exists(persistedServiceInfos.GetPathForKey(SERVICE_NAME)));
string[] files = Directory.GetFiles(persistedServiceInfos.GetPathForKey(SERVICE_NAME));
Assert.True(files.Length == 1);
Assert.StartsWith(SERVICE_NAME, Path.GetFileName(files[0]));
Directory.Delete(TEST_DIR, true);
}
[Fact]
public void Save_ReadingService_MetadataRead()
{
//Given
const string TEST_DIR = "services";
const string MODULE_NAME = "fake_module";
const string ASSEMBLY_NAME = "fake_assembly";
const string SERVICE_NAME = "fake_service";
FakePersistence<IReadOnlyDictionary<string, IServiceModule>> stubModulesPersistence = new FakePersistence<IReadOnlyDictionary<string, IServiceModule>>();
Dictionary<string, IServiceModule> stubAssemblyDict = new Dictionary<string, IServiceModule>();
FakeServiceModule stubServiceModule = new FakeServiceModule();
stubAssemblyDict[MODULE_NAME] = stubServiceModule;
stubModulesPersistence[ASSEMBLY_NAME] = stubAssemblyDict;
ServiceDescriptorPersistence persistedServices = new ServiceDescriptorPersistence(TEST_DIR, stubModulesPersistence);
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;
//When
ServiceDescriptor loadedService = persistedServices[SERVICE_NAME];
//Then
Assert.True(loadedService.GetModuleName().Equals(MODULE_NAME));
Assert.True(loadedService.GetAssemblyName().Equals(ASSEMBLY_NAME));
Directory.Delete(TEST_DIR, true);
}
}
}

View File

@ -0,0 +1,47 @@
using System;
using GameServiceWarden.Core.UI;
using Xunit;
namespace GameServiceWarden.Core.Tests.UI
{
public class IPCMediatorTest
{
[Fact]
public void Open_Closed_Opened()
{
//Given
const string NAME = "MEDIATOR";
IPCMediator mediator = new IPCMediator(NAME);
//When
mediator.Open();
//Then
Assert.True(mediator.IsRunning);
}
[Fact]
public void Open_AlreadyOpened_Exception()
{
//Given
const string NAME = "MEDIATOR";
IPCMediator mediator = new IPCMediator(NAME);
//When
mediator.Open();
//Then
Assert.Throws<InvalidOperationException>(() => mediator.Open());
}
[Fact]
public void Close_Opened_Closed()
{
//Given
const string NAME = "MEDIATOR";
IPCMediator mediator = new IPCMediator(NAME);
//When
mediator.Open();
//Then
Assert.True(mediator.IsRunning);
mediator.Close();
Assert.False(mediator.IsRunning);
}
}
}

View File

@ -0,0 +1,29 @@
using System;
using GameServiceWarden.Core.Logging;
using Xunit.Abstractions;
namespace GameServiceWarden.Core.Tests
{
public class XUnitLogger : ILogReceiver
{
public LogLevel Level => LogLevel.DEBUG;
public string Identifier => GetType().Name;
private ITestOutputHelper outputHelper;
public XUnitLogger(ITestOutputHelper output)
{
this.outputHelper = output;
}
public void Flush()
{
}
public void LogMessage(string message, DateTime time, LogLevel level)
{
outputHelper.WriteLine($"[{time.ToShortTimeString()}][{level.ToString()}]: {message}");
}
}
}