浏览代码

PSConnection Proxy

Enable Pool
Tested PullDocumentsWorker
Dalibor Votruba 4 年之前
父节点
当前提交
d4747759ce

+ 8 - 1
AppServer/Connector.PS/Connection.cs

@@ -11,7 +11,7 @@ using Quadarax.Foundation.Core.Logging;
 
 namespace Connector.PS
 {
-    public class Connection : AbstractConnection
+    public class Connection : AbstractConnection, IConnectionPS
     {
         #region *** Private fields ***
         protected override string ApiName => "PS";
@@ -58,6 +58,13 @@ namespace Connector.PS
 
         }
 
+        public async Task<DocumentRDto> GetDocumentInfoAsync(long documentId)
+        {
+            //{ticket}/queue/{registrationId}/document/{documentId}/info
+            var result = await CallRequestAsync<ResultValueDto<DocumentRDto>>($"{Ticket}/queue/{_registrationId}/document/{documentId}/info", RestCallTypeEnum.Get);
+            return result.Value;
+        }
+
         public async Task<DocumentStatusEnum> SetDocumentStateAsync(long documentId, DocumentStatusEnum status)
         {
             if (!(status == DocumentStatusEnum.New || status == DocumentStatusEnum.Processing || status == DocumentStatusEnum.DoneOk || status == DocumentStatusEnum.DoneFail))

+ 23 - 0
AppServer/Connector.PS/IConnectionPS.cs

@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using BO.AppServer.Metadata.Dto;
+using BO.AppServer.Metadata.Enums;
+
+namespace Connector.PS
+{
+    public interface IConnectionPS : IDisposable
+    {
+        Task<IEnumerable<DocumentRDto>> GetDocumentsAsync(DocumentsScopeEnums scope);
+        Task<DocumentStatusEnum> GetDocumentStateAsync(long documentId);
+        Task<DocumentRDto> GetDocumentInfoAsync(long documentId);
+        Task<DocumentStatusEnum> SetDocumentStateAsync(long documentId, DocumentStatusEnum status);
+        Task<Stream> GetDocumentContentAsync(long documentId, long artifactId);
+        Task<ArtifactRDto> SetDocumentContentAsync(long documentId, string fileName, string mimeType, bool overwriteIfExists, Stream contentStream);
+        bool IsOpen { get; }
+        void Open(string userName, string userPassword, string magicKey);
+        void Close();
+        bool Ping(out TimeSpan duration, out string status);
+    }
+}

+ 122 - 114
AppServer/Web/Services/PSController.cs

@@ -1,125 +1,133 @@
-using System;
-using System.Threading.Tasks;
-using BO.AppServer.Business.Services;
-using BO.AppServer.Metadata.Dto;
-using BO.AppServer.Metadata.Enums;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Logging;
-using Quadarax.Foundation.Core.Data.Interface.Entity.Dto;
-
-namespace BO.AppServer.Web.Services
-{
-    [Authorize]
-    [Route("api/[controller]")]
-    [ApiController]
-    public class PSController : Base<PSController>
+using System;
+using System.Threading.Tasks;
+using BO.AppServer.Business.Services;
+using BO.AppServer.Metadata.Dto;
+using BO.AppServer.Metadata.Enums;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using Quadarax.Foundation.Core.Data.Interface.Entity.Dto;
+
+namespace BO.AppServer.Web.Services
+{
+    [Authorize]
+    [Route("api/[controller]")]
+    [ApiController]
+    public class PSController : Base<PSController>
     {
-        #region *** Properties ***
+        #region *** Properties ***
         protected override RoleEnum LoginRoleAllowed => RoleEnum.System;
-        #endregion
+        #endregion
 
-        #region *** Private fields ***
+        #region *** Private fields ***
         private DocumentService _srvDocument;
-        #endregion
+        #endregion
+
+        #region *** Constructors ***
+
+        public PSController(ILoggerFactory logger, AccessService srvAccess) : base(logger, srvAccess)
+        {
+        }
+        #endregion
+
+        #region *** Registrations ***
+        [HttpPost("{ticket}/register")]
+        public async Task<ResultValueDto<RegistrationRDto>> Register(string ticket, [FromBody] RegistrationCDto registration)
+        {
+            CheckAccess(ticket);
+            //return await Call(async () => await _srvUsers.CreateUserAsync(user));
+            return null;
+        }
+
+        [HttpPatch("{ticket}/register")]
+        public async Task<ResultValueDto<RegistrationRDto>> Register(string ticket, [FromBody] RegistrationUDto registration)
+        {
+            CheckAccess(ticket);
+            //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 *** Constructors ***
+        #region *** Document gathering ***
+        [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}/queue/{registrationId}/working")]
+        public async Task<ResultsValueDto<DocumentRDto>> GetQueuePending(string ticket, long registrationId)
+        {
+            CheckAccess(ticket);
+            //return await Call(async () => await _srvUsers.CreateUserAsync(user));
+            return null;
+        }
 
-        public PSController(ILoggerFactory logger, AccessService srvAccess) : base(logger, srvAccess)
-        {
+        [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
+            try
+            {
+                CheckAccess(ticket);
+                //var id = await _srvWorkspace.GetWorkspaceIdByAPIAccess(wrkspApiKey, apiKeyPassword, context);
+                //var content = await _srvDocument.GetArtifactAsync(id, documentId, artifactId);
+                //return new FileStreamResult(content.Item1, content.Item2);
+                return null;
+            }
+            catch (Exception e)
+            {
+                return Problem(e.Message);
+            }
         }
-        #endregion
 
-        #region *** Registrations ***
-        [HttpPost("{ticket}/register")]
-        public async Task<ResultValueDto<RegistrationRDto>> Register(string ticket, [FromBody] RegistrationCDto registration)
-        {
-            CheckAccess(ticket);
-            //return await Call(async () => await _srvUsers.CreateUserAsync(user));
-            return null;
-        }
-
-        [HttpPatch("{ticket}/register")]
-        public async Task<ResultValueDto<RegistrationRDto>> Register(string ticket, [FromBody] RegistrationUDto registration)
-        {
-            CheckAccess(ticket);
-            //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}/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}/queue/{registrationId}/working")]
-        public async Task<ResultsValueDto<DocumentRDto>> GetQueuePending(string ticket, long registrationId)
-        {
-            CheckAccess(ticket);
-            //return await Call(async () => await _srvUsers.CreateUserAsync(user));
-            return null;
-        }
-
-        [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
-            try
-            {
-                CheckAccess(ticket);
-                //var id = await _srvWorkspace.GetWorkspaceIdByAPIAccess(wrkspApiKey, apiKeyPassword, context);
-                //var content = await _srvDocument.GetArtifactAsync(id, documentId, artifactId);
-                //return new FileStreamResult(content.Item1, content.Item2);
-                return null;
-            }
-            catch (Exception e)
-            {
-                return Problem(e.Message);
-            }
-        }
+        #endregion
 
-        #endregion
+        #region *** Document state operations ***
+        [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;
+        }
 
-        #region *** Document state operations ***
-        [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;
-        }
-
-        [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
+        [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;
+        }
 
-        #region *** Document uploading ***
-        [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));
-            return null;
-        }
-        #endregion
-    }
-}
+        [HttpGet("{ticket}/queue/{registrationId}/document/{documentId}/info")]
+        public async Task<ResultValueDto<DocumentRDto>> GetDocumentInfo(string ticket, long registrationId, long documentId)
+        {
+            CheckAccess(ticket);
+            //return await Call(async () => await _srvUsers.CreateUserAsync(user));
+            return null;
+        }
+
+        #endregion
+
+        #region *** Document uploading ***
+        [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));
+            return null;
+        }
+        #endregion
+    }
+}

+ 49 - 0
Common/qdr.fnd.core/Value/Extensions/EnumerableExt.cs

@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Quadarax.Foundation.Core.Value.Extensions
+{
+    public static class EnumerableExt
+    {
+        private static Random _rand = new Random();
+        private const string CS_MESSAGE_MUST_HAVE_ONE = "Collection must have at least one element.";
+        private const string CS_MESSAGE_MUST_POSITIVE = "Must be positive number.";
+        private const string CS_MESSAGE_MUST_SMALLER = "fromIndex must be smaller or equal to toIndex.";
+
+
+        public static TElement GetRandom<TElement>(this IEnumerable<TElement> owner)
+        {
+            if (owner == null)
+                throw new ArgumentNullException(nameof(owner));
+
+            if (!owner.Any())
+                throw new ArgumentOutOfRangeException(nameof(owner), CS_MESSAGE_MUST_HAVE_ONE);
+
+            return GetRandom(owner, 0, owner.Count());
+        }
+
+        public static TElement GetRandom<TElement>(this IEnumerable<TElement> owner, int fromIndex, int toIndex = -1)
+        {
+            if (owner == null)
+                throw new ArgumentNullException(nameof(owner));
+
+            if (!owner.Any())
+                throw new ArgumentOutOfRangeException(nameof(owner), CS_MESSAGE_MUST_HAVE_ONE);
+
+            if (toIndex == -1)
+                toIndex = owner.Count() - 1;
+            if (fromIndex<0)
+                throw new ArgumentOutOfRangeException(nameof(fromIndex), CS_MESSAGE_MUST_POSITIVE);
+            if (toIndex<0)
+                throw new ArgumentOutOfRangeException(nameof(fromIndex), CS_MESSAGE_MUST_POSITIVE);
+            if (fromIndex>toIndex)
+                throw new ArgumentOutOfRangeException(nameof(fromIndex), CS_MESSAGE_MUST_SMALLER);
+
+            var index = _rand.Next(fromIndex, toIndex);
+            return owner.Skip(index).Take(1).First();
+
+
+        }
+    }
+}

+ 183 - 0
ProcessServer/Business/ConnectionProxy.cs

@@ -0,0 +1,183 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Abstractions;
+using System.Linq;
+using System.Threading.Tasks;
+using BO.AppServer.Metadata.Dto;
+using BO.AppServer.Metadata.Enums;
+using BO.ProcessServer.Options;
+using Connector.PS;
+using Quadarax.Foundation.Core.IO.MimeTypes;
+using Quadarax.Foundation.Core.Object;
+using Quadarax.Foundation.Core.Value.Extensions;
+
+namespace BO.ProcessServer.Business
+{
+    internal class ConnectionProxy : DisposableObject, IConnectionPS
+    {
+
+
+        #region *** Private fields ***
+        private Configuration _configuration;
+        private Random _rand;
+        private IList<DocumentRDto> _documentsCache;
+        private IFileSystem _fileSystem;
+        private long _currentDocId;
+        private long _currentArtId;
+        #endregion
+
+        #region *** Properties ***
+        public bool IsOpen { get; private set; }
+        #endregion
+
+
+        #region *** Constructor ***
+        public ConnectionProxy(Configuration configuration, IFileSystem fileSystem)
+        {
+            _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
+            _rand = new Random();
+            _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
+            _documentsCache = new List<DocumentRDto>();
+            _currentDocId = _rand.Next(1, 1000);
+            _currentArtId = _rand.Next(1, 10) * _currentDocId;
+        }
+        #endregion
+        
+
+        #region *** Public Operations ***        
+        public async Task<IEnumerable<DocumentRDto>> GetDocumentsAsync(DocumentsScopeEnums scope)
+        {
+            if (!(scope == DocumentsScopeEnums.Pending || scope == DocumentsScopeEnums.Working))
+                throw new ArgumentOutOfRangeException(
+                    $"Document list scope value must be '{DocumentsScopeEnums.Working}' or '{DocumentsScopeEnums.Pending}' int this context.");
+
+            if (_documentsCache.Count(x=>x.Status == DocumentStatusEnum.New) + _configuration.System.Proxy.App.DefaultDocumentsCount < _configuration.System.Proxy.App.MaxDocumentsCount)
+                GenerateDocuments(DocumentStatusEnum.New);
+            
+            var documents = _documentsCache.Where(x => x.Status == DocumentStatusEnum.New)
+                .Take(_configuration.System.PoolPullDocuments).ToArray();
+            foreach (var document in documents)
+                document.Status = DocumentStatusEnum.Pending;
+
+            return documents;
+        }
+
+        public async Task<DocumentStatusEnum> GetDocumentStateAsync(long documentId)
+        {
+            return _documentsCache.Where(x => x.Id == documentId).Select(x=>x.Status).First();
+        }
+
+        public async Task<DocumentRDto> GetDocumentInfoAsync(long documentId)
+        {
+            return _documentsCache.First(x => x.Id == documentId);
+        }
+
+        public async Task<DocumentStatusEnum> SetDocumentStateAsync(long documentId, DocumentStatusEnum status)
+        {
+            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 document = _documentsCache.First(x => x.Id == documentId);
+            document.Status = status;
+            return status;
+        }
+
+        public async Task<Stream> GetDocumentContentAsync(long documentId, long artifactId)
+        {
+            var sw = new StreamWriter(new MemoryStream());
+            await sw.WriteLineAsync("This is a mock data");
+            await sw.FlushAsync();
+            sw.BaseStream.Seek(0, SeekOrigin.Begin);
+            return sw.BaseStream;
+        }
+
+        public async Task<ArtifactRDto> SetDocumentContentAsync(long documentId, string fileName, string mimeType, bool overwriteIfExists, Stream contentStream)
+        {
+            var document = _documentsCache.First(x => x.Id == documentId);
+            var artifacts = new List<ArtifactRDto>(document.Artifacts);
+            _currentArtId++;
+            var artifact = new ArtifactRDto()
+            {
+                Length = contentStream.Length,
+                Type = ArtifactTypeEnum.Output,
+                MimeType = mimeType,
+                Extension = _fileSystem.Path.GetExtension(fileName),
+                Id = _currentArtId
+            };
+            artifacts.Add(artifact);
+            document.Artifacts = artifacts;
+            return artifact;
+        }
+
+        public void Open(string userName, string userPassword, string magicKey)
+        {
+            IsOpen = true;
+        }
+
+        public void Close()
+        {
+            IsOpen = false;
+        }
+
+        public bool Ping(out TimeSpan duration, out string status)
+        {
+            duration = TimeSpan.FromMilliseconds(_rand.Next(2000));
+            status = "OK";
+            return true;
+        }
+        #endregion
+
+        #region *** Private Operations & Overrides ***
+        protected override void OnDisposing()
+        {
+            _configuration = null;
+        }
+
+        protected void GenerateDocuments(DocumentStatusEnum state)
+        {
+            for (int a = 0; a < _configuration.System.Proxy.App.DefaultDocumentsCount; a++)
+            {
+                var artifacts = new List<ArtifactRDto>();
+                for (int i = 0; i < _configuration.System.Proxy.App.DefaultArtifactCount; i++)
+                {
+                    var ext = _configuration.System.Proxy.App.Extension.GetRandom();
+                    var artifact = new ArtifactRDto()
+                    {
+                        Id = _currentArtId++,
+                        Name = $"{_configuration.System.Proxy.App.NameProfix.GetRandom()}_{_currentArtId:D8}",
+                        Extension = ext,
+                        MimeType = MimeTypeUtil.GetMimeType(ext),
+                        Type = ArtifactTypeEnum.Input
+                    };
+                    artifacts.Add(artifact);
+                }
+
+                var document = new DocumentRDto()
+                {
+                    Name = $"Doc{_currentDocId++:D8}",
+                    Id = _currentDocId,
+                    PresetCreditCost = _configuration.System.Proxy.App.PresetCreditCost.GetRandom(),
+                    PresetQuality = _configuration.System.Proxy.App.PresetQuality.GetRandom(),
+                    PresetProcessProfile = _configuration.System.Proxy.App.PresetProcessProfile.GetRandom(),
+                    PresetWatermark = _configuration.System.Proxy.App.PresetWatermark.GetRandom(),
+                    PresetTest = _configuration.System.Proxy.App.PresetTest.GetRandom(),
+                    Status = state,
+                    Artifacts = artifacts
+                };
+                _documentsCache.Add(document);
+            }
+
+            var toDelete = _documentsCache.Where(x =>
+                x.Status == DocumentStatusEnum.DoneFail || x.Status == DocumentStatusEnum.DoneOk ||
+                x.Status == DocumentStatusEnum.Downloaded || x.Status == DocumentStatusEnum.Expired).ToArray();
+            foreach (var del in toDelete)
+                _documentsCache.Remove(del);
+
+        }
+
+        #endregion
+    }
+}

+ 99 - 0
ProcessServer/Business/Connector.cs

@@ -0,0 +1,99 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Abstractions;
+using System.Net;
+using System.Threading.Tasks;
+using BO.AppServer.Metadata.Dto;
+using BO.AppServer.Metadata.Enums;
+using BO.ProcessServer.Business.Enums;
+using BO.ProcessServer.Options;
+using Connector.PS;
+using Quadarax.Foundation.Core.Object;
+
+namespace BO.ProcessServer.Business
+{
+    internal class Connector : DisposableObject, IConnectionPS
+    {
+
+        #region *** Private fields ***
+        private IConnectionPS _connection;
+        #endregion
+
+        #region *** Properties ***
+        public bool IsOpen => _connection?.IsOpen ?? false;
+        #endregion
+
+        #region *** Constructor ***
+        public Connector(Configuration configuration, IFileSystem fileSystem)
+        {
+            if (configuration.System.Mode == AppModeEnum.Proxy || configuration.System.Mode == AppModeEnum.ProxyApp)
+                _connection = new ConnectionProxy(configuration, fileSystem);
+            else
+                _connection = new Connection(configuration.ApiBaseUrl, configuration.ApiTimeout,
+                    configuration.System.Identification, IPAddress.Parse(configuration.System.PreferredInterface),
+                    configuration.System.PoolPullDocuments);
+        }
+        #endregion
+
+        #region *** Public Operations ***
+        public async Task<IEnumerable<DocumentRDto>> GetDocumentsAsync(DocumentsScopeEnums scope)
+        {
+            return await _connection?.GetDocumentsAsync(scope)!;
+        }
+
+        public async Task<DocumentStatusEnum> GetDocumentStateAsync(long documentId)
+        {
+            return await _connection?.GetDocumentStateAsync(documentId)!;
+        }
+
+        public async Task<DocumentRDto> GetDocumentInfoAsync(long documentId)
+        {
+            return await _connection?.GetDocumentInfoAsync(documentId)!;
+        }
+
+        public async Task<DocumentStatusEnum> SetDocumentStateAsync(long documentId, DocumentStatusEnum status)
+        {
+            return await _connection?.SetDocumentStateAsync(documentId, status)!;
+        }
+
+        public async Task<Stream> GetDocumentContentAsync(long documentId, long artifactId)
+        {
+            return await _connection?.GetDocumentContentAsync(documentId, artifactId)!;
+        }
+
+        public async Task<ArtifactRDto> SetDocumentContentAsync(long documentId, string fileName, string mimeType, bool overwriteIfExists,
+            Stream contentStream)
+        {
+            return await _connection?.SetDocumentContentAsync(documentId, fileName, mimeType, overwriteIfExists, contentStream)!;
+        }
+
+        public void Open(string userName, string userPassword, string magicKey)
+        {
+            _connection?.Open(userName, userPassword, magicKey);
+        }
+
+        public void Close()
+        {
+            _connection?.Close();
+        }
+
+        public bool Ping(out TimeSpan duration, out string status)
+        {
+            return _connection.Ping(out duration,out status);
+        }
+
+        #endregion
+
+        #region *** Private Operations & Overrides ***
+        protected override void OnDisposing()
+        {
+            _connection?.Dispose();
+            _connection = null;
+        }
+
+        #endregion
+        
+        
+    }
+}

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

@@ -5,6 +5,14 @@
         /// <summary>
         /// Works in simultaneous mode (documents are not processing and randomly generates output OK or Fail). Any call for processing documents enabled.
         /// </summary>
+        ProxyProcess,
+        /// <summary>
+        /// Works in simultaneous mode (documents are processing but call to application server returns randomly generated output). Any call for processing documents is disabled.
+        /// </summary>
+        ProxyApp,
+        /// <summary>
+        /// Combination of <see cref="ProxyProcess"/> and <see cref="ProxyApp"/>. Self test mode.
+        /// </summary>
         Proxy,
         /// <summary>
         /// Works in production mode

+ 9 - 0
ProcessServer/Business/Pool.cs

@@ -80,6 +80,15 @@ namespace BO.ProcessServer.Business
             return fullFileName;
         }
 
