diff --git a/.gitignore b/.gitignore index 59093f0..d7ecf5d 100644 --- a/.gitignore +++ b/.gitignore @@ -335,4 +335,11 @@ ASALocalRun/ coverage.cobertura.xml # VS Code config files -/.vscode \ No newline at end of file +/.vscode + +_packages +_codeCoverage +/Ais.Net.Receiver.sbom.spdx.json +/Ais.Net.Receiver.sbom.json +/Ais.Net.Receiver.sbom.html +/Ais.Net.Receiver.sbom.cyclonedx.xml diff --git a/README.md b/README.md index 692c2a7..ec16fd8 100644 --- a/README.md +++ b/README.md @@ -212,9 +212,9 @@ Use the following commands to install .NET 6.0 on your Pi. Use the following commands to install PowerShell on your Pi. -1. Download the latest package `wget https://github.com/PowerShell/PowerShell/releases/download/v7.2.1/powershell-7.2.1-linux-arm32.tar.gz` +1. Download the latest package `wget https://github.com/PowerShell/PowerShell/releases/download/v7.2.7/powershell-7.2.7-linux-arm32.tar.gz` 1. Create a directory for it to be unpacked into `mkdir ~/powershell` -1. Unpack `tar -xvf ./powershell-7.2.1-linux-arm32.tar.gz -C ~/powershell` +1. Unpack `tar -xvf ./powershell-7.2.7-linux-arm32.tar.gz -C ~/powershell` 1. Give it executable rights `sudo chmod +x /opt/microsoft/powershell/7/pwsh` 1. Create a symbolic link `sudo ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh` @@ -238,6 +238,54 @@ If you need to look at / edit the deployed `aisr.service` use `sudo nano /lib/s Use [Azure Storage Explorer](https://azure.microsoft.com/en-us/features/storage-explorer/) to browse to where files are captured. +### Configuration + +Configuration is read from `settings.json` and can also be overridden for local development by using a `settings.local.json` file. + +```json +{ + "Ais": { + "host": "153.44.253.27", + "port": "5631", + "loggerVerbosity": "Minimal", + "statisticsPeriodicity": "00:01:00", + "retryAttempts": 100, + "retryPeriodicity": "00:00:00:00.500" + }, + "Storage": { + "enableCapture": true, + "connectionString": "DefaultEndpointsProtocol=https;AccountName=;AccountKey=", + "containerName": "nmea-ais-dev", + "writeBatchSize": 500 + } +} +``` + +#### AIS + +These settings control the `ReceiverHost` and its behaviour. + +- `host`: IP Address or FQDN of the AIS Source +- `port`: Port number for the AIS Source +- `loggerVerbosity`: Controls the output to the console. + - `Quiet` = Essential only, + - `Minimal` = Statistics only. Sample rate of statistics controlled by `statisticsPeriodicity`, + - `Normal` = Vessel Names and Positions, + - `Detailed` = NMEA Sentences, + - `Diagnostic` = Messages and Errors +- `statisticsPeriodicity`: TimeSpan defining the sample rate of statistics to display +- `retryAttempts`: Number of retry attempts when a connection error occurs +- `retryPeriodicity`: How long to wait before a retry attempt. +- +#### Storage + +These settings control the capturing NMEA sentences to Azure Blob Storage. + +- `enableCapture`: Whether you want to capture the NMEA sentences and write them to Azure Blob Storage +- `connectionString`: Azure Storage Account Connection String +- `containerName`: Name of the container to capture the NMEA sentences. You can use this to separate a local dev storage container from your production storage container, within the same storage account. +- `writeBatchSize`: How many NMEA sentences to batch before writing to Azure Blob Storage. + ## Licenses [![GitHub license](https://img.shields.io/badge/license-Apache%202-blue.svg)](https://raw.githubusercontent.com/ais-dotnet/Ais.Net.Receiver/master/LICENSE) diff --git a/Solutions/Ais.Net.Models.Specs/Features/AisMessageType18.feature.cs b/Solutions/Ais.Net.Models.Specs/Features/AisMessageType18.feature.cs index abd29f7..97f7296 100644 --- a/Solutions/Ais.Net.Models.Specs/Features/AisMessageType18.feature.cs +++ b/Solutions/Ais.Net.Models.Specs/Features/AisMessageType18.feature.cs @@ -26,7 +26,7 @@ public partial class AisMessageType18Feature private TechTalk.SpecFlow.ITestRunner testRunner; - private string[] _featureTags = ((string[])(null)); + private static string[] featureTags = ((string[])(null)); #line 1 "AisMessageType18.feature" #line hidden @@ -35,7 +35,7 @@ public partial class AisMessageType18Feature public virtual void FeatureSetup() { testRunner = TechTalk.SpecFlow.TestRunnerManager.GetTestRunner(); - TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "Features", "AisMessageType18", null, ProgrammingLanguage.CSharp, ((string[])(null))); + TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "Features", "AisMessageType18", null, ProgrammingLanguage.CSharp, featureTags); testRunner.OnFeatureStart(featureInfo); } @@ -47,28 +47,28 @@ public virtual void FeatureTearDown() } [NUnit.Framework.SetUpAttribute()] - public virtual void TestInitialize() + public void TestInitialize() { } [NUnit.Framework.TearDownAttribute()] - public virtual void TestTearDown() + public void TestTearDown() { testRunner.OnScenarioEnd(); } - public virtual void ScenarioInitialize(TechTalk.SpecFlow.ScenarioInfo scenarioInfo) + public void ScenarioInitialize(TechTalk.SpecFlow.ScenarioInfo scenarioInfo) { testRunner.OnScenarioInitialize(scenarioInfo); testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(NUnit.Framework.TestContext.CurrentContext); } - public virtual void ScenarioStart() + public void ScenarioStart() { testRunner.OnScenarioStart(); } - public virtual void ScenarioCleanup() + public void ScenarioCleanup() { testRunner.CollectScenarioErrors(); } diff --git a/Solutions/Ais.Net.Models.Specs/packages.lock.json b/Solutions/Ais.Net.Models.Specs/packages.lock.json index bc2e23f..76b2e9d 100644 --- a/Solutions/Ais.Net.Models.Specs/packages.lock.json +++ b/Solutions/Ais.Net.Models.Specs/packages.lock.json @@ -4,40 +4,40 @@ "net6.0": { "Corvus.Testing.SpecFlow.NUnit": { "type": "Direct", - "requested": "[1.4.8, )", - "resolved": "1.4.8", - "contentHash": "4+9Ky6SMDm43HgbVYNYNnxpv1IRZUwFfD55dKiEHdbIsd0GQUapDwNxEWmtUl29pUS1NlU3YCfDdXH1EWxxsbg==", + "requested": "[1.5.1, )", + "resolved": "1.5.1", + "contentHash": "y0ztTHAvKYa9QTqmcQqVgNMMSeggmBy+je7v0cs0XIw8XMmdwxt3am66JT9hN354LD6l9l/Cey/pjWkAH1Kkxg==", "dependencies": { - "Corvus.Testing.SpecFlow": "1.4.8", + "Corvus.Testing.SpecFlow": "1.5.1", "Microsoft.NET.Test.Sdk": "16.11.0", - "Moq": "4.16.1", - "SpecFlow.NUnit.Runners": "3.9.50", + "Moq": "4.17.2", + "SpecFlow.NUnit.Runners": "3.9.74", "coverlet.msbuild": "3.1.2" } }, "Endjin.RecommendedPractices.GitHub": { "type": "Direct", - "requested": "[2.1.0, )", - "resolved": "2.1.0", - "contentHash": "I5hRIYqow1UTBh+mlsaC23Pi1ISIbc04HUeDbdnmv4a3tCZyOEYOJF8Q/SVWbSEdh9chEPYSr6qfdltv/GrCPA==", + "requested": "[2.1.2, )", + "resolved": "2.1.2", + "contentHash": "mBUCmeSdWWrIQKuuYd9zflcwupRDmpF39dsbb07e6azlNIQqaE1J5TQa17c3SFVRXn9IZrClsmKoMporRTAWwQ==", "dependencies": { - "Endjin.RecommendedPractices": "2.1.0", + "Endjin.RecommendedPractices": "2.1.2", "Microsoft.SourceLink.GitHub": "1.1.1" } }, "Roslynator.Analyzers": { "type": "Direct", - "requested": "[4.0.2, )", - "resolved": "4.0.2", - "contentHash": "UmKLY06/yIAAkARvvGHjIS5LA0XEeEn7pbRHmsDcxvbLFla2fqrTTPVBUW7HMttBAwFi2WKvkVzGNu3/0JDdxA==" + "requested": "[4.1.1, )", + "resolved": "4.1.1", + "contentHash": "3cPVlrB1PytlO1ztZZBOExDKQWpMZgI15ZDa0BqLu0l6xv+xIRfEpqjNRcpvUy3aLxWTkPgSKZbbaO+VoFEJ1g==" }, "StyleCop.Analyzers": { "type": "Direct", - "requested": "[1.2.0-beta.406, )", - "resolved": "1.2.0-beta.406", - "contentHash": "YbsYoczQPZyz+4nmQ7bBiU9uQkk7Q2KUizQWEv01S4/ImCdJFiHvJfm8HAINNS0cvSLOA7xM9Y+KWQ2FOYjgkA==", + "requested": "[1.2.0-beta.435, )", + "resolved": "1.2.0-beta.435", + "contentHash": "TADk7vdGXtfTnYCV7GyleaaRTQjfoSfZXprQrVMm7cSJtJbFc1QIbWPyLvrgrfGdfHbGmUPvaN4ODKNxg2jgPQ==", "dependencies": { - "StyleCop.Analyzers.Unstable": "1.2.0.406" + "StyleCop.Analyzers.Unstable": "1.2.0.435" } }, "Ais.Net": { @@ -55,8 +55,8 @@ }, "Castle.Core": { "type": "Transitive", - "resolved": "4.4.0", - "contentHash": "b5rRL5zeaau1y/5hIbI+6mGw3cwun16YjkHZnV9RRT5UyUIFsgLmNXJ0YnIN9p8Hw7K7AbG1q1UclQVU3DinAQ==", + "resolved": "4.4.1", + "contentHash": "zanbjWC0Y05gbx4eGXkzVycOQqVOFVeCjVsDSyuao9P4mtN1w3WxxTo193NGC7j3o2u3AJRswaoC6hEbnGACnQ==", "dependencies": { "NETStandard.Library": "1.6.1", "System.Collections.Specialized": "4.3.0", @@ -72,14 +72,14 @@ }, "Corvus.Testing.SpecFlow": { "type": "Transitive", - "resolved": "1.4.8", - "contentHash": "UNc7x4jSCWioi8kmjyP1zrkim4gntGF8yIqKARt80lPfYqQNZQKOtUkFpxSz4W0DWcAV7Hlwj8C3tY7ExTmNhg==", + "resolved": "1.5.1", + "contentHash": "V2T6PjkWwzMrkwew29GmaIceDepKEaev+CyBDJLfCOZth4lHiiyhEb0mlIv/GKJTwLLzvTUlUr8yxetIeRCE6Q==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "3.1.22", - "Microsoft.Extensions.DependencyInjection": "3.1.22", - "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.22", - "NUnit": "3.13.2", - "SpecFlow": "3.9.50", + "Microsoft.Extensions.Configuration.Abstractions": "3.1.24", + "Microsoft.Extensions.DependencyInjection": "3.1.24", + "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.24", + "NUnit": "3.13.3", + "SpecFlow": "3.9.74", "System.Management": "4.7.0" } }, @@ -90,8 +90,8 @@ }, "Endjin.RecommendedPractices": { "type": "Transitive", - "resolved": "2.1.0", - "contentHash": "2h0r1wg2Oc0LPzsW9VAI+yf4hNOk37oi/zi1SmjlCmT1Jin+4dFxbRXc3uTRvD2mrTGC8VOb0OBnyoZ1Qg41dA==", + "resolved": "2.1.2", + "contentHash": "Nbj0WS3zVDD2wjfU2/nbkWIWS9Ljg8VN+SSpaCuf5lHBOIEb0Ra201lGcyJHtRHybAciz+hQA9lHj7TnG52qqw==", "dependencies": { "Microsoft.Build.Tasks.Git": "1.1.1" } @@ -166,24 +166,24 @@ }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", - "resolved": "3.1.22", - "contentHash": "znkB/7CpLNzFPFrZP0dK5dLwLt/GgrDBdBCaTQvVAPAJdA96DkhizknBC5+vn0Le8JNOoGt4QlG7WMywswkA0w==", + "resolved": "3.1.24", + "contentHash": "3YeuWZKoBxLe3KjYMY9Hk0cUs9VpqqUOekfUkNP3Px2DlOLi5S+qJx2sUkOjKsbDztbb+35v5TUylSysvdNJvg==", "dependencies": { - "Microsoft.Extensions.Primitives": "3.1.22" + "Microsoft.Extensions.Primitives": "3.1.24" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", - "resolved": "3.1.22", - "contentHash": "QrzfKU8te2X0ykM8XY9YzLvzTGO8qOMq45/Y2sy5gZryQqYe9CxEr0ulwG0idpL+ByK7luX7djmtT8Nv1mMaZw==", + "resolved": "3.1.24", + "contentHash": "edKD2klSQqQood/i/S8Wcwj7GdpJrx6YNx6aEwrrD9HPb9E9b6K1jhAEqd5Pjy3YrIgB+lG4a59GB8ZkVDRN6A==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.22" + "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.24" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", - "resolved": "3.1.22", - "contentHash": "+zBl4NrqANk4JalElpCZ3P2rQ33A3ldRCF1K7RikOuNzEWG5B2M5C+Izas7q5Ub6bFMzAvCJh5E+BtT/gTUD6Q==" + "resolved": "3.1.24", + "contentHash": "vUVCGgYpwKK3C20t0swdxWKTmom3TByacyyj/sEMPWjfufcGxNM2W+qxgBOTMucFQpvbyPwBIFKAik7gYYFqxw==" }, "Microsoft.Extensions.DependencyModel": { "type": "Transitive", @@ -199,8 +199,8 @@ }, "Microsoft.Extensions.Primitives": { "type": "Transitive", - "resolved": "3.1.22", - "contentHash": "B5CNTMTdzVj/xMpazYcczFk3aUg/qduSfKAfUCH0gJ54NETETHaJBPy2GV6VlIeIw4UZqzXV3DroUkuHP561zg==" + "resolved": "3.1.24", + "contentHash": "MCg9BTY32DBAYCIltI5pkE7LbK/2KTpeK3ZknbTQuQOEV+guLVOHv5yNDW7NhiX2n3FTGTL9uhAf7effqYkAbg==" }, "Microsoft.NET.Test.Sdk": { "type": "Transitive", @@ -274,10 +274,10 @@ }, "Moq": { "type": "Transitive", - "resolved": "4.16.1", - "contentHash": "bw3R9q8cVNhWXNpnvWb0OGP4HadS4zvClq+T1zf7AF+tLY1haZ2AvbHidQekf4PDv1T40c6brZeT/V0IBq7cEQ==", + "resolved": "4.17.2", + "contentHash": "HytUPJ3/uks2UgJ9hIcyXm3YxpFAR4OJzbQwTHltbKGun3lFLhEHs97hiiPj1dY8jV/kasXeihTzDxct6Zf3iQ==", "dependencies": { - "Castle.Core": "4.4.0", + "Castle.Core": "4.4.1", "System.Threading.Tasks.Extensions": "4.5.4" } }, @@ -325,8 +325,8 @@ }, "NUnit": { "type": "Transitive", - "resolved": "3.13.2", - "contentHash": "u+fz/lXyR4vlamySNAEMrXvh+GhAQiB6/aVZtU5WjivR5zF26Ui0tfteDtWqT90k9D8y6g8rFKYQC97Z7d195w==", + "resolved": "3.13.3", + "contentHash": "KNPDpls6EfHwC3+nnA67fh5wpxeLb3VLFAfLxrug6JMYDLHH6InaQIWR7Sc3y75d/9IKzMksH/gi08W7XWbmnQ==", "dependencies": { "NETStandard.Library": "2.0.0" } @@ -490,8 +490,8 @@ }, "SpecFlow": { "type": "Transitive", - "resolved": "3.9.50", - "contentHash": "8iBOWWyLj5R0tEhbBLeg1VTCtuDrfvkqYBYn6RRsL+D0FGdtXvyimeW2IHZunKrCiCdPF1f31ZFRwsMoHSFt2Q==", + "resolved": "3.9.74", + "contentHash": "n6kcg9ZeQWxqJFoT23SsFT89U1QQNwvcN9pAX5alB6ZPr6K0p5D5nGIJ1PZsSaFaRFutiwQ+DicmxBCPAZVYIA==", "dependencies": { "BoDi": "1.5.0", "Gherkin": "19.0.3", @@ -509,36 +509,36 @@ }, "SpecFlow.NUnit": { "type": "Transitive", - "resolved": "3.9.50", - "contentHash": "D0dBAhECMbj5KtAYPpNW/jhOeOs98cZpAYDBg81N82NUoEfpXhhfSyXxpql53VII6QDtPzrRnczM9EJASfTv1w==", + "resolved": "3.9.74", + "contentHash": "nMPLztTT5IZDMnvNCUxklqaM+agn4kjuNy/qAcYQQOxau2G1MF73UxhL9OXjJQaEuPuyT8gJvXudOYCFZWztxA==", "dependencies": { "NUnit": "3.13.1", - "SpecFlow": "[3.9.50]", - "SpecFlow.Tools.MsBuild.Generation": "[3.9.50]" + "SpecFlow": "[3.9.74]", + "SpecFlow.Tools.MsBuild.Generation": "[3.9.74]" } }, "SpecFlow.NUnit.Runners": { "type": "Transitive", - "resolved": "3.9.50", - "contentHash": "HR3kO/rEXG8aqIBUfiUg2XGjRJ6PxMp9Uz2uhFUdsXMakIMTtK0fzMnh0gvfriGHetF6Ra5eA4gR/XKE9KyzbA==", + "resolved": "3.9.74", + "contentHash": "m595x3GM7CYco+KsXo96irQ2jcjC6+1+41bKdmnTdl3RAvnC4jUZ9f5B5FhGuaVK4+j4GwWi8MZtGMrT//zHLA==", "dependencies": { "NUnit.Console": "3.12.0", "NUnit3TestAdapter": "3.17.0", - "SpecFlow.NUnit": "[3.9.50]" + "SpecFlow.NUnit": "[3.9.74]" } }, "SpecFlow.Tools.MsBuild.Generation": { "type": "Transitive", - "resolved": "3.9.50", - "contentHash": "oJ5LXdKBnx2faO3I3hNVt+Pyeb6pE9mYxPFCvNXydQ2je3C1O8rDznK9Cxh3Tn8ixpx4LWCYuch9VSL9jZjfXQ==", + "resolved": "3.9.74", + "contentHash": "I/9OvmKOohJqIUNJ0xGYJCWfL6WKDaes8OoOAD/2yhGX+tzC5ofs9yqkP9Cu/xfnIx+11IR3pZs7YhBhGAcgWQ==", "dependencies": { - "SpecFlow": "[3.9.50]" + "SpecFlow": "[3.9.74]" } }, "StyleCop.Analyzers.Unstable": { "type": "Transitive", - "resolved": "1.2.0.406", - "contentHash": "FclNdBR81ynIo9l1QNlo+l0I/PaFIYaPQPlMram8XVIMh6G6G43KTa1aCxfwTj13uKlAJS/LhLy6KjOPOeAU4w==" + "resolved": "1.2.0.435", + "contentHash": "ouwPWZxbOV3SmCZxIRqHvljkSzkCyi1tDoMzQtDb/bRP8ctASV/iRJr+A2Gdj0QLaLmWnqTWDrH82/iP+X80Lg==" }, "System.AppContext": { "type": "Transitive", @@ -1463,7 +1463,7 @@ "ais.net.models": { "type": "Project", "dependencies": { - "Ais.Net": "0.4.2" + "Ais.Net": "[0.4.2, )" } } } diff --git a/Solutions/Ais.Net.Receiver.Host.Console/Ais.Net.Receiver.Host.Console.csproj b/Solutions/Ais.Net.Receiver.Host.Console/Ais.Net.Receiver.Host.Console.csproj index 209b642..1c91e58 100644 --- a/Solutions/Ais.Net.Receiver.Host.Console/Ais.Net.Receiver.Host.Console.csproj +++ b/Solutions/Ais.Net.Receiver.Host.Console/Ais.Net.Receiver.Host.Console.csproj @@ -1,5 +1,7 @@ + + Exe net6.0 @@ -25,6 +27,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Solutions/Ais.Net.Receiver.Host.Console/Ais/Net/Receiver/Host/Console/Program.cs b/Solutions/Ais.Net.Receiver.Host.Console/Ais/Net/Receiver/Host/Console/Program.cs new file mode 100644 index 0000000..e47841b --- /dev/null +++ b/Solutions/Ais.Net.Receiver.Host.Console/Ais/Net/Receiver/Host/Console/Program.cs @@ -0,0 +1,113 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Ais.Net.Receiver.Host.Console +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using System.Threading.Tasks.Dataflow; + + using Ais.Net.Models; + using Ais.Net.Models.Abstractions; + using Ais.Net.Receiver.Configuration; + using Ais.Net.Receiver.Receiver; + using Ais.Net.Receiver.Storage; + using Ais.Net.Receiver.Storage.Azure.Blob; + using Ais.Net.Receiver.Storage.Azure.Blob.Configuration; + + using Microsoft.Extensions.Configuration; + + /// + /// Host application for the . + /// + public static class Program + { + /// + /// Entry point for the application. + /// + /// Task representing the operation. + public static async Task Main() + { + IConfiguration config = new ConfigurationBuilder() + .AddJsonFile("settings.json", true, true) + .AddJsonFile("settings.local.json", true, true) + .Build(); + + AisConfig aisConfig = config.GetSection("Ais").Get(); + StorageConfig storageConfig = config.GetSection("Storage").Get(); + + INmeaReceiver receiver = new NetworkStreamNmeaReceiver( + aisConfig.Host, + aisConfig.Port, + aisConfig.RetryPeriodicity, + aisConfig.RetryAttempts); + + // If you wanted to run from a captured stream uncomment this line: + /*INmeaReceiver receiver = new FileStreamNmeaReceiver(@"PATH-TO-RECORDING.nm4");*/ + + var receiverHost = new ReceiverHost(receiver); + + if (aisConfig.LoggerVerbosity == LoggerVerbosity.Minimal) + { + receiverHost.GetStreamStatistics(aisConfig.StatisticsPeriodicity) + .Subscribe(statistics => Console.WriteLine($"{DateTime.UtcNow.ToUniversalTime()}: Sentences: {statistics.Sentence} | Messages: {statistics.Message} | Errors: {statistics.Error}")); + } + + if (aisConfig.LoggerVerbosity == LoggerVerbosity.Normal) + { + receiverHost.Messages.VesselNavigationWithNameStream().Subscribe(navigationWithName => + { + (uint mmsi, IVesselNavigation navigation, IVesselName name) = navigationWithName; + string positionText = navigation.Position is null ? "unknown position" : $"{navigation.Position.Latitude},{navigation.Position.Longitude}"; + + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"[{mmsi}: '{name.VesselName.CleanVesselName()}'] - [{positionText}] - [{navigation.CourseOverGroundDegrees ?? 0}]"); + Console.ResetColor(); + }); + } + + if (aisConfig.LoggerVerbosity == LoggerVerbosity.Detailed) + { + // Write out the messages as they are received over the wire. + receiverHost.Sentences.Subscribe(Console.WriteLine); + } + + if (aisConfig.LoggerVerbosity == LoggerVerbosity.Diagnostic) + { + receiverHost.Messages.Subscribe(Console.WriteLine); + + // Write out errors in the console + receiverHost.Errors.Subscribe(error => + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Error received: {error.Exception.Message}"); + Console.WriteLine($"Bad line: {error.Line}"); + Console.ResetColor(); + }); + } + + if (storageConfig.EnableCapture) + { + IStorageClient storageClient = new AzureAppendBlobStorageClient(storageConfig); + var batchBlock = new BatchBlock(storageConfig.WriteBatchSize); + var actionBlock = new ActionBlock>(storageClient.PersistAsync); + batchBlock.LinkTo(actionBlock); + + // Persist the messages as they are received over the wire. + receiverHost.Sentences.Subscribe(batchBlock.AsObserver()); + } + + var cts = new CancellationTokenSource(); + + Task task = receiverHost.StartAsync(cts.Token); + + // If you wanted to cancel the long running process: + /* cts.Cancel(); */ + + await task; + } + } +} \ No newline at end of file diff --git a/Solutions/Ais.Net.Receiver.Host.Console/Ais/Net/Receiver/Host/Console/ReceiverHostExtensions.cs b/Solutions/Ais.Net.Receiver.Host.Console/Ais/Net/Receiver/Host/Console/ReceiverHostExtensions.cs new file mode 100644 index 0000000..be37039 --- /dev/null +++ b/Solutions/Ais.Net.Receiver.Host.Console/Ais/Net/Receiver/Host/Console/ReceiverHostExtensions.cs @@ -0,0 +1,74 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +namespace Ais.Net.Receiver.Host.Console +{ + using System; + using System.Linq; + using System.Reactive.Linq; + using Ais.Net.Models.Abstractions; + using Ais.Net.Receiver.Receiver; + + /// + /// Extensions for the and its data streams. + /// + public static class ReceiverHostExtensions + { + /// + /// Calculates statistics about the number of , and + /// generated during the specified . + /// + /// The to extend. + /// The duration statistics should be collected for, before returning. + /// An observable sequence of tuple containing statistics. + public static IObservable<(long Message, long Sentence, long Error)> GetStreamStatistics(this ReceiverHost receiverHost, TimeSpan period) + { + IObservable<(long Messages, long Sentences, long Errors)> runningCounts = + receiverHost.Messages.RunningCount().CombineLatest( + receiverHost.Sentences.RunningCount(), + receiverHost.Errors.RunningCount(), + (messages, sentences, errors) => (messages, sentences, errors)); + + return runningCounts.Buffer(period) + .Select(window => (window[0], window[^1])) + .Select<((long, long, long), (long, long, long)), (long, long, long)>( + (((long Messages, long Sentences, long Errors) First, + (long Messages, long Sentences, long Errors) Last) pair) + => (pair.Last.Messages - pair.First.Messages, + pair.Last.Sentences - pair.First.Sentences, + pair.Last.Errors - pair.First.Errors)); + } + + /// + /// Groups and combines the AIS Messages so that vessel name and navigation information can be displayed. + /// + /// An observable stream of AIS Messages. + /// An observable sequence of tuple containing vessel information. + public static IObservable<(uint Mmsi, IVesselNavigation Navigation, IVesselName Name)> VesselNavigationWithNameStream(this IObservable messages) + { + // Decode the sentences into messages, and group by the vessel by Id + IObservable> byVessel = messages.GroupBy(m => m.Mmsi); + + // Combine the various message types required to create a stream containing name and navigation + return + from perVesselMessages in byVessel + let vesselNavigationUpdates = perVesselMessages.OfType() + let vesselNames = perVesselMessages.OfType() + let vesselLocationsWithNames = vesselNavigationUpdates.CombineLatest(vesselNames, (navigation, name) => (navigation, name)) + from vesselLocationAndName in vesselLocationsWithNames + select (Mmsi: perVesselMessages.Key, vesselLocationAndName.navigation, vesselLocationAndName.name); + } + + /// + /// Provides a running count of events provided by an observable stream. + /// + /// Type of events to count. + /// Observable stream of events to count. + /// An observable sequence representing the count of events. + private static IObservable RunningCount(this IObservable eventsForCount) + { + return eventsForCount.Scan(0L, (total, _) => total + 1); + } + } +} \ No newline at end of file diff --git a/Solutions/Ais.Net.Receiver.Host.Console/Program.cs b/Solutions/Ais.Net.Receiver.Host.Console/Program.cs deleted file mode 100644 index c60f3e2..0000000 --- a/Solutions/Ais.Net.Receiver.Host.Console/Program.cs +++ /dev/null @@ -1,100 +0,0 @@ -// -// Copyright (c) Endjin. All rights reserved. -// - -namespace Ais.Net.Receiver.Host.Console -{ - using System; - using System.Collections.Generic; - using System.Reactive.Linq; - using System.Threading; - using System.Threading.Tasks; - using System.Threading.Tasks.Dataflow; - - using Ais.Net.Models; - using Ais.Net.Models.Abstractions; - using Ais.Net.Receiver.Configuration; - using Ais.Net.Receiver.Receiver; - using Ais.Net.Receiver.Storage; - using Ais.Net.Receiver.Storage.Azure.Blob; - using Ais.Net.Receiver.Storage.Azure.Blob.Configuration; - - using Microsoft.Extensions.Configuration; - - public static class Program - { - public static async Task Main() - { - IConfiguration config = new ConfigurationBuilder() - .AddJsonFile("settings.json", true, true) - .AddJsonFile("settings.local.json", true, true) - .Build(); - - AisConfig aisConfig = config.GetSection("Ais").Get(); - StorageConfig storageConfig = config.GetSection("Storage").Get(); - - IStorageClient storageClient = new AzureAppendBlobStorageClient(storageConfig); - - INmeaReceiver receiver = new NetworkStreamNmeaReceiver( - aisConfig.Host, - aisConfig.Port, - aisConfig.RetryPeriodicity, - aisConfig.RetryAttempts); - - // INmeaReceiver receiver = new FileStreamNmeaReceiver(@"PATH-TO-RECORDING.nm4"); - - var receiverHost = new ReceiverHost(receiver); - - // Decode teh sentences into messages, and group by the vessel by Id - IObservable> byVessel = receiverHost.Messages.GroupBy(m => m.Mmsi); - - // Combine the various message types required to create a stream containing name and navigation - IObservable<(uint mmsi, IVesselNavigation navigation, IVesselName name)>? vesselNavigationWithNameStream = - from perVesselMessages in byVessel - let vesselNavigationUpdates = perVesselMessages.OfType() - let vesselNames = perVesselMessages.OfType() - let vesselLocationsWithNames = vesselNavigationUpdates.CombineLatest(vesselNames, (navigation, name) => (navigation, name)) - from vesselLocationAndName in vesselLocationsWithNames - select (mmsi: perVesselMessages.Key, vesselLocationAndName.navigation, vesselLocationAndName.name); - - vesselNavigationWithNameStream.Subscribe(navigationWithName => - { - (uint mmsi, IVesselNavigation navigation, IVesselName name) = navigationWithName; - string positionText = navigation.Position is null ? "unknown position" : $"{navigation.Position.Latitude},{navigation.Position.Longitude}"; - - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine($"[{mmsi}: '{name.VesselName.CleanVesselName()}'] - [{positionText}] - [{navigation.CourseOverGroundDegrees ?? 0}]"); - Console.ResetColor(); - }); - - var batchBlock = new BatchBlock(storageConfig.WriteBatchSize); - var actionBlock = new ActionBlock>(storageClient.PersistAsync); - - batchBlock.LinkTo(actionBlock); - - // Write out the messages as they are received over the wire. - receiverHost.Sentences.Subscribe(sentence => Console.WriteLine(sentence)); - - // Persist the messages as they are received over the wire. - receiverHost.Sentences.Subscribe(batchBlock.AsObserver()); - - // Write out errors in the console - receiverHost.Errors.Subscribe(error => - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"Error received: {error.Exception.Message}"); - Console.WriteLine($"Bad line: {error.Line}"); - Console.ResetColor(); - }); - - var cts = new CancellationTokenSource(); - - Task task = receiverHost.StartAsync(cts.Token); - - // If you wanted to cancel the long running process: - // cts.Cancel(); - - await task; - } - } -} \ No newline at end of file diff --git a/Solutions/Ais.Net.Receiver.Host.Console/packages.lock.json b/Solutions/Ais.Net.Receiver.Host.Console/packages.lock.json index 6a9f98f..6b11016 100644 --- a/Solutions/Ais.Net.Receiver.Host.Console/packages.lock.json +++ b/Solutions/Ais.Net.Receiver.Host.Console/packages.lock.json @@ -2,11 +2,21 @@ "version": 1, "dependencies": { "net6.0": { + "Endjin.RecommendedPractices.GitHub": { + "type": "Direct", + "requested": "[2.1.2, )", + "resolved": "2.1.2", + "contentHash": "mBUCmeSdWWrIQKuuYd9zflcwupRDmpF39dsbb07e6azlNIQqaE1J5TQa17c3SFVRXn9IZrClsmKoMporRTAWwQ==", + "dependencies": { + "Endjin.RecommendedPractices": "2.1.2", + "Microsoft.SourceLink.GitHub": "1.1.1" + } + }, "Microsoft.Extensions.Configuration": { "type": "Direct", "requested": "[6.0.*, )", - "resolved": "6.0.0", - "contentHash": "tq2wXyh3fL17EMF2bXgRhU7JrbO3on93MRKYxzz4JzzvuGSA1l0W3GI9/tl8EO89TH+KWEymP7bcFway6z9fXg==", + "resolved": "6.0.1", + "contentHash": "BUyFU9t+HzlSE7ri4B+AQN2BgTgHv/uM82s5ZkgU1BApyzWzIl48nDsG5wR1t0pniNuuyTBzG3qCW8152/NtSw==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", "Microsoft.Extensions.Primitives": "6.0.0" @@ -47,6 +57,21 @@ "System.Text.Json": "6.0.0" } }, + "Roslynator.Analyzers": { + "type": "Direct", + "requested": "[4.1.1, )", + "resolved": "4.1.1", + "contentHash": "3cPVlrB1PytlO1ztZZBOExDKQWpMZgI15ZDa0BqLu0l6xv+xIRfEpqjNRcpvUy3aLxWTkPgSKZbbaO+VoFEJ1g==" + }, + "StyleCop.Analyzers": { + "type": "Direct", + "requested": "[1.2.0-beta.435, )", + "resolved": "1.2.0-beta.435", + "contentHash": "TADk7vdGXtfTnYCV7GyleaaRTQjfoSfZXprQrVMm7cSJtJbFc1QIbWPyLvrgrfGdfHbGmUPvaN4ODKNxg2jgPQ==", + "dependencies": { + "StyleCop.Analyzers.Unstable": "1.2.0.435" + } + }, "System.Threading.Tasks.Dataflow": { "type": "Direct", "requested": "[6.0.*, )", @@ -63,35 +88,34 @@ }, "Azure.Core": { "type": "Transitive", - "resolved": "1.19.0", - "contentHash": "lcDjG635DPE4fU5tqSueVMmzrx0QrIfPuY0+y6evHN5GanQ0GB+/4nuMHMmoNPwEow6OUPkJu4cZQxfHJQXPdA==", + "resolved": "1.25.0", + "contentHash": "X8Dd4sAggS84KScWIjEbFAdt2U1KDolQopTPoHVubG2y3CM54f9l6asVrP5Uy384NWXjsspPYaJgz5xHc+KvTA==", "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "1.0.0", - "System.Buffers": "4.5.1", + "Microsoft.Bcl.AsyncInterfaces": "1.1.1", "System.Diagnostics.DiagnosticSource": "4.6.0", - "System.Memory": "4.5.4", "System.Memory.Data": "1.0.2", "System.Numerics.Vectors": "4.5.0", "System.Text.Encodings.Web": "4.7.2", - "System.Text.Json": "4.6.0", - "System.Threading.Tasks.Extensions": "4.5.2" + "System.Text.Json": "4.7.2", + "System.Threading.Tasks.Extensions": "4.5.4" } }, "Azure.Storage.Blobs": { "type": "Transitive", - "resolved": "12.10.0", - "contentHash": "yaijs9DPfn34C/X4TX+0TAxANEhuKSrFE650gkF9g1pz/nQljv86zOOtDwNwD5UsAY5LyrOiCASGo2dhuIxvdg==", + "resolved": "12.14.1", + "contentHash": "DvRBWUDMB2LjdRbsBNtz/LiVIYk56hqzSooxx4uq4rCdLj2M+7Vvoa1r+W35Dz6ZXL6p+SNcgEae3oZ+CkPfow==", "dependencies": { - "Azure.Storage.Common": "12.9.0", - "System.Text.Json": "4.6.0" + "Azure.Storage.Common": "12.13.0", + "System.Text.Json": "4.7.2" } }, "Azure.Storage.Common": { "type": "Transitive", - "resolved": "12.9.0", - "contentHash": "GuoigTmzz9HrCGdcdu7LyjD4pDr2XPt72LlWWTDyno+nYrjyuNwpwRFBvK/brxJvQFRHofQcBskf8vOxVxnI8g==", + "resolved": "12.13.0", + "contentHash": "jDv8xJWeZY2Er9zA6QO25BiGolxg87rItt9CwAp7L/V9EPJeaz8oJydaNL9Wj0+3ncceoMgdiyEv66OF8YUwWQ==", "dependencies": { - "Azure.Core": "1.19.0" + "Azure.Core": "1.25.0", + "System.IO.Hashing": "6.0.0" } }, "Corvus.Retry": { @@ -99,11 +123,24 @@ "resolved": "1.0.2", "contentHash": "Jzmv1VpjJnIaz+b0uadkl3yoNh+qnmzHvOcHUXc5oAo1fVqclzxLJAqhOPnm5BVURA5nlqgB3mtmI1YQQhwh9A==" }, + "Endjin.RecommendedPractices": { + "type": "Transitive", + "resolved": "2.1.2", + "contentHash": "Nbj0WS3zVDD2wjfU2/nbkWIWS9Ljg8VN+SSpaCuf5lHBOIEb0Ra201lGcyJHtRHybAciz+hQA9lHj7TnG52qqw==", + "dependencies": { + "Microsoft.Build.Tasks.Git": "1.1.1" + } + }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" }, + "Microsoft.Build.Tasks.Git": { + "type": "Transitive", + "resolved": "1.1.1", + "contentHash": "AT3HlgTjsqHnWpBHSNeR0KxbLZD7bztlZVj7I8vgeYG9SYqbeFGh0TM/KVtC6fg53nrWHl3VfZFvb5BiQFcY6Q==" + }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "6.0.0", @@ -143,16 +180,35 @@ "System.Runtime.CompilerServices.Unsafe": "6.0.0" } }, - "System.Buffers": { + "Microsoft.SourceLink.Common": { + "type": "Transitive", + "resolved": "1.1.1", + "contentHash": "WMcGpWKrmJmzrNeuaEb23bEMnbtR/vLmvZtkAP5qWu7vQsY59GqfRJd65sFpBszbd2k/bQ8cs8eWawQKAabkVg==" + }, + "Microsoft.SourceLink.GitHub": { + "type": "Transitive", + "resolved": "1.1.1", + "contentHash": "IaJGnOv/M7UQjRJks7B6p7pbPnOwisYGOIzqCz5ilGFTApZ3ktOR+6zJ12ZRPInulBmdAf1SrGdDG2MU8g6XTw==", + "dependencies": { + "Microsoft.Build.Tasks.Git": "1.1.1", + "Microsoft.SourceLink.Common": "1.1.1" + } + }, + "StyleCop.Analyzers.Unstable": { "type": "Transitive", - "resolved": "4.5.1", - "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + "resolved": "1.2.0.435", + "contentHash": "ouwPWZxbOV3SmCZxIRqHvljkSzkCyi1tDoMzQtDb/bRP8ctASV/iRJr+A2Gdj0QLaLmWnqTWDrH82/iP+X80Lg==" }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", "resolved": "4.6.0", "contentHash": "mbBgoR0rRfl2uimsZ2avZY8g7Xnh1Mza0rJZLPcxqiMWlkGukjmRkuMJ/er+AhQuiRIh80CR/Hpeztr80seV5g==" }, + "System.IO.Hashing": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "Rfm2jYCaUeGysFEZjDe7j1R4x6Z6BzumS/vUT5a1AA/AWJuGX71PoGB0RmpyX3VmrGqVnAwtfMn39OHR8Y/5+g==" + }, "System.IO.Pipelines": { "type": "Transitive", "resolved": "4.7.4", @@ -166,11 +222,6 @@ "Microsoft.Bcl.AsyncInterfaces": "6.0.0" } }, - "System.Memory": { - "type": "Transitive", - "resolved": "4.5.4", - "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==" - }, "System.Memory.Data": { "type": "Transitive", "resolved": "1.0.2", @@ -214,29 +265,29 @@ }, "System.Threading.Tasks.Extensions": { "type": "Transitive", - "resolved": "4.5.2", - "contentHash": "BG/TNxDFv0svAzx8OiMXDlsHfGw623BZ8tCXw4YLhDFDvDhNUEV58jKYMGRnkbJNm7c3JNNJDiN7JBMzxRBR2w==" + "resolved": "4.5.4", + "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==" }, "ais.net.models": { "type": "Project", "dependencies": { - "Ais.Net": "0.4.2" + "Ais.Net": "[0.4.2, )" } }, "ais.net.receiver": { "type": "Project", "dependencies": { - "Ais.Net.Models": "1.0.0", - "Corvus.Retry": "1.0.2", - "System.Linq.Async": "6.0.0", - "System.Reactive": "5.0.0" + "Ais.Net.Models": "[1.0.0, )", + "Corvus.Retry": "[1.0.2, )", + "System.Linq.Async": "[6.0.*, )", + "System.Reactive": "[5.0.*, )" } }, "ais.net.receiver.storage.azure.blob": { "type": "Project", "dependencies": { - "Ais.Net.Receiver": "1.0.0", - "Azure.Storage.Blobs": "12.10.0" + "Ais.Net.Receiver": "[1.0.0, )", + "Azure.Storage.Blobs": "[12.14.1, )" } } } diff --git a/Solutions/Ais.Net.Receiver.Host.Console/settings.json b/Solutions/Ais.Net.Receiver.Host.Console/settings.json index 26f8aa1..30c536c 100644 --- a/Solutions/Ais.Net.Receiver.Host.Console/settings.json +++ b/Solutions/Ais.Net.Receiver.Host.Console/settings.json @@ -2,10 +2,13 @@ "Ais": { "host": "153.44.253.27", "port": "5631", + "loggerVerbosity": "Minimal", + "statisticsPeriodicity": "00:00:01:00", "retryAttempts": 100, - "retryPeriodicity": "00:00:00:00.500" + "retryPeriodicity": "00:00:00:01" }, "Storage": { + "enableCapture": true, "connectionString": "", "containerName": "nmea-ais", "writeBatchSize": 500 diff --git a/Solutions/Ais.Net.Receiver.Storage.Azure.Blob/Ais.Net.Receiver.Storage.Azure.Blob.csproj b/Solutions/Ais.Net.Receiver.Storage.Azure.Blob/Ais.Net.Receiver.Storage.Azure.Blob.csproj index cc296a7..92e8b48 100644 --- a/Solutions/Ais.Net.Receiver.Storage.Azure.Blob/Ais.Net.Receiver.Storage.Azure.Blob.csproj +++ b/Solutions/Ais.Net.Receiver.Storage.Azure.Blob/Ais.Net.Receiver.Storage.Azure.Blob.csproj @@ -21,7 +21,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Solutions/Ais.Net.Receiver.Storage.Azure.Blob/Ais/Net/Receiver/Storage/Azure/Blob/Configuration/StorageConfig.cs b/Solutions/Ais.Net.Receiver.Storage.Azure.Blob/Ais/Net/Receiver/Storage/Azure/Blob/Configuration/StorageConfig.cs index c80ad8b..94ab5ab 100644 --- a/Solutions/Ais.Net.Receiver.Storage.Azure.Blob/Ais/Net/Receiver/Storage/Azure/Blob/Configuration/StorageConfig.cs +++ b/Solutions/Ais.Net.Receiver.Storage.Azure.Blob/Ais/Net/Receiver/Storage/Azure/Blob/Configuration/StorageConfig.cs @@ -14,6 +14,8 @@ public class StorageConfig public string ContainerName { get; set; } + public bool EnableCapture { get; set; } + public int WriteBatchSize { get; set; } } } \ No newline at end of file diff --git a/Solutions/Ais.Net.Receiver/Ais/Net/Receiver/Configuration/AisConfig.cs b/Solutions/Ais.Net.Receiver/Ais/Net/Receiver/Configuration/AisConfig.cs index 19c4543..a7e53a7 100644 --- a/Solutions/Ais.Net.Receiver/Ais/Net/Receiver/Configuration/AisConfig.cs +++ b/Solutions/Ais.Net.Receiver/Ais/Net/Receiver/Configuration/AisConfig.cs @@ -14,6 +14,10 @@ public class AisConfig { public string Host { get; set; } + public LoggerVerbosity LoggerVerbosity { get; set; } + + public TimeSpan StatisticsPeriodicity { get; set; } + public int Port { get; set; } public int RetryAttempts { get; set; } diff --git a/Solutions/Ais.Net.Receiver/Ais/Net/Receiver/Configuration/LoggerVerbosity.cs b/Solutions/Ais.Net.Receiver/Ais/Net/Receiver/Configuration/LoggerVerbosity.cs new file mode 100644 index 0000000..ecd6f3d --- /dev/null +++ b/Solutions/Ais.Net.Receiver/Ais/Net/Receiver/Configuration/LoggerVerbosity.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) Endjin Limited. All rights reserved. +// + +// Configuration binding types are typically better off as null-oblivious, because the contents +// of config files are outside the compiler's control. +#nullable disable annotations +namespace Ais.Net.Receiver.Configuration; + +/// +/// Defines the verbosity of the console output. +/// +public enum LoggerVerbosity +{ + /// + /// Essential only. + /// + Quiet = 0, + + /// + /// Statistics only. + /// + Minimal = 1, + + /// + /// Vessel Names and Positions. + /// + Normal = 2, + + /// + /// NMEA Sentences. + /// + Detailed = 3, + + /// + /// Messages and Errors. + /// + Diagnostic = 4 +} \ No newline at end of file diff --git a/Solutions/Ais.Net.Receiver/Ais/Net/Receiver/Receiver/ReceiverHost.cs b/Solutions/Ais.Net.Receiver/Ais/Net/Receiver/Receiver/ReceiverHost.cs index 3745983..aff0585 100644 --- a/Solutions/Ais.Net.Receiver/Ais/Net/Receiver/Receiver/ReceiverHost.cs +++ b/Solutions/Ais.Net.Receiver/Ais/Net/Receiver/Receiver/ReceiverHost.cs @@ -71,13 +71,20 @@ static void ProcessLineNonAsync(string line, INmeaLineStreamProcessor lineStream errorSubject.OnNext((ex, line)); } } + catch (NotImplementedException ex) + { + if (errorSubject.HasObservers) + { + errorSubject.OnNext((ex, line)); + } + } } this.sentences.OnNext(message); if (this.messages.HasObservers) { - ProcessLineNonAsync(message, adapter, errors); + ProcessLineNonAsync(message, adapter, this.errors); } } } diff --git a/build.ps1 b/build.ps1 index d900297..b9c25db 100644 --- a/build.ps1 +++ b/build.ps1 @@ -30,6 +30,12 @@ The path to import the Endjin.RecommendedPractices.Build module from. This is useful when testing pre-release versions of the Endjin.RecommendedPractices.Build that are not yet available in the PowerShell Gallery. +.PARAMETER BuildModuleVersion + The version of the Endjin.RecommendedPractices.Build module to import. This is useful when + testing pre-release versions of the Endjin.RecommendedPractices.Build that are not yet + available in the PowerShell Gallery. +.PARAMETER InvokeBuildModuleVersion + The version of the InvokeBuild module to be used. #> [CmdletBinding()] param ( @@ -37,7 +43,7 @@ param ( [string[]] $Tasks = @("."), [Parameter()] - [string] $Configuration = "Release", + [string] $Configuration = "Debug", [Parameter()] [string] $BuildRepositoryUri = "", @@ -62,17 +68,23 @@ param ( [switch] $Clean, [Parameter()] - [string] $BuildModulePath + [string] $BuildModulePath, + + [Parameter()] + [version] $BuildModuleVersion = "0.2.17", + + [Parameter()] + [version] $InvokeBuildModuleVersion = "5.7.1" ) $ErrorActionPreference = $ErrorActionPreference ? $ErrorActionPreference : 'Stop' -$InformationPreference = $InformationAction ? $InformationAction : 'Continue' +$InformationPreference = 'Continue' $here = Split-Path -Parent $PSCommandPath #region InvokeBuild setup if (!(Get-Module -ListAvailable InvokeBuild)) { - Install-Module InvokeBuild -RequiredVersion 5.7.1 -Scope CurrentUser -Force -Repository PSGallery + Install-Module InvokeBuild -RequiredVersion $InvokeBuildModuleVersion -Scope CurrentUser -Force -Repository PSGallery } Import-Module InvokeBuild # This handles calling the build engine when this file is run like a normal PowerShell script @@ -89,52 +101,78 @@ if ($MyInvocation.ScriptName -notlike '*Invoke-Build.ps1') { } #endregion -# Import shared tasks and initialise build framework +#region Import shared tasks and initialise build framework if (!($BuildModulePath)) { - if (!(Get-Module -ListAvailable Endjin.RecommendedPractices.Build)) { + if (!(Get-Module -ListAvailable Endjin.RecommendedPractices.Build | ? { $_.Version -eq $BuildModuleVersion })) { Write-Information "Installing 'Endjin.RecommendedPractices.Build' module..." - Install-Module Endjin.RecommendedPractices.Build -RequiredVersion 0.1.0 -AllowPrerelease -Scope CurrentUser -Force -Repository PSGallery + Install-Module Endjin.RecommendedPractices.Build -RequiredVersion $BuildModuleVersion -Scope CurrentUser -Force -Repository PSGallery } $BuildModulePath = "Endjin.RecommendedPractices.Build" } else { Write-Information "BuildModulePath: $BuildModulePath" } -Import-Module $BuildModulePath -Force +Import-Module $BuildModulePath -RequiredVersion $BuildModuleVersion -Force # Load the build process & tasks . Endjin.RecommendedPractices.Build.tasks +#endregion + # # Build process control options # +$SkipInit = $false $SkipVersion = $false $SkipBuild = $false -$CleanBuild = $false +$CleanBuild = $Clean $SkipTest = $false $SkipTestReport = $false +$SkipAnalysis = $false $SkipPackage = $false +$SkipPublish = $false -# Advanced build settings -$EnableGitVersionAdoVariableWorkaround = $false # # Build process configuration # $SolutionToBuild = (Resolve-Path (Join-Path $here ".\Solutions\Ais.Net.Receiver.sln")).Path +$ProjectsToPublish = @( + # "Solutions/MySolution/MyWebSite/MyWebSite.csproj" +) +$NuSpecFilesToPackage = @( + # "Solutions/MySolution/MyProject/MyProject.nuspec" +) +# +# Specify files to exclude from code coverage +# This option is for excluding generated code +# - Use file path or directory path with globbing (e.g dir1/*.cs) +# - Use single or multiple paths (separated by comma) (e.g. **/dir1/class1.cs,**/dir2/*.cs,**/dir3/**/*.cs) +# +$ExcludeFilesFromCodeCoverage = "" # Synopsis: Build, Test and Package task . FullBuild # build extensibility tasks +task RunFirst {} +task PreInit {} +task PostInit {} +task PreVersion {} +task PostVersion {} task PreBuild {} task PostBuild {} task PreTest {} task PostTest {} task PreTestReport {} task PostTestReport {} +task PreAnalysis {} +task PostAnalysis {} task PrePackage {} task PostPackage {} +task PrePublish {} +task PostPublish {} +task RunLast {}