using System; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Threading; using Bloomberg.Client.Components; using Bloomberg.Client.Components.Connection; using Bloomberg.Client.Components.Settings; using Bloomberg.Client.Crash; using Bloomberg.Client.Report; using Bloomberg.Message.Components; using Bloomberg.Message.Entities; using System.Linq; using System.Diagnostics; using JD.Core; namespace Bloomberg.Client { public class Reporter : IDisposable { public const string BloombergVersion = "2.2.0.4641"; public const int MaximumChangelistsFromIniFile = 10; public const int MaximumConnectionsFromIniFile = 10; #region Private members private readonly CrashHandler _crashHandler = new CrashHandler(); private InternalLogFile _logFile; private RemoteDataWriter _remoteDataWriter = new RemoteDataWriter(); private Mutex _crashAlreadyHandledFlag; private Mutex _managedFlagMutex; private const string CRASH_ALREADY_HANDLED_FLAG_BASE = "CRASH_ALREADY_HANDLED_FLAG"; private const string NATIVE_BLOOMBERG_MUTEX_NAME_BASE = "BLOOMBERG_NATIVE_CLIENT_FLAG"; private const string MANAGED_BLOOMBERG_MUTEX_NAME_BASE = "BLOOMBERG_MANAGED_CLIENT_FLAG"; private string _crashAlreadyHandledFlagName; private string _nativeBloombergMutexName; private string _managedBloombergMutexName; #endregion #region Properties /// /// Singleton /// public static Reporter Instance { get; private set; } public bool HasBeenInit { get; private set; } public BloombergInterface BloombergInterface { get; private set; } public ReportManager ReportManager { get; private set; } public RemoteDataCollector RemoteDataCollector { get; private set; } public int RemoteDataPort { get { return _remoteDataWriter.Port; } } /// /// Specify if the crash reporting is still enabled. /// This can be changed when connection to server or reporter failed. /// public bool CrashReportingEnabled { get { return _crashHandler.IsEnabled; } set { _crashHandler.IsEnabled = value; } } private BloombergSettings _bloombergSettings; public BloombergSettings BloombergSettings { get { return _bloombergSettings; } private set { _bloombergSettings = value; } } private GameSettings _gameSettings; public GameSettings GameSettings { get { return _gameSettings; } private set { _gameSettings = value; } } public ConnectionManager ConnectionManager { get; private set; } private SubTypeManager _subTypeManager; public SubTypeManager SubTypeManager { get { return _subTypeManager; } private set { _subTypeManager = value; } } #endregion #region Initialization /// /// Private constructor called by Initialize /// /// private Reporter(BloombergInterface bloombergInterface) { BloombergInterface = bloombergInterface; ReportManager = new ReportManager(); RemoteDataCollector = new RemoteDataCollector(); ConnectionManager = new ConnectionManager(); SubTypeManager = new SubTypeManager(); HasBeenInit = false; } /// /// Client has already instantiated his interface /// /// The interface implemented by the client /// True to enable crash handling public static void Initialize(BloombergInterface bloombergInterface, bool crashHandlerEnabled = true) { if (bloombergInterface == null) throw new NullReferenceException("The bloombergInterface parameter cannot be null"); if (Instance == null) { Instance = new Reporter(bloombergInterface); Instance.CrashReportingEnabled = crashHandlerEnabled; Instance.InternalInit(); Tracer.Log(TracerLogLevel.INFO, "Reporter initialized"); } else { Tracer.Log(TracerLogLevel.INFO, "Reporter already initialized."); } } /// /// Un uninitialize Bloomberg /// public static void UnInitialize() { if (Instance != null) { Instance.InternalUnInit(); } } /// /// This does the initialization of bloomberg. /// - Get the GameSettings & BloombergSettings /// - Initialize log file /// - VerifyConnection /// private void InternalInit() { _logFile = new InternalLogFile(); _logFile.Initialize(); InitGameSettings(); InitBloombergSettings(); InitializeBloombergMutexNames(); if (!CreateBloombergManagedMutex()) { return; } if (!string.IsNullOrEmpty(BloombergSettings.ConfigFilePath) && !BloombergSettings.ConfigFilePath.EndsWith(Path.DirectorySeparatorChar.ToString())) BloombergSettings.ConfigFilePath += Path.DirectorySeparatorChar; string commonFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonDocuments), "Ubisoft\\Bloomberg"); string userFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Ubisoft\\Bloomberg"); var currentAssemblyLocation = Path.GetDirectoryName(GetType().Assembly.Location()); var settingsPaths = new[] { BloombergSettings.ConfigFilePath, "c:\\", "", commonFolder, userFolder, currentAssemblyLocation }; SettingLoader settingLoader = null; foreach (var path in settingsPaths.Skip(string.IsNullOrEmpty(BloombergSettings.ConfigFilePath) ? 1 : 0)) { try { var fullFilePath = Path.Combine(path, BloombergSettings.BloombergSettingsFileName); Tracer.LogFormat(TracerLogLevel.INFO, "Trying to open Bloomberg settings file: {0}.", Path.GetFullPath(fullFilePath)); if (!File.Exists(fullFilePath)) continue; settingLoader = new SettingLoader(fullFilePath); Tracer.LogFormat(TracerLogLevel.INFO, "Reading Bloomberg settings from {0}.", Path.GetFullPath(fullFilePath)); break; } catch (FileNotFoundException) { // try next path } } if (settingLoader != null) { InitBloombergSettingsFromIniFile(settingLoader); InitGameSettingsFromIniFile(settingLoader); InitConnectionsFromIniFile(settingLoader); } else { Tracer.LogFormat(TracerLogLevel.INFO, "{0} not found. Default settings will be used.", BloombergSettings.BloombergSettingsFileName); } BloombergInterface.GetBloombergSettings(ref _bloombergSettings); BloombergInterface.GetGameSettings(ref _gameSettings); _crashHandler.Initialize(); _remoteDataWriter.StartListening(); HasBeenInit = true; Tracer.LogFormat(TracerLogLevel.INFO, "Reporter.InternalInit: Bloomberg is initialized."); } private void InitializeBloombergMutexNames() { int processId = Process.GetCurrentProcess().Id; _crashAlreadyHandledFlagName = CRASH_ALREADY_HANDLED_FLAG_BASE + "_" + processId; _nativeBloombergMutexName = NATIVE_BLOOMBERG_MUTEX_NAME_BASE + "_" + processId; _managedBloombergMutexName = MANAGED_BLOOMBERG_MUTEX_NAME_BASE + "_" + processId; } private bool CreateBloombergManagedMutex() { try { // We open a mutex to let know to the native Bloomberg client that the managed client has been initialized _managedFlagMutex = new Mutex(false, _managedBloombergMutexName); return true; } catch (Exception e) { Tracer.LogFormat(TracerLogLevel.ERROR, "Bloomberg managed Mutex creation failed. Bloomberg client won't be initialized. Is there another bloomberg CSharp client enabled ? Exception : {0}", e.Message); } return false; } private void InitBloombergSettings() { BloombergSettings = new BloombergSettings(); } private void InitGameSettings() { GameSettings = new GameSettings(); } class IniConnectionSpecs { // ReSharper disable UnusedAutoPropertyAccessor.Local // assigned through reflection public ConnectionType? ConnectionType { get; set; } public ReportType? ConnectionReportType { get; set; } public string ConnectionPath { get; set; } public string ConnectionIp { get; set; } public int? ConnectionPort { get; set; } public int? ConnectionRpcPort { get; set; } public string ConnectionProxyRanges { get; set; } // ReSharper restore UnusedAutoPropertyAccessor.Local } private void InitConnectionsFromIniFile(SettingLoader settingLoader) { string userName = null; string reporterMachineName = null; ReadReporterSettings(ref userName, ref reporterMachineName); if (userName != null) BloombergSettings.Username = userName; // iterate through all possible connection indices for (uint connIndex = 0; connIndex < MaximumConnectionsFromIniFile; ++connIndex) { // load and validate generic connection settings var connectionSpecs = new IniConnectionSpecs(); settingLoader.ProcessProperties(connectionSpecs, "Connections", null, connIndex); // Skip unspecified connections. This allows the user to comment a connection without having to renumber all others. if (!connectionSpecs.ConnectionType.HasValue) continue; if (!connectionSpecs.ConnectionReportType.HasValue) { Tracer.LogFormat(TracerLogLevel.WARN, "ConnectionReportType{0} in section [Connections] in {1} must be specified for the connection to be valid.", connIndex, BloombergSettings.BloombergSettingsFileName); continue; } if (connectionSpecs.ConnectionType.Value == ConnectionType.File) { // if it's a file connection, validate it and create it if (connectionSpecs.ConnectionPath == null) { Tracer.LogFormat(TracerLogLevel.WARN, "ConnectionPath{0} in section [Connections] in {1} must be specified for the connection to be valid.", connIndex, BloombergSettings.BloombergSettingsFileName); continue; } ConnectionManager.AddFileConnection(connectionSpecs.ConnectionPath, connectionSpecs.ConnectionReportType.Value); } else { // if it is a socket connection, validate it and create it if (!connectionSpecs.ConnectionPort.HasValue || connectionSpecs.ConnectionPort.Value < 1024 || connectionSpecs.ConnectionPort.Value > UInt16.MaxValue) { Tracer.LogFormat(TracerLogLevel.WARN, "ConnectionPort{0} in section [Connections] in {1} must be a 16-bit number >= 1024 for the connection to be valid.", connIndex, BloombergSettings.BloombergSettingsFileName); continue; } if (string.IsNullOrEmpty(connectionSpecs.ConnectionIp)) { if (connectionSpecs.ConnectionType != ConnectionType.Reporter) { Tracer.LogFormat(TracerLogLevel.WARN, "ConnectionIp{0} in section [Connections] in {1} must be specified for the connection to be valid.", connIndex, BloombergSettings.BloombergSettingsFileName); continue; } connectionSpecs.ConnectionIp = reporterMachineName ?? _bloombergSettings.MachineName; } string[] proxies = null; if (connectionSpecs.ConnectionProxyRanges != null) { proxies = connectionSpecs.ConnectionProxyRanges.Split(',').Where(s => s.Length > 0).ToArray(); } ConnectionManager.AddSocketConnection( new ServerInformation( connectionSpecs.ConnectionIp, connectionSpecs.ConnectionPort.Value, connectionSpecs.ConnectionRpcPort.HasValue ? connectionSpecs.ConnectionRpcPort.Value : 0 ), connectionSpecs.ConnectionReportType.Value, connectionSpecs.ConnectionType.Value, proxies ); } } } private static bool ReadReporterSettings(ref string userName, ref string reporterMachineName) { var settingsPaths = new[] { BloombergSettings.ConfigFilePath, "c:\\", "" }; string[] bloombergConfig = null; try { foreach (var path in settingsPaths.Skip(string.IsNullOrEmpty(BloombergSettings.ConfigFilePath) ? 1 : 0)) { var bloombergConfigPath = Path.Combine(path, BloombergSettings.BloombergConfigFileName); Tracer.LogFormat(TracerLogLevel.INFO, "Trying to open Reporter connection settings file: {0}.", Path.GetFullPath(bloombergConfigPath)); if (!File.Exists(bloombergConfigPath)) continue; try { bloombergConfig = File.ReadAllLines(bloombergConfigPath); Tracer.LogFormat(TracerLogLevel.INFO, "Reading reporter connection settings from {0}.", BloombergSettings.BloombergConfigFileName); break; } catch (FileNotFoundException) { // try next path } } } catch (ArgumentNullException) { Tracer.LogFormat(TracerLogLevel.WARN, "{0} is empty. Local user name and computer name will be used.", BloombergSettings.BloombergConfigFileName); return false; } if (bloombergConfig == null || bloombergConfig.Length == 0 || String.IsNullOrEmpty(bloombergConfig[0])) { Tracer.LogFormat(TracerLogLevel.WARN, "{0} was not found or is empty. Local user name and computer name will be used.", BloombergSettings.BloombergConfigFileName); return false; } var pair = bloombergConfig[0].Split('='); if (pair.Length != 2) { Tracer.LogFormat(TracerLogLevel.WARN, "{0} does not meet the username=computername format. Local user name and computer name will be used.", BloombergSettings.BloombergConfigFileName); return false; } userName = pair[0].Trim(); reporterMachineName = pair[1].Trim(); Tracer.LogFormat(TracerLogLevel.WARN, "Using username='{0}' and reporter computer name = {1} as read from {2}.", userName, reporterMachineName, BloombergSettings.BloombergConfigFileName); return true; } private void InitGameSettingsFromIniFile(SettingLoader settingLoader) { settingLoader.ProcessProperties(GameSettings, "Game"); } private void InitBloombergSettingsFromIniFile(SettingLoader settingLoader) { settingLoader.ProcessProperties(BloombergSettings, "Bloomberg"); } public bool CheckIfCrashAlreadyHandled() { bool isCrashAlreadyHandled = false; Mutex mutex = null; try { mutex = Mutex.OpenExisting(_crashAlreadyHandledFlagName); isCrashAlreadyHandled = true; Tracer.LogFormat(TracerLogLevel.INFO, "Mutex \"{0}\" already opened by native client. We don't analyze exception", _crashAlreadyHandledFlagName); } catch (WaitHandleCannotBeOpenedException) { Tracer.LogFormat(TracerLogLevel.DEBUG, "CrashHandler.CheckIfCrashAlreadyHandled: Crash is not already handled by native client."); } catch (Exception other) { Tracer.LogFormat(TracerLogLevel.ERROR, "CrashHandler.CheckIfCrashAlreadyHandled: Mutex.OpenExisting \"{0}\" failed for unexpected reason. Exception : {1}", _crashAlreadyHandledFlagName, other.Message); } if (mutex != null) { mutex.Close(); } return isCrashAlreadyHandled; } public void CreateCrashAlreadyHandledFlag() { Mutex mutex = null; try { mutex = new Mutex(false, _crashAlreadyHandledFlagName); Tracer.LogFormat(TracerLogLevel.DEBUG, "CrashHandler.CreateCrashAlreadyHandledFlag: Crash already handled Mutex created by managed client."); } catch (Exception other) { Tracer.LogFormat(TracerLogLevel.ERROR, "CrashHandler.CreateCrashAlreadyHandledFlag: Mutex.OpenExisting \"{0}\" failed for unexpected reason. Exception : {1}", _crashAlreadyHandledFlagName, other.Message); } _crashAlreadyHandledFlag = mutex; } public bool CheckIfNativeClientEnabled() { bool isNativeClientEnabled = false; Mutex mutex = null; try { mutex = Mutex.OpenExisting(_nativeBloombergMutexName); isNativeClientEnabled = true; Tracer.LogFormat(TracerLogLevel.DEBUG, "CrashHandler.CheckIfNativeClientEnabled: Native client flag Mutex created."); } catch (WaitHandleCannotBeOpenedException) { Tracer.LogFormat(TracerLogLevel.DEBUG, "CrashHandler.CheckIfNativeClientEnabled: Native client flag Mutex not created."); } catch (Exception other) { Tracer.LogFormat(TracerLogLevel.ERROR, "CrashHandler.CheckIfNativeClientEnabled: Mutex.OpenExisting \"{0}\" failed for unexpected reason. Exception : {1}", _nativeBloombergMutexName, other.Message); } if (mutex != null) { mutex.Close(); } return isNativeClientEnabled; } private class SettingLoader { private readonly IniFile _iniFile; private static readonly Dictionary> Translators; static SettingLoader() { Translators = new Dictionary>(); var intTranslator = new Func(val => Int32.Parse(val)); Translators.Add(typeof(int), intTranslator); Translators.Add(typeof(bool), delegate(string val) { bool boolVal; if (Boolean.TryParse(val, out boolVal)) return (object)boolVal; return (object)Convert.ToBoolean(intTranslator(val)); }); Translators.Add(typeof(uint), val => uint.Parse(val)); Translators.Add(typeof(TracerLogLevel), val => Enum.Parse(typeof(TracerLogLevel), val)); Translators.Add(typeof(ChangelistType), val => Enum.Parse(typeof(ChangelistType), val)); Translators.Add(typeof(ReportType), val => Enum.Parse(typeof(ReportType), val)); Translators.Add(typeof(ConnectionType), val => Enum.Parse(typeof(ConnectionType), val)); Translators.Add(typeof(string), val => val); } public SettingLoader(string filename) { _iniFile = new IniFile(filename); } private object ExtractSetting(string sectionName, object settings, PropertyInfo key, bool useKeyName, string prefix = null, uint? index = null) { var value = _iniFile.GetValue(sectionName, ( prefix ?? String.Empty) + (useKeyName ? key.Name : String.Empty) + (index.HasValue ? index.Value.ToString() : String.Empty)); if (value == null) { if (key.PropertyType.IsGenericType && key.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) key.SetValue(settings, null, null); return null; } try { var typeToUse = key.PropertyType.IsGenericType && key.PropertyType.GetGenericTypeDefinition() == typeof (Nullable<>) ? key.PropertyType.GetGenericArguments()[0] : key.PropertyType; var translatedVal = Translators[typeToUse](value); key.SetValue(settings, translatedVal, null); return translatedVal; } catch (Exception ex) { if (ex is FormatException || ex is ArgumentException || ex is OverflowException) Tracer.LogFormat(TracerLogLevel.ERROR, "Value <{0}> is not translatable to <{1}> for key {2}.", value, key.PropertyType.Name, key.Name); else if (ex is KeyNotFoundException) Tracer.LogFormat(TracerLogLevel.ERROR, "Value type <{0}> not implemented in settings loader for key {1}.", key.PropertyType.Name, key.Name); else throw; } return null; } public void ProcessProperties(object settingsObject, string sectionName, string prefix = null, uint? index = null) { // nice generic loop with ugly special cases so that the ini file setting names mean something and are similar to those of the native client foreach (var key in settingsObject.GetType().GetProperties()) { // it seems that testing directly for List does not work if (key.PropertyType.IsGenericType && key.PropertyType.GetGenericTypeDefinition() == typeof(List<>) && key.PropertyType.GetGenericArguments()[0] == typeof(ChangelistInfo)) { var clList = (List)key.GetValue(settingsObject, null); for (uint currentIndex = 0; currentIndex < MaximumChangelistsFromIniFile; ++currentIndex) { var clInfo = new ChangelistInfo(); ProcessProperties(clInfo, sectionName, "Changelist", currentIndex); ProcessProperties(clInfo, sectionName, "BranchRoot", currentIndex); if (clInfo.PerforceServer != null) clList.Add(clInfo); } } else if (key.PropertyType == typeof(VersionInfo)) ProcessProperties(key.GetValue(settingsObject, null), sectionName, prefix, index); else { bool useKeyName = true; string localPrefix = prefix; string localSectionName = sectionName; if (settingsObject is VersionInfo && key.Name == "Name") { useKeyName = false; localPrefix = "Version"; } else if (key.Name == "Changelist") localPrefix = null; ExtractSetting(localSectionName, settingsObject, key, useKeyName, localPrefix, index); } } } } /// /// Uninitialize Bloomberg /// private void InternalUnInit() { Tracer.LogFormat(TracerLogLevel.INFO, "Reporter.Dispose: Bloomberg will be uninitialized."); if (_remoteDataWriter != null) _remoteDataWriter.StopListening(); _managedFlagMutex = null; if (ReportStats.Instance != null) ReportStats.Instance.UnInitialize(); if (_crashHandler != null) _crashHandler.UnInitialize(); if (_logFile != null) _logFile.UnInitialize(); if (_crashAlreadyHandledFlag != null) { _crashAlreadyHandledFlag.Close(); } _crashAlreadyHandledFlag = null; if (_managedFlagMutex != null) { _managedFlagMutex.Dispose(); } } public void RegisterCrashHandler(bool registerCrashHandler) { _crashHandler.RegisterCrashHandler(registerCrashHandler); } public bool GetCrashHandlerRegistered() { return _crashHandler.IsRegistered(); } #endregion #region Reports /// /// These are multi thread safe: /// 1. Trigger a report by passing down your own report to log some statistics or other things. /// 2. Trigger a crash by passing and error type, error message /// 3. For stats and flags you can either pass a ReportStats or ReportFlag (it will use the TriggerReport method) /// or use the internal ReportStats and ReportFlags by ignoring this parameters. In this case, and only for stats, /// the report will be processed asynchronously if you are using the BloombergThread. /// /// /// public ActionResult TriggerReport(ReportInterface report) { return !HasBeenInit ? ActionResult.Error : ReportManager.Process(report); } // This is this method that will be eventually called public ActionResult TriggerError(ReportCrash reportCrash, string errorMessage, Exception e) { return !HasBeenInit ? ActionResult.Error : _crashHandler.TriggerCrash(reportCrash, errorMessage, e); } [Obsolete("This method is deprecated. Please use the TriggerError method.", true)] public ActionResult TriggerCrash(ReportCrash reportCrash, string errorMessage, Exception e) { return TriggerError(reportCrash, errorMessage, e); } [Obsolete("This method is deprecated. Please use the TriggerError method.", true)] public ActionResult TriggerCrash(ReportType type, string errorMessage) { return TriggerError(type, Bloomberg.Message.Entities.TypeInfo.SUBTYPE_DEFAULT, errorMessage, null, false); } [Obsolete("This method is deprecated. Please use the TriggerError method.", true)] public ActionResult TriggerCrash(ReportType type, string errorMessage, Exception e) { return TriggerError(type, Bloomberg.Message.Entities.TypeInfo.SUBTYPE_DEFAULT, errorMessage, e, false); } public ActionResult TriggerError(ReportType type, string errorMessage) { return TriggerError(type, Bloomberg.Message.Entities.TypeInfo.SUBTYPE_DEFAULT, errorMessage, null, false); } public ActionResult TriggerError(ReportType type, string errorMessage, Exception e) { return TriggerError(type, Bloomberg.Message.Entities.TypeInfo.SUBTYPE_DEFAULT, errorMessage, e, false); } public ActionResult TriggerError(ReportType type, string subType, string errorMessage, Exception e, bool isAsync) { SubTypeConfiguration subTypeConfiguration = Reporter.Instance.SubTypeManager.GetSubTypeConfiguration(subType); if (subTypeConfiguration == null) { Tracer.LogFormat(TracerLogLevel.ERROR, "Could not find sub-type configuration with name {0}. General configuration for report of type {1} will be applied.", subType, type.ToString()); subType = Bloomberg.Message.Entities.TypeInfo.SUBTYPE_DEFAULT; subTypeConfiguration = Reporter.Instance.SubTypeManager.GetSubTypeConfiguration(subType); } string subTypeConfigurationStr = subTypeConfiguration.ToString(); string subTypeConfigurationMessage = subTypeConfigurationStr == string.Empty ? string.Empty : $" and sub-type configuration : {subTypeConfigurationStr}"; Tracer.LogFormat(TracerLogLevel.INFO, "Triggering a report of type {0} with sub-type {1}{2}.", type.ToString(), subType, subTypeConfigurationMessage); ReportInterface report = ReportInterface.GetReportInstance(type); if (report != null && report is ReportCrash) { var crash = report as ReportCrash; crash.SetType(type); crash.SetSubType(subType); crash.IsAsync = isAsync; ActionResult result = TriggerError(crash, errorMessage, e); return isAsync ? ActionResult.None : result; } return ActionResult.Error; } public ActionResult TriggerErrorAsync(ReportType type, string errorMessage) { return TriggerErrorAsync(type, errorMessage, null); } public ActionResult TriggerErrorAsync(ReportType type, string errorMessage, Exception e) { return TriggerError(type, Bloomberg.Message.Entities.TypeInfo.SUBTYPE_DEFAULT, errorMessage, e, true); } public ActionResult TriggerFlag() { return TriggerFlag(ReportFlag.Instance); } public ActionResult TriggerFlag(ReportFlag reportFlag) { return TriggerReport(reportFlag); } public void TriggerStatsSend() { TriggerStatsSend(ReportStats.Instance); } public void TriggerStatsSend(ReportStats reportStats) { if (!BloombergSettings.EnableStats) return; if (reportStats != null) { reportStats.ForceSendingAllStats(); } else { ReportStats.Instance.ForceSendingAllStats(); } } public void TriggerStatsSendAsync() { TriggerStatsSendAsync(ReportStats.Instance); } public void TriggerStatsSendAsync(ReportStats reportStats) { if (!BloombergSettings.EnableStats) return; if (reportStats != null) { reportStats.ForceSendingAllStatsAsync(); } else { ReportStats.Instance.ForceSendingAllStatsAsync(); } } public void SetSessionName(string sessionName) { ReportStats.Instance.SessionName = sessionName; } #endregion public void Dispose() { _crashHandler?.Dispose(); _logFile?.Dispose(); _remoteDataWriter?.Dispose(); _crashAlreadyHandledFlag?.Dispose(); _managedFlagMutex?.Dispose(); RemoteDataCollector?.Dispose(); } } }