+        public async Task<IEnumerable<string>> GetSinglePendingDocumentFilesAsync()
+        {
+            var first = GetPoolBranch(PoolTypeEnum.Pending).EnumerateFiles(CS_ALL_FILES, SearchOption.TopDirectoryOnly).FirstOrDefault();
+            if (first == null) return new List<string>();
+
+            var documentId = GetDocumentIdFromFileName(first.Name);
+            return GetDocumentFiles(PoolTypeEnum.Pending, documentId);
+        }
+
         public long GetDocumentIdFromFileName(string fileName)
         {
             var fileNameOnly = _fileSystem.Path.GetFileName(fileName);

+ 15 - 9
ProcessServer/Business/Server.cs

@@ -23,12 +23,13 @@ namespace BO.ProcessServer.Business
         private IFileSystem _fileSystem;
         private IList<ServerProcess> _processes;
         protected ILog Log { get; }
+        protected ILog LogLifecycle { get; }
         protected Statistics Statistics { get; }
         private Pool _pool;
         private PullDocumentsWorker _wkPull;
         private RetentionWorker _wkRetention;
         private ProcessingDocumentsWorker _wkProcessing;
-        private Connection _connection;
+        private IConnectionPS _connection;
         #endregion
 
         #region *** Public properties ***
@@ -40,22 +41,25 @@ namespace BO.ProcessServer.Business
         {
             _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));
