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
21 changes: 20 additions & 1 deletion All.sln
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Speckle.Core.Tests.Unit", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Speckle.Core.Tests.Performance", "Core\Tests\Speckle.Core.Tests.Performance\Speckle.Core.Tests.Performance.csproj", "{1DE6EF69-0782-4FD7-A2A7-9F697426882D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Speckle.Core.Tests.Integration", "Core\Tests\Speckle.Core.Tests.Integration\Speckle.Core.Tests.Integration.csproj", "{FB2DEE1D-788B-45B6-B80C-D8F7C8390C37}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Speckle.Core.Tests.Integration", "Core\Tests\Speckle.Core.Tests.Integration\Speckle.Core.Tests.Integration.csproj", "{FB2DEE1D-788B-45B6-B80C-D8F7C8390C37}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConnectorGrasshopper8", "ConnectorGrasshopper\ConnectorGrasshopper8\ConnectorGrasshopper8.csproj", "{FDBC3082-1FAD-4701-A121-802F591D2D35}"
ProjectSection(ProjectDependencies) = postProject
Expand All @@ -457,6 +457,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConverterNavisworks2025", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConnectorNavisworks2025", "ConnectorNavisworks\ConnectorNavisworks2025\ConnectorNavisworks2025.csproj", "{2568500E-F1BC-440E-9150-DD4820B3FAD6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DllConflictManagement", "ConnectorCore\DllConflictManagement\DllConflictManagement.csproj", "{0D23858F-4CC1-4DCA-9207-5EDB8B6CE9DD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug Mac|Any CPU = Debug Mac|Any CPU
Expand Down Expand Up @@ -2291,6 +2293,22 @@ Global
{2568500E-F1BC-440E-9150-DD4820B3FAD6}.Release|Any CPU.Build.0 = Release|Any CPU
{2568500E-F1BC-440E-9150-DD4820B3FAD6}.Release|x64.ActiveCfg = Release|x64
{2568500E-F1BC-440E-9150-DD4820B3FAD6}.Release|x64.Build.0 = Release|x64
{0D23858F-4CC1-4DCA-9207-5EDB8B6CE9DD}.Debug Mac|Any CPU.ActiveCfg = Debug|Any CPU
{0D23858F-4CC1-4DCA-9207-5EDB8B6CE9DD}.Debug Mac|Any CPU.Build.0 = Debug|Any CPU
{0D23858F-4CC1-4DCA-9207-5EDB8B6CE9DD}.Debug Mac|x64.ActiveCfg = Debug|Any CPU
{0D23858F-4CC1-4DCA-9207-5EDB8B6CE9DD}.Debug Mac|x64.Build.0 = Debug|Any CPU
{0D23858F-4CC1-4DCA-9207-5EDB8B6CE9DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0D23858F-4CC1-4DCA-9207-5EDB8B6CE9DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0D23858F-4CC1-4DCA-9207-5EDB8B6CE9DD}.Debug|x64.ActiveCfg = Debug|Any CPU
{0D23858F-4CC1-4DCA-9207-5EDB8B6CE9DD}.Debug|x64.Build.0 = Debug|Any CPU
{0D23858F-4CC1-4DCA-9207-5EDB8B6CE9DD}.Release Mac|Any CPU.ActiveCfg = Release|Any CPU
{0D23858F-4CC1-4DCA-9207-5EDB8B6CE9DD}.Release Mac|Any CPU.Build.0 = Release|Any CPU
{0D23858F-4CC1-4DCA-9207-5EDB8B6CE9DD}.Release Mac|x64.ActiveCfg = Release|Any CPU
{0D23858F-4CC1-4DCA-9207-5EDB8B6CE9DD}.Release Mac|x64.Build.0 = Release|Any CPU
{0D23858F-4CC1-4DCA-9207-5EDB8B6CE9DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0D23858F-4CC1-4DCA-9207-5EDB8B6CE9DD}.Release|Any CPU.Build.0 = Release|Any CPU
{0D23858F-4CC1-4DCA-9207-5EDB8B6CE9DD}.Release|x64.ActiveCfg = Release|Any CPU
{0D23858F-4CC1-4DCA-9207-5EDB8B6CE9DD}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -2461,6 +2479,7 @@ Global
{9E74F0E6-94B4-46BD-B1CA-DD874B459399} = {8A909E95-7A39-4B21-A04A-E168478E71F0}
{0B6B5C52-54EC-461F-8729-6244ACA63646} = {B6C38DB9-7B20-4B7E-BC90-6A8CAFC16807}
{2568500E-F1BC-440E-9150-DD4820B3FAD6} = {B6887DDC-B9B9-4B00-95DC-1DD930A1E901}
{0D23858F-4CC1-4DCA-9207-5EDB8B6CE9DD} = {DA9DFC36-C53F-4B19-8911-BF7605230BA7}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {1D43D91B-4F01-4A78-8250-CC6F9BD93A14}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
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;

