From 467ddb80c81a002a5adae9bde5924a83373ea264 Mon Sep 17 00:00:00 2001 From: Doug Schmidt Date: Wed, 9 Feb 2022 09:46:38 -0800 Subject: [PATCH 1/2] PF-1365 - Minor tweaks 1) Ensure only one instance-per-config runs per computer. This allows you to schedule a configuration on a 3-hour schedule which sometimes takes 4 hours to complete, and avoid kicking off another cycle before the first one is actually done. 2) Log the number of exported observations per location 3) Documented the basic workflow and scheduling options --- .../ObservationReportExporter/Exporter.cs | 22 ++++-- .../ObservationReportExporter.csproj | 1 + .../ObservationReportExporter/Program.cs | 35 ++++++++- .../ObservationReportExporter/Readme.md | 32 ++++++++ .../SingleInstanceGuard.cs | 73 +++++++++++++++++++ 5 files changed, 154 insertions(+), 9 deletions(-) create mode 100644 Samples/DotNetSdk/ObservationReportExporter/SingleInstanceGuard.cs diff --git a/Samples/DotNetSdk/ObservationReportExporter/Exporter.cs b/Samples/DotNetSdk/ObservationReportExporter/Exporter.cs index 8ea5093..285e861 100644 --- a/Samples/DotNetSdk/ObservationReportExporter/Exporter.cs +++ b/Samples/DotNetSdk/ObservationReportExporter/Exporter.cs @@ -39,6 +39,7 @@ public class Exporter private List AppliedTags { get; } = new List(); private int ExportedLocations { get; set; } + private int ExportedObservations { get; set; } private int SkippedLocations { get; set; } private int Errors { get; set; } private int DeletedAttachments { get; set; } @@ -59,7 +60,7 @@ public void Run() ExportAllLocations(); } - LogAction($"Exported observations to {"location".ToQuantity(ExportedLocations)}, skipping {"unknown location".ToQuantity(SkippedLocations)}, deleting {"existing attachment".ToQuantity(DeletedAttachments)}, with {"detected error".ToQuantity(Errors)} in {stopwatch.Elapsed.Humanize()}."); + LogAction($"Exported {"observation".ToQuantity(ExportedObservations)} using '{ExportTemplate.CustomId}' template into {"location".ToQuantity(ExportedLocations)}, skipping {"unknown location".ToQuantity(SkippedLocations)}, deleting {"existing attachment".ToQuantity(DeletedAttachments)}, with {"detected error".ToQuantity(Errors)} in {stopwatch.Elapsed.Humanize()}."); } private void LogAction(string message) @@ -428,10 +429,6 @@ private void ExportLocation(SamplingLocation location) DeleteExistingAttachment(locationData, existingAttachment); } - LogAction($"Exporting observations from '{location.CustomId}' ..."); - - ++ExportedLocations; - var exportRequest = new GetExportObservations { EndObservedTime = FromDateTimeOffset(Context.EndTime), @@ -441,6 +438,21 @@ private void ExportLocation(SamplingLocation location) AnalyticalGroupIds = AnalyticalGroupIds, }; + var exportedObservationCount = Samples.Get(new GetObservationsV2 + { + EndObservedTime = exportRequest.EndObservedTime, + StartObservedTime = exportRequest.StartObservedTime, + SamplingLocationIds = exportRequest.SamplingLocationIds, + ObservedPropertyIds = exportRequest.ObservedPropertyIds, + AnalyticalGroupIds = exportRequest.AnalyticalGroupIds + }).TotalCount; + + LogAction($"Exporting {"observation".ToQuantity(exportedObservationCount)} from '{location.CustomId}' ..."); + + ++ExportedLocations; + ExportedObservations += exportedObservationCount; + + // Need to hack the URL until WI-4928 is fixed var url = $"{(Samples.Client as JsonServiceClient)?.BaseUri}{exportRequest.ToGetUrl()}&observationTemplateAttachmentId={ExportTemplate.Attachments.Single().Id}&format=xlsx"; if (Context.DryRun) diff --git a/Samples/DotNetSdk/ObservationReportExporter/ObservationReportExporter.csproj b/Samples/DotNetSdk/ObservationReportExporter/ObservationReportExporter.csproj index 2d6f47b..5c3010c 100644 --- a/Samples/DotNetSdk/ObservationReportExporter/ObservationReportExporter.csproj +++ b/Samples/DotNetSdk/ObservationReportExporter/ObservationReportExporter.csproj @@ -248,6 +248,7 @@ + diff --git a/Samples/DotNetSdk/ObservationReportExporter/Program.cs b/Samples/DotNetSdk/ObservationReportExporter/Program.cs index a4e9022..46fec49 100644 --- a/Samples/DotNetSdk/ObservationReportExporter/Program.cs +++ b/Samples/DotNetSdk/ObservationReportExporter/Program.cs @@ -3,6 +3,8 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Security.Cryptography; +using System.Text; using System.Text.RegularExpressions; using System.Xml; using Aquarius.Samples.Client; @@ -64,8 +66,7 @@ private static void ConfigureLogging() log4net.Config.XmlConfigurator.Configure(xml.DocumentElement); - // ReSharper disable once PossibleNullReferenceException - _log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + _log = LogManager.GetLogger(GetProgramType()); ServiceStack.Logging.LogManager.LogFactory = new Log4NetFactory(); } @@ -73,8 +74,7 @@ private static void ConfigureLogging() private static byte[] LoadEmbeddedResource(string path) { - // ReSharper disable once PossibleNullReferenceException - var resourceName = $"{MethodBase.GetCurrentMethod().DeclaringType.Namespace}.{path}"; + var resourceName = $"{GetProgramType().Namespace}.{path}"; using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName)) { @@ -85,6 +85,11 @@ private static byte[] LoadEmbeddedResource(string path) } } + private static Type GetProgramType() + { + return MethodBase.GetCurrentMethod()?.DeclaringType; + } + private static Context ParseArgs(string[] args) { var context = new Context(); @@ -349,11 +354,33 @@ public Program(Context context) private void Run() { + using (var guard = new SingleInstanceGuard(GetContextName())) + { + if (!guard.IsAnotherInstanceRunning()) + { + new Exporter { Context = _context } + .Run(); + } + else + { + _log.Warn($"Exiting while another instance of {guard.Name} is running."); + } + } new Exporter { Context = _context } .Run(); } + + private string GetContextName() + { + var jsonText = _context.ToJson(); + + using (var sha256 = new SHA256Managed()) + { + return BitConverter.ToString(sha256.ComputeHash(Encoding.UTF8.GetBytes(jsonText))); + } + } } } diff --git a/Samples/DotNetSdk/ObservationReportExporter/Readme.md b/Samples/DotNetSdk/ObservationReportExporter/Readme.md index 81327e2..cc3cfe0 100644 --- a/Samples/DotNetSdk/ObservationReportExporter/Readme.md +++ b/Samples/DotNetSdk/ObservationReportExporter/Readme.md @@ -12,12 +12,28 @@ The intent is to run this tool on a schedule so that the uploaded location attac - Results can be filtered by optional date range, location, location group, property, or analytical group. - All filters are cumulative (ie. they are AND-ed together). The more filters you add, the fewer results will be exported. - An exit code of 0 indicates the export was successful. An exit code greater than 0 indicates an error. This allows for easy error checking when invoked from scripts. +- A log file named `ObservationReportExporter.log` will be created in the same folder as the EXE. ## Requirements - The .NET 4.7 runtime is required, which is pre-installed on all Windows 10 and Windows Server 2016 systems, and on nearly all up-to-date Windows 7 and Windows Server 2008 systems. - No installer is needed. It is just a single .EXE which can be run from any folder. +## Basic workflow + +Each run of the tool will: +- Validate all the provided options and stop right away if any errors are detected. +- Determine the AQUARIUS Samples locations from which observations will be exported. This is either the list of `-LocationId=` options locations or locations belonging to the `-LocationGroupId=` options. +- For each exported location: + - Find the matching AQUARIUS Time Series location. + - If the Time Series location is not found: + - Log a warning and move on to the next export. + - Otherwise: + - Determine the exported attachment filename (see [controlling the attachment filename](#controlling-the-aqts-attachment-filename) for details). + - Delete any existing location attachments with the same filename. + - Export all the filtered observations from the AQUARIUS Samples location using the named template. + - Upload the exported spreadsheet of location-specific observations to the AQUARIUS Time-Series location. + ### Command line option syntax All command line options are case-insensitive, and support both common shell syntaxes: either `/Name=value` (for CMD.EXE) or `-Name=value` (for bash and PowerShell). @@ -26,6 +42,22 @@ In addition, the [`@options.txt` syntax](https://github.com/AquaticInformatics/e Try the `/help` option for a detailed list of options and their default values. +## Running the tool on a periodic schedule + +The `ObservationReportExporter.exe` too can be run from Windows Task Scheduler, or other scheduling software. + +Typical configuration involves: +- Storing all required command-line options in a single text file, in the same folder as the EXE. +- Specifying the executable as full path to the `ObservationReportExporter.exe` tool. +- Setting the working directory to the folder containing the EXE. +- Setting the arguments to the `@Options.txt` file containing all the required options. + +No special Windows account is required to run the tool. All the required credentials are supplied as command-line options, so it is fine to run the tool using a built-in Windows account like LocalSystem (NT_AUTHORITY/SYSTEM). + +The tool can be scheduled to run at whatever frequency you would like. The tool will quickly exit if it detects an identical export request already in progress. This allows for simple scheduling for normal loads. + +If a complete export cycle normally takes 2 hours, but can sometimes take 4 hours to complete, you can still safely schedule the tool to run every 3 hours, and tool will detect when a previous cycle hasn't completed and won't go crazy. + ## Authentication with AQUARIUS Samples Two options are required to tell the tool how to access your AQUARIUS Samples instance. diff --git a/Samples/DotNetSdk/ObservationReportExporter/SingleInstanceGuard.cs b/Samples/DotNetSdk/ObservationReportExporter/SingleInstanceGuard.cs new file mode 100644 index 0000000..b46efd1 --- /dev/null +++ b/Samples/DotNetSdk/ObservationReportExporter/SingleInstanceGuard.cs @@ -0,0 +1,73 @@ +using System; +using System.Reflection; +using System.Threading; +using log4net; + +namespace ObservationReportExporter +{ + public class SingleInstanceGuard : IDisposable + { + // ReSharper disable once PossibleNullReferenceException + private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + private Mutex InstanceMutex { get; set; } + public string Name { get; } + private bool ShouldRelease { get; set; } + + public SingleInstanceGuard(string name) + { + Name = $"{ExeHelper.ExeName}.{name}"; + InstanceMutex = new Mutex(true, Name); + ShouldRelease = true; + } + + public bool IsAnotherInstanceRunning() + { + try + { + var isAnotherInstanceRunning = !InstanceMutex.WaitOne(TimeSpan.Zero, true); + + if (isAnotherInstanceRunning) + { + ShouldRelease = false; + } + + return isAnotherInstanceRunning; + } + catch (AbandonedMutexException) + { + Log.Debug($"Previous run of the program did not clear the '{Name}' mutex cleanly."); + + return false; + } + catch (Exception ex) + { + Log.Warn($"Error occurred while checking if the program is still running:'{ex.Message}'. Will continue."); + } + + return false; + } + + public void Dispose() + { + Release(); + + InstanceMutex?.Dispose(); + } + + private void Release() + { + if (!ShouldRelease) + return; + + try + { + InstanceMutex?.ReleaseMutex(); + } + catch (Exception e) + { + Log.Warn($"Can't release mutex '{Name}': {e.Message}"); + } + } + } +} From b245225254ec93d6481f4ec2fa79cf1e562caa95 Mon Sep 17 00:00:00 2001 From: Doug Schmidt Date: Wed, 9 Feb 2022 10:41:20 -0800 Subject: [PATCH 2/2] PF-1365 - Fixed the broken build Thanks for nothing, @dependabot! --- Samples/DotNetSdk/LabFileImporter/LabFileImporter.csproj | 6 +++--- Samples/DotNetSdk/LabFileImporter/packages.config | 4 ++-- .../NWFWMD-LabFileImporter/NWFWMD-LabFileImporter.csproj | 6 +++--- Samples/DotNetSdk/NWFWMD-LabFileImporter/packages.config | 4 ++-- .../ObservationReportExporter.csproj | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Samples/DotNetSdk/LabFileImporter/LabFileImporter.csproj b/Samples/DotNetSdk/LabFileImporter/LabFileImporter.csproj index 6b517ae..1e038c2 100644 --- a/Samples/DotNetSdk/LabFileImporter/LabFileImporter.csproj +++ b/Samples/DotNetSdk/LabFileImporter/LabFileImporter.csproj @@ -60,8 +60,8 @@ ..\packages\NodaTime.1.3.0\lib\net35-Client\NodaTime.dll - - ..\packages\RestSharp.106.11.7\lib\net452\RestSharp.dll + + ..\packages\RestSharp.106.15.0\lib\net452\RestSharp.dll ..\packages\ServiceStack.Client.5.10.4\lib\net45\ServiceStack.Client.dll @@ -73,7 +73,7 @@ ..\packages\ServiceStack.Interfaces.5.10.4\lib\net472\ServiceStack.Interfaces.dll - ..\packages\ServiceStack.Logging.Log4Net.5.9.0\lib\net45\ServiceStack.Logging.Log4Net.dll + ..\packages\ServiceStack.Logging.Log4Net.5.10.4\lib\net45\ServiceStack.Logging.Log4Net.dll ..\packages\ServiceStack.Text.5.10.4\lib\net45\ServiceStack.Text.dll diff --git a/Samples/DotNetSdk/LabFileImporter/packages.config b/Samples/DotNetSdk/LabFileImporter/packages.config index 3f36e7d..7d765c6 100644 --- a/Samples/DotNetSdk/LabFileImporter/packages.config +++ b/Samples/DotNetSdk/LabFileImporter/packages.config @@ -13,11 +13,11 @@ - + - + diff --git a/Samples/DotNetSdk/NWFWMD-LabFileImporter/NWFWMD-LabFileImporter.csproj b/Samples/DotNetSdk/NWFWMD-LabFileImporter/NWFWMD-LabFileImporter.csproj index ee84f73..990dc02 100644 --- a/Samples/DotNetSdk/NWFWMD-LabFileImporter/NWFWMD-LabFileImporter.csproj +++ b/Samples/DotNetSdk/NWFWMD-LabFileImporter/NWFWMD-LabFileImporter.csproj @@ -58,8 +58,8 @@ ..\packages\NodaTime.1.3.0\lib\net35-Client\NodaTime.dll - - ..\packages\RestSharp.106.11.7\lib\net452\RestSharp.dll + + ..\packages\RestSharp.106.15.0\lib\net452\RestSharp.dll ..\packages\ServiceStack.Client.5.10.4\lib\net45\ServiceStack.Client.dll @@ -71,7 +71,7 @@ ..\packages\ServiceStack.Interfaces.5.10.4\lib\net472\ServiceStack.Interfaces.dll - ..\packages\ServiceStack.Logging.Log4Net.5.9.0\lib\net45\ServiceStack.Logging.Log4Net.dll + ..\packages\ServiceStack.Logging.Log4Net.5.10.4\lib\net45\ServiceStack.Logging.Log4Net.dll ..\packages\ServiceStack.Text.5.10.4\lib\net45\ServiceStack.Text.dll diff --git a/Samples/DotNetSdk/NWFWMD-LabFileImporter/packages.config b/Samples/DotNetSdk/NWFWMD-LabFileImporter/packages.config index c6bad63..ed84b68 100644 --- a/Samples/DotNetSdk/NWFWMD-LabFileImporter/packages.config +++ b/Samples/DotNetSdk/NWFWMD-LabFileImporter/packages.config @@ -12,11 +12,11 @@ - + - + diff --git a/Samples/DotNetSdk/ObservationReportExporter/ObservationReportExporter.csproj b/Samples/DotNetSdk/ObservationReportExporter/ObservationReportExporter.csproj index 5c3010c..cabab2d 100644 --- a/Samples/DotNetSdk/ObservationReportExporter/ObservationReportExporter.csproj +++ b/Samples/DotNetSdk/ObservationReportExporter/ObservationReportExporter.csproj @@ -144,8 +144,8 @@ ..\packages\System.Memory.4.5.4\lib\net461\System.Memory.dll - - ..\packages\System.Net.Http.4.3.0\lib\net46\System.Net.Http.dll + + ..\packages\System.Net.Http.4.3.4\lib\net46\System.Net.Http.dll True True @@ -215,7 +215,7 @@ - ..\packages\System.Text.RegularExpressions.4.3.0\lib\net463\System.Text.RegularExpressions.dll + ..\packages\System.Text.RegularExpressions.4.3.1\lib\net463\System.Text.RegularExpressions.dll True True