+            LogLifecycle = logger.GetLogger("Lifecycle");
             Statistics = new Statistics();
 
+            ValidateConfiguration();
+            
             _processes = new List<ServerProcess>();
 
-            _connection = new Connection(_configuration.ApiBaseUrl, _configuration.ApiTimeout,
-                _configuration.System.Identification, IPAddress.Parse(_configuration.System.PreferredInterface),
-                _configuration.System.PoolPullDocuments);
+            _connection = new Connector(_configuration, _fileSystem);
 
             _pool = new Pool(_configuration, _fileSystem, logger);
 
-            _wkPull = new PullDocumentsWorker(_connection, _configuration.System.PoolPullInterval, fileSystem, Statistics, _pool, 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);
+            _wkProcessing = new ProcessingDocumentsWorker(_configuration, _processes, _connection, _fileSystem, Statistics, _pool, logger);
         }
         #endregion
 
@@ -69,7 +73,6 @@ namespace BO.ProcessServer.Business
             }
 
             LogDebug($"Starting instance '{_configuration.System.Identification}' ...");
-            ValidateConfiguration();
 
             _wkPull.Start();
             _wkRetention.Start();
@@ -78,6 +81,7 @@ namespace BO.ProcessServer.Business
             Statistics.GetCounter<TimespanCounter>(Statistics.CNT_PS_ONLINE_DUR).Start();
             IsRunning = true;
             LogInfo( $"Instance '{_configuration.System.Identification}' STARTED.");
