Explorar el Código

ProcessServer basic, not complete (missing workers)

Dalibor Votruba hace 4 años
padre
commit
c22f0426ae
Se han modificado 50 ficheros con 1763 adiciones y 49 borrados
  1. 43 9
      AppServer/Connector.PS/Connection.cs
  2. 1 1
      AppServer/Connector/AbstractConnection.cs
  3. 20 11
      AppServer/Web/Services/PSController.cs
  4. 5 2
      AppServer/Web/appsettings.json
  5. 30 0
      Common/qdr.fnd.core.nlog/LogSeverityEnumExt.cs
  6. 43 0
      Common/qdr.fnd.core.nlog/Logger.cs
  7. 45 0
      Common/qdr.fnd.core.nlog/LoggerFactory.cs
  8. 21 0
      Common/qdr.fnd.core.nlog/qdr.fnd.core.nlog.csproj
  9. 1 1
      Common/qdr.fnd.core.qconsole/Value/DateTimeValue.cs
  10. 1 1
      Common/qdr.fnd.core.qconsole/Value/DateValue.cs
  11. 1 2
      Common/qdr.fnd.core.qconsole/Value/TimeValue.cs
  12. 24 0
      Common/qdr.fnd.core/IO/Extensions/DirectoryInfoExt.cs
  13. 162 0
      Common/qdr.fnd.core/Thread/AbstractWorker.cs
  14. 40 0
      Common/qdr.fnd.core/Thread/ContinousWorker.cs
  15. 38 0
      Common/qdr.fnd.core/Thread/Extensions/TaskExt.cs
  16. 15 0
      Common/qdr.fnd.core/Thread/IWorker.cs
  17. 6 0
      Common/qdr.fnd.core/Thread/IWorkerContext.cs
  18. 87 0
      Common/qdr.fnd.core/Thread/LoopWorker.cs
  19. 30 0
      ProcessServer/Business/DocumentContent.cs
  20. 14 0
      ProcessServer/Business/Enums/AppModeEnum.cs
  21. 13 0
      ProcessServer/Business/Enums/ServerProcessStateEnum.cs
  22. 181 0
      ProcessServer/Business/Pool.cs
  23. 221 0
      ProcessServer/Business/Server.cs
  24. 16 0
      ProcessServer/Business/ServerProcess.cs
  25. 16 0
      ProcessServer/Business/StatisticCounters/BaseCounter.cs
  26. 49 0
      ProcessServer/Business/StatisticCounters/IncrementalCounter.cs
  27. 76 0
      ProcessServer/Business/StatisticCounters/Statistics.cs
  28. 40 0
      ProcessServer/Business/StatisticCounters/TimespanAVGCounter.cs
  29. 55 0
      ProcessServer/Business/StatisticCounters/TimespanCounter.cs
  30. 38 0
      ProcessServer/Business/StatisticCounters/TimespanMAXCounter.cs
  31. 38 0
      ProcessServer/Business/StatisticCounters/TimespanMINCounter.cs
  32. 18 0
      ProcessServer/Business/Workers/Contexts/BaseCtx.cs
  33. 16 0
      ProcessServer/Business/Workers/Contexts/CfgCtx.cs
  34. 25 0
      ProcessServer/Business/Workers/Contexts/ProcessingDocumentsCtx.cs
  35. 24 0
      ProcessServer/Business/Workers/Contexts/PullCtx.cs
  36. 40 0
      ProcessServer/Business/Workers/ProcessingDocumentsWorker.cs
  37. 27 0
      ProcessServer/Business/Workers/PullDocumentsWorker.cs
  38. 10 0
      ProcessServer/Business/Workers/PushDocumentsWorker.cs
  39. 25 0
      ProcessServer/Business/Workers/RetentionWorker.cs
  40. 2 1
      ProcessServer/Commands/Base/BaseLocalCommand.cs
  41. 3 1
      ProcessServer/Commands/RunCommand.cs
  42. 1 0
      ProcessServer/Constants.cs
  43. 74 16
      ProcessServer/Options/Configuration.cs
  44. 15 0
      ProcessServer/ProcessServer.csproj
  45. 8 1
      ProcessServer/ProcessServer.sln
  46. 3 0
      ProcessServer/Program.cs
  47. 3 0
      ProcessServer/Scenarios/scenario-dummy.cmd
  48. 3 0
      ProcessServer/Scenarios/scenario-fail.cmd
  49. 51 0
      ProcessServer/processServer-nlog.config
  50. 45 3
      ProcessServer/processServer.json

+ 43 - 9
AppServer/Connector.PS/Connection.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.IO;
+using System.Net;
 using System.Threading.Tasks;
 using BO.AppServer.Metadata.Dto;
 using BO.AppServer.Metadata.Enums;
@@ -15,15 +16,21 @@ namespace Connector.PS
         #region *** Private fields ***
         protected override string ApiName => "PS";
         private string _psIdentifier;
+        private IPAddress _ip;
+        private int _poolSize;
+        private long? _registrationId;
         #endregion
 
 
         #region *** Constructor ***
-        public Connection(Uri uriApiBase, TimeSpan timeout,string psIdentifier, ILogHandler log = null, bool isDumpContentEnabled = false) : base(uriApiBase, timeout, log, isDumpContentEnabled)
+        public Connection(Uri uriApiBase, TimeSpan timeout,string psIdentifier,IPAddress ipAddress, int poolSize, ILogHandler log = null, bool isDumpContentEnabled = false) : base(uriApiBase, timeout, log, isDumpContentEnabled)
         {
             if (string.IsNullOrEmpty(psIdentifier))
                 throw new ArgumentNullException(nameof(psIdentifier));
             _psIdentifier = psIdentifier;
+            _ip = ipAddress ?? throw new ArgumentNullException(nameof(ipAddress));
+            _poolSize = poolSize;
+
         }
         #endregion
 
@@ -34,29 +41,45 @@ namespace Connector.PS
             if (!(scope == DocumentsScopeEnums.Pending || scope == DocumentsScopeEnums.Working))
                 throw new ArgumentOutOfRangeException(
                     $"Document list scope value must be '{DocumentsScopeEnums.Working}' or '{DocumentsScopeEnums.Pending}' int this context.");
-            var result = await CallRequestAsync<ResultsValueDto<DocumentRDto>>($"{Ticket}/{_psIdentifier}/documents/{scope}", RestCallTypeEnum.Get);
+            //{ticket}/ps/{registrationId}/new
+            var result = scope == DocumentsScopeEnums.Pending
+                ? await CallRequestAsync<ResultsValueDto<DocumentRDto>>($"{Ticket}/queue/{_registrationId}/pending",
+                    RestCallTypeEnum.Get)
+                : await CallRequestAsync<ResultsValueDto<DocumentRDto>>($"{Ticket}/queue/{_registrationId}/working",
+                    RestCallTypeEnum.Get);
             return result.Values;
         }
 
         public async Task<DocumentStatusEnum> GetDocumentStateAsync(long documentId)
         {
-            var result = await CallRequestAsync<ResultValueDto<PingDto>>($"{Ticket}/{_psIdentifier}/document/{documentId}", RestCallTypeEnum.Get);
+            //{ticket}/queue/{registrationId}/document/{documentId}/status
+            var result = await CallRequestAsync<ResultValueDto<PingDto>>($"{Ticket}/queue/{_registrationId}/document/{documentId}/status", RestCallTypeEnum.Get);
             return Enum.Parse<DocumentStatusEnum>(result.Value.Status);
 
         }
 
         public async Task<DocumentStatusEnum> SetDocumentStateAsync(long documentId, DocumentStatusEnum status)
         {
-            var result = await CallRequestAsync<ResultValueDto<PingDto>>($"{Ticket}/{_psIdentifier}/document/{documentId}", RestCallTypeEnum.Put,new PingDto(){Status = status.ToString()});
+            if (!(status == DocumentStatusEnum.New || status == DocumentStatusEnum.Processing || status == DocumentStatusEnum.DoneOk || status == DocumentStatusEnum.DoneFail))
+                throw new ArgumentOutOfRangeException(
+                    $"Document status value must be '{DocumentStatusEnum.New}' or '{DocumentStatusEnum.Processing}' or '{DocumentStatusEnum.DoneOk}' or '{DocumentStatusEnum.DoneFail}' in this context.");
+
+            var result = await CallRequestAsync<ResultValueDto<PingDto>>($"{Ticket}/queue/{_registrationId}/document/{documentId}/status/{status}", RestCallTypeEnum.Put);
             return Enum.Parse<DocumentStatusEnum>(result.Value.Status);
 
         }
-
-        public async Task<Stream> GetDocumentContentAsync(long artifactId)
+        public async Task<Stream> GetDocumentContentAsync(long documentId, long artifactId)
         {
-            var result = await CallGetStreamAsync($"{Ticket}/{_psIdentifier}/artifact/{artifactId}");
+            var result = await CallGetStreamAsync($"{Ticket}/queue/{_registrationId}/document/{documentId}/artifact/{artifactId}");
             return result;
         }
+
+        public async Task<ArtifactRDto> SetDocumentContentAsync(long documentId, string fileName, string mimeType, bool overwriteIfExists, Stream contentStream)
+        {
+            CheckIsOpen();
+            var result = await CallPostStreamMultipartAsync<ResultValueDto<ArtifactRDto>>($"{Ticket}/queue/{_registrationId}/document/{documentId}",fileName,contentStream,mimeType);
+            return result.Value;
+        }
         #endregion
 
 
@@ -64,10 +87,21 @@ namespace Connector.PS
         protected override void OnOpening()
         {
             base.OnOpening();
-            var result = Task.Run(() =>  CallRequestAsync<ResultPlain>($"{Ticket}/register", RestCallTypeEnum.Post, new PingDto(){Status = _psIdentifier}));
+            var result = Task.Run(() =>  CallRequestAsync<ResultValueDto<RegistrationRDto>>($"{Ticket}/register", RestCallTypeEnum.Get, GetRegistration()));
             if (!result.Result.IsSuccess)
                 throw result.Result.ToAggregateException();
-            Log(LogSeverityEnum.Info,$"Process server '{_psIdentifier}' is registered.");
+            Log(LogSeverityEnum.Info,$"Process server '{_psIdentifier}' [{result.Result.Value.Id}] is registered.");
+        }
+
+        private RegistrationCDto GetRegistration()
+        {
+            return new RegistrationCDto()
+            {
+                LocationIp = _ip.ToString(),
+                Psinstance = _psIdentifier,
+                PoolSize = _poolSize,
+                Id = _registrationId.GetValueOrDefault(0)
+            };
         }
         #endregion
     }

