Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CNX-8397-DLL-Conflict-Handling in existing DUI2 connectors #3273

Merged
merged 14 commits into from
Apr 23, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
using System.Diagnostics;
using System.Net.Http.Headers;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Web;
using Speckle.DllConflictManagement.EventEmitter;
using Speckle.DllConflictManagement.Serialization;

namespace Speckle.DllConflictManagement.Analytics;

/// <summary>
/// A version of the Analytics class in Core that doesn't have any dependencies. This class will load and subscribe
/// to the eventEmitter's Action event, but will hopefully get unsubscribed and replaced by the full version in Core
/// </summary>
public sealed class AnalyticsWithoutDependencies
{
private const string MIXPANEL_TOKEN = "acd87c5a50b56df91a795e999812a3a4";
private const string MIXPANEL_SERVER = "https://analytics.speckle.systems";
private readonly ISerializer _serializer;
private readonly string _hostApplication;
private readonly string _hostApplicationVersion;
private readonly DllConflictEventEmitter _eventEmitter;

public AnalyticsWithoutDependencies(
DllConflictEventEmitter eventEmitter,
ISerializer serializer,
string hostApplication,
string hostApplicationVersion
)
{
_eventEmitter = eventEmitter;
_serializer = serializer;
_hostApplication = hostApplication;
_hostApplicationVersion = hostApplicationVersion;
}

/// <summary>
/// <see langword="false"/> when the DEBUG pre-processor directive is <see langword="true"/>, <see langword="false"/> otherwise
/// </summary>
/// <remarks>This must be kept as a computed property, not a compile time const</remarks>
internal static bool IsReleaseMode =>
#if DEBUG
false;
#else
true;
#endif

/// <summary>
/// Tracks an event without specifying the email and server.
/// It's not always possible to know which account the user has selected, especially in visual programming.
/// Therefore we are caching the email and server values so that they can be used also when nodes such as "Serialize" are used.
/// If no account info is cached, we use the default account data.
/// </summary>
/// <param name="eventName">Name of the even</param>
/// <param name="customProperties">Additional parameters to pass in to event</param>
/// <param name="isAction">True if it's an action performed by a logged user</param>
public void TrackEvent(Events eventName, Dictionary<string, object>? customProperties = null, bool isAction = true)
{
Task.Run(async () => await TrackEventAsync(eventName, customProperties, isAction).ConfigureAwait(false));
}

/// <summary>
/// Tracks an event from a specified email and server, anonymizes personal information
/// </summary>
/// <param name="eventName">Name of the event</param>
/// <param name="customProperties">Additional parameters to pass to the event</param>
/// <param name="isAction">True if it's an action performed by a logged user</param>
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Design",
"CA1031:Do not catch general exception types",
Justification = "Catching all exceptions to avoid an unobserved exception that could crash the host app"
)]
Comment on lines +69 to +73
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting... could we do something like IsFatal here (though not directly that as it would require referencing something else)

private async Task TrackEventAsync(
Events eventName,
Dictionary<string, object>? customProperties = null,
bool isAction = true
)
{
if (!IsReleaseMode)
{
//only track in prod
return;
}

try
{
var executingAssembly = Assembly.GetExecutingAssembly();
var properties = new Dictionary<string, object>
{
{ "distinct_id", "undefined" },
{ "server_id", "no-account-server" },
{ "token", MIXPANEL_TOKEN },
{ "hostApp", _hostApplication },
{ "hostAppVersion", _hostApplicationVersion },
{
"core_version",
FileVersionInfo.GetVersionInfo(executingAssembly.Location).ProductVersion
?? executingAssembly.GetName().Version.ToString()
},
{ "$os", GetOs() }
};

if (isAction)
{
properties.Add("type", "action");
}

if (customProperties != null)
{
foreach (KeyValuePair<string, object> customProp in customProperties)
{
properties[customProp.Key] = customProp.Value;
}
}

string json = _serializer.Serialize(new { @event = eventName.ToString(), properties });

var query = new StreamContent(new MemoryStream(Encoding.UTF8.GetBytes("data=" + HttpUtility.UrlEncode(json))));

using HttpClient client = new();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain"));
query.Headers.ContentType = new MediaTypeHeaderValue("application/json");
var res = await client.PostAsync(MIXPANEL_SERVER + "/track?ip=1", query).ConfigureAwait(false);
res.EnsureSuccessStatusCode();
}
catch (Exception ex)
{
_eventEmitter.EmitError(
new LoggingEventArgs(
$"An exception was thrown in class {nameof(AnalyticsWithoutDependencies)} while attempting to record analytics",
ex
)
);
}
}

public void TrackEvent(object sender, ActionEventArgs args)
{
_ = Enum.TryParse(args.EventName, out Analytics.Events eventName);
TrackEvent(eventName, args.EventProperties);
}

private static string GetOs()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return "Windows";
}

if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return "Mac OS X";
}

if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return "Linux";
}

return "Unknown";
}
}

/// <summary>
/// Default Mixpanel events
/// </summary>
public enum Events
AlanRynne marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// Event triggered when data is sent to a Speckle Server
/// </summary>
Send,

/// <summary>
/// Event triggered when data is received from a Speckle Server
/// </summary>
Receive,

/// <summary>
/// Event triggered when a node is executed in a visual programming environment, it should contain the name of the action and the host application
/// </summary>
NodeRun,

/// <summary>
/// Event triggered when an action is executed in Desktop UI, it should contain the name of the action and the host application
/// </summary>
DUIAction,