+            LogLifecycle.Log(LogSeverityEnum.Info, $"Instance '{_configuration.System.Identification}' STARTED.");
         }
 
 
@@ -95,6 +99,8 @@ namespace BO.ProcessServer.Business
             Statistics.GetCounter<TimespanCounter>(Statistics.CNT_PS_ONLINE_DUR).Stop();
             IsRunning = false;
             LogInfo( $"Instance '{_configuration.System.Identification}' STOPPED.");
+            LogLifecycle.Log(LogSeverityEnum.Info, $"Instance '{_configuration.System.Identification}' STOPPED.");
+            LogLifecycle.Log(LogSeverityEnum.Info, Statistics.ToString());
         }
         #endregion
 
@@ -153,9 +159,9 @@ namespace BO.ProcessServer.Business
             var host = Dns.GetHostEntry(Dns.GetHostName());
             if (_configuration.System.PreferredInterface.Trim().ToLower() == "auto")
             {
-                _configuration.System.PreferredInterface = host.AddressList.FirstOrDefault(x =>
+                _configuration.System.PreferredInterface = host.AddressList.Where(x =>
                         x.AddressFamily == AddressFamily.InterNetwork ||
-                        x.AddressFamily == AddressFamily.InterNetworkV6)
+                        x.AddressFamily == AddressFamily.InterNetworkV6).OrderBy(x=>x.AddressFamily).FirstOrDefault()
                     ?.ToString();
             }
 

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