+ 1 - 1
AppServer/Connector/AbstractConnection.cs

@@ -30,7 +30,7 @@ namespace BO.Connector
         private bool _isDumpContentEnabled;
 
         protected abstract string ApiName { get; }
-        protected bool IsOpen => !string.IsNullOrEmpty(_ticket);
+        public bool IsOpen => !string.IsNullOrEmpty(_ticket);
         protected string Ticket => _ticket;
         #endregion
 

+ 20 - 11
AppServer/Web/Services/PS.cs → AppServer/Web/Services/PSController.cs

@@ -14,7 +14,7 @@ namespace BO.AppServer.Web.Services
     [Authorize]
     [Route("api/[controller]")]
     [ApiController]
-    public class PS : Base<PS>
+    public class PSController : Base<PSController>
     {
         #region *** Properties ***
         protected override RoleEnum LoginRoleAllowed => RoleEnum.System;
@@ -26,7 +26,7 @@ namespace BO.AppServer.Web.Services
 
         #region *** Constructors ***
 
-        public PS(ILoggerFactory logger, AccessService srvAccess) : base(logger, srvAccess)
+        public PSController(ILoggerFactory logger, AccessService srvAccess) : base(logger, srvAccess)
         {
         }
         #endregion
@@ -47,17 +47,25 @@ namespace BO.AppServer.Web.Services
             //return await Call(async () => await _srvUsers.CreateUserAsync(user));
             return null;
         }
+
+        [HttpGet("{ticket}/register")]
+        public async Task<ResultValueDto<RegistrationRDto>> RegisterEnsure(string ticket, [FromBody] RegistrationCDto registration)
+        {
+            CheckAccess(ticket);
+            //return await Call(async () => await _srvUsers.CreateUserAsync(user));
+            return null;
+        }
         #endregion
 
         #region *** Document gathering ***
-        [HttpGet("{ticket}/ps/{registrationId}/new")]
+        [HttpGet("{ticket}/queue/{registrationId}/pending")]
         public async Task<ResultsValueDto<DocumentRDto>> GetQueueNew(string ticket, long registrationId)
         {
             CheckAccess(ticket);
             //return await Call(async () => await _srvUsers.CreateUserAsync(user));
             return null;
         }
-        [HttpGet("{ticket}/ps/{registrationId}/pending")]
+        [HttpGet("{ticket}/queue/{registrationId}/working")]
         public async Task<ResultsValueDto<DocumentRDto>> GetQueuePending(string ticket, long registrationId)
         {
             CheckAccess(ticket);
@@ -65,7 +73,7 @@ namespace BO.AppServer.Web.Services
             return null;
         }
 
-        [HttpGet("{ticket}/ps/{registrationId}/document/{documentId}/artifact/{artifactId}")]
+        [HttpGet("{ticket}/queue/{registrationId}/document/{documentId}/artifact/{artifactId}")]
         public async Task<IActionResult> GetDocumentArtifactContent(string ticket, long documentId, long artifactId)
         {
             // TODO: May wrap to better way
@@ -86,26 +94,27 @@ namespace BO.AppServer.Web.Services
         #endregion
 
         #region *** Document state operations ***
-        [HttpPatch("{ticket}/ps/{registrationId}/document/{documentId}/status")]
-        public async Task<ResultValueDto<EmptyDto>> SetDocumentProcessingStatus(string ticket, long registrationId, long documentId)
+        [HttpPatch("{ticket}/queue/{registrationId}/document/{documentId}/status/{status}")]
+        public async Task<ResultValueDto<PingDto>> SetDocumentProcessingStatus(string ticket, long registrationId, long documentId, DocumentStatusEnum status)
         {
             CheckAccess(ticket);
             //return await Call(async () => await _srvUsers.CreateUserAsync(user));
             return null;
         }
 
-        [HttpPatch("{ticket}/ps/{registrationId}/document/{documentId}/free")]
-        public async Task<ResultsValueDto<EmptyDto>> SetFreeDocument(string ticket, long registrationId, long documentId)
+        [HttpGet("{ticket}/queue/{registrationId}/document/{documentId}/status")]
+        public async Task<ResultValueDto<PingDto>> GetDocumentProcessingStatus(string ticket, long registrationId, long documentId)
         {
             CheckAccess(ticket);
             //return await Call(async () => await _srvUsers.CreateUserAsync(user));
             return null;
         }
+
         #endregion
 
         #region *** Document uploading ***
-        [HttpPatch("{ticket}/ps/{registrationId}/document/{documentId}")]
-        public async Task<ResultValueDto<DocumentRDto>> UploadDocumentArtifact(string ticket, long registrationId, long documentId, [FromBody] ArtifactCDto artifact, IFormFile content)
+        [HttpPatch("{ticket}/queue/{registrationId}/document/{documentId}")]
+        public async Task<ResultValueDto<ArtifactRDto>> UploadDocumentArtifact(string ticket, long registrationId, long documentId,IFormFile content)
         {
             CheckAccess(ticket);
             //return await Call(async () => await _srvUsers.CreateUserAsync(user));

+ 5 - 2
AppServer/Web/appsettings.json

@@ -54,8 +54,11 @@
           "6DEC4167-57A6-4333-92DA-ADE009A97DE9",
           "SHARE"
         ]
-
-
+      },
+      "Process": {
+        "MagicKeys": [
+          "SHARE"
+        ]
       }
     }
   }

+ 30 - 0
Common/qdr.fnd.core.nlog/LogSeverityEnumExt.cs

@@ -0,0 +1,30 @@
+using System;
+using NLog;
+using Quadarax.Foundation.Core.Logging;
+
+namespace Quadarax.Foundation.Core.NLog
+{
+    public static class LogSeverityEnumExt
+    {
+        public static LogLevel ToNLogLevel(this LogSeverityEnum owner)
+        {
+            switch (owner)
+            {
+                    case LogSeverityEnum.Trace:
+                        return LogLevel.Trace;
+                    case LogSeverityEnum.Debug:
+                        return LogLevel.Debug;
+                    case LogSeverityEnum.Info:
+                        return LogLevel.Info;
+                    case LogSeverityEnum.Warn:
+                        return LogLevel.Warn;
+                    case LogSeverityEnum.Error:
+                        return LogLevel.Error;
+                    case LogSeverityEnum.Fatal:
+                        return LogLevel.Fatal;
+                    default:
+                        throw new NotSupportedException($"Not supported enum conversion for '{owner}'");
+            }
+        }
+    }
+}

+ 43 - 0
Common/qdr.fnd.core.nlog/Logger.cs

@@ -0,0 +1,43 @@
+using System;
+using NLog;
+using Quadarax.Foundation.Core.Logging;
+
+namespace Quadarax.Foundation.Core.NLog
+{
+    public class Logger : ILog
+    {
+
+        private global::NLog.ILogger _logger;
+        public string Name { get; }
+
+
+        public Logger(Type type) : this(type.Name)
+        {
+        }
+        public Logger(string name)
+        {
+            Name = name;
+            _logger = LogManager.GetLogger(name);
+        }
+
+        public void Log(LogSeverityEnum severity, int code, string message)
+        {
+            _logger.Log(severity.ToNLogLevel(), message, code);
+        }
+
+        public void Log(LogSeverityEnum severity, string message)
+        {
+            _logger.Log(severity.ToNLogLevel(), message);
+        }
+
+        public void Log(LogSeverityEnum severity, string message, Exception exception)
+        {
+            _logger.Log(severity.ToNLogLevel(), exception, message);
+        }
+
+        public void Log(LogSeverityEnum severity, int code, string message, Exception exception)
+        {
+            _logger.Log(severity.ToNLogLevel(), exception, message, code);
+        }
+    }
+}

+ 45 - 0
Common/qdr.fnd.core.nlog/LoggerFactory.cs

@@ -0,0 +1,45 @@
+using System;
+using NLog;
+using NLog.Config;
+using Quadarax.Foundation.Core.Logging;
+using Quadarax.Foundation.Core.Object;
+using ILogger = Quadarax.Foundation.Core.Logging.ILogger;
+
+namespace Quadarax.Foundation.Core.NLog
+{
+    public class LoggerFactory : Singleton<ILogger, LoggerFactory>, ILogger
+    {
+
+        public static string ConfigurationFileName { get; set; }
+
+        public void Configure(string configurationFileName)
+        {
+
+            if (!string.IsNullOrEmpty(configurationFileName))
+            {
+                LogManager.Configuration = new XmlLoggingConfiguration(configurationFileName);
+                var log = Instance.GetLogger(GetType());
+                log.Log(LogSeverityEnum.Debug, $"NLOG initialized from configuration file '{configurationFileName}'");
+            }
+        }
+
+        public ILog GetLogger(string loggerName)
+        {
+            return Instance.GetLogger(loggerName);
+        }
+
+        public ILog GetLogger(Type loggerForType)
+        {
+            return Instance.GetLogger(loggerForType);
+        }
+
+        ILog ILogger.GetLogger(string loggerName)
+        {
+            return new Logger(loggerName);
+        }
+        ILog ILogger.GetLogger(Type loggerForType)
+        {
+            return new Logger(loggerForType);
+        }
+    }
+}

+ 21 - 0
Common/qdr.fnd.core.nlog/qdr.fnd.core.nlog.csproj

@@ -0,0 +1,21 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netcoreapp3.1</TargetFramework>
+    <SignAssembly>true</SignAssembly>
+    <AssemblyOriginatorKeyFile>..\bo_strong_key_pair.snk</AssemblyOriginatorKeyFile>
+    <DelaySign>false</DelaySign>
+    <RootNamespace>Quadarax.Foundation.Core.NLog</RootNamespace>
+    <ApplicationIcon>..\quadarax_dll.ico</ApplicationIcon>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="NLog" Version="4.7.13" />
+    <PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\qdr.fnd.core\qdr.fnd.core.csproj" />
+  </ItemGroup>
+
+</Project>

+ 1 - 1
Common/qdr.fnd.core.qconsole/Value/DateTimeValue.cs

@@ -15,7 +15,7 @@ namespace Quadarax.Foundation.Core.QConsole.Value
         }
         public DateTimeValue(DateTime value) : base(value)
         {
-            _dateformat = Thread.CurrentThread.CurrentUICulture.DateTimeFormat.ShortDatePattern + " " +  Thread.CurrentThread.CurrentUICulture.DateTimeFormat.ShortTimePattern;
+            _dateformat = System.Threading.Thread.CurrentThread.CurrentUICulture.DateTimeFormat.ShortDatePattern + " " +  System.Threading.Thread.CurrentThread.CurrentUICulture.DateTimeFormat.ShortTimePattern;
         }
 
         public override string AsString()

