From e6e4b2ca21af230ddf77888ed0b0cc6fcb53b99f Mon Sep 17 00:00:00 2001 From: Steven Kuhn Date: Sat, 6 Apr 2024 11:50:34 -0500 Subject: [PATCH] Add telemetry support Refactor nuget references --- Source/v2/.editorconfig | 5 + .../Commands/Current/TelemetryCommand.cs | 69 +++++ .../Commands/Current/BaseCommand.cs | 26 ++ Source/v2/Meadow.Cli/Meadow.CLI.csproj | 19 +- Source/v2/Meadow.Cli/Program.cs | 5 + Source/v2/Meadow.Cli/Strings.cs | 19 +- .../Meadow.Cloud.Client.csproj | 8 +- Source/v2/Meadow.Dfu/Meadow.Dfu.csproj | 6 +- .../v2/Meadow.Firmware/Meadow.Firmware.csproj | 8 +- Source/v2/Meadow.Hcom/Meadow.HCom.csproj | 2 +- Source/v2/Meadow.Linker/Meadow.Linker.csproj | 32 +- .../Meadow.Tooling.Core.csproj | 11 +- .../Settings/SettingsManager.cs | 19 +- .../Telemetry/CIEnvironmentDetector.cs | 79 +++++ .../Telemetry/MeadowTelemetry.cs | 197 ++++++++++++ .../Telemetry/RuntimeEnvironment.cs | 281 ++++++++++++++++++ .../Meadow.UsbLib.Core.csproj | 4 +- Source/v2/Meadow.UsbLib/Meadow.UsbLib.csproj | 16 +- .../Meadow.UsbLibClassic.csproj | 18 +- .../Meadow.Cloud.Client.Unit.Tests.csproj | 6 +- ...w.SoftwareManager.Integration.Tests.csproj | 4 +- 21 files changed, 762 insertions(+), 72 deletions(-) create mode 100644 Source/v2/.editorconfig create mode 100644 Source/v2/Meadow.CLI/Commands/Current/TelemetryCommand.cs create mode 100644 Source/v2/Meadow.Tooling.Core/Telemetry/CIEnvironmentDetector.cs create mode 100644 Source/v2/Meadow.Tooling.Core/Telemetry/MeadowTelemetry.cs create mode 100644 Source/v2/Meadow.Tooling.Core/Telemetry/RuntimeEnvironment.cs diff --git a/Source/v2/.editorconfig b/Source/v2/.editorconfig new file mode 100644 index 00000000..f3ef191e --- /dev/null +++ b/Source/v2/.editorconfig @@ -0,0 +1,5 @@ +root = true + +[*.csproj] +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/Source/v2/Meadow.CLI/Commands/Current/TelemetryCommand.cs b/Source/v2/Meadow.CLI/Commands/Current/TelemetryCommand.cs new file mode 100644 index 00000000..22871a92 --- /dev/null +++ b/Source/v2/Meadow.CLI/Commands/Current/TelemetryCommand.cs @@ -0,0 +1,69 @@ +using CliFx.Attributes; +using CliFx.Extensibility; +using Meadow.Telemetry; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Meadow.CLI.Commands.DeviceManagement; + +[Command("telemetry", Description = "Manage participation in telemetry sharing")] +public class TelemetryCommand : BaseCommand +{ + public TelemetryCommand( + ILoggerFactory loggerFactory) + : base(loggerFactory) + { + } + + protected override ValueTask ExecuteCommand() + { + throw new CommandException("Specify one of the telemetry commands", true); + } +} + +[Command("telemetry enable", Description = "Enable and opt in to telemetry sharing")] +public class TelemetryEnableCommand : BaseCommand +{ + private readonly ISettingsManager _settingsManager; + + public TelemetryEnableCommand( + ISettingsManager settingsManager, + ILoggerFactory loggerFactory) + : base(loggerFactory) + { + _settingsManager = settingsManager; + } + + protected override ValueTask ExecuteCommand() + { + _settingsManager.SaveSetting(MeadowTelemetry.TelemetryEnabledSettingName, "true"); + + return ValueTask.CompletedTask; + } +} + +[Command("telemetry disable", Description = "Disable and opt out of telemetry sharing")] +public class TelemetryDisableCommand : BaseCommand +{ + private readonly ISettingsManager _settingsManager; + + public TelemetryDisableCommand( + ISettingsManager settingsManager, + ILoggerFactory loggerFactory) + : base(loggerFactory) + { + _settingsManager = settingsManager; + } + + protected override ValueTask ExecuteCommand() + { + _settingsManager.SaveSetting(MeadowTelemetry.TelemetryEnabledSettingName, "false"); + _settingsManager.DeleteSetting(MeadowTelemetry.MachineIdSettingName); + + return ValueTask.CompletedTask; + } +} \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs b/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs index 8684dac6..0ac35fde 100644 --- a/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs +++ b/Source/v2/Meadow.Cli/Commands/Current/BaseCommand.cs @@ -1,6 +1,10 @@ using CliFx; +using CliFx.Attributes; using CliFx.Infrastructure; +using Meadow.Telemetry; using Microsoft.Extensions.Logging; +using Spectre.Console; +using System.Reflection; namespace Meadow.CLI.Commands.DeviceManagement; @@ -28,6 +32,23 @@ public async ValueTask ExecuteAsync(IConsole console) _console = console; CancellationToken = _console.RegisterCancellationHandler(); + try + { + if (MeadowTelemetry.Current.ShouldAskForConsent) + { + AnsiConsole.MarkupLine(Strings.Telemetry.ConsentMessage); + + var result = AnsiConsole.Confirm(Strings.Telemetry.AskToParticipate, defaultValue: true); + MeadowTelemetry.Current.SetTelemetryEnabled(result); + } + + MeadowTelemetry.Current.TrackCommand(GetCommandName()); + } + catch + { + // Swallow any telemetry-related exceptions + } + await ExecuteCommand(); } catch (Exception ex) when (ex is not CommandException && ex is not CliFx.Exceptions.CommandException) @@ -40,4 +61,9 @@ public async ValueTask ExecuteAsync(IConsole console) throw new CommandException("Cancelled", CommandExitCode.UserCancelled); } } + + private string? GetCommandName() + { + return GetType().GetCustomAttribute(true)?.Name; + } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cli/Meadow.CLI.csproj b/Source/v2/Meadow.Cli/Meadow.CLI.csproj index a0d6bcda..1f847eaf 100644 --- a/Source/v2/Meadow.Cli/Meadow.CLI.csproj +++ b/Source/v2/Meadow.Cli/Meadow.CLI.csproj @@ -30,15 +30,16 @@ - - - - - - - - - + + + + + + + + + + diff --git a/Source/v2/Meadow.Cli/Program.cs b/Source/v2/Meadow.Cli/Program.cs index 99f3839e..e11e275d 100644 --- a/Source/v2/Meadow.Cli/Program.cs +++ b/Source/v2/Meadow.Cli/Program.cs @@ -5,6 +5,7 @@ using Meadow.Cloud.Client.Identity; using Meadow.Package; using Meadow.Software; +using Meadow.Telemetry; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -93,6 +94,10 @@ public static async Task Main(string[] _) Console.WriteLine($"Operation failed: {ex.Message}"); returnCode = 1; } + finally + { + MeadowTelemetry.Current.Dispose(); + } return returnCode; } diff --git a/Source/v2/Meadow.Cli/Strings.cs b/Source/v2/Meadow.Cli/Strings.cs index be0e437b..1524cd4b 100644 --- a/Source/v2/Meadow.Cli/Strings.cs +++ b/Source/v2/Meadow.Cli/Strings.cs @@ -1,4 +1,6 @@ -namespace Meadow.CLI; +using Meadow.Telemetry; + +namespace Meadow.CLI; public static class Strings { @@ -65,4 +67,19 @@ public static class Strings public const string NewMeadowDeviceNotFound = "New Meadow device not found"; public const string NoFirmwarePackagesFound = "No firmware packages found, run 'meadow firmware download' to download the latest firmware"; public const string NoDefaultFirmwarePackageSet = "No default firmware package set, run 'meadow firmware default' to set the default firmware"; + + public static class Telemetry + { + public const string ConsentMessage = @$" +Let's improve the Meadow experience together +-------------------------------------------- +To help improve the Meadow experience, we'd like to collect anonymous usage data. This data helps us understand how our tools are used, so we can make them better for everyone. This usage data is not tied to individuals and no personally identifiable information is collected. + +Our privacy policy is available at https://www.wildernesslabs.co/privacy-policy. + +You can change your mind at any time by running the ""[bold]meadow telemetry [[enable|disable]][/]"" command or by setting the [bold]{MeadowTelemetry.TelemetryEnvironmentVariable}[/] environment variable to '1' or '0' ('true' or 'false', respectively). +"; + + public const string AskToParticipate = "Would you like to participate?"; + } } \ No newline at end of file diff --git a/Source/v2/Meadow.Cloud.Client/Meadow.Cloud.Client.csproj b/Source/v2/Meadow.Cloud.Client/Meadow.Cloud.Client.csproj index c0d51f6e..a113572c 100644 --- a/Source/v2/Meadow.Cloud.Client/Meadow.Cloud.Client.csproj +++ b/Source/v2/Meadow.Cloud.Client/Meadow.Cloud.Client.csproj @@ -1,5 +1,5 @@  - + netstandard2.0 enable @@ -8,12 +8,12 @@ - + - - + + diff --git a/Source/v2/Meadow.Dfu/Meadow.Dfu.csproj b/Source/v2/Meadow.Dfu/Meadow.Dfu.csproj index c00495da..97bbaea2 100644 --- a/Source/v2/Meadow.Dfu/Meadow.Dfu.csproj +++ b/Source/v2/Meadow.Dfu/Meadow.Dfu.csproj @@ -1,9 +1,9 @@  - netstandard2.0 - enable - 10 + netstandard2.0 + enable + 10 diff --git a/Source/v2/Meadow.Firmware/Meadow.Firmware.csproj b/Source/v2/Meadow.Firmware/Meadow.Firmware.csproj index 090a206c..20695d89 100644 --- a/Source/v2/Meadow.Firmware/Meadow.Firmware.csproj +++ b/Source/v2/Meadow.Firmware/Meadow.Firmware.csproj @@ -2,9 +2,9 @@ net8.0 - enable - enable - 10 + enable + enable + 10 @@ -16,7 +16,7 @@ - + diff --git a/Source/v2/Meadow.Hcom/Meadow.HCom.csproj b/Source/v2/Meadow.Hcom/Meadow.HCom.csproj index afe28480..92152ab6 100644 --- a/Source/v2/Meadow.Hcom/Meadow.HCom.csproj +++ b/Source/v2/Meadow.Hcom/Meadow.HCom.csproj @@ -3,7 +3,7 @@ netstandard2.0 enable - 10 + 10 diff --git a/Source/v2/Meadow.Linker/Meadow.Linker.csproj b/Source/v2/Meadow.Linker/Meadow.Linker.csproj index 06d572c1..f94cfcd1 100644 --- a/Source/v2/Meadow.Linker/Meadow.Linker.csproj +++ b/Source/v2/Meadow.Linker/Meadow.Linker.csproj @@ -1,9 +1,9 @@  - netstandard2.0 - enable - 10 + netstandard2.0 + enable + 10 @@ -12,18 +12,18 @@ - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + diff --git a/Source/v2/Meadow.Tooling.Core/Meadow.Tooling.Core.csproj b/Source/v2/Meadow.Tooling.Core/Meadow.Tooling.Core.csproj index 6fe571c8..b8a06474 100644 --- a/Source/v2/Meadow.Tooling.Core/Meadow.Tooling.Core.csproj +++ b/Source/v2/Meadow.Tooling.Core/Meadow.Tooling.Core.csproj @@ -2,15 +2,16 @@ netstandard2.0;net8.0 - enable - 10 + enable + 10 - - + + - + + diff --git a/Source/v2/Meadow.Tooling.Core/Settings/SettingsManager.cs b/Source/v2/Meadow.Tooling.Core/Settings/SettingsManager.cs index 221a7c73..40a06965 100644 --- a/Source/v2/Meadow.Tooling.Core/Settings/SettingsManager.cs +++ b/Source/v2/Meadow.Tooling.Core/Settings/SettingsManager.cs @@ -33,14 +33,23 @@ public Dictionary GetPublicSettings() public string? GetSetting(string setting) { var settings = GetSettings(); - if (settings.Public.TryGetValue(setting.ToString(), out var ret)) + Dictionary target; + + if (setting.StartsWith(PrivatePrefix)) + { + setting = setting.Substring(PrivatePrefix.Length); + target = settings.Private; + } + else { - return ret; + target = settings.Public; } - else if (settings.Private.TryGetValue(setting.ToString(), out var pret)) + + if (target.TryGetValue(setting, out var value)) { - return pret; + return value; } + return null; } @@ -59,7 +68,7 @@ public void DeleteSetting(string setting) target = settings.Public; } - if (target.ContainsKey(setting.ToString())) + if (target.ContainsKey(setting)) { target.Remove(setting); diff --git a/Source/v2/Meadow.Tooling.Core/Telemetry/CIEnvironmentDetector.cs b/Source/v2/Meadow.Tooling.Core/Telemetry/CIEnvironmentDetector.cs new file mode 100644 index 00000000..458733e3 --- /dev/null +++ b/Source/v2/Meadow.Tooling.Core/Telemetry/CIEnvironmentDetector.cs @@ -0,0 +1,79 @@ +// This contains code from the .NET SDK project at +// https://github.com/dotnet/sdk/blob/v8.0.203/src/Cli/dotnet/Telemetry/CIEnvironmentDetectorForTelemetry.cs. +// +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// +// It has been modified by Wilderness Labs and are licensed under the Apache License, Version 2.0. +// You may obtain a copy of the License at https://github.com/WildernessLabs/Meadow.CLI/blob/main/LICENSE +using System; +using System.Linq; + +namespace Meadow.Telemetry; + +internal static class CIEnvironmentDetector +{ + // Systems that provide boolean values only, so we can simply parse and check for true + private static readonly string[] _booleanVariables = new string[] { + // Azure Pipelines - https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables#system-variables-devops-services + "TF_BUILD", + // GitHub Actions - https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables + "GITHUB_ACTIONS", + // AppVeyor - https://www.appveyor.com/docs/environment-variables/ + "APPVEYOR", + // A general-use flag - Many of the major players support this: AzDo, GitHub, GitLab, AppVeyor, Travis CI, CircleCI. + // Given this, we could potentially remove all of these other options? + "CI", + // Travis CI - https://docs.travis-ci.com/user/environment-variables/#default-environment-variables + "TRAVIS", + // CircleCI - https://circleci.com/docs/2.0/env-vars/#built-in-environment-variables + "CIRCLECI", + }; + + // Systems where every variable must be present and not-null before returning true + private static readonly string[][] _allNotNullVariables = new string[][] { + // AWS CodeBuild - https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-env-vars.html + new string[]{ "CODEBUILD_BUILD_ID", "AWS_REGION" }, + // Jenkins - https://github.com/jenkinsci/jenkins/blob/master/core/src/main/resources/jenkins/model/CoreEnvironmentContributor/buildEnv.groovy + new string[]{ "BUILD_ID", "BUILD_URL" }, + // Google Cloud Build - https://cloud.google.com/build/docs/configuring-builds/substitute-variable-values#using_default_substitutions + new string[]{ "BUILD_ID", "PROJECT_ID" } + }; + + // Systems where the variable must be present and not-null + private static readonly string[] _ifNonNullVariables = new string[] { + // TeamCity - https://www.jetbrains.com/help/teamcity/predefined-build-parameters.html#Predefined+Server+Build+Parameters + "TEAMCITY_VERSION", + // JetBrains Space - https://www.jetbrains.com/help/space/automation-environment-variables.html#general + "JB_SPACE_API_URL" + }; + + public static bool IsCIEnvironment() + { + foreach (var booleanVariable in _booleanVariables) + { + if (bool.TryParse(Environment.GetEnvironmentVariable(booleanVariable), out bool envVar) && envVar) + { + return true; + } + } + + foreach (var variables in _allNotNullVariables) + { + if (variables.All((variable) => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(variable)))) + { + return true; + } + } + + foreach (var variable in _ifNonNullVariables) + { + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(variable))) + { + return true; + } + } + + return false; + } +} diff --git a/Source/v2/Meadow.Tooling.Core/Telemetry/MeadowTelemetry.cs b/Source/v2/Meadow.Tooling.Core/Telemetry/MeadowTelemetry.cs new file mode 100644 index 00000000..3e967e9e --- /dev/null +++ b/Source/v2/Meadow.Tooling.Core/Telemetry/MeadowTelemetry.cs @@ -0,0 +1,197 @@ +using Meadow.CLI; +using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.DataContracts; +using Microsoft.ApplicationInsights.Extensibility; +using System; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Threading; + +namespace Meadow.Telemetry; + +public sealed class MeadowTelemetry : IDisposable +{ + private const string ConnectionString = "InstrumentationKey=1c5d5d93-e50e-47bc-b14e-23e32dc1bcac"; + private static readonly Lazy _current = new(() => new MeadowTelemetry(new SettingsManager())); + + public const string TelemetryEnvironmentVariable = "MEADOW_TELEMETRY"; + public const string MachineIdSettingName = "private.telemetry.machineid"; + public const string TelemetryEnabledSettingName = "telemetry.enabled"; + + private readonly ISettingsManager _settingsManager; + private TelemetryConfiguration? _telemetryConfiguration; + + public TelemetryClient? TelemetryClient { get; private set; } + + public static MeadowTelemetry Current => _current.Value; + + public MeadowTelemetry(ISettingsManager settingsManager) + { + _settingsManager = settingsManager ?? throw new ArgumentNullException(nameof(settingsManager)); + Initialize(); + } + + private void Initialize() + { + if (TelemetryClient != null) + { + return; + } + + _telemetryConfiguration = CreateTelemetryConfiguration(); + + if (_telemetryConfiguration == null) + { + return; + } + + TelemetryClient = new TelemetryClient(_telemetryConfiguration); + ConfigureTelemetryContext(TelemetryClient.Context); + } + + public void Dispose() + { + TelemetryClient?.FlushAsync(CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); + _telemetryConfiguration?.Dispose(); + + TelemetryClient = null; + _telemetryConfiguration = null; + } + + public bool IsEnabled + { + get + { + if (CIEnvironmentDetector.IsCIEnvironment()) + { + return false; + } + + if (bool.TryParse(Environment.GetEnvironmentVariable(TelemetryEnvironmentVariable), out bool isEnabled)) + { + return isEnabled; + } + + if (bool.TryParse(_settingsManager.GetSetting(TelemetryEnabledSettingName), out isEnabled)) + { + return isEnabled; + } + + return false; + } + } + + public bool ShouldAskForConsent + { + get + { + if (CIEnvironmentDetector.IsCIEnvironment()) + { + return false; + } + + var envVar = Environment.GetEnvironmentVariable(TelemetryEnvironmentVariable); + if (!string.IsNullOrWhiteSpace(envVar) && bool.TryParse(envVar, out _)) + { + return false; + } + + var setting = _settingsManager.GetSetting(TelemetryEnabledSettingName); + if (!string.IsNullOrWhiteSpace(setting) && bool.TryParse(setting, out _)) + { + return false; + } + + return true; + } + } + + public void SetTelemetryEnabled(bool enabled) + { + _settingsManager.SaveSetting(TelemetryEnabledSettingName, enabled.ToString().ToLowerInvariant()); + + if (enabled) + { + Initialize(); + } + else + { + Dispose(); + _settingsManager.DeleteSetting(MachineIdSettingName); + } + } + + public void TrackCommand(string? commandName) + { + if (TelemetryClient == null) + { + return; + } + + var eventTelemetry = new EventTelemetry("meadow/cli/command") + { + Timestamp = DateTimeOffset.UtcNow + }; + + eventTelemetry.Properties["Command"] = commandName?.ToLowerInvariant() ?? ""; + TelemetryClient.TrackEvent(eventTelemetry); + } + + private TelemetryConfiguration? CreateTelemetryConfiguration() + { + if (!IsEnabled) + { + return null; + } + + var telemetryConfiguration = new TelemetryConfiguration + { + ConnectionString = ConnectionString + }; + +#if DEBUG + telemetryConfiguration.TelemetryChannel.DeveloperMode = true; +#endif + + return telemetryConfiguration; + } + + private void ConfigureTelemetryContext(TelemetryContext context) + { + context.Cloud.RoleInstance = ""; + context.Device.Type = ""; + context.Location.Ip = "0.0.0.0"; + context.Session.Id = GetRandomString(24); + context.User.Id = GetMachineId(); + + context.GlobalProperties["OS Version"] = RuntimeEnvironment.OperatingSystemVersion; + context.GlobalProperties["OS Platform"] = RuntimeEnvironment.OperatingSystemPlatform.ToString(); + context.GlobalProperties["OS Architecture"] = RuntimeInformation.OSArchitecture.ToString(); + context.GlobalProperties["Kernel Version"] = RuntimeInformation.OSDescription; + context.GlobalProperties["Runtime Identifier"] = AppContext.GetData("RUNTIME_IDENTIFIER") as string ?? ""; + context.GlobalProperties["Assembly Name"] = Assembly.GetEntryAssembly()?.GetName().Name; + context.GlobalProperties["Assembly Version"] = Assembly.GetEntryAssembly()?.GetName().Version?.ToString(3); + } + + private string GetMachineId() + { + var machineId = _settingsManager.GetSetting(MachineIdSettingName) ?? string.Empty; + if (string.IsNullOrWhiteSpace(machineId)) + { + machineId = Guid.NewGuid().ToString("N"); + _settingsManager.SaveSetting(MachineIdSettingName, machineId); + } + + return machineId; + } + + private static string GetRandomString(int length) + { + using var rng = RandomNumberGenerator.Create(); + byte[] bytes = new byte[length]; + rng.GetBytes(bytes); + + return Convert.ToBase64String(bytes); + } +} diff --git a/Source/v2/Meadow.Tooling.Core/Telemetry/RuntimeEnvironment.cs b/Source/v2/Meadow.Tooling.Core/Telemetry/RuntimeEnvironment.cs new file mode 100644 index 00000000..f8be50ca --- /dev/null +++ b/Source/v2/Meadow.Tooling.Core/Telemetry/RuntimeEnvironment.cs @@ -0,0 +1,281 @@ +// This contains code from the .NET SDK project at +// https://github.com/dotnet/sdk/blob/v8.0.203/src/Cli/Microsoft.DotNet.Cli.Utils/RuntimeEnvironment.cs. +// +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// +// It has been modified by Wilderness Labs and are licensed under the Apache License, Version 2.0. +// You may obtain a copy of the License at https://github.com/WildernessLabs/Meadow.CLI/blob/main/LICENSE + +#nullable disable + +using System.Runtime.InteropServices; +using System; +using System.IO; + +namespace Meadow.Telemetry; + +internal enum Platform +{ + Unknown = 0, + Windows = 1, + Linux = 2, + Darwin = 3, + FreeBSD = 4, + illumos = 5, + Solaris = 6, + Haiku = 7 +} + +internal static class RuntimeEnvironment +{ + private static readonly Lazy _platform = new(DetermineOSPlatform); + private static readonly Lazy _distroInfo = new(LoadDistroInfo); + + public static Platform OperatingSystemPlatform { get; } = GetOSPlatform(); + public static string OperatingSystemVersion { get; } = GetOSVersion(); + public static string OperatingSystem { get; } = GetOSName(); + + private class DistroInfo + { + public string Id; + public string VersionId; + } + + private static string GetOSName() + { + switch (GetOSPlatform()) + { + case Platform.Windows: + return nameof(Platform.Windows); + case Platform.Linux: + return GetDistroId() ?? nameof(Platform.Linux); + case Platform.Darwin: + return "Mac OS X"; + case Platform.FreeBSD: + return nameof(Platform.FreeBSD); + case Platform.illumos: + return GetDistroId() ?? nameof(Platform.illumos); + case Platform.Solaris: + return nameof(Platform.Solaris); + case Platform.Haiku: + return nameof(Platform.Haiku); + default: + return nameof(Platform.Unknown); + } + } + + private static string GetOSVersion() + { + switch (GetOSPlatform()) + { + case Platform.Windows: + return Environment.OSVersion.Version.ToString(3); + case Platform.Linux: + case Platform.illumos: + return GetDistroVersionId() ?? string.Empty; + case Platform.Darwin: + return Environment.OSVersion.Version.ToString(2); + case Platform.FreeBSD: + return GetFreeBSDVersion() ?? string.Empty; + case Platform.Solaris: + // RuntimeInformation.OSDescription example on Solaris 11.3: SunOS 5.11 11.3 + // we only need the major version; 11 + return RuntimeInformation.OSDescription.Split(' ')[2].Split('.')[0]; + case Platform.Haiku: + return Environment.OSVersion.Version.ToString(1); + default: + return string.Empty; + } + } + + private static string GetFreeBSDVersion() + { + // This is same as sysctl kern.version + // FreeBSD 11.0-RELEASE-p1 FreeBSD 11.0-RELEASE-p1 #0 r306420: Thu Sep 29 01:43:23 UTC 2016 root@releng2.nyi.freebsd.org:/usr/obj/usr/src/sys/GENERIC + // What we want is major release as minor releases should be compatible. + string version = RuntimeInformation.OSDescription; + try + { + // second token up to first dot + return RuntimeInformation.OSDescription.Split()[1].Split('.')[0]; + } + catch + { + } + return string.Empty; + } + + private static Platform GetOSPlatform() + { + return _platform.Value; + } + + private static string GetDistroId() + { + return _distroInfo.Value?.Id; + } + + private static string GetDistroVersionId() + { + return _distroInfo.Value?.VersionId; + } + + private static DistroInfo LoadDistroInfo() + { + switch (GetOSPlatform()) + { + case Platform.Linux: + return LoadDistroInfoFromLinux(); + case Platform.illumos: + return LoadDistroInfoFromIllumos(); + } + + return null; + } + + private static DistroInfo LoadDistroInfoFromLinux() + { + DistroInfo result = null; + + // Sample os-release file: + // NAME="Ubuntu" + // VERSION = "14.04.3 LTS, Trusty Tahr" + // ID = ubuntu + // ID_LIKE = debian + // PRETTY_NAME = "Ubuntu 14.04.3 LTS" + // VERSION_ID = "14.04" + // HOME_URL = "http://www.ubuntu.com/" + // SUPPORT_URL = "http://help.ubuntu.com/" + // BUG_REPORT_URL = "http://bugs.launchpad.net/ubuntu/" + // We use ID and VERSION_ID + + if (File.Exists("/etc/os-release")) + { + var lines = File.ReadAllLines("/etc/os-release"); + result = new DistroInfo(); + foreach (var line in lines) + { + if (line.StartsWith("ID=", StringComparison.Ordinal)) + { + result.Id = line.Substring(3).Trim('"', '\''); + } + else if (line.StartsWith("VERSION_ID=", StringComparison.Ordinal)) + { + result.VersionId = line.Substring(11).Trim('"', '\''); + } + } + } + + if (result != null) + { + result = NormalizeDistroInfo(result); + } + + return result; + } + + private static DistroInfo LoadDistroInfoFromIllumos() + { + DistroInfo result = null; + // examples: + // on OmniOS + // SunOS 5.11 omnios-r151018-95eaa7e + // on OpenIndiana Hipster: + // SunOS 5.11 illumos-63878f749f + // on SmartOS: + // SunOS 5.11 joyent_20200408T231825Z + var versionDescription = RuntimeInformation.OSDescription.Split(' ')[2]; + switch (versionDescription) + { + case string version when version.StartsWith("omnios"): + result = new DistroInfo + { + Id = "OmniOS", + VersionId = version.Substring("omnios-r".Length, 2) // e.g. 15 + }; + break; + case string version when version.StartsWith("joyent"): + result = new DistroInfo + { + Id = "SmartOS", + VersionId = version.Substring("joyent_".Length, 4) // e.g. 2020 + }; + break; + case string version when version.StartsWith("illumos"): + result = new DistroInfo + { + Id = "OpenIndiana" + // version-less + }; + break; + } + + return result; + } + + // For some distros, we don't want to use the full version from VERSION_ID. One example is + // Red Hat Enterprise Linux, which includes a minor version in their VERSION_ID but minor + // versions are backwards compatable. + // + // In this case, we'll normalized RIDs like 'rhel.7.2' and 'rhel.7.3' to a generic + // 'rhel.7'. This brings RHEL in line with other distros like CentOS or Debian which + // don't put minor version numbers in their VERSION_ID fields because all minor versions + // are backwards compatible. + private static DistroInfo NormalizeDistroInfo(DistroInfo distroInfo) + { + // Handle if VersionId is null by just setting the index to -1. + int lastVersionNumberSeparatorIndex = distroInfo.VersionId?.IndexOf('.') ?? -1; + + if (lastVersionNumberSeparatorIndex != -1 && distroInfo.Id == "alpine") + { + // For Alpine, the version reported has three components, so we need to find the second version separator + lastVersionNumberSeparatorIndex = distroInfo.VersionId.IndexOf('.', lastVersionNumberSeparatorIndex + 1); + } + + if (lastVersionNumberSeparatorIndex != -1 && (distroInfo.Id == "rhel" || distroInfo.Id == "alpine")) + { + distroInfo.VersionId = distroInfo.VersionId.Substring(0, lastVersionNumberSeparatorIndex); + } + + return distroInfo; + } + + private static Platform DetermineOSPlatform() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return Platform.Windows; + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return Platform.Linux; + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return Platform.Darwin; + } +#if NETCOREAPP + if (RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD)) + { + return Platform.FreeBSD; + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("ILLUMOS"))) + { + return Platform.illumos; + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("SOLARIS"))) + { + return Platform.Solaris; + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("HAIKU"))) + { + return Platform.Haiku; + } +#endif + + return Platform.Unknown; + } +} + +#nullable enable \ No newline at end of file diff --git a/Source/v2/Meadow.UsbLib.Core/Meadow.UsbLib.Core.csproj b/Source/v2/Meadow.UsbLib.Core/Meadow.UsbLib.Core.csproj index a20a37f9..00a0d973 100644 --- a/Source/v2/Meadow.UsbLib.Core/Meadow.UsbLib.Core.csproj +++ b/Source/v2/Meadow.UsbLib.Core/Meadow.UsbLib.Core.csproj @@ -1,8 +1,8 @@  - netstandard2.0 - 10 + netstandard2.0 + 10 diff --git a/Source/v2/Meadow.UsbLib/Meadow.UsbLib.csproj b/Source/v2/Meadow.UsbLib/Meadow.UsbLib.csproj index 0894bbf6..1ba17b4d 100644 --- a/Source/v2/Meadow.UsbLib/Meadow.UsbLib.csproj +++ b/Source/v2/Meadow.UsbLib/Meadow.UsbLib.csproj @@ -2,16 +2,16 @@ net5.0 - 10 - enable + 10 + enable - - - + + + - - - + + + diff --git a/Source/v2/Meadow.UsbLibClassic/Meadow.UsbLibClassic.csproj b/Source/v2/Meadow.UsbLibClassic/Meadow.UsbLibClassic.csproj index 608d4def..76aa4cfe 100644 --- a/Source/v2/Meadow.UsbLibClassic/Meadow.UsbLibClassic.csproj +++ b/Source/v2/Meadow.UsbLibClassic/Meadow.UsbLibClassic.csproj @@ -1,17 +1,17 @@  - netstandard2.0 - 10 - enable + netstandard2.0 + 10 + enable - - - + + + - - - + + + diff --git a/Source/v2/Tests/Meadow.Cloud.Client.Unit.Tests/Meadow.Cloud.Client.Unit.Tests.csproj b/Source/v2/Tests/Meadow.Cloud.Client.Unit.Tests/Meadow.Cloud.Client.Unit.Tests.csproj index d100079a..3a981e60 100644 --- a/Source/v2/Tests/Meadow.Cloud.Client.Unit.Tests/Meadow.Cloud.Client.Unit.Tests.csproj +++ b/Source/v2/Tests/Meadow.Cloud.Client.Unit.Tests/Meadow.Cloud.Client.Unit.Tests.csproj @@ -11,8 +11,8 @@ - all - runtime; build; native; contentfiles; analyzers; buildtransitive + all + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -28,7 +28,7 @@ - + diff --git a/Source/v2/Tests/Meadow.SoftwareManager.Integration.Tests/Meadow.SoftwareManager.Integration.Tests.csproj b/Source/v2/Tests/Meadow.SoftwareManager.Integration.Tests/Meadow.SoftwareManager.Integration.Tests.csproj index 070df34c..1487cdea 100644 --- a/Source/v2/Tests/Meadow.SoftwareManager.Integration.Tests/Meadow.SoftwareManager.Integration.Tests.csproj +++ b/Source/v2/Tests/Meadow.SoftwareManager.Integration.Tests/Meadow.SoftwareManager.Integration.Tests.csproj @@ -22,8 +22,8 @@ - - + +