@@ -52,6 +52,11 @@ namespace BO.ProcessServer.Business.StatisticCounters
             CheckExistenceCounter(name);
 
         }
+        public override string ToString()
+        {
+            return base.ToString();
+        }
+
         #endregion
 
         #region *** Private Operations ***

+ 2 - 2
ProcessServer/Business/Workers/Contexts/ProcessingDocumentsCtx.cs

@@ -11,11 +11,11 @@ namespace BO.ProcessServer.Business.Workers.Contexts
     {
 
         public IList<ServerProcess> Processes { get; }
-        public Connection Connection { get; set; }
+        public IConnectionPS 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)
+        public ProcessingDocumentsCtx(Configuration configuration,IList<ServerProcess> processes,IConnectionPS 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));

+ 2 - 3
ProcessServer/Business/Workers/Contexts/PullCtx.cs

@@ -3,17 +3,16 @@ 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 IConnectionPS 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)
+        public PullCtx(IConnectionPS connection, IFileSystem fileSystem, Statistics statistics, Pool pool) : base(statistics, pool)
         {
             Connection = connection ?? throw new ArgumentNullException(nameof(connection));
             FileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));

+ 1 - 1
ProcessServer/Business/Workers/ProcessingDocumentsWorker.cs

@@ -13,7 +13,7 @@ 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) 
+        public ProcessingDocumentsWorker(Configuration configuration,IList<ServerProcess> processes, IConnectionPS connection, IFileSystem fileSystem,Statistics statistics, Pool pool, ILogger logger) 
             : base(CS_WK_NAME, new ProcessingDocumentsCtx(configuration, processes, connection, fileSystem, statistics, pool), logger)
         {
         }

+ 46 - 1
ProcessServer/Business/Workers/PullDocumentsWorker.cs

@@ -1,5 +1,9 @@
 using System;
+using System.Collections.Generic;
 using System.IO.Abstractions;
+using System.Linq;
+using System.Threading.Tasks;
+using BO.AppServer.Metadata.Enums;
 using BO.ProcessServer.Business.StatisticCounters;
 using BO.ProcessServer.Business.Workers.Contexts;
 using Connector.PS;
@@ -10,8 +14,12 @@ namespace BO.ProcessServer.Business.Workers
 {
     internal class PullDocumentsWorker : LoopWorker
     {
+        #region *** Constants ***
         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)
+        #endregion
+
+        #region *** Constructors ***
+        public PullDocumentsWorker(IConnectionPS connection, TimeSpan loopEveryTimeSpan, IFileSystem fileSystem, Statistics statistics,Pool pool, ILogger logger) : base(CS_WK_NAME, new PullCtx(connection, fileSystem, statistics, pool), logger)
         {
             IterationsDelayBetween = loopEveryTimeSpan;
         }
@@ -23,5 +31,42 @@ namespace BO.ProcessServer.Business.Workers
         public PullDocumentsWorker(string workerName, IWorkerContext context, ILogger logger = null) : base(workerName, context, logger)
         {
         }
+        #endregion
+
+        #region *** Iteration ***
+
+        protected override async Task DoIteration(IWorkerContext context)
+        {
+            var ctx = (PullCtx)context;
+
+            var documents = await ctx.Connection.GetDocumentsAsync(DocumentsScopeEnums.Pending);
+            foreach (var document in documents)
+            {
+                var downloadedFiles = new List<string>();
+                foreach (var art in document.Artifacts.Where(x=>x.Type == ArtifactTypeEnum.Input))
+                {
+                    await using (var content = await ctx.Connection.GetDocumentContentAsync(document.Id, art.Id))
+                    {
+                        var fileName = ctx.FileSystem.Path.ChangeExtension(art.Name, art.Extension);
+                        fileName = await ctx.Pool.PutDocumentAsync(document.Id, fileName, content);
+                        downloadedFiles.Add(fileName);
+                        Log(LogSeverityEnum.Info, $"Document '{document.Name}' [{document.Id}] file '{fileName}' downloaded. (size {content.Length} bytes)");
+                    }
+                }
+
+                if (!downloadedFiles.All(x => ctx.FileSystem.File.Exists(x)))
+                {
+                    // not all  downloaded -> rollback
+                    ctx.Pool.DeleteDocumentFiles(Pool.PoolTypeEnum.Pending, document.Id);
+                    await ctx.Connection.SetDocumentStateAsync(document.Id, DocumentStatusEnum.New);
+                    Log(LogSeverityEnum.Warn, $"Document '{document.Name}' [{document.Id}] roll-backed.");
+                }
+            }
+        }
+
+        #endregion
+
+        #region *** Private Operations ***
+        #endregion
     }
 }

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