+ 1 - 1
Common/qdr.fnd.core.qconsole/Value/DateValue.cs

@@ -18,7 +18,7 @@ namespace Quadarax.Foundation.Core.QConsole.Value
         }
         public DateValue(DateTime value) : base(typeof(DateTime), value)
         {
-            _dateformat = Thread.CurrentThread.CurrentUICulture.DateTimeFormat.ShortDatePattern;
+            _dateformat = System.Threading.Thread.CurrentThread.CurrentUICulture.DateTimeFormat.ShortDatePattern;
         }
 
         protected override object ConvertStringToValue(string valueAsString)

+ 1 - 2
Common/qdr.fnd.core.qconsole/Value/TimeValue.cs

@@ -1,5 +1,4 @@
 using System;
-using System.Threading;
 
 namespace Quadarax.Foundation.Core.QConsole.Value
 {
@@ -17,7 +16,7 @@ namespace Quadarax.Foundation.Core.QConsole.Value
 
         public TimeValue(DateTime value) : base(value)
         {
-            _dateformat = Thread.CurrentThread.CurrentUICulture.DateTimeFormat.ShortTimePattern;
+            _dateformat = System.Threading.Thread.CurrentThread.CurrentUICulture.DateTimeFormat.ShortTimePattern;
         }
     }
 }

+ 24 - 0
Common/qdr.fnd.core/IO/Extensions/DirectoryInfoExt.cs

@@ -25,6 +25,30 @@ namespace Quadarax.Foundation.Core.IO.Extensions
             return dirInfo.FileSystem.File.OpenWrite(fullFileName);
         }
 
+        public static bool DeleteFile(this IDirectoryInfo dirInfo, string fileNameWithExtension)
+        {
+            if (dirInfo == null)
+                throw new ArgumentNullException(nameof(dirInfo));
+            if (string.IsNullOrEmpty(fileNameWithExtension))
+                throw new ArgumentNullException(nameof(fileNameWithExtension));
+
+
+            var fileInfo = dirInfo.GetFiles(fileNameWithExtension).FirstOrDefault();
+            var result = fileInfo != null;
+            fileInfo?.Delete();
+            return result;
+        }
+
+        public static string GetFullFileName(this IDirectoryInfo dirInfo, string fileNameWithExtension)
+        {
+            if (dirInfo == null)
+                throw new ArgumentNullException(nameof(dirInfo));
+            if (string.IsNullOrEmpty(fileNameWithExtension))
+                throw new ArgumentNullException(nameof(fileNameWithExtension));
+
+            return dirInfo.FileSystem.Path.Combine(dirInfo.FullName, fileNameWithExtension);
+        }
+
         public static bool ExistsFile(this IDirectoryInfo dirInfo, string fileNameWithExtension)
         {
             if (dirInfo == null)

+ 162 - 0
Common/qdr.fnd.core/Thread/AbstractWorker.cs

@@ -0,0 +1,162 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Quadarax.Foundation.Core.Logging;
+using Quadarax.Foundation.Core.Object;
+using Quadarax.Foundation.Core.Thread.Extensions;
+
+namespace Quadarax.Foundation.Core.Thread
+{
+    public abstract class AbstractWorker : DisposableObject, IWorker
+    {
+
+        #region *** Private fields ***
+        private Task _thread;
+        private ILog _log;
+        private TimeSpan _delay;
+        private CancellationTokenSource _cancel;
+        #endregion
+
+        #region *** Properties ***
+        public bool IsRunning { get; private set; }
+        public bool IsTransient { get; private set; }
+        public string Name { get; }
+        public int CurrentThreadId => System.Threading.Thread.CurrentThread.ManagedThreadId;
+        public TimeSpan Timeout { get; set; }
+        protected IWorkerContext Context { get; private set; }
+        #endregion
+
+        #region *** Constructors ***
+        protected AbstractWorker() : this($"Worker#{Guid.NewGuid():N}", null)
+        {
+
+        }
+        protected AbstractWorker(IWorkerContext context) : this($"Worker#{Guid.NewGuid():N}", context)
+        {
+
+        }
+        protected AbstractWorker(string workerName) : this(workerName, null)
+        {
+
+        }
+        protected AbstractWorker(string workerName, IWorkerContext context, ILogger logger = null)
+        {
+            if (string.IsNullOrEmpty(workerName))
+                throw new ArgumentNullException(nameof(workerName));
+            Name = workerName;
+            Context = context;
+            _log = logger?.GetLogger(GetType());
+            Timeout = TimeSpan.Zero;
+        }
+        #endregion
+
+        #region *** Public Operations ***
+
+        public void Start(bool silentlyIfNotValidState = false)
+        {
+            Start(TimeSpan.Zero, silentlyIfNotValidState);
+        }
+        public void Start(TimeSpan runDelay, bool silentlyIfNotValidState = false)
+        {
+            if (IsTransient || IsRunning)
+                if (!silentlyIfNotValidState)
+                    throw new InvalidOperationException(
+                        $"Worker '{Name}' is in transient or running state. Cannot start.");
+                else
+                    return;
+
+            OnBeforeStart();
+
+            _cancel = new CancellationTokenSource();
+            _delay = runDelay;
+            _thread = Task.Factory.StartNew(() => DoInternal(this), _cancel.Token);
+            IsRunning = true;
+            
+        }
+
+
+        public void Stop(bool silentlyIfNotValidState = false)
+        {
+            if (IsTransient || !IsRunning)
+                if (!silentlyIfNotValidState)
+                    throw new InvalidOperationException(
+                        $"Worker '{Name}' is in transient or running state. Cannot start.");
+                else
+                    return;
+            IsTransient = true;
+            Abort();
+            OnAfterStop();
+        }
+        #endregion
+
+        #region *** Virtuals and protected ***
+        protected virtual void OnBeforeStart()
+        {
+        }
+        protected virtual void OnAfterStop()
+        {
+        }
+        protected virtual async Task Do(IWorkerContext context)
+        {
+            throw new NotImplementedException("AbstractWorker.Do() must be overriden.");
+        }
+
+        protected void Log(LogSeverityEnum severity, string message)
+        {
+            _log?.Log(severity, message);
+        }
+
+        protected void Log(LogSeverityEnum severity, string message, Exception exception)
+        {
+            _log?.Log(severity, message, exception);
+        }
+
+        protected override void OnDisposing()
+        {
+            Abort();
+            Context = null;
+            _log = null;
+        }
+
+        #endregion
+
+        #region *** Private Operations ***
+        private async void DoInternal(object? context)
+        {
+            var wk = (AbstractWorker)context;
+            if (wk._delay != TimeSpan.Zero)
+                wk._thread.Wait(wk._delay);
+            wk.IsTransient = false;
+            try
+            {
+                if (Timeout == TimeSpan.Zero)
+                    await wk.Do(wk.Context);
+                else
+                    await wk.Do(wk.Context).TimeoutAfter(Timeout);
+            }
+            catch (Exception e)
+            {
+                wk.Log(LogSeverityEnum.Error, $"Worker '{Name}' failed.", e);
+                throw;
+            }
+            finally
+            {
+                wk.IsRunning = false;
+            }
+        }
+
+        private void Abort()
+        {
+            if (_thread == null)
+                return;
+            _cancel.Cancel();
+            _cancel.Dispose();
+            _cancel = null;
+            _thread = null;
+            IsRunning = false;
+            IsTransient = false;
+            
+        }
+        #endregion
+    }
+}

+ 40 - 0
Common/qdr.fnd.core/Thread/ContinousWorker.cs

@@ -0,0 +1,40 @@
+using System;
+using System.Threading.Tasks;
+using Quadarax.Foundation.Core.Logging;
+
+namespace Quadarax.Foundation.Core.Thread
+{
+    public class ContinousWorker : AbstractWorker
+    {
+        #region *** Private fields ***
+        private Action _body;
+        #endregion
+
+        #region *** Properties ***
+        #endregion
+
+        #region *** Constructors ***
+        public ContinousWorker(Action body = null) : base()
+        {
+            _body = body;
+        }
+        public ContinousWorker(string workerName, ILogger logger = null) : base(workerName,null, logger)
+        {
+        }
+        #endregion
+
+        #region *** Public Operations ***
+        #endregion
+
+        #region *** Virtuals and protected ***
+        protected override async Task Do(IWorkerContext context)
+        {
+            _body?.Invoke();
+        }
+        #endregion
+
+        #region *** Private Operations ***
+        #endregion
+
+    }
+}

+ 38 - 0
Common/qdr.fnd.core/Thread/Extensions/TaskExt.cs

@@ -0,0 +1,38 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Quadarax.Foundation.Core.Thread.Extensions
+{
+    public static class TaskExt
+    {
+        public static async Task<TResult> TimeoutAfter<TResult>(this Task<TResult> task, TimeSpan timeout) {
+
+            using (var timeoutCancellationTokenSource = new CancellationTokenSource()) {
+
+                var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token));
+                if (completedTask == task) {
+                    timeoutCancellationTokenSource.Cancel();
+                    return await task;  // Very important in order to propagate exceptions
+                }
+
+                throw new TimeoutException("The operation has timed out.");
+            }
+        }
+
+        public static async Task TimeoutAfter(this Task task, TimeSpan timeout) {
+
+            using (var timeoutCancellationTokenSource = new CancellationTokenSource()) {
+
+                var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token));
+                if (completedTask == task) {
+                    timeoutCancellationTokenSource.Cancel();
+                    await task;  // Very important in order to propagate exceptions
+                    return;
+                }
+
+                throw new TimeoutException("The operation has timed out.");
+            }
+        }
+    }
+}

+ 15 - 0
Common/qdr.fnd.core/Thread/IWorker.cs

@@ -0,0 +1,15 @@
+using System;
+
+namespace Quadarax.Foundation.Core.Thread
+{
+    public interface IWorker : IDisposable
+    {
+        bool IsRunning { get; }
+        bool IsTransient { get; }
+        string Name { get; }
+        TimeSpan Timeout { get; }
+        void Start(bool silentlyIfNotValidState = false);
+        void Start(TimeSpan runDelay,bool silentlyIfNotValidState = false);
+        void Stop(bool silentlyIfNotValidState = false);
+    }
+}

+ 6 - 0
Common/qdr.fnd.core/Thread/IWorkerContext.cs

