Updated to .NET 7.0 and added Jenkinsfile.

This commit is contained in:
2022-12-01 17:51:54 +00:00
parent 0073efc9ac
commit a1401c63e9
58 changed files with 79 additions and 7 deletions

View File

@@ -0,0 +1,95 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace GameServiceWarden.Core.Collection
{
public class LRUCache<K, V> : IEnumerable<V>
{
private class Node
{
public K key;
public V value;
public Node front;
public Node back;
}
public int Size {get { return size; } }
public int Count {get { return valueDictionary.Count; } }
private readonly int size;
private Node top;
private Node bottom;
private Dictionary<K, Node> valueDictionary;
private Action<V> cleanupAction;
public LRUCache(int size = 100, Action<V> cleanup = null)
{
this.size = size;
valueDictionary = new Dictionary<K, Node>(size);
this.cleanupAction = cleanup;
}
private void MoveToTop(K key) {
Node node = valueDictionary[key];
if (node != null && top != node) {
node.front.back = node.back;
node.back = top;
node.front = null;
top = node;
}
}
private Node AddToTop(K key, V value) {
Node node = new Node();
node.front = null;
node.back = top;
node.value = value;
node.key = key;
top = node;
if (bottom == null) {
bottom = node;
} else if (valueDictionary.Count == Size) {
valueDictionary.Remove(bottom.key);
cleanupAction?.Invoke(bottom.value);
bottom = bottom.front;
}
valueDictionary[key] = node;
return node;
}
public V Use(K key, Func<V> generate) {
if (generate == null) throw new ArgumentNullException("generate");
Node value = null;
if (valueDictionary.ContainsKey(key)) {
value = valueDictionary[key];
MoveToTop(key);
} else {
value = AddToTop(key, generate());
}
return value.value;
}
public bool IsCached(K key) {
return valueDictionary.ContainsKey(key);
}
public void Clear() {
top = null;
bottom = null;
valueDictionary.Clear();
}
public IEnumerator<V> GetEnumerator()
{
foreach (Node node in valueDictionary.Values)
{
yield return node.value;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\GameServiceWarden.ModuleFramework\GameServiceWarden.ModuleFramework.csproj" />
<ProjectReference Include="..\GameServiceWarden.InteractionAPI\GameServiceWarden.InteractionAPI.csproj" />
<ProjectReference Include="..\..\SimpleLogger\SimpleLogger.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
</PropertyGroup>
</Project>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,10 @@
using GameServiceWarden.InteractionAPI.Module;
namespace GameServiceWarden.Core.Module
{
public interface IServiceManagerMonitor
{
void Present(ServiceManagerTotal state);
void Present(ServiceManagerDelta delta);
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Reflection;
using System.Runtime.Loader;
namespace GameServiceWarden.Core.Module
{
class ModuleLoadContext : AssemblyLoadContext
{
private readonly AssemblyDependencyResolver dependencyResolver;
public ModuleLoadContext(string path) {
dependencyResolver = new AssemblyDependencyResolver(path);
}
protected override Assembly Load(AssemblyName assemblyName)
{
string assemblyPath = dependencyResolver.ResolveAssemblyToPath(assemblyName);
if (assemblyPath != null) {
return LoadFromAssemblyPath(assemblyPath);
}
return null;
}
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
String libraryPath = dependencyResolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (libraryPath != null) {
return LoadUnmanagedDllFromPath(libraryPath);
}
return IntPtr.Zero;
}
}
}

View File

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

View File

@@ -0,0 +1,156 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using GameServiceWarden.Core.Module.Exceptions;
using SimpleLogger;
using GameServiceWarden.ModuleFramework;
using System.Net.Sockets;
//TODO Update UML
namespace GameServiceWarden.Core.Module
{
public class ServiceDescriptor //entity
{
public const string MODULE_PROPERTY = "SERVICE_MODULE";
public const string ASSEMBLY_PROPERTY = "SERVICE_ASSEMBLY";
private const int TIMEOUT = 1000;
/// <summary>
/// The name of the service itself, independent of the name of the module this service is using.
/// </summary>
public string ServiceName { get { return serviceName; } }
private readonly string serviceName;
private volatile ServiceState state;
private readonly IService service;
private string moduleName;
private readonly string assemblyName;
/// <summary>
/// Name of module this service uses.
/// </summary>
private readonly IReadOnlyDictionary<string, IServiceConfigurable> configurables;
public event EventHandler<ServiceState> ServiceStateChangeEvent;
public event EventHandler<string> LogUpdateEvent;
public ServiceDescriptor(IService service, string serviceName, string moduleName, string assemblyName)
{
this.service = service ?? throw new ArgumentNullException("service");
this.moduleName = moduleName ?? throw new ArgumentNullException("moduleName");
this.assemblyName = assemblyName ?? throw new ArgumentNullException("assemblyName");
this.serviceName = serviceName ?? throw new ArgumentNullException("serviceName");
this.service.StateChangeEvent += OnServiceStateChange;
this.service.UpdateLogEvent += OnLogUpdate;
Dictionary<string, IServiceConfigurable> tempConfigurables = new Dictionary<string, IServiceConfigurable>();
foreach (IServiceConfigurable configurable in service.Configurables)
{
tempConfigurables.Add(configurable.OptionName, configurable);
}
this.configurables = new ReadOnlyDictionary<string, IServiceConfigurable>(tempConfigurables);
}
/// <summary>
/// Starts this service.
/// </summary>
/// <exception cref="InvalidOperationException">Is thrown when the service is already running.</exception>
public void Start()
{
Logger.Log($"\"{ServiceName}\" is starting.");
if (state == ServiceState.Running) throw new InvalidOperationException("Service instance already running.");
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(TIMEOUT);
Task initializationTask = Task.Run(() => service.InitializeService(), cancellationTokenSource.Token);
try {
initializationTask.Wait();
} catch (AggregateException a) {
a.Handle((e) => e is TaskCanceledException);
}
cancellationTokenSource.Dispose();
}
/// <summary>
/// Stops the service.
/// </summary>
/// <exception cref="InvalidOperationException">Is thrown when the is not running.</exception>
public void Stop()
{
if (state != ServiceState.Running) throw new InvalidOperationException("Service instance not running.");
Logger.Log($"\"{ServiceName}\" is stopping.");
service.ElegantShutdown();
}
/// <summary>
/// Sends a command to this service to execute.
/// </summary>
/// <param name="command">The command to execute.</param>
/// <exception cref="InvalidOperationException">Is thrown when the service is not running.</exception>
public void ExecuteCommand(string command)
{
Logger.Log($"\"{ServiceName}\" is executing command \"{command}\".", LogLevel.Debug);
if (state != ServiceState.Running) throw new InvalidOperationException("Service instance not running.");
service.ExecuteCommand(command);
}
/// <summary>
/// Gets the possible <see cref="IServiceConfigurable"/>'s names for this service.
/// </summary>
/// <returns>A <see cref="ISet{string}"/> returned where the string is the option's name.</returns>
public ISet<string> GetConfigurableOptions()
{
return new HashSet<string>(this.configurables.Keys);
}
public bool SetConfigurableValue(string configurationName, string value)
{
if (!this.configurables.ContainsKey(configurationName)) throw new KeyNotFoundException($"Unable to find option with name \"{configurationName}\".");
return this.configurables[configurationName].SetValue(value);
}
public string GetConfigurableValue(string configurationName)
{
if (!this.configurables.ContainsKey(configurationName)) throw new KeyNotFoundException($"Unable to find option with name \"{configurationName}\".");
return this.configurables[configurationName].GetValue();
}
public ServiceState GetServiceState()
{
return state;
}
public string GetServiceName()
{
return serviceName;
}
public string GetModuleName()
{
return moduleName;
}
/// <returns>The name of assembly this module is contained in.</returns>
public string GetAssemblyName()
{
return assemblyName;
}
public byte[] GetLogBuffer() {
if (state == ServiceState.Stopped) throw new InvalidOperationException("Cannot get log of service that is not running.");
return service.GetLogBuffer();
}
private void OnServiceStateChange(object sender, ServiceState state)
{
this.state = state;
Logger.Log($"The service \"{ServiceName}\" is changing states to {this.state}.", LogLevel.Debug);
ServiceStateChangeEvent?.Invoke(this, this.state);
}
private void OnLogUpdate(object sender, string log) {
LogUpdateEvent?.Invoke(this, log);
}
}
}

View File

@@ -0,0 +1,247 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using GameServiceWarden.InteractionAPI.Module;
using GameServiceWarden.Core.Persistence;
using GameServiceWarden.ModuleFramework;
using GameServiceWarden.Core.Collection;
using System.Text;
namespace GameServiceWarden.Core.Module
{
public class ServiceManager : IServiceManagerActionExecuter
{
private IServiceManagerMonitor managerMonitor;
private readonly ConcurrentDictionary<string, ServiceDescriptor> running;
private readonly IPersistent<IReadOnlyDictionary<string, string>> services;
private readonly IReadOnlyPersistent<IReadOnlyDictionary<string, IServiceModule>> modules;
private readonly LRUCache<string, ServiceDescriptor> descriptorCache;
public ServiceManager(IServiceManagerMonitor actionMonitor, IPersistent<IReadOnlyDictionary<string, string>> services, IReadOnlyPersistent<IReadOnlyDictionary<string, IServiceModule>> modules)
{
this.services = services ?? throw new ArgumentNullException("services");
this.modules = modules ?? throw new ArgumentNullException("modules");
this.managerMonitor = actionMonitor ?? throw new ArgumentNullException("actionMonitor");
this.running = new ConcurrentDictionary<string, ServiceDescriptor>();
this.descriptorCache = new LRUCache<string, ServiceDescriptor>(100);
}
public void CreateService(string serviceName, string assemblyName, string moduleName)
{
if (!this.modules.ContainsKey(assemblyName)) throw new KeyNotFoundException($"No file \"{assemblyName}\" found.");
IReadOnlyDictionary<string, IServiceModule> assemblyModules = this.modules[assemblyName];
if (services.ContainsKey(serviceName)) throw new ArgumentException($"Service of Name \"{serviceName}\" already exists.");
Dictionary<string, string> data = new Dictionary<string, string>();
data[ServiceDescriptor.ASSEMBLY_PROPERTY] = assemblyName;
data[ServiceDescriptor.MODULE_PROPERTY] = moduleName;
services.AddToPersistence(serviceName, data);
ServiceManagerDelta managerState = new ServiceManagerDelta();
managerState.subtract = false;
managerState.service = serviceName;
managerMonitor.Present(managerState);
}
public void DeleteService(string serviceName)
{
if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found.");
if (running.ContainsKey(serviceName)) running[serviceName].Stop();
services.Delete(serviceName);
ServiceManagerDelta managerState = new ServiceManagerDelta();
managerState.subtract = true;
managerState.service = serviceName;
managerMonitor.Present(managerState);
}
public IEnumerable<string> GetModuleNames()
{
ServiceManagerTotal managerState = new ServiceManagerTotal();
managerState.modules = modules.Keys.ToImmutableArray();
managerMonitor.Present(managerState);
return modules.Keys;
}
public IEnumerable<string> GetServiceNames()
{
ServiceManagerTotal managerState = new ServiceManagerTotal();
managerState.services = services.Keys.ToImmutableArray();
managerMonitor.Present(managerState);
return services.Keys;
}
public IEnumerable<string> GetRunningServiceNames() {
ServiceManagerTotal managerState = new ServiceManagerTotal();
managerState.running = running.Keys.ToImmutableArray();
managerMonitor.Present(managerState);
return running.Keys;
}
private IEnumerable<string> GetServiceOptions(string serviceName)
{
if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found.");
IReadOnlyDictionary<string, string> info = services[serviceName];
ServiceDescriptor service = descriptorCache.Use(serviceName, () => GenerateDescriptor(serviceName, info[ServiceDescriptor.ASSEMBLY_PROPERTY], info[ServiceDescriptor.MODULE_PROPERTY]));
return service.GetConfigurableOptions();
}
private string GetServiceOptionValue(string serviceName, string optionName)
{
if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found.");
IReadOnlyDictionary<string, string> info = services[serviceName];
ServiceDescriptor service = descriptorCache.Use(serviceName, () => GenerateDescriptor(serviceName, info[ServiceDescriptor.ASSEMBLY_PROPERTY], info[ServiceDescriptor.MODULE_PROPERTY]));
if (!service.GetConfigurableOptions().Contains(optionName)) throw new KeyNotFoundException($"Option \"{optionName}\" for service \"{serviceName}\" not found.");
return service.GetConfigurableValue(optionName);
}
public IReadOnlyDictionary<string, IReadOnlyDictionary<string, string>> GetOptions() {
ServiceManagerTotal managerState = new ServiceManagerTotal();
Dictionary<string, IReadOnlyDictionary<string, string>> serviceOptions = new Dictionary<string, IReadOnlyDictionary<string, string>>();
foreach (string service in GetServiceNames())
{
Dictionary<string, string> optionsOfService = new Dictionary<string, string>();
foreach (string option in GetServiceOptions(service))
{
optionsOfService.Add(option, GetServiceOptionValue(service, option));
}
serviceOptions.Add(service, optionsOfService);
}
managerState.serviceOptions = serviceOptions;
managerMonitor.Present(managerState);
return serviceOptions;
}
public bool SetServiceOptionValue(string serviceName, string optionName, string value)
{
if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found.");
IReadOnlyDictionary<string, string> info = services[serviceName];
ServiceDescriptor service = descriptorCache.Use(serviceName, () => GenerateDescriptor(serviceName, info[ServiceDescriptor.ASSEMBLY_PROPERTY], info[ServiceDescriptor.MODULE_PROPERTY]));
if (!service.GetConfigurableOptions().Contains(optionName)) throw new KeyNotFoundException($"Option \"{optionName}\" for service \"{serviceName}\" not found.");
ServiceManagerDelta managerState = new ServiceManagerDelta();
if (service.SetConfigurableValue(optionName, value)) {
managerState.optionName = optionName;
managerState.optionValue = GetServiceOptionValue(serviceName, optionName);
managerMonitor.Present(managerState);
return true;
}
return false;
}
public void StartService(string serviceName)
{
if (!services.ContainsKey(serviceName)) throw new KeyNotFoundException($"Service under name \"{serviceName}\" not found.");
if (running.ContainsKey(serviceName)) throw new InvalidOperationException($"Service under name \"{serviceName}\" is already running.");
IReadOnlyDictionary<string, string> info = services[serviceName];
ServiceDescriptor service = descriptorCache.Use(serviceName, () => GenerateDescriptor(serviceName, info[ServiceDescriptor.ASSEMBLY_PROPERTY], info[ServiceDescriptor.MODULE_PROPERTY]));
service.ServiceStateChangeEvent += OnServiceStateChange;
service.LogUpdateEvent += OnLogUpdated;
service.Start();
}
public void StopService(string serviceName)
{
if (!running.ContainsKey(serviceName)) throw new InvalidOperationException($"Service under name \"{serviceName}\" is not running.");
running[serviceName].Stop();
}
public void ExecuteCommand(string serviceName, string command)
{
if (!running.ContainsKey(serviceName)) throw new InvalidOperationException($"Service under name \"{serviceName}\" is not running.");
running[serviceName].ExecuteCommand(command);
}
private byte[] GetServiceLogBuffer(string serviceName) {
if (!running.ContainsKey(serviceName)) throw new InvalidOperationException($"Service under name \"{serviceName}\" is not running and therefore, does not have a log.");
IReadOnlyDictionary<string, string> info = services[serviceName];
ServiceDescriptor service = descriptorCache.Use(serviceName, () => GenerateDescriptor(serviceName, info[ServiceDescriptor.ASSEMBLY_PROPERTY], info[ServiceDescriptor.MODULE_PROPERTY]));
return service.GetLogBuffer();
}
public IReadOnlyDictionary<string, byte[]> GetLogBuffers() {
Dictionary<string, byte[]> logs = new Dictionary<string, byte[]>();
foreach (string service in running.Keys)
{
logs.Add(service, running[service].GetLogBuffer());
}
ServiceManagerTotal managerState = new ServiceManagerTotal();
managerState.logs = logs;
managerMonitor.Present(managerState);
return logs;
}
private ServiceDescriptor GenerateDescriptor(string name, string assembly, string module) {
return new ServiceDescriptor(modules[assembly][module].InstantiateService(services.GetPathForKey(name)), name, module, assembly);
}
private void OnServiceStateChange(object sender, ServiceState state) {
ServiceDescriptor serviceInfo = (ServiceDescriptor)sender;
ServiceManagerDelta managerChange = new ServiceManagerDelta();
switch (state)
{
case ServiceState.Running:
if (running.TryAdd(serviceInfo.ServiceName, serviceInfo)) {
managerChange.running = serviceInfo.ServiceName;
}
break;
case ServiceState.Stopped:
ServiceDescriptor removed;
if (running.TryRemove(serviceInfo.ServiceName, out removed)) {
removed.ServiceStateChangeEvent -= OnServiceStateChange;
removed.LogUpdateEvent -= OnLogUpdated;
Dictionary<string, string> removedInfo = new Dictionary<string, string>();
removedInfo[ServiceDescriptor.ASSEMBLY_PROPERTY] = removed.GetAssemblyName();
removedInfo[ServiceDescriptor.MODULE_PROPERTY] = removed.GetModuleName();
services[serviceInfo.ServiceName] = removedInfo;
managerChange.subtract = true;
managerChange.running = serviceInfo.ServiceName;
}
break;
}
managerMonitor.Present(managerChange);
}
void OnLogUpdated(object sender, string update) {
ServiceDescriptor service = (ServiceDescriptor)sender;
ServiceManagerDelta delta = new ServiceManagerDelta();
delta.logs = Encoding.UTF8.GetBytes(update);
managerMonitor.Present(delta);
}
public void ExecuteAction(ServiceManagerAction action)
{
switch (action.action)
{
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;
}
}
public void View()
{
GetServiceNames();
GetRunningServiceNames();
GetModuleNames();
GetLogBuffers();
GetOptions();
}
}
}

View File

@@ -0,0 +1,14 @@
using System.Collections.Generic;
using GameServiceWarden.Core.Module;
namespace GameServiceWarden.Core.Persistence
{
public interface IPersistent<V> : IReadOnlyPersistent<V>
{
new V this[string key] { get; set; }
void AddToPersistence(string key, V value);
void Clear();
bool Delete(string key);
}
}

View File

@@ -0,0 +1,29 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using GameServiceWarden.ModuleFramework;
namespace GameServiceWarden.Core.Persistence
{
public interface IReadOnlyPersistent<V> : IEnumerable<KeyValuePair<string, V>>
{
V this[string key] { get; }
/// <summary>
/// The directory for this dictionary to use.
/// </summary>
string MapDirectory { get; }
int Count { get; }
IEnumerable<V> Values { get; }
IEnumerable<string> Keys { get; }
bool ContainsKey(string key);
/// <summary>
/// The path to the data representing a specific key.
/// </summary>
/// <param name="key">The key to get the path for.</param>
/// <returns>A <see cref="string"/> representing the key.</returns>
string GetPathForKey(string key);
bool TryLoadValue(string key, [MaybeNullWhen(false)] out V value);
}
}

View File

@@ -0,0 +1,172 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using GameServiceWarden.Core.Module;
using GameServiceWarden.ModuleFramework;
using System.Collections.ObjectModel;
namespace GameServiceWarden.Core.Persistence
{
public class ServiceDescriptorPersistence : IPersistent<IReadOnlyDictionary<string, string>>
{
private readonly string mapDirectory;
private const string EXTENSION = ".sin";
public ServiceDescriptorPersistence(string mapDirectory)
{
this.mapDirectory = mapDirectory;
}
public IReadOnlyDictionary<string, string> this[string key]
{
set
{
SaveService(key, value[ServiceDescriptor.ASSEMBLY_PROPERTY], value[ServiceDescriptor.MODULE_PROPERTY]);
}
get
{
if (!ContainsKey(key)) throw new KeyNotFoundException();
Dictionary<string, string> info = new Dictionary<string, string>();
info[ServiceDescriptor.ASSEMBLY_PROPERTY] = GetServiceInfoValue(key, ServiceDescriptor.ASSEMBLY_PROPERTY);
info[ServiceDescriptor.MODULE_PROPERTY] = GetServiceInfoValue(key, ServiceDescriptor.MODULE_PROPERTY);
return new ReadOnlyDictionary<string, string>(info);
}
}
public string MapDirectory { get { return mapDirectory; } }
public int Count
{
get
{
return Directory.GetFiles(mapDirectory).Length;
}
}
public IEnumerable<IReadOnlyDictionary<string,string>> Values {
get {
IEnumerable<string> keys = Keys;
List<IReadOnlyDictionary<string,string>> res = new List<IReadOnlyDictionary<string,string>>();
foreach (string key in keys)
{
res.Add(this[key]);
}
return res;
}
}
public IEnumerable<string> Keys {
get {
IEnumerable<string> keys = Directory.EnumerateDirectories(mapDirectory);
List<string> res = new List<string>();
foreach (string key in keys)
{
res.Add(Path.GetDirectoryName(key));
}
return res;
}
}
public void AddToPersistence(string key, IReadOnlyDictionary<string,string> value)
{
if (key == null) throw new ArgumentNullException();
if (ContainsKey(key)) throw new ArgumentException();
this[key] = value;
}
public void Clear()
{
IEnumerable<string> directories = Keys;
foreach (string directory in directories)
{
Directory.Delete(directory);
}
}
public bool ContainsKey(string key)
{
return Directory.Exists(GetPathForKey(key));
}
public IEnumerator<KeyValuePair<string, IReadOnlyDictionary<string,string>>> GetEnumerator()
{
IEnumerable<string> keys = Keys;
List<KeyValuePair<string, IReadOnlyDictionary<string,string>>> result = new List<KeyValuePair<string, IReadOnlyDictionary<string,string>>>();
foreach (string key in keys)
{
result.Add(new KeyValuePair<string, IReadOnlyDictionary<string,string>>(key, this[key]));
}
return result.GetEnumerator();
}
public string GetPathForKey(string key)
{
return Path.Combine(mapDirectory, key);
}
public bool Delete(string key)
{
if (!ContainsKey(key)) return false;
try
{
Directory.Delete(GetPathForKey(key));
}
catch (System.Exception)
{
return false;
throw;
}
return true;
}
public bool TryLoadValue(string key, [MaybeNullWhen(false)] out IReadOnlyDictionary<string,string> value)
{
try {
value = this[key];
} catch (KeyNotFoundException) {
value = null;
return false;
}
return true;
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public void SaveService(string key, string assemblyName, string moduleName)
{
if (key == null) throw new ArgumentNullException("key");
if (assemblyName == null) throw new ArgumentNullException("assemblyName");
if (moduleName == null) throw new ArgumentNullException("moduleName");
string serviceInfoPath = GetPathForKey(key);
Directory.CreateDirectory(serviceInfoPath);
using (StreamWriter writer = File.CreateText(Path.Combine(serviceInfoPath, key + EXTENSION)))
{
writer.WriteLine($"{ServiceDescriptor.ASSEMBLY_PROPERTY}: {assemblyName}");
writer.WriteLine($"{ServiceDescriptor.MODULE_PROPERTY}: {moduleName}");
}
}
private string GetServiceInfoValue(string key, string value)
{
string path = GetPathForKey(key);
IEnumerable<string> lines = File.ReadAllLines(Path.Combine(path, key + EXTENSION));
foreach (string line in lines)
{
if (line.StartsWith($"{value}: "))
{
return line.Substring(value.Length + 2);
}
}
throw new FormatException($"\"{path}\" is corrupted. Could not find value for: {value}.");
}
}
}

View File

@@ -0,0 +1,128 @@
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using GameServiceWarden.Core.Module;
using GameServiceWarden.ModuleFramework;
namespace GameServiceWarden.Core.Persistence
{
public class ServiceModules : IReadOnlyPersistent<IReadOnlyDictionary<string, IServiceModule>>
{
private readonly string mapDirectory;
private readonly ModuleLoader loader = new ModuleLoader();
public ServiceModules(string mapDirectory)
{
this.mapDirectory = mapDirectory;
}
public IReadOnlyDictionary<string, IServiceModule> this[string key]
{
get
{
if (!ContainsKey(key)) throw new KeyNotFoundException($"Key \"{key}\" not found.");
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, IServiceModule>(res);
}
}
public string MapDirectory
{
get
{
return mapDirectory;
}
}
public IEnumerable<string> Keys
{
get
{
IEnumerable<string> files = Directory.EnumerateFiles(mapDirectory);
foreach (string file in files)
{
if (Path.GetExtension(file).ToLower().Equals("dll"))
{
yield return Path.GetFileName(file);
}
}
}
}
public IEnumerable<IReadOnlyDictionary<string, IServiceModule>> Values
{
get
{
IEnumerable<string> keys = Keys;
foreach (string key in keys)
{
yield return this[key];
}
}
}
public int Count
{
get
{
int count = 0;
IEnumerable<string> files = Directory.EnumerateFiles(mapDirectory);
foreach (string file in files)
{
if (Path.GetExtension(file).ToLower().Equals("dll"))
{
count++;
}
}
return count;
}
}
public bool ContainsKey(string key)
{
string path = GetPathForKey(key);
return File.Exists(path) && Path.GetExtension(path).ToLower().Equals("dll");
}
public IEnumerator<KeyValuePair<string, IReadOnlyDictionary<string, IServiceModule>>> GetEnumerator()
{
IEnumerable<string> keys = Keys;
foreach (string key in keys)
{
yield return new KeyValuePair<string, IReadOnlyDictionary<string, IServiceModule>>(key, this[key]);
}
}
public string GetPathForKey(string key)
{
return mapDirectory + Path.DirectorySeparatorChar + key;
}
public bool TryLoadValue(string key, [MaybeNullWhen(false)] out IReadOnlyDictionary<string, IServiceModule> value)
{
try
{
value = this[key];
return true;
}
catch (KeyNotFoundException)
{
value = null;
return false;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

View File

@@ -0,0 +1,12 @@
using System;
namespace GameServiceWarden.Core
{
class Program
{
static void Main(string[] args)
{
}
}
}

View File

@@ -0,0 +1,41 @@
using System.Diagnostics;
using System.Text.Json;
using GameServiceWarden.InteractionAPI;
using GameServiceWarden.InteractionAPI.Communicable.Requests;
using GameServiceWarden.Core.Module;
using SimpleLogger;
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.Delta:
ServiceRequest delta = JsonSerializer.Deserialize<ServiceRequest>(action.Item3);
serviceExecutioner.ExecuteAction(delta.serviceManagerAction);
break;
case CommunicableType.View:
serviceExecutioner.View();
break;
}
}
mediator.Close();
}
}
}

View File

@@ -0,0 +1,284 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipes;
using System.Net.Sockets;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using GameServiceWarden.InteractionAPI;
using GameServiceWarden.InteractionAPI.Communicable.Requests;
using GameServiceWarden.InteractionAPI.Communicable.Responses;
using SimpleLogger;
namespace GameServiceWarden.Core.UI
{
public class IPCMediator
{
private const int TIMEOUT = 1000;
public string PipeName {get { return name + ".pipe"; } }
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 connectionTask;
private CancellationTokenSource stopAcceptingToken;
private volatile NamedPipeServerStream connectingPipe;
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.");
Logger.Log($"IPCMediator with name \"{name}\" opened.");
active = true;
stopAcceptingToken = new CancellationTokenSource();
connectionTask = AcceptConnections();
Logger.Log($"IPCMediator \"{name}\" has begun asynchronously accepting interfaces.", LogLevel.Debug);
}
public void Close()
{
if (!active) throw new InvalidOperationException("IPC not open.");
Logger.Log("Closing IPC mediator.");
active = false;
stopAcceptingToken.Cancel();
connectingPipe.Dispose();
InitiateDisconnectAll("Closing GameServiceWarden.").Wait();
try
{
if (!connectionTask.Wait(TIMEOUT)) throw new TimeoutException($"IPCMediator \"{name}\" was unable to stop accepting task within {TIMEOUT}ms.");
}
catch (AggregateException e)
{
e.Handle((exception) => exception is TaskCanceledException || (exception is SocketException && exception.Message.Equals("Operation canceled")));
}
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 Interface connections.");
while (active)
{
connectingPipe = new NamedPipeServerStream(PipeName, PipeDirection.InOut, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
Logger.Log("Waiting for connection.", LogLevel.Debug);
await connectingPipe.WaitForConnectionAsync(stopAcceptingToken.Token);
connectionTasks.Add(OnConnection(connectingPipe));
for (int i = 0; i < connectionTasks.Count; i++)
{
if (connectionTasks[i].IsCompleted) {
Task connectionTask = connectionTasks[i];
connectionTasks.RemoveAt(i);
connectionTask.Wait();
}
}
}
Logger.Log("Waiting for connection tasks.", LogLevel.Debug);
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(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 {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(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 {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(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 {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.TryRemove(identifier, out removedPipe);
await removedPipe.Item1.DisposeAsync();
Logger.Log($"Stopped listening to interface \"{identifier}\".", LogLevel.Debug);
}
}
}

View File

@@ -0,0 +1,30 @@
using System.Text.Json;
using System.Threading.Tasks;
using GameServiceWarden.InteractionAPI;
using GameServiceWarden.InteractionAPI.Module;
using GameServiceWarden.Core.Module;
namespace GameServiceWarden.Core.UI
{
public class IPCPresenter : IServiceManagerMonitor
{
private IPCMediator mediator;
public IPCPresenter(IPCMediator mediator)
{
this.mediator = mediator;
}
public void Present(ServiceManagerTotal state)
{
Task replyTask = mediator.ReplyAll(CommunicableType.View, JsonSerializer.SerializeToUtf8Bytes(state));
replyTask.Wait();
}
public void Present(ServiceManagerDelta delta)
{
Task replyTask = mediator.ReplyAll(CommunicableType.Delta, JsonSerializer.SerializeToUtf8Bytes(delta));
replyTask.Wait();
}
}
}

View File

@@ -0,0 +1,546 @@
<mxfile host="65bd71144e" pages="2">
<diagram id="LHR7ubqCPd17_LyHkaH9" name="Structure">
<mxGraphModel dx="518" 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">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<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="420" as="geometry">
<mxRectangle x="762" y="1030" width="130" height="26" as="alternateBounds"/>
</mxGeometry>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-6" value="+ ServiceName: string property&#10;- serviceName: string&#10;- runningUID: Guid&#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="204" 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;" parent="dmd0HlDYcxYugIlahWj0-5" vertex="1">
<mxGeometry y="230" width="510" height="8" as="geometry"/>
</mxCell>
<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="238" width="510" height="182" 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;" 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;" parent="1" source="dmd0HlDYcxYugIlahWj0-11" target="wwlaSBDwwZOn0hO83bWU-2" 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;" parent="1" source="dmd0HlDYcxYugIlahWj0-11" target="zFFzFwxISwJASp9ezwbr-1" 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;" parent="1" source="dmd0HlDYcxYugIlahWj0-11" target="fdKXkHfjRXYybK0fejAG-9" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<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="39" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;" edge="1" parent="1" source="dmd0HlDYcxYugIlahWj0-11" target="35">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<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="380" as="geometry">
<mxRectangle x="25" y="490" width="120" height="26" as="alternateBounds"/>
</mxGeometry>
</mxCell>
<mxCell id="dmd0HlDYcxYugIlahWj0-12" value="- managerMonitor: IServiceManagerMonitor&#10;- running: ConcurrentDictionary&lt;string, ServiceDescriptor&gt;&#10;- services: IPersistent&lt;IReadOnlyDictionary&lt;string, string&gt;&gt;&#10;- modules: IReadOnlyPersistent&lt;IReadOnlyDictionary&lt;string, IServiceModule&gt;&gt;&#10;- descriptorCache: LRUCache&lt;string, ServiceDescriptor&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="84" 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;" parent="dmd0HlDYcxYugIlahWj0-11" vertex="1">
<mxGeometry y="110" 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;+ 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;- GetServiceLogBuffer(serviceName: string): byte[]&#10;+ GetLogBuffers(): IReadOnlyDictionary&lt;string, byte[]&gt;&#10;- GenerateDescriptor(name: string, assembly: string, module: string): ServiceDescriptor&#10;- OnServiceStateChange(sender: object, state: bool): void&#10;- OnLogUpdated(sender: object, update: 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;" parent="dmd0HlDYcxYugIlahWj0-11" vertex="1">
<mxGeometry y="118" width="561" height="262" as="geometry"/>
</mxCell>
<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="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">
<mxPoint x="1414.5" y="133" as="targetPoint"/>
</mxGeometry>
</mxCell>
<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="wwlaSBDwwZOn0hO83bWU-2" value="&lt;&lt;Interface&gt;&gt;&#10;IServiceExecuter" 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="90" 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;" parent="wwlaSBDwwZOn0hO83bWU-2" vertex="1">
<mxGeometry y="40" width="305" height="8" as="geometry"/>
</mxCell>
<mxCell id="wwlaSBDwwZOn0hO83bWU-5" value="+ ExecuteAction(action: ServiceManagerAction): void&#10;+ View(): 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="42" 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;" parent="1" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="850" y="78" as="sourcePoint"/>
</mxGeometry>
</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;" parent="1" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="340" y="192" as="targetPoint"/>
</mxGeometry>
</mxCell>
<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="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-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="41" value="Use" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;dashed=1;endArrow=open;endFill=0;" edge="1" parent="1" source="zFFzFwxISwJASp9ezwbr-1" target="35">
<mxGeometry relative="1" as="geometry"/>
</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="460" width="295" height="90" 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&#10;+ Present(delta: ServiceManagerDelta): 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="42" as="geometry"/>
</mxCell>
<mxCell id="fdKXkHfjRXYybK0fejAG-9" value="&lt;&lt;DS&gt;&gt;&#10;ServiceManagerTotal" 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="850" width="485" height="138" as="geometry"/>
</mxCell>
<mxCell id="fdKXkHfjRXYybK0fejAG-10" value="+ 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="90" 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="130" 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" 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="80" width="520" height="350" as="geometry"/>
</mxCell>
<mxCell id="7" value="- CONNECT_TIMEOUT: int&#10;+ PipeName: property string property (appends &quot;.pipe&quot;)&#10;- name: string&#10;- pipes: 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;- connectionTask: Task&#10;- stopAcceptingToken: CancellationTokenSource&#10;- connectingPipe: NamedPipeServerStream" 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="154" 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="180" 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="188" 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="252" width="250" height="60" 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="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>
<mxCell id="35" value="&lt;&lt;DS&gt;&gt;&#10;ServiceManagerDelta" 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;" vertex="1" parent="1">
<mxGeometry x="440" y="630" width="160" height="158" as="geometry"/>
</mxCell>
<mxCell id="36" value="+ substract: bool&#10;+ service: string&#10;+ running: string&#10;+ modules: string&#10;+ logs: byte[]&#10;+ optionName: string&#10;+ optionValue: 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;" vertex="1" parent="35">
<mxGeometry y="40" width="160" height="110" as="geometry"/>
</mxCell>
<mxCell id="37" value="" style="line;strokeWidth=1;fillColor=none;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" vertex="1" parent="35">
<mxGeometry y="150" width="160" 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;
<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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<root>
&#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;
<mxCell id="jVG6p58vlRYGO9X4wXeX-1" parent="jVG6p58vlRYGO9X4wXeX-0"/>
&#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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<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; &#xa; &#xa;&#xa;&#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;
</mxCell>
&#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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<mxCell id="jVG6p58vlRYGO9X4wXeX-3" value="Console View" style="whiteSpace=wrap;html=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#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;
</mxCell>
&#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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<mxCell id="jVG6p58vlRYGO9X4wXeX-4" value="string command (request)" style="whiteSpace=wrap;html=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#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;
</mxCell>
&#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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<mxCell id="jVG6p58vlRYGO9X4wXeX-5" value="MainController" style="whiteSpace=wrap;html=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#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;
</mxCell>
&#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; &#xa; &#xa;&#xa;&#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;
</mxCell>
&#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; &#xa; &#xa;&#xa;&#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;
</mxCell>
&#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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry">
&#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;
</mxGeometry>
&#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;
<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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<mxCell id="jVG6p58vlRYGO9X4wXeX-9" value="ServiceController" style="whiteSpace=wrap;html=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#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;
</mxCell>
&#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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry">
&#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;
<mxPoint x="960" y="338"/>
&#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;
</Array>
&#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;
</mxCell>
&#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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry">
&#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;
<mxPoint x="960" y="338"/>
&#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;
</Array>
&#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;
</mxCell>
&#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; &#xa; &#xa;&#xa;&#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;
</mxCell>
&#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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<mxCell id="28FAlPysTx9DMYvLwa-2-5" value="ServicePresenter" style="whiteSpace=wrap;html=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#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;
</mxCell>
&#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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<mxCell id="28FAlPysTx9DMYvLwa-2-7" value="String Output" style="whiteSpace=wrap;html=1;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#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;
</mxCell>
&#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; &#xa; &#xa;&#xa;&#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;
</mxCell>
&#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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<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; &#xa; &#xa;&#xa;&#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;
</mxCell>
&#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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<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; &#xa; &#xa;&#xa;&#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;
</mxCell>
&#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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<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; &#xa; &#xa;&#xa;&#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;
</mxCell>
&#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; &#xa; &#xa;&#xa;&#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;
</mxCell>
&#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; &#xa; &#xa;&#xa;&#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;
</mxCell>
&#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; &#xa; &#xa;&#xa;&#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;
</mxCell>
&#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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<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; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
<mxGeometry relative="1" as="geometry"/>
&#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;
<mxCell id="tM_Gde3HH8YiZ2frBV5J-0" value="MainPresenter" style="html=1;dashed=0;whitespace=wrap;" parent="jVG6p58vlRYGO9X4wXeX-1" vertex="1">
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#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;
</mxCell>
&#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; &#xa; &#xa;&#xa;&#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;
</mxCell>
&#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;
</mxGraphModel>
&#xa; &#xa; &#xa; &#xa; &#xa; &#xa; &#xa;&#xa;&#xa;&#xa;&#xa;&#xa;&#xa;
</diagram>
</mxfile>

View File

@@ -0,0 +1,13 @@
using System;
namespace GameServiceWarden.InteractionAPI
{
public enum CommunicableType : uint
{
Disconnect,
Connect,
View,
Delta,
UnexpectedCommunication
}
}

View File

@@ -0,0 +1,11 @@
namespace GameServiceWarden.InteractionAPI.Communicable.Requests
{
public struct ConnectRequest
{
public string requestedIdentifier;
public string programName;
public string programAuthor;
public string versionNumber;
public string details;
}
}

View File

@@ -0,0 +1,9 @@
using GameServiceWarden.InteractionAPI.Module;
namespace GameServiceWarden.InteractionAPI.Communicable.Requests
{
public struct ServiceRequest
{
public ServiceManagerAction serviceManagerAction;
}
}

View File

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

View File

@@ -0,0 +1,13 @@
using System;
using System.IO.Pipes;
namespace GameServiceWarden.InteractionAPI.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,12 @@
using System.Security.Cryptography.X509Certificates;
namespace GameServiceWarden.InteractionAPI.Communicable.Responses
{
public struct ConnectResponse
{
public string identifier;
public bool nameTaken;
public bool invalidName;
public string errorMsg;
}
}

View File

@@ -0,0 +1,9 @@
using GameServiceWarden.InteractionAPI.Module;
namespace GameServiceWarden.InteractionAPI.Communicable.Responses
{
public struct DeltaResponse
{
public ServiceManagerTotal gameServiceDelta;
}
}

View File

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

View File

@@ -0,0 +1,14 @@
using System;
namespace GameServiceWarden.InteractionAPI.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,8 @@
namespace GameServiceWarden.InteractionAPI.Communicable.Responses
{
public struct UnexpectedRequestResponse
{
public CommunicableType origin;
public string message;
}
}

View File

@@ -0,0 +1,8 @@
using GameServiceWarden.InteractionAPI.Module;
namespace GameServiceWarden.InteractionAPI.Communicable.Responses
{
public struct ViewResponse {
public ServiceManagerTotal state;
}
}

View File

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

View File

@@ -0,0 +1,24 @@
namespace GameServiceWarden.InteractionAPI.Module
{
public struct ServiceManagerAction
{
public enum Type
{
Start,
Stop,
CreateService,
DeleteService,
ExecuteCommand,
SetServiceOption,
}
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.InteractionAPI.Module
{
public struct ServiceManagerDelta
{
public bool subtract;
public string service;
public string running;
public string modules;
public byte[] logs;
public string optionName;
public string optionValue;
}
}

View File

@@ -0,0 +1,13 @@
using System.Collections.Generic;
namespace GameServiceWarden.InteractionAPI.Module
{
public struct ServiceManagerTotal
{
public ICollection<string> services;
public ICollection<string> running;
public ICollection<string> modules;
public IReadOnlyDictionary<string, byte[]> logs;
public IReadOnlyDictionary<string, IReadOnlyDictionary<string, string>> serviceOptions;
}
}

View File

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

View File

@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace GameServiceWarden.ModuleFramework
{
public interface IService
{
event EventHandler<ServiceState> StateChangeEvent;
event EventHandler<string> UpdateLogEvent;
IReadOnlyCollection<IServiceConfigurable> Configurables{ get; }
void InitializeService();
void ElegantShutdown();
byte[] GetLogBuffer();
void ExecuteCommand(string command);
}
}

View File

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

View File

@@ -0,0 +1,29 @@
using System.Collections.Generic;
namespace GameServiceWarden.ModuleFramework
{
public interface IServiceModule
{
/// <summary>
/// The name of the game service this module handles.
/// </summary>
string Name { get; }
/// <summary>
/// Description of the game service this module handles.
/// </summary>
string Description { get; }
/// <summary>
/// The authors responsible for creating this module.
/// </summary>
IEnumerable<string> Authors { get; }
/// <summary>
/// Creates an instance of a the service to be used.
/// </summary>
/// <param name="workspace">The workspace directory. All service required files should be stored here. Expect the directory to be created.</param>
/// <returns>The <see cref="IService"/> responsible for the instance of the game service.</returns>
IService InstantiateService(string workspace);
}
}

View File

@@ -0,0 +1,9 @@
namespace GameServiceWarden.ModuleFramework
{
public enum ServiceState
{
Stopped,
Running,
RestartPending
}
}

View File

@@ -0,0 +1,34 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameServiceWarden.Core", "GameServiceWarden.Core\GameServiceWarden.Core.csproj", "{72FE4FFA-8730-4043-BCE9-794E816542CA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameServiceWarden.InteractionAPI", "GameServiceWarden.InteractionAPI\GameServiceWarden.InteractionAPI.csproj", "{A254A364-8089-4799-8F45-02E683D59F75}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameServiceWarden.ModuleFramework", "GameServiceWarden.ModuleFramework\GameServiceWarden.ModuleFramework.csproj", "{23F7A773-D146-4522-8200-BDD60E39B79F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{72FE4FFA-8730-4043-BCE9-794E816542CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{72FE4FFA-8730-4043-BCE9-794E816542CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{72FE4FFA-8730-4043-BCE9-794E816542CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{72FE4FFA-8730-4043-BCE9-794E816542CA}.Release|Any CPU.Build.0 = Release|Any CPU
{A254A364-8089-4799-8F45-02E683D59F75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A254A364-8089-4799-8F45-02E683D59F75}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A254A364-8089-4799-8F45-02E683D59F75}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A254A364-8089-4799-8F45-02E683D59F75}.Release|Any CPU.Build.0 = Release|Any CPU
{23F7A773-D146-4522-8200-BDD60E39B79F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{23F7A773-D146-4522-8200-BDD60E39B79F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{23F7A773-D146-4522-8200-BDD60E39B79F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{23F7A773-D146-4522-8200-BDD60E39B79F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal