diff --git a/Azaion.Annotator/Annotator.xaml.cs b/Azaion.Annotator/Annotator.xaml.cs index 9ba1775..3b4b39b 100644 --- a/Azaion.Annotator/Annotator.xaml.cs +++ b/Azaion.Annotator/Annotator.xaml.cs @@ -261,11 +261,11 @@ public partial class Annotator _appConfig.UIConfig.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value; _appConfig.UIConfig.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value; - await ThrottleExt.ThrottleRunFirst(() => + await ThrottleExt.Throttle(() => { _configUpdater.Save(_appConfig); return Task.CompletedTask; - }, SaveConfigTaskId, TimeSpan.FromSeconds(5)); + }, TimeSpan.FromSeconds(5)); } private void ShowTimeAnnotations(TimeSpan time) diff --git a/Azaion.Annotator/AnnotatorEventHandler.cs b/Azaion.Annotator/AnnotatorEventHandler.cs index 28deefa..e0961e8 100644 --- a/Azaion.Annotator/AnnotatorEventHandler.cs +++ b/Azaion.Annotator/AnnotatorEventHandler.cs @@ -1,15 +1,13 @@ using System.IO; -using System.Reflection.Metadata; using System.Windows; using System.Windows.Input; using Azaion.Annotator.DTO; using Azaion.Common; using Azaion.Common.DTO; -using Azaion.Common.DTO.Config; -using Azaion.Common.DTO.Queue; using Azaion.Common.Events; using Azaion.Common.Extensions; using Azaion.Common.Services; +using Azaion.CommonSecurity.DTO; using LibVLCSharp.Shared; using MediatR; using Microsoft.Extensions.Logging; diff --git a/Azaion.Common/Constants.cs b/Azaion.Common/Constants.cs index 1237b9a..917f3b1 100644 --- a/Azaion.Common/Constants.cs +++ b/Azaion.Common/Constants.cs @@ -1,5 +1,9 @@ using System.Windows; +using System.Windows.Media; +using Azaion.Common.Database; using Azaion.Common.DTO; +using Azaion.Common.DTO.Config; +using Azaion.Common.Extensions; namespace Azaion.Common; @@ -21,20 +25,32 @@ public class Constants #region AnnotatorConfig + public static readonly AnnotationConfig DefaultAnnotationConfig = new() + { + DetectionClasses = DefaultAnnotationClasses, + VideoFormats = DefaultVideoFormats, + ImageFormats = DefaultImageFormats, + AnnotationsDbFile = DEFAULT_ANNOTATIONS_DB_FILE + }; + public static readonly List DefaultAnnotationClasses = [ - new() { Id = 0, Name = "Броньована техніка", ShortName = "Бронь" }, - new() { Id = 1, Name = "Вантажівка", ShortName = "Вантаж" }, - new() { Id = 2, Name = "Машина легкова", ShortName = "Машина" }, - new() { Id = 3, Name = "Артилерія", ShortName = "Арта" }, - new() { Id = 4, Name = "Тінь від техніки", ShortName = "Тінь" }, - new() { Id = 5, Name = "Окопи", ShortName = "Окопи" }, - new() { Id = 6, Name = "Військовий", ShortName = "Військов" }, - new() { Id = 7, Name = "Накати", ShortName = "Накати" }, - new() { Id = 8, Name = "Танк з захистом", ShortName = "Танк захист" }, - new() { Id = 9, Name = "Дим", ShortName = "Дим" }, - new() { Id = 10, Name = "Літак", ShortName = "Літак" }, - new() { Id = 11, Name = "Мотоцикл", ShortName = "Мото" } + new() { Id = 0, Name = "ArmorVehicle", ShortName = "Броня", Color = "#FF0000".ToColor() }, + new() { Id = 1, Name = "Truck", ShortName = "Вантаж.", Color = "#00FF00".ToColor() }, + new() { Id = 2, Name = "Vehicle", ShortName = "Машина", Color = "#0000FF".ToColor() }, + new() { Id = 3, Name = "Atillery", ShortName = "Арта", Color = "#FFFF00".ToColor() }, + new() { Id = 4, Name = "Shadow", ShortName = "Тінь", Color = "#FF00FF".ToColor() }, + new() { Id = 5, Name = "Trenches", ShortName = "Окопи", Color = "#00FFFF".ToColor() }, + new() { Id = 6, Name = "MilitaryMan", ShortName = "Військов", Color = "#188021".ToColor() }, + new() { Id = 7, Name = "TyreTracks", ShortName = "Накати", Color = "#800000".ToColor() }, + new() { Id = 8, Name = "AdditArmoredTank", ShortName = "Танк.захист", Color = "#008000".ToColor() }, + new() { Id = 9, Name = "Smoke", ShortName = "Дим", Color = "#000080".ToColor() }, + new() { Id = 10, Name = "Plane", ShortName = "Літак", Color = "#000080".ToColor() }, + new() { Id = 11, Name = "Moto", ShortName = "Мото", Color = "#808000".ToColor() }, + new() { Id = 12, Name = "CamouflageNet", ShortName = "Сітка", Color = "#800080".ToColor() }, + new() { Id = 13, Name = "CamouflageBranches", ShortName = "Гілки", Color = "#2f4f4f".ToColor() }, + new() { Id = 14, Name = "Roof", ShortName = "Дах", Color = "#1e90ff".ToColor() }, + new() { Id = 15, Name = "Building", ShortName = "Будівля", Color = "#ffb6c1".ToColor() } ]; public static readonly List DefaultVideoFormats = ["mp4", "mov", "avi"]; @@ -49,6 +65,15 @@ public class Constants # region AIRecognitionConfig + public static readonly AIRecognitionConfig DefaultAIRecognitionConfig = new() + { + FrameRecognitionSeconds = DEFAULT_FRAME_RECOGNITION_SECONDS, + TrackingDistanceConfidence = TRACKING_DISTANCE_CONFIDENCE, + TrackingProbabilityIncrease = TRACKING_PROBABILITY_INCREASE, + TrackingIntersectionThreshold = TRACKING_INTERSECTION_THRESHOLD, + FramePeriodRecognition = DEFAULT_FRAME_PERIOD_RECOGNITION + }; + public const double DEFAULT_FRAME_RECOGNITION_SECONDS = 2; public const double TRACKING_DISTANCE_CONFIDENCE = 0.15; public const double TRACKING_PROBABILITY_INCREASE = 15; @@ -60,6 +85,12 @@ public class Constants #region Thumbnails + public static readonly ThumbnailConfig DefaultThumbnailConfig = new() + { + Size = DefaultThumbnailSize, + Border = DEFAULT_THUMBNAIL_BORDER + }; + public static readonly Size DefaultThumbnailSize = new(240, 135); public const int DEFAULT_THUMBNAIL_BORDER = 10; diff --git a/Azaion.Common/DTO/Config/AppConfig.cs b/Azaion.Common/DTO/Config/AppConfig.cs index 701170e..b03f988 100644 --- a/Azaion.Common/DTO/Config/AppConfig.cs +++ b/Azaion.Common/DTO/Config/AppConfig.cs @@ -45,14 +45,7 @@ public class ConfigUpdater : IConfigUpdater var appConfig = new AppConfig { - AnnotationConfig = new AnnotationConfig - { - DetectionClasses = Constants.DefaultAnnotationClasses, - VideoFormats = Constants.DefaultVideoFormats, - ImageFormats = Constants.DefaultImageFormats, - - AnnotationsDbFile = Constants.DEFAULT_ANNOTATIONS_DB_FILE - }, + AnnotationConfig = Constants.DefaultAnnotationConfig, UIConfig = new UIConfig { @@ -72,20 +65,8 @@ public class ConfigUpdater : IConfigUpdater GpsRouteDirectory = Constants.DEFAULT_GPS_ROUTE_DIRECTORY }, - ThumbnailConfig = new ThumbnailConfig - { - Size = Constants.DefaultThumbnailSize, - Border = Constants.DEFAULT_THUMBNAIL_BORDER - }, - - AIRecognitionConfig = new AIRecognitionConfig - { - FrameRecognitionSeconds = Constants.DEFAULT_FRAME_RECOGNITION_SECONDS, - TrackingDistanceConfidence = Constants.TRACKING_DISTANCE_CONFIDENCE, - TrackingProbabilityIncrease = Constants.TRACKING_PROBABILITY_INCREASE, - TrackingIntersectionThreshold = Constants.TRACKING_INTERSECTION_THRESHOLD, - FramePeriodRecognition = Constants.DEFAULT_FRAME_PERIOD_RECOGNITION - } + ThumbnailConfig = Constants.DefaultThumbnailConfig, + AIRecognitionConfig = Constants.DefaultAIRecognitionConfig }; Save(appConfig); } diff --git a/Azaion.Common/Extensions/ColorExtensions.cs b/Azaion.Common/Extensions/ColorExtensions.cs index 30839c9..76e56d6 100644 --- a/Azaion.Common/Extensions/ColorExtensions.cs +++ b/Azaion.Common/Extensions/ColorExtensions.cs @@ -12,4 +12,7 @@ public static class ColorExtensions color.A = (byte)(MIN_ALPHA + (int)Math.Round(confidence * (MAX_ALPHA - MIN_ALPHA))); return color; } + + public static Color ToColor(this string hexColor) => + (Color)ColorConverter.ConvertFromString(hexColor); } \ No newline at end of file diff --git a/Azaion.Common/Extensions/ThrottleExtensions.cs b/Azaion.Common/Extensions/ThrottleExtensions.cs index 72cd29e..b6c12e7 100644 --- a/Azaion.Common/Extensions/ThrottleExtensions.cs +++ b/Azaion.Common/Extensions/ThrottleExtensions.cs @@ -1,57 +1,22 @@ -using System.Collections.Concurrent; - -namespace Azaion.Common.Extensions; +namespace Azaion.Common.Extensions; public static class ThrottleExt { - private static ConcurrentDictionary _taskStates = new(); + private static readonly Dictionary LastExecution = new(); + private static readonly object Lock = new(); - public static async Task ThrottleRunFirst(Func func, Guid actionId, TimeSpan? throttleTime = null, CancellationToken cancellationToken = default) + public static async Task Throttle(this Func func, TimeSpan interval, CancellationToken ct = default) { - if (_taskStates.ContainsKey(actionId) && _taskStates[actionId]) - return; + ArgumentNullException.ThrowIfNull(func); - _taskStates[actionId] = true; - try + lock (Lock) { - await func(); + if (LastExecution.ContainsKey(func) && DateTime.UtcNow - LastExecution[func] < interval) + return; + + func(); + LastExecution[func] = DateTime.UtcNow; } - catch (Exception e) - { - Console.WriteLine(e); - } - - _ = Task.Run(async () => - { - await Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500), cancellationToken); - _taskStates[actionId] = false; - }, cancellationToken); - } - - public static async Task ThrottleRunAfter(Func func, Guid actionId, TimeSpan? throttleTime = null, CancellationToken cancellationToken = default) - { - if (_taskStates.ContainsKey(actionId) && _taskStates[actionId]) - return; - - _taskStates[actionId] = true; - _ = Task.Run(async () => - { - try - { - await Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500), cancellationToken); - await func(); - } - catch (Exception) - { - _taskStates[actionId] = false; - } - finally - { - _taskStates[actionId] = false; - } - - }, cancellationToken); - await Task.CompletedTask; } } \ No newline at end of file diff --git a/Azaion.Common/Services/AnnotationService.cs b/Azaion.Common/Services/AnnotationService.cs index 30bad22..0f45792 100644 --- a/Azaion.Common/Services/AnnotationService.cs +++ b/Azaion.Common/Services/AnnotationService.cs @@ -26,15 +26,13 @@ public class AnnotationService : INotificationHandler private readonly FailsafeAnnotationsProducer _producer; private readonly IGalleryService _galleryService; private readonly IMediator _mediator; - private readonly IHardwareService _hardwareService; - private readonly IAuthProvider _authProvider; + private readonly IAzaionApi _api; private readonly QueueConfig _queueConfig; private Consumer _consumer = null!; private readonly UIConfig _uiConfig; private static readonly Guid SaveTaskId = Guid.NewGuid(); public AnnotationService( - IResourceLoader resourceLoader, IDbFactory dbFactory, FailsafeAnnotationsProducer producer, IOptions queueConfig, @@ -42,14 +40,13 @@ public class AnnotationService : INotificationHandler IGalleryService galleryService, IMediator mediator, IHardwareService hardwareService, - IAuthProvider authProvider) + IAzaionApi api) { _dbFactory = dbFactory; _producer = producer; _galleryService = galleryService; _mediator = mediator; - _hardwareService = hardwareService; - _authProvider = authProvider; + _api = api; _queueConfig = queueConfig.Value; _uiConfig = uiConfig.Value; @@ -58,7 +55,7 @@ public class AnnotationService : INotificationHandler private async Task Init(CancellationToken cancellationToken = default) { - if (!_authProvider.CurrentUser.Role.IsValidator()) + if (!_api.CurrentUser.Role.IsValidator()) return; var consumerSystem = await StreamSystem.Create(new StreamSystemConfig @@ -68,13 +65,11 @@ public class AnnotationService : INotificationHandler Password = _queueConfig.ConsumerPassword }); - var offset = (await _dbFactory.Run(db => db.QueueOffsets.FirstOrDefaultAsync( - x => x.QueueName == Constants.MQ_ANNOTATIONS_QUEUE, token: cancellationToken)) - )?.Offset ?? 0; + var offset = (ulong)(_api.CurrentUser.UserConfig?.QueueConfig?.AnnotationsOffset ?? 0); _consumer = await Consumer.Create(new ConsumerConfig(consumerSystem, Constants.MQ_ANNOTATIONS_QUEUE) { - Reference = _hardwareService.GetHardware().Hash, + Reference = _api.CurrentUser.Email, OffsetSpec = new OffsetTypeOffset(offset + 1), MessageHandler = async (_, _, context, message) => { @@ -84,13 +79,13 @@ public class AnnotationService : INotificationHandler .Set(x => x.Offset, context.Offset) .UpdateAsync(token: cancellationToken)); - await ThrottleExt.ThrottleRunAfter(() => + await ThrottleExt.Throttle(() => { _dbFactory.SaveToDisk(); return Task.CompletedTask; - }, SaveTaskId, TimeSpan.FromSeconds(5), cancellationToken); + }, TimeSpan.FromSeconds(10), cancellationToken); - if (msg.CreatedEmail == _authProvider.CurrentUser.Email) //Don't process messages by yourself + if (msg.CreatedEmail == _api.CurrentUser.Email) //Don't process messages by yourself return; await SaveAnnotationInner( @@ -114,18 +109,18 @@ public class AnnotationService : INotificationHandler { a.Time = TimeSpan.FromMilliseconds(a.Milliseconds); return await SaveAnnotationInner(DateTime.Now, a.OriginalMediaName, a.Time, a.Detections.ToList(), - SourceEnum.AI, new MemoryStream(a.Image), _authProvider.CurrentUser.Role, _authProvider.CurrentUser.Email, generateThumbnail: true, token: ct); + SourceEnum.AI, new MemoryStream(a.Image), _api.CurrentUser.Role, _api.CurrentUser.Email, generateThumbnail: true, token: ct); } //Manual public async Task SaveAnnotation(string originalMediaName, TimeSpan time, List detections, Stream? stream = null, CancellationToken token = default) => await SaveAnnotationInner(DateTime.UtcNow, originalMediaName, time, detections, SourceEnum.Manual, stream, - _authProvider.CurrentUser.Role, _authProvider.CurrentUser.Email, generateThumbnail: true, token: token); + _api.CurrentUser.Role, _api.CurrentUser.Email, generateThumbnail: true, token: token); //Manual Validate existing public async Task ValidateAnnotation(Annotation annotation, CancellationToken token = default) => await SaveAnnotationInner(DateTime.UtcNow, annotation.OriginalMediaName, annotation.Time, annotation.Detections.ToList(), SourceEnum.Manual, null, - _authProvider.CurrentUser.Role, _authProvider.CurrentUser.Email, token: token); + _api.CurrentUser.Role, _api.CurrentUser.Email, token: token); // Manual save from Validators -> Validated -> stream: azaion-annotations-confirm // AI, Manual save from Operators -> Created -> stream: azaion-annotations @@ -199,11 +194,11 @@ public class AnnotationService : INotificationHandler await _producer.SendToInnerQueue(annotation, token); await _mediator.Publish(new AnnotationCreatedEvent(annotation), token); - await ThrottleExt.ThrottleRunAfter(() => + await ThrottleExt.Throttle(() => { _dbFactory.SaveToDisk(); return Task.CompletedTask; - }, SaveTaskId, TimeSpan.FromSeconds(5), token); + }, TimeSpan.FromSeconds(5), token); return annotation; } diff --git a/Azaion.Common/Services/GPSMatcherService.cs b/Azaion.Common/Services/GPSMatcherService.cs index cd803d6..03c4370 100644 --- a/Azaion.Common/Services/GPSMatcherService.cs +++ b/Azaion.Common/Services/GPSMatcherService.cs @@ -3,6 +3,7 @@ using System.IO; using Azaion.Common.DTO; using Azaion.Common.DTO.Config; using Azaion.CommonSecurity; +using Azaion.CommonSecurity.DTO; using Microsoft.Extensions.Options; namespace Azaion.Common.Services; diff --git a/Azaion.Common/Services/InferenceService.cs b/Azaion.Common/Services/InferenceService.cs index c66baff..3ce4ef6 100644 --- a/Azaion.Common/Services/InferenceService.cs +++ b/Azaion.Common/Services/InferenceService.cs @@ -17,10 +17,11 @@ public interface IInferenceService void StopInference(); } -public class InferenceService(ILogger logger, IInferenceClient client, IOptions aiConfigOptions) : IInferenceService +public class InferenceService(ILogger logger, IInferenceClient client, IAzaionApi azaionApi, IOptions aiConfigOptions) : IInferenceService { public async Task RunInference(List mediaPaths, Func processAnnotation, CancellationToken detectToken = default) { + client.Send(RemoteCommand.Create(CommandType.Login, azaionApi.Credentials)); var aiConfig = aiConfigOptions.Value; aiConfig.Paths = mediaPaths; diff --git a/Azaion.Common/Services/SatelliteDownloader.cs b/Azaion.Common/Services/SatelliteDownloader.cs index 4660820..7c7d0be 100644 --- a/Azaion.Common/Services/SatelliteDownloader.cs +++ b/Azaion.Common/Services/SatelliteDownloader.cs @@ -7,6 +7,7 @@ using Azaion.Common.DTO; using Azaion.Common.DTO.Config; using Azaion.Common.Extensions; using Azaion.CommonSecurity; +using Azaion.CommonSecurity.DTO; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json; diff --git a/Azaion.CommonSecurity/Azaion.CommonSecurity.csproj b/Azaion.CommonSecurity/Azaion.CommonSecurity.csproj index 4e4f70d..88fbb9f 100644 --- a/Azaion.CommonSecurity/Azaion.CommonSecurity.csproj +++ b/Azaion.CommonSecurity/Azaion.CommonSecurity.csproj @@ -7,9 +7,11 @@ + + diff --git a/Azaion.CommonSecurity/DTO/BusinessExceptionDto.cs b/Azaion.CommonSecurity/DTO/BusinessExceptionDto.cs new file mode 100644 index 0000000..5315856 --- /dev/null +++ b/Azaion.CommonSecurity/DTO/BusinessExceptionDto.cs @@ -0,0 +1,7 @@ +namespace Azaion.CommonSecurity.DTO; + +internal class BusinessExceptionDto +{ + public int ErrorCode { get; set; } + public string Message { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/Azaion.Common/DTO/Config/DirectoriesConfig.cs b/Azaion.CommonSecurity/DTO/DirectoriesConfig.cs similarity index 80% rename from Azaion.Common/DTO/Config/DirectoriesConfig.cs rename to Azaion.CommonSecurity/DTO/DirectoriesConfig.cs index b36a57b..5e84edd 100644 --- a/Azaion.Common/DTO/Config/DirectoriesConfig.cs +++ b/Azaion.CommonSecurity/DTO/DirectoriesConfig.cs @@ -1,7 +1,9 @@ -namespace Azaion.Common.DTO.Config; +namespace Azaion.CommonSecurity.DTO; public class DirectoriesConfig { + public string ApiResourcesDirectory { get; set; } = null!; + public string VideosDirectory { get; set; } = null!; public string LabelsDirectory { get; set; } = null!; public string ImagesDirectory { get; set; } = null!; diff --git a/Azaion.CommonSecurity/DTO/ExternalClientsConfig.cs b/Azaion.CommonSecurity/DTO/ExternalClientsConfig.cs index aaf7d64..691a922 100644 --- a/Azaion.CommonSecurity/DTO/ExternalClientsConfig.cs +++ b/Azaion.CommonSecurity/DTO/ExternalClientsConfig.cs @@ -8,10 +8,7 @@ public abstract class ExternalClientConfig public int RetryCount {get;set;} } -public class InferenceClientConfig : ExternalClientConfig -{ - public string ResourcesFolder { get; set; } = ""; -} +public class InferenceClientConfig : ExternalClientConfig; public class GpsDeniedClientConfig : ExternalClientConfig { diff --git a/Azaion.CommonSecurity/DTO/HardwareInfo.cs b/Azaion.CommonSecurity/DTO/HardwareInfo.cs index 292c212..4b87a34 100644 --- a/Azaion.CommonSecurity/DTO/HardwareInfo.cs +++ b/Azaion.CommonSecurity/DTO/HardwareInfo.cs @@ -6,6 +6,4 @@ public class HardwareInfo public string GPU { get; set; } = null!; public string MacAddress { get; set; } = null!; public string Memory { get; set; } = null!; - - public string Hash { get; set; } = null!; } \ No newline at end of file diff --git a/Azaion.CommonSecurity/DTO/SecureAppConfig.cs b/Azaion.CommonSecurity/DTO/SecureAppConfig.cs index 80ea07f..a40d736 100644 --- a/Azaion.CommonSecurity/DTO/SecureAppConfig.cs +++ b/Azaion.CommonSecurity/DTO/SecureAppConfig.cs @@ -4,4 +4,5 @@ public class SecureAppConfig { public InferenceClientConfig InferenceClientConfig { get; set; } = null!; public GpsDeniedClientConfig GpsDeniedClientConfig { get; set; } = null!; + public DirectoriesConfig DirectoriesConfig { get; set; } = null!; } \ No newline at end of file diff --git a/Azaion.CommonSecurity/DTO/User.cs b/Azaion.CommonSecurity/DTO/User.cs index bb2a2ba..9f6add9 100644 --- a/Azaion.CommonSecurity/DTO/User.cs +++ b/Azaion.CommonSecurity/DTO/User.cs @@ -1,11 +1,21 @@ -using MessagePack; - namespace Azaion.CommonSecurity.DTO; -[MessagePackObject] public class User { - [Key("i")] public string Id { get; set; } = ""; - [Key("e")] public string Email { get; set; } = ""; - [Key("r")]public RoleEnum Role { get; set; } + public string Id { get; set; } = ""; + public string Email { get; set; } = ""; + public RoleEnum Role { get; set; } + public UserConfig? UserConfig { get; set; } = null!; +} + +public class UserConfig +{ + public UserQueueOffsets? QueueConfig { get; set; } = new(); +} + +public class UserQueueOffsets +{ + public int AnnotationsOffset { get; set; } + public int AnnotationsConfirmOffset { get; set; } + public int AnnotationsCommandsOffset { get; set; } } \ No newline at end of file diff --git a/Azaion.CommonSecurity/SecurityConstants.cs b/Azaion.CommonSecurity/SecurityConstants.cs index da0bffc..2498753 100644 --- a/Azaion.CommonSecurity/SecurityConstants.cs +++ b/Azaion.CommonSecurity/SecurityConstants.cs @@ -9,6 +9,9 @@ public class SecurityConstants public const string DUMMY_DIR = "dummy"; #region ExternalClientsConfig + //public const string API_URL = "http://localhost:5219"; + public const string API_URL = "https://api.azaion.com"; + public const string EXTERNAL_INFERENCE_PATH = "azaion-inference.exe"; public const string EXTERNAL_GPS_DENIED_FOLDER = "gps-denied"; public static readonly string ExternalGpsDeniedPath = Path.Combine(EXTERNAL_GPS_DENIED_FOLDER, "image-matcher.exe"); @@ -22,6 +25,13 @@ public class SecurityConstants public const int DEFAULT_RETRY_COUNT = 25; public const int DEFAULT_TIMEOUT_SECONDS = 5; + # region Cache keys + + public const string CURRENT_USER_CACHE_KEY = "CurrentUser"; + public const string HARDWARE_INFO_KEY = "HardwareInfo"; + + # endregion + public static readonly SecureAppConfig DefaultSecureAppConfig = new() { InferenceClientConfig = new InferenceClientConfig @@ -29,8 +39,7 @@ public class SecurityConstants ZeroMqHost = DEFAULT_ZMQ_INFERENCE_HOST, ZeroMqPort = DEFAULT_ZMQ_INFERENCE_PORT, OneTryTimeoutSeconds = DEFAULT_TIMEOUT_SECONDS, - RetryCount = DEFAULT_RETRY_COUNT, - ResourcesFolder = "" + RetryCount = DEFAULT_RETRY_COUNT }, GpsDeniedClientConfig = new GpsDeniedClientConfig { @@ -38,6 +47,10 @@ public class SecurityConstants ZeroMqPort = DEFAULT_ZMQ_GPS_DENIED_PORT, OneTryTimeoutSeconds = DEFAULT_TIMEOUT_SECONDS, RetryCount = DEFAULT_RETRY_COUNT, + }, + DirectoriesConfig = new DirectoriesConfig + { + ApiResourcesDirectory = "" } }; #endregion ExternalClientsConfig diff --git a/Azaion.CommonSecurity/Services/AuthProvider.cs b/Azaion.CommonSecurity/Services/AuthProvider.cs index ebc5d0d..235a7e4 100644 --- a/Azaion.CommonSecurity/Services/AuthProvider.cs +++ b/Azaion.CommonSecurity/Services/AuthProvider.cs @@ -1,26 +1,117 @@ -using Azaion.CommonSecurity.DTO; -using Azaion.CommonSecurity.DTO.Commands; -using Microsoft.Extensions.DependencyInjection; +using System.Net; +using System.Net.Http.Headers; +using System.Text; +using Azaion.CommonSecurity.DTO; +using Newtonsoft.Json; namespace Azaion.CommonSecurity.Services; -public interface IAuthProvider +public interface IAzaionApi { - void Login(ApiCredentials credentials); + ApiCredentials Credentials { get; } User CurrentUser { get; } + T? Get(string url); + Stream GetResource(string filename); } -public class AuthProvider(IInferenceClient inferenceClient) : IAuthProvider +public class AzaionApi(HttpClient client, ICache cache, ApiCredentials credentials, IHardwareService hardwareService) : IAzaionApi { - public User CurrentUser { get; private set; } = null!; + private string _jwtToken = null!; + const string APP_JSON = "application/json"; + public ApiCredentials Credentials => credentials; - public void Login(ApiCredentials credentials) + public User CurrentUser { - inferenceClient.Send(RemoteCommand.Create(CommandType.Login, credentials)); - var user = inferenceClient.Get(); - if (user == null) - throw new Exception("Can't get user from Auth provider"); + get + { + var user = cache.GetFromCache(SecurityConstants.CURRENT_USER_CACHE_KEY, + () => Get("currentUser")); + if (user == null) + throw new Exception("Can't get current user"); - CurrentUser = user; + return user; + } + + } + + private HttpResponseMessage Send(HttpRequestMessage request, CancellationToken ct = default) + { + if (string.IsNullOrEmpty(_jwtToken)) + Authorize(); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _jwtToken); + var response = client.Send(request); + + if (response.StatusCode == HttpStatusCode.Unauthorized) + { + Authorize(); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _jwtToken); + response = client.Send(request); + } + + if (response.IsSuccessStatusCode) + return response; + + var stream = response.Content.ReadAsStream(); + var content = new StreamReader(stream).ReadToEnd(); + if (response.StatusCode == HttpStatusCode.Conflict) + { + var result = JsonConvert.DeserializeObject(content); + throw new Exception($"Failed: {response.StatusCode}! Error Code: {result.ErrorCode}. Message: {result.Message}"); + } + throw new Exception($"Failed: {response.StatusCode}! Result: {content}"); + } + + public Stream GetResource(string filename) + { + var hardware = cache.GetFromCache(SecurityConstants.HARDWARE_INFO_KEY, hardwareService.GetHardware); + + var response = Send(new HttpRequestMessage(HttpMethod.Post, $"/resources/get/{credentials.Folder}") + { + Content = new StringContent(JsonConvert.SerializeObject(new { filename, credentials.Password, hardware }), Encoding.UTF8, APP_JSON) + }); + return response.Content.ReadAsStream(); + } + + private void Authorize() + { + try + { + if (string.IsNullOrEmpty(credentials.Email) || credentials.Password.Length == 0) + throw new Exception("Email or password is empty! Please do EnterCredentials first!"); + + var payload = new + { + email = credentials.Email, + password = credentials.Password + }; + var content = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, APP_JSON); + var message = new HttpRequestMessage(HttpMethod.Post, "login") { Content = content }; + var response = client.Send(message); + + if (!response.IsSuccessStatusCode) + throw new Exception($"EnterCredentials failed: {response.StatusCode}"); + + var stream = response.Content.ReadAsStream(); + var json = new StreamReader(stream).ReadToEnd(); + var result = JsonConvert.DeserializeObject(json); + + if (string.IsNullOrEmpty(result?.Token)) + throw new Exception("JWT Token not found in response"); + + _jwtToken = result.Token; + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + public T? Get(string url) + { + var response = Send(new HttpRequestMessage(HttpMethod.Get, url)); + var stream = response.Content.ReadAsStream(); + var json = new StreamReader(stream).ReadToEnd(); + return JsonConvert.DeserializeObject(json); } } \ No newline at end of file diff --git a/Azaion.CommonSecurity/Services/Cache.cs b/Azaion.CommonSecurity/Services/Cache.cs new file mode 100644 index 0000000..b98d8a3 --- /dev/null +++ b/Azaion.CommonSecurity/Services/Cache.cs @@ -0,0 +1,27 @@ +using LazyCache; + +namespace Azaion.CommonSecurity.Services; + +public interface ICache +{ + T GetFromCache(string key, Func fetchFunc, TimeSpan? expiration = null); + void Invalidate(string key); +} + +public class MemoryCache : ICache +{ + private readonly IAppCache _cache = new CachingService(); + + public T GetFromCache(string key, Func fetchFunc, TimeSpan? expiration = null) + { + expiration ??= TimeSpan.FromHours(4); + return _cache.GetOrAdd(key, entry => + { + var result = fetchFunc(); + entry.AbsoluteExpirationRelativeToNow = expiration; + return result; + }); + } + + public void Invalidate(string key) => _cache.Remove(key); +} \ No newline at end of file diff --git a/Azaion.CommonSecurity/Services/HardwareService.cs b/Azaion.CommonSecurity/Services/HardwareService.cs index 3e1a815..e61cadf 100644 --- a/Azaion.CommonSecurity/Services/HardwareService.cs +++ b/Azaion.CommonSecurity/Services/HardwareService.cs @@ -38,29 +38,20 @@ public class HardwareService : IHardwareService .Replace("Name=", "") .Replace(" ", " ") .Trim() - .Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries); + .Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim()) + .ToArray(); - var memoryStr = "Unknown RAM"; - if (lines.Length > 0) - { - memoryStr = lines[0]; - if (int.TryParse(memoryStr, out var memKb)) - memoryStr = $"{Math.Round(memKb / 1024.0 / 1024.0)} Gb"; - } + if (lines.Length < 3) + throw new Exception("Can't get hardware info"); - var macAddress = MacAddress(); var hardwareInfo = new HardwareInfo { - Memory = memoryStr, - CPU = lines.Length > 1 && string.IsNullOrEmpty(lines[1]) - ? "Unknown CPU" - : lines[1].Trim(), - GPU = lines.Length > 2 && string.IsNullOrEmpty(lines[2]) - ? "Unknown GPU" - : lines[2], - MacAddress = macAddress + CPU = lines[0], + GPU = lines[1], + Memory = lines[2], + MacAddress = GetMacAddress() }; - hardwareInfo.Hash = ToHash($"Az|{hardwareInfo.CPU}|{hardwareInfo.GPU}|{macAddress}"); return hardwareInfo; } catch (Exception ex) @@ -70,7 +61,7 @@ public class HardwareService : IHardwareService } } - private string MacAddress() + private string GetMacAddress() { var macAddress = NetworkInterface .GetAllNetworkInterfaces() diff --git a/Azaion.CommonSecurity/Services/InferenceClient.cs b/Azaion.CommonSecurity/Services/InferenceClient.cs index 401243a..7fc307f 100644 --- a/Azaion.CommonSecurity/Services/InferenceClient.cs +++ b/Azaion.CommonSecurity/Services/InferenceClient.cs @@ -71,9 +71,6 @@ public class InferenceClient : IInferenceClient _dealer.SendFrame(MessagePackSerializer.Serialize(command)); } - public void SendString(string text) => - Send(new RemoteCommand(CommandType.Load, MessagePackSerializer.Serialize(text))); - public T? Get(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default) where T : class { var bytes = GetBytes(retries, tryTimeoutSeconds, ct); @@ -83,8 +80,9 @@ public class InferenceClient : IInferenceClient public byte[]? GetBytes(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default) { var tryNum = 0; - while (!ct.IsCancellationRequested && tryNum++ < retries) + while (!ct.IsCancellationRequested && tryNum < retries) { + tryNum++; if (!_dealer.TryReceiveFrameBytes(TimeSpan.FromSeconds(tryTimeoutSeconds), out var bytes)) continue; @@ -92,7 +90,7 @@ public class InferenceClient : IInferenceClient } if (!ct.IsCancellationRequested) - throw new Exception($"Unable to get bytes after {tryNum} retries, {tryTimeoutSeconds} seconds each"); + throw new Exception($"Unable to get bytes after {tryNum - 1} retries, {tryTimeoutSeconds} seconds each"); return null; } diff --git a/Azaion.CommonSecurity/Services/ResourceLoader.cs b/Azaion.CommonSecurity/Services/ResourceLoader.cs index 7929ad1..38a4d70 100644 --- a/Azaion.CommonSecurity/Services/ResourceLoader.cs +++ b/Azaion.CommonSecurity/Services/ResourceLoader.cs @@ -13,7 +13,7 @@ public class ResourceLoader([FromKeyedServices(SecurityConstants.EXTERNAL_INFERE public MemoryStream LoadFile(string fileName, string? folder = null) { inferenceClient.Send(RemoteCommand.Create(CommandType.Load, new LoadFileData(fileName, folder))); - var bytes = inferenceClient.GetBytes(); + var bytes = inferenceClient.GetBytes(2, 3); if (bytes == null) throw new Exception($"Unable to receive {fileName}"); diff --git a/Azaion.Dataset/DatasetExplorer.xaml.cs b/Azaion.Dataset/DatasetExplorer.xaml.cs index 758eca1..a0e1e7f 100644 --- a/Azaion.Dataset/DatasetExplorer.xaml.cs +++ b/Azaion.Dataset/DatasetExplorer.xaml.cs @@ -7,6 +7,7 @@ using Azaion.Common.DTO; using Azaion.Common.DTO.Config; using Azaion.Common.Events; using Azaion.Common.Services; +using Azaion.CommonSecurity.DTO; using LinqToDB; using MediatR; using Microsoft.Extensions.Logging; diff --git a/Azaion.Inference/azaion-inference.spec b/Azaion.Inference/azaion-inference.spec index a01fda6..37f6901 100644 --- a/Azaion.Inference/azaion-inference.spec +++ b/Azaion.Inference/azaion-inference.spec @@ -4,6 +4,8 @@ from PyInstaller.utils.hooks import collect_all datas = [] binaries = [] hiddenimports = ['constants', 'annotation', 'credentials', 'file_data', 'user', 'security', 'secure_model', 'api_client', 'hardware_service', 'remote_command', 'ai_config', 'inference_engine', 'inference', 'remote_command_handler'] +tmp_ret = collect_all('pyyaml') +datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] tmp_ret = collect_all('jwt') datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] tmp_ret = collect_all('requests') diff --git a/Azaion.Suite/App.xaml.cs b/Azaion.Suite/App.xaml.cs index 8ca24e1..9561987 100644 --- a/Azaion.Suite/App.xaml.cs +++ b/Azaion.Suite/App.xaml.cs @@ -1,8 +1,12 @@ using System.IO; +using System.Net.Http; using System.Reflection; +using System.Text; +using System.Text.Unicode; using System.Windows; using System.Windows.Threading; using Azaion.Annotator; +using Azaion.Common; using Azaion.Common.Database; using Azaion.Common.DTO; using Azaion.Common.DTO.Config; @@ -11,8 +15,10 @@ using Azaion.Common.Extensions; using Azaion.Common.Services; using Azaion.CommonSecurity; using Azaion.CommonSecurity.DTO; +using Azaion.CommonSecurity.DTO.Commands; using Azaion.CommonSecurity.Services; using Azaion.Dataset; +using LazyCache; using LibVLCSharp.Shared; using MediatR; using Microsoft.Extensions.Configuration; @@ -35,12 +41,14 @@ public partial class App private IInferenceClient _inferenceClient = null!; private IResourceLoader _resourceLoader = null!; - private IAuthProvider _authProvider = null!; - private Stream _securedConfig = null!; private Stream _systemConfig = null!; private static readonly Guid KeyPressTaskId = Guid.NewGuid(); + private readonly ICache _cache = new MemoryCache(); + private readonly IHardwareService _hardwareService = new HardwareService(); + private IAzaionApi _azaionApi = null!; + private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) { _logger.LogError(e.Exception, e.Exception.Message); @@ -83,21 +91,32 @@ public partial class App var secureAppConfig = ReadSecureAppConfig(); _inferenceClient = new InferenceClient(new OptionsWrapper(secureAppConfig.InferenceClientConfig)); _resourceLoader = new ResourceLoader(_inferenceClient); - _authProvider = new AuthProvider(_inferenceClient); - var login = new Login(); - login.Closed += (sender, args) => - { - if (!login.MainSuiteOpened) - _inferenceClient.Stop(); - }; - login.CredentialsEntered += (_, credentials) => + login.CredentialsEntered += async (_, credentials) => { - credentials.Folder = secureAppConfig.InferenceClientConfig.ResourcesFolder; - _authProvider.Login(credentials); - _securedConfig = _resourceLoader.LoadFile("config.secured.json"); - _systemConfig = _resourceLoader.LoadFile("config.system.json"); + credentials.Folder = secureAppConfig.DirectoriesConfig.ApiResourcesDirectory; + + _inferenceClient.Send(RemoteCommand.Create(CommandType.Login, credentials)); + _azaionApi = new AzaionApi(new HttpClient { BaseAddress = new Uri(SecurityConstants.API_URL) }, _cache, credentials, _hardwareService); + + try + { + _securedConfig = _resourceLoader.LoadFile("config.secured.json"); + _systemConfig = _resourceLoader.LoadFile("config.system.json"); + } + catch (Exception e) + { + Console.WriteLine(e); + _securedConfig = new MemoryStream("{}"u8.ToArray()); + var systemConfig = new + { + AnnotationConfig = Constants.DefaultAnnotationConfig, + AIRecognitionConfig = Constants.DefaultAIRecognitionConfig, + ThumbnailConfig = Constants.DefaultThumbnailConfig, + }; + _systemConfig = new MemoryStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(systemConfig))); + } AppDomain.CurrentDomain.AssemblyResolve += (_, a) => { @@ -168,14 +187,13 @@ public partial class App services.ConfigureSection(context.Configuration); services.ConfigureSection(context.Configuration); - services.AddSingleton(_inferenceClient); + services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(_resourceLoader); - services.AddSingleton(_authProvider); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddHttpClient(); + services.AddSingleton(_azaionApi); #endregion services.AddSingleton(); @@ -220,7 +238,7 @@ public partial class App { var args = (KeyEventArgs)e; var keyEvent = new KeyEvent(sender, args, _formState.ActiveWindow); - _ = ThrottleExt.ThrottleRunFirst(() => _mediator.Publish(keyEvent), KeyPressTaskId, TimeSpan.FromMilliseconds(50)); + _ = ThrottleExt.Throttle(() => _mediator.Publish(keyEvent), TimeSpan.FromMilliseconds(50)); } protected override async void OnExit(ExitEventArgs e) diff --git a/Azaion.Suite/MainSuite.xaml.cs b/Azaion.Suite/MainSuite.xaml.cs index c3854c1..14d527b 100644 --- a/Azaion.Suite/MainSuite.xaml.cs +++ b/Azaion.Suite/MainSuite.xaml.cs @@ -25,7 +25,6 @@ public partial class MainSuite private readonly IGalleryService _galleryService; private readonly IDbFactory _dbFactory; private readonly Dictionary _openedWindows = new(); - private readonly IResourceLoader _resourceLoader; private readonly IInferenceClient _inferenceClient; private readonly IGpsMatcherClient _gpsMatcherClient; private static readonly Guid SaveConfigTaskId = Guid.NewGuid(); @@ -36,7 +35,6 @@ public partial class MainSuite IServiceProvider sp, IGalleryService galleryService, IDbFactory dbFactory, - IResourceLoader resourceLoader, IInferenceClient inferenceClient, IGpsMatcherClient gpsMatcherClient) { @@ -45,11 +43,10 @@ public partial class MainSuite _sp = sp; _galleryService = galleryService; _dbFactory = dbFactory; - _resourceLoader = resourceLoader; _inferenceClient = inferenceClient; _gpsMatcherClient = gpsMatcherClient; - _appConfig = appConfig.Value; + InitializeComponent(); Loaded += OnLoaded; Closed += OnFormClosed; @@ -135,11 +132,11 @@ public partial class MainSuite private async Task SaveUserSettings() { - await ThrottleExt.ThrottleRunFirst(() => + await ThrottleExt.Throttle(() => { _configUpdater.Save(_appConfig); return Task.CompletedTask; - }, SaveConfigTaskId, TimeSpan.FromSeconds(2)); + }, TimeSpan.FromSeconds(2)); } private void OnFormClosed(object? sender, EventArgs e) diff --git a/Azaion.Suite/config.json b/Azaion.Suite/config.json index da36fa7..3f0cdf1 100644 --- a/Azaion.Suite/config.json +++ b/Azaion.Suite/config.json @@ -3,8 +3,7 @@ "ZeroMqHost": "127.0.0.1", "ZeroMqPort": 5127, "RetryCount": 25, - "TimeoutSeconds": 5, - "ResourcesFolder": "stage" + "TimeoutSeconds": 5 }, "GpsDeniedClientConfig": { "ZeroMqHost": "127.0.0.1", @@ -14,6 +13,7 @@ "TimeoutSeconds": 5 }, "DirectoriesConfig": { + "ApiResourcesDirectory": "stage", "VideosDirectory": "E:\\Azaion6", "LabelsDirectory": "E:\\labels", "ImagesDirectory": "E:\\images", diff --git a/Azaion.Suite/config.production.json b/Azaion.Suite/config.production.json index dd879a7..b06b752 100644 --- a/Azaion.Suite/config.production.json +++ b/Azaion.Suite/config.production.json @@ -3,8 +3,7 @@ "ZeroMqHost": "127.0.0.1", "ZeroMqPort": 5131, "RetryCount": 25, - "TimeoutSeconds": 5, - "ResourcesFolder": "" + "TimeoutSeconds": 5 }, "GpsDeniedClientConfig": { "ZeroMqHost": "127.0.0.1", @@ -14,6 +13,7 @@ "TimeoutSeconds": 5 }, "DirectoriesConfig": { + "ApiResourcesDirectory": "", "VideosDirectory": "videos", "LabelsDirectory": "labels", "ImagesDirectory": "images", diff --git a/Azaion.Suite/config.system.json b/Azaion.Suite/config.system.json index bc78f06..a0beb77 100644 --- a/Azaion.Suite/config.system.json +++ b/Azaion.Suite/config.system.json @@ -13,7 +13,7 @@ { "Id": 9, "Name": "Smoke", "ShortName": "Дим", "Color": "#000080" }, { "Id": 10, "Name": "Plane", "ShortName": "Літак", "Color": "#000080" }, { "Id": 11, "Name": "Moto", "ShortName": "Мото", "Color": "#808000" }, - { "Id": 12, "Name": "CamouflageNnet", "ShortName": "Сітка", "Color": "#800080" }, + { "Id": 12, "Name": "CamouflageNet", "ShortName": "Сітка", "Color": "#800080" }, { "Id": 13, "Name": "CamouflageBranches", "ShortName": "Гілки", "Color": "#2f4f4f" }, { "Id": 14, "Name": "Roof", "ShortName": "Дах", "Color": "#1e90ff" }, { "Id": 15, "Name": "Building", "ShortName": "Будівля", "Color": "#ffb6c1" } diff --git a/Azaion.Suite/postbuild.cmd b/Azaion.Suite/postbuild.cmd index 7423da5..93ca1b7 100644 --- a/Azaion.Suite/postbuild.cmd +++ b/Azaion.Suite/postbuild.cmd @@ -14,7 +14,7 @@ set SUITE_FOLDER=%cd%\bin\%CONFIG%\net8.0-windows\ rem Inference set INFERENCE_PATH=%cd%\..\Azaion.Inference -xcopy /E %INFERENCE_PATH%\dist\azaion-inference %SUITE_FOLDER% +xcopy /E /Y %INFERENCE_PATH%\dist\azaion-inference %SUITE_FOLDER% copy %INFERENCE_PATH%\venv\Lib\site-packages\tensorrt_libs\nvinfer_10.dll %SUITE_FOLDER% copy %INFERENCE_PATH%\venv\Lib\site-packages\tensorrt_libs\nvinfer_plugin_10.dll %SUITE_FOLDER% copy %INFERENCE_PATH%\venv\Lib\site-packages\tensorrt_libs\nvonnxparser_10.dll %SUITE_FOLDER% diff --git a/Azaion.Test/GetTilesTest.cs b/Azaion.Test/GetTilesTest.cs index d27e8d3..64e011f 100644 --- a/Azaion.Test/GetTilesTest.cs +++ b/Azaion.Test/GetTilesTest.cs @@ -1,5 +1,6 @@ using Azaion.Common.DTO.Config; using Azaion.Common.Services; +using Azaion.CommonSecurity.DTO; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options;