@@ -0,0 +1,6 @@
+namespace Quadarax.Foundation.Core.Thread
+{
+    public interface IWorkerContext
+    {
+    }
+}

+ 87 - 0
Common/qdr.fnd.core/Thread/LoopWorker.cs

@@ -0,0 +1,87 @@
+using System;
+using System.Threading.Tasks;
+using Quadarax.Foundation.Core.Logging;
+
+namespace Quadarax.Foundation.Core.Thread
+{
+    public class LoopWorker : AbstractWorker
+    {
+        #region *** Private fields ***
+        private TimeSpan _defaultTimeout;
+        private long _cntLimit;
+        #endregion
+
+        #region *** Properties ***
+        public long IterationsLimit { get; set; } = 0;
+        public TimeSpan IterationsDelayBetween { get; set; } = TimeSpan.Zero;
+        #endregion
+
+        #region *** Constructors ***
+        public LoopWorker(IWorkerContext context) : base(context)
+        {
+
+        }
+        public LoopWorker(string workerName, IWorkerContext context) : base(workerName, context)
+        {
+        }
+        public LoopWorker(string workerName) : base(workerName)
+        {
+
+        }
+        public LoopWorker(string workerName, IWorkerContext context, ILogger logger = null) : base(workerName, context, logger)
+        {
+        }
+        #endregion
+
+        #region *** Public Operations ***
+        #endregion
+
+        #region *** Virtuals and protected ***
+
+        protected override async Task Do(IWorkerContext context)
+        {
+            var exit = false;
+            var first = true;
+            while (!exit)
+            {
+                if (first)
+                    first = false;
+                else if (IterationsDelayBetween != TimeSpan.Zero)
+                    System.Threading.Thread.CurrentThread.Join(IterationsDelayBetween);
+
+                if (IterationsLimit == 0)
+                {
+                    await DoIteration(context);
+                }
+                else
+                {
+                    await DoIteration(context);
+                    _cntLimit++;
+                    if (_cntLimit > IterationsLimit)
+                        exit = true;
+                }
+            }
+        }
+        protected virtual async Task DoIteration(IWorkerContext context)
+        {
+        }
+
+        protected override void OnBeforeStart()
+        {
+            base.OnBeforeStart();
+            _defaultTimeout = Timeout;
+            Timeout = TimeSpan.Zero;
+        }
+        protected override void OnAfterStop()
+        {
+            base.OnAfterStop();
+            Timeout = _defaultTimeout;
+            _cntLimit = 0;
+        }
+        #endregion
+
+        #region *** Private Operations ***
+        #endregion
+
+    }
+}

+ 30 - 0
ProcessServer/Business/DocumentContent.cs

@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.IO.Abstractions;
+using Quadarax.Foundation.Core.IO.MimeTypes;
+
+namespace BO.ProcessServer.Business
+{
+    public class DocumentContent
+    {
+        public long DocumentId { get; set; }
+        public IList<FileContent> Files { get; }
+
+        public DocumentContent(long documentId, IFileSystem fileSystem, IEnumerable<string> fileNames)
+        {
+            if (fileSystem==null) throw new ArgumentNullException(nameof(fileSystem));
+            if (fileNames == null) throw new ArgumentNullException(nameof(fileNames));
+
+            foreach (var fileName in fileNames)
+                Files.Add(new FileContent() {FileName = fileName, MimeType = MimeTypeUtil.GetMimeType(fileSystem.Path.GetExtension(fileName))});
+
+            DocumentId = documentId;
+        }
+    }
+
+    public class FileContent
+    {
+        public string FileName { get; set; }
+        public string MimeType { get; set; }
+    }
+}

+ 14 - 0
ProcessServer/Business/Enums/AppModeEnum.cs

@@ -0,0 +1,14 @@
+namespace BO.ProcessServer.Business.Enums
+{
+    public enum AppModeEnum
+    {
+        /// <summary>
+        /// Works in simultaneous mode (documents are not processing and randomly generates output OK or Fail). Any call for processing documents enabled.
+        /// </summary>
+        Proxy,
+        /// <summary>
+        /// Works in production mode
+        /// </summary>
+        Production
+    }
+}

+ 13 - 0
ProcessServer/Business/Enums/ServerProcessStateEnum.cs

@@ -0,0 +1,13 @@
+namespace BO.ProcessServer.Business.Enums
+{
+    public enum ServerProcessStateEnum
+    {
+        Pending,
+        Running,
+        FailExit,
+        FailTimeout,
+        FailInternal,
+        Done,
+        Obsolete
+    }
+}

+ 181 - 0
ProcessServer/Business/Pool.cs

@@ -0,0 +1,181 @@
+using BO.ProcessServer.Options;
+using Quadarax.Foundation.Core.Object;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Abstractions;
+using System.Linq;
+using System.Threading.Tasks;
+using Quadarax.Foundation.Core.IO.Extensions;
+using Quadarax.Foundation.Core.Logging;
+
+// ReSharper disable once InconsistentNaming
+namespace BO.ProcessServer.Business
+{
+    internal class Pool : DisposableObject
+    {
+        #region *** Enums ***
+        public enum PoolTypeEnum
+        {
+            Pending,
+            Working,
+            Done,
+            Fail
+        }
+        #endregion
+
+        #region *** Constants ***
+        private const string CS_ALL_FILES = "*.*";
+        #endregion
+
+        #region *** Private fields ***
+        private readonly IFileSystem _fileSystem;
+        private IDirectoryInfo _dirPending;
+        private IDirectoryInfo _dirWorking;
+        private IDirectoryInfo _dirDone;
+        private IDirectoryInfo _dirFail;
+
+        private ILog _log;
+        #endregion
+
+        #region *** Constructors ***
+        public Pool(Configuration configuration, IFileSystem fileSystem, ILogger logger)
+        {
+            _log = logger.GetLogger(GetType());
+            if (configuration==null) throw new ArgumentNullException(nameof(configuration));
+
+            _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
+            var basePath = TranslatePath(configuration.System.PoolPathBase);
+            if (!_fileSystem.Directory.Exists(TranslatePath(configuration.System.PoolPathBase)))
+                throw new FileNotFoundException($"BasePool directory '{basePath}' not found!");
+
+            _dirPending = _fileSystem.DirectoryInfo.FromDirectoryName(_fileSystem.Path.Combine(basePath, configuration.System.PoolPathPending));
+            _dirWorking = _fileSystem.DirectoryInfo.FromDirectoryName(_fileSystem.Path.Combine(basePath, configuration.System.PoolPathWorking));
+            _dirDone = _fileSystem.DirectoryInfo.FromDirectoryName(_fileSystem.Path.Combine(basePath, configuration.System.PoolPathDone));
+            _dirFail = _fileSystem.DirectoryInfo.FromDirectoryName(_fileSystem.Path.Combine(basePath, configuration.System.PoolPathFail));
+
+            _log.Log(LogSeverityEnum.Info, $"Open pool in '{basePath}'. Initial status (files): Pending={_dirPending.GetFiles().Length};Working={_dirWorking.GetFiles().Length};Done={_dirDone.GetFiles().Length};Fail={_dirFail.GetFiles().Length}");
+        }
+        #endregion
+
+        #region *** Public Operations ***
+        public async Task<string> PutDocumentAsync(long documentId, string fileName, Stream streamToWrite)
+        {
+            var poolFileName = TranslateFileName(documentId, fileName);
+            if (_dirPending.ExistsFile(poolFileName))
+            {
+                _log.Log(LogSeverityEnum.Warn, $"Pool [Pending]: File '{poolFileName}' already exists. Overwrite.");
+                _dirPending.DeleteFile(poolFileName);
+            }
+
+            var fullFileName = _dirPending.GetFullFileName(poolFileName);
+            await using (var sw = _fileSystem.File.OpenWrite(fullFileName))
+            {
+                streamToWrite.Seek(0, SeekOrigin.Begin);
+                await streamToWrite.CopyToAsync(sw);
+                await sw.FlushAsync();
+                _log.Log(LogSeverityEnum.Debug, $"Pool [Pending]: File '{poolFileName}' created.");
+
+            }
+            return fullFileName;
+        }
+
+        public long GetDocumentIdFromFileName(string fileName)
+        {
+            var fileNameOnly = _fileSystem.Path.GetFileName(fileName);
+            var pos = fileNameOnly.IndexOf("_", StringComparison.InvariantCulture);
+            if (pos < 0)
+                throw new ArgumentOutOfRangeException(nameof(fileName),
+                    $"FileName '{fileName}' format is unexpected (00000000_name.ext expects).");
+
+            return long.Parse(fileNameOnly.Substring(0, pos));
+        }
+
+        public IEnumerable<string> GetDocumentsCount(PoolTypeEnum poolBranchFrom)
+        {
+            return GetPoolBranch(poolBranchFrom).GetFiles(CS_ALL_FILES, SearchOption.TopDirectoryOnly).Select(x=>x.FullName);
+        }
+
+        public IEnumerable<string> GetDocumentFiles(PoolTypeEnum poolBranchFrom, long documentId)
+        {
+            return GetPoolBranch(poolBranchFrom).GetFiles(TranslateSearchPattern(documentId), SearchOption.TopDirectoryOnly).Select(x=>x.FullName);
+        }
+
+        public IEnumerable<string> MoveDocumentFiles(PoolTypeEnum poolBranchFrom, PoolTypeEnum poolBranchTo, long documentId)
+        {
+            var files = GetDocumentFiles(poolBranchFrom, documentId).ToArray();
+            var tasks = new List<Task>();
+            foreach (var file in files)
+            {
+                tasks.Add(Task.Factory.StartNew(() =>
+                    {
+                        _fileSystem.File.Move(file, GetPoolBranch(poolBranchTo).GetFullFileName(_fileSystem.Path.GetFileName(file)));
+                        _log.Log(LogSeverityEnum.Debug, $"Pool [{poolBranchFrom}]: File '{_fileSystem.Path.GetFileName(file)}' moved to [{poolBranchTo}].");
+                    }
+                ));
+            }
+
+            Task.WaitAll(tasks.ToArray());
+            return files;
+        }
+
+        public IEnumerable<string> DeleteDocumentFiles(PoolTypeEnum poolBranchFrom, long documentId)
+        {
+            var files = GetPoolBranch(poolBranchFrom).GetFiles(TranslateSearchPattern(documentId), SearchOption.TopDirectoryOnly);
+            foreach (var file in files)
+            {
+                file.Delete();
+                _log.Log(LogSeverityEnum.Debug, $"Pool [{poolBranchFrom}]: File '{file.Name}' deleted.");
+            }
+
+            return files.Select(x => x.FullName);
+        }
+        #endregion
+
+
+        #region *** Private operations & overrides ***
+
+        protected override void OnDisposing()
+        {
+            _dirPending = null;
+            _dirWorking  = null;
+            _dirDone  = null;
+            _dirFail  = null;
+        }
+
+        private string TranslatePath(string path)
+        {
+            if (string.IsNullOrEmpty(path))
+                return path;
+            return path.Replace("{CD}", _fileSystem.Directory.GetCurrentDirectory());
+        }
+
+        private string TranslateFileName(long documentId, string originalFileName)
+        {
+            return $"{documentId:D8}_{originalFileName}";
+        }
+
+        private string TranslateSearchPattern(long documentId)
+        {
+            return $"{documentId:D8}_{CS_ALL_FILES}";
+        }
+
+        private IDirectoryInfo GetPoolBranch(PoolTypeEnum poolBranch)
+        {
+            switch (poolBranch)
+            {
+                case PoolTypeEnum.Done:
+                    return _dirDone;
+                case PoolTypeEnum.Fail:
+                    return _dirFail;
+                case PoolTypeEnum.Pending:
+                    return _dirPending;
+                case PoolTypeEnum.Working:
+                    return _dirWorking;
+                default:
+                    throw new NotSupportedException();
+            }
+        }
+        #endregion
+    }
+}

+ 221 - 0
ProcessServer/Business/Server.cs

@@ -0,0 +1,221 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Abstractions;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using BO.ProcessServer.Business.StatisticCounters;
+using BO.ProcessServer.Business.Workers;
+using BO.ProcessServer.Business.Workers.Contexts;
+using BO.ProcessServer.Options;
+using Connector.PS;
+using Quadarax.Foundation.Core.IO;
+using Quadarax.Foundation.Core.Logging;
+using Quadarax.Foundation.Core.Object;
+
+namespace BO.ProcessServer.Business
+{
+    internal class Server : DisposableObject
+    {
+        #region *** Private fields ***
+        private Configuration _configuration;
+        private IFileSystem _fileSystem;
+        private IList<ServerProcess> _processes;
+        protected ILog Log { get; }
+        protected Statistics Statistics { get; }
+        private Pool _pool;
+        private PullDocumentsWorker _wkPull;
+        private RetentionWorker _wkRetention;
+        private ProcessingDocumentsWorker _wkProcessing;
+        private Connection _connection;
+        #endregion
+
+        #region *** Public properties ***
+        public bool IsRunning { get; private set; }
+        #endregion
+
+        #region *** Constructors ***
+        public Server(Configuration serverConfiguration, IFileSystem fileSystem, ILogger logger)
+        {
+            _configuration = serverConfiguration ?? throw new ArgumentNullException(nameof(serverConfiguration));
+            _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
+            if (logger == null)
+                throw new ArgumentNullException(nameof(logger));
+            Log = logger.GetLogger(typeof(Server));
+            Statistics = new Statistics();
+
+            _processes = new List<ServerProcess>();
+
+            _connection = new Connection(_configuration.ApiBaseUrl, _configuration.ApiTimeout,
+                _configuration.System.Identification, IPAddress.Parse(_configuration.System.PreferredInterface),
+                _configuration.System.PoolPullDocuments);
+
+            _pool = new Pool(_configuration, _fileSystem, logger);
+
+            _wkPull = new PullDocumentsWorker(_connection, _configuration.System.PoolPullInterval, fileSystem, Statistics, _pool, logger);
+            _wkRetention = new RetentionWorker(new CfgCtx(_configuration, Statistics, _pool));
+            _wkProcessing = new ProcessingDocumentsWorker(_configuration, _processes, _connection, fileSystem, Statistics, _pool, logger);
+        }
+        #endregion
+
+        #region *** Public operations ***
+        public void Start()
+        {
+            if (IsRunning)
+            {
+                LogWarn($"Instance '{_configuration.System.Identification}' already running. Start skipped.");
+                return;
+            }
+
+            LogDebug($"Starting instance '{_configuration.System.Identification}' ...");
+            ValidateConfiguration();
+
+            _wkPull.Start();
+            _wkRetention.Start();
+            _wkProcessing.Start();
+
+            Statistics.GetCounter<TimespanCounter>(Statistics.CNT_PS_ONLINE_DUR).Start();
+            IsRunning = true;
+            LogInfo( $"Instance '{_configuration.System.Identification}' STARTED.");
+        }
+
+
+        public void Stop()
+        {
+            if (!IsRunning)
+            {
+                LogWarn($"Instance '{_configuration.System.Identification}' already stopped. Stop skipped.");
+                return;
+            }
+            LogDebug($"Stopping instance '{_configuration.System.Identification}' ...");
+
+            StopAll();
+
+            Statistics.GetCounter<TimespanCounter>(Statistics.CNT_PS_ONLINE_DUR).Stop();
+            IsRunning = false;
+            LogInfo( $"Instance '{_configuration.System.Identification}' STOPPED.");
+        }
+        #endregion
+
+        #region *** Overrides ***
+        protected override void OnDisposing()
+        {
+            if (IsRunning)
+                StopAll(true);
+            _wkPull?.Dispose();
+            _wkRetention?.Dispose();
+            _wkProcessing?.Dispose();
+            _connection?.Dispose();
+            _pool?.Dispose();
+
+            LogDebug($"Instance '{_configuration.System.Identification}' disposed.");
+            _configuration = null;
+            _fileSystem = null;
+            _wkPull=null;
+            _wkRetention=null;
+            _wkProcessing = null;
+            _connection=null;
+            _pool = null;
+        }
+        #endregion
+
+        #region *** Private Operations ***
+
+        public void LogDebug(string message)
+        {
+            LogRaw(LogSeverityEnum.Debug, message);
+        }
+        public void LogInfo(string message)
+        {
+            LogRaw(LogSeverityEnum.Info, message);
+        }
+        public void LogWarn(string message)
+        {
+            LogRaw(LogSeverityEnum.Warn, message);
+        }
+
+        public void LogRaw(LogSeverityEnum severity, string message)
+        {
+            Log.Log(severity, message);
+        }
+
+        private void ValidateConfiguration()
+        {
+            if (string.IsNullOrEmpty(_configuration.System.Identification))
+                throw new ArgumentNullException(nameof(_configuration.System.Identification),
+                    "Mandatory process server identification.");
+
+            if (string.IsNullOrEmpty(_configuration.System.PreferredInterface))
+                _configuration.System.PreferredInterface = "auto";
+
+            
+            var host = Dns.GetHostEntry(Dns.GetHostName());
+            if (_configuration.System.PreferredInterface.Trim().ToLower() == "auto")
+            {
+                _configuration.System.PreferredInterface = host.AddressList.FirstOrDefault(x =>
+                        x.AddressFamily == AddressFamily.InterNetwork ||
+                        x.AddressFamily == AddressFamily.InterNetworkV6)
+                    ?.ToString();
+            }
+
+            if (!host.AddressList.Any(x =>
+                    (x.AddressFamily == AddressFamily.InterNetwork ||
+                     x.AddressFamily == AddressFamily.InterNetworkV6) && x.ToString().ToLower() ==
+                    _configuration.System.PreferredInterface.Trim().ToLower()))
+                throw new ArgumentNullException(nameof(_configuration.System.PreferredInterface),
+                    "Mandatory preferred interface (or auto) for server identification.");
+
+            // Pool directories structure
+            EnsureDirectory(_configuration.System.PoolPathBase);
+            EnsureDirectory(_fileSystem.Path.Combine(_configuration.System.PoolPathBase, _configuration.System.PoolPathDone));
+            EnsureDirectory(_fileSystem.Path.Combine(_configuration.System.PoolPathBase, _configuration.System.PoolPathFail));
+            EnsureDirectory(_fileSystem.Path.Combine(_configuration.System.PoolPathBase, _configuration.System.PoolPathPending));
+            EnsureDirectory(_fileSystem.Path.Combine(_configuration.System.PoolPathBase, _configuration.System.PoolPathWorking));
+
+            // Scenario structure
+            EnsureDirectory(_configuration.System.ScenarioPathBase);
+            if (_fileSystem.Directory.GetFiles(TranslatePath(_configuration.System.ScenarioPathBase), "*.*").Length == 0)
+                LogWarn($"Scenario directory '{TranslatePath(_configuration.System.ScenarioPathBase)}' is empty.");
+
+            LogInfo("Configuration validated.");
+        }
+
+        private string EnsureDirectory(string path)
+        {
+            var dir = FileUtils.EnsurePathEndsWithDirSeparator(_fileSystem, TranslatePath(path));
+            if (!_fileSystem.Directory.Exists(dir))
+            {
+                _fileSystem.Directory.CreateDirectory(dir);
+                LogDebug($"Directory '{dir}' created.");
+            }
+
+            if (!_fileSystem.Directory.Exists(dir))
+                throw new FileNotFoundException($"Directory '{dir}' not found!");
+            return dir;
+        }
+
+        private string TranslatePath(string path)
+        {
+            if (string.IsNullOrEmpty(path))
+                return path;
+            return path.Replace("{CD}", _fileSystem.Directory.GetCurrentDirectory());
+        }
+
+        private void StopAll(bool silently = false)
+        {
+            _wkPull.Stop(silently);
+            _wkRetention.Stop(silently);
+            _wkProcessing.Stop(silently);
+
+            foreach (var process in _processes)
+                process.Stop(silently);
+
+            foreach (var process in _processes)
+                process.Dispose();
+
+            _processes.Clear();
+        }
+        #endregion
+    }
+}

+ 16 - 0
ProcessServer/Business/ServerProcess.cs

@@ -0,0 +1,16 @@
+using System;
+using BO.ProcessServer.Business.Enums;
+using Quadarax.Foundation.Core.Thread;
+
+namespace BO.ProcessServer.Business
+{
+    internal class ServerProcess : ContinousWorker
+    {
+        public string ShellCommand { get; set; }
+        public ServerProcessStateEnum Status { get; set; }
+        public DateTime Started { get; set; }
+        public DateTime Finished { get; set; }
+
+
+    }
+}

+ 16 - 0
ProcessServer/Business/StatisticCounters/BaseCounter.cs

@@ -0,0 +1,16 @@
+namespace BO.ProcessServer.Business.StatisticCounters
+{
+    internal abstract class BaseCounter
+    {
+        public string Name { get; }
+
+        public BaseCounter(string name)
+        {
+            Name = name;
+        }
+
+        public abstract object GetValue();
+
+        public abstract void Reset();
+    }
+}