/// <summary>
/// Event triggered when a node is first created in a visual programming environment, it should contain the name of the action and the host application
/// </summary>
NodeCreate,

/// <summary>
/// Event triggered when the import/export alert is launched or closed
/// </summary>
ImportExportAlert,

/// <summary>
/// Event triggered when the connector is registered
/// </summary>
Registered,

/// <summary>
/// Event triggered by the Mapping Tool
/// </summary>
MappingsAction
}
11 changes: 11 additions & 0 deletions ConnectorCore/DllConflictManagement/AssemblyConflictInfo.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Reflection;
using System.Text;

namespace Speckle.DllConflictManagement;

Expand All @@ -15,4 +16,14 @@ public AssemblyConflictInfo(AssemblyName speckleDependencyAssemblyName, Assembly

public string GetConflictingExternalAppName() =>
new DirectoryInfo(Path.GetDirectoryName(ConflictingAssembly.Location)).Name;

public override string ToString()
{
StringBuilder sb = new();
sb.AppendLine($"Conflict DLL: {SpeckleDependencyAssemblyName.Name}");
sb.AppendLine($"SpeckleVer: {SpeckleDependencyAssemblyName.Version}");
sb.AppendLine($"LoadedVer: {ConflictingAssembly.GetName().Version}");
sb.AppendLine($"Folder: {GetConflictingExternalAppName()}");
return sb.ToString();
}
}
67 changes: 67 additions & 0 deletions ConnectorCore/DllConflictManagement/AssemblyConflictInfoDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System.Reflection;

namespace Speckle.DllConflictManagement;

/// <summary>
/// Some system object types (System.Type, System.Reflection.AssemblyName, System.Reflection.Assembly) were throwing
/// when I was attempting to serialize the <see cref="AssemblyConflictInfo"/> objects to be sent to mixpanel.
/// Therefore I made this DTO to make an object that would hold the info we need and would be serializable.
/// </summary>
public sealed class AssemblyConflictInfoDto
{
public AssemblyConflictInfoDto(
AssemblyName speckleDependencyAssemblyName,
AssemblyName conflictingAssembly,
string folderName,
string fullPath
)
{
ConflictingAssemblyName = conflictingAssembly.Name;
ConflictingAssemblyVersion = conflictingAssembly.Version.ToString();
SpeckleExpectedVersion = speckleDependencyAssemblyName.Version.ToString();
FolderName = folderName;
Path = fullPath;
}

public string SpeckleExpectedVersion { get; }
public string ConflictingAssemblyName { get; }
public string ConflictingAssemblyVersion { get; }
public string FolderName { get; }
public string Path { get; }
}

public static class AssemblyConflictInfoExtensions
{
public static AssemblyConflictInfoDto ToDto(this AssemblyConflictInfo assemblyConflictInfo)
{
return new(
assemblyConflictInfo.SpeckleDependencyAssemblyName,
assemblyConflictInfo.ConflictingAssembly.GetName(),
assemblyConflictInfo.GetConflictingExternalAppName(),
GetPathFromAutodeskOrFullPath(assemblyConflictInfo.ConflictingAssembly.Location)
);
}

public static IEnumerable<AssemblyConflictInfoDto> ToDtos(
this IEnumerable<AssemblyConflictInfo> assemblyConflictInfos
)
{
foreach (var assemblyConflictInfo in assemblyConflictInfos)
{
yield return assemblyConflictInfo.ToDto();
}
}

private static readonly string[] separator = new[] { "Autodesk" };

private static string GetPathFromAutodeskOrFullPath(string fullPath)
{
string[] splitOnAutodesk = fullPath.Split(separator, StringSplitOptions.None);

if (splitOnAutodesk.Length == 2)
{
return splitOnAutodesk[1];
}
return fullPath;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Autodesk specific? 🤔 Ideally this project should work on any connector. Not a deal breaker, just found it weird.

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Speckle.DllConflictManagement;
namespace Speckle.DllConflictManagement.ConflictManagementOptions;

public sealed class DllConflictManagmentOptions
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
using System.IO;
using System.Text.Json;
using Speckle.DllConflictManagement.ConflictManagementOptions;
using Speckle.DllConflictManagement.Serialization;

namespace Speckle.DllConflictManagement;

public sealed class DllConflictManagmentOptionsLoader
{
private readonly ISerializer _serializer;
private readonly string _filePath;
private readonly string _fileName;

public DllConflictManagmentOptionsLoader(string hostAppName, string hostAppVersion)
public DllConflictManagmentOptionsLoader(ISerializer serializer, string hostAppName, string hostAppVersion)
{
_serializer = serializer;
_filePath = Path.Combine(GetAppDataFolder(), "Speckle", "DllConflictManagement");
_fileName = $"DllConflictManagmentOptions-{hostAppName}{hostAppVersion}.json";
}

private readonly JsonSerializerOptions _jsonSerializerOptions = new() { WriteIndented = true };

private string FullPath => Path.Combine(_filePath, _fileName);

public DllConflictManagmentOptions LoadOptions()
Expand All @@ -29,12 +29,12 @@ public DllConflictManagmentOptions LoadOptions()
}

string jsonString = File.ReadAllText(FullPath);
return JsonSerializer.Deserialize<DllConflictManagmentOptions>(jsonString)!;
return _serializer.Deserialize<DllConflictManagmentOptions>(jsonString)!;
}

public void SaveOptions(DllConflictManagmentOptions options)
{
var json = JsonSerializer.Serialize(options, _jsonSerializerOptions);
var json = _serializer.Serialize(options);
File.WriteAllText(FullPath, json);
}

Expand Down
Loading