// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Threading.Tasks; using CoCoL; namespace Duplicati.Library.UsageReporter { /// /// The usage reporter library interface /// public static class Reporter { /// /// The tag used for logging /// private static readonly string LOGTAG = Logging.Log.LogTagFromType(typeof(Reporter)); /// /// The primary input channel for new report messages /// private static IWriteChannel _eventChannel; /// /// The task to await before shutdown /// private static Task ShutdownTask; /// /// Reports an event, information by default /// /// The event name /// The event data /// The event type public static void Report(string key, string data = null, ReportType type = ReportType.Information) { if (_eventChannel != null && type >= MaxReportLevel) try { _eventChannel.WriteNoWait(new ReportItem(type, null, key, data)); } catch { } } /// /// Reports an event, information by default /// /// The event name /// The event count /// The event type public static void Report(string key, long count, ReportType type = ReportType.Information) { if (_eventChannel != null && type >= MaxReportLevel) try { _eventChannel.WriteNoWait(new ReportItem(type, count, key, count.ToString())); } catch { } } /// /// Reports an exception event, error by default /// /// The exception /// The event type public static void Report(Exception ex, ReportType type = ReportType.Warning) { if (_eventChannel != null && type >= MaxReportLevel) try { _eventChannel.WriteNoWait(new ReportItem(type, null, "EXCEPTION", ex.ToString())); } catch { } } /// /// Initializes the usage reporter library /// public static void Initialize() { if (_eventChannel == null || _eventChannel.IsRetiredAsync.Result) { if (IsDisabled) return; var rsu = ReportSetUploader.Run(); var ep = EventProcessor.Run(rsu.Item2); _eventChannel = ep.Item2; ShutdownTask = Task.WhenAll(ep.Item1, rsu.Item1); // TODO: Disable on debug builds AppDomain.CurrentDomain.UnhandledException += HandleUncaughtException; //AppDomain.CurrentDomain.UnhandledException += HandleUncaughtException; //AppDomain.CurrentDomain.ProcessExit Report("Started"); } } /// /// Handles an uncaught exception. /// /// Sender. /// Arguments. private static void HandleUncaughtException(object sender, UnhandledExceptionEventArgs args) { if (args.ExceptionObject is Exception exception) Report(exception, ReportType.Crash); } /// /// Terminates the usage reporter library /// public static void ShutDown() { if (_eventChannel != null && !_eventChannel.IsRetiredAsync.Result) _eventChannel.Retire(); if (ShutdownTask != null) { ShutdownTask.Wait(TimeSpan.FromSeconds(30)); if (!ShutdownTask.IsCompleted) Logging.Log.WriteWarningMessage(LOGTAG, "ReporterShutdownFailuer", null, "Failed to shut down usage reporter after 30 seconds, leaving hanging ..."); } AppDomain.CurrentDomain.UnhandledException -= HandleUncaughtException; } /// /// Allow opt-out /// internal const string DISABLED_ENVNAME_TEMPLATE = "USAGEREPORTER_{0}_LEVEL"; /// /// Cached value with the max report level /// private static ReportType? Cached_MaxReportLevel; /// /// A value indicating if the usage reporter library is forced disabled /// private static bool Forced_Disabled = false; /// /// Gets the environment default report level /// /// The system default report level. public static string DefaultReportLevel { get { return IsDisabledByEnvironment ? "Disabled" : MaxReportLevel.ToString(); } } /// /// The maximum allowed report level /// /// The type of the max report. private static ReportType MaxReportLevel { get { if (Cached_MaxReportLevel == null) { var str = Environment.GetEnvironmentVariable(string.Format(DISABLED_ENVNAME_TEMPLATE, Duplicati.Library.AutoUpdater.AutoUpdateSettings.AppName)); ReportType tmp; if (string.IsNullOrWhiteSpace(str) || !Enum.TryParse(str, true, out tmp)) Cached_MaxReportLevel = ReportType.Information; else Cached_MaxReportLevel = tmp; } return Cached_MaxReportLevel.Value; } } /// /// Gets a value indicating if the user has opted out of usage reporting /// /// true if is disabled; otherwise, false. private static bool IsDisabled { get { if (Forced_Disabled) return true; return IsDisabledByEnvironment; } } /// /// Gets a value indicating if the user has opted out of usage reporting, /// but without reading the local override option /// /// true if is disabled; otherwise, false. private static bool IsDisabledByEnvironment { get { var str = Environment.GetEnvironmentVariable(string.Format(DISABLED_ENVNAME_TEMPLATE, AutoUpdater.AutoUpdateSettings.AppName)); #if DEBUG // Default to not report crashes etc from debug builds if (string.IsNullOrWhiteSpace(str)) str = "none"; #endif return string.Equals(str, "none", StringComparison.OrdinalIgnoreCase) || Utility.Utility.ParseBool(str, false); } } /// /// Sets the usage reporter level /// /// The maximum level of events to report, or null to set default. /// True to disable usage reportingfalse otherwise. public static void SetReportLevel(ReportType? maxreportlevel, bool disable) { if (disable) { Forced_Disabled = true; ShutDown(); } else { Forced_Disabled = false; Cached_MaxReportLevel = maxreportlevel; Initialize(); } } } }