+ 49 - 0
ProcessServer/Business/StatisticCounters/IncrementalCounter.cs

@@ -0,0 +1,49 @@
+namespace BO.ProcessServer.Business.StatisticCounters
+{
+    internal class IncrementalCounter : BaseCounter
+    {
+
+        #region *** Private fields ***
+        private long _value;
+        #endregion
+
+        #region *** Constructor ***
+        public IncrementalCounter(string name) : base(name)
+        {
+        }
+        #endregion
+
+        #region *** Public Operations ***
+
+        public void Increment()
+        {
+            _value++;
+        }
+        public void Decrement()
+        {
+            _value--;
+        }
+
+        public void SetValue(long newValue)
+        {
+            _value = newValue;
+        }
+
+        public override object GetValue()
+        {
+            return _value;
+        }
+
+        public long GetTypedValue()
+        {
+            return (long) GetValue();
+        }
+
+
+        public override void Reset()
+        {
+            _value = 0;
+        }
+        #endregion
+    }
+}

+ 76 - 0
ProcessServer/Business/StatisticCounters/Statistics.cs

@@ -0,0 +1,76 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+// ReSharper disable InconsistentNaming
+
+namespace BO.ProcessServer.Business.StatisticCounters
+{
+    internal class Statistics
+    {
+        #region *** Constants ***
+        public const string CNT_PS_ONLINE_DUR = "ProcessServerOnline";
+
+        public const string CNT_SCENARIO_PROCESSED_INC = "ScenariosProcessedCount";
+        public const string CNT_SCENARIO_PROCESSED_OK_INC = "ScenariosProcessedOkCount";
+        public const string CNT_SCENARIO_PROCESSED_FAIL_INC = "ScenariosProcessedFailCount";
+
+        public const string CNT_SCENARIO_AVG_DUR = "ScenariosAVGDuration";
+        public const string CNT_SCENARIO_MIN_DUR = "ScenariosMINDuration";
+        public const string CNT_SCENARIO_MAX_DUR = "ScenariosMAXDuration";
+        #endregion
+
+        #region *** Private fields ***
+        private IList<BaseCounter> _counters = new List<BaseCounter>();
+        #endregion
+
+        #region *** Constructors ***
+        public Statistics()
+        {
+            _counters.Add(new TimespanAVGCounter(CNT_SCENARIO_AVG_DUR));
+            _counters.Add(new TimespanMINCounter(CNT_SCENARIO_MIN_DUR));
+            _counters.Add(new TimespanMAXCounter(CNT_SCENARIO_MAX_DUR));
+            _counters.Add(new IncrementalCounter(CNT_SCENARIO_PROCESSED_INC));
+            _counters.Add(new IncrementalCounter(CNT_SCENARIO_PROCESSED_OK_INC));
+            _counters.Add(new IncrementalCounter(CNT_SCENARIO_PROCESSED_FAIL_INC));
+            _counters.Add(new TimespanCounter(CNT_PS_ONLINE_DUR));
+        }
+        #endregion
+
+        #region *** Public Operations ***
+        public BaseCounter GetCounter(string name)
+        {
+            CheckExistenceCounter(name);
+            return _counters.First(x => x.Name == name);
+        }
+        public TCounter GetCounter<TCounter>(string name) where TCounter : BaseCounter
+        {
+            return (TCounter)GetCounter(name);
+        }
+        public void ResetCounter(string name)
+        {
+            CheckExistenceCounter(name);
+
+        }
+        #endregion
+
+        #region *** Private Operations ***
+
+        private bool ExistsCounter(string name)
+        {
+            return GetCounterInternal(name)!=null;
+        }
+
+        private BaseCounter GetCounterInternal(string name)
+        {
+            return _counters.FirstOrDefault(x => x.Name == name);
+        }
+
+        private void CheckExistenceCounter(string name)
+        {
+            if (!ExistsCounter(name))
+                throw new ArgumentOutOfRangeException(nameof(name),$"Counter '{name}' doesn't exists!");
+        }
+        #endregion
+    }
+}

+ 40 - 0
ProcessServer/Business/StatisticCounters/TimespanAVGCounter.cs

@@ -0,0 +1,40 @@
+using System;
+// ReSharper disable InconsistentNaming
+
+namespace BO.ProcessServer.Business.StatisticCounters
+{
+    internal class TimespanAVGCounter : TimespanCounter
+    {
+        public TimeSpan TotalValue { get; private set; }
+        public int Items { get; private set; }
+
+        public TimespanAVGCounter(string name) : base(name)
+        {
+            Items = 0;
+        }
+
+        public override void Stop()
+        {
+            base.Stop();
+            Items++;
+            TotalValue.Add(GetValueInternal());
+        }
+
+        public override object GetValue()
+        {
+            var msec = GetValueInternal().TotalMilliseconds;
+            return TimeSpan.FromMilliseconds(msec / Items);
+        }
+
+        private TimeSpan GetValueInternal()
+        {
+            if (!StartTimestamp.HasValue)
+                return TimeSpan.Zero;
+
+            if (!StopTimestamp.HasValue)
+                return DateTime.Now - StartTimestamp.GetValueOrDefault();
+
+            return StopTimestamp.GetValueOrDefault() - StartTimestamp.GetValueOrDefault();
+        }
+    }
+}

+ 55 - 0
ProcessServer/Business/StatisticCounters/TimespanCounter.cs

@@ -0,0 +1,55 @@
+using System;
+
+namespace BO.ProcessServer.Business.StatisticCounters
+{
+    internal class TimespanCounter : BaseCounter
+    {
+
+        public bool IsRunning { get; private set; }
+        public DateTime? StartTimestamp { get; private set; }
+        public DateTime? StopTimestamp { get; private set; }
+
+        public TimespanCounter(string name) : base(name)
+        {
+        }
+
+        public TimeSpan GetTypedValue()
+        {
+            return (TimeSpan) GetValue();
+        }
+
+        public override object GetValue()
+        {
+            if (!StartTimestamp.HasValue)
+                return TimeSpan.Zero;
+
+            if (!StopTimestamp.HasValue)
+                return DateTime.Now - StartTimestamp.GetValueOrDefault();
+
+            return StopTimestamp.GetValueOrDefault() - StartTimestamp.GetValueOrDefault();
+        }
+
+        public virtual void Start()
+        {
+            if (IsRunning)
+                throw new InvalidOperationException($"Cannot start counter '{Name}' is already running.");
+            IsRunning = true;
+            StartTimestamp = DateTime.Now;
+        }
+
+        public virtual void Stop()
+        {
+            if (!IsRunning)
+                return;
+            IsRunning = false;
+            StopTimestamp = DateTime.Now;
+        }
+
+        public override void Reset()
+        {
+            IsRunning = false;
+            StartTimestamp = null;
+            StopTimestamp = null;
+        }
+    }
+}

+ 38 - 0
ProcessServer/Business/StatisticCounters/TimespanMAXCounter.cs

@@ -0,0 +1,38 @@
+using System;
+// ReSharper disable InconsistentNaming
+
+namespace BO.ProcessServer.Business.StatisticCounters
+{
+    internal class TimespanMAXCounter : TimespanCounter
+    {
+        public TimeSpan TotalValue { get; private set; }
+
+        public TimespanMAXCounter(string name) : base(name)
+        {
+        }
+
+        public override void Stop()
+        {
+            base.Stop();
+            var value = GetValueInternal();
+            if (TotalValue < value)
+                TotalValue = value;
+        }
+
+        public override object GetValue()
+        {
+            return TotalValue;
+        }
+
+        private TimeSpan GetValueInternal()
+        {
+            if (!StartTimestamp.HasValue)
+                return TimeSpan.Zero;
+
+            if (!StopTimestamp.HasValue)
+                return DateTime.Now - StartTimestamp.GetValueOrDefault();
+
+            return StopTimestamp.GetValueOrDefault() - StartTimestamp.GetValueOrDefault();
+        }
+    }
+}

+ 38 - 0
ProcessServer/Business/StatisticCounters/TimespanMINCounter.cs

@@ -0,0 +1,38 @@
+using System;
+// ReSharper disable InconsistentNaming
+
+namespace BO.ProcessServer.Business.StatisticCounters
+{
+    internal class TimespanMINCounter : TimespanCounter
+    {
+        public TimeSpan TotalValue { get; private set; }
+
+        public TimespanMINCounter(string name) : base(name)
+        {
+        }
+
+        public override void Stop()
+        {
+            base.Stop();
+            var value = GetValueInternal();
+            if (TotalValue > value)
+                TotalValue = value;
+        }
+
+        public override object GetValue()
+        {
+            return TotalValue;
+        }
+
+        private TimeSpan GetValueInternal()
+        {
+            if (!StartTimestamp.HasValue)
+                return TimeSpan.Zero;
+
+            if (!StopTimestamp.HasValue)
+                return DateTime.Now - StartTimestamp.GetValueOrDefault();
+
+            return StopTimestamp.GetValueOrDefault() - StartTimestamp.GetValueOrDefault();
+        }
+    }
+}

+ 18 - 0
ProcessServer/Business/Workers/Contexts/BaseCtx.cs

@@ -0,0 +1,18 @@
+using System;
+using BO.ProcessServer.Business.StatisticCounters;
+using Quadarax.Foundation.Core.Thread;
+
+namespace BO.ProcessServer.Business.Workers.Contexts
+{
+    internal abstract class BaseCtx : IWorkerContext
+    {
+        public Statistics Statistics { get; }
+        public Pool Pool { get; }
+
+        protected BaseCtx(Statistics statistics, Pool pool)
+        {
+            Statistics = statistics ?? throw new ArgumentNullException(nameof(statistics));
+            Pool = pool ?? throw new ArgumentNullException(nameof(pool));
+        }
+    }
+}

+ 16 - 0
ProcessServer/Business/Workers/Contexts/CfgCtx.cs

@@ -0,0 +1,16 @@
+using System;
+using BO.ProcessServer.Business.StatisticCounters;
+using BO.ProcessServer.Options;
+
+namespace BO.ProcessServer.Business.Workers.Contexts
+{
+    internal class CfgCtx : BaseCtx
+    {
+        public Configuration Configuration { get; }
+
+        public CfgCtx(Configuration configuration, Statistics statistics, Pool pool) : base(statistics, pool)
+        {
+            Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
+        }
+    }
+}

