diff --git a/Directory.Build.targets b/Directory.Build.targets index e389b647e0..9b68e5aacb 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -23,6 +23,11 @@ + + + + + @@ -65,9 +70,7 @@ - + +#include + +static int loadStatus = -1; // unitialized + +static Class SentrySDK; +static Class SentryScope; +static Class SentryBreadcrumb; +static Class SentryUser; +static Class SentryOptions; +static Class PrivateSentrySDKOnly; + +#define LOAD_CLASS_OR_BREAK(name) \ + name = (__bridge Class)dlsym(dylib, "OBJC_CLASS_$_" #name); \ + if (!name) { \ + NSLog(@"Sentry (bridge): Couldn't load class '" #name "' from the dynamic library"); \ + break; \ + } + +// Returns (bool): 0 on failure, 1 on success +// WARNING: you may only call other Sentry* functions AFTER calling this AND only if it returned "1" +int SentryNativeBridgeLoadLibrary() +{ + if (loadStatus == -1) { + loadStatus = 0; // init to "error" + do { + void *dylib = dlopen("libSentry.dylib", RTLD_LAZY); + if (!dylib) { + NSLog(@"Sentry (bridge): Couldn't load Sentry.dylib - dlopen() failed"); + break; + } + + LOAD_CLASS_OR_BREAK(SentrySDK) + LOAD_CLASS_OR_BREAK(SentryScope) + LOAD_CLASS_OR_BREAK(SentryBreadcrumb) + LOAD_CLASS_OR_BREAK(SentryUser) + LOAD_CLASS_OR_BREAK(SentryOptions) + LOAD_CLASS_OR_BREAK(PrivateSentrySDKOnly) + + // everything above passed - mark as successfully loaded + loadStatus = 1; + } while (false); + } + return loadStatus; +} + +const void *SentryNativeBridgeOptionsNew() +{ + NSMutableDictionary *dictOptions = [[NSMutableDictionary alloc] init]; + dictOptions[@"sdk"] = @ { @"name" : @"sentry.cocoa.unity" }; + dictOptions[@"enableAutoSessionTracking"] = @NO; + dictOptions[@"enableAppHangTracking"] = @NO; + return CFBridgingRetain(dictOptions); +} + +void SentryNativeBridgeOptionsSetString(const void *options, const char *name, const char *value) +{ + NSMutableDictionary *dictOptions = (__bridge NSMutableDictionary *)options; + dictOptions[[NSString stringWithUTF8String:name]] = [NSString stringWithUTF8String:value]; +} + +void SentryNativeBridgeOptionsSetInt(const void *options, const char *name, int32_t value) +{ + NSMutableDictionary *dictOptions = (__bridge NSMutableDictionary *)options; + dictOptions[[NSString stringWithUTF8String:name]] = [NSNumber numberWithInt:value]; +} + +void SentryNativeBridgeStartWithOptions(const void *options) +{ + NSMutableDictionary *dictOptions = (__bridge_transfer NSMutableDictionary *)options; + id sentryOptions = [[SentryOptions alloc] + performSelector:@selector(initWithDict:didFailWithError:) + withObject:dictOptions withObject:nil]; + + [SentrySDK performSelector:@selector(startWithOptions:) withObject:sentryOptions]; +} + +void SentryConfigureScope(void (^callback)(id)) +{ + // setValue:forKey: may throw if the property is not found; same for performSelector. + // Even though this shouldn't happen, better not take the chance of letting an unhandled + // exception while setting error info - it would just crash the app immediately. + @try { + [SentrySDK performSelector:@selector(configureScope:) withObject:callback]; + } @catch (NSException *exception) { + NSLog(@"Sentry (bridge): failed to configure scope: %@", exception.reason); + } +} + +/*******************************************************************************/ +/* The remaining code is a copy of iOS/SentryNativeBridge.m with changes to */ +/* make it work with dynamically loaded classes. Mainly: */ +/* - call: [class performSelector:@selector(arg1:arg2:) */ +/* withObject:arg1Value withObject:arg2Value]; */ +/* or xCode warns of class/instance method not found */ +/* - use `id` as variable types */ +/* - use [obj setValue:value forKey:@"prop"] instead of `obj.prop = value` */ +/*******************************************************************************/ + +int SentryNativeBridgeCrashedLastRun() +{ + @try { + return [SentrySDK performSelector:@selector(crashedLastRun)] ? 1 : 0; + } @catch (NSException *exception) { + NSLog(@"Sentry (bridge): failed to get crashedLastRun: %@", exception.reason); + } + return -1; +} + +void SentryNativeBridgeClose() +{ + @try { + [SentrySDK performSelector:@selector(close)]; + } @catch (NSException *exception) { + NSLog(@"Sentry (bridge): failed to close: %@", exception.reason); + } +} + +void SentryNativeBridgeAddBreadcrumb( + const char *timestamp, const char *message, const char *type, const char *category, int level) +{ + if (timestamp == NULL && message == NULL && type == NULL && category == NULL) { + return; + } + + SentryConfigureScope(^(id scope) { + id breadcrumb = [[SentryBreadcrumb alloc] init]; + + if (timestamp != NULL) { + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setDateFormat:NSCalendarIdentifierISO8601]; + [breadcrumb + setValue:[dateFormatter dateFromString:[NSString stringWithUTF8String:timestamp]] + forKey:@"timestamp"]; + } + + if (message != NULL) { + [breadcrumb setValue:[NSString stringWithUTF8String:message] forKey:@"message"]; + } + + if (type != NULL) { + [breadcrumb setValue:[NSString stringWithUTF8String:type] forKey:@"type"]; + } + + if (category != NULL) { + [breadcrumb setValue:[NSString stringWithUTF8String:category] forKey:@"category"]; + } + + [breadcrumb setValue:[NSNumber numberWithInt:level] forKey:@"level"]; + + [scope performSelector:@selector(addBreadcrumb:) withObject:breadcrumb]; + }); +} + +void SentryNativeBridgeSetExtra(const char *key, const char *value) +{ + if (key == NULL) { + return; + } + + SentryConfigureScope(^(id scope) { + if (value != NULL) { + [scope performSelector:@selector(setExtraValue:forKey:) + withObject:[NSString stringWithUTF8String:value] + withObject:[NSString stringWithUTF8String:key]]; + } else { + [scope performSelector:@selector(removeExtraForKey:) + withObject:[NSString stringWithUTF8String:key]]; + } + }); +} + +void SentryNativeBridgeSetTag(const char *key, const char *value) +{ + if (key == NULL) { + return; + } + + SentryConfigureScope(^(id scope) { + if (value != NULL) { + [scope performSelector:@selector(setTagValue:forKey:) + withObject:[NSString stringWithUTF8String:value] + withObject:[NSString stringWithUTF8String:key]]; + } else { + [scope performSelector:@selector(removeTagForKey:) + withObject:[NSString stringWithUTF8String:key]]; + } + }); +} + +void SentryNativeBridgeUnsetTag(const char *key) +{ + if (key == NULL) { + return; + } + + SentryConfigureScope(^(id scope) { + [scope performSelector:@selector(removeTagForKey:) + withObject:[NSString stringWithUTF8String:key]]; + }); +} + +void SentryNativeBridgeSetUser( + const char *email, const char *userId, const char *ipAddress, const char *username) +{ + if (email == NULL && userId == NULL && ipAddress == NULL && username == NULL) { + return; + } + + SentryConfigureScope(^(id scope) { + id user = [[SentryUser alloc] init]; + + if (email != NULL) { + [user setValue:[NSString stringWithUTF8String:email] forKey:@"email"]; + } + + if (userId != NULL) { + [user setValue:[NSString stringWithUTF8String:userId] forKey:@"userId"]; + } + + if (ipAddress != NULL) { + [user setValue:[NSString stringWithUTF8String:ipAddress] forKey:@"ipAddress"]; + } + + if (username != NULL) { + [user setValue:[NSString stringWithUTF8String:username] forKey:@"username"]; + } + + [scope performSelector:@selector(setUser:) withObject:user]; + }); +} + +void SentryNativeBridgeUnsetUser() +{ + SentryConfigureScope( + ^(id scope) { [scope performSelector:@selector(setUser:) withObject:nil]; }); +} + +char *SentryNativeBridgeGetInstallationId() +{ + // Create a null terminated C string on the heap as expected by marshalling. + // See Tips for iOS in https://docs.unity3d.com/Manual/PluginsForIOS.html + const char *nsStringUtf8 = + [[PrivateSentrySDKOnly performSelector:@selector(installationID)] UTF8String]; + size_t len = strlen(nsStringUtf8) + 1; + char *cString = (char *)malloc(len); + memcpy(cString, nsStringUtf8, len); + return cString; +} + +static inline NSString *_NSStringOrNil(const char *value) +{ + return value ? [NSString stringWithUTF8String:value] : nil; +} + +static inline NSString *_NSNumberOrNil(int32_t value) +{ + return value == 0 ? nil : @(value); +} + +static inline NSNumber *_NSBoolOrNil(int8_t value) +{ + if (value == 0) { + return @NO; + } + if (value == 1) { + return @YES; + } + return nil; +} + +void SentryNativeBridgeWriteScope( // clang-format off + // // const char *AppStartTime, + // const char *AppBuildType, + // // const char *OperatingSystemRawDescription, + // int DeviceProcessorCount, + // const char *DeviceCpuDescription, + // const char *DeviceTimezone, + // int8_t DeviceSupportsVibration, + // const char *DeviceName, + // int8_t DeviceSimulator, + // const char *DeviceDeviceUniqueIdentifier, + // const char *DeviceDeviceType, + // // const char *DeviceModel, + // // long DeviceMemorySize, + int32_t GpuId, + const char *GpuName, + const char *GpuVendorName, + int32_t GpuMemorySize, + const char *GpuNpotSupport, + const char *GpuVersion, + const char *GpuApiType, + int32_t GpuMaxTextureSize, + int8_t GpuSupportsDrawCallInstancing, + int8_t GpuSupportsRayTracing, + int8_t GpuSupportsComputeShaders, + int8_t GpuSupportsGeometryShaders, + const char *GpuVendorId, + int8_t GpuMultiThreadedRendering, + const char *GpuGraphicsShaderLevel, + const char *EditorVersion, + const char *UnityInstallMode, + const char *UnityTargetFrameRate, + const char *UnityCopyTextureSupport, + const char *UnityRenderingThreadingMode +) // clang-format on +{ + // Note: we're using a NSMutableDictionary because it will skip fields with nil values. + SentryConfigureScope(^(id scope) { + NSMutableDictionary *gpu = [[NSMutableDictionary alloc] init]; + gpu[@"id"] = _NSNumberOrNil(GpuId); + gpu[@"name"] = _NSStringOrNil(GpuName); + gpu[@"vendor_name"] = _NSStringOrNil(GpuVendorName); + gpu[@"memory_size"] = _NSNumberOrNil(GpuMemorySize); + gpu[@"npot_support"] = _NSStringOrNil(GpuNpotSupport); + gpu[@"version"] = _NSStringOrNil(GpuVersion); + gpu[@"api_type"] = _NSStringOrNil(GpuApiType); + gpu[@"max_texture_size"] = _NSNumberOrNil(GpuMaxTextureSize); + gpu[@"supports_draw_call_instancing"] = _NSBoolOrNil(GpuSupportsDrawCallInstancing); + gpu[@"supports_ray_tracing"] = _NSBoolOrNil(GpuSupportsRayTracing); + gpu[@"supports_compute_shaders"] = _NSBoolOrNil(GpuSupportsComputeShaders); + gpu[@"supports_geometry_shaders"] = _NSBoolOrNil(GpuSupportsGeometryShaders); + gpu[@"vendor_id"] = _NSStringOrNil(GpuVendorId); + gpu[@"multi_threaded_rendering"] = _NSBoolOrNil(GpuMultiThreadedRendering); + gpu[@"graphics_shader_level"] = _NSStringOrNil(GpuGraphicsShaderLevel); + [scope performSelector:@selector(setContextValue:forKey:) withObject:gpu withObject:@"gpu"]; + + NSMutableDictionary *unity = [[NSMutableDictionary alloc] init]; + unity[@"editor_version"] = _NSStringOrNil(EditorVersion); + unity[@"install_mode"] = _NSStringOrNil(UnityInstallMode); + unity[@"target_frame_rate"] = _NSStringOrNil(UnityTargetFrameRate); + unity[@"copy_texture_support"] = _NSStringOrNil(UnityCopyTextureSupport); + unity[@"rendering_threading_mode"] = _NSStringOrNil(UnityRenderingThreadingMode); + [scope performSelector:@selector(setContextValue:forKey:) + withObject:unity + withObject:@"unity"]; + }); +} + +void crashBridge() +{ + int *p = 0; + *p = 0; +} diff --git a/lib/sentry-native/macos/libBridge.dylib b/lib/sentry-native/macos/libBridge.dylib new file mode 100755 index 0000000000..fd818afc89 Binary files /dev/null and b/lib/sentry-native/macos/libBridge.dylib differ diff --git a/src/Sentry/CrashType.cs b/src/Sentry/CrashType.cs index 0fb43468e1..7957eb4075 100644 --- a/src/Sentry/CrashType.cs +++ b/src/Sentry/CrashType.cs @@ -29,7 +29,7 @@ public enum CrashType JavaBackgroundThread, #endif -#if __MOBILE__ +#if __MOBILE__ || MACOS /// /// A native operation that will crash the application will be performed by a C library. /// diff --git a/src/Sentry/Internal/ScopeObserver.cs b/src/Sentry/Internal/ScopeObserver.cs new file mode 100644 index 0000000000..5fb6efa488 --- /dev/null +++ b/src/Sentry/Internal/ScopeObserver.cs @@ -0,0 +1,87 @@ +namespace Sentry; + +// https://github.com/getsentry/sentry-unity/blob/3eb6eca6ed270c5ec023bf75ee53c1ca00bb7c82/src/Sentry.Unity/ScopeObserver.cs + +/// +/// Sentry Unity Scope Observer wrapper for the common behaviour accross platforms. +/// +internal abstract class ScopeObserver : Sentry.IScopeObserver +{ + private readonly SentryOptions _options; + private readonly string _name; + + public ScopeObserver( + string name, SentryOptions options) + { + _name = name; + _options = options; + } + + public void AddBreadcrumb(Breadcrumb breadcrumb) + { + _options.DiagnosticLogger?.Log(SentryLevel.Debug, + "{0} Scope Sync - Adding breadcrumb m:\"{1}\" l:\"{2}\"", null, _name, + breadcrumb.Message, breadcrumb.Level); + AddBreadcrumbImpl(breadcrumb); + } + + public abstract void AddBreadcrumbImpl(Breadcrumb breadcrumb); + + public void SetExtra(string key, object? value) + { + // var serialized = value is null ? null : SafeSerializer.SerializeSafely(value); + // if (value is not null && serialized is null) + // { + // _options.DiagnosticLogger?.Log(SentryLevel.Warning, + // "{0} Scope Sync - SetExtra k:\"{1}\" v:\"{2}\" - value was serialized as null", + // null, _name, key, value); + // } + // else + // { + // _options.DiagnosticLogger?.Log(SentryLevel.Debug, + // "{0} Scope Sync - Setting Extra k:\"{1}\" v:\"{2}\"", null, _name, key, value); + // } + // SetExtraImpl(key, serialized); + } + + public abstract void SetExtraImpl(string key, string? value); + + public void SetTag(string key, string value) + { + _options.DiagnosticLogger?.Log(SentryLevel.Debug, + "{0} Scope Sync - Setting Tag k:\"{1}\" v:\"{2}\"", null, _name, key, value); + SetTagImpl(key, value); + } + + public abstract void SetTagImpl(string key, string value); + + public void UnsetTag(string key) + { + _options.DiagnosticLogger?.Log( + SentryLevel.Debug, "{0} Scope Sync - Unsetting Tag k:\"{1}\"", null, _name, key); + UnsetTagImpl(key); + } + + public abstract void UnsetTagImpl(string key); + + public void SetUser(User? user) + { + if (user is null) + { + _options.DiagnosticLogger?.Log( + SentryLevel.Debug, "{0} Scope Sync - Unsetting User", null, _name); + UnsetUserImpl(); + } + else + { + _options.DiagnosticLogger?.Log(SentryLevel.Debug, + "{0} Scope Sync - Setting User i:\"{1}\" n:\"{2}\"", null, _name, user.Id, + user.Username); + SetUserImpl(user); + } + } + + public abstract void SetUserImpl(User user); + + public abstract void UnsetUserImpl(); +} diff --git a/src/Sentry/Platforms/Native/BaseContextWriter.cs b/src/Sentry/Platforms/Native/BaseContextWriter.cs new file mode 100644 index 0000000000..9ff5f58742 --- /dev/null +++ b/src/Sentry/Platforms/Native/BaseContextWriter.cs @@ -0,0 +1,155 @@ +namespace Sentry.Native; + +/// +/// Allows synchronizing Context from .NET to native layers. +/// We're providing a single method that the implementations should override. +/// They can choose to either have the single method directly in native using p/invoke, +/// or use a more fine-grained interface, whatever is best for the platform. +/// +/// +/// WriteScope() is called in a new Task (background thread from a pool). +/// +internal abstract class BaseContextWriter +{ + internal static void WriteApp(string? AppStartTime, string? AppBuildType) + { + var obj = C.sentry_value_new_object(); + C.SetValueIfNotNull(obj, "app_start_time", AppStartTime); + C.SetValueIfNotNull(obj, "build_type", AppBuildType); + C.sentry_set_context(Sentry.Protocol.App.Type, obj); + } + + internal static void WriteOS(string? OperatingSystemRawDescription) + { + var obj = C.sentry_value_new_object(); + C.SetValueIfNotNull(obj, "raw_description", OperatingSystemRawDescription); + C.sentry_set_context(Sentry.Protocol.OperatingSystem.Type, obj); + } + + internal static void WriteDevice( + int? DeviceProcessorCount, + string? DeviceCpuDescription, + string? DeviceTimezone, + bool? DeviceSupportsVibration, + string? DeviceName, + bool? DeviceSimulator, + string? DeviceDeviceUniqueIdentifier, + string? DeviceDeviceType, + string? DeviceModel, + long? DeviceMemorySize) + { + var obj = C.sentry_value_new_object(); + C.SetValueIfNotNull(obj, "processor_count", DeviceProcessorCount); + C.SetValueIfNotNull(obj, "cpu_description", DeviceCpuDescription); + C.SetValueIfNotNull(obj, "timezone", DeviceTimezone); + C.SetValueIfNotNull(obj, "supports_vibration", DeviceSupportsVibration); + C.SetValueIfNotNull(obj, "name", DeviceName); + C.SetValueIfNotNull(obj, "simulator", DeviceSimulator); + C.SetValueIfNotNull(obj, "device_unique_identifier", DeviceDeviceUniqueIdentifier); + C.SetValueIfNotNull(obj, "device_type", DeviceDeviceType); + C.SetValueIfNotNull(obj, "model", DeviceModel); + C.SetValueIfNotNull(obj, "memory_size", DeviceMemorySize); + C.sentry_set_context(Sentry.Protocol.Device.Type, obj); + } + + internal static void WriteGpu( + int? GpuId, + string? GpuName, + string? GpuVendorName, + int? GpuMemorySize, + string? GpuNpotSupport, + string? GpuVersion, + string? GpuApiType, + int? GpuMaxTextureSize, + bool? GpuSupportsDrawCallInstancing, + bool? GpuSupportsRayTracing, + bool? GpuSupportsComputeShaders, + bool? GpuSupportsGeometryShaders, + string? GpuVendorId, + bool? GpuMultiThreadedRendering, + string? GpuGraphicsShaderLevel) + { + var obj = C.sentry_value_new_object(); + C.SetValueIfNotNull(obj, "id", GpuId); + C.SetValueIfNotNull(obj, "name", GpuName); + C.SetValueIfNotNull(obj, "vendor_name", GpuVendorName); + C.SetValueIfNotNull(obj, "memory_size", GpuMemorySize); + C.SetValueIfNotNull(obj, "npot_support", GpuNpotSupport); + C.SetValueIfNotNull(obj, "version", GpuVersion); + C.SetValueIfNotNull(obj, "api_type", GpuApiType); + C.SetValueIfNotNull(obj, "max_texture_size", GpuMaxTextureSize); + C.SetValueIfNotNull(obj, "supports_draw_call_instancing", GpuSupportsDrawCallInstancing); + C.SetValueIfNotNull(obj, "supports_ray_tracing", GpuSupportsRayTracing); + C.SetValueIfNotNull(obj, "supports_compute_shaders", GpuSupportsComputeShaders); + C.SetValueIfNotNull(obj, "supports_geometry_shaders", GpuSupportsGeometryShaders); + C.SetValueIfNotNull(obj, "vendor_id", GpuVendorId); + C.SetValueIfNotNull(obj, "multi_threaded_rendering", GpuMultiThreadedRendering); + C.SetValueIfNotNull(obj, "graphics_shader_level", GpuGraphicsShaderLevel); + C.sentry_set_context(Sentry.Protocol.Gpu.Type, obj); + } + + public void Write(Scope scope) + { + WriteScope( + scope.Contexts.App.StartTime?.ToString("o"), + scope.Contexts.App.BuildType, + scope.Contexts.OperatingSystem.RawDescription, + scope.Contexts.Device.ProcessorCount, + scope.Contexts.Device.CpuDescription, + scope.Contexts.Device.Timezone?.Id, + scope.Contexts.Device.SupportsVibration, + scope.Contexts.Device.Name, + scope.Contexts.Device.Simulator, + scope.Contexts.Device.DeviceUniqueIdentifier, + scope.Contexts.Device.DeviceType, + scope.Contexts.Device.Model, + scope.Contexts.Device.MemorySize, + scope.Contexts.Gpu.Id, + scope.Contexts.Gpu.Name, + scope.Contexts.Gpu.VendorName, + scope.Contexts.Gpu.MemorySize, + scope.Contexts.Gpu.NpotSupport, + scope.Contexts.Gpu.Version, + scope.Contexts.Gpu.ApiType, + scope.Contexts.Gpu.MaxTextureSize, + scope.Contexts.Gpu.SupportsDrawCallInstancing, + scope.Contexts.Gpu.SupportsRayTracing, + scope.Contexts.Gpu.SupportsComputeShaders, + scope.Contexts.Gpu.SupportsGeometryShaders, + scope.Contexts.Gpu.VendorId, + scope.Contexts.Gpu.MultiThreadedRendering, + scope.Contexts.Gpu.GraphicsShaderLevel + ); + } + + protected abstract void WriteScope( + string? AppStartTime, + string? AppBuildType, + string? OperatingSystemRawDescription, + int? DeviceProcessorCount, + string? DeviceCpuDescription, + string? DeviceTimezone, + bool? DeviceSupportsVibration, + string? DeviceName, + bool? DeviceSimulator, + string? DeviceDeviceUniqueIdentifier, + string? DeviceDeviceType, + string? DeviceModel, + long? DeviceMemorySize, + int? GpuId, + string? GpuName, + string? GpuVendorName, + int? GpuMemorySize, + string? GpuNpotSupport, + string? GpuVersion, + string? GpuApiType, + int? GpuMaxTextureSize, + bool? GpuSupportsDrawCallInstancing, + bool? GpuSupportsRayTracing, + bool? GpuSupportsComputeShaders, + bool? GpuSupportsGeometryShaders, + string? GpuVendorId, + bool? GpuMultiThreadedRendering, + string? GpuGraphicsShaderLevel + ); +} diff --git a/src/Sentry/Platforms/Native/NativeContextWriter.cs b/src/Sentry/Platforms/Native/NativeContextWriter.cs new file mode 100644 index 0000000000..88503f7ee9 --- /dev/null +++ b/src/Sentry/Platforms/Native/NativeContextWriter.cs @@ -0,0 +1,73 @@ +namespace Sentry.Native; +using CWUtil = BaseContextWriter; + +// https://github.com/getsentry/sentry-unity/blob/3eb6eca6ed270c5ec023bf75ee53c1ca00bb7c82/src/Sentry.Unity.Native/NativeContextWriter.cs + +internal class NativeContextWriter : BaseContextWriter +{ + protected override void WriteScope( + string? AppStartTime, + string? AppBuildType, + string? OperatingSystemRawDescription, + int? DeviceProcessorCount, + string? DeviceCpuDescription, + string? DeviceTimezone, + bool? DeviceSupportsVibration, + string? DeviceName, + bool? DeviceSimulator, + string? DeviceDeviceUniqueIdentifier, + string? DeviceDeviceType, + string? DeviceModel, + long? DeviceMemorySize, + int? GpuId, + string? GpuName, + string? GpuVendorName, + int? GpuMemorySize, + string? GpuNpotSupport, + string? GpuVersion, + string? GpuApiType, + int? GpuMaxTextureSize, + bool? GpuSupportsDrawCallInstancing, + bool? GpuSupportsRayTracing, + bool? GpuSupportsComputeShaders, + bool? GpuSupportsGeometryShaders, + string? GpuVendorId, + bool? GpuMultiThreadedRendering, + string? GpuGraphicsShaderLevel + ) + { + CWUtil.WriteApp(AppStartTime, AppBuildType); + + CWUtil.WriteOS(OperatingSystemRawDescription); + + CWUtil.WriteDevice( + DeviceProcessorCount, + DeviceCpuDescription, + DeviceTimezone, + DeviceSupportsVibration, + DeviceName, + DeviceSimulator, + DeviceDeviceUniqueIdentifier, + DeviceDeviceType, + DeviceModel, + DeviceMemorySize + ); + + CWUtil.WriteGpu( + GpuId, + GpuName, + GpuVendorName, + GpuMemorySize, + GpuNpotSupport, + GpuVersion, + GpuApiType, + GpuMaxTextureSize, + GpuSupportsDrawCallInstancing, + GpuSupportsRayTracing, + GpuSupportsComputeShaders, + GpuSupportsGeometryShaders, + GpuVendorId, + GpuMultiThreadedRendering, + GpuGraphicsShaderLevel); + } +} diff --git a/src/Sentry/Platforms/Native/NativeScopeObserver.cs b/src/Sentry/Platforms/Native/NativeScopeObserver.cs new file mode 100644 index 0000000000..669752d341 --- /dev/null +++ b/src/Sentry/Platforms/Native/NativeScopeObserver.cs @@ -0,0 +1,46 @@ +namespace Sentry.Native; + +// https://github.com/getsentry/sentry-unity/blob/3eb6eca6ed270c5ec023bf75ee53c1ca00bb7c82/src/Sentry.Unity.Native/NativeScopeObserver.cs +/// +/// Scope Observer for Native through P/Invoke. +/// +/// +internal class NativeScopeObserver : ScopeObserver +{ + public NativeScopeObserver(SentryOptions options) : base("Native", options) { } + + public override void AddBreadcrumbImpl(Breadcrumb breadcrumb) + { + // see https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/ + var crumb = C.sentry_value_new_breadcrumb(breadcrumb.Type, breadcrumb.Message); + C.sentry_value_set_by_key(crumb, "level", C.sentry_value_new_string(breadcrumb.Level.ToString().ToLower())); + C.sentry_value_set_by_key(crumb, "timestamp", C.sentry_value_new_string(GetTimestamp(breadcrumb.Timestamp))); + C.SetValueIfNotNull(crumb, "category", breadcrumb.Category); + C.sentry_add_breadcrumb(crumb); + } + + public override void SetExtraImpl(string key, string? value) => + C.sentry_set_extra(key, value is null ? C.sentry_value_new_null() : C.sentry_value_new_string(value)); + + public override void SetTagImpl(string key, string value) => C.sentry_set_tag(key, value); + + public override void UnsetTagImpl(string key) => C.sentry_remove_tag(key); + + public override void SetUserImpl(User user) + { + // see https://develop.sentry.dev/sdk/event-payloads/user/ + var cUser = C.sentry_value_new_object(); + C.SetValueIfNotNull(cUser, "id", user.Id); + C.SetValueIfNotNull(cUser, "username", user.Username); + C.SetValueIfNotNull(cUser, "email", user.Email); + C.SetValueIfNotNull(cUser, "ip_address", user.IpAddress); + C.sentry_set_user(cUser); + } + + public override void UnsetUserImpl() => C.sentry_remove_user(); + + private static string GetTimestamp(DateTimeOffset timestamp) => + // "o": Using ISO 8601 to make sure the timestamp makes it to the bridge correctly. + // https://docs.microsoft.com/en-gb/dotnet/standard/base-types/standard-date-and-time-format-strings#Roundtrip + timestamp.ToString("o"); +} diff --git a/src/Sentry/Platforms/Native/SentryNative.cs b/src/Sentry/Platforms/Native/SentryNative.cs new file mode 100644 index 0000000000..ca40eefebf --- /dev/null +++ b/src/Sentry/Platforms/Native/SentryNative.cs @@ -0,0 +1,65 @@ +using Sentry.Extensibility; + +namespace Sentry.Native; + +// https://github.com/getsentry/sentry-unity/blob/3eb6eca6ed270c5ec023bf75ee53c1ca00bb7c82/src/Sentry.Unity.Native/SentryNative.cs + +/// +/// Access to the Sentry native support of the sentry-native SDK. +/// +internal static class SentryNative +{ + private static readonly Dictionary PerDirectoryCrashInfo = new(); + + /// + /// Configures the native SDK. + /// + /// The Sentry Unity options to use. + public static void Configure(SentryOptions options) + { + if (!SentryNativeBridge.Init(options)) + { + options.DiagnosticLogger? + .LogWarning("Sentry native initialization failed - native crashes are not captured."); + return; + } + + // ApplicationAdapter.Instance.Quitting += () => + // { + // options.DiagnosticLogger?.LogDebug("Closing the sentry-native SDK"); + // SentryNativeBridge.Close(); + // }; + options.ScopeObserver = new NativeScopeObserver(options); + options.EnableScopeSync = true; + + // options.NativeContextWriter = new NativeContextWriter(); + + // // Use AnalyticsSessionInfo.userId as the default UserID in native & dotnet + // options.DefaultUserId = AnalyticsSessionInfo.userId; + // if (options.DefaultUserId is not null) + // { + // options.ScopeObserver.SetUser(new User { Id = options.DefaultUserId }); + // } + + // Note: we must actually call the function now and on every other call use the value we get here. + // Additionally, we cannot call this multiple times for the same directory, because the result changes + // on subsequent runs. Therefore, we cache the value during the whole runtime of the application. + var cacheDirectory = SentryNativeBridge.GetCacheDirectory(options); + var crashedLastRun = false; + // In the event the SDK is re-initialized with a different path on disk, we need to track which ones were already read + // Similarly we need to cache the value of each call since a subsequent call would return a different value + // as the file used on disk to mark it as crashed is deleted after we read it. + lock (PerDirectoryCrashInfo) + { + if (!PerDirectoryCrashInfo.TryGetValue(cacheDirectory, out crashedLastRun)) + { + crashedLastRun = SentryNativeBridge.HandleCrashedLastRun(options); + PerDirectoryCrashInfo.Add(cacheDirectory, crashedLastRun); + + options.DiagnosticLogger? + .LogDebug("Native SDK reported: 'crashedLastRun': '{0}'", crashedLastRun); + } + } + options.CrashedLastRun = () => crashedLastRun; + } +} diff --git a/src/Sentry/Platforms/Native/SentryNativeBridge.cs b/src/Sentry/Platforms/Native/SentryNativeBridge.cs new file mode 100644 index 0000000000..f03a22b2d4 --- /dev/null +++ b/src/Sentry/Platforms/Native/SentryNativeBridge.cs @@ -0,0 +1,276 @@ +using Sentry.Extensibility; + +namespace Sentry.Native; + +// https://github.com/getsentry/sentry-unity/blob/3eb6eca6ed270c5ec023bf75ee53c1ca00bb7c82/src/Sentry.Unity.Native/SentryNativeBridge.cs + +/// +/// P/Invoke to `sentry-native` functions. +/// +/// +internal static class SentryNativeBridge +{ + public static bool Init(SentryOptions options) + { + _isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + + var cOptions = sentry_options_new(); + + // Note: DSN is not null because options.IsValid() must have returned true for this to be called. + sentry_options_set_dsn(cOptions, options.Dsn!); + + if (options.Release is not null) + { + options.DiagnosticLogger?.LogDebug("Setting Release: {0}", options.Release); + sentry_options_set_release(cOptions, options.Release); + } + + if (options.Environment is not null) + { + options.DiagnosticLogger?.LogDebug("Setting Environment: {0}", options.Environment); + sentry_options_set_environment(cOptions, options.Environment); + } + + options.DiagnosticLogger?.LogDebug("Setting Debug: {0}", options.Debug); + sentry_options_set_debug(cOptions, options.Debug ? 1 : 0); + + if (options.SampleRate.HasValue) + { + options.DiagnosticLogger?.LogDebug("Setting Sample Rate: {0}", options.SampleRate.Value); + sentry_options_set_sample_rate(cOptions, options.SampleRate.Value); + } + + // Disabling the native in favor of the C# layer for now + options.DiagnosticLogger?.LogDebug("Disabling native auto session tracking"); + sentry_options_set_auto_session_tracking(cOptions, 0); + + var dir = GetCacheDirectory(options); + // Note: don't use RuntimeInformation.IsOSPlatform - it will report windows on WSL. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + options.DiagnosticLogger?.LogDebug("Setting CacheDirectoryPath on Windows: {0}", dir); + sentry_options_set_database_pathw(cOptions, dir); + } + else + { + options.DiagnosticLogger?.LogDebug("Setting CacheDirectoryPath: {0}", dir); + sentry_options_set_database_path(cOptions, dir); + } + + if (options.DiagnosticLogger is null) + { + _logger?.LogDebug("Unsetting the current native logger"); + _logger = null; + } + else + { + options.DiagnosticLogger.LogDebug($"{(_logger is null ? "Setting a" : "Replacing the")} native logger"); + _logger = options.DiagnosticLogger; + sentry_options_set_logger(cOptions, new sentry_logger_function_t(nativeLog), IntPtr.Zero); + } + + options.DiagnosticLogger?.LogDebug("Initializing sentry native"); + return 0 == sentry_init(cOptions); + } + + /// + /// Closes the native SDK. + /// + public static void Close() => sentry_close(); + + // Call after native init() to check if the application has crashed in the previous run and clear the status. + // Because the file is removed, the result will change on subsequent calls so it must be cached for the current runtime. + internal static bool HandleCrashedLastRun(SentryOptions options) + { + var result = sentry_get_crashed_last_run() == 1; + sentry_clear_crashed_last_run(); + return result; + } + + internal static string GetCacheDirectory(SentryOptions options) + { + if (options.CacheDirectoryPath is null) + { + // same as the default of sentry-native + return Path.Combine(Directory.GetCurrentDirectory(), ".sentry-native"); + } + else + { + return Path.Combine(options.CacheDirectoryPath, "SentryNative"); + } + } + + // libsentry.so + [DllImport("sentry")] + private static extern IntPtr sentry_options_new(); + + [DllImport("sentry")] + private static extern void sentry_options_set_dsn(IntPtr options, string dsn); + + [DllImport("sentry")] + private static extern void sentry_options_set_release(IntPtr options, string release); + + [DllImport("sentry")] + private static extern void sentry_options_set_debug(IntPtr options, int debug); + + [DllImport("sentry")] + private static extern void sentry_options_set_environment(IntPtr options, string environment); + + [DllImport("sentry")] + private static extern void sentry_options_set_sample_rate(IntPtr options, double rate); + + [DllImport("sentry")] + private static extern void sentry_options_set_database_path(IntPtr options, string path); + + [DllImport("sentry")] + private static extern void sentry_options_set_database_pathw(IntPtr options, [MarshalAs(UnmanagedType.LPWStr)] string path); + + [DllImport("sentry")] + private static extern void sentry_options_set_auto_session_tracking(IntPtr options, int debug); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl, SetLastError = true)] + private delegate void sentry_logger_function_t(int level, IntPtr message, IntPtr argsAddress, IntPtr userData); + + [DllImport("sentry")] + private static extern void sentry_options_set_logger(IntPtr options, [MarshalAs(UnmanagedType.FunctionPtr)]sentry_logger_function_t logger, IntPtr userData); + + // The logger we should forward native messages to. This is referenced by nativeLog() which in turn for. + private static IDiagnosticLogger? _logger; + private static bool _isLinux = false; + + // This method is called from the C library and forwards incoming messages to the currently set _logger. + // TODO: Tried replacing this with [MarshalAs(UnmanagedType.FunctionPtr)] on the target argument (above) + // [MonoPInvokeCallback(typeof(sentry_logger_function_t))] + private static void nativeLog(int cLevel, IntPtr format, IntPtr args, IntPtr userData) + { + try + { + nativeLogImpl(cLevel, format, args, userData); + } + catch + { + // never allow an exception back to native code - it would crash the app + } + } + + private static void nativeLogImpl(int cLevel, IntPtr format, IntPtr args, IntPtr userData) + { + var logger = _logger; + if (logger is null || format == IntPtr.Zero || args == IntPtr.Zero) + { + return; + } + + // see sentry.h: sentry_level_e + var level = cLevel switch + { + -1 => SentryLevel.Debug, + 0 => SentryLevel.Info, + 1 => SentryLevel.Warning, + 2 => SentryLevel.Error, + 3 => SentryLevel.Fatal, + _ => SentryLevel.Info, + }; + + if (!logger.IsEnabled(level)) + { + return; + } + + string? message = null; + try + { + // We cannot access C var-arg (va_list) in c# thus we pass it back to vsnprintf to do the formatting. + // For Linux, we must make a copy of the VaList to be able to pass it back... + if (_isLinux) + { + var argsStruct = Marshal.PtrToStructure(args); + var formattedLength = 0; + WithMarshalledStruct(argsStruct, argsPtr => + { + formattedLength = 1 + vsnprintf_linux(IntPtr.Zero, UIntPtr.Zero, format, argsPtr); + }); + + WithAllocatedPtr(formattedLength, buffer => + WithMarshalledStruct(argsStruct, argsPtr => + { + vsnprintf_linux(buffer, (UIntPtr)formattedLength, format, argsPtr); + message = Marshal.PtrToStringAnsi(buffer); + })); + } + else + { + var formattedLength = 1 + vsnprintf_windows(IntPtr.Zero, UIntPtr.Zero, format, args); + WithAllocatedPtr(formattedLength, buffer => + { + vsnprintf_windows(buffer, (UIntPtr)formattedLength, format, args); + message = Marshal.PtrToStringAnsi(buffer); + }); + } + } + catch (Exception err) + { + logger.LogError("Exception in native log forwarder.", err); + } + + if (message == null) + { + // try to at least print the unreplaced message pattern + message = Marshal.PtrToStringAnsi(format); + } + + if (message != null) + { + logger.Log(level, $"Native: {message}"); + } + } + + [DllImport("msvcrt", EntryPoint = "vsnprintf")] + private static extern int vsnprintf_windows(IntPtr buffer, UIntPtr bufferSize, IntPtr format, IntPtr args); + + [DllImport("libc", EntryPoint = "vsnprintf")] + private static extern int vsnprintf_linux(IntPtr buffer, UIntPtr bufferSize, IntPtr format, IntPtr args); + + // https://stackoverflow.com/a/4958507/2386130 + [StructLayout(LayoutKind.Sequential, Pack = 4)] + private struct VaListLinux64 + { + uint gp_offset; + uint fp_offset; + IntPtr overflow_arg_area; + IntPtr reg_save_area; + } + + private static void WithAllocatedPtr(int size, Action action) + { + var ptr = IntPtr.Zero; + try + { + ptr = Marshal.AllocHGlobal(size); + action(ptr); + } + finally + { + Marshal.FreeHGlobal(ptr); + } + } + + private static void WithMarshalledStruct(T structure, Action action) where T : notnull => + WithAllocatedPtr(Marshal.SizeOf(structure), ptr => + { + Marshal.StructureToPtr(structure, ptr, false); + action(ptr); + }); + + [DllImport("sentry")] + private static extern int sentry_init(IntPtr options); + + [DllImport("sentry")] + private static extern int sentry_close(); + + [DllImport("sentry")] + private static extern int sentry_get_crashed_last_run(); + + [DllImport("sentry")] + private static extern int sentry_clear_crashed_last_run(); +} diff --git a/src/Sentry/Platforms/macOS/CocoaContextWriter.cs b/src/Sentry/Platforms/macOS/CocoaContextWriter.cs new file mode 100644 index 0000000000..69efb2ae75 --- /dev/null +++ b/src/Sentry/Platforms/macOS/CocoaContextWriter.cs @@ -0,0 +1,102 @@ +using Sentry.Internal; + +namespace Sentry.macOS; + +internal class CocoaContextWriter +{ + protected void WriteScope( + string? AppStartTime, + string? AppBuildType, + string? OperatingSystemRawDescription, + int? DeviceProcessorCount, + string? DeviceCpuDescription, + string? DeviceTimezone, + bool? DeviceSupportsVibration, + string? DeviceName, + bool? DeviceSimulator, + string? DeviceDeviceUniqueIdentifier, + string? DeviceDeviceType, + string? DeviceModel, + long? DeviceMemorySize, + int? GpuId, + string? GpuName, + string? GpuVendorName, + int? GpuMemorySize, + string? GpuNpotSupport, + string? GpuVersion, + string? GpuApiType, + int? GpuMaxTextureSize, + bool? GpuSupportsDrawCallInstancing, + bool? GpuSupportsRayTracing, + bool? GpuSupportsComputeShaders, + bool? GpuSupportsGeometryShaders, + string? GpuVendorId, + bool? GpuMultiThreadedRendering, + string? GpuGraphicsShaderLevel + ) => SentryNativeBridgeWriteScope( + // // AppStartTime, + // AppBuildType, + // // OperatingSystemRawDescription, + // DeviceProcessorCount ?? 0, + // DeviceCpuDescription, + // DeviceTimezone, + // marshallNullableBool(DeviceSupportsVibration), + // DeviceName, + // marshallNullableBool(DeviceSimulator), + // DeviceDeviceUniqueIdentifier, + // DeviceDeviceType, + // // DeviceModel, + // // DeviceMemorySize, + GpuId ?? 0, + GpuName, + GpuVendorName, + GpuMemorySize ?? 0, + GpuNpotSupport, + GpuVersion, + GpuApiType, + GpuMaxTextureSize ?? 0, + marshallNullableBool(GpuSupportsDrawCallInstancing), + marshallNullableBool(GpuSupportsRayTracing), + marshallNullableBool(GpuSupportsComputeShaders), + marshallNullableBool(GpuSupportsGeometryShaders), + GpuVendorId, + marshallNullableBool(GpuMultiThreadedRendering), + GpuGraphicsShaderLevel + ); + + private static sbyte marshallNullableBool(bool? value) => (sbyte)(value.HasValue ? (value.Value ? 1 : 0) : -1); + + // Note: we only forward information that's missing or significantly different in cocoa SDK events. + // Additionally, there's currently no way to update existing contexts, so no more Device info for now... + [DllImport("libBridge")] + private static extern void SentryNativeBridgeWriteScope( + // // string? AppStartTime, + // string? AppBuildType, + // // string? OperatingSystemRawDescription, + // int DeviceProcessorCount, + // string? DeviceCpuDescription, + // string? DeviceTimezone, + // sbyte DeviceSupportsVibration, + // string? DeviceName, + // sbyte DeviceSimulator, + // string? DeviceDeviceUniqueIdentifier, + // string? DeviceDeviceType, + // // string? DeviceModel, + // // long? DeviceMemorySize, + int GpuId, + string? GpuName, + string? GpuVendorName, + int GpuMemorySize, + string? GpuNpotSupport, + string? GpuVersion, + string? GpuApiType, + int GpuMaxTextureSize, + sbyte GpuSupportsDrawCallInstancing, + sbyte GpuSupportsRayTracing, + sbyte GpuSupportsComputeShaders, + sbyte GpuSupportsGeometryShaders, + string? GpuVendorId, + sbyte GpuMultiThreadedRendering, + string? GpuGraphicsShaderLevel + ); +} diff --git a/src/Sentry/Platforms/macOS/CocoaScopeObserver.cs b/src/Sentry/Platforms/macOS/CocoaScopeObserver.cs new file mode 100644 index 0000000000..20fadba1ba --- /dev/null +++ b/src/Sentry/Platforms/macOS/CocoaScopeObserver.cs @@ -0,0 +1,43 @@ +namespace Sentry.macOS; + +internal class CocoaScopeObserver : ScopeObserver +{ + public CocoaScopeObserver(string name, SentryOptions options) : base(name, options) { } + + public override void AddBreadcrumbImpl(Breadcrumb breadcrumb) + { + var level = GetBreadcrumbLevel(breadcrumb.Level); + var timestamp = GetTimestamp(breadcrumb.Timestamp); + + SentryCocoaBridgeProxy.SentryNativeBridgeAddBreadcrumb(timestamp, breadcrumb.Message, breadcrumb.Type, breadcrumb.Category, level); + } + + public override void SetExtraImpl(string key, string? value) => + SentryCocoaBridgeProxy.SentryNativeBridgeSetExtra(key, value); + + public override void SetTagImpl(string key, string value) => SentryCocoaBridgeProxy.SentryNativeBridgeSetTag(key, value); + + public override void UnsetTagImpl(string key) => SentryCocoaBridgeProxy.SentryNativeBridgeUnsetTag(key); + + public override void SetUserImpl(User user) => + SentryCocoaBridgeProxy.SentryNativeBridgeSetUser(user.Email, user.Id, user.IpAddress, user.Username); + + public override void UnsetUserImpl() => SentryCocoaBridgeProxy.SentryNativeBridgeUnsetUser(); + + internal static string GetTimestamp(DateTimeOffset timestamp) => + // "o": Using ISO 8601 to make sure the timestamp makes it to the bridge correctly. + // https://docs.microsoft.com/en-gb/dotnet/standard/base-types/standard-date-and-time-format-strings#Roundtrip + timestamp.ToString("o"); + + internal static int GetBreadcrumbLevel(BreadcrumbLevel breadcrumbLevel) => + // https://github.com/getsentry/sentry-cocoa/blob/50f955aeb214601dd62b5dae7abdaddc8a1f24d9/Sources/Sentry/Public/SentryDefines.h#L99-L105 + breadcrumbLevel switch + { + BreadcrumbLevel.Debug => 1, + BreadcrumbLevel.Info => 2, + BreadcrumbLevel.Warning => 3, + BreadcrumbLevel.Error => 4, + BreadcrumbLevel.Critical => 5, + _ => 0 + }; +} diff --git a/src/Sentry/Platforms/macOS/Sentry.macOS.props b/src/Sentry/Platforms/macOS/Sentry.macOS.props new file mode 100644 index 0000000000..2aa2ee23fb --- /dev/null +++ b/src/Sentry/Platforms/macOS/Sentry.macOS.props @@ -0,0 +1,22 @@ + + + + + + + + + + + + PreserveNewest + true + %(RecursiveDir)\%(Filename)%(Extension) + + . + + + + + + diff --git a/src/Sentry/Platforms/macOS/SentryCocoaBridge.cs b/src/Sentry/Platforms/macOS/SentryCocoaBridge.cs new file mode 100644 index 0000000000..4bd0b864cc --- /dev/null +++ b/src/Sentry/Platforms/macOS/SentryCocoaBridge.cs @@ -0,0 +1,53 @@ +using Sentry.Extensibility; + +// https://github.com/getsentry/sentry-unity/blob/3eb6eca6ed270c5ec023bf75ee53c1ca00bb7c82/src/Sentry.Unity.iOS/SentryNativeCocoa.cs + +namespace Sentry.macOS; + + /// + /// Access to the Sentry native support on iOS/macOS. + /// + internal static class SentryCocoaBridge + { + internal static void Configure(SentryOptions options) + // , ISentryUnityInfo sentryUnityInfo, RuntimePlatform platform) + { + if (!options.EnableNativeCrashReporting) + { + options.DiagnosticLogger?.LogDebug("Not initializing Sentry Cocoa. EnableNativeCrashReporting is false."); + return; + } + if (!SentryCocoaBridgeProxy.Init(options)) + { + options.DiagnosticLogger?.LogError("Failed to initialize the native SDK. This doesn't affect .NET monitoring."); + return; + } + + // options.NativeContextWriter = new CocoaContextWriter(); + options.ScopeObserver = new CocoaScopeObserver("macOS", options); + options.EnableScopeSync = true; + options.CrashedLastRun = () => + { + var crashedLastRun = SentryCocoaBridgeProxy.CrashedLastRun() == 1; + options.DiagnosticLogger? + .LogDebug("Native SDK reported: 'crashedLastRun': '{0}'", crashedLastRun); + + return crashedLastRun; + }; + + // options.NativeSupportCloseCallback += () => Close(options.DiagnosticLogger); + // if (sentryUnityInfo.IL2CPP) + // { + // options.DefaultUserId = SentryCocoaBridgeProxy.GetInstallationId(); + // } + } + + /// + /// Closes the native Cocoa support. + /// + public static void Close(IDiagnosticLogger? logger = null) + { + logger?.LogDebug("Closing the sentry-cocoa SDK"); + SentryCocoaBridgeProxy.Close(); + } + } diff --git a/src/Sentry/Platforms/macOS/SentryCocoaBridgeProxy.cs b/src/Sentry/Platforms/macOS/SentryCocoaBridgeProxy.cs new file mode 100644 index 0000000000..7d3c3bb10c --- /dev/null +++ b/src/Sentry/Platforms/macOS/SentryCocoaBridgeProxy.cs @@ -0,0 +1,110 @@ +using Sentry.Extensibility; + +namespace Sentry.macOS; + +// https://github.com/getsentry/sentry-unity/blob/3eb6eca6ed270c5ec023bf75ee53c1ca00bb7c82/src/Sentry.Unity.iOS/SentryCocoaBridgeProxy.cs + +/// +/// P/Invoke to SentryNativeBridge.m which communicates with the `sentry-cocoa` SDK. +/// +/// +/// Functions are declared in `SentryNativeBridge.m` +/// +/// +internal static class SentryCocoaBridgeProxy +{ + // Note: used on macOS only + public static bool Init(SentryOptions options) + { + if (LoadLibrary() != 1) + { + return false; + } + + var cOptions = OptionsNew(); + + // Note: DSN is not null because options.IsValid() must have returned true for this to be called. + OptionsSetString(cOptions, "dsn", options.Dsn!); + + if (options.Release is not null) + { + options.DiagnosticLogger?.LogDebug("Setting Release: {0}", options.Release); + OptionsSetString(cOptions, "release", options.Release); + } + + if (options.Environment is not null) + { + options.DiagnosticLogger?.LogDebug("Setting Environment: {0}", options.Environment); + OptionsSetString(cOptions, "environment", options.Environment); + } + + options.DiagnosticLogger?.LogDebug("Setting Debug: {0}", options.Debug); + OptionsSetInt(cOptions, "debug", options.Debug ? 1 : 0); + + var diagnosticLevel = options.DiagnosticLevel.ToString().ToLowerInvariant(); + options.DiagnosticLogger?.LogDebug("Setting DiagnosticLevel: {0}", diagnosticLevel); + OptionsSetString(cOptions, "diagnosticLevel", diagnosticLevel); + + options.DiagnosticLogger?.LogDebug("Setting SendDefaultPii: {0}", options.SendDefaultPii); + OptionsSetInt(cOptions, "sendDefaultPii", options.SendDefaultPii ? 1 : 0); + + // macOS screenshots currently don't work, because there's no UIKit. Cocoa logs: "Sentry - info:: NO UIKit" + // options.DiagnosticLogger?.LogDebug("Setting AttachScreenshot: {0}", options.AttachScreenshot); + // OptionsSetInt(cOptions, "attachScreenshot", options.AttachScreenshot ? 1 : 0); + OptionsSetInt(cOptions, "attachScreenshot", 0); + + options.DiagnosticLogger?.LogDebug("Setting MaxBreadcrumbs: {0}", options.MaxBreadcrumbs); + OptionsSetInt(cOptions, "maxBreadcrumbs", options.MaxBreadcrumbs); + + options.DiagnosticLogger?.LogDebug("Setting MaxCacheItems: {0}", options.MaxCacheItems); + OptionsSetInt(cOptions, "maxCacheItems", options.MaxCacheItems); + + StartWithOptions(cOptions); + return true; + } + + [DllImport("libBridge", EntryPoint = "SentryNativeBridgeLoadLibrary")] + private static extern int LoadLibrary(); + + [DllImport("libBridge", EntryPoint = "SentryNativeBridgeOptionsNew")] + private static extern IntPtr OptionsNew(); + + [DllImport("libBridge", EntryPoint = "SentryNativeBridgeOptionsSetString")] + private static extern void OptionsSetString(IntPtr options, string name, string value); + + [DllImport("libBridge", EntryPoint = "SentryNativeBridgeOptionsSetInt")] + private static extern void OptionsSetInt(IntPtr options, string name, int value); + + [DllImport("libBridge", EntryPoint = "SentryNativeBridgeStartWithOptions")] + private static extern void StartWithOptions(IntPtr options); + + [DllImport("libBridge", EntryPoint = "SentryNativeBridgeCrashedLastRun")] + public static extern int CrashedLastRun(); + + [DllImport("libBridge", EntryPoint = "SentryNativeBridgeClose")] + public static extern void Close(); + + [DllImport("libBridge")] + public static extern void SentryNativeBridgeAddBreadcrumb(string timestamp, string? message, string? type, string? category, int level); + + [DllImport("libBridge")] + public static extern void SentryNativeBridgeSetExtra(string key, string? value); + + [DllImport("libBridge")] + public static extern void SentryNativeBridgeSetTag(string key, string value); + + [DllImport("libBridge")] + public static extern void SentryNativeBridgeUnsetTag(string key); + + [DllImport("libBridge")] + public static extern void SentryNativeBridgeSetUser(string? email, string? userId, string? ipAddress, string? username); + + [DllImport("libBridge")] + public static extern void SentryNativeBridgeUnsetUser(); + + [DllImport("libBridge", EntryPoint = "SentryNativeBridgeGetInstallationId")] + public static extern string GetInstallationId(); + + [DllImport("libBridge", EntryPoint = "crashBridge")] + public static extern void Crash(); +} diff --git a/src/Sentry/Platforms/macOS/SentryOptions.cs b/src/Sentry/Platforms/macOS/SentryOptions.cs new file mode 100644 index 0000000000..8a0dda6514 --- /dev/null +++ b/src/Sentry/Platforms/macOS/SentryOptions.cs @@ -0,0 +1,9 @@ +namespace Sentry; + +public partial class SentryOptions +{ + /// + /// Enables native macOS crash reporting. + /// + public bool EnableNativeCrashReporting { get; set; } = true; +} diff --git a/src/Sentry/Sentry.csproj b/src/Sentry/Sentry.csproj index aae6ad3a30..f109d955be 100644 --- a/src/Sentry/Sentry.csproj +++ b/src/Sentry/Sentry.csproj @@ -4,6 +4,8 @@ Official SDK for Sentry - Open-source error tracking that helps developers monitor and fix crashes in real time. $(NoWarn);RS0017 true + + true true @@ -12,6 +14,7 @@ $(TargetFrameworks);net7.0-android $(TargetFrameworks);net7.0-ios $(TargetFrameworks);net7.0-maccatalyst + $(TargetFrameworks);net7.0-macos @@ -22,6 +25,7 @@ +