@@ -6,6 +6,7 @@ using System.Text.Json;
 using BO.ProcessServer.Options;
 using Quadarax.Foundation.Core.Console;
 using Quadarax.Foundation.Core.Data.Interface.Entity;
+using Quadarax.Foundation.Core.Json;
 using Quadarax.Foundation.Core.Logging;
 using Quadarax.Foundation.Core.Object.Extensions;
 using Quadarax.Foundation.Core.QConsole;
@@ -45,7 +46,8 @@ namespace BO.ProcessServer.Commands.Base
         protected override void OnInitialize()
         {
             base.OnInitialize();
-            Configuration = JsonSerializer.Deserialize<Configuration>(FileSystem.File.ReadAllText(Constants.CS_CONFIGURATION_FILE));
+            var binder = new Binder(FileSystem);
+            Configuration = binder.Load<Configuration>(Constants.CS_CONFIGURATION_FILE);
         }
 
         protected override void OnValidateArguments()

+ 18 - 0
ProcessServer/Commands/RunCommand.cs

@@ -3,11 +3,13 @@ using BO.ProcessServer.Business;
 using BO.ProcessServer.Commands.Base;
 using Quadarax.Foundation.Core.NLog;
 using Quadarax.Foundation.Core.QConsole;
+using Quadarax.Foundation.Core.QConsole.Attributes;
 using Quadarax.Foundation.Core.Value;
 // ReSharper disable InconsistentNaming
 
 namespace BO.ProcessServer.Commands
 {
+    [CommandDefinition]
     internal class RunCommand : BaseLocalCommand
     {
         #region *** Constants ***
@@ -34,6 +36,22 @@ namespace BO.ProcessServer.Commands
 
         protected override Result OnExecute()
         {
+            using (var server = new Server(Configuration, FileSystem, LoggerFactory.Instance))
+            {
+                WriteInfo("Starting...");
+                server.Start();
+                WriteInfo("Started.");
+                WriteCaption("Press Esc to exit.");
+                var exit = false;
+                while (!exit)
+                {
+                    var keyInfo = Console.ReadKey();
+                    exit = keyInfo.Key == ConsoleKey.Escape;
+                }
+                server.Stop();
+            }
+
+
             return new Result();
         }
         #endregion

+ 29 - 2
ProcessServer/Options/Configuration.cs

@@ -33,6 +33,7 @@ namespace BO.ProcessServer.Options
             [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; }
+            [JsonPropertyName("proxy")] public CfgProxies Proxy { get; set; }
         }
 
         public class CfgScenarioAcceptFilter
@@ -56,7 +57,7 @@ namespace BO.ProcessServer.Options
         public class CfgStatusMonitoring
         {
             [JsonPropertyName("enabled")] public bool Enabled { get; set; }
-            [JsonPropertyName("interval")] public string IntervalRaw { get; set; }
+            [JsonPropertyName("monitoring-interval")] public string IntervalRaw { get; set; }
             public TimeSpan Interval => TimeSpan.Parse(IntervalRaw);
             [JsonPropertyName("file")] public string File { get; set; }
         }
@@ -64,11 +65,37 @@ namespace BO.ProcessServer.Options
         public class CfgPoolRetention
         {
             [JsonPropertyName("enabled")] public bool Enabled { get; set; }
-            [JsonPropertyName("interval")] public string IntervalRaw { get; set; }
+            [JsonPropertyName("retention-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; }
         }
+
+        public class CfgProxies
+        {
+            [JsonPropertyName("process")] public CfgProxiesProcess Process { get; set; }
+            [JsonPropertyName("app")] public CfgProxiesApp App { get; set; }
+        }
+
+        public class CfgProxiesProcess
+        {
+            [JsonPropertyName("succ")] public IEnumerable<bool> Succ { get; set; }
+            [JsonPropertyName("output")]public IEnumerable<string> Output { get; set; }
+        }
+
+        public class CfgProxiesApp
+        {
+            [JsonPropertyName("default-documents-count")] public int DefaultDocumentsCount { get; set; }
+            [JsonPropertyName("max-documents-count")] public int MaxDocumentsCount { get; set; }
+            [JsonPropertyName("default-artifacts-count")] public int DefaultArtifactCount { get; set; }
+            [JsonPropertyName("extension")] public IEnumerable<string> Extension { get; set; }
+            [JsonPropertyName("nameprefix")] public IEnumerable<string> NameProfix { get; set; }
+            [JsonPropertyName("presetcreditcost")] public IEnumerable<int> PresetCreditCost { get; set; }
+            [JsonPropertyName("presetwatermark")] public IEnumerable<bool> PresetWatermark { get; set; }
+            [JsonPropertyName("presetquality")] public IEnumerable<int> PresetQuality { get; set; }
+            [JsonPropertyName("presetprocessprofile")] public IEnumerable<string> PresetProcessProfile { get; set; }
+            [JsonPropertyName("presettest")] public IEnumerable<bool> PresetTest { get; set; }
+        }
     }
 
 }

+ 8 - 0
ProcessServer/Properties/launchSettings.json

@@ -0,0 +1,8 @@
+{
+  "profiles": {
+    "ProcessServer": {
+      "commandName": "Project",
+      "commandLineArgs": "run"
+    }
+  }
+}

+ 16 - 1
ProcessServer/processServer-nlog.config

@@ -37,6 +37,20 @@
       keepFileOpen="true"
       encoding="utf-8"
       writeBom="true" />
+    <target xsi:type="File" 
+            name="OutToFileLifecycle" 
+            layout="${date:format=yyyy-MM-dd HH\:mm\:ss.fff}|${level:uppercase=true}|${threadid}|${message}|${logger}|${all-event-properties}" 
+            fileName="${logDirectory}\${appName}-Lifecycle.log"
+            archiveFileName="${logDirectory}\${appName}-Lifecycle.{#}.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}" />
@@ -46,6 +60,7 @@
   <rules>
     <logger name="*" writeTo="OutToConsole" />
     <logger name="*" writeTo="OutToFile" />
-    <logger name="*" level="Error" writeTo="OutToFileException" />    
+    <logger name="*" level="Error" writeTo="OutToFileException" />
+    <logger name="Lifecycle" writeTo="OutToFileLifecycle" /> 
   </rules>
 </nlog>

+ 55 - 5
ProcessServer/processServer.json

@@ -5,7 +5,7 @@
 	"api-magic": "SHARE",
 	"api-timeout": "00:10:00",
 	"System": {
-		"mode": "proxy",
+		"mode": "Proxy",
 		"identification": "DevNode#1",
 		"preferred-interface": "auto",
 		"pool-pull-documents": 4,
@@ -17,17 +17,17 @@
 		"pool-path-fail": ".fail",
 		"pool-retention": {
 			"enabled": true,
-			"interval": "24:00:00",
+			"retention-": "24:00:00",
 			"size": 1000000000,
 			"items": 100
 		},
 		"status-monitoring": {
 			"enabled": true,
 			"file": "{CD}\\status.csv",
-			"interval": "00:00:10"
+			"monitoring-interval": "00:00:10"
 		},
 
-		"scenario-base": "{CD}\\Scenarios",
+		"scenario-path-base": "{CD}\\Scenarios",
     "scenario-parallels": 2,
 		"Scenarios": [
 			{
@@ -44,6 +44,56 @@
 				"exitcode-succ": 0,
 				"exitcode-fail": 1
 			}
-		]
+		],
+		"Proxy" : {
+			"Process": {
+				"Succ": [
+					true,
+					false
+				],
+        "Output": [
+          "result.txt"
+        ]
+			},
+			"App": {
+				"default-documents-count": 4,
+        "max-documents-count": 20,
+				"default-artifacts-count": 2,
+				"Extension": [
+					".cad",
+					".txt",
+					".catia"
+				],
+				"NamePrefix": [
+					"object",
+					"wing",
+					"holo"
+				],
+				"PresetCreditCost": [
+					1,
+					2,
+					3
+				],
+				"PresetWatermark": [
+					true,
+					false
+				],
+				"PresetQuality": [
+					0,
+					1,
+					2,
+					3
+				],
+				"PresetProcessProfile": [
+					"TEST",
+					"HIRES",
+					"LOWRES"
+				],
+				"PresetTest": [
+					true,
+					false
+				]
+			} 
+    } 
 	}
 }