+ 25 - 0
ProcessServer/Business/Workers/Contexts/ProcessingDocumentsCtx.cs

@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.IO.Abstractions;
+using BO.ProcessServer.Business.StatisticCounters;
+using BO.ProcessServer.Options;
+using Connector.PS;
+
+namespace BO.ProcessServer.Business.Workers.Contexts
+{
+    internal class ProcessingDocumentsCtx : CfgCtx
+    {
+
+        public IList<ServerProcess> Processes { get; }
+        public Connection Connection { get; set; }
+        public IFileSystem FileSystem { get; set; }
+
+
+        public ProcessingDocumentsCtx(Configuration configuration,IList<ServerProcess> processes,Connection connection, IFileSystem fileSystem, Statistics statistics, Pool pool) : base(configuration, statistics, pool)
+        {
+            Processes = processes ?? throw new ArgumentNullException(nameof(processes));
+            Connection = connection ?? throw new ArgumentNullException(nameof(connection));
+            FileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
+        }
+    }
+}

+ 24 - 0
ProcessServer/Business/Workers/Contexts/PullCtx.cs

@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.IO.Abstractions;
+using BO.ProcessServer.Business.StatisticCounters;
+using Connector.PS;
+using Quadarax.Foundation.Core.IO.MimeTypes;
+
+namespace BO.ProcessServer.Business.Workers.Contexts
+{
+    internal class PullCtx : BaseCtx
+    {
+        public Connection Connection { get; set; }
+        public IFileSystem FileSystem { get; set; }
+        public IList<DocumentContent> Documents { get; }
+
+        public PullCtx(Connection connection, IFileSystem fileSystem, Statistics statistics, Pool pool) : base(statistics, pool)
+        {
+            Connection = connection ?? throw new ArgumentNullException(nameof(connection));
+            FileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
+            Documents = new List<DocumentContent>();
+        }
+
+    }
+}

+ 40 - 0
ProcessServer/Business/Workers/ProcessingDocumentsWorker.cs

@@ -0,0 +1,40 @@
+using System.Collections.Generic;
+using System.IO.Abstractions;
+using System.Threading.Tasks;
+using BO.ProcessServer.Business.StatisticCounters;
+using BO.ProcessServer.Business.Workers.Contexts;
+using BO.ProcessServer.Options;
+using Connector.PS;
+using Quadarax.Foundation.Core.Logging;
+using Quadarax.Foundation.Core.Thread;
+
+namespace BO.ProcessServer.Business.Workers
+{
+    internal class ProcessingDocumentsWorker: LoopWorker
+    {
+        private const string CS_WK_NAME = "ProcessingDocument";
+        public ProcessingDocumentsWorker(Configuration configuration,IList<ServerProcess> processes, Connection connection, IFileSystem fileSystem,Statistics statistics, Pool pool, ILogger logger) 
+            : base(CS_WK_NAME, new ProcessingDocumentsCtx(configuration, processes, connection, fileSystem, statistics, pool), logger)
+        {
+        }
+
+        public ProcessingDocumentsWorker(string workerName) : base(workerName)
+        {
+        }
+
+        public ProcessingDocumentsWorker(string workerName, IWorkerContext context, ILogger logger = null) : base(workerName, context, logger)
+        {
+        }
+
+        protected override async Task DoIteration(IWorkerContext context)
+        {
+            var ctx = (ProcessingDocumentsCtx)context;
+            // ensure connection
+            if (!ctx.Connection.IsOpen)
+                ctx.Connection.Open(ctx.Configuration.ApiUser, ctx.Configuration.ApiUserPwd, ctx.Configuration.ApiMagic);
+
+                
+
+        }
+    }
+}

+ 27 - 0
ProcessServer/Business/Workers/PullDocumentsWorker.cs

@@ -0,0 +1,27 @@
+using System;
+using System.IO.Abstractions;
+using BO.ProcessServer.Business.StatisticCounters;
+using BO.ProcessServer.Business.Workers.Contexts;
+using Connector.PS;
+using Quadarax.Foundation.Core.Logging;
+using Quadarax.Foundation.Core.Thread;
+
+namespace BO.ProcessServer.Business.Workers
+{
+    internal class PullDocumentsWorker : LoopWorker
+    {
+        private const string CS_WK_NAME = "PullDocument";
+        public PullDocumentsWorker(Connection connection, TimeSpan loopEveryTimeSpan, IFileSystem fileSystem, Statistics statistics,Pool pool, ILogger logger) : base(CS_WK_NAME, new PullCtx(connection, fileSystem, statistics, pool), logger)
+        {
+            IterationsDelayBetween = loopEveryTimeSpan;
+        }
+
+        public PullDocumentsWorker(string workerName) : base(workerName)
+        {
+        }
+
+        public PullDocumentsWorker(string workerName, IWorkerContext context, ILogger logger = null) : base(workerName, context, logger)
+        {
+        }
+    }
+}

+ 10 - 0
ProcessServer/Business/Workers/PushDocumentsWorker.cs

@@ -0,0 +1,10 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace BO.ProcessServer.Business.Workers
+{
+    class PushDocumentsWorker
+    {
+    }
+}

+ 25 - 0
ProcessServer/Business/Workers/RetentionWorker.cs

@@ -0,0 +1,25 @@
+using Quadarax.Foundation.Core.Logging;
+using Quadarax.Foundation.Core.Thread;
+
+namespace BO.ProcessServer.Business.Workers
+{
+    public class RetentionWorker : LoopWorker
+    {
+        private const string CS_WK_NAME = "Retention";
+        public RetentionWorker(IWorkerContext context) : base(CS_WK_NAME, context)
+        {
+        }
+
+        public RetentionWorker(string workerName, IWorkerContext context) : base(workerName, context)
+        {
+        }
+
+        public RetentionWorker(string workerName) : base(workerName)
+        {
+        }
+
+        public RetentionWorker(string workerName, IWorkerContext context, ILogger logger = null) : base(workerName, context, logger)
+        {
+        }
+    }
+}

+ 2 - 1
ProcessServer/Commands/Base/BaseLocalCommand.cs

@@ -3,7 +3,7 @@ using System.Collections.Generic;
 using System.IO.Abstractions;
 using System.Linq;
 using System.Text.Json;
-using BO.AppServer.Metadata.Configuration;
+using BO.ProcessServer.Options;
 using Quadarax.Foundation.Core.Console;
 using Quadarax.Foundation.Core.Data.Interface.Entity;
 using Quadarax.Foundation.Core.Logging;
@@ -36,6 +36,7 @@ namespace BO.ProcessServer.Commands.Base
 
         protected IFileSystem FileSystem { get; private set; }
 
+
         protected BaseLocalCommand(Engine engine) : base(engine)
         {
 

+ 3 - 1
ProcessServer/Commands/RunCommand.cs

@@ -1,5 +1,7 @@
 using System;
+using BO.ProcessServer.Business;
 using BO.ProcessServer.Commands.Base;
+using Quadarax.Foundation.Core.NLog;
 using Quadarax.Foundation.Core.QConsole;
 using Quadarax.Foundation.Core.Value;
 // ReSharper disable InconsistentNaming
@@ -32,7 +34,7 @@ namespace BO.ProcessServer.Commands
 
         protected override Result OnExecute()
         {
-            throw new NotImplementedException();
+            return new Result();
         }
         #endregion
 

+ 1 - 0
ProcessServer/Constants.cs

@@ -4,5 +4,6 @@
     internal class Constants
     {
         public const string CS_CONFIGURATION_FILE = "processserver.json";
+        public const string CS_NLOG_CONFIGURATION_FILE = "processserver-nlog.config";
     }
 }

+ 74 - 16
ProcessServer/Options/Configuration.cs

@@ -1,16 +1,74 @@
-using System;
-using System.Text.Json.Serialization;
-
-namespace BO.ProcessServer.Options
-{
-    internal class Configuration
-    {
-        [JsonPropertyName("api-base-url")] public Uri ApiBaseUrl { get; set; }
-        [JsonPropertyName("api-user")] public string ApiUser { get; set; }
-        [JsonPropertyName("api-pwd")] public string ApiUserPwd { get; set; }
-        [JsonPropertyName("api-magic")] public string ApiMagic { get; set; }
-        [JsonPropertyName("api-timeout")] public string ApiTimeoutRaw { get; set; }
-        public TimeSpan ApiTimeout => TimeSpan.Parse(ApiTimeoutRaw);
-    }
-
-}
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+using BO.ProcessServer.Business.Enums;
+
+namespace BO.ProcessServer.Options
+{
+    internal class Configuration
+    {
+        [JsonPropertyName("api-base-url")] public Uri ApiBaseUrl { get; set; }
+        [JsonPropertyName("api-user")] public string ApiUser { get; set; }
+        [JsonPropertyName("api-pwd")] public string ApiUserPwd { get; set; }
+        [JsonPropertyName("api-magic")] public string ApiMagic { get; set; }
+        [JsonPropertyName("api-timeout")] public string ApiTimeoutRaw { get; set; }
+        public TimeSpan ApiTimeout => TimeSpan.Parse(ApiTimeoutRaw);
+
+        public CfgSystem System { get; set; }
+        public class CfgSystem
+        {
+            [JsonPropertyName("mode")]  public AppModeEnum Mode { get; set; }
+            [JsonPropertyName("identification")] public string Identification { get; set; }
+            [JsonPropertyName("preferred-interface")] public string PreferredInterface { get; set; }
+            [JsonPropertyName("pool-pull-documents")] public int PoolPullDocuments { get; set; }
+            [JsonPropertyName("pool-pull-interval")] public string PoolPullIntervalRaw { get; set; }
+            public TimeSpan PoolPullInterval => TimeSpan.Parse(PoolPullIntervalRaw);
+            [JsonPropertyName("pool-path-base")] public string PoolPathBase { get; set; }
+            [JsonPropertyName("pool-path-pending")] public string PoolPathPending { get; set; }
+            [JsonPropertyName("pool-path-working")] public string PoolPathWorking { get; set; }
+            [JsonPropertyName("pool-path-done")] public string PoolPathDone { get; set; }
+            [JsonPropertyName("pool-path-fail")] public string PoolPathFail { get; set; }
+            [JsonPropertyName("pool-retention")] public CfgPoolRetention Retention { get; set; }
+            [JsonPropertyName("status-monitoring")] public CfgStatusMonitoring Status { get; set; }
+            [JsonPropertyName("scenario-path-base")] public string ScenarioPathBase { get; set; }
+            [JsonPropertyName("scenario-parallels")] public int ScenarioParallelThreads { get; set; }
+            [JsonPropertyName("scenarios")] public IEnumerable<CfgScenario> Scenarios { get; set; }
+        }
+
+        public class CfgScenarioAcceptFilter
+        {
+            [JsonPropertyName("code")] public string Code { get; set; }
+            [JsonPropertyName("value")] public string Value { get; set; }
+        }
+
+        public class CfgScenario
+        {
+            [JsonPropertyName("name")] public string Name { get; set; }
+            [JsonPropertyName("cmd")] public string ShellCommand { get; set; }
+            [JsonPropertyName("args")] public string ShellCommandArguments { get; set; }
+            [JsonPropertyName("accept-filters")] public IEnumerable<CfgScenarioAcceptFilter> AcceptFilters { get; set; }
+            [JsonPropertyName("timeout-interval")] public string TimeoutIntervalRaw { get; set; }
+            public TimeSpan TimeoutInterval => TimeSpan.Parse(TimeoutIntervalRaw);
+            [JsonPropertyName("exitcode-succ")] public int ExitCodeSucc { get; set; }
+            [JsonPropertyName("exitcode-fail")] public int ExitCodeFail { get; set; }
+        }
+
+        public class CfgStatusMonitoring
+        {
+            [JsonPropertyName("enabled")] public bool Enabled { get; set; }
+            [JsonPropertyName("interval")] public string IntervalRaw { get; set; }
+            public TimeSpan Interval => TimeSpan.Parse(IntervalRaw);
+            [JsonPropertyName("file")] public string File { get; set; }
+        }
+
+        public class CfgPoolRetention
+        {
+            [JsonPropertyName("enabled")] public bool Enabled { get; set; }
+            [JsonPropertyName("interval")] public string IntervalRaw { get; set; }
+            public TimeSpan Interval => TimeSpan.Parse(IntervalRaw);
+            [JsonPropertyName("size")] public long Size { get; set; }
+            [JsonPropertyName("items")] public int Items { get; set; }
+        }
+    }
+
+}

