-
Notifications
You must be signed in to change notification settings - Fork 177
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
Changes from 7 commits
35e75c3
a792905
9a2a77b
f3d14de
7e444b9
33d6c50
1358fec
0b0c917
af77d11
7dfa78e
d1fd72e
888ef8a
2e762b9
0d885c5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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" | ||
)] | ||
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 | ||
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 | ||
} |
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 | ||
); | ||
} | ||
} |
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> | ||
AlanRynne marked this conversation as resolved.
Show resolved
Hide resolved
|
||
</PropertyGroup> | ||
|
||
<PropertyGroup Condition="'$(IsDesktopBuild)' == false"> | ||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="System.Text.Json" Version="4.6.0" /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> |
There was a problem hiding this comment.
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)