/// <summary>
/// Hashed email
/// </summary>
private readonly string _hashedEmail;

/// <summary>
/// Hashed server URL
/// </summary>
private readonly string _hashedServer;
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;
_hashedEmail = "undefined";
_hashedServer = "no-account-server";
}

/// <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
Contributor

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", _hashedEmail },
{ "server_id", _hashedServer },
{ "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
{
/// <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
}
29 changes: 29 additions & 0 deletions ConnectorCore/DllConflictManagement/AssemblyConflictInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Reflection;
using System.Text;

namespace Speckle.DllConflictManagement;

public sealed class AssemblyConflictInfo
{
public AssemblyConflictInfo(AssemblyName speckleDependencyAssemblyName, Assembly conflictingAssembly)
{
SpeckleDependencyAssemblyName = speckleDependencyAssemblyName;
ConflictingAssembly = conflictingAssembly;
}

public AssemblyName SpeckleDependencyAssemblyName { get; set; }
public Assembly ConflictingAssembly { get; set; }

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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Speckle.DllConflictManagement.ConflictManagementOptions;

public sealed class DllConflictManagmentOptions
{
public HashSet<string> DllsToIgnore { get; private set; }

public DllConflictManagmentOptions(HashSet<string> dllsToIgnore)
{
DllsToIgnore = dllsToIgnore;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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(ISerializer serializer, string hostAppName, string hostAppVersion)
{
_serializer = serializer;
_filePath = Path.Combine(GetAppDataFolder(), "Speckle", "DllConflictManagement");
_fileName = $"DllConflictManagmentOptions-{hostAppName}{hostAppVersion}.json";
}

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

public DllConflictManagmentOptions LoadOptions()
{
if (!File.Exists(FullPath))
{
Directory.CreateDirectory(_filePath);
var defaultOptions = new DllConflictManagmentOptions(new HashSet<string>());
SaveOptions(defaultOptions);
return defaultOptions;
}

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

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

private string GetAppDataFolder()
{
return Environment.GetFolderPath(
Environment.SpecialFolder.ApplicationData,
// if the folder doesn't exist, we get back an empty string on OSX,
// which in turn, breaks other stuff down the line.
// passing in the Create option ensures that this directory exists,
// which is not a given on all OS-es.
Environment.SpecialFolderOption.Create
);
}
}
19 changes: 19 additions & 0 deletions ConnectorCore/DllConflictManagement/DllConflictManagement.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Speckle.DllConflictManagement</RootNamespace>
<IsPackable>false</IsPackable>
</PropertyGroup>

<PropertyGroup Condition="'$(IsDesktopBuild)' == false">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="System.Text.Json" Version="4.6.0" />
Copy link
Contributor

Choose a reason for hiding this comment

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

Note to @BovineOx and my future self. We may want to consider using Speckle.Newtonsoft considering many more people will use System.Text.Json in varying versions

Copy link
Contributor

@BovineOx BovineOx Apr 22, 2024

Choose a reason for hiding this comment

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

ha, yeah that's interesting, because System.Text.Json is becoming the more mainstream package, though there are still gaps, but maybe so.

</ItemGroup>

</Project>
Loading