// Copyright (C) 2025, 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.IO; using System.Threading.Tasks; namespace Duplicati.Library.Crashlog; #nullable enable /// /// Utility class to wrap a method in a try-catch block and log any exceptions to a file /// public static class CrashlogHelper { /// /// The system temp path /// private static readonly string SystemTempPath #if DEBUG = Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly()?.Location) ?? System.IO.Path.GetTempPath(); #else = System.IO.Path.GetTempPath(); #endif /// /// The default directory to write crashlogs to /// public static string DefaultLogDir { get; set; } = SystemTempPath; /// /// Event to subscribe to for unobserved task exceptions /// public static event Action? OnUnobservedTaskException; /// /// Handler for unobserved task exceptions /// /// Unused sender /// The exception event args private static void TaskSchedulerOnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e) { try { OnUnobservedTaskException?.Invoke(e.Exception); } catch { } } /// /// Wraps a method in a try-catch block and logs any exceptions to a file /// /// The return type of the method /// The method to wrap /// The directory to write the crashlog to /// The name of the application to write to the crashlog /// The result of the method public static T WrapWithCrashLog(Func method, string? logdir = null, string? applicationName = null) { try { TaskScheduler.UnobservedTaskException += TaskSchedulerOnUnobservedTaskException; return method(); } catch (Exception ex) { LogCrashException(ex, logdir, applicationName); throw; } finally { TaskScheduler.UnobservedTaskException -= TaskSchedulerOnUnobservedTaskException; } } /// /// Wraps a method in a try-catch block and logs any exceptions to a file /// /// The return type of the method /// The method to wrap /// The directory to write the crashlog to /// The name of the application to write to the crashlog /// The result of the method public static async Task WrapWithCrashLog(Func> method, string? logdir = null, string? applicationName = null) { try { TaskScheduler.UnobservedTaskException += TaskSchedulerOnUnobservedTaskException; return await method().ConfigureAwait(false); } catch (Exception ex) { LogCrashException(ex, logdir, applicationName); throw; } finally { TaskScheduler.UnobservedTaskException -= TaskSchedulerOnUnobservedTaskException; } } /// /// Logs the exception to a file /// /// The exception to log /// The directory to write the crashlog to /// The name of the application to write to the crashlog public static void LogCrashException(Exception ex, string? logdir, string? applicationName) { try { Console.WriteLine("Crash! {0}{1}", Environment.NewLine, ex); } catch { } try { if (string.IsNullOrWhiteSpace(logdir)) { var def = DefaultLogDir; logdir = string.IsNullOrWhiteSpace(def) ? SystemTempPath : def; } applicationName = string.IsNullOrWhiteSpace(applicationName) ? System.Reflection.Assembly.GetEntryAssembly()?.GetName().Name ?? Guid.NewGuid().ToString()[..8] : applicationName; var report_file = System.IO.Path.Combine(logdir, $"{applicationName}-crashlog.txt"); System.IO.File.WriteAllText(report_file, ex.ToString()); } catch (Exception writeex) { try { Console.WriteLine("Failed to write crashlog: {0}", writeex); } catch { } } } }