+ 15 - 0
ProcessServer/ProcessServer.csproj

@@ -10,18 +10,33 @@
 
   <ItemGroup>
     <ProjectReference Include="..\AppServer\Connector.PS\Connector.PS.csproj" />
+    <ProjectReference Include="..\Common\qdr.fnd.core.nlog\qdr.fnd.core.nlog.csproj" />
     <ProjectReference Include="..\Common\qdr.fnd.core.qconsole\qdr.fnd.core.qconsole.csproj" />
     <ProjectReference Include="..\Common\qdr.fnd.core\qdr.fnd.core.csproj" />
   </ItemGroup>
 
   <ItemGroup>
     <Folder Include="Commands\Base\" />
+    <Folder Include="Scenarios\" />
   </ItemGroup>
 
   <ItemGroup>
+    <PackageReference Include="NLog" Version="4.7.13" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <None Update="processServer-nlog.config">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
     <None Update="processServer.json">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </None>
+    <None Update="Scenarios\scenario-dummy.cmd">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="Scenarios\scenario-fail.cmd">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
   </ItemGroup>
 
 </Project>

+ 8 - 1
ProcessServer/ProcessServer.sln

@@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
 # Visual Studio Version 16
 VisualStudioVersion = 16.0.32002.261
 MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProcessServer", "ProcessServer.csproj", "{4AA6E6D4-D619-46E8-9211-E4863F65A492}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProcessServer", "ProcessServer.csproj", "{4AA6E6D4-D619-46E8-9211-E4863F65A492}"
 EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{86FAB9A1-4A29-4C77-984D-6BB615E70466}"
 EndProject
@@ -21,6 +21,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Metadata", "..\AppServer\Me
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connector", "..\AppServer\Connector\Connector.csproj", "{7249C7C1-4A69-40C1-882B-993788B961DE}"
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "qdr.fnd.core.nlog", "..\Common\qdr.fnd.core.nlog\qdr.fnd.core.nlog.csproj", "{16646C21-0493-4893-BC49-93EEA6BE674F}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -55,6 +57,10 @@ Global
 		{7249C7C1-4A69-40C1-882B-993788B961DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{7249C7C1-4A69-40C1-882B-993788B961DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{7249C7C1-4A69-40C1-882B-993788B961DE}.Release|Any CPU.Build.0 = Release|Any CPU
+		{16646C21-0493-4893-BC49-93EEA6BE674F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{16646C21-0493-4893-BC49-93EEA6BE674F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{16646C21-0493-4893-BC49-93EEA6BE674F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{16646C21-0493-4893-BC49-93EEA6BE674F}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -66,6 +72,7 @@ Global
 		{21857816-CD4F-4F46-9C8F-9EC7491CD8A9} = {86FAB9A1-4A29-4C77-984D-6BB615E70466}
 		{7723EB63-8B98-462B-8BC7-580CDDA3815D} = {BBFD1410-90B2-4C40-970A-D19FB35DFE7A}
 		{7249C7C1-4A69-40C1-882B-993788B961DE} = {BBFD1410-90B2-4C40-970A-D19FB35DFE7A}
+		{16646C21-0493-4893-BC49-93EEA6BE674F} = {86FAB9A1-4A29-4C77-984D-6BB615E70466}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {1E109E7F-8E32-4A46-982B-F8FC763EB73D}

+ 3 - 0
ProcessServer/Program.cs

@@ -1,4 +1,5 @@
 using System.Reflection;
+using Quadarax.Foundation.Core.NLog;
 using Quadarax.Foundation.Core.QConsole;
 using Quadarax.Foundation.Core.QConsole.Configuration;
 using Quadarax.Foundation.Core.QConsole.Context;
@@ -10,6 +11,8 @@ namespace BO.ProcessServer
     {
         static void Main(string[] args)
         {
+            new LoggerFactory().Configure(Constants.CS_NLOG_CONFIGURATION_FILE);
+
             var config = new StartupConfiguration("BO ProcessServer", Assembly.GetExecutingAssembly().GetName().Version);
             config.ConsoleCopyright = Assembly.GetExecutingAssembly().GetCopyright();
             config.AllowInteractive = false;

+ 3 - 0
ProcessServer/Scenarios/scenario-dummy.cmd

@@ -0,0 +1,3 @@
+@echo off
+rem -- dummy scenario always returns OK
+exit 0

+ 3 - 0
ProcessServer/Scenarios/scenario-fail.cmd

@@ -0,0 +1,3 @@
+@echo off
+rem -- dummy scenario always returns FAILS
+exit 1

+ 51 - 0
ProcessServer/processServer-nlog.config

@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- XSD manual extracted from package NLog.Schema: https://www.nuget.org/packages/NLog.Schema-->
+<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xsi:schemaLocation="NLog NLog.xsd"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  autoReload="true" >
+
+  <variable name="logDirectory" value="D:\Logs\BO" />
+  <variable name="appName" value="BO.ProcessServer" />
+
+  <!-- the targets to write to -->
+  <targets>
+    <target xsi:type="File" 
+      name="OutToFile" 
+      layout="${date:format=yyyy-MM-dd HH\:mm\:ss.fff}|${level:uppercase=true}|${threadid}|${message}|${logger}|${all-event-properties}" 
+      fileName="${logDirectory}\${appName}.log"
+      archiveFileName="${logDirectory}\${appName}.{#}.log"
+      archiveEvery="Day"
+      archiveAboveSize="104857600"
+      archiveNumbering="DateAndSequence"
+      archiveDateFormat="yyyy-MM-dd"
+      maxArchiveFiles="30"
+      concurrentWrites="false"
+      keepFileOpen="true"
+      encoding="utf-8"
+      writeBom="true" />
+    <target xsi:type="File" 
+      name="OutToFileException" 
+      layout="${date:format=yyyy-MM-dd HH\:mm\:ss.fff}|${level:uppercase=true}|${threadid}|${message} ${exception}|${logger}|${all-event-properties}"
+      fileName="${logDirectory}\${appName}-Exceptions.log"
+      archiveFileName="${logDirectory}\${appName}-Exceptions.{#}.log"
+      archiveEvery="Day"
+      archiveAboveSize="104857600"
+      archiveNumbering="DateAndSequence"
+      archiveDateFormat="yyyy-MM-dd"
+      maxArchiveFiles="30"
+      concurrentWrites="false"
+      keepFileOpen="true"
+      encoding="utf-8"
+      writeBom="true" />
+    <target xsi:type="Console" 
+            name="OutToConsole"
+            layout="[${level:uppercase=true}/${threadid}] ${message}" />
+  </targets>
+
+  <!-- rules to map from logger name to target -->
+  <rules>
+    <logger name="*" writeTo="OutToConsole" />
+    <logger name="*" writeTo="OutToFile" />
+    <logger name="*" level="Error" writeTo="OutToFileException" />    
+  </rules>
+</nlog>

+ 45 - 3
ProcessServer/processServer.json

@@ -1,7 +1,49 @@
 {
 	"api-base-url": "http://localhost:5000/api/",
-	"api-user": "Console",
-	"api-pwd": "A1Abastr0vA",
-	"api-magic": "6DEC4167-57A6-4333-92DA-ADE009A97DE9",
+	"api-user": "Process",
+	"api-pwd": "SmRAd1avKA",
+	"api-magic": "SHARE",
 	"api-timeout": "00:10:00",
+	"System": {
+		"mode": "proxy",
+		"identification": "DevNode#1",
+		"preferred-interface": "auto",
+		"pool-pull-documents": 4,
+		"pool-pull-interval": "00:01:00",
+		"pool-path-base": "D:\\BO.PS",
+		"pool-path-pending": ".pending",
+		"pool-path-working": ".working",
+		"pool-path-done": ".done",
+		"pool-path-fail": ".fail",
+		"pool-retention": {
+			"enabled": true,
+			"interval": "24:00:00",
+			"size": 1000000000,
+			"items": 100
+		},
+		"status-monitoring": {
+			"enabled": true,
+			"file": "{CD}\\status.csv",
+			"interval": "00:00:10"
+		},
+
+		"scenario-base": "{CD}\\Scenarios",
+    "scenario-parallels": 2,
+		"Scenarios": [
+			{
+				"name": "test",
+				"accept-filters": [
+					{
+						"code": "profile",
+						"value": "test"
+					}
+				],
+				"cmd": "scenario-dummy.cmd",
+				"args": "{filenames} {billingplan}",
+				"timeout-interval": "01:00:00",
+				"exitcode-succ": 0,
+				"exitcode-fail": 1
+			}
+		]
+	}
 }