From 930799637b5c5a18931f7961a710545773166f95 Mon Sep 17 00:00:00 2001 From: ChrisMaunder Date: Mon, 22 Jan 2024 11:12:33 -0500 Subject: [PATCH] v2.5.1: Reworking modulesettings schema / correcting Promise/async dichotomy --- src/SDK/NET/API/ApiClient.cs | 6 +- src/SDK/NET/API/ExplorerUI.cs | 38 + src/SDK/NET/API/ModuleResponses.cs | 5 + src/SDK/NET/API/VersionInfo.cs | 18 +- src/SDK/NET/Analysis/BackendClient.cs | 22 +- src/SDK/NET/Analysis/BackendRequests.cs | 2 +- src/SDK/NET/Analysis/ModuleWorkerBase.cs | 35 +- src/SDK/NET/Common/Constants.cs | 81 + src/SDK/NET/Common/ModuleBase.cs | 121 +- src/SDK/NET/Common/ModuleDescription.cs | 5 +- src/SDK/NET/Common/ProcessStatus.cs | 31 +- src/SDK/NET/Common/SystemInfo.cs | 8 +- src/SDK/Python/module_options.py | 5 +- src/SDK/Python/module_runner.py | 188 +- src/SDK/Python/request_data.py | 2 +- src/SDK/Scripts/install_cuDNN.sh | 6 +- src/SDK/Scripts/utils.sh | 3 +- src/SDK/install.sh | 2 +- src/create_packages.bat | 4 +- src/modules/ALPR-RKNN/ALPR_adapter.py | 42 +- src/modules/ALPR-RKNN/explore.html | 8 +- src/modules/ALPR-RKNN/modulesettings.json | 75 +- src/modules/ALPR-RKNN/options.py | 2 +- src/modules/ALPR/ALPR.py | 2 +- src/modules/ALPR/ALPR_adapter.py | 49 +- src/modules/ALPR/explore.html | 8 +- src/modules/ALPR/modulesettings.jetson.json | 10 +- src/modules/ALPR/modulesettings.json | 94 +- src/modules/ALPR/modulesettings.linux.json | 4 +- src/modules/ALPR/modulesettings.macos.json | 4 +- src/modules/ALPR/modulesettings.orangepi.json | 10 +- .../ALPR/modulesettings.raspberrypi.json | 10 +- src/modules/ALPR/modulesettings.windows.json | 10 +- src/modules/ALPR/options.py | 4 +- src/modules/BackgroundRemover/explore.html | 8 +- .../modulesettings.jetson.json | 6 +- .../BackgroundRemover/modulesettings.json | 91 +- .../modulesettings.orangepi.json | 6 +- .../modulesettings.raspberrypi.json | 6 +- .../BackgroundRemover/rembg_adapter.py | 39 +- .../Cartooniser/cartooniser_adapter.py | 46 +- src/modules/Cartooniser/explore.html | 10 +- .../Cartooniser/modulesettings.jetson.json | 6 +- src/modules/Cartooniser/modulesettings.json | 84 +- .../Cartooniser/modulesettings.orangepi.json | 6 +- .../modulesettings.raspberrypi.json | 6 +- .../Cartooniser/modulesettings.windows.json | 6 +- src/modules/Cartooniser/options.py | 2 - src/modules/FaceProcessing/explore.html | 49 +- .../FaceProcessing/intelligencelayer/face.py | 126 +- .../modulesettings.docker.build.json | 13 +- .../FaceProcessing/modulesettings.jetson.json | 6 +- .../FaceProcessing/modulesettings.json | 103 +- .../modulesettings.orangepi.json | 4 +- .../modulesettings.raspberrypi.json | 6 +- .../modulesettings.windows.json | 6 +- src/modules/LlamaChat/Llama.pyproj | 1 - src/modules/LlamaChat/Llama_adapter.py | 38 +- src/modules/LlamaChat/explore.html | 8 +- src/modules/LlamaChat/install.sh | 2 +- src/modules/LlamaChat/modulesettings.json | 78 +- .../LlamaChat/modulesettings.windows.json | 4 +- src/modules/OCR/OCR_adapter.py | 40 +- src/modules/OCR/explore.html | 8 +- src/modules/OCR/modulesettings.jetson.json | 10 +- src/modules/OCR/modulesettings.json | 89 +- src/modules/OCR/modulesettings.linux.json | 4 +- src/modules/OCR/modulesettings.macos.json | 4 +- src/modules/OCR/modulesettings.orangepi.json | 10 +- .../OCR/modulesettings.raspberrypi.json | 10 +- src/modules/OCR/modulesettings.windows.json | 10 +- src/modules/OCR/options.py | 2 +- .../ObjectDetectionCoral.pyproj | 6 - src/modules/ObjectDetectionCoral/explore.html | 64 +- .../modulesettings.docker.build.json | 18 +- .../modulesettings.docker.build.rpi64.json | 20 +- .../modulesettings.jetson.json | 7 +- .../ObjectDetectionCoral/modulesettings.json | 138 +- .../modulesettings.macos.json | 4 +- .../modulesettings.orangepi.json | 6 +- .../modulesettings.raspberrypi.json | 6 +- .../modulesettings.windows.json | 4 +- .../objectdetection_coral_adapter.py | 63 +- .../detect_adapter.py | 114 +- .../ObjectDetectionYOLOv5-3.1/explore.html | 65 +- .../modulesettings.docker.build.jetson.json | 16 +- .../modulesettings.docker.build.rpi64.json | 15 +- .../modulesettings.jetson.json | 7 +- .../modulesettings.json | 113 +- .../modulesettings.raspberrypi.json | 8 +- .../modulesettings.windows.json | 6 +- .../detect_adapter.py | 73 +- .../ObjectDetectionYOLOv5-6.2/explore.html | 63 +- .../modulesettings.docker.build.arm64.json | 15 +- .../modulesettings.docker.build.gpu.json | 14 +- .../modulesettings.docker.build.json | 16 +- .../modulesettings.docker.build.rpi64.json | 16 +- .../modulesettings.jetson.json | 7 +- .../modulesettings.json | 113 +- .../modulesettings.orangepi.json | 7 +- .../modulesettings.raspberrypi.json | 7 +- .../modulesettings.windows.json | 4 +- .../ObjectDetectionWorker.cs | 25 +- .../Properties/launchSettings.json | 1 - .../YOLOv5/YoloLabel.cs | 4 +- .../ObjectDetectionYOLOv5Net/explore.html | 65 +- .../ObjectDetectionYOLOv5Net/install.bat | 6 + .../ObjectDetectionYOLOv5Net/install.sh | 5 + .../modulesettings.development.json | 2 + .../modulesettings.docker.build.arm64.json | 10 +- .../modulesettings.docker.build.json | 6 +- .../modulesettings.docker.build.rpi64.json | 13 +- .../modulesettings.json | 108 +- .../modulesettings.windows.development.json | 4 +- .../modulesettings.windows.json | 4 +- .../ObjectDetectionYOLOv5Net/package.bat | 12 +- .../ObjectDetectionYOLOv8/detect_adapter.py | 63 +- .../ObjectDetectionYOLOv8/explore.html | 71 +- src/modules/ObjectDetectionYOLOv8/install.sh | 5 +- .../modulesettings.docker.build.arm64.json | 16 +- .../modulesettings.docker.build.gpu.json | 14 +- .../modulesettings.docker.build.json | 15 +- .../modulesettings.docker.build.rpi64.json | 18 +- .../modulesettings.jetson.json | 6 +- .../ObjectDetectionYOLOv8/modulesettings.json | 109 +- .../modulesettings.orangepi.json | 6 +- .../modulesettings.raspberrypi.json | 6 +- .../modulesettings.windows.json | 4 +- src/modules/ObjectDetectionYOLOv8/options.py | 4 +- .../patch/ultralytics/nn/tasks.py | 398 ++-- .../ObjectDetectionYOLOv8/post_install.bat | 2 + .../ObjectDetectionYOLOv8/post_install.sh | 18 + .../requirements.linux.arm64.txt | 3 +- .../requirements.linux.cuda.txt | 3 +- .../requirements.linux.cuda11.txt | 3 +- .../requirements.linux.cuda11_5.txt | 3 +- .../requirements.linux.cuda12.txt | 3 +- .../requirements.linux.rocm.txt | 3 +- .../requirements.linux.txt | 3 +- .../requirements.macos.arm64.txt | 3 +- .../requirements.macos.txt | 3 +- .../requirements.orangepi.txt | 3 +- .../requirements.raspberrypi.txt | 3 +- .../ObjectDetectionYOLOv8/requirements.txt | 3 +- .../requirements.windows.cuda.txt | 3 +- .../requirements.windows.rocm.txt | 3 +- .../ObjectDetectionYoloRKNN/explore.html | 65 +- .../modulesettings.json | 98 +- .../objectdetection_fd_rknn_adapter.py | 75 +- src/modules/PortraitFilter/explore.html | 8 +- src/modules/PortraitFilter/install.bat | 10 +- src/modules/PortraitFilter/install.sh | 9 +- .../modulesettings.development.json | 8 - .../PortraitFilter/modulesettings.json | 85 +- .../modulesettings.linux.development.json | 4 +- .../PortraitFilter/modulesettings.linux.json | 6 +- .../modulesettings.macos.development.json | 4 +- .../PortraitFilter/modulesettings.macos.json | 4 +- .../modulesettings.windows.development.json | 10 + src/modules/PortraitFilter/test.html | 152 -- src/modules/SceneClassifier/explore.html | 8 +- .../SceneClassifier/modulesettings.json | 90 +- .../SceneClassifier/modulesettings.linux.json | 11 +- .../modulesettings.windows.json | 6 +- src/modules/SceneClassifier/scene_adapter.py | 54 +- src/modules/SentimentAnalysis/explore.html | 8 +- src/modules/SentimentAnalysis/install.bat | 10 +- src/modules/SentimentAnalysis/install.sh | 9 +- .../modulesettings.development.json | 8 - .../SentimentAnalysis/modulesettings.json | 83 +- .../modulesettings.linux.development.json | 4 +- .../modulesettings.linux.json | 4 +- .../modulesettings.macos.development.json | 4 +- .../modulesettings.macos.json | 4 +- .../modulesettings.windows.development.json | 10 + .../SoundClassifierTF.pyproj | 6 - src/modules/SoundClassifierTF/explore.html | 8 +- .../SoundClassifierTF/modulesettings.json | 69 +- .../sound_classification_adapter.py | 40 +- src/modules/SuperResolution/explore.html | 8 +- .../modulesettings.jetson.json | 10 +- .../SuperResolution/modulesettings.json | 91 +- .../modulesettings.windows.json | 6 +- .../SuperResolution/superres_adapter.py | 38 +- src/modules/TextSummary/explore.html | 23 +- .../TextSummary/modulesettings.jetson.json | 4 +- src/modules/TextSummary/modulesettings.json | 87 +- .../TextSummary/modulesettings.windows.json | 6 +- src/modules/TextSummary/summary_adapter.py | 40 +- .../explore.html | 468 +++-- .../modulesettings.json | 91 +- .../modulesettings.macos.json | 8 +- .../modulesettings.windows.json | 6 +- src/server/Backend/QueueServices.cs | 17 +- src/server/Backend/TriggerTaskRunner.cs | 2 +- .../Config/PersistedOverrideSettings.cs | 13 +- src/server/Config/ServerSettingsJsonWriter.cs | 7 +- src/server/Controllers/ModuleController.cs | 2 +- src/server/Controllers/ProxyController.cs | 120 +- src/server/Controllers/QueueController.cs | 15 +- src/server/Controllers/SettingsController.cs | 23 +- src/server/Controllers/StatusController.cs | 1 + src/server/Mesh/MeshSummary.cs | 2 +- src/server/Modules/BackendRouteMap.cs | 17 + src/server/Modules/LegacyModuleConfig.cs | 320 +++ src/server/Modules/ModuleCollection.cs | 663 ++++-- src/server/Modules/ModuleInstaller.cs | 50 +- src/server/Modules/ModuleProcessServices.cs | 92 +- src/server/Modules/ModuleRunner.cs | 16 +- src/server/Modules/ModuleSettings.cs | 54 +- src/server/Modules/ModuleSupportExtensions.cs | 17 +- src/server/Program.cs | 119 +- src/server/Startup.cs | 63 +- src/server/Utilities/ConfigExtensions.cs | 4 +- src/server/Utilities/PackageDownloader.cs | 4 +- .../Version/ServerVersionProcessRunner.cs | 2 +- src/server/version.json | 6 +- src/server/wwwroot/assets/dashboard.js | 1187 +++++------ src/server/wwwroot/assets/explorer.js | 632 ++++-- src/server/wwwroot/assets/server.css | 5 +- src/server/wwwroot/assets/server.js | 217 +- src/server/wwwroot/explorer.html | 1791 +---------------- src/server/wwwroot/index.html | 23 +- src/setup.bat | 14 +- src/setup.sh | 24 +- 225 files changed, 6024 insertions(+), 5302 deletions(-) create mode 100644 src/SDK/NET/Common/Constants.cs create mode 100644 src/modules/ObjectDetectionYOLOv8/post_install.sh delete mode 100644 src/modules/PortraitFilter/modulesettings.development.json create mode 100644 src/modules/PortraitFilter/modulesettings.windows.development.json delete mode 100644 src/modules/PortraitFilter/test.html delete mode 100644 src/modules/SentimentAnalysis/modulesettings.development.json create mode 100644 src/modules/SentimentAnalysis/modulesettings.windows.development.json create mode 100644 src/server/Modules/LegacyModuleConfig.cs diff --git a/src/SDK/NET/API/ApiClient.cs b/src/SDK/NET/API/ApiClient.cs index c55a142a..3eebca59 100644 --- a/src/SDK/NET/API/ApiClient.cs +++ b/src/SDK/NET/API/ApiClient.cs @@ -145,7 +145,7 @@ public async Task GetAsync(string route) } catch (Exception ex) { - response = new ServerErrorResponse(ex.Message); + response = new ServerErrorResponse("GetAsync error: " + ex.Message); } return response; @@ -178,7 +178,7 @@ public async Task GetAsync(string route) } catch (Exception ex) { - response = new ServerErrorResponse(ex.Message); + response = new ServerErrorResponse("GetAsync error: " + ex.Message); } return response; @@ -212,7 +212,7 @@ public async Task PostAsync(string route, ServerRequestConten } catch (Exception ex) { - response = new ServerErrorResponse(ex.Message); + response = new ServerErrorResponse("PostAsync error: " + ex.Message); } return response; diff --git a/src/SDK/NET/API/ExplorerUI.cs b/src/SDK/NET/API/ExplorerUI.cs index 666499ad..557a3a4b 100644 --- a/src/SDK/NET/API/ExplorerUI.cs +++ b/src/SDK/NET/API/ExplorerUI.cs @@ -2,6 +2,44 @@ namespace CodeProject.AI.SDK.API { + /// + /// Represents an option in a dropdown menu in the dashboard + /// + public class DashboardMenuOption + { + /// + /// Gets or sets the label for this menu option + /// + public string? Label { get; set; } + + /// + /// Gets or sets the setting to be modified by this menu option + /// + public string? Setting { get; set; } + + /// + /// Gets or sets the value to be set by this menu option + /// + public string? Value { get; set; } + } + + /// + /// Represents a dropdown menu in the dashboard. This will be used to construct a dropdown menu + /// in the CodeProject.AI Server dashboard for the given module. + /// + public class DashboardMenu + { + /// + /// Gets or sets the label for this menu + /// + public string? Label { get; set; } + + /// + /// Gets or sets the options for this menu + /// + public DashboardMenuOption[]? Options { get; set; } + } + /// /// A structure containing the HTML, CSS and Javascript that are to be injected into the /// Explorer web app to allow the user to explore and test a module. diff --git a/src/SDK/NET/API/ModuleResponses.cs b/src/SDK/NET/API/ModuleResponses.cs index d8c8bcd4..ca70cec9 100644 --- a/src/SDK/NET/API/ModuleResponses.cs +++ b/src/SDK/NET/API/ModuleResponses.cs @@ -48,6 +48,11 @@ public class ModuleResponse : BaseResponse /// response. /// public long AnalysisRoundTripMs { get; set; } + + /// + /// Gets or set a dictionary representing the current module status + /// + public object? StatusData { get; set; } } /// diff --git a/src/SDK/NET/API/VersionInfo.cs b/src/SDK/NET/API/VersionInfo.cs index d7f615e8..eab99208 100644 --- a/src/SDK/NET/API/VersionInfo.cs +++ b/src/SDK/NET/API/VersionInfo.cs @@ -30,7 +30,7 @@ public class VersionInfo /// /// Gets or sets a value indicating whether this version contains a security update /// - public bool? SecurityUpdate { get; set; } + public bool SecurityUpdate { get; set; } /// /// Gets or sets the build number @@ -155,11 +155,19 @@ public static int Compare(VersionInfo versionA, VersionInfo versionB) if (versionA.Build != versionB.Build) return versionA.Build - versionB.Build; - // int result = (versionA.PreRelease ?? "").CompareTo(versionB.PreRelease ?? ""); - // if (result != 0) - // return result; + // A pre-release string will be greater than an empty string. We actually want the + // opposite. 2.5.0 > 2.5.0-RC1 and 2.5.0-RTM > 2.5.0-RC1 - return (versionA.SecurityUpdate ?? false).CompareTo(versionB.SecurityUpdate ?? false); + if (string.IsNullOrWhiteSpace(versionA.PreRelease) && !string.IsNullOrWhiteSpace(versionB.PreRelease)) + return 1; + + if (!string.IsNullOrWhiteSpace(versionA.PreRelease) && string.IsNullOrWhiteSpace(versionB.PreRelease)) + return -1; + + if (!(versionA.PreRelease ?? string.Empty).Equals(versionB.PreRelease ?? string.Empty)) + return (versionA.PreRelease ?? string.Empty).CompareTo(versionB.PreRelease ?? string.Empty); + + return versionA.SecurityUpdate.CompareTo(versionB.SecurityUpdate); } } } diff --git a/src/SDK/NET/Analysis/BackendClient.cs b/src/SDK/NET/Analysis/BackendClient.cs index 2afcf0e2..f1680634 100644 --- a/src/SDK/NET/Analysis/BackendClient.cs +++ b/src/SDK/NET/Analysis/BackendClient.cs @@ -34,7 +34,7 @@ public BackendClient(string url, TimeSpan timeout = default, CancellationToken t _httpClient ??= new HttpClient { BaseAddress = new Uri(url), - Timeout = (timeout == default) ? TimeSpan.FromMinutes(1) : timeout + Timeout = (timeout == default) ? TimeSpan.FromMinutes(1) : timeout }; loggingTask = ProcessLoggingQueue(token); @@ -77,33 +77,27 @@ public BackendClient(string url, TimeSpan timeout = default, CancellationToken t BackendRequest? request = null; try { - //request = await _httpClient!.GetFromJsonAsync(requestUri, token) - // .ConfigureAwait(false); - var response = await _httpClient!.GetAsync(requestUri, token); - // only process responses that have content + HttpResponseMessage response = await _httpClient!.GetAsync(requestUri, token); if (response.StatusCode == System.Net.HttpStatusCode.OK) - { request = await response.Content.ReadFromJsonAsync(); - } - else - { - return null; - } } catch (JsonException) { +#if DEBUG + Debug.WriteLine("JsonException GetRequest"); +#endif // This is probably due to timing out and therefore no JSON to parse. } #if DEBUG catch (Exception ex) { - Debug.WriteLine(ex); + Debug.WriteLine("Error in GetRequest: " + ex.Message); #else catch (Exception /*ex*/) { #endif Console.WriteLine($"Unable to get request from {queueName} for {moduleId}"); - _errorPauseSecs = Math.Min(_errorPauseSecs > 0 ? _errorPauseSecs * 2 : 5, 60); + _errorPauseSecs = Math.Min(_errorPauseSecs > 0 ? _errorPauseSecs * 3/2 : 5, 30); if (!token.IsCancellationRequested && _errorPauseSecs > 0) { @@ -199,7 +193,7 @@ private async Task ProcessLoggingQueue(CancellationToken token = default) } catch(Exception e) { - Debug.Write(e); + Debug.Write("Error processing logging queue: " + e.Message); } } } diff --git a/src/SDK/NET/Analysis/BackendRequests.cs b/src/SDK/NET/Analysis/BackendRequests.cs index 1eca7d6a..c2ea5092 100644 --- a/src/SDK/NET/Analysis/BackendRequests.cs +++ b/src/SDK/NET/Analysis/BackendRequests.cs @@ -216,7 +216,7 @@ public void AddFile(string filePath) } catch (Exception e) { - Debug.WriteLine(e); + Debug.WriteLine("Error adding file: " + e.Message); } } diff --git a/src/SDK/NET/Analysis/ModuleWorkerBase.cs b/src/SDK/NET/Analysis/ModuleWorkerBase.cs index 61a1cdff..39b22816 100644 --- a/src/SDK/NET/Analysis/ModuleWorkerBase.cs +++ b/src/SDK/NET/Analysis/ModuleWorkerBase.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Dynamic; using System.Net.Http.Json; using CodeProject.AI.SDK.API; @@ -28,8 +29,11 @@ public abstract class ModuleWorkerBase : BackgroundService private readonly ILogger _logger; private readonly IHostApplicationLifetime _appLifetime; - private bool _cancelled = false; - private bool _performSelfTest = false; + private bool _cancelled = false; + private bool _performSelfTest = false; + private int _successInferences; + private long _totalSuccessInf_ms; + private int _failedInferences; /// /// Gets or sets the name of this Module @@ -93,7 +97,7 @@ public ModuleWorkerBase(ILogger logger, IConfiguration configuration, EnableGPU = configuration.GetValue("CPAI_MODULE_ENABLE_GPU", true); _accelDeviceName = configuration.GetValue("CPAI_ACCEL_DEVICE_NAME", null); _halfPrecision = configuration.GetValue("CPAI_HALF_PRECISION", null) ?? "enable"; // Can be enable, disable or force - _logVerbosity = configuration.GetValue("CPAI_LOG_VERBOSITY", null) ?? "info"; // Can be Quiet, Info or Loud + _logVerbosity = configuration.GetValue("CPAI_LOG_VERBOSITY", null) ?? "quiet"; // Can be Quiet, Info or Loud _performSelfTest = configuration.GetValue("CPAI_MODULE_DO_SELFTEST", false); @@ -127,6 +131,22 @@ protected virtual void InitModule() /// An object to serialize back to the server. protected abstract ModuleResponse ProcessRequest(BackendRequest request); + /// + /// Returns an object containing current stats for this module + /// + /// An object + protected virtual dynamic? Status() + { + dynamic status = new ExpandoObject(); + status.SuccessfulInferences = _successInferences; + status.FailedInferences = _failedInferences; + status.NumInferences = _successInferences + _failedInferences; + status.AverageInferenceMs = _successInferences > 0 + ? _totalSuccessInf_ms / _successInferences : 0; + + return status; + } + /// /// Called when the module is asked to execute a self-test to ensure it install and runs /// correctly @@ -188,6 +208,14 @@ private async Task ProcessQueue(CancellationToken token, int taskNumber) ModuleResponse response = ProcessRequest(request); stopWatch.Stop(); + if (response.Success) + { + _successInferences++; + _totalSuccessInf_ms += response.InferenceMs; + } + else + _failedInferences++; + long processMs = stopWatch.ElapsedMilliseconds; response.ModuleName = ModuleName; response.ModuleId = _moduleId; @@ -195,6 +223,7 @@ private async Task ProcessQueue(CancellationToken token, int taskNumber) response.ExecutionProvider = ExecutionProvider ?? string.Empty; response.Command = request.payload?.command ?? string.Empty; response.CanUseGPU = CanUseGPU; + response.StatusData = Status(); HttpContent content = JsonContent.Create(response, response.GetType()); diff --git a/src/SDK/NET/Common/Constants.cs b/src/SDK/NET/Common/Constants.cs new file mode 100644 index 00000000..9f03d34b --- /dev/null +++ b/src/SDK/NET/Common/Constants.cs @@ -0,0 +1,81 @@ +namespace CodeProject.AI.SDK.Common +{ + /// + /// Contains constants for CodeProject.AI Server + /// + public static class Constants + { + /// + /// The name of the company publishing this (will first check project file - it should be there) + /// + public const string Company = "CodeProject"; + + /// + /// The product name (will first check project file - it should be there) + /// + public const string ProductName = "CodeProject.AI Server"; + + /// + /// The category of product. Handy for placing this in a subdir of the main company dir. + /// + public const string ProductCategory = "AI"; + + /// + /// The default port the server listens on using HTTP + /// + public const int DefaultPort = 32168; + + /// + /// The default port the server listens on when using HTTP with SSL/TLS + /// + public const int DefaultPortSsl = 32016; + + /// + /// The legacy port (Deepstack compatibility) the server listens on + /// + public const int LegacyPort = 5000; + + /// + /// The legacy port the server listens on when running under macOS + /// + public const int LegacyPortOsx = 5500; + + /// + /// Used wherever there needs to be a distinction between release and dev. Typically used in + /// filenames + /// + public const string Development = "development"; + + /// + /// The name of the file containing the settings for the server + /// + public const string ServerSettingsFilename = "serversettings.json"; + + /// + /// The name of the file containing the settings for the server for the development environment + /// + public const string DevServerSettingsFilename = $"serversettings.{Development}.json"; + + /// + /// The name of the modules settings file without extension + /// + public const string ModulesSettingFilenameNoExt = "modulesettings"; + + /// + /// The name of the file containing the settings for a module, or a collection of modules + /// (This file can handle one or more sets of settings) + /// + public const string ModuleSettingsFilename = ModulesSettingFilenameNoExt + ".json"; + + /// + /// The name of the file containing the settings for a module, or a collection of modules, + /// for the development environment + /// + public const string DevModuleSettingsFilename = $"{ModulesSettingFilenameNoExt}.{Development}.json"; + + /// + /// The name of the file that stores the list of known modules + /// + public const string ModulesListingFilename = "modules.json"; + }; +} \ No newline at end of file diff --git a/src/SDK/NET/Common/ModuleBase.cs b/src/SDK/NET/Common/ModuleBase.cs index 121436f0..d4d2283b 100644 --- a/src/SDK/NET/Common/ModuleBase.cs +++ b/src/SDK/NET/Common/ModuleBase.cs @@ -40,37 +40,17 @@ public class ModuleRelease /// Basic module information shared between module listings for download, and module /// settings on installed modules. /// - public class ModuleBase + public class PublishingInfo { /// - /// Gets or sets the Id of the Module - /// - public string? ModuleId { get; set; } - - /// - /// Gets or sets the Name to be displayed. - /// - public string? Name { get; set; } - - /// - /// Gets or sets the platforms on which this module is supported. Options include: windows, - /// windows-arm64, linux, linux-arm64, macos, macos-arm64, raspberrypi, orangepi, jetson. - /// If any of these is preceded by a "!" then that platform is specifically not supported. - /// This allows options such as "linux-arm64, !jetson" to mean all Linux arm64 platforms - /// except NVIDIA Jetson. - /// - public string[] Platforms { get; set; } = Array.Empty(); - - /// - /// Gets or sets the number of MB of memory needed for this module to perform operations. - /// If null, then no checks done. + /// Gets or sets the Description for the module. /// - public int? RequiredMb { get; set; } + public string? Description { get; set; } /// - /// Gets or sets the Description for the module. + /// Gets or sets the URL of the icon for this module. /// - public string? Description { get; set; } + public string? IconURL { get; set; } /// /// Gets or sets the Category of this module. @@ -78,15 +58,9 @@ public class ModuleBase public string? Category { get; set; } /// - /// Gets or sets the version of this module + /// Gets or sets the tech stack that this module is based on. /// - public string? Version { get; set; } - - /// - /// Gets or sets the list of module versions and the server version that matches - /// each of these versions. - /// - public ModuleRelease[] ModuleReleases { get; set; } = Array.Empty(); + public string? Stack { get; set; } /// /// Gets or sets the current version. @@ -117,13 +91,72 @@ public class ModuleBase /// Gets or sets the URL of the project this module is based on /// public string? BasedOnUrl { get; set; } + } + + /// + /// The installation options / settings for this module + /// + public class InstallOptions + { + /// + /// Gets or sets the platforms on which this module is supported. Options include: windows, + /// windows-arm64, linux, linux-arm64, macos, macos-arm64, raspberrypi, orangepi, jetson. + /// If any of these is preceded by a "!" then that platform is specifically not supported. + /// This allows options such as "linux-arm64, !jetson" to mean all Linux arm64 platforms + /// except NVIDIA Jetson. + /// + public string[] Platforms { get; set; } = Array.Empty(); /// - /// Gets or sets a value indicating whether this module was pre-installed (eg Docker). - /// If the module was preinstalled, this value is true, otherwise false. + /// Gets or sets a value indicating whether this module was pre-installed (eg Docker). If + /// the module was preinstalled, this value is true, otherwise false. /// public bool PreInstalled { get; set; } = false; + /// + /// Gets or sets the list of module versions and the server version that matches each of + /// these versions. This determines whether the module can be installed on a given server. + /// + public ModuleRelease[] ModuleReleases { get; set; } = Array.Empty(); + } + + /// + /// Basic module information shared between module listings for download, and module + /// settings on installed modules. + /// + public class ModuleBase + { + /// + /// Gets or sets the Id of the Module + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string? ModuleId { get; set; } + + /// + /// Gets or sets the Name to be displayed. + /// + /// + [JsonPropertyOrder(1)] + public string? Name { get; set; } + + /// + /// Gets or sets the version of this module + /// + [JsonPropertyOrder(2)] + public string? Version { get; set; } + + /// + /// Gets or sets the publishing info for this module + /// + [JsonPropertyOrder(3)] + public PublishingInfo? PublishingInfo { get; set; } + + /// + /// Gets or sets the installation options / settings for this module + /// + [JsonPropertyOrder(6)] + public InstallOptions? InstallOptions { get; set; } + /// /// Gets or sets the absolute path to this module. /// @@ -141,14 +174,14 @@ public class ModuleBase /// Gets a value indicating whether or not this is a valid module that can actually be /// started. /// - /// true if this module has valid settings; false otherwise. + [JsonIgnore] public virtual bool Valid { get { return !string.IsNullOrWhiteSpace(ModuleId) && !string.IsNullOrWhiteSpace(Name) && - Platforms?.Length > 0; + InstallOptions?.Platforms?.Length > 0; } } @@ -158,7 +191,7 @@ public virtual bool Valid /// A string object public override string ToString() { - return $"{Name} ({ModuleId}) {Version} {License ?? ""}"; + return $"{Name} ({ModuleId ?? "not set"}) {Version} {PublishingInfo?.License ?? ""}"; } } @@ -186,9 +219,9 @@ public static bool IsCompatible(this ModuleBase module, string? currentServerVer // compatible with the current server? if (versionOK) { - if (module.ModuleReleases?.Any() ?? false) + if (module.InstallOptions?.ModuleReleases?.Any() ?? false) { - foreach (ModuleRelease release in module.ModuleReleases) + foreach (ModuleRelease release in module.InstallOptions.ModuleReleases) { if (release.ServerVersionRange is null || release.ServerVersionRange.Length < 2) continue; @@ -212,8 +245,8 @@ public static bool IsCompatible(this ModuleBase module, string? currentServerVer // Second check: Is this module included in available platforms? bool available = versionOK && - ( module!.Platforms!.Any(p => p.EqualsIgnoreCase("all")) || - module!.Platforms!.Any(p => p.EqualsIgnoreCase(SystemInfo.Platform)) ); + ( module!.InstallOptions!.Platforms!.Any(p => p.EqualsIgnoreCase("all")) || + module!.InstallOptions!.Platforms!.Any(p => p.EqualsIgnoreCase(SystemInfo.Platform)) ); // Third check. In the second check we've checked directly against the current platform. // For any non Pi, non-Jetson device, SystemInfo.Platform is windows, mac or linux, @@ -223,11 +256,11 @@ public static bool IsCompatible(this ModuleBase module, string? currentServerVer // architecture. If a module is not meant to work for a given OS/architecture then it // should include "!Platform" (eg !Jetson) in the Platforms list. if (!available && !SystemInfo.Platform.EqualsIgnoreCase(SystemInfo.OSAndArchitecture)) - available = module!.Platforms!.Any(p => p.EqualsIgnoreCase(SystemInfo.OSAndArchitecture)); + available = module!.InstallOptions!.Platforms!.Any(p => p.EqualsIgnoreCase(SystemInfo.OSAndArchitecture)); // Final check: is the module specifically excluded from the current platform? return available && - !module!.Platforms!.Any(p => p.EqualsIgnoreCase($"!{SystemInfo.Platform}")); + !module!.InstallOptions!.Platforms!.Any(p => p.EqualsIgnoreCase($"!{SystemInfo.Platform}")); } } } \ No newline at end of file diff --git a/src/SDK/NET/Common/ModuleDescription.cs b/src/SDK/NET/Common/ModuleDescription.cs index 3045ced2..cc710ae6 100644 --- a/src/SDK/NET/Common/ModuleDescription.cs +++ b/src/SDK/NET/Common/ModuleDescription.cs @@ -161,8 +161,7 @@ public static class ModuleDescriptionExtensions public static void Initialise(this ModuleDescription module, string currentServerVersion, string modulesDirPath, string preInstalledModulesDirPath) { - // Currently these are unused. There are here only as an experiment - if (module.PreInstalled) + if (module.InstallOptions!.PreInstalled) module.ModuleDirPath = Path.Combine(preInstalledModulesDirPath, module.ModuleId!); else module.ModuleDirPath = Path.Combine(modulesDirPath, module.ModuleId!); @@ -179,7 +178,7 @@ public static void Initialise(this ModuleDescription module, string currentServe private static void SetLatestCompatibleVersion(ModuleDescription module, string currentServerVersion) { - foreach (ModuleRelease release in module!.ModuleReleases!) + foreach (ModuleRelease release in module!.InstallOptions!.ModuleReleases!) { if (release.ServerVersionRange is null || release.ServerVersionRange.Length < 2) continue; diff --git a/src/SDK/NET/Common/ProcessStatus.cs b/src/SDK/NET/Common/ProcessStatus.cs index 52eee5c2..4a53fe9f 100644 --- a/src/SDK/NET/Common/ProcessStatus.cs +++ b/src/SDK/NET/Common/ProcessStatus.cs @@ -2,6 +2,8 @@ using System.Text; using System.Text.Json.Serialization; +using CodeProject.AI.SDK.API; + namespace CodeProject.AI.SDK { /// @@ -82,7 +84,7 @@ public enum ProcessStatusType /// public class ProcessStatus { - private int _processed; + private int _requestCount; /// /// Gets or sets the module Id @@ -119,10 +121,15 @@ public class ProcessStatus /// public ProcessStatusType Status { get; set; } = ProcessStatusType.Unknown; + /// + /// The status data as returned by the module + /// + public object? StatusData { get; set; } + /// /// Gets the number of requests processed /// - public int Processed { get => _processed; } + public int RequestCount { get => _requestCount; } /// /// Gets or sets the name of the hardware acceleration provider. @@ -139,6 +146,12 @@ public class ProcessStatus /// public bool? CanUseGPU { get; set; } = false; + /// + /// Gets or sets the menus to be displayed in the dashboard based on the current status of + /// this module + /// + public DashboardMenu[]? Menus { get; set; } + /// /// Gets or sets the human readable notes regarding how this process was installed. /// @@ -153,7 +166,16 @@ public class ProcessStatus /// Increments the number of requests processed by 1. /// /// the incremented value - public int IncrementProcessedCount() => Interlocked.Increment(ref _processed); + public int IncrementRequestCount() => Interlocked.Increment(ref _requestCount); + + /// + /// The string representation of this module + /// + /// A string object + public override string ToString() + { + return $"{Name} ({ModuleId}) {Status}"; + } /// /// Gets a human readable summary of the process running the given module @@ -172,10 +194,11 @@ public string Summary string lastSeen = (LastSeen is null) ? "Not seen" : LastSeen.Value.ToLocalTime().ToString(format) + " " + timezone; + summary.AppendLine($"Status Data: {StatusData}"); summary.AppendLine($"Started: {started}"); summary.AppendLine($"LastSeen: {lastSeen}"); summary.AppendLine($"Status: {Status}"); - summary.AppendLine($"Processed: {Processed}"); + summary.AppendLine($"Requests: {RequestCount} (includes status calls)"); summary.AppendLine($"Provider: {ExecutionProvider}"); summary.AppendLine($"CanUseGPU: {CanUseGPU}"); summary.AppendLine($"HardwareType: {HardwareType}"); diff --git a/src/SDK/NET/Common/SystemInfo.cs b/src/SDK/NET/Common/SystemInfo.cs index 7e9413cf..d47fb8dc 100644 --- a/src/SDK/NET/Common/SystemInfo.cs +++ b/src/SDK/NET/Common/SystemInfo.cs @@ -288,8 +288,8 @@ public static RuntimeEnvironment RuntimeEnvironment // reference to IHostEnvironment. // We also can't use these because if VSCode is running, these are set. This // messes up production installs. - // || Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT").EqualsIgnoreCase("Development") || - // || Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT").EqualsIgnoreCase("Development") + // || Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT").EqualsIgnoreCase(Constants.Development) || + // || Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT").EqualsIgnoreCase(Constants.Development) ) { return RuntimeEnvironment.Development; @@ -1167,7 +1167,7 @@ private static async Task MonitorSystemUsageAsync() catch (Exception ex) { _hasNvidiaCard = false; - Debug.WriteLine(ex.ToString()); + Debug.WriteLine("Error getting Jetson HW status: " + ex.ToString()); return null; } @@ -1716,7 +1716,7 @@ private static List GetCounters(string category, string metr #if DEBUG catch (Exception e) { - Console.WriteLine(e); + Console.WriteLine("Error in GetProcessInfoAsync: " + e.Message); return null; } #else diff --git a/src/SDK/Python/module_options.py b/src/SDK/Python/module_options.py index 0fd6a40c..2b49baee 100644 --- a/src/SDK/Python/module_options.py +++ b/src/SDK/Python/module_options.py @@ -6,6 +6,7 @@ def _get_env_var(name: str, default: any = "") -> any: value = os.getenv(name, "") + # print(f"_get_env_var: {name} = {value}. default = {default}") if value == "" and default != "": value = default # print(f"Debug: {name} not found. Setting to default {str(default)}") @@ -100,7 +101,9 @@ def getEnvVariable(name: str, default: any = "") -> any: if isinstance(log_verbosity, str): log_verbosity = LogVerbosity(log_verbosity.lower()) elif not isinstance(log_verbosity, LogVerbosity): - log_verbosity = LogVerbosity.Info + log_verbosity = LogVerbosity.Quiet + if not log_verbosity: + log_verbosity = LogVerbosity.Quiet parallelism = int(parallelism) if isinstance(parallelism, int) else 0 if parallelism <= 0: diff --git a/src/SDK/Python/module_runner.py b/src/SDK/Python/module_runner.py index 3bba013d..0926501f 100644 --- a/src/SDK/Python/module_runner.py +++ b/src/SDK/Python/module_runner.py @@ -3,7 +3,6 @@ import json import os import platform -from platform import uname import sys import time import traceback @@ -14,7 +13,7 @@ warnings.simplefilter("ignore", DeprecationWarning) # Some commands are just annoying in the logs -ignore_timing_commands = [ "list-custom" ] +ignore_timing_commands = [ "list-custom", "status" ] # Ensure the Python import system looks in the right spot for packages. # ie .../pythonXX/venv/Lib/site-packages. This depends on the VENV we're @@ -36,7 +35,7 @@ # Import the CodeProject.AI SDK as the last step from common import JSON from system_info import SystemInfo -from module_logging import LogMethod, ModuleLogger +from module_logging import LogMethod, ModuleLogger, LogVerbosity from request_data import RequestData from module_options import ModuleOptions # from utils.environment_check import check_requirements @@ -57,6 +56,13 @@ async def initialise(self) -> None: """ pass + async def initialize(self) -> None: + """ + Called when this module first starts up. To be overridden by child classes. + This method provided for those who like month-day-year. + """ + pass + async def process(self, data: RequestData) -> JSON: """ Called each time a request is retrieved from this module's queue is to @@ -117,41 +123,39 @@ def __init__(self) -> None: # Public fields ------------------------------------------------------- # A note about the use of ModuleOptions. ModuleOptions is simply a way - # to hide all the calls to _get_env_var behind a simple class. While + # to hide all the calls to _get_env_var behind a single class. While # there is a lot of repetition in self.property = ModuleOptions.property, # it means we have the means of keeping the initial values the module # had at launch separate from the working values which may change during - # runtime. It's tempting to remove all values that ModuleOptions supplies, - # and instead just use ModuleRunner.ModuleOptions.property, but many - # properties such as module_id are intrinsic to this module, and exposing - # a ModuleOptions property exposes too much information on the internals - # of this class. + # runtime. ie we prefer to keep a record of the initial settings in the + # ModuleOptions class, and have the transferred values in this class be + # mutable. # Module Descriptors - self.module_id = ModuleOptions.module_id - self.module_name = ModuleOptions.module_name + self.module_id = ModuleOptions.module_id # ID of the module + self.module_name = ModuleOptions.module_name # Name of the module # Server API location and Queue - self.base_api_url = ModuleOptions.base_api_url - self.port = ModuleOptions.port - self.queue_name = ModuleOptions.queue_name + self.base_api_url = ModuleOptions.base_api_url # Base URL for making calls to the server + self.port = ModuleOptions.port # Port on which the server is listening + self.queue_name = ModuleOptions.queue_name # Name of request queue for this module # General Module and Server settings - self.server_root_path = ModuleOptions.server_root_path - self.module_path = ModuleOptions.module_path - self.python_dir = current_python_dir - self.python_pkgs_dir = package_path - self.log_verbosity = ModuleOptions.log_verbosity - self.launched_by_server = ModuleOptions.launched_by_server + self.server_root_path = ModuleOptions.server_root_path # Absolute folder path to root of this application + self.module_path = ModuleOptions.module_path # Absolute folder path to this module + self.python_dir = current_python_dir # Absolute folder path to Python venv if applicable + self.python_pkgs_dir = package_path # Absolute folder path to Python venv packages folder if app. + self.log_verbosity = ModuleOptions.log_verbosity # Logging level: Quiet, Info or Loud + self.launched_by_server = ModuleOptions.launched_by_server # Was this module launched by the server (or launched separately?) # Hardware / accelerator info - self.required_MB = int(ModuleOptions.required_MB or 0) - self.enable_GPU = ModuleOptions.enable_GPU - self.accel_device_name = ModuleOptions.accel_device_name - self.half_precision = ModuleOptions.half_precision - self.parallelism = ModuleOptions.parallelism - self.processor_type = "CPU" # may be overridden by the module - self.can_use_GPU = False # Whether or not this module provides GPU support for the current hardware + self.required_MB = int(ModuleOptions.required_MB or 0) # Min RAM needed to launch this module + self.accel_device_name = ModuleOptions.accel_device_name # eg CUDA:0, usb:0. Module/library specific + self.half_precision = ModuleOptions.half_precision # Whether to use half precision in CUDA (module specific) + self.parallelism = ModuleOptions.parallelism # Number of parallel instances launched at runtime + self.enable_GPU = ModuleOptions.enable_GPU # Whether to use GPU support if available + self.processor_type = "CPU" # The processor type reported as being used (CPU, GPU, TPU etc) + self.can_use_GPU = False # Whether this module can support the current hardware # General purpose flags. These aren't currently supported as common flags # self.use_CUDA = ModuleOptions.use_CUDA @@ -165,11 +169,15 @@ def __init__(self) -> None: # self._logger = ModuleLogger(self.port, self.server_root_path) self._logger = None + # Private fields + self._base_queue_url = self.base_api_url + "queue/" + + # General setup + + # Do this now in case we forget to do it later if self.enable_GPU and self.system_info.hasTorchMPS: os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1" - # Private fields - self._base_queue_url = self.base_api_url + "queue/" @property def execution_provider(self) -> str: @@ -211,20 +219,33 @@ def start_loop(self) -> None: # just fragile. if len(sys.argv) > 1 and sys.argv[1] == "--selftest": + + if self.log_verbosity == LogVerbosity.Loud: + print(f"{self.module_id} self-test called") + self._logger = ModuleLogger(self.port, self.server_root_path) self._performing_self_test = True - self.initialise() + # We allow 'initialize' and 'initialise'. Find which was overridden + if self.initialize.__qualname__ == "ModuleRunner.initialize": + self.initialise() + else: + self.initialize() + if self.selftest_check_packages: self.check_packages() result = self.selftest() self.cleanup() self._performing_self_test = False - if result and hasattr(result, "success") and not result["success"]: - quit(1) - else: + if result and "success" in result and result["success"]: + if self.log_verbosity == LogVerbosity.Loud: + print(f"{self.module_id} self-test succeeded") quit(0) + else: + if self.log_verbosity == LogVerbosity.Loud: + print(f"{self.module_id} self-test failed") + quit(1) # No self test, so on to the main show @@ -250,7 +271,7 @@ def start_loop(self) -> None: "filename": __file__, "method": sys._getframe().f_code.co_name, "loglevel": "error", - "message": message, + "message": "Error running main_init: " + message, "exception_type": ex.__class__.__name__ }) @@ -285,9 +306,16 @@ async def main_init(self): This method also sets up the shared logging task. """ + + if self.log_verbosity == LogVerbosity.Loud: + print(f"{self.module_id} starting main_init") + async with aiohttp.ClientSession() as session: self._request_session = session + if self.log_verbosity == LogVerbosity.Loud: + print(f"{self.module_id} starting logging_loop") + # Start with just running one logging loop self._logger = ModuleLogger(self.port, self.server_root_path) logging_task = asyncio.create_task(self._logger.logging_loop()) @@ -304,22 +332,38 @@ async def main_init(self): # Overriding issue here: We need to await self.initialise in the # asyncio loop. This means we can't just 'await self.initialise' + # We also allow initialise or initialize, so let's see which one + # has been overridden + + # We allow 'initialize' and 'initialise'. Find which was overridden + init_method = self.initialize + if self.initialize.__qualname__ == "ModuleRunner.initialize": + init_method = self.initialise + + if self.log_verbosity == LogVerbosity.Loud: + print(f"{self.module_id} call module's init method") - if asyncio.iscoroutinefunction(self.initialise): + if asyncio.iscoroutinefunction(init_method): # if initialise is async, then it's a coroutine. In this # case we create an awaitable asyncio task to execute this # method. - init_task = asyncio.create_task(self.initialise()) + init_task = asyncio.create_task(init_method()) else: # If the method is not async, then we wrap it in an awaitable # method which we await. loop = asyncio.get_running_loop() - init_task = loop.run_in_executor(None, self.initialise) + init_task = loop.run_in_executor(None, init_method) await init_task + if self.log_verbosity == LogVerbosity.Loud: + print(f"{self.module_id} module init complete") + sys.stdout.flush() + if self.log_verbosity == LogVerbosity.Loud: + print(f"{self.module_id} setting up main loop") + # Add main processing loop tasks tasks = [ asyncio.create_task(self.main_loop(task_id)) for task_id in range(self.parallelism) ] @@ -334,6 +378,10 @@ async def main_init(self): }) await asyncio.gather(*tasks) + + if self.log_verbosity == LogVerbosity.Loud: + print(f"{self.module_id} all tasks complete. Ending.") + self._request_session = None @@ -351,6 +399,9 @@ async def main_loop(self, task_id) -> None: carefully. """ + if self.log_verbosity == LogVerbosity.Loud: + print(f"{self.module_id} starting main_loop {task_id}") + get_command_task = asyncio.create_task(self.get_command(task_id)) send_response_task = None @@ -378,8 +429,15 @@ async def main_loop(self, task_id) -> None: if data.command: command = data.command.lower() + if self.log_verbosity == LogVerbosity.Loud: + print(f"{self.module_id} command {command} pulled from queue for task {task_id}") + # Special requests if command == "quit" and self.module_id.lower() == data.get_value("moduleId").lower(): + + if self.log_verbosity == LogVerbosity.Loud: + print(f"{self.module_id} 'quit' called. Signaling shutdown for task {task_id}") + await self.log_async(LogMethod.Info | LogMethod.File | LogMethod.Server, { "process": self.module_name, "filename": __file__, @@ -390,9 +448,8 @@ async def main_loop(self, task_id) -> None: self._cancelled = True break - elif data.command.lower() == "status": + elif command == "status": method_to_call = self.status - suppress_timing_log = True elif command == "selftest": # NOTE: selftest generally won't actually be called here - it'll be called @@ -415,6 +472,9 @@ async def main_loop(self, task_id) -> None: # Overriding issue here: We need to await self.process in the # asyncio loop. This means we can't just 'await self.process' + if self.log_verbosity == LogVerbosity.Loud: + print(f"{self.module_id} calling process with '{command}' for task {task_id}") + if asyncio.iscoroutinefunction(method_to_call): # if process is async, then it's a coroutine. In this # case we create an awaitable asyncio task to execute @@ -429,6 +489,9 @@ async def main_loop(self, task_id) -> None: # Await output = await callbacktask + if self.log_verbosity == LogVerbosity.Loud: + print(f"{self.module_id} process call complete for task {task_id}") + # print(f"Process Response is {output['message']}") except asyncio.CancelledError: @@ -446,7 +509,7 @@ async def main_loop(self, task_id) -> None: "filename": __file__, "method": sys._getframe().f_code.co_name, "loglevel": "error", - "message": message, + "message": "Error during main_loop: " + message, "exception_type": ex.__class__.__name__ }) @@ -456,7 +519,8 @@ async def main_loop(self, task_id) -> None: try: if send_response_task != None: - # print("awaiting old send task") + if self.log_verbosity == LogVerbosity.Loud: + print(f"{self.module_id} awaiting previous send operation for task {task_id}") await send_response_task output["moduleId"] = self.module_id @@ -465,19 +529,28 @@ async def main_loop(self, task_id) -> None: output["command"] = data.command or '' output["executionProvider"] = self.execution_provider or 'CPU' output["canUseGPU"] = self.can_use_GPU + output["statusData"] = self.status(data) - # print("creating new send task") + if self.log_verbosity == LogVerbosity.Loud: + print(f"{self.module_id} sending result of process to server for task {task_id}") + send_response_task = asyncio.create_task(self.send_response(data.request_id, output)) - except Exception: - print(f"An exception occurred sending the inference response (#reqid {data.request_id})") + except Exception as ex: + print(f"An exception occurred sending the inference response (#reqid {data.request_id}): {str(ex)}") + if self.log_verbosity == LogVerbosity.Loud: + print(f"{self.module_id} completed. Cleaning up task {task_id}") + # Cleanup self.cleanup() # method is ending. Let's clean up. self._cancelled == True at this point. self._logger.cancel_logging() + if self.log_verbosity == LogVerbosity.Loud: + print(f"{self.module_id} task {task_id} complete.") + # Performance timer ======================================================= @@ -551,6 +624,9 @@ async def get_command(self, task_id) -> "list[str]": """ commands = [] + if self.log_verbosity == LogVerbosity.Loud: + print(f"{self.module_id} in get_command for task {task_id}") + try: url = self._base_queue_url + self.queue_name + "?moduleId=" + self.module_id if self.execution_provider: @@ -603,15 +679,18 @@ async def get_command(self, task_id) -> "list[str]": if self._current_error_pause_secs \ else self._error_pause_secs - except TimeoutError: + except asyncio.TimeoutError as t_ex: + # We long-poll the server to get commands, so timeouts are how we roll. + pass + """ if not self._cancelled: await self.log_async(LogMethod.Error | LogMethod.Server, { "message": f"Timeout retrieving command from queue {self.queue_name}", "method": sys._getframe().f_code.co_name, "loglevel": "error", - "process": self.queue_name, + "process": 'get_command', "filename": __file__, - "exception_type": ex.__class__.__name__ + "exception_type": t_ex.__class__.__name__ }) # We'll only calculate the error pause time in task #0, but all @@ -620,8 +699,9 @@ async def get_command(self, task_id) -> "list[str]": self._current_error_pause_secs = self._current_error_pause_secs * 2 \ if self._current_error_pause_secs \ else self._error_pause_secs + """ - except ConnectionRefusedError: + except ConnectionRefusedError as c_ex: if not self._cancelled: await self.log_async(LogMethod.Error, { "message": f"Connection refused trying to check the command queue {self.queue_name}.", @@ -629,7 +709,7 @@ async def get_command(self, task_id) -> "list[str]": "loglevel": "error", "process": self.queue_name, "filename": __file__, - "exception_type": ex.__class__.__name__ + "exception_type": c_ex.__class__.__name__ }) # We'll only calculate the error pause time in task #0, but all @@ -671,7 +751,7 @@ async def get_command(self, task_id) -> "list[str]": pause_on_error = True else: - err_msg = str(ex) + err_msg = "Error in get_command: " + str(ex) if not self._cancelled and err_msg and err_msg != 'Session is closed': pause_on_error = True @@ -742,6 +822,14 @@ async def send_response(self, request_id : str, body : JSON) -> bool: success = True # print("Response sent") + except asyncio.TimeoutError as t_ex: + await asyncio.sleep(self._error_pause_secs) + + if self._verbose_exceptions: + print(f"Timeout sending response: {str(t_ex)}") + else: + print(f"Timeout sending response: Is the API Server running? [" + t_ex.__class__.__name__ + "]") + except Exception as ex: await asyncio.sleep(self._error_pause_secs) diff --git a/src/SDK/Python/request_data.py b/src/SDK/Python/request_data.py index 2dbe44e3..8fc6b294 100644 --- a/src/SDK/Python/request_data.py +++ b/src/SDK/Python/request_data.py @@ -169,7 +169,7 @@ def get_image(self, index : int) -> Image: """ err_msg = "Unable to get image from request" if self._verbose_exceptions: - err_msg = str(ex) + err_msg = "Error in get_image: " + str(ex) self.log(LogMethod.Error|LogMethod.Server, { "message": err_msg, diff --git a/src/SDK/Scripts/install_cuDNN.sh b/src/SDK/Scripts/install_cuDNN.sh index 2ea5c0f6..c2c74aff 100644 --- a/src/SDK/Scripts/install_cuDNN.sh +++ b/src/SDK/Scripts/install_cuDNN.sh @@ -29,7 +29,7 @@ # # - Using signing key # wget https://developer.download.nvidia.com/compute/cuda/repos/${OS_name}/${arch}/cuda-${OS_name}.pin -# sudo mv -f cuda-${OS_name}.pin /etc/apt/preferences.d/cuda-repository-pin-600 +# sudo mv cuda-${OS_name}.pin /etc/apt/preferences.d/cuda-repository-pin-600 # sudo apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/${OS_name}/${arch}/${key}.pub # sudo add-apt-repository "deb https://developer.download.nvidia.com/compute/cuda/repos/${OS_name}/${arch}/ /" # @@ -208,12 +208,12 @@ spin $! writeLine "Done" $color_success # wget https://developer.download.nvidia.com/compute/cuda/repos///cuda-archive-keyring.gpg -# sudo mv -f cuda-archive-keyring.gpg /usr/share/keyrings/cuda-archive-keyring.gpg +# sudo mv cuda-archive-keyring.gpg /usr/share/keyrings/cuda-archive-keyring.gpg # # echo "deb [signed-by=/usr/share/keyrings/cuda-archive-keyring.gpg] https://developer.download.nvidia.com/compute/cuda/repos/// /" | sudo tee /etc/apt/sources.list.d/cuda--.list # # wget https://developer.download.nvidia.com/compute/cuda/repos///cuda-.pin -# sudo mv -f cuda-.pin /etc/apt/preferences.d/cuda-repository-pin-600 +# sudo mv cuda-.pin /etc/apt/preferences.d/cuda-repository-pin-600 # install the cuDNN library write " - Installing cuDNN libraries..." $color_mute diff --git a/src/SDK/Scripts/utils.sh b/src/SDK/Scripts/utils.sh index 5af034d1..435fbc50 100644 --- a/src/SDK/Scripts/utils.sh +++ b/src/SDK/Scripts/utils.sh @@ -342,6 +342,7 @@ function checkForAdminRights () { if [ "$isAdmin" = false ] && [ "$requestPassword" = true ]; then if [ "$os" == "macos" ]; then + # THIS DOES NOT WORK # This shows the password prompt, but the admin rights starts and ends with the "whoami" # call. Once that call finishes, admin rights no longer apply. /usr/bin/osascript -e 'do shell script "whoami 2>&1" with administrator privileges' @@ -1848,7 +1849,7 @@ function getFromServer () { move_recursive() { if [ ! -d "$1" ] || [ ! -e "$2" ]; then - mv -f "$1" "$2" || exit + mv "$1" "$2" || exit return fi for entry in "$1/"* "$1/."[!.]* "$1/.."?*; do diff --git a/src/SDK/install.sh b/src/SDK/install.sh index 86e30cec..def153c4 100644 --- a/src/SDK/install.sh +++ b/src/SDK/install.sh @@ -136,7 +136,7 @@ fi if [ "${useJq}" = false ]; then dotnet build "${sdkPath}/Utilities/ParseJSON" /property:GenerateFullPaths=true /consoleloggerparameters:NoSummary -c Release > /dev/null 2>&1 pushd "${sdkPath}/Utilities/ParseJSON/bin/Release/net7.0/" >/dev/null - mv -f ./* ../../../ + mv ./* ../../../ popd >/dev/null fi diff --git a/src/create_packages.bat b/src/create_packages.bat index 7a8c4879..7d88482a 100644 --- a/src/create_packages.bat +++ b/src/create_packages.bat @@ -138,8 +138,8 @@ set success=true pushd SDK\Utilities\ParseJSON if not exist ParseJSON.exe ( - dotnet build /property:GenerateFullPaths=true /consoleloggerparameters:NoSummary -c Release - if exist .\bin\Release\net7.0\ move .\bin\Release\net7.0\* . > /dev/null + dotnet build /property:GenerateFullPaths=true /consoleloggerparameters:NoSummary -c Release >NUL + if exist .\bin\Release\net7.0\ move .\bin\Release\net7.0\* . >nul ) popd diff --git a/src/modules/ALPR-RKNN/ALPR_adapter.py b/src/modules/ALPR-RKNN/ALPR_adapter.py index b93810cc..09f0478e 100644 --- a/src/modules/ALPR-RKNN/ALPR_adapter.py +++ b/src/modules/ALPR-RKNN/ALPR_adapter.py @@ -34,6 +34,11 @@ def initialise(self) -> None: init_detect_platenumber(self.opts) + self.success_inferences = 0 + self.total_success_inf_ms = 0 + self.failed_inferences = 0 + self.num_items_found = 0 + def process(self, data: RequestData) -> JSON: @@ -44,9 +49,10 @@ def process(self, data: RequestData) -> JSON: result = detect_platenumber(self, self.opts, image) # result = detect_platenumber(self, self.opts, image) - if "error" in result and result["error"]: - return { "success": False, "error": result["error"] } + response = { "success": False, "error": result["error"] } + self._update_statistics(response) + return response predictions = result["predictions"] if len(predictions) > 3: @@ -56,7 +62,7 @@ def process(self, data: RequestData) -> JSON: else: message = "No plates found" - return { + response = { "success": True, "predictions": predictions, "message": message, @@ -64,8 +70,38 @@ def process(self, data: RequestData) -> JSON: "inferenceMs" : result["inferenceMs"] } + self._update_statistics(response) + return response + + + def status(self, data: RequestData = None) -> JSON: + return { + "successfulInferences" : self.success_inferences, + "failedInferences" : self.failed_inferences, + "numInferences" : self.success_inferences + self.failed_inferences, + "numItemsFound" : self.num_items_found, + "averageInferenceMs" : 0 if not self.success_inferences + else self.total_success_inf_ms / self.success_inferences, + } + + def cleanup(self) -> None: pass + + def _update_statistics(self, response): + + if "success" in response and response["success"]: + if "predictions" in response: + if "inferenceMs" in response: + self.total_success_inf_ms += response["inferenceMs"] + self.success_inferences += 1 + predictions = response["predictions"] + self.num_items_found += len(predictions) + else: + self.failed_inferences += 1 + + + if __name__ == "__main__": ALPR_adapter().start_loop() diff --git a/src/modules/ALPR-RKNN/explore.html b/src/modules/ALPR-RKNN/explore.html index cbe7b1fa..8087b083 100644 --- a/src/modules/ALPR-RKNN/explore.html +++ b/src/modules/ALPR-RKNN/explore.html @@ -65,6 +65,7 @@ + - - - - - -
-

Portrait Filter Test Page

-
- -
-
- - -
-
- - -
-
-
-
- -
-
- -
-
- -
- - -
-
- - -
-
- - - - - \ No newline at end of file diff --git a/src/modules/SceneClassifier/explore.html b/src/modules/SceneClassifier/explore.html index 1b2895eb..73f8a5e0 100644 --- a/src/modules/SceneClassifier/explore.html +++ b/src/modules/SceneClassifier/explore.html @@ -69,6 +69,7 @@ +