diff --git a/.buildkite/pipeline.full.yml b/.buildkite/pipeline.full.yml index 02fe640f07..88b261970f 100644 --- a/.buildkite/pipeline.full.yml +++ b/.buildkite/pipeline.full.yml @@ -6,14 +6,16 @@ steps: queue: macos-14 artifact_paths: build/fixture-minimal.apk command: make fixture-minimal + env: + JAVA_VERSION: 17 - label: ':android: Build Example App' timeout_in_minutes: 5 agents: queue: macos-14 + command: 'make example-app' env: JAVA_VERSION: 17 - command: 'make example-app' - label: ':android: Build debug fixture APK' key: "fixture-debug" @@ -24,6 +26,8 @@ steps: - "build/fixture-debug.apk" - "build/fixture-debug/*" command: make fixture-debug + env: + JAVA_VERSION: 17 - label: ':android: Build Scan' timeout_in_minutes: 10 @@ -488,6 +492,72 @@ steps: concurrency_group: 'bitbar' concurrency_method: eager + - label: ':bitbar: Android 14 NDK r21 end-to-end tests - batch 1' + depends_on: "fixture-r21" + timeout_in_minutes: 60 + plugins: + artifacts#v1.9.0: + download: + - "build/fixture-r21-url.txt" + - "build/fixture-r21/*" + upload: + - "maze_output/failed/**/*" + - "maze_output/metrics.csv" + docker-compose#v4.7.0: + pull: maze-runner + run: maze-runner + service-ports: true + command: + - "features/full_tests" + - "--exclude=features/full_tests/[^a-k].*.feature" + - "--app=@build/fixture-r21-url.txt" + - "--app-activity=com.bugsnag.android.mazerunner.MainActivity" + - "--app-package=com.bugsnag.android.mazerunner" + - "--appium-version=1.22" + - "--farm=bb" + - "--device=ANDROID_14" + - "--no-tunnel" + - "--aws-public-ip" + - "--fail-fast" + env: + TEST_FIXTURE_SYMBOL_DIR: "build/fixture-r21" + concurrency: 25 + concurrency_group: 'bitbar' + concurrency_method: eager + + - label: ':bitbar: Android 14 NDK r21 end-to-end tests - batch 2' + depends_on: "fixture-r21" + timeout_in_minutes: 60 + plugins: + artifacts#v1.9.0: + download: + - "build/fixture-r21-url.txt" + - "build/fixture-r21/*" + upload: + - "maze_output/failed/**/*" + - "maze_output/metrics.csv" + docker-compose#v4.7.0: + pull: maze-runner + run: maze-runner + service-ports: true + command: + - "features/full_tests" + - "--exclude=features/full_tests/[^l-z].*.feature" + - "--app=@build/fixture-r21-url.txt" + - "--app-activity=com.bugsnag.android.mazerunner.MainActivity" + - "--app-package=com.bugsnag.android.mazerunner" + - "--appium-version=1.22" + - "--farm=bb" + - "--device=ANDROID_14" + - "--no-tunnel" + - "--aws-public-ip" + - "--fail-fast" + env: + TEST_FIXTURE_SYMBOL_DIR: "build/fixture-r21" + concurrency: 25 + concurrency_group: 'bitbar' + concurrency_method: eager + # If there is a tag present activate a manual publishing step - block: 'Trigger package publish' diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 1ed4bcd747..fc336dedd3 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -21,6 +21,8 @@ steps: - bundle install - make fixture-r19 - bundle exec upload-app --farm=bb --app=./build/fixture-r19.apk --app-id-file=build/fixture-r19-url.txt + env: + JAVA_VERSION: 17 - label: ':android: Build fixture APK r21' key: "fixture-r21" @@ -35,6 +37,8 @@ steps: - bundle install - make fixture-r21 - bundle exec upload-app --farm=bb --app=./build/fixture-r21.apk --app-id-file=build/fixture-r21-url.txt + env: + JAVA_VERSION: 17 - label: ':android: Coding standards checks' timeout_in_minutes: 20 @@ -67,6 +71,8 @@ steps: commands: - cd features/fixtures/mazerunner - ./gradlew ktlintCheck detekt checkstyle + env: + JAVA_VERSION: 17 - label: ':android: Android size reporting' timeout_in_minutes: 10 @@ -80,6 +86,16 @@ steps: queue: macos-14 command: './gradlew test' + - label: ':android: Instrumentation tests' + timeout_in_minutes: 10 + plugins: + artifacts#v1.9.0: + upload: "bugsnag-*/build/reports/androidTests/connected/**/*.html" + agents: + queue: macos-14 + command: './scripts/run-connected-checks.rb' + env: + API_LEVEL: 30 # # BitBar steps # @@ -346,6 +362,38 @@ steps: concurrency_group: 'bitbar' concurrency_method: eager + - label: ':bitbar: Android 14 NDK r21 smoke tests' + depends_on: "fixture-r21" + timeout_in_minutes: 60 + plugins: + artifacts#v1.9.0: + download: + - "build/fixture-r21-url.txt" + - "build/fixture-r21/*" + upload: + - "maze_output/failed/**/*" + - "maze_output/metrics.csv" + docker-compose#v4.7.0: + pull: maze-runner + run: maze-runner + service-ports: true + command: + - "features/smoke_tests" + - "--app=@build/fixture-r21-url.txt" + - "--app-activity=com.bugsnag.android.mazerunner.MainActivity" + - "--app-package=com.bugsnag.android.mazerunner" + - "--appium-version=1.22" + - "--farm=bb" + - "--device=ANDROID_14" + - "--no-tunnel" + - "--aws-public-ip" + - "--fail-fast" + env: + TEST_FIXTURE_SYMBOL_DIR: "build/fixture-r21" + concurrency: 25 + concurrency_group: 'bitbar' + concurrency_method: eager + - label: 'Conditionally include device farms/full tests' agents: queue: macos-14 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e501cbd60..42901e967d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 6.6.0 (2024-06-19) + +### Enhancements + +* Support for Android Kernels with a 16kB page size + [#2040](https://github.com/bugsnag/bugsnag-android/pull/2040) + +### Bug fixes + +* Corrected the behavior when `Bugsnag.startSession` is called when `config.autoTrackSessions=true`, the first automatic session will now be correctly discarded + [#2033](https://github.com/bugsnag/bugsnag-android/pull/2033) +* Avoid a possible crash in the ANR plugin when the native ANR library failed to load. + [#2039](https://github.com/bugsnag/bugsnag-android/pull/2039) + ## 6.5.0 (2024-05-15) ### Enhancements diff --git a/Makefile b/Makefile index 0e5bba7b8f..2ee1038902 100644 --- a/Makefile +++ b/Makefile @@ -34,31 +34,31 @@ notifier: fixture-r19: notifier # Build the r19 test fixture - @./gradlew -PTEST_FIXTURE_NDK_VERSION=19.2.5345600 \ + @cd ./features/fixtures/mazerunner && ./gradlew -PTEST_FIXTURE_NDK_VERSION=19.2.5345600 \ -PTEST_FIXTURE_NAME=fixture-r19.apk \ - -p=features/fixtures/mazerunner assembleRelease -x check + assembleRelease -x check @ruby scripts/copy-build-files.rb release r19 fixture-r21: notifier # Build the r21 test fixture - @./gradlew -PTEST_FIXTURE_NDK_VERSION=21.4.7075529 \ + @cd ./features/fixtures/mazerunner && ./gradlew -PTEST_FIXTURE_NDK_VERSION=21.4.7075529 \ -PTEST_FIXTURE_NAME=fixture-r21.apk \ - -p=features/fixtures/mazerunner assembleRelease -x check + assembleRelease -x check @ruby scripts/copy-build-files.rb release r21 fixture-minimal: notifier # Build the minimal test fixture - @./gradlew -PMINIMAL_FIXTURE=true \ + @cd ./features/fixtures/mazerunner && ./gradlew -PMINIMAL_FIXTURE=true \ -PTEST_FIXTURE_NDK_VERSION=17.2.4988734 \ -PTEST_FIXTURE_NAME=fixture-minimal.apk \ - -p=features/fixtures/mazerunner assembleRelease -x check + assembleRelease -x check @ruby scripts/copy-build-files.rb release minimal fixture-debug: notifier # Build the minimal test fixture - @./gradlew -PTEST_FIXTURE_NDK_VERSION=17.2.4988734 \ + @cd ./features/fixtures/mazerunner && ./gradlew -PTEST_FIXTURE_NDK_VERSION=17.2.4988734 \ -PTEST_FIXTURE_NAME=fixture-debug.apk \ - -p=features/fixtures/mazerunner assembleDebug -x check + assembleDebug -x check @ruby scripts/copy-build-files.rb debug debug example-app: @@ -86,4 +86,4 @@ endif check: @./gradlew lint detekt ktlintCheck checkstyle @./scripts/run-cpp-check.sh - @./scripts/run-clang-format-ci-check.sh + @./scripts/run-clang-format-ci-check.sh \ No newline at end of file diff --git a/bugsnag-android-core/api/bugsnag-android-core.api b/bugsnag-android-core/api/bugsnag-android-core.api index ecca487084..04f73e66b5 100644 --- a/bugsnag-android-core/api/bugsnag-android-core.api +++ b/bugsnag-android-core/api/bugsnag-android-core.api @@ -378,6 +378,7 @@ public class com/bugsnag/android/Event : com/bugsnag/android/FeatureFlagAware, c public fun setContext (Ljava/lang/String;)V public fun setGroupingHash (Ljava/lang/String;)V public fun setSeverity (Lcom/bugsnag/android/Severity;)V + public fun setTraceCorrelation (Ljava/util/UUID;J)V public fun setUnhandled (Z)V public fun setUser (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V protected fun shouldDiscardClass ()Z @@ -476,6 +477,7 @@ public class com/bugsnag/android/NativeInterface { public static fun addMetadata (Ljava/lang/String;Ljava/util/Map;)V public static fun clearMetadata (Ljava/lang/String;Ljava/lang/String;)V public static fun createEvent (Ljava/lang/Throwable;Lcom/bugsnag/android/Client;Lcom/bugsnag/android/SeverityReason;)Lcom/bugsnag/android/Event; + public static fun deliverReport (Ljava/io/File;)V public static fun deliverReport ([B[B[BLjava/lang/String;Z)V public static fun getApp ()Ljava/util/Map; public static fun getAppVersion ()Ljava/lang/String; diff --git a/bugsnag-android-core/detekt-baseline.xml b/bugsnag-android-core/detekt-baseline.xml index a5e99e257b..80214ff537 100644 --- a/bugsnag-android-core/detekt-baseline.xml +++ b/bugsnag-android-core/detekt-baseline.xml @@ -20,6 +20,9 @@ LongParameterList:NativeStackframe.kt$NativeStackframe$( /** * The name of the method that was being executed */ var method: String?, /** * The location of the source file */ var file: String?, /** * The line number within the source file this stackframe refers to */ var lineNumber: Number?, /** * The address of the instruction where the event occurred. */ var frameAddress: Long?, /** * The address of the function where the event occurred. */ var symbolAddress: Long?, /** * The address of the library where the event occurred. */ var loadAddress: Long?, /** * Whether this frame identifies the program counter */ var isPC: Boolean?, /** * The type of the error */ var type: ErrorType? = null, /** * Identifies the exact build this frame originates from. */ var codeIdentifier: String? = null, ) LongParameterList:StateEvent.kt$StateEvent.Install$( @JvmField val apiKey: String, @JvmField val autoDetectNdkCrashes: Boolean, @JvmField val appVersion: String?, @JvmField val buildUuid: String?, @JvmField val releaseStage: String?, @JvmField val lastRunInfoPath: String, @JvmField val consecutiveLaunchCrashes: Int, @JvmField val sendThreads: ThreadSendPolicy, @JvmField val maxBreadcrumbs: Int ) LongParameterList:ThreadState.kt$ThreadState$( allThreads: List<JavaThread>, currentThread: JavaThread, exc: Throwable?, isUnhandled: Boolean, maxThreadCount: Int, threadCollectionTimeLimitMillis: Long, projectPackages: Collection<String>, logger: Logger ) + MagicNumber:BugsnagEventMapper.kt$BugsnagEventMapper$16 + MagicNumber:BugsnagEventMapper.kt$BugsnagEventMapper$32 + MagicNumber:BugsnagEventMapper.kt$BugsnagEventMapper$56 MagicNumber:DefaultDelivery.kt$DefaultDelivery$299 MagicNumber:DefaultDelivery.kt$DefaultDelivery$429 MagicNumber:DefaultDelivery.kt$DefaultDelivery$499 @@ -55,7 +58,6 @@ SwallowedException:DeviceDataCollector.kt$DeviceDataCollector$exception: Exception SwallowedException:DeviceIdFilePersistence.kt$DeviceIdFilePersistence$exc: OverlappingFileLockException SwallowedException:EventStore.kt$EventStore$exception: RejectedExecutionException - SwallowedException:EventStore.kt$EventStore$ioe: Exception SwallowedException:ForegroundDetector.kt$ForegroundDetector$e: Exception SwallowedException:ImmutableConfig.kt$e: Exception SwallowedException:JsonHelperTest.kt$JsonHelperTest$e: IllegalArgumentException diff --git a/bugsnag-android-core/src/main/CMakeLists.txt b/bugsnag-android-core/src/main/CMakeLists.txt index c2cebef409..281c8ee11a 100644 --- a/bugsnag-android-core/src/main/CMakeLists.txt +++ b/bugsnag-android-core/src/main/CMakeLists.txt @@ -1,15 +1,19 @@ set(BUGSNAG_VERSION 1.0.1) add_library( # Specifies the name of the library. - bugsnag-root-detection - # Sets the library as a shared library. - SHARED - # Provides a relative path to your source file(s). - jni/root_detection.c - ) + bugsnag-root-detection + # Sets the library as a shared library. + SHARED + # Provides a relative path to your source file(s). + jni/root_detection.c +) include_directories(jni) -set_target_properties(bugsnag-root-detection - PROPERTIES - COMPILE_OPTIONS - -Werror -Wall -pedantic) +set(EXTRA_LINK_FLAGS "-Wl,-z,max-page-size=16384") + +set_target_properties( + bugsnag-root-detection + PROPERTIES + COMPILE_OPTIONS -Werror -Wall -pedantic + LINK_FLAGS "${EXTRA_LINK_FLAGS}" +) diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt index c3af9864b5..e44902d423 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt @@ -7,6 +7,7 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import java.util.TimeZone +import java.util.UUID internal class BugsnagEventMapper( private val logger: Logger @@ -93,6 +94,16 @@ internal class BugsnagEventMapper( // populate internalMetrics event.internalMetrics = InternalMetricsImpl(map["usage"] as MutableMap?) + // populate correlation + (map["correlation"] as? Map)?.let { + val traceId = parseTraceId(it["traceId"]) + val spanId = it["spanId"]?.parseUnsignedLong() + + if (traceId != null && spanId != null) { + event.traceCorrelation = TraceCorrelation(traceId, spanId) + } + } + return event } @@ -234,16 +245,15 @@ internal class BugsnagEventMapper( } } - // SimpleDateFormat isn't thread safe, cache one instance per thread as needed. - private val ndkDateFormatHolder = object : ThreadLocal() { - override fun initialValue(): DateFormat { - return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply { - timeZone = TimeZone.getTimeZone("UTC") + private fun String.toDate(): Date { + if (isNotEmpty() && this[0] == 't') { + // date is in the format 't{epoch millis}' + val timestamp = substring(1) + timestamp.toLongOrNull()?.let { + return Date(it) } } - } - private fun String.toDate(): Date { return try { DateUtils.fromIso8601(this) } catch (pe: IllegalArgumentException) { @@ -251,4 +261,31 @@ internal class BugsnagEventMapper( ?: throw IllegalArgumentException("cannot parse date $this") } } + + private fun parseTraceId(traceId: String?): UUID? { + if (traceId?.length != 32) return null + val mostSigBits = traceId.substring(0, 16).parseUnsignedLong() ?: return null + val leastSigBits = traceId.substring(16).parseUnsignedLong() ?: return null + + return UUID(mostSigBits, leastSigBits) + } + + private fun String.parseUnsignedLong(): Long? { + if (length != 16) return null + return try { + (substring(0, 2).toLong(16) shl 56) or + substring(2).toLong(16) + } catch (nfe: NumberFormatException) { + null + } + } + + // SimpleDateFormat isn't thread safe, cache one instance per thread as needed. + private val ndkDateFormatHolder = object : ThreadLocal() { + override fun initialValue(): DateFormat { + return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply { + timeZone = TimeZone.getTimeZone("UTC") + } + } + } } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Event.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Event.java index ac498bb174..4dc9b31e4f 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Event.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Event.java @@ -10,6 +10,7 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.regex.Pattern; /** @@ -382,6 +383,21 @@ public void setUnhandled(boolean unhandled) { impl.setUnhandled(unhandled); } + /** + * Associate this event with a specific trace. This is usually done automatically when + * using bugsnag-android-performance, but can also be set manually if required. + * + * @param traceId the ID of the trace the event occurred within + * @param spanId the ID of the span that the event occurred within + */ + public void setTraceCorrelation(@NonNull UUID traceId, long spanId) { + if (traceId != null) { + impl.setTraceCorrelation(new TraceCorrelation(traceId, spanId)); + } else { + logNull("traceId"); + } + } + protected boolean shouldDiscardClass() { return impl.shouldDiscardClass(); } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/EventInternal.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventInternal.kt index 4a1881f71d..88b46df334 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/EventInternal.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventInternal.kt @@ -119,6 +119,8 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata */ internal var userImpl: User + var traceCorrelation: TraceCorrelation? = null + fun getUnhandledOverridden(): Boolean = severityReason.unhandledOverridden fun getOriginalUnhandled(): Boolean = severityReason.originalUnhandled @@ -193,6 +195,10 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata writer.name("featureFlags").value(featureFlags) + traceCorrelation?.let { correlation -> + writer.name("correlation").value(correlation) + } + if (session != null) { val copy = Session.copySession(session) writer.name("session").beginObject() diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/EventStore.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventStore.kt index 6eb471d22d..e6d8401ae9 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/EventStore.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventStore.kt @@ -196,6 +196,7 @@ internal class EventStore( return null } } catch (ioe: Exception) { + logger.w("could not parse event payload", ioe) eventSource.clear() } val processedEvent = eventSource.event diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/FileStore.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/FileStore.kt index 1e0ba13731..235ba699bf 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/FileStore.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/FileStore.kt @@ -14,7 +14,7 @@ import java.util.concurrent.locks.Lock import java.util.concurrent.locks.ReentrantLock internal abstract class FileStore( - private val storageDir: File, + val storageDir: File, private val maxStoreCount: Int, private val comparator: Comparator, protected open val logger: Logger, diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/NativeInterface.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/NativeInterface.java index 6f622fdf27..de0da4a21c 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/NativeInterface.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/NativeInterface.java @@ -21,6 +21,7 @@ import java.util.Map; import java.util.regex.Pattern; + /** * Used as the entry point for native code to allow proguard to obfuscate other areas if needed */ @@ -430,6 +431,25 @@ public static void deliverReport(@Nullable byte[] releaseStageBytes, } } + /** + * Attempt to deliver an existing event file that is not current enqueued for delivery. The + * filename is expected to be in the standard {@link EventFilenameInfo} format, and the file + * should contain a correctly formatted {@link Event} object. This method will attempt to + * move the file into place, and flush the queue asynchronously. If the file cannot be moved + * into the queue directory, the file is deleted before returning. + * + * @param reportFile the file to enqueue for delivery + */ + public static void deliverReport(@NonNull File reportFile) { + EventStore eventStore = getClient().eventStore; + File eventFile = new File(eventStore.getStorageDir(), reportFile.getName()); + if (reportFile.renameTo(eventFile)) { + eventStore.flushAsync(); + } else { + reportFile.delete(); + } + } + /** * Notifies using the Android SDK * diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt index 4cc446d679..f89f73b880 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt @@ -7,7 +7,7 @@ import java.io.IOException */ class Notifier @JvmOverloads constructor( var name: String = "Android Bugsnag Notifier", - var version: String = "6.5.0", + var version: String = "6.6.0", var url: String = "https://bugsnag.com" ) : JsonStream.Streamable { diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionTracker.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionTracker.java index 9fc2747f5b..25fb7058c4 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionTracker.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionTracker.java @@ -36,7 +36,7 @@ class SessionTracker extends BaseObservable implements ForegroundDetector.OnActi private volatile Session currentSession = null; final BackgroundTaskService backgroundTaskService; final Logger logger; - private boolean shouldSuppressFirstAutoSession = false; + private boolean shouldSuppressFirstAutoSession = true; SessionTracker(ImmutableConfig configuration, CallbackState callbackState, @@ -108,9 +108,13 @@ private boolean shouldDiscardSession(boolean autoCaptured) { && existingSession != null && !existingSession.isAutoCaptured() && shouldSuppressFirstAutoSession) { - shouldSuppressFirstAutoSession = true; + shouldSuppressFirstAutoSession = false; return true; } + + if (autoCaptured) { + shouldSuppressFirstAutoSession = false; + } } return false; } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/TraceCorrelation.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/TraceCorrelation.kt new file mode 100644 index 0000000000..4fc01ac60f --- /dev/null +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/TraceCorrelation.kt @@ -0,0 +1,20 @@ +package com.bugsnag.android + +import java.util.UUID + +internal data class TraceCorrelation(val traceId: UUID, val spanId: Long) : JsonStream.Streamable { + override fun toStream(writer: JsonStream) { + writer.beginObject() + .name("traceId").value(traceId.toHexString()) + .name("spanId").value(spanId.toHexString()) + writer.endObject() + } + + private fun UUID.toHexString(): String { + return "%016x%016x".format(mostSignificantBits, leastSignificantBits) + } + + private fun Long.toHexString(): String { + return "%016x".format(this) + } +} diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/InternalMetricsImpl.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/InternalMetricsImpl.kt index 6e648e5f48..0900b9166c 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/InternalMetricsImpl.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/InternalMetricsImpl.kt @@ -53,7 +53,7 @@ class InternalMetricsImpl(source: Map? = null) : InternalMetrics { // This is currently the only place where we set static data. // When that changes in future, we'll need a StaticData object to properly merge data // coming from multiple sources. - NdkPluginCaller.setStaticData(mapOf("usage" to mapOf("config" to configDifferences))) + NdkPluginCaller.setStaticData(mapOf("config" to configDifferences)) } override fun setCallbackCounts(newCallbackCounts: Map) { diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventSerializationTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventSerializationTest.kt index f930fe313c..84175e34e7 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventSerializationTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventSerializationTest.kt @@ -9,6 +9,7 @@ import org.junit.runners.Parameterized import org.junit.runners.Parameterized.Parameter import org.junit.runners.Parameterized.Parameters import java.util.Date +import java.util.UUID @RunWith(Parameterized::class) internal class EventSerializationTest { @@ -35,7 +36,8 @@ internal class EventSerializationTest { createEvent { val user = User("123", "foo@example.com", "Joe") val apiKey = "BUGSNAG_API_KEY" - it.session = Session("123", Date(0), user, false, Notifier(), NoopLogger, apiKey) + it.session = + Session("123", Date(0), user, false, Notifier(), NoopLogger, apiKey) }, // threads included @@ -82,6 +84,14 @@ internal class EventSerializationTest { createEvent { it.addFeatureFlag("no_variant") it.addFeatureFlag("flag", "with_variant") + }, + + // with a trace correlation + createEvent { + it.setTraceCorrelation( + UUID(0x24b8b82900d34da3, -0x659434b74a5b9edc), + 0x3dbe7c7ae84945b9 + ) } ) } diff --git a/bugsnag-android-core/src/test/resources/event_serialization_8.json b/bugsnag-android-core/src/test/resources/event_serialization_8.json new file mode 100644 index 0000000000..982656fdb9 --- /dev/null +++ b/bugsnag-android-core/src/test/resources/event_serialization_8.json @@ -0,0 +1,41 @@ +{ + "metaData": {}, + "severity": "warning", + "severityReason": { + "type": "handledException", + "unhandledOverridden": false + }, + "unhandled": false, + "exceptions": [], + "projectPackages":[ + "com.example.foo" + ], + "user": {}, + "app": { + "type": "android", + "versionCode": 0 + }, + "device": { + "cpuAbi": [], + "manufacturer": "samsung", + "model": "s7", + "osName": "android", + "osVersion": "7.1", + "runtimeVersions": { + "osBuild": "bulldog", + "androidApiLevel": "24" + }, + "totalMemory": 109230923452, + "freeDisk": 22234423124, + "freeMemory": 92340255592, + "orientation": "portrait", + "time": "1970-01-01T00:00:00.000Z" + }, + "breadcrumbs": [], + "threads": [], + "featureFlags": [], + "correlation": { + "traceId": "24b8b82900d34da39a6bcb48b5a46124", + "spanId": "3dbe7c7ae84945b9" + } +} diff --git a/bugsnag-plugin-android-anr/src/main/CMakeLists.txt b/bugsnag-plugin-android-anr/src/main/CMakeLists.txt index f4bc87c22e..3777341157 100644 --- a/bugsnag-plugin-android-anr/src/main/CMakeLists.txt +++ b/bugsnag-plugin-android-anr/src/main/CMakeLists.txt @@ -1,23 +1,27 @@ set(BUGSNAG_VERSION 1.0.1) add_library( # Specifies the name of the library. - bugsnag-plugin-android-anr - # Sets the library as a shared library. - SHARED - # Provides a relative path to your source file(s). - jni/anr_google.c - jni/anr_handler.c - jni/bugsnag_anr.c - jni/utils/string.c - ) + bugsnag-plugin-android-anr + # Sets the library as a shared library. + SHARED + # Provides a relative path to your source file(s). + jni/anr_google.c + jni/anr_handler.c + jni/bugsnag_anr.c + jni/utils/string.c +) include_directories(jni) +set(EXTRA_LINK_FLAGS "-Wl,-z,max-page-size=16384") + target_link_libraries( # Specifies the target library. - bugsnag-plugin-android-anr - # Links the log library to the target library. - log) + bugsnag-plugin-android-anr + # Links the log library to the target library. + log) -set_target_properties(bugsnag-plugin-android-anr - PROPERTIES - COMPILE_OPTIONS - -Werror -Wall -pedantic) +set_target_properties( + bugsnag-plugin-android-anr + PROPERTIES + COMPILE_OPTIONS -Werror -Wall -pedantic + LINK_FLAGS "${EXTRA_LINK_FLAGS}" +) diff --git a/bugsnag-plugin-android-anr/src/main/java/com/bugsnag/android/AnrPlugin.kt b/bugsnag-plugin-android-anr/src/main/java/com/bugsnag/android/AnrPlugin.kt index f2bcb924c2..5745b3e2fe 100644 --- a/bugsnag-plugin-android-anr/src/main/java/com/bugsnag/android/AnrPlugin.kt +++ b/bugsnag-plugin-android-anr/src/main/java/com/bugsnag/android/AnrPlugin.kt @@ -67,22 +67,25 @@ internal class AnrPlugin : Plugin { } private fun performOneTimeSetup(client: Client) { - libraryLoader.loadLibrary("bugsnag-plugin-android-anr", client) { + val isLoaded = libraryLoader.loadLibrary("bugsnag-plugin-android-anr", client) { val error = it.errors[0] error.errorClass = "AnrLinkError" error.errorMessage = LOAD_ERR_MSG true } - @Suppress("UNCHECKED_CAST") - val clz = loadClass("com.bugsnag.android.NdkPlugin") as Class? - if (clz != null) { - val ndkPlugin = client.getPlugin(clz) - if (ndkPlugin != null) { - val method = ndkPlugin.javaClass.getMethod("getSignalUnwindStackFunction") - - @Suppress("UNCHECKED_CAST") - val function = method.invoke(ndkPlugin) as Long - setUnwindFunction(function) + + if (isLoaded) { + @Suppress("UNCHECKED_CAST") + val clz = loadClass("com.bugsnag.android.NdkPlugin") as Class? + if (clz != null) { + val ndkPlugin = client.getPlugin(clz) + if (ndkPlugin != null) { + val method = ndkPlugin.javaClass.getMethod("getSignalUnwindStackFunction") + + @Suppress("UNCHECKED_CAST") + val function = method.invoke(ndkPlugin) as Long + setUnwindFunction(function) + } } } } diff --git a/bugsnag-plugin-android-ndk/api/bugsnag-plugin-android-ndk.api b/bugsnag-plugin-android-ndk/api/bugsnag-plugin-android-ndk.api index cdc5a7496c..4b7eea278f 100644 --- a/bugsnag-plugin-android-ndk/api/bugsnag-plugin-android-ndk.api +++ b/bugsnag-plugin-android-ndk/api/bugsnag-plugin-android-ndk.api @@ -16,12 +16,11 @@ public final class com/bugsnag/android/ndk/NativeBridge : com/bugsnag/android/in public final fun clearFeatureFlag (Ljava/lang/String;)V public final fun clearFeatureFlags ()V public final fun clearMetadataTab (Ljava/lang/String;)V - public final fun deliverReportAtPath (Ljava/lang/String;)V public final fun getCurrentCallbackSetCounts ()Ljava/util/Map; public final fun getCurrentNativeApiCallUsage ()Ljava/util/Map; public final fun getSignalUnwindStackFunction ()J public final fun initCallbackCounts (Ljava/util/Map;)V - public final fun install (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IZIZII)V + public final fun install (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IZIZII)V public final fun notifyAddCallback (Ljava/lang/String;)V public final fun notifyRemoveCallback (Ljava/lang/String;)V public fun onStateChange (Lcom/bugsnag/android/StateEvent;)V diff --git a/bugsnag-plugin-android-ndk/detekt-baseline.xml b/bugsnag-plugin-android-ndk/detekt-baseline.xml index 57df3f72f2..af4ed869eb 100644 --- a/bugsnag-plugin-android-ndk/detekt-baseline.xml +++ b/bugsnag-plugin-android-ndk/detekt-baseline.xml @@ -3,9 +3,8 @@ CyclomaticComplexMethod:NativeBridge.kt$NativeBridge$override fun onStateChange(event: StateEvent) - LongMethod:EventOnDiskTests.kt$EventOnDiskTests$@Test fun testEvent() - LongParameterList:NativeBridge.kt$NativeBridge$( apiKey: String, reportingDirectory: String, lastRunInfoPath: String, consecutiveLaunchCrashes: Int, autoDetectNdkCrashes: Boolean, apiLevel: Int, is32bit: Boolean, threadSendPolicy: Int, maxBreadcrumbs: Int, ) - NestedBlockDepth:NativeBridge.kt$NativeBridge$private fun deliverPendingReports() + LongParameterList:NativeBridge.kt$NativeBridge$( apiKey: String, reportingDirectory: String, lastRunInfoPath: String, eventUUID: String, consecutiveLaunchCrashes: Int, autoDetectNdkCrashes: Boolean, apiLevel: Int, is32bit: Boolean, threadSendPolicy: Int, maxBreadcrumbs: Int, ) + SwallowedException:ReportDiscardScanner.kt$ReportDiscardScanner$ex: Exception TooManyFunctions:NativeBridge.kt$NativeBridge : StateObserver UseCheckOrError:ResourceUtils.kt$throw IllegalStateException("Failed to read JSON from $resourceName") diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/AppMetadataSerializationTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/AppMetadataSerializationTest.kt deleted file mode 100644 index 2d050023b9..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/AppMetadataSerializationTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.bugsnag.android.ndk - -import org.junit.Assert.assertEquals -import org.junit.Test - -internal class AppMetadataSerializationTest { - - companion object { - init { - System.loadLibrary("bugsnag-ndk") - System.loadLibrary("bugsnag-ndk-test") - } - } - - external fun run(): String - - @Test - fun testPassesNativeSuite() { - val expected = loadJson("app_meta_data_serialization.json") - val json = run() - assertEquals(expected, json) - } -} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/AppSerializationTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/AppSerializationTest.kt deleted file mode 100644 index bb67499cd2..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/AppSerializationTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.bugsnag.android.ndk - -import org.junit.Assert.assertEquals -import org.junit.Test - -internal class AppSerializationTest { - - companion object { - init { - System.loadLibrary("bugsnag-ndk") - System.loadLibrary("bugsnag-ndk-test") - } - } - - external fun run(): String - - @Test - fun testPassesNativeSuite() { - val expected = loadJson("app_serialization.json") - val json = run() - assertEquals(expected, json) - } -} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/BreadcrumbStateSerializationTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/BreadcrumbStateSerializationTest.kt deleted file mode 100644 index aa9de489ea..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/BreadcrumbStateSerializationTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.bugsnag.android.ndk - -import org.junit.Assert.assertEquals -import org.junit.Test - -internal class BreadcrumbStateSerializationTest { - - companion object { - init { - System.loadLibrary("bugsnag-ndk") - System.loadLibrary("bugsnag-ndk-test") - } - } - - external fun run(): String - - @Test - fun testBreadcrumbSerialization() { - val expected = loadJson("breadcrumbs_serialization.json") - val json = run() - assertEquals(expected, json) - } -} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ContextSerializationTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ContextSerializationTest.kt deleted file mode 100644 index 969f25e7b6..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ContextSerializationTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.bugsnag.android.ndk - -import org.junit.Assert.assertEquals -import org.junit.Test - -internal class ContextSerializationTest { - - companion object { - init { - System.loadLibrary("bugsnag-ndk") - System.loadLibrary("bugsnag-ndk-test") - } - } - - external fun run(): String - - @Test - fun testPassesNativeSuite() { - val expected = loadJson("context_serialization.json") - val json = run() - assertEquals(expected, json) - } -} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/CustomMetadataSerializationTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/CustomMetadataSerializationTest.kt deleted file mode 100644 index 0d2c5d95f5..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/CustomMetadataSerializationTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.bugsnag.android.ndk - -import org.junit.Assert.assertEquals -import org.junit.Test - -internal class CustomMetadataSerializationTest { - - companion object { - init { - System.loadLibrary("bugsnag-ndk") - System.loadLibrary("bugsnag-ndk-test") - } - } - - external fun run(): String - - @Test - fun testPassesNativeSuite() { - val expected = loadJson("custom_meta_data_serialization.json") - val json = run() - assertEquals(expected, json) - } -} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/DeviceSerializationTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/DeviceSerializationTest.kt deleted file mode 100644 index 2953883f00..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/DeviceSerializationTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.bugsnag.android.ndk - -import org.junit.Assert.assertEquals -import org.junit.Test - -internal class DeviceSerializationTest { - - companion object { - init { - System.loadLibrary("bugsnag-ndk") - System.loadLibrary("bugsnag-ndk-test") - } - } - - external fun run(): String - - @Test - fun testPassesNativeSuite() { - val expected = loadJson("device_serialization.json") - val json = run() - assertEquals(expected, json) - } -} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ExceptionSerializationTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ExceptionSerializationTest.kt deleted file mode 100644 index c187ac0f34..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ExceptionSerializationTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.bugsnag.android.ndk - -import org.junit.Assert.assertEquals -import org.junit.Test - -internal class ExceptionSerializationTest { - - companion object { - init { - System.loadLibrary("bugsnag-ndk") - System.loadLibrary("bugsnag-ndk-test") - } - } - - external fun run(): String - - @Test - fun testPassesNativeSuite() { - val expected = loadJson("exception_serialization.json") - val json = run() - assertEquals(expected, json) - } -} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/NativeJsonSerializeTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/NativeJsonSerializeTest.kt index 652db32832..f866a97837 100644 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/NativeJsonSerializeTest.kt +++ b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/NativeJsonSerializeTest.kt @@ -1,6 +1,10 @@ package com.bugsnag.android.ndk +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before import org.junit.Test +import java.io.File class NativeJsonSerializeTest { @@ -11,10 +15,25 @@ class NativeJsonSerializeTest { } } - external fun run(): Int + private val path = File(System.getProperty("java.io.tmpdir"), this::class.simpleName!!) + + @Before + fun setupTmpdir() { + path.mkdirs() + } + + @After + fun deleteTmpdir() { + path.deleteRecursively() + } + + external fun run(outputDir: String): Int @Test fun testPassesNativeSuite() { - verifyNativeRun(run()) + verifyNativeRun(run(path.absolutePath)) + val jsonFile = path.listFiles()!!.maxByOrNull { it.lastModified() }!! + val expected = loadJson("event_serialization.json") + assertEquals(expected, jsonFile.readText()) } } diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/NativeStructToFileTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/NativeStructToFileTest.kt deleted file mode 100644 index d2fe5fdfa7..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/NativeStructToFileTest.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.bugsnag.android.ndk - -import org.junit.Test - -class NativeStructToFileTest { - - companion object { - init { - System.loadLibrary("bugsnag-ndk") - System.loadLibrary("bugsnag-ndk-test") - } - } - - external fun run(): Int - - @Test - fun testPassesNativeSuite() { - verifyNativeRun(run()) - } -} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ReportDiscardScannerTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ReportDiscardScannerTest.kt new file mode 100644 index 0000000000..889cc207c6 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ReportDiscardScannerTest.kt @@ -0,0 +1,38 @@ +package com.bugsnag.android.ndk + +import com.bugsnag.android.Client +import com.bugsnag.android.Logger +import com.bugsnag.android.NativeInterface +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnitRunner +import java.io.File + +@RunWith(MockitoJUnitRunner::class) +class ReportDiscardScannerTest { + @Mock + lateinit var client: Client + + @Before + fun setupNativeInterface() { + NativeInterface.setClient(client) + } + + @Test + fun discardStaticData() { + val discardScanner = ReportDiscardScanner(object : Logger {}, emptySet()) + assertTrue(discardScanner.shouldDiscard(File("/data/data/something/there_is_some.static_data.json"))) + } + + @Test + fun discardNonJson() { + val discardScanner = ReportDiscardScanner(object : Logger {}, emptySet()) + assertTrue(discardScanner.shouldDiscard(File("/data/data/683c6b92-b325-4987-80ad-77086509ca1e.dump"))) + assertTrue(discardScanner.shouldDiscard(File("/data/data/683c6b92-b325-4987-80ad-77086509ca1e.binary"))) + assertTrue(discardScanner.shouldDiscard(File("/data/data/something_not_quite.static_data.binary"))) + assertTrue(discardScanner.shouldDiscard(File("/data/data/data.binary"))) + } +} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/SessionSerializationTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/SessionSerializationTest.kt deleted file mode 100644 index d21934e9d9..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/SessionSerializationTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.bugsnag.android.ndk - -import org.junit.Assert.assertEquals -import org.junit.Test - -internal class SessionSerializationTest { - - companion object { - init { - System.loadLibrary("bugsnag-ndk") - System.loadLibrary("bugsnag-ndk-test") - } - } - - external fun run(): String - - @Test - fun testPassesNativeSuite() { - val expected = loadJson("session_serialization.json") - val json = run() - assertEquals(expected, json) - } -} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/SeverityReasonSerializationTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/SeverityReasonSerializationTest.kt deleted file mode 100644 index 4fedf3c166..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/SeverityReasonSerializationTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.bugsnag.android.ndk - -import org.junit.Assert.assertEquals -import org.junit.Test - -internal class SeverityReasonSerializationTest { - - companion object { - init { - System.loadLibrary("bugsnag-ndk") - System.loadLibrary("bugsnag-ndk-test") - } - } - - external fun run(): String - - @Test - fun testPassesNativeSuite() { - val expected = loadJson("severity_reason_serialization.json") - val json = run() - assertEquals(expected, json) - } -} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/StackframeSerializationTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/StackframeSerializationTest.kt deleted file mode 100644 index 783942452a..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/StackframeSerializationTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.bugsnag.android.ndk - -import org.junit.Assert.assertEquals -import org.junit.Test - -internal class StackframeSerializationTest { - - companion object { - init { - System.loadLibrary("bugsnag-ndk") - System.loadLibrary("bugsnag-ndk-test") - } - } - - external fun run(): String - - @Test - fun testPassesNativeSuite() { - val expected = loadJson("stackframe_serialization.json") - val json = run() - assertEquals(expected, json) - } -} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ThreadSerializationTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ThreadSerializationTest.kt deleted file mode 100644 index ca690e464d..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/ThreadSerializationTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.bugsnag.android.ndk - -import org.junit.Assert -import org.junit.Test - -class ThreadSerializationTest { - - companion object { - init { - System.loadLibrary("bugsnag-ndk") - System.loadLibrary("bugsnag-ndk-test") - } - } - - external fun run(): String - - @Test - fun testPassesNativeSuite() { - val expected = loadJson("thread_serialization.json") - val json = run() - Assert.assertEquals(expected, json) - } -} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/UserSerializationTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/UserSerializationTest.kt deleted file mode 100644 index 844e6873b7..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/UserSerializationTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.bugsnag.android.ndk - -import org.junit.Assert.assertEquals -import org.junit.Test - -internal class UserSerializationTest { - - companion object { - init { - System.loadLibrary("bugsnag-ndk") - System.loadLibrary("bugsnag-ndk-test") - } - } - - external fun run(): String - - @Test - fun testPassesNativeSuite() { - val expected = loadJson("user_serialization.json") - val json = run() - assertEquals(expected, json) - } -} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationTest.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationTest.kt deleted file mode 100644 index 006e5dfab9..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventMigrationTest.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.bugsnag.android.ndk.migrations - -import android.content.Context -import androidx.test.platform.app.InstrumentationRegistry -import com.bugsnag.android.repackaged.dslplatform.json.DslJson -import org.junit.Before -import java.io.File - -open class EventMigrationTest { - - private lateinit var context: Context - private val json = DslJson>() - - @Before - fun setup() { - context = InstrumentationRegistry.getInstrumentation().targetContext - } - - internal fun createTempFile(): File { - return File.createTempFile("migrated_event", ".tmp", context.cacheDir).apply { - deleteOnExit() - } - } - - internal fun parseJSON(file: File): Map { - return deserialize(file.readBytes()) - } - - internal fun parseJSON(text: String): Map { - return deserialize(text.toByteArray()) - } - - private fun deserialize(contents: ByteArray): Map { - val result = json.deserialize(Map::class.java, contents, contents.size) - @Suppress("UNCHECKED_CAST") - return result as Map - } - - companion object NativeLibs { - init { - System.loadLibrary("bugsnag-ndk") - System.loadLibrary("bugsnag-ndk-test") - } - } -} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventOnDiskTests.kt b/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventOnDiskTests.kt deleted file mode 100644 index 87f49fe60d..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/java/com/bugsnag/android/ndk/migrations/EventOnDiskTests.kt +++ /dev/null @@ -1,224 +0,0 @@ -package com.bugsnag.android.ndk.migrations - -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotEquals -import org.junit.Assert.fail -import org.junit.Test - -/** Migration v12 added telemetry data */ -class EventOnDiskTests : EventMigrationTest() { - - @Test - /** check notifier and api key, since they aren't included in event JSON */ - fun testPayloadInfo() { - val infoFile = createTempFile() - - val info = generatePayloadInfo(infoFile.absolutePath) - - assertEquals( - mapOf( - "apiKey" to "5d1e5fbd39a74caa1200142706a90b20", - "notifierName" to "Test Library", - "notifierURL" to "https://example.com/test-lib", - "notifierVersion" to "2.0.11" - ), - parseJSON(info) - ) - } - - @Test - fun testEvent() { - val eventFile = createTempFile() - - generateAndStoreEvent(eventFile.absolutePath) - assertNotEquals(0, eventFile.length()) - - val output = parseJSON(eventFile) - - assertEquals( - "00000000000m0r3.61ee9e6e099d3dd7448f740d395768da6b2df55d5.m4g1c", - output["context"] - ) - assertEquals( - "a1d34088a096987361ee9e6e099d3dd7448f740d395768da6b2df55d5160f33", - output["groupingHash"] - ) - assertEquals("info", output["severity"]) - - // app - assertEquals( - mapOf( - "binaryArch" to "mips", - "buildUUID" to "1234-9876-adfe", - "duration" to 81395165021L, - "durationInForeground" to 81395165010L, - "id" to "com.example.PhotoSnapPlus", - "inForeground" to true, - "isLaunching" to true, - "releaseStage" to "リリース", - "type" to "red", - "version" to "2.0.52", - "versionCode" to 8139512718L - ), - output["app"] - ) - - // breadcrumbs - val crumbs = output["breadcrumbs"] - if (crumbs is List) { - assertEquals(50, crumbs.size) - crumbs.forEachIndexed { index, crumb -> - assertEquals( - mapOf( - "type" to "state", - "name" to "mission $index", - "timestamp" to "2021-12-08T19:43:50.014Z", - "metaData" to mapOf( - "message" to "Now we know what they mean by 'advanced' tactical training." - ) - ), - crumb - ) - } - } else { - fail("breadcrumbs is not a list of crumb objects?!") - } - - // device - assertEquals( - mapOf( - "cpuAbi" to listOf("mipsx"), - "id" to "ffffa", - "locale" to "en_AU#Melbun", - "jailbroken" to true, - "manufacturer" to "HI-TEC™", - "model" to "🍨", - "orientation" to "sideup", - "osName" to "BOX BOX", - "osVersion" to "98.7", - "runtimeVersions" to mapOf( - "osBuild" to "beta1-2", - "androidApiLevel" to "32" - ), - "time" to "2021-12-08T19:43:50Z", - "totalMemory" to 3839512576L - ), - output["device"] - ) - - // feature flags - assertEquals( - listOf( - mapOf( - "featureFlag" to "bluebutton", - "variant" to "on" - ), - mapOf( - "featureFlag" to "redbutton", - "variant" to "off" - ), - mapOf("featureFlag" to "nobutton"), - mapOf( - "featureFlag" to "switch", - "variant" to "left" - ) - ), - output["featureFlags"] - ) - - // exceptions - assertEquals( - listOf( - mapOf( - "errorClass" to "SIGBUS", - "message" to "POSIX is serious about oncoming traffic", - "type" to "c", - "stacktrace" to listOf( - mapOf( - "frameAddress" to "0xfffffffe", - "lineNumber" to 4194967233L, - "loadAddress" to "0x242023", - "symbolAddress" to "0x308", - "method" to "makinBacon", - "file" to "lib64/libfoo.so", - "isPC" to true - ), - mapOf( - "frameAddress" to "0xb37a644b", - "lineNumber" to 0L, - "loadAddress" to "0x0", - "symbolAddress" to "0x0", - "method" to "0xb37a644b" // test address to method hex - ) - ) - ) - ), - output["exceptions"] - ) - - // metadata - assertEquals( - mapOf( - "app" to mapOf( - "activeScreen" to "Menu", - "weather" to "rain" - ), - "metrics" to mapOf( - "experimentX" to false, - "subject" to "percy", - "counter" to 47.5.toBigDecimal() - ) - ), - output["metaData"] - ) - - // session info - assertEquals( - mapOf( - "id" to "aaaaaaaaaaaaaaaa", - "startedAt" to "2031-07-09T11:08:21+00:00", - "events" to mapOf( - "handled" to 5L, - "unhandled" to 2L - ) - ), - output["session"] - ) - - // threads - val threads = output["threads"] - if (threads is List) { - assertEquals(8, threads.size) - threads.forEachIndexed { index, thread -> - assertEquals( - mapOf( - "name" to "Thread #$index", - "state" to "paused-$index", - "id" to (1000L + index).toString(), - "type" to "c" - ), - thread - ) - } - } else { - fail("threads is not a list of thread objects?!") - } - - // user - assertEquals( - mapOf( - "email" to "fenton@io.example.com", - "name" to "Fenton", - "id" to "fex01" - ), - output["user"] - ) - } - - /** Generate an event to the latest format, writing JSON to tempFilePath */ - external fun generateAndStoreEvent(tempFilePath: String) - - /** Generate notifier and apiKey info to a bespoke structure (apiKey and - * notifier are not included in event info written to disk) */ - external fun generatePayloadInfo(tempFilePath: String): String -} diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/app_meta_data_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/app_meta_data_serialization.json deleted file mode 100644 index 779fbb2d83..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/resources/app_meta_data_serialization.json +++ /dev/null @@ -1 +0,0 @@ -{"metaData":{"app":{"activeScreen":"MainActivity"}}} \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/app_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/app_serialization.json deleted file mode 100644 index e7a69b0d5d..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/resources/app_serialization.json +++ /dev/null @@ -1 +0,0 @@ -{"app":{"version":"22","id":"com.bugsnag.example","type":"android","releaseStage":"prod","versionCode":55,"buildUUID":"1234-uuid","binaryArch":"x86","duration":6502,"durationInForeground":6502,"inForeground":true,"isLaunching":true}} \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/breadcrumbs_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/breadcrumbs_serialization.json deleted file mode 100644 index 5605a4987f..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/resources/breadcrumbs_serialization.json +++ /dev/null @@ -1 +0,0 @@ -[{"name":"Jane","timestamp":"2018-10-08T12:07:09Z","type":"user","metaData":{"str":"Foo"}},{"name":"Something went wrong","timestamp":"2018-10-08T12:07:11Z","type":"manual","metaData":{"bool":true}},{"name":"MainActivity","timestamp":"2018-10-08T12:07:15.563Z","type":"navigation"},{"name":"Updated store","timestamp":"2018-10-08T12:07:16Z","type":"state"}] \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/context_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/context_serialization.json deleted file mode 100644 index f503716fb7..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/resources/context_serialization.json +++ /dev/null @@ -1 +0,0 @@ -{"context":"CustomContext"} \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/custom_meta_data_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/custom_meta_data_serialization.json deleted file mode 100644 index 7000565f31..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/resources/custom_meta_data_serialization.json +++ /dev/null @@ -1 +0,0 @@ -{"metaData":{"custom":{"str":"Foo","bool":true,"num":55}}} \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/device_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/device_serialization.json deleted file mode 100644 index 91c48f9f9e..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/resources/device_serialization.json +++ /dev/null @@ -1 +0,0 @@ -{"device":{"osName":"android","id":"f5gh7","locale":"En","osVersion":"8.1","manufacturer":"Samsung","model":"S7","orientation":"portrait","runtimeVersions":{"androidApiLevel":"29","osBuild":"BullDog 5.2"},"cpuAbi":["x86"],"totalMemory":512340922,"jailbroken":true,"time":"2029-01-01T00:00:00Z"}} \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/event_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/event_serialization.json new file mode 100644 index 0000000000..1e3189a138 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/androidTest/resources/event_serialization.json @@ -0,0 +1 @@ +{"context":"Foo","metaData":{},"severity":"info","unhandled":true,"severityReason":{"unhandledOverridden":false,"type":"signal","attributes":{"signalType":"SIGSEGV"}},"exceptions":[{"errorClass":"SIGSEGV","message":"Whoops!","type":"c","stacktrace":[{"frameAddress":"0x0","symbolAddress":"0x0","loadAddress":"0x0","lineNumber":58,"isPC":true,"file":"Something.c","method":"foo()"}]}],"user":{"id":"123","name":"Bob Bobbiton","email":"bob@example.com"},"app":{"version":"1.0","id":"fa02","type":"C","releaseStage":"dev","versionCode":55,"buildUUID":"123","binaryArch":"x86","duration":9019,"durationInForeground":7017,"inForeground":true,"isLaunching":true},"device":{"osName":"android","id":"my-id-123","locale":"en","osVersion":"9.1","manufacturer":"Google","model":"Nexus","orientation":"portrait","runtimeVersions":{"androidApiLevel":0,"osBuild":""},"cpuAbi":[],"totalMemory":1095092340,"jailbroken":true,"time":"1970-01-01T02:06:49Z"},"breadcrumbs":[],"groupingHash":"Bar","usage":{"callbacks":{}},"threads":[]} \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/exception_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/exception_serialization.json deleted file mode 100644 index 4bb42ad10e..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/resources/exception_serialization.json +++ /dev/null @@ -1 +0,0 @@ -{"stacktrace":[{"frameAddress":"0x20000000","symbolAddress":"0x16000000","loadAddress":"0x12000000","lineNumber":52,"isPC":true,"file":"foo.c","method":"bar()"}],"errorClass":"signal","message":"whoops something went wrong","type":"c"} \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/session_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/session_serialization.json deleted file mode 100644 index 57da6eac70..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/resources/session_serialization.json +++ /dev/null @@ -1 +0,0 @@ -{"session":{"startedAt":"2018-10-08T12:07:09Z","id":"123","events":{"handled":2,"unhandled":1}}} \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/severity_reason_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/severity_reason_serialization.json deleted file mode 100644 index 18142a7369..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/resources/severity_reason_serialization.json +++ /dev/null @@ -1 +0,0 @@ -{"severity":"error","unhandled":true,"severityReason":{"unhandledOverridden":false,"type":"signal","attributes":{"signalType":"SIGABRT"}}} \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/stackframe_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/stackframe_serialization.json deleted file mode 100644 index f62a8babb8..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/resources/stackframe_serialization.json +++ /dev/null @@ -1 +0,0 @@ -[{"frameAddress":"0x20000000","symbolAddress":"0x16000000","loadAddress":"0x12000000","lineNumber":52,"file":"foo.c","method":"bar()"}] \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/thread_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/thread_serialization.json deleted file mode 100644 index 81c37a66c1..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/resources/thread_serialization.json +++ /dev/null @@ -1 +0,0 @@ -[{"id":"1234","name":"Binder 1","state":"Running","type":"c"}] \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/user_serialization.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/user_serialization.json deleted file mode 100644 index 5527107eb3..0000000000 --- a/bugsnag-plugin-android-ndk/src/androidTest/resources/user_serialization.json +++ /dev/null @@ -1 +0,0 @@ -{"user":{"name":"Fenton","email":"fenton@io.example.com","id":"1234"}} \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt b/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt index 6492ae9e7d..73d98025d4 100644 --- a/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt +++ b/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt @@ -1,58 +1,62 @@ set(BUGSNAG_VERSION 1.0.1) add_library( # Specifies the name of the library. - bugsnag-ndk - - # Sets the library as a shared library. - SHARED - - # Provides a relative path to your source file(s). - jni/bugsnag_ndk.c - jni/bugsnag.c - jni/metadata.c - jni/safejni.c - jni/jni_cache.c - jni/event.c - jni/featureflags.c - jni/internal_metrics.c - jni/handlers/signal_handler.c - jni/handlers/cpp_handler.cpp - jni/utils/crash_info.c - jni/utils/serializer/buffered_writer.c - jni/utils/serializer/event_reader.c - jni/utils/serializer/event_writer.c - jni/utils/serializer/json_writer.c - jni/utils/stack_unwinder.cpp + bugsnag-ndk + + # Sets the library as a shared library. + SHARED + + # Provides a relative path to your source file(s). + jni/bugsnag_ndk.c + jni/bugsnag.c + jni/metadata.c + jni/safejni.c + jni/jni_cache.c + jni/event.c + jni/featureflags.c + jni/internal_metrics.c + jni/handlers/signal_handler.c + jni/handlers/cpp_handler.cpp + jni/utils/crash_info.c + jni/utils/serializer/buffered_writer.c + jni/utils/serializer/event_writer.c + jni/utils/serializer/BSG_KSJSONCodec.c + jni/utils/serializer/BSG_KSCrashStringConversion.c + jni/utils/stack_unwinder.cpp jni/utils/seqlock.c - jni/utils/serializer.c - jni/utils/string.c - jni/utils/threads.c - jni/utils/memory.c - jni/deps/parson/parson.c - ) + jni/utils/serializer.c + jni/utils/string.c + jni/utils/threads.c + jni/utils/memory.c + jni/deps/parson/parson.c +) include_directories( - jni - jni/deps - jni/external/libunwindstack-ndk/include - ) + jni + jni/deps + jni/external/libunwindstack-ndk/include +) target_include_directories(bugsnag-ndk PRIVATE ${BUGSNAG_DIR}/assets/include) -target_link_libraries( # Specifies the target library. - bugsnag-ndk - # Links the log library to the target library. - log) +target_link_libraries( + # Specifies the target library. + bugsnag-ndk + # Links the log library to the target library. + log +) # Avoid exporting symbols in release mode to keep internals private # More symbols are exported in debug mode for the sake of unit testing -set(EXTRA_LINK_FLAGS "-Wl,--version-script=${CMAKE_CURRENT_LIST_DIR}/exported_native_symbols-${CMAKE_BUILD_TYPE}.txt") - -set_target_properties(bugsnag-ndk - PROPERTIES - COMPILE_OPTIONS -Werror -Wall -pedantic - LINK_FLAGS "${EXTRA_LINK_FLAGS}" - CXX_STANDARD 17 - CXX_STANDARD_REQUIRED YES) +set(EXTRA_LINK_FLAGS "-Wl,-z,max-page-size=16384,--version-script=${CMAKE_CURRENT_LIST_DIR}/exported_native_symbols-${CMAKE_BUILD_TYPE}.txt") + +set_target_properties( + bugsnag-ndk + PROPERTIES + COMPILE_OPTIONS -Werror -Wall -pedantic + LINK_FLAGS "${EXTRA_LINK_FLAGS}" + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED YES +) add_subdirectory(jni/external/libunwindstack-ndk/cmake) target_link_libraries(bugsnag-ndk unwindstack) diff --git a/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.kt b/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.kt index 33115f355c..793eec21ef 100644 --- a/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.kt +++ b/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.kt @@ -22,7 +22,6 @@ import com.bugsnag.android.internal.BackgroundTaskService import com.bugsnag.android.internal.StateObserver import com.bugsnag.android.internal.TaskType import java.io.File -import java.io.FileFilter import java.util.UUID import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.locks.ReentrantLock @@ -48,6 +47,7 @@ class NativeBridge(private val bgTaskService: BackgroundTaskService) : StateObse apiKey: String, reportingDirectory: String, lastRunInfoPath: String, + eventUUID: String, consecutiveLaunchCrashes: Int, autoDetectNdkCrashes: Boolean, apiLevel: Int, @@ -63,7 +63,6 @@ class NativeBridge(private val bgTaskService: BackgroundTaskService) : StateObse unhandledCount: Int ) - external fun deliverReportAtPath(filePath: String) fun addBreadcrumb(name: String, type: String, timestamp: String, metadata: Any) { val breadcrumbType = BreadcrumbType.values() .find { it.toString() == type } @@ -109,7 +108,7 @@ class NativeBridge(private val bgTaskService: BackgroundTaskService) : StateObse when (event) { is Install -> handleInstallMessage(event) - DeliverPending -> deliverPendingReports() + is DeliverPending -> deliverPendingReports() is AddMetadata -> handleAddMetadata(event) is ClearMetadataSection -> clearMetadataTab(event.section) is ClearMetadataValue -> removeMetadata( @@ -172,6 +171,17 @@ class NativeBridge(private val bgTaskService: BackgroundTaskService) : StateObse } } + private fun deliverPendingReports() { + val discardScanner = ReportDiscardScanner(logger) + reportDirectory.listFiles()?.forEach { reportFile -> + if (discardScanner.shouldDiscard(reportFile)) { + reportFile.delete() + } else { + NativeInterface.deliverReport(reportFile) + } + } + } + private fun isInvalidMessage(msg: Any?): Boolean { if (msg == null || msg !is StateEvent) { return true @@ -183,39 +193,16 @@ class NativeBridge(private val bgTaskService: BackgroundTaskService) : StateObse return false } - private fun deliverPendingReports() { - val filenameRegex = """.*\.crash$""".toRegex() - lock.lock() - try { - val outDir = reportDirectory - if (outDir.exists()) { - val fileList = - outDir.listFiles(FileFilter { filenameRegex.containsMatchIn(it.name) }) - if (fileList != null) { - for (file in fileList) { - deliverReportAtPath(file.absolutePath) - } - } - } else { - logger.w("Payload directory does not exist, cannot read pending reports") - } - } catch (ex: Exception) { - logger.w("Failed to parse/write pending reports: $ex") - } finally { - lock.unlock() - } - } - private fun handleInstallMessage(arg: Install) { lock.withLock { if (installed.get()) { logger.w("Received duplicate setup message with arg: $arg") } else { - val reportPath = File(reportDirectory, "${UUID.randomUUID()}.crash").absolutePath install( arg.apiKey, - reportPath, + reportDirectory.absolutePath, arg.lastRunInfoPath, + UUID.randomUUID().toString(), arg.consecutiveLaunchCrashes, arg.autoDetectNdkCrashes, Build.VERSION.SDK_INT, diff --git a/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/ReportDiscardScanner.kt b/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/ReportDiscardScanner.kt new file mode 100644 index 0000000000..cb48661821 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/ReportDiscardScanner.kt @@ -0,0 +1,135 @@ +package com.bugsnag.android.ndk + +import android.util.JsonReader +import android.util.JsonToken +import androidx.annotation.VisibleForTesting +import com.bugsnag.android.Logger +import com.bugsnag.android.NativeInterface +import java.io.File + +internal class ReportDiscardScanner( + private val logger: Logger, + private val enabledReleaseStages: Collection = + NativeInterface.getEnabledReleaseStages() ?: emptySet(), +) { + /** + * Checks whether a given report file should be discarded due to its `releaseStage` or any of + * the configured `discardClasses`. + * + * @return true if the report should be discarded instead of being sent + */ + fun shouldDiscard(report: File): Boolean { + if (!report.name.endsWith(".json") || + report.name.endsWith(".static_data.json") + ) { + return true + } + + return try { + report.bufferedReader().use { reader -> + JsonReader(reader).use { json -> shouldDiscard(json) } + } + } catch (ex: Exception) { + false + } + } + + @VisibleForTesting + internal fun shouldDiscard(json: JsonReader): Boolean { + json.beginObject() + var pendingAppCheck = true + var pendingExceptionsCheck = true + + while (json.hasNext() && (pendingAppCheck || pendingExceptionsCheck)) { + val nextName = json.nextName() + val discard: Boolean = when (nextName) { + "app" -> { + pendingAppCheck = false + shouldDiscardForApp(json) + } + + "exceptions" -> { + pendingExceptionsCheck = false + shouldDiscardForExceptions(json) + } + + else -> { + json.skipValue() + false + } + } + + if (discard) { + return true + } + } + + return false + } + + private fun shouldDiscardForApp(json: JsonReader): Boolean { + if (enabledReleaseStages.isEmpty()) { + json.skipValue() + return false + } + + json.beginObject() + while (json.peek() != JsonToken.END_OBJECT) { + val nextName = json.nextName() + when (nextName) { + "releaseStage" -> { + val releaseStage = json.nextString() + if (releaseStage !in enabledReleaseStages) { + return true + } + // do not early exit, make sure the entire "app" object is consumed + // before returning + } + + else -> json.skipValue() + } + } + json.endObject() + + return false + } + + private fun shouldDiscardForExceptions(json: JsonReader): Boolean { + json.beginArray() + + while (json.peek() != JsonToken.END_ARRAY) { + if (shouldDiscardException(json)) { + logger.d("Discarding native report due to errorClass") + return true + } + } + + json.endArray() + + return false + } + + private fun shouldDiscardException(json: JsonReader): Boolean { + json.beginObject() + + while (json.peek() != JsonToken.END_OBJECT) { + val name = json.nextName() + when (name) { + "errorClass" -> { + val errorClass = json.nextString() + if (NativeInterface.isDiscardErrorClass(errorClass)) { + return true + } + // do not early exit, make sure the entire "exception" object is consumed + // before returning + } + + else -> json.skipValue() + } + } + + json.endObject() + + return false + } +} diff --git a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c index 8b06c9b17d..13278dfceb 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c @@ -16,15 +16,12 @@ #include "metadata.h" #include "safejni.h" #include "utils/serializer.h" -#include "utils/serializer/event_reader.h" #include "utils/string.h" #ifdef __cplusplus extern "C" { #endif -#define STATIC_DATA_FILENAME_EXTENSION ".static_data.json" - static bsg_environment *bsg_global_env; static pthread_mutex_t bsg_global_env_write_mutex = PTHREAD_MUTEX_INITIALIZER; @@ -148,9 +145,10 @@ void bsg_update_next_run_info(bsg_environment *env) { JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_install( JNIEnv *env, jobject _this, jstring _api_key, jstring _event_path, - jstring _last_run_info_path, jint consecutive_launch_crashes, - jboolean auto_detect_ndk_crashes, jint _api_level, jboolean is32bit, - jint send_threads, jint max_breadcrumbs) { + jstring _last_run_info_path, jstring _event_uuid, + jint consecutive_launch_crashes, jboolean auto_detect_ndk_crashes, + jint _api_level, jboolean is32bit, jint send_threads, + jint max_breadcrumbs) { if (!bsg_jni_cache_init(env)) { BUGSNAG_LOG("Could not init JNI jni_cache."); @@ -178,11 +176,18 @@ JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_install( if (event_path == NULL) { goto error; } - sprintf(bugsnag_env->next_event_path, "%s", event_path); - sprintf(bugsnag_env->next_event_static_data_path, "%s%s", event_path, - STATIC_DATA_FILENAME_EXTENSION); + bugsnag_env->event_path = strdup(event_path); bsg_safe_release_string_utf_chars(env, _event_path, event_path); + // copy the event UUID to the env struct + const char *event_uuid = bsg_safe_get_string_utf_chars(env, _event_uuid); + if (event_uuid == NULL) { + goto error; + } + bsg_strncpy(bugsnag_env->event_uuid, event_uuid, + sizeof(bugsnag_env->event_uuid)); + bsg_safe_release_string_utf_chars(env, _event_uuid, event_uuid); + // copy last run info path to env struct const char *last_run_info_path = bsg_safe_get_string_utf_chars(env, _last_run_info_path); @@ -236,102 +241,6 @@ JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_install( free(bugsnag_env); } -JNIEXPORT void JNICALL -Java_com_bugsnag_android_ndk_NativeBridge_deliverReportAtPath( - JNIEnv *env, jobject _this, jstring _report_path) { - static pthread_mutex_t bsg_native_delivery_mutex = PTHREAD_MUTEX_INITIALIZER; - pthread_mutex_lock(&bsg_native_delivery_mutex); - - const char *event_path = NULL; - char static_data_path[384]; - bugsnag_event *event = NULL; - jbyteArray jpayload = NULL; - jbyteArray jstage = NULL; - char *payload = NULL; - char *static_data = NULL; - jbyteArray jstatic_data = NULL; - jstring japi_key = NULL; - jstring errorClass = NULL; - - if (!bsg_jni_cache->initialized) { - BUGSNAG_LOG("deliverReportAtPath failed: JNI cache not initialized."); - goto exit; - } - - event_path = bsg_safe_get_string_utf_chars(env, _report_path); - if (event_path == NULL) { - goto exit; - } - event = bsg_deserialize_event_from_file((char *)event_path); - - snprintf(static_data_path, sizeof(static_data_path), "%s%s", event_path, - STATIC_DATA_FILENAME_EXTENSION); - if (bsg_read_text_file(static_data_path, &static_data) > 0) { - jstatic_data = bsg_byte_ary_from_string(env, static_data); - } - - // remove persisted NDK struct early - this reduces the chance of crash loops - // in delivery. - remove(event_path); - remove(static_data_path); - - if (event == NULL) { - BUGSNAG_LOG("Failed to read event at file: %s", event_path); - goto exit; - } - - errorClass = bsg_safe_new_string_utf(env, event->error.errorClass); - if (bsg_safe_call_static_boolean_method( - env, bsg_jni_cache->NativeInterface, - bsg_jni_cache->NativeInterface_isDiscardErrorClass, errorClass)) { - goto exit; - } - - payload = bsg_serialize_event_to_json_string(event); - if (payload == NULL) { - BUGSNAG_LOG("Failed to serialize event as JSON: %s", event_path); - goto exit; - } - - // generate payload bytearray - jpayload = bsg_byte_ary_from_string(env, payload); - if (jpayload == NULL) { - goto exit; - } - - // generate releaseStage bytearray - jstage = bsg_byte_ary_from_string(env, event->app.release_stage); - if (jstage == NULL) { - goto exit; - } - - // call NativeInterface.deliverReport() - japi_key = bsg_safe_new_string_utf(env, event->api_key); - if (japi_key != NULL) { - bool is_launching = event->app.is_launching; - bsg_safe_call_static_void_method( - env, bsg_jni_cache->NativeInterface, - bsg_jni_cache->NativeInterface_deliverReport, jstage, jpayload, - jstatic_data, japi_key, is_launching); - } - -exit: - bsg_safe_delete_local_ref(env, errorClass); - bsg_safe_release_string_utf_chars(env, _report_path, event_path); - if (event != NULL) { - bsg_safe_release_byte_array_elements(env, jstage, - (jbyte *)event->app.release_stage); - bsg_free_feature_flags(event); - free(event); - } - bsg_safe_release_byte_array_elements(env, jpayload, (jbyte *)payload); - free(payload); - bsg_safe_release_byte_array_elements(env, jstatic_data, (jbyte *)static_data); - free(static_data); - - pthread_mutex_unlock(&bsg_native_delivery_mutex); -} - JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_addHandledEvent(JNIEnv *env, jobject _this) { diff --git a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h index 5373abd018..411c7fffdc 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.h @@ -24,12 +24,11 @@ typedef struct { /** * File path on disk where the next crash report will be written if needed. */ - char next_event_path[384]; + char *event_path; /** - * File path on disk where the next crash report static data will be written - * if needed. + * The pre-generated UUID of the next crash report. */ - char next_event_static_data_path[384]; + char event_uuid[37]; /** * File path on disk where the last run info will be written if needed. */ diff --git a/bugsnag-plugin-android-ndk/src/main/jni/metadata.c b/bugsnag-plugin-android-ndk/src/main/jni/metadata.c index f466322c9a..0968a3d380 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/metadata.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/metadata.c @@ -376,101 +376,71 @@ static void populate_metadata_value(JNIEnv *env, bugsnag_metadata *dst, } static void populate_metadata_obj(JNIEnv *env, bugsnag_metadata *dst, - jobject section, jobject section_keylist, - int index) { - jstring section_key = NULL; - const char *name = NULL; - jobject _value = NULL; - - if (!bsg_jni_cache->initialized) { - goto exit; - } - - section_key = bsg_safe_call_object_method( - env, section_keylist, bsg_jni_cache->ArrayList_get, (jint)index); - if (section_key == NULL) { - goto exit; - } + const char *section, jobject section_map) { + jobject entryset = NULL; + jobject entries = NULL; - _value = bsg_safe_call_object_method(env, section, bsg_jni_cache->Map_get, - section_key); - name = bsg_safe_get_string_utf_chars(env, section_key); - if (name == NULL) { + if (section_map == NULL) { goto exit; } - - populate_metadata_value(env, dst, section, name, _value); - -exit: - bsg_safe_release_string_utf_chars(env, section_key, name); - bsg_safe_delete_local_ref(env, section_key); - bsg_safe_delete_local_ref(env, _value); -} - -static void populate_metadata_section(JNIEnv *env, bugsnag_metadata *dst, - jobject metadata, jobject keylist, - int i) { - jstring _key = NULL; - const char *section = NULL; - jobject _section = NULL; - jobject section_keyset = NULL; - jobject section_keylist = NULL; - if (!bsg_jni_cache->initialized) { goto exit; } - _key = bsg_safe_call_object_method(env, keylist, bsg_jni_cache->ArrayList_get, - (jint)i); - if (_key == NULL) { - goto exit; - } - section = bsg_safe_get_string_utf_chars(env, _key); - if (section == NULL) { - goto exit; - } - _section = - bsg_safe_call_object_method(env, metadata, bsg_jni_cache->Map_get, _key); - if (_section == NULL) { + // get size of metadata map + jint map_size = + bsg_safe_call_int_method(env, section_map, bsg_jni_cache->Map_size); + if (map_size <= 0) { goto exit; } - jint section_size = - bsg_safe_call_int_method(env, _section, bsg_jni_cache->Map_size); - if (section_size == -1) { + + // create a list of metadata keys + entryset = bsg_safe_call_object_method(env, section_map, + bsg_jni_cache->Map_entrySet); + if (entryset == NULL) { goto exit; } - section_keyset = - bsg_safe_call_object_method(env, _section, bsg_jni_cache->Map_keySet); - if (section_keyset == NULL) { + entries = + bsg_safe_call_object_method(env, entryset, bsg_jni_cache->Set_iterator); + if (entries == NULL) { goto exit; } - section_keylist = bsg_safe_new_object( - env, bsg_jni_cache->ArrayList, - bsg_jni_cache->ArrayList_constructor_collection, section_keyset); - if (section_keylist == NULL) { - goto exit; - } - for (int j = 0; j < section_size; j++) { - populate_metadata_obj(env, dst, _section, section_keylist, j); + // we iterate against the size of the Map, avoiding calling Iterator.hasNext() + // on each loop (avoiding the small JNI overhead). Any concurrent modification + // will be caught as an exception and handled by bsg_safe_call_object_method + for (int i = 0; i < map_size; i++) { + (*env)->PushLocalFrame(env, 3); + { + jobject entry = bsg_safe_call_object_method(env, entries, + bsg_jni_cache->Iterator_next); + jstring _key = bsg_safe_call_object_method( + env, entry, bsg_jni_cache->MapEntry_getKey); + jobject _value = bsg_safe_call_object_method( + env, entry, bsg_jni_cache->MapEntry_getValue); + + if (_key != NULL && _value != NULL) { + const char *key = bsg_safe_get_string_utf_chars(env, _key); + if (key != NULL) { + populate_metadata_value(env, dst, section, key, _value); + bsg_safe_release_string_utf_chars(env, _key, key); + } + } + } + (*env)->PopLocalFrame(env, NULL); } - goto exit; exit: - bsg_safe_release_string_utf_chars(env, _key, section); - bsg_safe_delete_local_ref(env, _key); - bsg_safe_delete_local_ref(env, _section); - bsg_safe_delete_local_ref(env, section_keyset); - bsg_safe_delete_local_ref(env, section_keylist); + bsg_safe_delete_local_ref(env, entries); + bsg_safe_delete_local_ref(env, entryset); } // Internal API - void bsg_populate_metadata(JNIEnv *env, bugsnag_metadata *dst, jobject metadata) { jobject _metadata = NULL; - jobject keyset = NULL; - jobject keylist = NULL; + jobject entrySet = NULL; + jobject entryIterator = NULL; if (!bsg_jni_cache->initialized) { goto exit; @@ -489,90 +459,64 @@ void bsg_populate_metadata(JNIEnv *env, bugsnag_metadata *dst, } int size = bsg_safe_call_int_method(env, metadata, bsg_jni_cache->Map_size); - if (size == -1) { + if (size <= 0) { goto exit; } - // create a list of metadata keys - keyset = bsg_safe_call_static_object_method(env, metadata, - bsg_jni_cache->Map_keySet); - if (keyset == NULL) { + // retrieve the Map.Entry set + entrySet = + bsg_safe_call_object_method(env, metadata, bsg_jni_cache->Map_entrySet); + if (entrySet == NULL) { goto exit; } - keylist = bsg_safe_new_object(env, bsg_jni_cache->ArrayList, - bsg_jni_cache->ArrayList_constructor_collection, - keyset); - if (keylist == NULL) { + + // retrieve an Iterator + entryIterator = + bsg_safe_call_object_method(env, entrySet, bsg_jni_cache->Set_iterator); + if (entryIterator == NULL) { goto exit; } + // we iterate against the size of the Map, avoiding calling Iterator.hasNext() + // on each loop (avoiding the small JNI overhead). Any concurrent modification + // will be caught as an exception and handled by bsg_safe_call_object_method for (int i = 0; i < size; i++) { - populate_metadata_section(env, dst, metadata, keylist, i); + (*env)->PushLocalFrame(env, 3); + { + jobject entry = bsg_safe_call_object_method(env, entryIterator, + bsg_jni_cache->Iterator_next); + jobject _key = bsg_safe_call_object_method( + env, entry, bsg_jni_cache->MapEntry_getKey); + jobject _value = bsg_safe_call_object_method( + env, entry, bsg_jni_cache->MapEntry_getValue); + + const char *section_name = bsg_safe_get_string_utf_chars(env, _key); + + if (section_name != NULL && _value != NULL) { + populate_metadata_obj(env, dst, section_name, _value); + bsg_safe_release_string_utf_chars(env, _key, section_name); + } + } + (*env)->PopLocalFrame(env, NULL); } exit: bsg_safe_delete_local_ref(env, _metadata); - bsg_safe_delete_local_ref(env, keyset); - bsg_safe_delete_local_ref(env, keylist); + bsg_safe_delete_local_ref(env, entrySet); + bsg_safe_delete_local_ref(env, entryIterator); } void bsg_populate_crumb_metadata(JNIEnv *env, bugsnag_breadcrumb *crumb, jobject metadata) { - jobject entryset = NULL; - jobject entries = NULL; if (metadata == NULL) { - goto exit; + return; } if (!bsg_jni_cache->initialized) { - goto exit; - } - - // get size of metadata map - jint map_size = - bsg_safe_call_int_method(env, metadata, bsg_jni_cache->Map_size); - if (map_size <= 0) { - goto exit; - } - - // create a list of metadata keys - entryset = - bsg_safe_call_object_method(env, metadata, bsg_jni_cache->Map_entrySet); - if (entryset == NULL) { - goto exit; - } - entries = - bsg_safe_call_object_method(env, entryset, bsg_jni_cache->Set_iterator); - if (entries == NULL) { - goto exit; - } - - while (bsg_safe_call_boolean_method(env, entries, - bsg_jni_cache->Iterator_hasNext)) { - (*env)->PushLocalFrame(env, 3); - { - jobject entry = bsg_safe_call_object_method(env, entries, - bsg_jni_cache->Iterator_next); - jstring _key = bsg_safe_call_object_method( - env, entry, bsg_jni_cache->MapEntry_getKey); - jobject _value = bsg_safe_call_object_method( - env, entry, bsg_jni_cache->MapEntry_getValue); - - if (_key != NULL && _value != NULL) { - const char *key = bsg_safe_get_string_utf_chars(env, _key); - if (key != NULL) { - populate_metadata_value(env, &crumb->metadata, "metaData", key, - _value); - bsg_safe_release_string_utf_chars(env, _key, key); - } - } - } - (*env)->PopLocalFrame(env, NULL); + return; } -exit: - bsg_safe_delete_local_ref(env, entries); - bsg_safe_delete_local_ref(env, entryset); + populate_metadata_obj(env, &crumb->metadata, "metaData", metadata); } void bsg_populate_event(JNIEnv *env, bugsnag_event *event) { diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c index 42289bf4b6..3e64ff9cf4 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c @@ -1,7 +1,5 @@ #include "serializer.h" -#include "serializer/event_reader.h" #include "serializer/event_writer.h" -#include "serializer/json_writer.h" bool bsg_serialize_last_run_info_to_file(bsg_environment *env) { return bsg_lastrun_write(env); @@ -10,11 +8,3 @@ bool bsg_serialize_last_run_info_to_file(bsg_environment *env) { bool bsg_serialize_event_to_file(bsg_environment *env) { return bsg_event_write(env); } - -bugsnag_event *bsg_deserialize_event_from_file(char *filepath) { - return bsg_read_event(filepath); -} - -char *bsg_serialize_event_to_json_string(bugsnag_event *event) { - return bsg_event_to_json(event); -} diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.h index bcdc00d3dd..400614c40a 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.h @@ -16,8 +16,6 @@ bool bsg_serialize_event_to_file(bsg_environment *env) __asyncsafe; */ bool bsg_serialize_last_run_info_to_file(bsg_environment *env) __asyncsafe; -bugsnag_event *bsg_deserialize_event_from_file(char *filepath); - #ifdef __cplusplus } #endif diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSCrashStringConversion.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSCrashStringConversion.c new file mode 100644 index 0000000000..e727ec3407 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSCrashStringConversion.c @@ -0,0 +1,279 @@ +// +// BSG_KSCrashStringConversion.c +// Bugsnag +// +// Created by Karl Stenerud on 31.05.22. +// Copyright © 2022 Bugsnag Inc. All rights reserved. +// + +#include "BSG_KSCrashStringConversion.h" +#include +#include +#include + +// Max uint64 is 18446744073709551615 +#define MAX_UINT64_DIGITS 20 + +size_t bsg_uint64_to_string(uint64_t value, char *dst) { + if (value == 0) { + dst[0] = '0'; + dst[1] = 0; + return 1; + } + + char buff[MAX_UINT64_DIGITS + 1]; + buff[sizeof(buff) - 1] = 0; + size_t index = sizeof(buff) - 2; + for (;;) { + buff[index] = (value % 10) + '0'; + value /= 10; + if (value == 0) { + break; + } + index--; + } + + size_t length = sizeof(buff) - index; + memcpy(dst, buff + index, length); + return length - 1; +} + +size_t bsg_int64_to_string(int64_t value, char *dst) { + if (value < 0) { + dst[0] = '-'; + return bsg_uint64_to_string((uint64_t)-value, dst + 1) + 1; + } + return bsg_uint64_to_string((uint64_t)value, dst); +} + +static char g_hexNybbles[] = {'0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + +size_t bsg_uint64_to_hex(uint64_t value, char *dst, int min_digits) { + if (min_digits < 1) { + min_digits = 1; + } else if (min_digits > 16) { + min_digits = 16; + } + + char buff[MAX_UINT64_DIGITS + 1]; + buff[sizeof(buff) - 1] = 0; + size_t index = sizeof(buff) - 2; + for (int digitCount = 1;; digitCount++) { + buff[index] = g_hexNybbles[(value & 15)]; + value >>= 4; + if (value == 0 && digitCount >= min_digits) { + break; + } + index--; + } + + size_t length = sizeof(buff) - index; + memcpy(dst, buff + index, length); + return length - 1; +} + +/** + * Convert a positive double to a string, allowing up to max_sig_digits. + * To reduce the complexity of this algorithm, values with an exponent + * other than 0 are always printed in exponential form. + * + * Values are rounded half-up. + * + * This function makes use of compiler intrinsic functions which, though not + * officially async-safe, are actually async-safe (no allocations, locks, etc). + * + * This function will write a maximum of 21 characters (including the NUL) to + * dst. + * + * Returns the length of the string written to dst (not including the NUL). + */ +static size_t positive_double_to_string(const double value, char *dst, + int max_sig_digits) { + const char *const orig_dst = dst; + if (max_sig_digits > 16) { + max_sig_digits = 16; + } + + if (value == 0) { + dst[0] = '0'; + dst[1] = 0; + return 1; + } + + // isnan() is basically ((x) != (x)) + if (isnan(value)) { + strlcpy(dst, "nan", 4); + return 3; + } + + // isinf() is a compiler intrinsic. + if (isinf(value)) { + strlcpy(dst, "inf", 4); + return 3; + } + + // log10() is a compiler intrinsic. + int exponent = (int)log10(value); + // Values < 1.0 must subtract 1 from exponent to handle zero wraparound. + if (value < 1.0) { + exponent--; + } + + // pow() is a compiler intrinsic. + double normalized = value / pow(10, exponent); + // Special case for 0.1, 0.01, 0.001, etc giving a normalized value of 10.xyz. + // We use 9.999... because 10.0 converts to a value > 10 in ieee754 binary + // floats. + if (normalized > 9.99999999999999822364316059975) { + exponent++; + normalized = value / pow(10, exponent); + } + + // Put all of the digits we'll use into an integer. + double digits_and_remainder = normalized * pow(10, max_sig_digits - 1); + uint64_t digits = (uint64_t)digits_and_remainder; + // Also round up if necessary (note: 0.5 is exact in both binary and decimal). + if (digits_and_remainder - (double)digits >= 0.5) { + digits++; + // Special case: Adding one bumps us to next magnitude. + if (digits >= (uint64_t)pow(10, max_sig_digits)) { + exponent++; + digits /= 10; + } + } + + // Extract the fractional digits. + for (int i = max_sig_digits; i > 1; i--) { + dst[i] = digits % 10 + '0'; + digits /= 10; + } + // Extract the single-digit whole part. + dst[0] = (char)digits + '0'; + dst[1] = '.'; + + // Strip off trailing zeroes, and also the '.' if there is no fractional part. + int e_offset = max_sig_digits; + for (int i = max_sig_digits; i > 0; i--) { + if (dst[i] != '0') { + if (dst[i] == '.') { + e_offset = i; + } else { + e_offset = i + 1; + } + break; + } + } + dst += e_offset; + + // Add the exponent if it's not 0. + if (exponent != 0) { + *dst++ = 'e'; + if (exponent >= 0) { + *dst++ = '+'; + } + dst += bsg_int64_to_string(exponent, dst); + } else { + *dst = 0; + } + + return (size_t)(dst - orig_dst); +} + +size_t bsg_double_to_string(double value, char *dst, int max_sig_digits) { + if (max_sig_digits < 1) { + max_sig_digits = 1; + } + if (value < 0) { + dst[0] = '-'; + return positive_double_to_string(-value, dst + 1, max_sig_digits) + 1; + } + return positive_double_to_string(value, dst, max_sig_digits); +} + +// async-safe gmtime_r implementation +static void safe_gmtime_r(time_t time, struct tm *out) { + const int seconds_per_day = 86400; + const int days_per_400years = 365 * 400 + 97; + const int days_per_100years = 365 * 100 + 24; + const int days_in_month_leap_year[] = {31, 29, 31, 30, 31, 30, + 31, 31, 30, 31, 30, 31}; + const int days_in_month_non_leap_year[] = {31, 28, 31, 30, 31, 30, + 31, 31, 30, 31, 30, 31}; + const int days_per_4years = 365 * 4 + 1; + + int days = time / seconds_per_day; + int secs = time % seconds_per_day; + int years = 1970; + + int quotient = days / days_per_400years; + days -= quotient * days_per_400years; + years += quotient * 400; + + quotient = days / days_per_100years; + days -= quotient * days_per_100years; + years += quotient * 100; + + quotient = days / days_per_4years; + days -= quotient * days_per_4years; + years += quotient * 4; + + quotient = days / 365; + days -= quotient * 365; + years += quotient; + + out->tm_year = years - 1900; + out->tm_yday = days; + + const int *days_per_month = + out->tm_year % 4 == 0 + ? (out->tm_year % 100 == 0 + ? (out->tm_year % 400 == 0 ? days_in_month_leap_year + : days_in_month_non_leap_year) + : days_in_month_leap_year) + : days_in_month_non_leap_year; + + int month = 0; + while (days >= days_per_month[month]) { + days -= days_per_month[month]; + month++; + } + + out->tm_mon = month; + out->tm_mday = days + 1; + + out->tm_hour = secs / 3600; + secs %= 3600; + out->tm_min = secs / 60; + out->tm_sec = secs % 60; +} + +size_t bsg_time_to_simplified_iso8601_string(time_t time, char *dst) { + struct tm tm; + safe_gmtime_r(time, &tm); + size_t length = 0; + length += bsg_uint64_to_string(tm.tm_year + 1900, dst + length); + dst[length++] = '-'; + if (tm.tm_mon + 1 < 10) + dst[length++] = '0'; + length += bsg_uint64_to_string(tm.tm_mon + 1, dst + length); + dst[length++] = '-'; + if (tm.tm_mday < 10) + dst[length++] = '0'; + length += bsg_uint64_to_string(tm.tm_mday, dst + length); + dst[length++] = 'T'; + if (tm.tm_hour < 10) + dst[length++] = '0'; + length += bsg_uint64_to_string(tm.tm_hour, dst + length); + dst[length++] = ':'; + if (tm.tm_min < 10) + dst[length++] = '0'; + length += bsg_uint64_to_string(tm.tm_min, dst + length); + dst[length++] = ':'; + if (tm.tm_sec < 10) + dst[length++] = '0'; + length += bsg_uint64_to_string(tm.tm_sec, dst + length); + dst[length++] = 'Z'; + dst[length] = '\0'; + return length; +} diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSCrashStringConversion.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSCrashStringConversion.h new file mode 100644 index 0000000000..36db4ce8b6 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSCrashStringConversion.h @@ -0,0 +1,85 @@ +// +// BSG_KSCrashStringConversion.h +// Bugsnag +// +// Created by Karl Stenerud on 31.05.22. +// Copyright © 2022 Bugsnag Inc. All rights reserved. +// + +#ifndef BSG_KSCrashStringConversion_h +#define BSG_KSCrashStringConversion_h + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Convert an unsigned integer to a string. + * This will write a maximum of 21 characters (including the NUL) to dst. + * + * Returns the length of the string written to dst (not including the NUL). + */ +size_t bsg_uint64_to_string(uint64_t value, char *dst); + +/** + * Convert an integer to a string. + * This will write a maximum of 22 characters (including the null terminator) to + * dst. + * + * Returns the length of the string written to dst (not including the null + * termination byte). + */ +size_t bsg_int64_to_string(int64_t value, char *dst); + +/** + * Convert an unsigned integer to a hex string. + * This will write a maximum of 17 characters (including the NUL) to dst. + * + * If min_digits is greater than 1, it will prepad with zeroes to reach this + * number of digits (up to a maximum of 16 digits). + * + * Returns the length of the string written to dst (not including the NUL). + */ +size_t bsg_uint64_to_hex(uint64_t value, char *dst, int min_digits); + +/** + * Convert a positive double to a string, allowing up to max_sig_digits. + * To reduce the complexity of this algorithm, values with an exponent + * other than 0 are always printed in exponential form. + * + * Values are rounded half-up, and thus will differ slightly from printf. + * + * This function makes use of compiler intrinsic functions which, though not + * officially async-safe, are actually async-safe (no allocations, locks, etc). + * + * Note: Double conversion is not intended to be round-trippable. + * It is 99.99% correct but has subtle differences from printf. + * + * This function will write a maximum of 22 characters (including the NUL) to + * dst. + * + * max_sig_digits is capped between 1 and 16 (inclusive) because that's the + * range of significant digits an ieee754 binary float64 can represent. + * + * Returns the length of the string written to dst (not including the NUL). + */ +size_t bsg_double_to_string(double value, char *dst, int max_sig_digits); + +/** + * Convert a time to a simplified ISO8601 date string. No timezone information + * is added, and the output date is always in UTC. + * + * Returns the length of the string written to dst (not including the null + * termination byte). + */ +size_t bsg_time_to_simplified_iso8601_string(time_t time, char *dst); + +#ifdef __cplusplus +} +#endif + +#endif /* BSG_KSCrashStringConversion_h */ \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSJSONCodec.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSJSONCodec.c new file mode 100644 index 0000000000..be03d36e5b --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSJSONCodec.c @@ -0,0 +1,545 @@ +// +// BSG_KSJSONCodec.c +// +// Created by Karl Stenerud on 2012-01-07. +// +// Copyright (c) 2012 Karl Stenerud. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall remain in place +// in this source code. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#include "BSG_KSJSONCodec.h" +#include "BSG_KSCrashStringConversion.h" + +#include +#include +#include +#include + +// ============================================================================ +#pragma mark - Configuration - +// ============================================================================ + +/** Set to 1 if you're also compiling BSG_KSLogger and want to use it here */ +#ifndef BSG_KSJSONCODEC_UseKSLogger +#define BSG_KSJSONCODEC_UseKSLogger 0 +#endif + +#if BSG_KSJSONCODEC_UseKSLogger +#include "BSG_KSLogger.h" +#else +#define BSG_KSLOG_ERROR(FMT, ...) +#endif + +/** The work buffer size to use when escaping string values. + * There's little reason to change this since nothing ever gets truncated. + */ +#ifndef BSG_KSJSONCODEC_WorkBufferSize +#define BSG_KSJSONCODEC_WorkBufferSize 512 +#endif + +/** + * The maximum number of significant digits when printing floats. + * 7 (6 + 1 whole digit in exp form) is the default used by the old sprintf + * code. + */ +#define MAX_SIGNIFICANT_DIGITS 7 + +// ============================================================================ +#pragma mark - Helpers - +// ============================================================================ + +// Compiler hints for "if" statements +#define likely_if(x) if (__builtin_expect(x, 1)) +#define unlikely_if(x) if (__builtin_expect(x, 0)) + +/** Used for writing hex string values. */ +static char bsg_g_hexNybbles[] = {'0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + +const char *bsg_ksjsonstringForError(const int error) { + switch (error) { + case BSG_KSJSON_ERROR_INVALID_CHARACTER: + return "Invalid character"; + case BSG_KSJSON_ERROR_CANNOT_ADD_DATA: + return "Cannot add data"; + case BSG_KSJSON_ERROR_INCOMPLETE: + return "Incomplete data"; + case BSG_KSJSON_ERROR_INVALID_DATA: + return "Invalid data"; + default: + return "(unknown error)"; + } +} + +// ============================================================================ +#pragma mark - Encode - +// ============================================================================ + +// Avoiding static functions due to linker issues. + +/** Add JSON encoded data to an external handler. + * The external handler will decide how to handle the data (store/transmit/etc). + * + * @param context The encoding context. + * + * @param data The encoded data. + * + * @param length The length of the data. + * + * @return true if the data was handled successfully. + */ +#define addJSONData(CONTEXT, DATA, LENGTH) \ + (CONTEXT)->addJSONData(DATA, LENGTH, (CONTEXT)->userData) + +/** Escape a string portion for use with JSON and send to data handler. + * + * @param context The JSON context. + * + * @param string The string to escape and write. + * + * @param length The length of the string. + * + * @return true if the data was handled successfully. + */ +int bsg_ksjsoncodec_i_appendEscapedString( + BSG_KSJSONEncodeContext *const context, const char *restrict const string, + size_t length) { + char workBuffer[BSG_KSJSONCODEC_WorkBufferSize]; + const char *const srcEnd = string + length; + + const char *restrict src = string; + char *restrict dst = workBuffer; + + // Simple case (no escape or special characters) + for (; src < srcEnd && *src != '\\' && *src != '\"' && + (unsigned char)*src >= ' '; + src++) { + *dst++ = *src; + } + + // Deal with complicated case (if any) + int result; + for (; src < srcEnd; src++) { + + // If we add an escaped control character this may exceed the buffer by up + // to 6 characters: add this chunk now, reset the buffer and carry on + if (dst + 6 > workBuffer + BSG_KSJSONCODEC_WorkBufferSize) { + size_t encLength = (size_t)(dst - workBuffer); + unlikely_if((result = addJSONData(context, dst - encLength, encLength)) != + BSG_KSJSON_OK) { + return result; + } + dst = workBuffer; + } + + switch (*src) { + case '\\': + case '\"': + *dst++ = '\\'; + *dst++ = *src; + break; + case '\b': + *dst++ = '\\'; + *dst++ = 'b'; + break; + case '\f': + *dst++ = '\\'; + *dst++ = 'f'; + break; + case '\n': + *dst++ = '\\'; + *dst++ = 'n'; + break; + case '\r': + *dst++ = '\\'; + *dst++ = 'r'; + break; + case '\t': + *dst++ = '\\'; + *dst++ = 't'; + break; + default: + + // escape control chars (U+0000 - U+001F) + // see https://www.ietf.org/rfc/rfc4627.txt + + if ((unsigned char)*src < ' ') { + unsigned int last = (unsigned int)*src % 16; + unsigned int first = ((unsigned int)*src - last) / 16; + + *dst++ = '\\'; + *dst++ = 'u'; + *dst++ = '0'; + *dst++ = '0'; + *dst++ = bsg_g_hexNybbles[first]; + *dst++ = bsg_g_hexNybbles[last]; + } else { + *dst++ = *src; + } + break; + } + } + size_t encLength = (size_t)(dst - workBuffer); + return addJSONData(context, dst - encLength, encLength); +} + +/** Escape a string for use with JSON and send to data handler. + * + * @param context The JSON context. + * + * @param string The string to escape and write. + * + * @param length The length of the string. + * + * @return true if the data was handled successfully. + */ +int bsg_ksjsoncodec_i_addEscapedString(BSG_KSJSONEncodeContext *const context, + const char *restrict const string, + size_t length) { + int result = BSG_KSJSON_OK; + + // Keep adding portions until the whole string has been processed. + size_t offset = 0; + while (offset < length) { + size_t toAdd = length - offset; + unlikely_if(toAdd > BSG_KSJSONCODEC_WorkBufferSize) { + toAdd = BSG_KSJSONCODEC_WorkBufferSize; + } + result = + bsg_ksjsoncodec_i_appendEscapedString(context, string + offset, toAdd); + unlikely_if(result != BSG_KSJSON_OK) { break; } + offset += toAdd; + } + return result; +} + +/** Escape and quote a string for use with JSON and send to data handler. + * + * @param context The JSON context. + * + * @param string The string to escape and write. + * + * @param length The length of the string. + * + * @return true if the data was handled successfully. + */ +int bsg_ksjsoncodec_i_addQuotedEscapedString( + BSG_KSJSONEncodeContext *const context, const char *restrict const string, + size_t length) { + int result; + unlikely_if((result = addJSONData(context, "\"", 1)) != BSG_KSJSON_OK) { + return result; + } + unlikely_if((result = bsg_ksjsoncodec_i_addEscapedString( + context, string, length)) != BSG_KSJSON_OK) { + return result; + } + return addJSONData(context, "\"", 1); +} + +int bsg_ksjsonbeginElement(BSG_KSJSONEncodeContext *const context, + const char *const name) { + int result = BSG_KSJSON_OK; + + // Decide if a comma is warranted. + unlikely_if(context->containerFirstEntry) { + context->containerFirstEntry = false; + } + else { + unlikely_if((result = addJSONData(context, ",", 1)) != BSG_KSJSON_OK) { + return result; + } + } + + // Pretty printing + unlikely_if(context->prettyPrint && context->containerLevel > 0) { + unlikely_if((result = addJSONData(context, "\n", 1)) != BSG_KSJSON_OK) { + return result; + } + for (int i = 0; i < context->containerLevel; i++) { + unlikely_if((result = addJSONData(context, " ", 4)) != BSG_KSJSON_OK) { + return result; + } + } + } + + // Add a name field if we're in an object. + if (context->isObject[context->containerLevel]) { + unlikely_if(name == NULL) { + BSG_KSLOG_ERROR("Name was null inside an object"); + return BSG_KSJSON_ERROR_INVALID_DATA; + } + unlikely_if((result = bsg_ksjsoncodec_i_addQuotedEscapedString( + context, name, strlen(name))) != BSG_KSJSON_OK) { + return result; + } + unlikely_if(context->prettyPrint) { + unlikely_if((result = addJSONData(context, ": ", 2)) != BSG_KSJSON_OK) { + return result; + } + } + else { + unlikely_if((result = addJSONData(context, ":", 1)) != BSG_KSJSON_OK) { + return result; + } + } + } + return result; +} + +int bsg_ksjsonaddRawJSONData(BSG_KSJSONEncodeContext *const context, + const char *const data, const size_t length) { + return addJSONData(context, data, length); +} + +int bsg_ksjsonaddBooleanElement(BSG_KSJSONEncodeContext *const context, + const char *const name, const bool value) { + int result = bsg_ksjsonbeginElement(context, name); + unlikely_if(result != BSG_KSJSON_OK) { return result; } + if (value) { + return addJSONData(context, "true", 4); + } else { + return addJSONData(context, "false", 5); + } +} + +int bsg_ksjsonaddFloatingPointElement(BSG_KSJSONEncodeContext *const context, + const char *const name, double value) { + int result = bsg_ksjsonbeginElement(context, name); + unlikely_if(result != BSG_KSJSON_OK) { return result; } + unlikely_if(isnan(value) || isinf(value)) { + return addJSONData(context, "null", 4); + } + char buff[30]; + bsg_double_to_string(value, buff, MAX_SIGNIFICANT_DIGITS); + return addJSONData(context, buff, strlen(buff)); +} + +int bsg_ksjsonaddIntegerElement(BSG_KSJSONEncodeContext *const context, + const char *const name, long long value) { + int result = bsg_ksjsonbeginElement(context, name); + unlikely_if(result != BSG_KSJSON_OK) { return result; } + char buff[30]; + bsg_int64_to_string(value, buff); + return addJSONData(context, buff, strlen(buff)); +} + +int bsg_ksjsonaddUIntegerElement(BSG_KSJSONEncodeContext *const context, + const char *const name, + unsigned long long value) { + int result = bsg_ksjsonbeginElement(context, name); + unlikely_if(result != BSG_KSJSON_OK) { return result; } + char buff[30]; + bsg_uint64_to_string(value, buff); + return addJSONData(context, buff, strlen(buff)); +} + +int bsg_ksjsonaddJSONElement(BSG_KSJSONEncodeContext *const context, + const char *restrict const name, + const char *restrict const element, + size_t length) { + unlikely_if(element == NULL) { + return bsg_ksjsonaddNullElement(context, name); + } + size_t idx = 0; + while (idx < length && + (element[idx] == ' ' || element[idx] == '\r' || element[idx] == '\n' || + element[idx] == '\t' || element[idx] == '\f')) { + idx++; + } + unlikely_if(idx >= length) { + BSG_KSLOG_ERROR("JSON element contained no JSON data: %s", element); + return BSG_KSJSON_ERROR_INVALID_DATA; + } + switch (element[idx]) { + case '[': + case '{': + case '\"': + case 'f': + case 't': + case 'n': + case '-': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + break; + default: + BSG_KSLOG_ERROR("Invalid character '%c' in: ", element[idx]); + return BSG_KSJSON_ERROR_INVALID_DATA; + } + + int result = bsg_ksjsonbeginElement(context, name); + unlikely_if(result != BSG_KSJSON_OK) { return result; } + return addJSONData(context, element, length); +} + +int bsg_ksjsonaddNullElement(BSG_KSJSONEncodeContext *const context, + const char *const name) { + int result = bsg_ksjsonbeginElement(context, name); + unlikely_if(result != BSG_KSJSON_OK) { return result; } + return addJSONData(context, "null", 4); +} + +int bsg_ksjsonaddStringElement(BSG_KSJSONEncodeContext *const context, + const char *const name, const char *const value, + size_t length) { + unlikely_if(value == NULL) { return bsg_ksjsonaddNullElement(context, name); } + int result = bsg_ksjsonbeginElement(context, name); + unlikely_if(result != BSG_KSJSON_OK) { return result; } + if (length == BSG_KSJSON_SIZE_AUTOMATIC) { + length = strlen(value); + } + return bsg_ksjsoncodec_i_addQuotedEscapedString(context, value, length); +} + +int bsg_ksjsonbeginStringElement(BSG_KSJSONEncodeContext *const context, + const char *const name) { + int result = bsg_ksjsonbeginElement(context, name); + unlikely_if(result != BSG_KSJSON_OK) { return result; } + return addJSONData(context, "\"", 1); +} + +int bsg_ksjsonappendStringElement(BSG_KSJSONEncodeContext *const context, + const char *const value, size_t length) { + return bsg_ksjsoncodec_i_addEscapedString(context, value, length); +} + +int bsg_ksjsonendStringElement(BSG_KSJSONEncodeContext *const context) { + return addJSONData(context, "\"", 1); +} + +int bsg_ksjsonaddDataElement(BSG_KSJSONEncodeContext *const context, + const char *name, const char *value, + size_t length) { + int result = BSG_KSJSON_OK; + result = bsg_ksjsonbeginDataElement(context, name); + if (result == BSG_KSJSON_OK) { + result = bsg_ksjsonappendDataElement(context, value, length); + } + if (result == BSG_KSJSON_OK) { + result = bsg_ksjsonendDataElement(context); + } + return result; +} + +int bsg_ksjsonbeginDataElement(BSG_KSJSONEncodeContext *const context, + const char *const name) { + return bsg_ksjsonbeginStringElement(context, name); +} + +int bsg_ksjsonappendDataElement(BSG_KSJSONEncodeContext *const context, + const char *const value, size_t length) { + const unsigned char *currentByte = (const unsigned char *)value; + const unsigned char *end = currentByte + length; + char chars[2]; + int result = BSG_KSJSON_OK; + while (currentByte < end) { + chars[0] = bsg_g_hexNybbles[(*currentByte >> 4) & 15]; + chars[1] = bsg_g_hexNybbles[*currentByte & 15]; + result = addJSONData(context, chars, sizeof(chars)); + if (result != BSG_KSJSON_OK) { + break; + } + currentByte++; + } + return result; +} + +int bsg_ksjsonendDataElement(BSG_KSJSONEncodeContext *const context) { + return bsg_ksjsonendStringElement(context); +} + +int bsg_ksjsonbeginArray(BSG_KSJSONEncodeContext *const context, + const char *const name) { + likely_if(context->containerLevel >= 0) { + int result = bsg_ksjsonbeginElement(context, name); + unlikely_if(result != BSG_KSJSON_OK) { return result; } + } + + context->containerLevel++; + context->isObject[context->containerLevel] = false; + context->containerFirstEntry = true; + + return addJSONData(context, "[", 1); +} + +int bsg_ksjsonbeginObject(BSG_KSJSONEncodeContext *const context, + const char *const name) { + likely_if(context->containerLevel >= 0) { + int result = bsg_ksjsonbeginElement(context, name); + unlikely_if(result != BSG_KSJSON_OK) { return result; } + } + + context->containerLevel++; + context->isObject[context->containerLevel] = true; + context->containerFirstEntry = true; + + return addJSONData(context, "{", 1); +} + +int bsg_ksjsonendContainer(BSG_KSJSONEncodeContext *const context) { + unlikely_if(context->containerLevel <= 0) { return BSG_KSJSON_OK; } + + bool isObject = context->isObject[context->containerLevel]; + context->containerLevel--; + + // Pretty printing + unlikely_if(context->prettyPrint && !context->containerFirstEntry) { + int result; + unlikely_if((result = addJSONData(context, "\n", 1)) != BSG_KSJSON_OK) { + return result; + } + for (int i = 0; i < context->containerLevel; i++) { + unlikely_if((result = addJSONData(context, " ", 4)) != BSG_KSJSON_OK) { + return result; + } + } + } + context->containerFirstEntry = false; + return addJSONData(context, isObject ? "}" : "]", 1); +} + +void bsg_ksjsonbeginEncode(BSG_KSJSONEncodeContext *const context, + bool prettyPrint, + BSG_KSJSONAddDataFunc addJSONDataFunc, + void *const userData) { + memset(context, 0, sizeof(*context)); + context->addJSONData = addJSONDataFunc; + context->userData = userData; + context->prettyPrint = prettyPrint; + context->containerFirstEntry = true; +} + +int bsg_ksjsonendEncode(BSG_KSJSONEncodeContext *const context) { + int result = BSG_KSJSON_OK; + while (context->containerLevel > 0) { + unlikely_if((result = bsg_ksjsonendContainer(context)) != BSG_KSJSON_OK) { + return result; + } + } + return result; +} \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSJSONCodec.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSJSONCodec.h new file mode 100644 index 0000000000..a0a9a635c0 --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/BSG_KSJSONCodec.h @@ -0,0 +1,372 @@ +// +// BSG_KSJSONCodec.h +// +// Created by Karl Stenerud on 2012-01-07. +// +// Copyright (c) 2012 Karl Stenerud. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall remain in place +// in this source code. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +/* Reads and writes JSON encoded data. + */ + +#ifndef HDR_BSG_KSJSONCodec_h +#define HDR_BSG_KSJSONCodec_h + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Tells the encoder to automatically determine the length of a field value. + * Currently, this is done using strlen(). + */ +#define BSG_KSJSON_SIZE_AUTOMATIC ((size_t)~0) + +enum { + /** Encoding or decoding: Everything completed without error */ + BSG_KSJSON_OK = 0, + + /** Encoding or decoding: Encountered an unexpected or invalid character */ + BSG_KSJSON_ERROR_INVALID_CHARACTER = 1, + + /** Encoding: addJSONData could not handle the data. + * This code is not used by the decoder, but is meant to be returned by + * the addJSONData callback method if it couldn't handle the data. + */ + BSG_KSJSON_ERROR_CANNOT_ADD_DATA = 2, + + /** Decoding: Source data appears to be truncated. */ + BSG_KSJSON_ERROR_INCOMPLETE = 3, + + /** Decoding: Parsing failed due to bad data structure/type/contents. + * This code is not used by the decoder, but is meant to be returned + * by the user callback methods if the decoded data is incorrect for + * semantic or structural reasons. + */ + BSG_KSJSON_ERROR_INVALID_DATA = 4, +}; + +/** Get a description for an error code. + * + * @param error The error code. + * + * @return A string describing the error. + */ +const char *bsg_ksjsonstringForError(const int error); + +// ============================================================================ +// Encode +// ============================================================================ + +/** Function pointer for adding more UTF-8 encoded JSON data. + * + * @param data The UTF-8 data to add. + * + * @param length The length of the data. + * + * @param userData user-specified contextual data. + * + * @return BSG_KSJSON_OK if the data was handled. + * otherwise BSG_KSJSON_ERROR_CANNOT_ADD_DATA. + */ +typedef int (*BSG_KSJSONAddDataFunc)(const char *data, size_t length, + void *userData); + +typedef struct { + /** Function to call to add more encoded JSON data. */ + BSG_KSJSONAddDataFunc addJSONData; + + /** User-specified data */ + void *userData; + + /** How many containers deep we are. */ + int containerLevel; + + /** Whether or not the current container is an object. */ + bool isObject[200]; + + /** true if this is the first entry at the current container level. */ + bool containerFirstEntry; + + bool prettyPrint; + +} BSG_KSJSONEncodeContext; + +/** Begin a new encoding process. + * + * @param context The encoding context. + * + * @param prettyPrint If true, insert whitespace to make the output pretty. + * + * @param addJSONData Function to handle adding data. + * + * @param userData User-specified data which gets passed to addJSONData. + */ +void bsg_ksjsonbeginEncode(BSG_KSJSONEncodeContext *context, bool prettyPrint, + BSG_KSJSONAddDataFunc addJSONData, void *userData); + +/** End the encoding process, ending any remaining open containers. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonendEncode(BSG_KSJSONEncodeContext *context); + +/** Add a boolean element. + * + * @param context The encoding context. + * + * @param name The element's name. + * + * @param value The element's value. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonaddBooleanElement(BSG_KSJSONEncodeContext *context, + const char *name, bool value); + +/** Add an integer element. + * + * @param context The encoding context. + * + * @param name The element's name. + * + * @param value The element's value. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonaddIntegerElement(BSG_KSJSONEncodeContext *context, + const char *name, long long value); + +/** Add an unsigned integer element. + * + * @param context The encoding context. + * + * @param name The element's name. + * + * @param value The element's value. + * + * @return KSJSON_OK if the process was successful. + */ +int bsg_ksjsonaddUIntegerElement(BSG_KSJSONEncodeContext *context, + const char *name, unsigned long long value); + +/** Add a floating point element. + * + * @param context The encoding context. + * + * @param name The element's name. + * + * @param value The element's value. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonaddFloatingPointElement(BSG_KSJSONEncodeContext *context, + const char *name, double value); + +/** Add a null element. + * + * @param context The encoding context. + * + * @param name The element's name. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonaddNullElement(BSG_KSJSONEncodeContext *context, + const char *name); + +/** Add a string element. + * + * @param context The encoding context. + * + * @param name The element's name. + * + * @param value The element's value. + * + * @param length the length of the string, or BSG_KSJSON_SIZE_AUTOMATIC. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonaddStringElement(BSG_KSJSONEncodeContext *context, + const char *name, const char *value, + size_t length); + +/** Start an incrementally-built string element. + * + * Use this for constructing very large strings. + * + * @param context The encoding context. + * + * @param name The element's name. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonbeginStringElement(BSG_KSJSONEncodeContext *context, + const char *name); + +/** Add a string fragment to an incrementally-built string element. + * + * @param context The encoding context. + * + * @param value The string fragment. + * + * @param length the length of the string fragment. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonappendStringElement(BSG_KSJSONEncodeContext *context, + const char *value, size_t length); + +/** End an incrementally-built string element. + * + * @param context The encoding context. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonendStringElement(BSG_KSJSONEncodeContext *context); + +/** Add a string element. The element will be converted to string-coded hex. + * + * @param context The encoding context. + * + * @param name The element's name. + * + * @param value The element's value. + * + * @param length the length of the data. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonaddDataElement(BSG_KSJSONEncodeContext *const context, + const char *name, const char *value, + size_t length); + +/** Start an incrementally-built data element. The element will be converted + * to string-coded hex. + * + * Use this for constructing very large data elements. + * + * @param context The encoding context. + * + * @param name The element's name. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonbeginDataElement(BSG_KSJSONEncodeContext *const context, + const char *const name); + +/** Add a data fragment to an incrementally-built data element. + * + * @param context The encoding context. + * + * @param value The data fragment. + * + * @param length the length of the data fragment. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonappendDataElement(BSG_KSJSONEncodeContext *const context, + const char *const value, size_t length); + +/** End an incrementally-built data element. + * + * @param context The encoding context. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonendDataElement(BSG_KSJSONEncodeContext *const context); + +/** Add a pre-formatted JSON element. + * + * @param context The encoding context. + * + * @param name The element's name. + * + * @param value The element's value. MUST BE VALID JSON! + * + * @param length The length of the element. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonaddJSONElement(BSG_KSJSONEncodeContext *context, + const char *restrict name, + const char *restrict value, size_t length); + +/** Begin a new object container. + * + * @param context The encoding context. + * + * @param name The object's name. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonbeginObject(BSG_KSJSONEncodeContext *context, const char *name); + +/** Begin a new array container. + * + * @param context The encoding context. + * + * @param name The array's name. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonbeginArray(BSG_KSJSONEncodeContext *context, const char *name); + +/** Begin a generic JSON element, adding any necessary JSON preamble text, + * including commas and names. + * Note: This does not add any object or array specifiers ('{', '['). + * + * @param context The JSON context. + * + * @param name The name of the next element (only needed if parent is a + * dictionary). + */ +int bsg_ksjsonbeginElement(BSG_KSJSONEncodeContext *const context, + const char *const name); + +/** Add JSON data manually. + * This function just passes your data directly through, even if it's malforned. + * + * @param context The encoding context. + * + * @param data The data to write. + * + * @param length The length of the data. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonaddRawJSONData(BSG_KSJSONEncodeContext *const context, + const char *const data, const size_t length); + +/** End the current container and return to the next higher level. + * + * @param context The encoding context. + * + * @return BSG_KSJSON_OK if the process was successful. + */ +int bsg_ksjsonendContainer(BSG_KSJSONEncodeContext *context); + +#ifdef __cplusplus +} +#endif + +#endif // HDR_KSJSONCodec_h \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.c deleted file mode 100644 index 021de3e18e..0000000000 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.c +++ /dev/null @@ -1,298 +0,0 @@ -#include "event_reader.h" - -#include "../../event.h" -#include "../string.h" -#include "utils/logger.h" - -#include -#include -#include -#include -#include - -const int BSG_MIGRATOR_CURRENT_VERSION = 14; - -void bsg_read_feature_flags(int fd, bool expect_verification, - bsg_feature_flag **out_feature_flags, - size_t *out_feature_flag_count); - -void bsg_read_opaque_metadata(int fd, bugsnag_metadata *metadata); - -void bsg_read_opaque_breadcrumb_metadata(int fd, - bugsnag_breadcrumb *breadcrumbs, - int crumb_count); - -bool bsg_read_breadcrumbs(int fd, bugsnag_event *event); - -bsg_report_header *bsg_report_header_read(int fd) { - bsg_report_header *header = calloc(1, sizeof(bsg_report_header)); - ssize_t len = read(fd, header, sizeof(bsg_report_header)); - if (len != sizeof(bsg_report_header)) { - free(header); - return NULL; - } - - return header; -} - -bugsnag_event *bsg_read_event(char *filepath) { - int fd = open(filepath, O_RDONLY); - if (fd == -1) { - return NULL; - } - - bsg_report_header *header = bsg_report_header_read(fd); - if (header == NULL) { - return NULL; - } - - int version = header->version; - free(header); - - if (version != BSG_MIGRATOR_CURRENT_VERSION) { - return NULL; - } - - size_t event_size = sizeof(bugsnag_event); - bugsnag_event *event = calloc(1, event_size); - - ssize_t len = read(fd, event, event_size); - if (len != event_size) { - goto error; - } - - // read the breadcrumbs - if (!bsg_read_breadcrumbs(fd, event)) { - goto error; - } - // read the feature flags, if possible - bsg_read_feature_flags(fd, true, &event->feature_flags, - &event->feature_flag_count); - bsg_read_opaque_metadata(fd, &event->metadata); - bsg_read_opaque_breadcrumb_metadata(fd, event->breadcrumbs, - event->crumb_count); - - return event; -error: - free(event); - return NULL; -} - -bool bsg_read_breadcrumbs(int fd, bugsnag_event *event) { - bugsnag_breadcrumb *breadcrumbs = - calloc(event->max_crumb_count, sizeof(bugsnag_breadcrumb)); - if (breadcrumbs == NULL) { - goto error; - } - const size_t bytes_to_read = - event->max_crumb_count * sizeof(bugsnag_breadcrumb); - const ssize_t read_count = read(fd, breadcrumbs, bytes_to_read); - if (read_count != bytes_to_read) { - goto error; - } - - event->breadcrumbs = breadcrumbs; - return true; -error: - event->breadcrumbs = NULL; - event->crumb_count = 0; - event->crumb_first_index = 0; - return false; -} - -static char *read_string(int fd) { - ssize_t len; - uint32_t string_length; - len = read(fd, &string_length, sizeof(string_length)); - - if (len != sizeof(string_length)) { - return NULL; - } - - // allocate enough space, zero filler, and with a trailing '\0' terminator - char *string_buffer = calloc(1, (string_length + 1)); - if (!string_buffer) { - return NULL; - } - - len = read(fd, string_buffer, string_length); - if (len != string_length) { - free(string_buffer); - return NULL; - } - - return string_buffer; -} - -int read_byte(int fd) { - char value; - if (read(fd, &value, 1) != 1) { - return -1; - } - - return value; -} - -void bsg_read_feature_flags(int fd, bool expect_verification, - bsg_feature_flag **out_feature_flags, - size_t *out_feature_flag_count) { - - ssize_t len; - uint32_t feature_flag_count = 0; - len = read(fd, &feature_flag_count, sizeof(feature_flag_count)); - if (len != sizeof(feature_flag_count)) { - goto feature_flags_error; - } - - bsg_feature_flag *flags = - calloc(feature_flag_count, sizeof(bsg_feature_flag)); - for (uint32_t index = 0; index < feature_flag_count; index++) { - char *name = read_string(fd); - if (!name) { - goto feature_flags_error; - } - - int variant_exists = read_byte(fd); - if (variant_exists < 0) { - goto feature_flags_error; - } - - char *variant = NULL; - if (variant_exists) { - variant = read_string(fd); - if (!variant) { - goto feature_flags_error; - } - } - - flags[index].name = name; - flags[index].variant = variant; - } - - if (expect_verification) { - const uint8_t feature_flags_valid = read_byte(fd); - if (feature_flags_valid != 0) { - goto feature_flags_error; - } - } - - *out_feature_flag_count = feature_flag_count; - *out_feature_flags = flags; - - return; - -feature_flags_error: - // something wrong - we release all allocated memory - for (uint32_t index = 0; index < feature_flag_count; index++) { - if (flags[index].name) { - free(flags[index].name); - } - - if (flags[index].variant) { - free(flags[index].variant); - } - } - - free(flags); - - // clear the out fields to indicate no feature-flags are available - *out_feature_flag_count = 0; - *out_feature_flags = NULL; -} - -void bsg_read_opaque_metadata(int fd, bugsnag_metadata *metadata) { - size_t read_index = 0; - for (; read_index < metadata->value_count; read_index++) { - if (metadata->values[read_index].type == BSG_METADATA_OPAQUE_VALUE && - metadata->values[read_index].opaque_value_size > 0) { - - size_t opaque_value_size = metadata->values[read_index].opaque_value_size; - - void *opaque_value = calloc(1, opaque_value_size); - if (opaque_value == NULL) { - goto opaque_metadata_fail; - } - - if (read(fd, opaque_value, opaque_value_size) != opaque_value_size) { - free(opaque_value); - goto opaque_metadata_fail; - } - - metadata->values[read_index].opaque_value_size = opaque_value_size; - metadata->values[read_index].opaque_value = opaque_value; - } - } - - return; - -opaque_metadata_fail: - // ensure that only the OPAQUE values we read successfully are considered - // "valid" this allows for a partial recovery of the OPAQUE data - for (; read_index < metadata->value_count; read_index++) { - if (metadata->values[read_index].type == BSG_METADATA_OPAQUE_VALUE) { - // set all unread OPAQUE values to NONE as their opaque_values are invalid - metadata->values[read_index].type = BSG_METADATA_NONE_VALUE; - metadata->values[read_index].opaque_value_size = 0; - metadata->values[read_index].opaque_value = NULL; - } - } -} - -void bsg_read_opaque_breadcrumb_metadata(int fd, - bugsnag_breadcrumb *breadcrumbs, - int crumb_count) { - - for (int breadcrumb_index = 0; breadcrumb_index < crumb_count; - breadcrumb_index++) { - - bsg_read_opaque_metadata(fd, &(breadcrumbs[breadcrumb_index].metadata)); - } -} - -static bool read_from_file(int fd, ssize_t length, char *buffer) { - ssize_t bytes_read = 0; - ssize_t total_bytes_read = 0; - while (total_bytes_read < length) { - ssize_t bytes_to_read = length - total_bytes_read; - if ((bytes_read = read(fd, buffer + total_bytes_read, bytes_to_read)) < 0) { - return false; - } - total_bytes_read += bytes_read; - } - return true; -} - -ssize_t bsg_read_text_file(const char *filename, char **buffer_pointer) { - char *data = NULL; - ssize_t length = 0; - struct stat stats; - int fd = open(filename, O_RDONLY); - if (fd < 0) { - goto fail; - } - if (fstat(fd, &stats) < 0) { - goto fail; - } - length = (ssize_t)stats.st_size; - data = malloc(length + 1); - if (data == NULL) { - goto fail; - } - if (!read_from_file(fd, length, data)) { - goto fail; - } - data[length] = 0; - *buffer_pointer = data; - goto success; - -fail: - length = -1; -success: - if (fd > 0) { - close(fd); - } - if (length < 0) { - free(data); - } - return length; -} diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.h deleted file mode 100644 index a003946053..0000000000 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_reader.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include - -extern const int BSG_MIGRATOR_CURRENT_VERSION; - -/** - * Read an event from a file path, converting from older formats if needed. - * - * The report version is serialized in the file header, and old formats are - * maintained in migrate.h for backwards compatibility. These are then migrated - * to the current bugsnag_event struct. - * - * @param filepath A full path to a file - * - * @return An allocated event or NULL if no event could be read - */ -bugsnag_event *bsg_read_event(char *filepath); - -/** - * Read a text file from disk. Caller is responsible for freeing the buffer. - * - * @param filename The file to load - * @param buffer_pointer Pointer to the pointer to allocate a buffer. - * @return The length of the file, or -1 if the file could not be loaded. - */ -ssize_t bsg_read_text_file(const char *filename, char **buffer_pointer); diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c index df787e479d..451982d800 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c @@ -2,156 +2,732 @@ #include #include +#include #include -#include "../string.h" +#include "BSG_KSCrashStringConversion.h" +#include "BSG_KSJSONCodec.h" #include "buffered_writer.h" -#include "utils/seqlock.h" +#include "event.h" +#include "internal_metrics.h" +#include "utils/string.h" -bool bsg_write_breadcrumbs(bugsnag_event *event, bsg_buffered_writer *writer); -bool bsg_write_feature_flags(bugsnag_event *event, bsg_buffered_writer *writer); +#define CHECKED(e) \ + if ((e) != BSG_KSJSON_OK) { \ + goto error; \ + } + +#define JSON_CONSTANT_ELEMENT(name, value) \ + bsg_ksjsonaddStringElement(json, name, value, \ + sizeof(value) - 1 /* remove terminator */) +#define JSON_LIMITED_STRING_ELEMENT(name, value) \ + bsg_ksjsonaddStringElement(json, (name), (value), \ + strnlen((value), sizeof((value)))) + +#define STR_CONST_CAT(dst, src) bsg_strncpy((dst), (src), sizeof(src)) + +static bool bsg_write_metadata(BSG_KSJSONEncodeContext *json, + bugsnag_metadata *metadata); +static bool bsg_write_severity_reason(BSG_KSJSONEncodeContext *json, + bugsnag_event *event); +static bool bsg_write_user(BSG_KSJSONEncodeContext *json, bugsnag_user *user); +static bool bsg_write_error(BSG_KSJSONEncodeContext *json, bsg_error *error); +static bool bsg_write_app(BSG_KSJSONEncodeContext *json, bsg_app_info *app); +static bool bsg_write_device(BSG_KSJSONEncodeContext *json, + bsg_device_info *device); +static bool bsg_write_breadcrumbs(BSG_KSJSONEncodeContext *json, + bugsnag_breadcrumb *breadcrumbs, + int first_crumb_index, int crumb_count, + int max_crumb_count); +static bool bsg_write_threads(BSG_KSJSONEncodeContext *json, + bsg_thread *threads, int thread_count); +static bool bsg_write_feature_flags(BSG_KSJSONEncodeContext *json, + bsg_feature_flag *flags, int flag_count); +static bool bsg_write_session(BSG_KSJSONEncodeContext *json, + bugsnag_event *event); +static bool bsg_write_usage(BSG_KSJSONEncodeContext *json, + bsg_environment *env); + +static inline bool string_is_not_empty(const char *restrict s) { + return (*(s) != 0); +} + +static inline bool string_is_empty(const char *restrict s) { + return (*(s) == 0); +} + +static int bsg_write(const char *data, size_t length, void *userData) { + bsg_buffered_writer *writer = userData; + return writer->write(writer, data, length) ? BSG_KSJSON_OK + : BSG_KSJSON_ERROR_CANNOT_ADD_DATA; +} + +/* + * Build the event filename in the same format as defined in EventFilenameInfo: + * "[timestamp]_[apiKey]_[errorTypes]_[UUID]_[startupcrash|not-jvm].json" + */ +static size_t build_filename(bsg_environment *env, char *out) { + time_t now; + time(&now); -bool bsg_write_opaque_metadata(bugsnag_event *event, - bsg_buffered_writer *writer); + int length = strnlen(env->event_path, 4096); + memcpy(out, env->event_path, length); + out[length++] = '/'; -bool bsg_write_breadcrumbs(bugsnag_event *event, bsg_buffered_writer *writer); -bool bsg_report_header_write(bsg_report_header *header, int fd) { - ssize_t len = write(fd, header, sizeof(bsg_report_header)); + // the timestamp is encoded as unix time + length += bsg_uint64_to_string(now, &out[length]); - return len == sizeof(bsg_report_header); + // append the api_key to the filename + out[length++] = '_'; + length += bsg_strncpy(out + length, env->next_event.api_key, + sizeof(env->next_event.api_key)); + + // append the errorType (c) + out[length++] = '_'; + out[length++] = 'c'; + out[length++] = '_'; + + // use the pre-generated UUID in `env` - avoiding needing any source of + // randomness on the signal-handler path + length += bsg_strncpy(out + length, env->event_uuid, sizeof(env->event_uuid)); + + if (env->next_event.app.is_launching) { + length += STR_CONST_CAT(out + length, "_startupcrash"); + } else { + length += STR_CONST_CAT(out + length, "_not-jvm"); + } + + length += STR_CONST_CAT(out + length, ".json"); + + return length; } bool bsg_event_write(bsg_environment *env) { + char filename[sizeof(env->event_path) + 256]; + filename[0] = '\0'; + build_filename(env, filename); + + return bsg_write_event_file(env, filename); +} + +bool bsg_write_event_file(bsg_environment *env, const char *filename) { + BSG_KSJSONEncodeContext jsonContext; bsg_buffered_writer writer; - if (!bsg_buffered_writer_open(&writer, env->next_event_path)) { + bugsnag_event *event = &env->next_event; + BSG_KSJSONEncodeContext *json = &jsonContext; + + if (!bsg_buffered_writer_open(&writer, filename)) { return false; } - bool result = - // write header - determines format version, etc - bsg_report_header_write(&env->report_header, writer.fd) && - // add cached event info - writer.write(&writer, &env->next_event, sizeof(bugsnag_event)) && - // append the breadcrumbs after the event structure - bsg_write_breadcrumbs(&env->next_event, &writer) && - // append feature flags - bsg_write_feature_flags(&env->next_event, &writer) && - // append opaque metadata after the feature flags - bsg_write_opaque_metadata(&env->next_event, &writer); + bsg_ksjsonbeginEncode(json, false, &bsg_write, &writer); + CHECKED(bsg_ksjsonbeginObject(json, NULL)); + { + CHECKED(bsg_ksjsonaddStringElement( + json, "context", event->context, + strnlen(event->context, sizeof(event->context)))); + + CHECKED(bsg_ksjsonbeginObject(json, "metaData")); + { + if (!bsg_write_metadata(json, &event->metadata)) { + goto error; + } + } + CHECKED(bsg_ksjsonendContainer(json)); + + if (!bsg_write_severity_reason(json, event)) { + goto error; + } + + // Write the exception / error info + if (!bsg_write_error(json, &event->error)) { + goto error; + } + + // Write user info + if (!bsg_write_user(json, &event->user)) { + goto error; + } + + // Write diagnostics + if (!bsg_write_app(json, &event->app)) { + goto error; + } + if (!bsg_write_device(json, &event->device)) { + goto error; + } + if (!bsg_write_breadcrumbs(json, event->breadcrumbs, + event->crumb_first_index, event->crumb_count, + event->max_crumb_count)) { + goto error; + } + if (string_is_not_empty(event->grouping_hash)) { + CHECKED( + JSON_LIMITED_STRING_ELEMENT("groupingHash", event->grouping_hash)); + } + if (!bsg_write_usage(json, env)) { + goto error; + } + if (!bsg_write_threads(json, event->threads, event->thread_count)) { + goto error; + } + if (!bsg_write_feature_flags(json, event->feature_flags, + event->feature_flag_count)) { + goto error; + } + if (!bsg_write_session(json, event)) { + goto error; + } + } + CHECKED(bsg_ksjsonendContainer(json)); + + bsg_ksjsonendEncode(json); + writer.dispose(&writer); + return true; +error: writer.dispose(&writer); + return false; +} - const char *static_json_data = atomic_load(&env->static_json_data); - if (result && static_json_data != NULL) { - // Attempt to write the static data, but don't worry if it fails. - // We'll check for truncated/missing static data on load. - if (bsg_buffered_writer_open(&writer, env->next_event_static_data_path)) { - writer.write(&writer, static_json_data, strlen(static_json_data)); - writer.dispose(&writer); +static bool bsg_write_metadata_value(BSG_KSJSONEncodeContext *json, + bsg_metadata_value *value) { + switch (value->type) { + case BSG_METADATA_BOOL_VALUE: + CHECKED(bsg_ksjsonaddBooleanElement(json, value->name, value->bool_value)); + break; + case BSG_METADATA_CHAR_VALUE: + CHECKED(JSON_LIMITED_STRING_ELEMENT(value->name, value->char_value)); + break; + case BSG_METADATA_NUMBER_VALUE: + if (value->double_value == (double)((long long)value->double_value)) { + CHECKED(bsg_ksjsonaddIntegerElement(json, value->name, + (long long)value->double_value)); + } else { + CHECKED(bsg_ksjsonaddFloatingPointElement(json, value->name, + value->double_value)); } + break; + case BSG_METADATA_OPAQUE_VALUE: + CHECKED(bsg_ksjsonbeginElement(json, value->name)); + CHECKED(bsg_ksjsonaddRawJSONData(json, value->opaque_value, + value->opaque_value_size)); + break; + default: + break; } - return result; + return true; +error: + return false; } -bool bsg_lastrun_write(bsg_environment *env) { - char *path = env->last_run_info_path; - int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); - if (fd == -1) { - return false; +static bool bsg_write_metadata(BSG_KSJSONEncodeContext *json, + bugsnag_metadata *metadata) { + const int value_count = metadata->value_count; + // The metadata values can appear in any order and each entry records which + // "section" it should appear in. We need to group the values by section and + // write them out in that order. This array keeps track of which values have + // already been written, and each time we write a section we mark the values + // as written. + // When running through the array of metadata, we have an inner-loop to write + // all of the entries in the same "section" (a bit like a bubble-sort, if that + // helps explain it). + bool written[value_count]; + + memset(written, 0, sizeof(bool) * value_count); + for (int i = 0; i < value_count; i++) { + bsg_metadata_value *value = &metadata->values[i]; + if (written[i] || value->type == BSG_METADATA_NONE_VALUE) { + continue; + } + + CHECKED(bsg_ksjsonbeginObject(json, value->section)); + if (!bsg_write_metadata_value(json, value)) { + goto error; + } + + // we flush all of the values within this section, marking each one as + // written + const char *section = value->section; + for (int j = i + 1; j < value_count; j++) { + bsg_metadata_value *value2 = &metadata->values[j]; + if (written[j] || value2->type == BSG_METADATA_NONE_VALUE) { + continue; + } + + if (strncmp(section, value2->section, sizeof(value->section)) == 0) { + if (!bsg_write_metadata_value(json, value2)) { + goto error; + } + + // remember that we have already written this element + written[j] = true; + } + } + CHECKED(bsg_ksjsonendContainer(json)); } - int size = bsg_strlen(env->next_last_run_info); - ssize_t len = write(fd, env->next_last_run_info, size); - return len == size; + return true; +error: + return false; } -static bool write_feature_flag(bsg_buffered_writer *writer, - bsg_feature_flag *flag) { - if (!writer->write_string(writer, flag->name)) { - return false; +static bool bsg_write_severity(BSG_KSJSONEncodeContext *json, + bugsnag_severity severity) { + + switch (severity) { + case BSG_SEVERITY_ERR: + CHECKED(JSON_CONSTANT_ELEMENT("severity", "error")); + break; + case BSG_SEVERITY_WARN: + CHECKED(JSON_CONSTANT_ELEMENT("severity", "warning")); + break; + case BSG_SEVERITY_INFO: + CHECKED(JSON_CONSTANT_ELEMENT("severity", "info")); + break; } + return true; +error: + return false; +} + +static bool bsg_write_severity_reason(BSG_KSJSONEncodeContext *json, + bugsnag_event *event) { + + if (!bsg_write_severity(json, event->severity)) { + goto error; + } + CHECKED(bsg_ksjsonaddBooleanElement(json, "unhandled", event->unhandled)); + + CHECKED(bsg_ksjsonbeginObject(json, "severityReason")); + { + // unhandled == false always means that the state has been overridden by the + // user, as this codepath is only executed for unhandled native errors + CHECKED(bsg_ksjsonaddBooleanElement(json, "unhandledOverridden", + !event->unhandled)); + CHECKED(JSON_CONSTANT_ELEMENT("type", "signal")); + + bsg_error *error = &event->error; + CHECKED(bsg_ksjsonbeginObject(json, "attributes")); + { CHECKED(JSON_LIMITED_STRING_ELEMENT("signalType", error->errorClass)); } + CHECKED(bsg_ksjsonendContainer(json)); + } + CHECKED(bsg_ksjsonendContainer(json)); + return true; +error: + return false; +} + +static bool bsg_write_stackframe(BSG_KSJSONEncodeContext *json, + bugsnag_stackframe *frame, bool isPC) { - if (flag->variant) { - if (!writer->write_byte(writer, 1)) { - return false; + CHECKED(bsg_ksjsonbeginObject(json, NULL)); + { + // we use a single buffer for all of the hex encoded strings + // the bsg_uint64_to_hex doesn't prefix the '0x' so we pre-place that in the + // buffer, and then overwrite the rest of the buffer + char hex_str[2 /* '0x' */ + 20 /* number */ + 1 /* NULL */] = "0x"; + char *hex_output_buffer = &hex_str[2]; + bsg_uint64_to_hex(frame->frame_address, hex_output_buffer, 1); + CHECKED(JSON_LIMITED_STRING_ELEMENT("frameAddress", hex_str)); + + bsg_uint64_to_hex(frame->symbol_address, hex_output_buffer, 1); + CHECKED(JSON_LIMITED_STRING_ELEMENT("symbolAddress", hex_str)); + + bsg_uint64_to_hex(frame->load_address, hex_output_buffer, 1); + CHECKED(JSON_LIMITED_STRING_ELEMENT("loadAddress", hex_str)); + + CHECKED( + bsg_ksjsonaddUIntegerElement(json, "lineNumber", frame->line_number)); + + if (isPC) { + CHECKED(bsg_ksjsonaddBooleanElement(json, "isPC", true)); } - if (!writer->write_string(writer, flag->variant)) { - return false; + if (string_is_not_empty(frame->filename)) { + CHECKED(JSON_LIMITED_STRING_ELEMENT("file", frame->filename)); } - } else { - if (!writer->write_byte(writer, 0)) { - return false; + + if (string_is_not_empty(frame->method)) { + CHECKED(JSON_LIMITED_STRING_ELEMENT("method", frame->method)); + } else { + bsg_uint64_to_hex(frame->symbol_address, hex_output_buffer, 1); + CHECKED(JSON_LIMITED_STRING_ELEMENT("method", hex_str)); + } + + if (string_is_not_empty(frame->code_identifier)) { + CHECKED(JSON_LIMITED_STRING_ELEMENT("codeIdentifier", + frame->code_identifier)); } } + CHECKED(bsg_ksjsonendContainer(json)); return true; +error: + return false; } -bool bsg_write_breadcrumbs(bugsnag_event *event, bsg_buffered_writer *writer) { - return writer->write(writer, event->breadcrumbs, - sizeof(bugsnag_breadcrumb) * event->max_crumb_count); +static bool bsg_write_stacktrace(BSG_KSJSONEncodeContext *json, + bugsnag_stackframe *stacktrace, + size_t frame_count) { + + for (int findex = 0; findex < frame_count; findex++) { + if (!bsg_write_stackframe(json, &stacktrace[findex], findex == 0)) { + goto error; + } + } + + return true; +error: + return false; } -extern bsg_seqlock_t bsg_feature_flag_lock; +static bool bsg_write_error(BSG_KSJSONEncodeContext *json, bsg_error *error) { + CHECKED(bsg_ksjsonbeginArray(json, "exceptions")); + { + CHECKED(bsg_ksjsonbeginObject(json, NULL)); + { + CHECKED(JSON_LIMITED_STRING_ELEMENT("errorClass", error->errorClass)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("message", error->errorMessage)); + CHECKED(JSON_CONSTANT_ELEMENT("type", "c")); -bool bsg_write_feature_flags(bugsnag_event *event, - bsg_buffered_writer *writer) { - bsg_seqlock_status_t lock_status = - bsg_seqlock_optimistic_read(&bsg_feature_flag_lock); - const uint32_t feature_flag_count = event->feature_flag_count; - if (!writer->write(writer, &feature_flag_count, sizeof(feature_flag_count))) { - return false; + const ssize_t frame_count = error->frame_count; + // assuming that the initial frame is the program counter. This logic will + // need to be revisited if (for example) we add more intelligent + // processing for stack overflow-type errors, like discarding the top + // frames, which would mean no stored frame is the program counter. + if (frame_count > 0) { + CHECKED(bsg_ksjsonbeginArray(json, "stacktrace")); + { + if (!bsg_write_stacktrace(json, error->stacktrace, frame_count)) { + goto error; + } + } + CHECKED(bsg_ksjsonendContainer(json)); + } + } + CHECKED(bsg_ksjsonendContainer(json)); } + CHECKED(bsg_ksjsonendContainer(json)); + + return true; +error: + return false; +} + +static bool bsg_write_user(BSG_KSJSONEncodeContext *json, bugsnag_user *user) { + const bool has_id = string_is_not_empty(user->id); + const bool has_name = string_is_not_empty(user->name); + const bool has_email = string_is_not_empty(user->email); + + const bool has_user = has_id || has_name || has_email; + if (has_user) { + CHECKED(bsg_ksjsonbeginObject(json, "user")); + { + if (has_id) { + CHECKED(JSON_LIMITED_STRING_ELEMENT("id", user->id)); + } + + if (has_name) { + CHECKED(JSON_LIMITED_STRING_ELEMENT("name", user->name)); + } - for (uint32_t index = 0; index < feature_flag_count; index++) { - if (!write_feature_flag(writer, &event->feature_flags[index])) { - return false; + if (has_email) { + CHECKED(JSON_LIMITED_STRING_ELEMENT("email", user->email)); + } } + CHECKED(bsg_ksjsonendContainer(json)); } + return true; +error: + return false; +} - // write a single byte to indicate whether the feature flags were modified - // when writing them out, 0 indicates "valid", anything else indicates - // "invalid" - writer->write_byte( - writer, - bsg_seqlock_validate(&bsg_feature_flag_lock, lock_status) ? 0 : 1); +static bool bsg_write_app(BSG_KSJSONEncodeContext *json, bsg_app_info *app) { + CHECKED(bsg_ksjsonbeginObject(json, "app")); + { + CHECKED(JSON_LIMITED_STRING_ELEMENT("version", app->version)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("id", app->id)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("type", app->type)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("releaseStage", app->release_stage)); + CHECKED( + bsg_ksjsonaddIntegerElement(json, "versionCode", app->version_code)); + if (string_is_not_empty(app->build_uuid)) { + CHECKED(JSON_LIMITED_STRING_ELEMENT("buildUUID", app->build_uuid)); + } + + CHECKED(JSON_LIMITED_STRING_ELEMENT("binaryArch", app->binary_arch)); + CHECKED(bsg_ksjsonaddIntegerElement(json, "duration", app->duration)); + CHECKED(bsg_ksjsonaddIntegerElement(json, "durationInForeground", + app->duration_in_foreground)); + CHECKED( + bsg_ksjsonaddBooleanElement(json, "inForeground", app->in_foreground)); + CHECKED( + bsg_ksjsonaddBooleanElement(json, "isLaunching", app->is_launching)); + } + CHECKED(bsg_ksjsonendContainer(json)); return true; +error: + return false; } -static bool bsg_write_opaque_metadata_unit(bugsnag_metadata *metadata, - bsg_buffered_writer *writer) { +static bool bsg_write_device(BSG_KSJSONEncodeContext *json, + bsg_device_info *device) { - for (size_t index = 0; index < metadata->value_count; index++) { - uint32_t value_size = metadata->values[index].opaque_value_size; - if (metadata->values[index].type == BSG_METADATA_OPAQUE_VALUE && - value_size > 0) { - if (!writer->write(writer, metadata->values[index].opaque_value, - value_size)) { - return false; + CHECKED(bsg_ksjsonbeginObject(json, "device")); + { + CHECKED(JSON_LIMITED_STRING_ELEMENT("osName", device->os_name)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("id", device->id)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("locale", device->locale)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("osVersion", device->os_version)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("manufacturer", device->manufacturer)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("model", device->model)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("orientation", device->orientation)); + + CHECKED(bsg_ksjsonbeginObject(json, "runtimeVersions")); + { + CHECKED(bsg_ksjsonaddIntegerElement(json, "androidApiLevel", + device->api_level)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("osBuild", device->os_build)); + } + CHECKED(bsg_ksjsonendContainer(json)); + + CHECKED(bsg_ksjsonbeginArray(json, "cpuAbi")); + { + const int cpu_api_count = device->cpu_abi_count; + for (int i = 0; i < cpu_api_count; i++) { + CHECKED(JSON_LIMITED_STRING_ELEMENT(NULL, device->cpu_abi[i].value)); } } + CHECKED(bsg_ksjsonendContainer(json)); + + CHECKED(bsg_ksjsonaddUIntegerElement(json, "totalMemory", + device->total_memory)); + CHECKED( + bsg_ksjsonaddBooleanElement(json, "jailbroken", device->jailbroken)); + + if (device->time > 0) { + char buffer[sizeof "2018-10-08T12:07:09Z"]; + bsg_time_to_simplified_iso8601_string(device->time, buffer); + CHECKED(JSON_LIMITED_STRING_ELEMENT("time", buffer)); + } } + CHECKED(bsg_ksjsonendContainer(json)); + return true; +error: + return false; +} +static bool bsg_write_breadcrumb_type(BSG_KSJSONEncodeContext *json, + bugsnag_breadcrumb_type type) { + + switch (type) { + case BSG_CRUMB_ERROR: + CHECKED(JSON_CONSTANT_ELEMENT("type", "error")); + break; + case BSG_CRUMB_LOG: + CHECKED(JSON_CONSTANT_ELEMENT("type", "log")); + break; + case BSG_CRUMB_MANUAL: + CHECKED(JSON_CONSTANT_ELEMENT("type", "manual")); + break; + case BSG_CRUMB_NAVIGATION: + CHECKED(JSON_CONSTANT_ELEMENT("type", "navigation")); + break; + case BSG_CRUMB_PROCESS: + CHECKED(JSON_CONSTANT_ELEMENT("type", "process")); + break; + case BSG_CRUMB_REQUEST: + CHECKED(JSON_CONSTANT_ELEMENT("type", "request")); + break; + case BSG_CRUMB_STATE: + CHECKED(JSON_CONSTANT_ELEMENT("type", "state")); + break; + case BSG_CRUMB_USER: + CHECKED(JSON_CONSTANT_ELEMENT("type", "user")); + break; + } return true; +error: + return false; } -bool bsg_write_opaque_metadata(bugsnag_event *event, - bsg_buffered_writer *writer) { +static bool bsg_write_breadcrumb(BSG_KSJSONEncodeContext *json, + bugsnag_breadcrumb *breadcrumb) { + CHECKED(bsg_ksjsonbeginObject(json, NULL)); + { + CHECKED(JSON_LIMITED_STRING_ELEMENT("timestamp", breadcrumb->timestamp)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("name", breadcrumb->name)); + if (!bsg_write_breadcrumb_type(json, breadcrumb->type)) { + goto error; + } + if (!bsg_write_metadata(json, &breadcrumb->metadata)) { + goto error; + } + } + CHECKED(bsg_ksjsonendContainer(json)); + return true; +error: + return false; +} - if (!bsg_write_opaque_metadata_unit(&event->metadata, writer)) { - return false; +static bool bsg_write_breadcrumbs(BSG_KSJSONEncodeContext *json, + bugsnag_breadcrumb *breadcrumbs, + int first_crumb_index, int crumb_count, + int max_crumb_count) { + + CHECKED(bsg_ksjsonbeginArray(json, "breadcrumbs")); + { + for (int i = 0; i < crumb_count; i++) { + int crumb_index = i % max_crumb_count; + if (!bsg_write_breadcrumb(json, &breadcrumbs[crumb_index])) { + goto error; + } + } + } + CHECKED(bsg_ksjsonendContainer(json)); + return true; +error: + return false; +} +static bool bsg_write_threads(BSG_KSJSONEncodeContext *json, + bsg_thread *threads, int thread_count) { + + CHECKED(bsg_ksjsonbeginArray(json, "threads")); + { + for (int i = 0; i < thread_count; i++) { + bsg_thread *thread = &threads[i]; + char id_string[30]; + bsg_uint64_to_string(thread->id, id_string); + + CHECKED(bsg_ksjsonbeginObject(json, NULL)); + { + CHECKED(JSON_LIMITED_STRING_ELEMENT("id", id_string)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("name", thread->name)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("state", thread->state)); + CHECKED(JSON_CONSTANT_ELEMENT("type", "c")); + } + CHECKED(bsg_ksjsonendContainer(json)); + } + } + CHECKED(bsg_ksjsonendContainer(json)); + + return true; +error: + return false; +} + +static bool bsg_write_feature_flags(BSG_KSJSONEncodeContext *json, + bsg_feature_flag *flags, int flag_count) { + if (flag_count == 0) { + return true; + } + + CHECKED(bsg_ksjsonbeginArray(json, "featureFlags")); + { + for (int i = 0; i < flag_count; i++) { + bsg_feature_flag *flag = &flags[i]; + CHECKED(bsg_ksjsonbeginObject(json, NULL)); + { + CHECKED(bsg_ksjsonaddStringElement(json, "featureFlag", flag->name, + strlen(flag->name))); + if (flag->variant != NULL) { + CHECKED(bsg_ksjsonaddStringElement(json, "variant", flag->variant, + strlen(flag->variant))); + } + } + CHECKED(bsg_ksjsonendContainer(json)); + } } + CHECKED(bsg_ksjsonendContainer(json)); + + return true; +error: + return false; +} + +static bool bsg_write_session(BSG_KSJSONEncodeContext *json, + bugsnag_event *event) { - for (int breadcrumb_index = 0; breadcrumb_index < event->crumb_count; - breadcrumb_index++) { - if (!bsg_write_opaque_metadata_unit( - &event->breadcrumbs[breadcrumb_index].metadata, writer)) { - return false; + if (string_is_empty(event->session_id)) { + return true; + } + + CHECKED(bsg_ksjsonbeginObject(json, "session")); + { + CHECKED(JSON_LIMITED_STRING_ELEMENT("id", event->session_id)); + CHECKED(JSON_LIMITED_STRING_ELEMENT("startedAt", event->session_start)); + + CHECKED(bsg_ksjsonbeginObject(json, "events")); + { + CHECKED( + bsg_ksjsonaddIntegerElement(json, "handled", event->handled_events)); + CHECKED(bsg_ksjsonaddIntegerElement(json, "unhandled", + event->unhandled_events)); } + CHECKED(bsg_ksjsonendContainer(json)); } + CHECKED(bsg_ksjsonendContainer(json)); return true; +error: + return false; +} + +static bool bsg_write_usage(BSG_KSJSONEncodeContext *json, + bsg_environment *env) { + bugsnag_event *event = &env->next_event; + CHECKED(bsg_ksjsonbeginObject(json, "usage")); + { + CHECKED(bsg_ksjsonbeginObject(json, "callbacks")); + { + static const int callbacks_count = sizeof(event->set_callback_counts) / + sizeof(*event->set_callback_counts); + + for (int i = 0; i < callbacks_count; i++) { + if (event->set_callback_counts[i].count > 0) { + CHECKED(bsg_ksjsonaddIntegerElement( + json, event->set_callback_counts[i].name, + event->set_callback_counts[i].count)); + } + } + + for (int i = 0; i < bsg_called_apis_count; i++) { + if (bsg_was_api_called(event, i)) { + CHECKED( + bsg_ksjsonaddBooleanElement(json, bsg_called_api_names[i], true)); + } + } + } + CHECKED(bsg_ksjsonendContainer(json)); + + if (env->static_json_data != NULL) { + const size_t length = strlen(env->static_json_data); + // the static_json_data *must* be more than simply "{}" + if (length > 2) { + CHECKED(bsg_ksjsonaddRawJSONData(json, ",", 1)); + CHECKED(bsg_ksjsonaddRawJSONData(json, &(env->static_json_data[1]), + length - 2)); + } + } + } + CHECKED(bsg_ksjsonendContainer(json)); + + return true; +error: + return false; +} + +bool bsg_lastrun_write(bsg_environment *env) { + char *path = env->last_run_info_path; + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd == -1) { + return false; + } + + int size = strlen(env->next_last_run_info); + ssize_t len = write(fd, env->next_last_run_info, size); + return len == size; } diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.h index 6f2ddfaeff..d8593dc818 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.h @@ -6,4 +6,7 @@ bool bsg_event_write(bsg_environment *env) __asyncsafe; +bool bsg_write_event_file(bsg_environment *env, + const char *filename) __asyncsafe; + bool bsg_lastrun_write(bsg_environment *env) __asyncsafe; diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.c deleted file mode 100644 index ad3c356a5b..0000000000 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.c +++ /dev/null @@ -1,489 +0,0 @@ -#include "json_writer.h" - -#include -#include -#include -#include - -#include - -#include "../logger.h" -#include "internal_metrics.h" - -const char *bsg_crumb_type_string(bugsnag_breadcrumb_type type) { - switch (type) { - case BSG_CRUMB_ERROR: - return "error"; - case BSG_CRUMB_LOG: - return "log"; - case BSG_CRUMB_MANUAL: - return "manual"; - case BSG_CRUMB_NAVIGATION: - return "navigation"; - case BSG_CRUMB_PROCESS: - return "process"; - case BSG_CRUMB_REQUEST: - return "request"; - case BSG_CRUMB_STATE: - return "state"; - case BSG_CRUMB_USER: - return "user"; - default: - return ""; - } -} - -const char *bsg_severity_string(bugsnag_severity type) { - switch (type) { - case BSG_SEVERITY_INFO: - return "info"; - case BSG_SEVERITY_WARN: - return "warn"; - case BSG_SEVERITY_ERR: - return "error"; - default: - return ""; - } -} - -void bsg_serialize_context(const bugsnag_event *event, JSON_Object *event_obj) { - json_object_set_string(event_obj, "context", event->context); -} - -void bsg_serialize_grouping_hash(const bugsnag_event *event, - JSON_Object *event_obj) { - if (strlen(event->grouping_hash) > 0) { - json_object_set_string(event_obj, "groupingHash", event->grouping_hash); - } -} - -void bsg_serialize_severity_reason(const bugsnag_event *event, - JSON_Object *event_obj) { - // FUTURE(dm): severityReason/unhandled attributes are currently - // over-optimized for signal handling. in the future we may want to handle - // C++ exceptions, etc as well. - json_object_set_string(event_obj, "severity", - bsg_severity_string(event->severity)); - bool unhandled = event->unhandled; - json_object_dotset_boolean(event_obj, "unhandled", unhandled); - - // unhandled == false always means that the state has been overridden by the - // user, as this codepath is only executed for unhandled native errors - json_object_dotset_boolean(event_obj, "severityReason.unhandledOverridden", - !unhandled); - json_object_dotset_string(event_obj, "severityReason.type", "signal"); - json_object_dotset_string(event_obj, "severityReason.attributes.signalType", - event->error.errorClass); -} - -void bsg_serialize_app(const bsg_app_info app, JSON_Object *event_obj) { - json_object_dotset_string(event_obj, "app.version", app.version); - json_object_dotset_string(event_obj, "app.id", app.id); - json_object_dotset_string(event_obj, "app.type", app.type); - - json_object_dotset_string(event_obj, "app.releaseStage", app.release_stage); - json_object_dotset_number(event_obj, "app.versionCode", app.version_code); - if (strlen(app.build_uuid) > 0) { - json_object_dotset_string(event_obj, "app.buildUUID", app.build_uuid); - } - json_object_dotset_string(event_obj, "app.binaryArch", app.binary_arch); - json_object_dotset_number(event_obj, "app.duration", app.duration); - json_object_dotset_number(event_obj, "app.durationInForeground", - app.duration_in_foreground); - json_object_dotset_boolean(event_obj, "app.inForeground", app.in_foreground); - json_object_dotset_boolean(event_obj, "app.isLaunching", app.is_launching); -} - -void bsg_serialize_app_metadata(const bsg_app_info app, - JSON_Object *event_obj) { - json_object_dotset_string(event_obj, "metaData.app.activeScreen", - app.active_screen); -} - -void bsg_serialize_device(const bsg_device_info device, - JSON_Object *event_obj) { - json_object_dotset_string(event_obj, "device.osName", device.os_name); - json_object_dotset_string(event_obj, "device.id", device.id); - json_object_dotset_string(event_obj, "device.locale", device.locale); - json_object_dotset_string(event_obj, "device.osVersion", device.os_version); - json_object_dotset_string(event_obj, "device.manufacturer", - device.manufacturer); - json_object_dotset_string(event_obj, "device.model", device.model); - json_object_dotset_string(event_obj, "device.orientation", - device.orientation); - char android_api_level[sizeof "1234"]; - snprintf(android_api_level, 4, "%d", device.api_level); - json_object_dotset_string(event_obj, "device.runtimeVersions.androidApiLevel", - android_api_level); - json_object_dotset_string(event_obj, "device.runtimeVersions.osBuild", - device.os_build); - - JSON_Value *abi_val = json_value_init_array(); - JSON_Array *cpu_abis = json_value_get_array(abi_val); - json_object_dotset_value(event_obj, "device.cpuAbi", abi_val); - for (int i = 0; i < device.cpu_abi_count; i++) { - json_array_append_string(cpu_abis, device.cpu_abi[i].value); - } - - json_object_dotset_number(event_obj, "device.totalMemory", - device.total_memory); - json_object_dotset_boolean(event_obj, "device.jailbroken", device.jailbroken); - - char report_time[sizeof "2018-10-08T12:07:09Z"]; - if (device.time > 0) { - strftime(report_time, sizeof report_time, "%FT%TZ", gmtime(&device.time)); - json_object_dotset_string(event_obj, "device.time", report_time); - } -} - -void bsg_serialize_device_metadata(const bsg_device_info device, - JSON_Object *event_obj) {} - -static JSON_Value * -bsg_json_for_opaque_metadata(const bsg_metadata_value *metadata) { - return (metadata->opaque_value_size > 0) - ? json_parse_string(metadata->opaque_value) - : NULL; -} - -void bsg_serialize_custom_metadata(const bugsnag_metadata metadata, - JSON_Object *event_obj) { - for (int i = 0; i < metadata.value_count; i++) { - char *format = calloc(1, sizeof(char) * 256); - bsg_metadata_value value = metadata.values[i]; - - switch (value.type) { - case BSG_METADATA_BOOL_VALUE: - sprintf(format, "metaData.%s.%s", value.section, value.name); - json_object_dotset_boolean(event_obj, format, value.bool_value); - break; - case BSG_METADATA_CHAR_VALUE: - sprintf(format, "metaData.%s.%s", value.section, value.name); - json_object_dotset_string(event_obj, format, value.char_value); - break; - case BSG_METADATA_NUMBER_VALUE: - sprintf(format, "metaData.%s.%s", value.section, value.name); - json_object_dotset_number(event_obj, format, value.double_value); - break; - case BSG_METADATA_OPAQUE_VALUE: - sprintf(format, "metaData.%s.%s", value.section, value.name); - JSON_Value *metadata_json_value = bsg_json_for_opaque_metadata(&value); - json_object_dotset_value(event_obj, format, metadata_json_value); - break; - default: - break; - } - free(format); - } -} - -void bsg_serialize_breadcrumb_metadata(const bugsnag_metadata metadata, - JSON_Object *event_obj) { - for (int i = 0; i < metadata.value_count; i++) { - char *format = calloc(1, sizeof(char) * 256); - bsg_metadata_value value = metadata.values[i]; - - switch (value.type) { - case BSG_METADATA_BOOL_VALUE: - sprintf(format, "metaData.%s", value.name); - json_object_dotset_boolean(event_obj, format, value.bool_value); - break; - case BSG_METADATA_CHAR_VALUE: - sprintf(format, "metaData.%s", value.name); - json_object_dotset_string(event_obj, format, value.char_value); - break; - case BSG_METADATA_NUMBER_VALUE: - sprintf(format, "metaData.%s", value.name); - json_object_dotset_number(event_obj, format, value.double_value); - break; - case BSG_METADATA_OPAQUE_VALUE: - sprintf(format, "metaData.%s", value.name); - JSON_Value *metadata_json_value = bsg_json_for_opaque_metadata(&value); - json_object_dotset_value(event_obj, format, metadata_json_value); - break; - default: - break; - } - free(format); - } -} - -void bsg_serialize_user(const bugsnag_user user, JSON_Object *event_obj) { - if (strlen(user.name) > 0) - json_object_dotset_string(event_obj, "user.name", user.name); - if (strlen(user.email) > 0) - json_object_dotset_string(event_obj, "user.email", user.email); - if (strlen(user.id) > 0) - json_object_dotset_string(event_obj, "user.id", user.id); -} - -void bsg_serialize_session(bugsnag_event *event, JSON_Object *event_obj) { - if (bsg_event_has_session(event)) { - json_object_dotset_string(event_obj, "session.startedAt", - event->session_start); - json_object_dotset_string(event_obj, "session.id", event->session_id); - json_object_dotset_number(event_obj, "session.events.handled", - event->handled_events); - json_object_dotset_number(event_obj, "session.events.unhandled", - event->unhandled_events); - } -} - -void bsg_serialize_error(bsg_error exc, JSON_Object *exception, - JSON_Array *stacktrace) { - json_object_set_string(exception, "errorClass", exc.errorClass); - json_object_set_string(exception, "message", exc.errorMessage); - json_object_set_string(exception, "type", "c"); - // assuming that the initial frame is the program counter. This logic will - // need to be revisited if (for example) we add more intelligent processing - // for stack overflow-type errors, like discarding the top frames, which - // would mean no stored frame is the program counter. - if (exc.frame_count > 0) { - bsg_serialize_stackframe(&(exc.stacktrace[0]), true, stacktrace); - } - for (int findex = 1; findex < exc.frame_count; findex++) { - bugsnag_stackframe stackframe = exc.stacktrace[findex]; - bsg_serialize_stackframe(&stackframe, false, stacktrace); - } -} - -static void set_hex_number(JSON_Object *frame, const char *name, - unsigned long value) { - char hex_str[20]; - sprintf(hex_str, "0x%lx", value); - json_object_set_string(frame, name, hex_str); -} - -void bsg_serialize_stackframe(bugsnag_stackframe *stackframe, bool is_pc, - JSON_Array *stacktrace) { - JSON_Value *frame_val = json_value_init_object(); - JSON_Object *frame = json_value_get_object(frame_val); - set_hex_number(frame, "frameAddress", (*stackframe).frame_address); - set_hex_number(frame, "symbolAddress", (*stackframe).symbol_address); - set_hex_number(frame, "loadAddress", (*stackframe).load_address); - json_object_set_number(frame, "lineNumber", (*stackframe).line_number); - if (is_pc) { - // only necessary to set to true, false is the default value and omitting - // the field keeps payload sizes smaller. - json_object_set_boolean(frame, "isPC", true); - } - if (strlen((*stackframe).filename) > 0) { - json_object_set_string(frame, "file", (*stackframe).filename); - } - if (strlen((*stackframe).method) == 0) { - set_hex_number(frame, "method", (*stackframe).frame_address); - } else { - json_object_set_string(frame, "method", (*stackframe).method); - } - - if (*stackframe->code_identifier != 0) { - char code_identifier[sizeof(stackframe->code_identifier) + 1]; - code_identifier[sizeof(stackframe->code_identifier)] = - 0; // force zero terminator - strncpy(code_identifier, stackframe->code_identifier, - sizeof(stackframe->code_identifier)); - json_object_set_string(frame, "codeIdentifier", code_identifier); - } - - json_array_append_value(stacktrace, frame_val); -} - -#if defined(__i386__) || defined(__arm__) -#define TIMESTAMP_T long long -#define TIMESTAMP_DECODE atoll -#define TIMESTAMP_MILLIS_FORMAT "%s.%03lldZ" -#elif defined(__x86_64__) || defined(__aarch64__) -#define TIMESTAMP_T long -#define TIMESTAMP_DECODE atol -#define TIMESTAMP_MILLIS_FORMAT "%s.%03ldZ" -#endif - -/** - * Convert a string representing the number of milliseconds since the epoch - * into the date format "yyyy-MM-ddTHH:mm:ss.SSSZ". Safe for all dates earlier - * than 2038. - * - * @param source the timestamp string, should be something like: 1636710533109 - * @param dest a buffer large enough to hold the 24 characters required in the - * date format - * - * @return true if the conversion succeeded - */ -static bool timestamp_to_iso8601_millis(const char *source, char *dest) { - TIMESTAMP_T timestamp = TIMESTAMP_DECODE(source); - if (timestamp) { - time_t seconds = timestamp / 1000; - TIMESTAMP_T milliseconds = timestamp - (seconds * 1000LL); - if (milliseconds > 1000) { // round to nearest second - seconds++; - milliseconds -= 1000; - } - struct tm timer; - // gmtime(3) can fail if "the year does not fit into an integer". Hopefully - // nobody is running this code by then. - if (gmtime_r(&seconds, &timer)) { - char buffer[26]; - strftime(buffer, 26, "%Y-%m-%dT%H:%M:%S", &timer); - sprintf(dest, TIMESTAMP_MILLIS_FORMAT, buffer, milliseconds); - return true; - } else { - BUGSNAG_LOG("Hello, people of the far future! Please use your time " - "machine to file a bug in the year 2021."); - } - } - return false; -} - -#undef TIMESTAMP_T -#undef TIMESTAMP_DECODE -#undef TIMESTAMP_MILLIS_FORMAT - -void bsg_serialize_breadcrumbs(const bugsnag_event *event, JSON_Array *crumbs) { - if (event->crumb_count > 0) { - int current_index = event->crumb_first_index; - while (json_array_get_count(crumbs) < event->crumb_count) { - JSON_Value *crumb_val = json_value_init_object(); - JSON_Object *crumb = json_value_get_object(crumb_val); - json_array_append_value(crumbs, crumb_val); - - bugsnag_breadcrumb breadcrumb = event->breadcrumbs[current_index]; - json_object_set_string(crumb, "name", breadcrumb.name); - // check whether to decode milliseconds into ISO8601 date format - if (breadcrumb.timestamp[0] == 't') { - char *unix_timestamp_str = breadcrumb.timestamp + 1; - char buffer[32]; - if (timestamp_to_iso8601_millis(unix_timestamp_str, buffer)) { - json_object_set_string(crumb, "timestamp", buffer); - } else { - // at least we tried. - json_object_set_string(crumb, "timestamp", unix_timestamp_str); - } - } else { - json_object_set_string(crumb, "timestamp", breadcrumb.timestamp); - } - json_object_set_string(crumb, "type", - bsg_crumb_type_string(breadcrumb.type)); - bsg_serialize_breadcrumb_metadata(breadcrumb.metadata, crumb); - current_index++; - if (current_index == event->max_crumb_count) { - current_index = 0; - } - } - } -} - -void bsg_serialize_threads(const bugsnag_event *event, JSON_Array *threads) { - if (event->thread_count <= 0) { - return; - } - - for (int index = 0; index < event->thread_count; index++) { - JSON_Value *thread_val = json_value_init_object(); - JSON_Object *json_thread = json_value_get_object(thread_val); - json_array_append_value(threads, thread_val); - - const bsg_thread *thread = &event->threads[index]; - char buffer[32]; - sprintf(buffer, "%d", thread->id); - json_object_set_string(json_thread, "id", buffer); - json_object_set_string(json_thread, "name", thread->name); - json_object_set_string(json_thread, "state", thread->state); - json_object_set_string(json_thread, "type", "c"); - } -} - -void bsg_serialize_feature_flags(const bugsnag_event *event, - JSON_Array *feature_flags) { - if (event->feature_flag_count <= 0) { - return; - } - - for (int index = 0; index < event->feature_flag_count; index++) { - JSON_Value *feature_flag_val = json_value_init_object(); - JSON_Object *feature_flag = json_value_get_object(feature_flag_val); - json_array_append_value(feature_flags, feature_flag_val); - - const bsg_feature_flag *flag = &event->feature_flags[index]; - json_object_set_string(feature_flag, "featureFlag", flag->name); - - if (flag->variant) { - json_object_set_string(feature_flag, "variant", flag->variant); - } - } -} - -static void bsg_serialize_callbacks(const bugsnag_event *event, - JSON_Object *callbacks_obj) { - static const int callbacks_count = - sizeof(event->set_callback_counts) / sizeof(*event->set_callback_counts); - - for (int i = 0; i < callbacks_count; i++) { - if (event->set_callback_counts[i].count > 0) { - json_object_set_number(callbacks_obj, event->set_callback_counts[i].name, - event->set_callback_counts[i].count); - } - } - - for (int i = 0; i < bsg_called_apis_count; i++) { - if (bsg_was_api_called(event, i)) { - json_object_set_boolean(callbacks_obj, bsg_called_api_names[i], true); - } - } -} - -static void bsg_serialize_usage(const bugsnag_event *event, - JSON_Object *event_obj) { - JSON_Value *usage_val = json_value_init_object(); - JSON_Object *usage_obj = json_value_get_object(usage_val); - json_object_set_value(event_obj, "usage", usage_val); - - JSON_Value *callbacks_val = json_value_init_object(); - JSON_Object *callbacks = json_value_get_object(callbacks_val); - json_object_set_value(usage_obj, "callbacks", callbacks_val); - bsg_serialize_callbacks(event, callbacks); -} - -char *bsg_event_to_json(bugsnag_event *event) { - JSON_Value *event_val = json_value_init_object(); - JSON_Object *event_obj = json_value_get_object(event_val); - JSON_Value *crumbs_val = json_value_init_array(); - JSON_Array *crumbs = json_value_get_array(crumbs_val); - JSON_Value *exceptions_val = json_value_init_array(); - JSON_Array *exceptions = json_value_get_array(exceptions_val); - JSON_Value *ex_val = json_value_init_object(); - JSON_Object *exception = json_value_get_object(ex_val); - JSON_Value *threads_val = json_value_init_array(); - JSON_Array *threads = json_value_get_array(threads_val); - JSON_Value *stack_val = json_value_init_array(); - JSON_Array *stacktrace = json_value_get_array(stack_val); - JSON_Value *feature_flags_val = json_value_init_array(); - JSON_Array *feature_flags = json_value_get_array(feature_flags_val); - json_object_set_value(event_obj, "exceptions", exceptions_val); - json_object_set_value(event_obj, "breadcrumbs", crumbs_val); - json_object_set_value(event_obj, "threads", threads_val); - json_object_set_value(exception, "stacktrace", stack_val); - json_object_set_value(event_obj, "featureFlags", feature_flags_val); - json_array_append_value(exceptions, ex_val); - char *serialized_string = NULL; - { - bsg_serialize_context(event, event_obj); - bsg_serialize_grouping_hash(event, event_obj); - bsg_serialize_severity_reason(event, event_obj); - bsg_serialize_app(event->app, event_obj); - bsg_serialize_app_metadata(event->app, event_obj); - bsg_serialize_device(event->device, event_obj); - bsg_serialize_device_metadata(event->device, event_obj); - bsg_serialize_custom_metadata(event->metadata, event_obj); - bsg_serialize_user(event->user, event_obj); - bsg_serialize_session(event, event_obj); - bsg_serialize_error(event->error, exception, stacktrace); - bsg_serialize_breadcrumbs(event, crumbs); - bsg_serialize_threads(event, threads); - bsg_serialize_feature_flags(event, feature_flags); - bsg_serialize_usage(event, event_obj); - - serialized_string = json_serialize_to_string(event_val); - json_value_free(event_val); - } - return serialized_string; -} diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.h deleted file mode 100644 index 9b9696dd5a..0000000000 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/json_writer.h +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once -#include - -#include "../../event.h" - -char *bsg_event_to_json(bugsnag_event *event); - -/** Serialization components (exposed for testing) */ - -void bsg_serialize_context(const bugsnag_event *event, JSON_Object *event_obj); -void bsg_serialize_severity_reason(const bugsnag_event *event, - JSON_Object *event_obj); -void bsg_serialize_app(const bsg_app_info app, JSON_Object *event_obj); -void bsg_serialize_app_metadata(const bsg_app_info app, JSON_Object *event_obj); -void bsg_serialize_device(const bsg_device_info device, JSON_Object *event_obj); -void bsg_serialize_device_metadata(const bsg_device_info device, - JSON_Object *event_obj); -void bsg_serialize_custom_metadata(const bugsnag_metadata metadata, - JSON_Object *event_obj); -void bsg_serialize_user(const bugsnag_user user, JSON_Object *event_obj); -void bsg_serialize_session(bugsnag_event *event, JSON_Object *event_obj); -/** - * Append a JSON-serialized stackframe to an array - * - * @param stackframe the frame to serialize - * @param is_pc true if the current frame is the program counter - * @param stacktrace the destination array - */ -void bsg_serialize_stackframe(bugsnag_stackframe *stackframe, bool is_pc, - JSON_Array *stacktrace); -void bsg_serialize_error(bsg_error exc, JSON_Object *exception, - JSON_Array *stacktrace); -void bsg_serialize_breadcrumbs(const bugsnag_event *event, JSON_Array *crumbs); -void bsg_serialize_threads(const bugsnag_event *event, JSON_Array *threads); -void bsg_serialize_feature_flags(const bugsnag_event *event, - JSON_Array *feature_flags); diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/string.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/string.c index 02959082ea..5774ebdca3 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/string.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/string.c @@ -15,14 +15,15 @@ size_t bsg_strlen(const char *str) { return strnlen(str, STRING_MAX_LENGTH); } -void bsg_strncpy(char *dst, const char *src, size_t dst_size) { +size_t bsg_strncpy(char *dst, const char *src, size_t dst_size) { if (dst == NULL || dst_size == 0) { - return; + return 0; } dst[0] = '\0'; if (src != NULL) { - strncat(dst, src, dst_size - 1); + return strlcat(dst, src, dst_size); } + return 0; } void bsg_hex_encode(char *dst, const void *src, size_t byte_count, diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/string.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/string.h index edafcfa60b..7e79f041da 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/string.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/string.h @@ -21,7 +21,7 @@ size_t bsg_strlen(const char *str) __asyncsafe; /** * Copy a maximum number of bytes from src to dst */ -void bsg_strncpy(char *dst, const char *src, size_t len) __asyncsafe; +size_t bsg_strncpy(char *dst, const char *src, size_t len) __asyncsafe; /** * Encode a number of bytes into dst while hex encoding the data. diff --git a/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt b/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt index f1dac42308..d16594b30e 100644 --- a/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt +++ b/bugsnag-plugin-android-ndk/src/test/CMakeLists.txt @@ -12,7 +12,7 @@ add_library(bugsnag-ndk-test SHARED cpp/test_breadcrumbs.c cpp/test_bsg_event.c cpp/test_featureflags.c - cpp/migrations/EventOnDiskTests.cpp + cpp/test_bsg_event.c cpp/UnwindTest.cpp ) target_link_libraries(bugsnag-ndk-test bugsnag-ndk) diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/main.c b/bugsnag-plugin-android-ndk/src/test/cpp/main.c index 68624d369a..2ab71894fb 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/main.c +++ b/bugsnag-plugin-android-ndk/src/test/cpp/main.c @@ -3,13 +3,14 @@ #include #include -#define GREATEST_FPRINTF(ignore, fmt, ...) __android_log_print(ANDROID_LOG_INFO, "BugsnagNDKTest", fmt, ##__VA_ARGS__) +#define GREATEST_FPRINTF(ignore, fmt, ...) \ + __android_log_print(ANDROID_LOG_INFO, "BugsnagNDKTest", fmt, ##__VA_ARGS__) #include #include +#include "test_bsg_event.h" #include "test_serializer.h" -#include SUITE(suite_string_utils); SUITE(suite_json_serialization); @@ -29,11 +30,11 @@ GREATEST_MAIN_DEFS(); * @return the exit code of the test suite */ int run_test_suite(void (*test_suite)(void)) { - int argc = 0; - char *argv[] = {}; - GREATEST_MAIN_BEGIN(); - RUN_SUITE(test_suite); - GREATEST_MAIN_END(); + int argc = 0; + char *argv[] = {}; + GREATEST_MAIN_BEGIN(); + RUN_SUITE(test_suite); + GREATEST_MAIN_END(); } /** @@ -43,196 +44,68 @@ int run_test_suite(void (*test_suite)(void)) { * @return the exit code of the test suite */ int run_test(enum greatest_test_res (*test)(void)) { - int argc = 0; - char *argv[] = {}; - GREATEST_MAIN_BEGIN(); - RUN_TEST(test); - GREATEST_MAIN_END(); + int argc = 0; + char *argv[] = {}; + GREATEST_MAIN_BEGIN(); + RUN_TEST(test); + GREATEST_MAIN_END(); } -JNIEXPORT int JNICALL Java_com_bugsnag_android_ndk_NativeStringTest_run( - JNIEnv *_env, jobject _this) { - return run_test_suite(suite_string_utils); -} - -JNIEXPORT int JNICALL Java_com_bugsnag_android_ndk_NativeJsonSerializeTest_run( - JNIEnv *_env, jobject _this) { - return run_test_suite(suite_json_serialization); -} - -JNIEXPORT int JNICALL Java_com_bugsnag_android_ndk_NativeStructToFileTest_run( - JNIEnv *_env, jobject _this) { - return run_test_suite(suite_struct_to_file); -} - -JNIEXPORT int JNICALL Java_com_bugsnag_android_ndk_NativeBreadcrumbTest_run( - JNIEnv *_env, jobject _this) { - return run_test_suite(suite_breadcrumbs); -} - -JNIEXPORT int JNICALL Java_com_bugsnag_android_ndk_NativeEventMutatorsTest_run( - JNIEnv *_env, jobject _this) { - return run_test_suite(suite_event_mutators); -} - -JNIEXPORT int JNICALL Java_com_bugsnag_android_ndk_NativeEventAppMutatorsTest_run( - JNIEnv *_env, jobject _this) { - return run_test_suite(suite_event_app_mutators); +JNIEXPORT int JNICALL +Java_com_bugsnag_android_ndk_NativeStringTest_run(JNIEnv *_env, jobject _this) { + return run_test_suite(suite_string_utils); } -JNIEXPORT int JNICALL Java_com_bugsnag_android_ndk_NativeEventDeviceMutatorsTest_run( - JNIEnv *_env, jobject _this) { - return run_test_suite(suite_event_device_mutators); -} - -JNIEXPORT jint JNICALL -Java_com_bugsnag_android_ndk_NativeFeatureFlagsTest_run(JNIEnv *env, jobject thiz) { - return run_test_suite(suite_feature_flags); -} +extern bool bsg_event_write(bsg_environment *env); -JNIEXPORT jstring JNICALL Java_com_bugsnag_android_ndk_UserSerializationTest_run( - JNIEnv *env, jobject _this) { - bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); - loadUserTestCase(event); - JSON_Value *event_val = json_value_init_object(); - JSON_Object *event_obj = json_value_get_object(event_val); - bsg_serialize_user(event->user, event_obj); - char *string = json_serialize_to_string(event_val); - return (*env)->NewStringUTF(env, string); -} +JNIEXPORT int JNICALL Java_com_bugsnag_android_ndk_NativeJsonSerializeTest_run( + JNIEnv *_env, jobject _this, jstring _dir) { -JNIEXPORT jstring JNICALL Java_com_bugsnag_android_ndk_AppSerializationTest_run( - JNIEnv *env, jobject _this) { - bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); - loadAppTestCase(event); - JSON_Value *event_val = json_value_init_object(); - JSON_Object *event_obj = json_value_get_object(event_val); - bsg_serialize_app(event->app, event_obj); - char *string = json_serialize_to_string(event_val); - return (*env)->NewStringUTF(env, string); -} + const char *dir = (*_env)->GetStringUTFChars(_env, _dir, NULL); + if (dir == NULL) { + return 0; + } -JNIEXPORT jstring JNICALL Java_com_bugsnag_android_ndk_AppMetadataSerializationTest_run( - JNIEnv *env, jobject _this) { - bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); - loadAppMetadataTestCase(event); - JSON_Value *event_val = json_value_init_object(); - JSON_Object *event_obj = json_value_get_object(event_val); - bsg_serialize_app_metadata(event->app, event_obj); - char *string = json_serialize_to_string(event_val); - return (*env)->NewStringUTF(env, string); -} + BUGSNAG_LOG("Writing event file to %s", dir); + bsg_environment env; + bugsnag_event *event = init_event(); + memcpy(&env.next_event, event, sizeof(bugsnag_event)); -JNIEXPORT jstring JNICALL Java_com_bugsnag_android_ndk_DeviceSerializationTest_run( - JNIEnv *env, jobject _this) { - bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); - loadDeviceTestCase(event); - JSON_Value *event_val = json_value_init_object(); - JSON_Object *event_obj = json_value_get_object(event_val); - bsg_serialize_device(event->device, event_obj); - char *string = json_serialize_to_string(event_val); - return (*env)->NewStringUTF(env, string); -} + env.event_path = strdup(dir); + strcpy(env.event_uuid, "test-uuid"); -TEST test_custom_meta_data_serialization(test_case *test_case) { - JSON_Value *event_val = json_value_init_object(); - JSON_Object *event = json_value_get_object(event_val); - bugsnag_metadata *meta_data = test_case->data_ptr; - bsg_serialize_custom_metadata(*meta_data, event); - free(meta_data); - return validate_serialized_json(test_case, event_val); -} + bsg_event_write(&env); -JNIEXPORT jstring JNICALL Java_com_bugsnag_android_ndk_CustomMetadataSerializationTest_run( - JNIEnv *env, jobject _this) { - bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); - loadCustomMetadataTestCase(event); - JSON_Value *event_val = json_value_init_object(); - JSON_Object *event_obj = json_value_get_object(event_val); - bsg_serialize_custom_metadata(event->metadata, event_obj); - char *string = json_serialize_to_string(event_val); - return (*env)->NewStringUTF(env, string); -} + free(event); -JNIEXPORT jstring JNICALL Java_com_bugsnag_android_ndk_ContextSerializationTest_run( - JNIEnv *env, jobject _this) { - bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); - loadContextTestCase(event); - JSON_Value *event_val = json_value_init_object(); - JSON_Object *event_obj = json_value_get_object(event_val); - bsg_serialize_context(event, event_obj); - char *string = json_serialize_to_string(event_val); - return (*env)->NewStringUTF(env, string); -} + (*_env)->ReleaseStringUTFChars(_env, _dir, dir); -JNIEXPORT jstring JNICALL Java_com_bugsnag_android_ndk_SeverityReasonSerializationTest_run( - JNIEnv *env, jobject _this) { - bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); - loadSeverityReasonTestCase(event); - JSON_Value *event_val = json_value_init_object(); - JSON_Object *event_obj = json_value_get_object(event_val); - bsg_serialize_severity_reason(event, event_obj); - char *string = json_serialize_to_string(event_val); - return (*env)->NewStringUTF(env, string); + return 0; } -JNIEXPORT jstring JNICALL Java_com_bugsnag_android_ndk_SessionSerializationTest_run( - JNIEnv *env, jobject _this) { - bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); - loadSessionTestCase(event); - JSON_Value *event_val = json_value_init_object(); - JSON_Object *event_obj = json_value_get_object(event_val); - bsg_serialize_session(event, event_obj); - char *string = json_serialize_to_string(event_val); - return (*env)->NewStringUTF(env, string); +JNIEXPORT int JNICALL Java_com_bugsnag_android_ndk_NativeBreadcrumbTest_run( + JNIEnv *_env, jobject _this) { + return run_test_suite(suite_breadcrumbs); } -JNIEXPORT jstring JNICALL -Java_com_bugsnag_android_ndk_BreadcrumbStateSerializationTest_run(JNIEnv *env, - jobject thiz) { - bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); - event->max_crumb_count = 50; - event->breadcrumbs = calloc(event->max_crumb_count, sizeof(bugsnag_breadcrumb)); - loadBreadcrumbsTestCase(event); - JSON_Value *eventVal = json_value_init_array(); - JSON_Array *eventAry = json_value_get_array(eventVal); - bsg_serialize_breadcrumbs(event, eventAry); - char *string = json_serialize_to_string(eventVal); - return (*env)->NewStringUTF(env, string); +JNIEXPORT int JNICALL Java_com_bugsnag_android_ndk_NativeEventMutatorsTest_run( + JNIEnv *_env, jobject _this) { + return run_test_suite(suite_event_mutators); } -JNIEXPORT jstring JNICALL Java_com_bugsnag_android_ndk_StackframeSerializationTest_run( - JNIEnv *env, jobject _this) { - bugsnag_stackframe *frame = loadStackframeTestCase(); - JSON_Value *event_val = json_value_init_array(); - JSON_Array *event_obj = json_value_get_array(event_val); - bsg_serialize_stackframe(frame, false, event_obj); - char *string = json_serialize_to_string(event_val); - return (*env)->NewStringUTF(env, string); +JNIEXPORT int JNICALL +Java_com_bugsnag_android_ndk_NativeEventAppMutatorsTest_run(JNIEnv *_env, + jobject _this) { + return run_test_suite(suite_event_app_mutators); } -JNIEXPORT jstring JNICALL Java_com_bugsnag_android_ndk_ExceptionSerializationTest_run( - JNIEnv *env, jobject _this) { - bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); - loadExceptionTestCase(event); - JSON_Value *event_val = json_value_init_object(); - JSON_Object *exception = json_value_get_object(event_val); - JSON_Value *stack_val = json_value_init_array(); - JSON_Array *stacktrace = json_value_get_array(stack_val); - json_object_set_value(exception, "stacktrace", stack_val); - bsg_serialize_error(event->error, exception, stacktrace); - char *string = json_serialize_to_string(event_val); - return (*env)->NewStringUTF(env, string); - +JNIEXPORT int JNICALL +Java_com_bugsnag_android_ndk_NativeEventDeviceMutatorsTest_run(JNIEnv *_env, + jobject _this) { + return run_test_suite(suite_event_device_mutators); } -JNIEXPORT jstring JNICALL -Java_com_bugsnag_android_ndk_ThreadSerializationTest_run(JNIEnv *env, jobject thiz) { - bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); - loadThreadTestCase(event); - JSON_Value *threads_val = json_value_init_array(); - JSON_Array *threads_array = json_value_get_array(threads_val); - bsg_serialize_threads(event, threads_array); - char *string = json_serialize_to_string(threads_val); - return (*env)->NewStringUTF(env, string); +JNIEXPORT jint JNICALL Java_com_bugsnag_android_ndk_NativeFeatureFlagsTest_run( + JNIEnv *env, jobject thiz) { + return run_test_suite(suite_feature_flags); } diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventOnDiskTests.cpp b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventOnDiskTests.cpp deleted file mode 100644 index 18a06b4acf..0000000000 --- a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/EventOnDiskTests.cpp +++ /dev/null @@ -1,201 +0,0 @@ -#include - -#include - -#include "utils.hpp" - -static void *create_payload_info_event() { - auto event = (bugsnag_event *)calloc(1, sizeof(bugsnag_event)); - - strcpy(event->api_key, "5d1e5fbd39a74caa1200142706a90b20"); - strcpy(event->notifier.name, "Test Library"); - strcpy(event->notifier.url, "https://example.com/test-lib"); - strcpy(event->notifier.version, "2.0.11"); - - return event; -} - -/** - * Create a new event in the current format - */ -static void *create_full_event() { - auto event = (bugsnag_event *)calloc(1, sizeof(bugsnag_event)); - - strcpy(event->context, - "00000000000m0r3.61ee9e6e099d3dd7448f740d395768da6b2df55d5.m4g1c"); - strcpy(event->grouping_hash, - "a1d34088a096987361ee9e6e099d3dd7448f740d395768da6b2df55d5160f33"); - event->severity = BSG_SEVERITY_INFO; - - // app - strcpy(event->app.binary_arch, "mips"); - strcpy(event->app.build_uuid, "1234-9876-adfe"); - event->app.duration = 81395165021; - event->app.duration_in_foreground = 81395165010; - event->app.in_foreground = true; - event->app.is_launching = true; - strcpy(event->app.id, "com.example.PhotoSnapPlus"); - strcpy(event->app.release_stage, "リリース"); - strcpy(event->app.type, "red"); - strcpy(event->app.version, "2.0.52"); - event->app.version_code = 8139512718; - - // breadcrumbs - event->max_crumb_count = 50; - event->breadcrumbs = new bugsnag_breadcrumb[event->max_crumb_count]; - auto max = event->max_crumb_count; - event->crumb_first_index = 2; // test the circular buffer logic - char name[30]; - for (int i = event->crumb_first_index; i < max; i++) { - sprintf(name, "mission %d", i - event->crumb_first_index); - insert_crumb(event->breadcrumbs, i, name, BSG_CRUMB_STATE, 1638992630014, - "Now we know what they mean by 'advanced' tactical training."); - } - for (int i = 0; i < event->crumb_first_index; i++) { - sprintf(name, "mission %d", (max - event->crumb_first_index) + i); - insert_crumb(event->breadcrumbs, i, name, BSG_CRUMB_STATE, 1638992630014, - "Now we know what they mean by 'advanced' tactical training."); - } - event->crumb_count = max; - - // device - event->device.cpu_abi_count = 1; - strcpy(event->device.cpu_abi[0].value, "mipsx"); - strcpy(event->device.id, "ffffa"); - event->device.jailbroken = true; - strcpy(event->device.locale, "en_AU#Melbun"); - strcpy(event->device.manufacturer, "HI-TEC™"); - strcpy(event->device.model, "🍨"); - strcpy(event->device.orientation, "sideup"); - strcpy(event->device.os_name, "BOX BOX"); - strcpy(event->device.os_version, "98.7"); - { // -- runtime versions - strcpy(event->device.os_build, "beta1-2"); - event->device.api_level = 32; - } - event->device.time = 1638992630; - event->device.total_memory = 3839512576; - - // feature flags - event->feature_flag_count = 4; - event->feature_flags = - (bsg_feature_flag *)calloc(4, sizeof(bsg_feature_flag)); - event->feature_flags[0].name = strdup("bluebutton"); - event->feature_flags[0].variant = strdup("on"); - event->feature_flags[1].name = strdup("redbutton"); - event->feature_flags[1].variant = strdup("off"); - event->feature_flags[2].name = strdup("nobutton"); - event->feature_flags[3].name = strdup("switch"); - event->feature_flags[3].variant = strdup("left"); - - // exceptions - strcpy(event->error.errorClass, "SIGBUS"); - strcpy(event->error.errorMessage, "POSIX is serious about oncoming traffic"); - strcpy(event->error.type, "C"); - event->error.frame_count = 2; - event->error.stacktrace[0].frame_address = (uintptr_t)4294967294; - event->error.stacktrace[0].load_address = (uintptr_t)2367523; - event->error.stacktrace[0].symbol_address = 776; - event->error.stacktrace[0].line_number = (uintptr_t)4194967233; - strcpy(event->error.stacktrace[0].method, "makinBacon"); - strcpy(event->error.stacktrace[0].filename, "lib64/libfoo.so"); - event->error.stacktrace[1].frame_address = - (uintptr_t)3011142731; // will become method hex - - // metadata - strcpy(event->app.active_screen, "Menu"); - bugsnag_event_add_metadata_bool(event, "metrics", "experimentX", false); - bugsnag_event_add_metadata_string(event, "metrics", "subject", "percy"); - bugsnag_event_add_metadata_string(event, "app", "weather", "rain"); - bugsnag_event_add_metadata_double(event, "metrics", "counter", 47.5); - - // session info - event->handled_events = 5; - event->unhandled_events = 2; - strcpy(event->session_id, "aaaaaaaaaaaaaaaa"); - strcpy(event->session_start, "2031-07-09T11:08:21+00:00"); - - // threads - event->thread_count = 8; - for (int i = 0; i < event->thread_count; i++) { - event->threads[i].id = 1000 + i; - sprintf(event->threads[i].name, "Thread #%d", i); - sprintf(event->threads[i].state, "paused-%d", i); - } - - // user - strcpy(event->user.email, "fenton@io.example.com"); - strcpy(event->user.name, "Fenton"); - strcpy(event->user.id, "fex01"); - - return event; -} - -static const char *write_event(JNIEnv *env, jstring temp_file, - void *(event_generator)()) { - auto event_ctx = (bsg_environment *)calloc(1, sizeof(bsg_environment)); - event_ctx->report_header.version = BUGSNAG_EVENT_VERSION; - const char *path = (*env).GetStringUTFChars(temp_file, nullptr); - sprintf(event_ctx->next_event_path, "%s", path); - - // (old format) event struct -> file on disk - void *old_event = event_generator(); - memcpy(&event_ctx->next_event, old_event, sizeof(bugsnag_event)); - free(old_event); - bsg_serialize_event_to_file(event_ctx); - free(event_ctx); - return path; -} - -#ifdef __cplusplus -extern "C" { -#endif - -JNIEXPORT jstring JNICALL -Java_com_bugsnag_android_ndk_migrations_EventOnDiskTests_generatePayloadInfo( - JNIEnv *env, jobject _this, jstring temp_file) { - const char *path = write_event(env, temp_file, create_payload_info_event); - - // file on disk -> latest event type - bugsnag_event *parsed_event = bsg_deserialize_event_from_file((char *)path); - - // write json object - JSON_Value *event_val = json_value_init_object(); - JSON_Object *event_obj = json_value_get_object(event_val); - json_object_set_string(event_obj, "apiKey", parsed_event->api_key); - json_object_set_string(event_obj, "notifierName", - parsed_event->notifier.name); - json_object_set_string(event_obj, "notifierURL", parsed_event->notifier.url); - json_object_set_string(event_obj, "notifierVersion", - parsed_event->notifier.version); - char *json_str = json_serialize_to_string(event_val); - auto result = (*env).NewStringUTF(json_str); - free(json_str); - - return result; -} - -JNIEXPORT void JNICALL -Java_com_bugsnag_android_ndk_migrations_EventOnDiskTests_generateAndStoreEvent( - JNIEnv *env, jobject _this, jstring temp_file) { - const char *path = write_event(env, temp_file, create_full_event); - - // file on disk -> latest event type - bugsnag_event *parsed_event = bsg_deserialize_event_from_file((char *)path); - char *output = bsg_serialize_event_to_json_string(parsed_event); - for (int i = 0; i < parsed_event->feature_flag_count; i++) { - free(parsed_event->feature_flags[i].name); - free(parsed_event->feature_flags[i].variant); - } - free(parsed_event->breadcrumbs); - free(parsed_event->feature_flags); - free(parsed_event); - - // latest event type -> temp file - write_str_to_file(output, path); - free(output); -} - -#ifdef __cplusplus -} -#endif diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/utils.hpp b/bugsnag-plugin-android-ndk/src/test/cpp/migrations/utils.hpp deleted file mode 100644 index 38a2c10c38..0000000000 --- a/bugsnag-plugin-android-ndk/src/test/cpp/migrations/utils.hpp +++ /dev/null @@ -1,90 +0,0 @@ -/** Helper functions for writing migration tests */ -#pragma once - -#include -#include -#include -#include - -#include - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -bool bsg_report_header_write(bsg_report_header *header, int fd); - -#ifdef __cplusplus -} -#endif - -static void write_str_to_file(char *contents, const char *path) { - int fd = open(path, O_WRONLY | O_CREAT, 0644); - if (fd == -1) { - return; - } - - write(fd, contents, strlen(contents)); -} - -/** - * Write an event to disk - */ -static bool write_struct_to_file(void *event, int version, size_t expected_length, - const char *path) { - int fd = open(path, O_WRONLY | O_CREAT, 0644); - if (fd == -1) { - return false; - } - - bsg_report_header header = {version, 0, {0}}; - if (!bsg_report_header_write(&header, fd)) { - return false; - } - - ssize_t actual_length = write(fd, event, expected_length); - close(fd); - - return actual_length == expected_length; -} - -static void insert_crumb(bugsnag_breadcrumb *array, int index, const char *name, - bugsnag_breadcrumb_type type, long long timestamp, - const char *meta_str) { - auto crumb = bugsnag_breadcrumb{.type = type}; - strcpy(crumb.name, name); - sprintf(crumb.timestamp, "t%llu", timestamp); - bsg_add_metadata_value_str(&crumb.metadata, "metadata", "message", meta_str); - - memcpy(&(array[index]), &crumb, sizeof(bugsnag_breadcrumb)); -} - -/** - * Writes a JSON file based on a struct generated by a function - * - * @param env JNI Environment for parsing temp file path - * @param event_generator A function returning an allocated event struct - * @param version The version of the event struct from generator - * @param event_length Length of the allocated event struct in bytes - * @param temp_file File path for writing JSON output - */ -static void write_json_for_event(JNIEnv *env, void *(event_generator)(), int version, - int event_length, jstring temp_file) { - const char *path = (*env).GetStringUTFChars(temp_file, nullptr); - - // (old format) event struct -> file on disk - void *old_event = event_generator(); - bool success = write_struct_to_file(old_event, version, event_length, path); - free(old_event); - - // file on disk -> latest event type - bugsnag_event *parsed_event = bsg_deserialize_event_from_file((char *)path); - char *output = bsg_serialize_event_to_json_string(parsed_event); - free(parsed_event); - - // latest event type -> temp file - write_str_to_file(output, path); - free(output); -} diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/test_breadcrumbs.c b/bugsnag-plugin-android-ndk/src/test/cpp/test_breadcrumbs.c index 5f5a9588c8..7973a7a349 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/test_breadcrumbs.c +++ b/bugsnag-plugin-android-ndk/src/test/cpp/test_breadcrumbs.c @@ -1,7 +1,6 @@ #include #include #include -#include bugsnag_breadcrumb *init_breadcrumb(const char *name, const char *message, bugsnag_breadcrumb_type type) { bugsnag_breadcrumb *crumb = calloc(1, sizeof(bugsnag_breadcrumb)); diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/test_bsg_event.c b/bugsnag-plugin-android-ndk/src/test/cpp/test_bsg_event.c index 4dcd44093c..b89a4dc635 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/test_bsg_event.c +++ b/bugsnag-plugin-android-ndk/src/test/cpp/test_bsg_event.c @@ -1,8 +1,7 @@ #include -#include #include #include "../../main/jni/include/bugsnag.h" -#include +#include "test_bsg_event.h" bugsnag_event *init_event() { bugsnag_event *event = calloc(1, sizeof(bugsnag_event)); diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/test_bsg_event.h b/bugsnag-plugin-android-ndk/src/test/cpp/test_bsg_event.h new file mode 100644 index 0000000000..691908547b --- /dev/null +++ b/bugsnag-plugin-android-ndk/src/test/cpp/test_bsg_event.h @@ -0,0 +1,8 @@ +#ifndef BUGSNAG_ANDROID_TEST_BSG_EVENT_H +#define BUGSNAG_ANDROID_TEST_BSG_EVENT_H + +#include + +bugsnag_event *init_event(); + +#endif // BUGSNAG_ANDROID_TEST_BSG_EVENT_H diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c b/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c index cd1835580d..2565b47185 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c +++ b/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c @@ -7,150 +7,13 @@ #include #include -#include #include -#define SERIALIZE_TEST_FILE "/data/data/com.bugsnag.android.ndk.test/cache/foo.crash" - -bugsnag_breadcrumb *init_breadcrumb(const char *name, const char *message, bugsnag_breadcrumb_type type); - -bool bsg_report_header_write(bsg_report_header *header, int fd); - -void generate_basic_report(bugsnag_event *event) { - strcpy(event->grouping_hash, "foo-hash"); - strcpy(event->api_key, "5d1e5fbd39a74caa1200142706a90b20"); - strcpy(event->context, "SomeActivity"); - strcpy(event->error.errorClass, "SIGBUS"); - strcpy(event->error.errorMessage, "POSIX is serious about oncoming traffic"); - event->error.stacktrace[0].frame_address = 454379; - event->error.stacktrace[1].frame_address = 342334; - event->error.frame_count = 2; - strcpy(event->error.type, "C"); - strcpy(event->error.stacktrace[0].method, "makinBacon"); - strcpy(event->app.id, "com.example.PhotoSnapPlus"); - strcpy(event->app.release_stage, "リリース"); - strcpy(event->app.version, "2.0.52"); - event->app.version_code = 57; - strcpy(event->app.build_uuid, "1234-9876-adfe"); - strcpy(event->device.manufacturer, "HI-TEC™"); - strcpy(event->device.model, "Rasseur"); - strcpy(event->device.locale, "en_AU#Melbun"); - strcpy(event->device.os_name, "android"); - strcpy(event->user.email, "fenton@io.example.com"); - strcpy(event->user.id, "fex"); - event->device.total_memory = 234678100; - event->app.duration = 6502; - event->metadata.value_count = 4; - event->metadata.values[0] = (bsg_metadata_value) { - .name = {"weather"}, - .section = {"app"}, - .type = BSG_METADATA_CHAR_VALUE, - .char_value = {"rain"}, - }; - event->metadata.values[1] = (bsg_metadata_value) { - .name = {"experimentX"}, - .section = {"metrics"}, - .type = BSG_METADATA_BOOL_VALUE, - .bool_value = false, - }; - event->metadata.values[2] = (bsg_metadata_value) { - .name = {"subject"}, - .section = {"metrics"}, - .type = BSG_METADATA_CHAR_VALUE, - .char_value = {"percy"}, - }; - event->metadata.values[3] = (bsg_metadata_value) { - .name = {"counter"}, - .section = {"metrics"}, - .type = BSG_METADATA_NUMBER_VALUE, - .double_value = 47.8, - }; - - event->crumb_count = 0; - event->crumb_first_index = 0; - bugsnag_breadcrumb *crumb1 = init_breadcrumb("decrease torque", "Moving laterally 26º", BSG_CRUMB_STATE); - bsg_event_add_breadcrumb(event, crumb1); - - bugsnag_breadcrumb *crumb2 = init_breadcrumb("enable blasters", "this is a drill.", BSG_CRUMB_USER); - bsg_event_add_breadcrumb(event, crumb2); - - event->handled_events = 1; - event->unhandled_events = 1; - strcpy(event->session_id, "f1ab"); - strcpy(event->session_start, "2019-03-19T12:58:19+00:00"); - - strcpy(event->notifier.version, "1.0"); - strcpy(event->notifier.url, "bugsnag.com"); - strcpy(event->notifier.name, "Test Notifier"); -} - -bugsnag_event *bsg_generate_event(void) { - bugsnag_event *report = calloc(1, sizeof(bugsnag_event)); - report->max_crumb_count = 50; - report->breadcrumbs = - calloc(report->max_crumb_count, sizeof(bugsnag_breadcrumb)); - strcpy(report->grouping_hash, "foo-hash"); - strcpy(report->api_key, "5d1e5fbd39a74caa1200142706a90b20"); - strcpy(report->context, "SomeActivity"); - strcpy(report->error.errorClass, "SIGBUS"); - strcpy(report->error.errorMessage, "POSIX is serious about oncoming traffic"); - report->error.stacktrace[0].frame_address = 454379; - report->error.stacktrace[1].frame_address = 342334; - report->error.frame_count = 2; - strcpy(report->error.type, "C"); - strcpy(report->error.stacktrace[0].method, "makinBacon"); - strcpy(report->app.id, "com.example.PhotoSnapPlus"); - strcpy(report->app.release_stage, "リリース"); - strcpy(report->app.version, "2.0.52"); - report->app.version_code = 57; - strcpy(report->app.build_uuid, "1234-9876-adfe"); - strcpy(report->device.manufacturer, "HI-TEC™"); - strcpy(report->device.model, "Rasseur"); - strcpy(report->device.locale, "en_AU#Melbun"); - strcpy(report->device.os_name, "android"); - strcpy(report->user.email, "fenton@io.example.com"); - strcpy(report->user.id, "fex"); - report->device.total_memory = 234678100; - report->app.duration = 6502; - bugsnag_event_add_metadata_bool(report, "metrics", "experimentX", false); - bugsnag_event_add_metadata_string(report, "metrics", "subject", "percy"); - bugsnag_event_add_metadata_string(report, "app", "weather", "rain"); - bugsnag_event_add_metadata_double(report, "metrics", "counter", 47.8); - - report->crumb_count = 0; - report->crumb_first_index = 0; - bugsnag_breadcrumb *crumb1 = init_breadcrumb("decrease torque", "Moving laterally 26º", - BSG_CRUMB_STATE); - bsg_event_add_breadcrumb(report, crumb1); - - bugsnag_breadcrumb *crumb2 = init_breadcrumb("enable blasters", "this is a drill.", - BSG_CRUMB_USER); - bsg_event_add_breadcrumb(report, crumb2); - - report->handled_events = 1; - report->unhandled_events = 1; - strcpy(report->session_id, "f1ab"); - strcpy(report->session_start, "2019-03-19T12:58:19+00:00"); - - strcpy(report->notifier.version, "1.0"); - strcpy(report->notifier.url, "bugsnag.com"); - strcpy(report->notifier.name, "Test Notifier"); - report->unhandled_events = 2; - return report; -} - +#define SERIALIZE_TEST_FILE "/data/data/com.bugsnag.android.ndk.test/cache/" void bsg_update_next_run_info(bsg_environment *env); -char *test_read_last_run_info(const bsg_environment *env) { - int fd = open(SERIALIZE_TEST_FILE, O_RDONLY); - size_t size = sizeof(env->next_last_run_info); - char *buf = calloc(1, size); - read(fd, buf, size); - return buf; -} - TEST test_last_run_info_serialization(void) { bsg_environment *env = calloc(1, sizeof(bsg_environment)); strcpy(env->last_run_info_path, SERIALIZE_TEST_FILE); @@ -171,252 +34,6 @@ TEST test_last_run_info_serialization(void) { PASS(); } -TEST test_report_to_file(void) { - bsg_environment *env = calloc(1, sizeof(bsg_environment)); - env->report_header.version = 7; - env->report_header.big_endian = 1; - bugsnag_event *report = bsg_generate_event(); - memcpy(&env->next_event, report, sizeof(bugsnag_event)); - strcpy(env->report_header.os_build, "macOS Sierra"); - strcpy(env->next_event_path, SERIALIZE_TEST_FILE); - ASSERT(bsg_serialize_event_to_file(env)); - free(report->breadcrumbs); - free(report); - free(env); - PASS(); -} - -TEST test_report_with_feature_flags_to_file(void) { - bsg_environment *env = calloc(1, sizeof(bsg_environment)); - env->report_header.version = BSG_MIGRATOR_CURRENT_VERSION; - env->report_header.big_endian = 1; - bugsnag_event *report = bsg_generate_event(); - memcpy(&env->next_event, report, sizeof(bugsnag_event)); - bsg_set_feature_flag(&env->next_event, "sample_group", "a"); - bsg_set_feature_flag(&env->next_event, "demo_mode", NULL); - strcpy(env->report_header.os_build, "macOS Sierra"); - strcpy(env->next_event_path, SERIALIZE_TEST_FILE); - ASSERT(bsg_serialize_event_to_file(env)); - free(report->breadcrumbs); - free(report); - free(env); - PASS(); -} - -TEST test_file_to_report(void) { - bsg_environment *env = calloc(1, sizeof(bsg_environment)); - env->report_header.version = BSG_MIGRATOR_CURRENT_VERSION; - env->report_header.big_endian = 1; - strcpy(env->report_header.os_build, "macOS Sierra"); - bugsnag_event *generated_report = bsg_generate_event(); - memcpy(&env->next_event, generated_report, sizeof(bugsnag_event)); - strcpy(env->next_event_path, SERIALIZE_TEST_FILE); - bsg_serialize_event_to_file(env); - - bugsnag_event *report = bsg_deserialize_event_from_file(SERIALIZE_TEST_FILE); - ASSERT(report != NULL); - ASSERT(strcmp("SIGBUS", report->error.errorClass) == 0); - ASSERT(strcmp("POSIX is serious about oncoming traffic", report->error.errorMessage) == 0); - free(generated_report->breadcrumbs); - free(generated_report); - free(env); - free(report); - PASS(); -} - -TEST test_report_with_feature_flags_from_file(void) { - bsg_environment *env = calloc(1, sizeof(bsg_environment)); - env->report_header.version = BSG_MIGRATOR_CURRENT_VERSION; - env->report_header.big_endian = 1; - bugsnag_event *report = bsg_generate_event(); - memcpy(&env->next_event, report, sizeof(bugsnag_event)); - bsg_set_feature_flag(&env->next_event, "sample_group", "a"); - bsg_set_feature_flag(&env->next_event, "demo_mode", NULL); - strcpy(env->report_header.os_build, "macOS Sierra"); - strcpy(env->next_event_path, "/data/data/com.bugsnag.android.ndk.test/cache/features.crash"); - ASSERT(bsg_serialize_event_to_file(env)); - - bugsnag_event *event = bsg_deserialize_event_from_file("/data/data/com.bugsnag.android.ndk.test/cache/features.crash"); - - ASSERT_EQ(2, event->feature_flag_count); - - free(report->breadcrumbs); - free(report); - free(env); - free(event); - PASS(); -} - -TEST test_report_with_opaque_metadata_from_file(void) { - bsg_environment *env = calloc(1, sizeof(bsg_environment)); - env->report_header.version = BSG_MIGRATOR_CURRENT_VERSION; - env->report_header.big_endian = 1; - bugsnag_event *report = bsg_generate_event(); - memcpy(&env->next_event, report, sizeof(bugsnag_event)); - bsg_add_metadata_value_opaque(&env->next_event.metadata, "opaque", "map", "{\"user\": \"Bobby Tables\"}"); - bsg_add_metadata_value_opaque(&env->next_event.metadata, "opaque", "list", "[1,2,3,4]"); - strcpy(env->report_header.os_build, "macOS Sierra"); - strcpy(env->next_event_path, "/data/data/com.bugsnag.android.ndk.test/cache/features.crash"); - ASSERT(bsg_serialize_event_to_file(env)); - - bugsnag_event *event = bsg_deserialize_event_from_file("/data/data/com.bugsnag.android.ndk.test/cache/features.crash"); - - ASSERT_EQ(6, event->metadata.value_count); - - ASSERT_EQ(BSG_METADATA_OPAQUE_VALUE, bugsnag_event_has_metadata(event, "opaque", "map")); - ASSERT_EQ(BSG_METADATA_OPAQUE_VALUE, bugsnag_event_has_metadata(event, "opaque", "list")); - - free(report->breadcrumbs); - free(report); - free(env); - free(event); - PASS(); -} - -// helper function -JSON_Value *bsg_generate_json(void) { - bugsnag_event *event = bsg_generate_event(); - char *json = bsg_serialize_event_to_json_string(event); - JSON_Value *root_value = json_parse_string(json); - free(json); - free(event->breadcrumbs); - free(event); - return root_value; -} - -TEST test_app_info_to_json(void) { - JSON_Value *root_value = bsg_generate_json(); - JSON_Object *event = json_value_get_object(root_value); - ASSERT(strcmp("2.0.52", json_object_dotget_string(event, "app.version")) == 0); - ASSERT(strcmp( "リリース", json_object_dotget_string(event, "app.releaseStage")) == 0); - ASSERT_EQ(57, json_object_dotget_number(event, "app.versionCode")); - ASSERT(strcmp( "1234-9876-adfe", json_object_dotget_string(event, "app.buildUUID")) == 0); - json_value_free(root_value); - PASS(); -} - -TEST test_session_handled_counts(void) { - JSON_Value *root_value = bsg_generate_json(); - JSON_Object *event = json_value_get_object(root_value); - ASSERT(strcmp("f1ab", json_object_dotget_string(event, "session.id")) == 0); - ASSERT(strcmp("2019-03-19T12:58:19+00:00", json_object_dotget_string(event, "session.startedAt")) == 0); - ASSERT_EQ(1, json_object_dotget_number(event, "session.events.handled")); - ASSERT_EQ(2, json_object_dotget_number(event, "session.events.unhandled")); - PASS(); -} - -TEST test_grouping_hash_to_json(void) { - JSON_Value *root_value = bsg_generate_json(); - JSON_Object *event = json_value_get_object(root_value); - ASSERT(event != NULL); - ASSERT_STR_EQ("foo-hash", json_object_get_string(event, "groupingHash")); - PASS(); -} - -TEST test_context_to_json(void) { - JSON_Value *root_value = bsg_generate_json(); - JSON_Object *event = json_value_get_object(root_value); - ASSERT(event != NULL); - ASSERT(strcmp( "SomeActivity", json_object_get_string(event, "context")) == 0); - PASS(); -} - -TEST test_device_info_to_json(void) { - JSON_Value *root_value = bsg_generate_json(); - JSON_Object *event = json_value_get_object(root_value); - ASSERT(event != NULL); - ASSERT(strcmp( "HI-TEC™", json_object_dotget_string(event, "device.manufacturer")) == 0); - ASSERT(strcmp( "Rasseur", json_object_dotget_string(event, "device.model")) == 0); - ASSERT(strcmp( "en_AU#Melbun", json_object_dotget_string(event, "device.locale")) == 0); - json_value_free(root_value); - PASS(); -} - -TEST test_user_info_to_json(void) { - JSON_Value *root_value = bsg_generate_json(); - JSON_Object *event = json_value_get_object(root_value); - ASSERT(event != NULL); - ASSERT(strcmp( "fex", json_object_dotget_string(event, "user.id")) == 0); - ASSERT(strcmp( "fenton@io.example.com", json_object_dotget_string(event, "user.email")) == 0); - ASSERT(json_object_dotget_string(event, "user.name") == NULL); - json_value_free(root_value); - PASS(); -} - -TEST test_custom_info_to_json(void) { - JSON_Value *root_value = bsg_generate_json(); - JSON_Object *event = json_value_get_object(root_value); - ASSERT(event != NULL); - ASSERT(strcmp( "percy", json_object_dotget_string(event, "metaData.metrics.subject")) == 0); - ASSERT(strcmp( "rain", json_object_dotget_string(event, "metaData.app.weather")) == 0); - ASSERT(json_object_dotget_boolean(event, "metaData.app.experimentX") == -1); - ASSERT(json_object_dotget_number(event, "metaData.app.counter") - 47.8 < 0.01); - json_value_free(root_value); - PASS(); -} - -TEST test_exception_to_json(void) { - JSON_Value *root_value = bsg_generate_json(); - JSON_Object *event = json_value_get_object(root_value); - ASSERT(event != NULL); - JSON_Array *exceptions = json_object_get_array(event, "exceptions"); - ASSERT(exceptions != NULL); - JSON_Object *exception = json_array_get_object(exceptions, 0); - ASSERT(exception != NULL); - ASSERT(strcmp("SIGBUS", json_object_get_string(exception, "errorClass")) == 0); - ASSERT(strcmp("POSIX is serious about oncoming traffic", json_object_get_string(exception, "message")) == 0); - ASSERT(strcmp("c", json_object_get_string(exception, "type")) == 0); - JSON_Array *stacktrace = json_object_get_array(exception, "stacktrace"); - ASSERT(stacktrace != NULL); - ASSERT_EQ(2, json_array_get_count(stacktrace)); - ASSERT(strcmp("makinBacon", json_object_get_string(json_array_get_object(stacktrace, 0), "method")) == 0); - ASSERT_STR_EQ("0x6eeeb", json_object_get_string(json_array_get_object(stacktrace, 0), "frameAddress")); - ASSERT(strcmp("0x5393e", json_object_get_string(json_array_get_object(stacktrace, 1), "method")) == 0); - ASSERT_STR_EQ("0x5393e", json_object_get_string(json_array_get_object(stacktrace, 1), "frameAddress")); - json_value_free(root_value); - PASS(); -} - -TEST test_breadcrumbs_to_json(void) { - JSON_Value *root_value = bsg_generate_json(); - JSON_Object *event = json_value_get_object(root_value); - ASSERT(event != NULL); - JSON_Array *breadcrumbs = json_object_get_array(event, "breadcrumbs"); - ASSERT(breadcrumbs != NULL); - ASSERT_EQ(2, json_array_get_count(breadcrumbs)); - - JSON_Object *crumb1 = json_array_get_object(breadcrumbs, 0); - ASSERT_STR_EQ("decrease torque", json_object_get_string(crumb1, "name")); - ASSERT_STR_EQ("state", json_object_get_string(crumb1, "type")); - ASSERT_EQ(1, json_object_get_count(json_object_get_object(crumb1, "metaData"))); - ASSERT_STR_EQ("Moving laterally 26º", json_object_get_string(json_object_get_object(crumb1, "metaData"), "message")); - - JSON_Object *crumb2 = json_array_get_object(breadcrumbs, 1); - ASSERT_STR_EQ("enable blasters", json_object_get_string(crumb2, "name")); - ASSERT_STR_EQ("user", json_object_get_string(crumb2, "type")); - ASSERT_EQ(1, json_object_get_count(json_object_get_object(crumb2, "metaData"))); - ASSERT_STR_EQ("this is a drill.", json_object_get_string(json_object_get_object(crumb2, "metaData"), "message")); - PASS(); -} - - SUITE(suite_json_serialization) { RUN_TEST(test_last_run_info_serialization); - RUN_TEST(test_session_handled_counts); - RUN_TEST(test_context_to_json); - RUN_TEST(test_grouping_hash_to_json); - RUN_TEST(test_app_info_to_json); - RUN_TEST(test_device_info_to_json); - RUN_TEST(test_user_info_to_json); - RUN_TEST(test_custom_info_to_json); - RUN_TEST(test_exception_to_json); - RUN_TEST(test_breadcrumbs_to_json); -} - -SUITE(suite_struct_to_file) { - RUN_TEST(test_report_to_file); - RUN_TEST(test_file_to_report); - RUN_TEST(test_report_with_feature_flags_to_file); - RUN_TEST(test_report_with_feature_flags_from_file); - RUN_TEST(test_report_with_opaque_metadata_from_file); } diff --git a/examples/sdk-app-example/app/CMakeLists.txt b/examples/sdk-app-example/app/CMakeLists.txt index c803a1ddd6..f9aa3ccd01 100644 --- a/examples/sdk-app-example/app/CMakeLists.txt +++ b/examples/sdk-app-example/app/CMakeLists.txt @@ -5,5 +5,14 @@ find_package(bugsnag-plugin-android-ndk REQUIRED CONFIG) add_library(entrypoint SHARED src/main/cpp/entrypoint.cpp) +set(EXTRA_LINK_FLAGS "-Wl,-z,max-page-size=16384") + target_include_directories(entrypoint PRIVATE ${BUGSNAG_INCLUDE_DIR}) target_link_libraries(entrypoint bugsnag-plugin-android-ndk::bugsnag-ndk) + +set_target_properties( + entrypoint + PROPERTIES + COMPILE_OPTIONS -Werror -Wall -pedantic + LINK_FLAGS "${EXTRA_LINK_FLAGS}" +) diff --git a/examples/sdk-app-example/app/build.gradle b/examples/sdk-app-example/app/build.gradle index 28a225a86b..5def256fa5 100644 --- a/examples/sdk-app-example/app/build.gradle +++ b/examples/sdk-app-example/app/build.gradle @@ -42,8 +42,8 @@ android { } dependencies { - implementation "com.bugsnag:bugsnag-android:6.5.0" - implementation "com.bugsnag:bugsnag-plugin-android-okhttp:6.5.0" + implementation "com.bugsnag:bugsnag-android:6.6.0" + implementation "com.bugsnag:bugsnag-plugin-android-okhttp:6.6.0" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "androidx.appcompat:appcompat:1.6.1" implementation "com.google.android.material:material:1.11.0" diff --git a/features/fixtures/mazerunner/app/build.gradle b/features/fixtures/mazerunner/app/build.gradle index aab4b30864..2534707394 100644 --- a/features/fixtures/mazerunner/app/build.gradle +++ b/features/fixtures/mazerunner/app/build.gradle @@ -9,7 +9,7 @@ android { defaultConfig { minSdkVersion 17 - targetSdkVersion 31 + targetSdkVersion 33 versionCode 34 versionName "1.1.14" manifestPlaceholders = [ @@ -79,6 +79,7 @@ android { kotlinOptions { jvmTarget = "1.8" } + namespace 'com.bugsnag.android.mazerunner' } dependencies { diff --git a/features/fixtures/mazerunner/app/detekt-baseline.xml b/features/fixtures/mazerunner/app/detekt-baseline.xml index a708a443f8..c54e54cfd0 100644 --- a/features/fixtures/mazerunner/app/detekt-baseline.xml +++ b/features/fixtures/mazerunner/app/detekt-baseline.xml @@ -1,7 +1,8 @@ - + - + + ComplexMethod:MainActivity.kt$MainActivity$// Starts a thread to poll for Maze Runner actions to perform private fun startCommandRunner() MagicNumber:MainActivity.kt$MainActivity$1000 MagicNumber:MainActivity.kt$MainActivity$250 SwallowedException:NetworkStatus.kt$catch (e: Exception) { NetworkStatus.NO_INTERNET } diff --git a/features/fixtures/mazerunner/app/proguard-rules.pro b/features/fixtures/mazerunner/app/proguard-rules.pro index ba642cad1e..759bdb97ed 100644 --- a/features/fixtures/mazerunner/app/proguard-rules.pro +++ b/features/fixtures/mazerunner/app/proguard-rules.pro @@ -1,2 +1,4 @@ -keep class com.bugsnag.android.mazerunner.** {*;} -keep class com.bugsnag.android.DeliveryDelegate {*;} +-keepattributes LineNumberTable,SourceFile +-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/features/fixtures/mazerunner/app/src/main/AndroidManifest.xml b/features/fixtures/mazerunner/app/src/main/AndroidManifest.xml index d0439114c9..a152aba337 100644 --- a/features/fixtures/mazerunner/app/src/main/AndroidManifest.xml +++ b/features/fixtures/mazerunner/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + diff --git a/features/fixtures/mazerunner/build.gradle b/features/fixtures/mazerunner/build.gradle index 09adc72ba8..0076b5b5f7 100644 --- a/features/fixtures/mazerunner/build.gradle +++ b/features/fixtures/mazerunner/build.gradle @@ -18,16 +18,16 @@ buildscript { mavenCentral() maven { url "https://plugins.gradle.org/m2/" } } - ext.kotlin_version = "1.4.32" + ext.kotlin_version = "1.8.20" dependencies { def agpVersion = project.hasProperty("USE_AGP_VERSION") ? project.property("USE_AGP_VERSION") - : "7.1.0" + : "8.3.2" project.logger.lifecycle("Using AGP $agpVersion") classpath "com.android.tools.build:gradle:$agpVersion" - classpath "com.bugsnag:bugsnag-android-gradle-plugin:7.1.0" + classpath "com.bugsnag:bugsnag-android-gradle-plugin:8.1.0" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.18.1" classpath "org.jlleitschuh.gradle:ktlint-gradle:10.2.0" diff --git a/features/fixtures/mazerunner/cxx-scenarios-bugsnag/build.gradle b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/build.gradle index 3d2486d0c1..1cc184d0e3 100644 --- a/features/fixtures/mazerunner/cxx-scenarios-bugsnag/build.gradle +++ b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/build.gradle @@ -37,6 +37,7 @@ android { buildFeatures.prefab = true packagingOptions.jniLibs.pickFirsts += ["**/libbugsnag-ndk.so"] + namespace 'com.bugsnag.android.mazerunner.cxxscenariosbugsnag' } dependencies { diff --git a/features/fixtures/mazerunner/cxx-scenarios-bugsnag/detekt-baseline.xml b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/detekt-baseline.xml index 1c1387a874..a4f1610554 100644 --- a/features/fixtures/mazerunner/cxx-scenarios-bugsnag/detekt-baseline.xml +++ b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/detekt-baseline.xml @@ -1,6 +1,6 @@ - + - + MagicNumber:CXXExceptionSmokeScenario.kt$CXXExceptionSmokeScenario$500 MagicNumber:CXXExceptionSmokeScenario.kt$CXXExceptionSmokeScenario$999 diff --git a/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/AndroidManifest.xml b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/AndroidManifest.xml index 855e977fee..8072ee00db 100644 --- a/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/AndroidManifest.xml +++ b/features/fixtures/mazerunner/cxx-scenarios-bugsnag/src/main/AndroidManifest.xml @@ -1,2 +1,2 @@ - + diff --git a/features/fixtures/mazerunner/cxx-scenarios/build.gradle b/features/fixtures/mazerunner/cxx-scenarios/build.gradle index ca3600068b..a9723680de 100644 --- a/features/fixtures/mazerunner/cxx-scenarios/build.gradle +++ b/features/fixtures/mazerunner/cxx-scenarios/build.gradle @@ -34,6 +34,7 @@ android { path "CMakeLists.txt" } } + namespace 'com.bugsnag.android.mazerunner.cxxscenarios' } dependencies { diff --git a/features/fixtures/mazerunner/cxx-scenarios/detekt-baseline.xml b/features/fixtures/mazerunner/cxx-scenarios/detekt-baseline.xml index 104065a6ca..63419479af 100644 --- a/features/fixtures/mazerunner/cxx-scenarios/detekt-baseline.xml +++ b/features/fixtures/mazerunner/cxx-scenarios/detekt-baseline.xml @@ -1,6 +1,6 @@ - + - + MagicNumber:CXXDelayedCrashScenario.kt$CXXDelayedCrashScenario$405 MagicNumber:CXXIgnoredSigabrtScenario.kt$CXXIgnoredSigabrtScenario$2726 diff --git a/features/fixtures/mazerunner/cxx-scenarios/src/main/AndroidManifest.xml b/features/fixtures/mazerunner/cxx-scenarios/src/main/AndroidManifest.xml index bdb6ed5246..8072ee00db 100644 --- a/features/fixtures/mazerunner/cxx-scenarios/src/main/AndroidManifest.xml +++ b/features/fixtures/mazerunner/cxx-scenarios/src/main/AndroidManifest.xml @@ -1,2 +1,2 @@ - + diff --git a/features/fixtures/mazerunner/cxx-scenarios/src/main/cpp/CXXStackoverflowScenario.cpp b/features/fixtures/mazerunner/cxx-scenarios/src/main/cpp/CXXStackoverflowScenario.cpp index fbe5fce359..1a4b7ddd65 100644 --- a/features/fixtures/mazerunner/cxx-scenarios/src/main/cpp/CXXStackoverflowScenario.cpp +++ b/features/fixtures/mazerunner/cxx-scenarios/src/main/cpp/CXXStackoverflowScenario.cpp @@ -5,8 +5,13 @@ extern "C" { int __attribute__((optnone)) __attribute__((noinline)) crash_stack_overflow(int counter, char *input) { char stack[7]; + char *output = stack; - strcpy(stack, input); + while (*input) { + *output = *input; + input++; + output++; + } return 4 / counter; } diff --git a/features/fixtures/mazerunner/gradle/wrapper/gradle-wrapper.properties b/features/fixtures/mazerunner/gradle/wrapper/gradle-wrapper.properties index d2880ba800..e411586a54 100644 --- a/features/fixtures/mazerunner/gradle/wrapper/gradle-wrapper.properties +++ b/features/fixtures/mazerunner/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/features/fixtures/mazerunner/jvm-scenarios/build.gradle b/features/fixtures/mazerunner/jvm-scenarios/build.gradle index c14c318d52..5ac7256c16 100644 --- a/features/fixtures/mazerunner/jvm-scenarios/build.gradle +++ b/features/fixtures/mazerunner/jvm-scenarios/build.gradle @@ -32,6 +32,7 @@ android { lintOptions { tasks.lint.enabled = false } + namespace 'com.bugsnag.android.mazerunner.jvmscenarios' } dependencies { @@ -42,7 +43,7 @@ dependencies { implementation "com.squareup.okhttp3:okhttp:3.12.0" } else { project.logger.lifecycle("Using OkHttp 4 dependency in test fixture") - implementation "com.squareup.okhttp3:okhttp:4.9.1" + implementation "com.squareup.okhttp3:okhttp:4.12.0" } implementation "com.bugsnag:bugsnag-android-performance:1.2.2" } diff --git a/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml b/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml index d18bb02cbd..cbe2ac2438 100644 --- a/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml +++ b/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml @@ -1,9 +1,9 @@ - + - + MagicNumber:AnrHelper.kt$1000 - MagicNumber:AnrHelper.kt$<no name provided>$60000 + MagicNumber:AnrHelper.kt$<no name provided>$60000 MagicNumber:AutoDetectAnrsFalseScenario.kt$AutoDetectAnrsFalseScenario$100000 MagicNumber:AutoDetectAnrsTrueScenario.kt$AutoDetectAnrsTrueScenario$100000 MagicNumber:BugsnagInitScenario.kt$BugsnagInitScenario$25 @@ -22,11 +22,11 @@ MagicNumber:Scenario.kt$Scenario$100 MagicNumber:Scenario.kt$Scenario$1000 MagicNumber:StartupCrashFlushScenario.kt$StartupCrashFlushScenario$6000 - MagicNumber:TestHarnessHooks.kt$<no name provided>$500 + MagicNumber:TestHarnessHooks.kt$<no name provided>$500 MagicNumber:TrimmedStacktraceScenario.kt$TrimmedStacktraceScenario$100000 MagicNumber:UnhandledExceptionEventDetailChangeScenario.kt$UnhandledExceptionEventDetailChangeScenario$123 MagicNumber:UnhandledExceptionEventDetailChangeScenario.kt$UnhandledExceptionEventDetailChangeScenario$123456 - ThrowingExceptionsWithoutMessageOrCause:AnrHelper.kt$<no name provided>$IllegalStateException() + ThrowingExceptionsWithoutMessageOrCause:AnrHelper.kt$<no name provided>$IllegalStateException() ThrowingExceptionsWithoutMessageOrCause:BugsnagInitScenario.kt$BugsnagInitScenario$RuntimeException() ThrowingExceptionsWithoutMessageOrCause:CustomPluginNotifierDescriptionScenario.kt$CustomPluginNotifierDescriptionScenario$RuntimeException() ThrowingExceptionsWithoutMessageOrCause:TestHarnessHooks.kt$RuntimeException() diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/AndroidManifest.xml b/features/fixtures/mazerunner/jvm-scenarios/src/main/AndroidManifest.xml index 15312f0fe5..8072ee00db 100644 --- a/features/fixtures/mazerunner/jvm-scenarios/src/main/AndroidManifest.xml +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/AndroidManifest.xml @@ -1,2 +1,2 @@ - + diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/UnhandledExceptionEventDetailChangeScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/UnhandledExceptionEventDetailChangeScenario.kt index fdacbf4c3e..2cac1f5885 100644 --- a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/UnhandledExceptionEventDetailChangeScenario.kt +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/UnhandledExceptionEventDetailChangeScenario.kt @@ -17,7 +17,6 @@ internal class UnhandledExceptionEventDetailChangeScenario( ) : Scenario(config, context, eventMetadata) { init { - config.addOnError( OnErrorCallback { event -> event.apiKey = "0000111122223333aaaabbbbcccc9999" @@ -75,12 +74,18 @@ internal class UnhandledExceptionEventDetailChangeScenario( Bugsnag.addMetadata("custom_data1", "data", "hello") Bugsnag.addMetadata("custom_data2", "data", "hello") - Bugsnag.addMetadata("custom_data3", "test data", "divert all available power to the crash reporter") - + Bugsnag.addMetadata( + "custom_data3", + "test data", + "divert all available power to the crash reporter" + ) Bugsnag.addFeatureFlag("test1") Bugsnag.addFeatureFlag("test2") - Bugsnag.notify(RuntimeException("UnhandledExceptionEventDetailChangeScenario")) - throw NullPointerException("something broke") + if (eventMetadata == "notify") { + Bugsnag.notify(RuntimeException("UnhandledExceptionEventDetailChangeScenario")) + } else { + throw NullPointerException("something broke") + } } } diff --git a/features/full_tests/detect_anr_jvm.feature b/features/full_tests/detect_anr_jvm.feature index c65be424c6..e80fdfbc16 100644 --- a/features/full_tests/detect_anr_jvm.feature +++ b/features/full_tests/detect_anr_jvm.feature @@ -41,8 +41,7 @@ Feature: ANRs triggered in JVM code are captured @skip_android_10 Scenario: ANR triggered in JVM code is not captured when detectAnrs = false When I run "JvmAnrDisabledScenario" - And I wait for 2 seconds - And I tap the screen 3 times + # No screen taps for this scenario as it seems to break Appium with Android 8 Then I wait to receive an error And the error is valid for the error reporting API version "4.0" for the "Android Bugsnag Notifier" notifier And the error payload field "events" is an array with 1 elements diff --git a/features/full_tests/error_callback_alters_fields.feature b/features/full_tests/error_callback_alters_fields.feature index 2748be9376..d851efd911 100644 --- a/features/full_tests/error_callback_alters_fields.feature +++ b/features/full_tests/error_callback_alters_fields.feature @@ -3,11 +3,67 @@ Feature: When the api key is altered in an Event the JSON payload reflects this Background: Given I clear all persistent data - Scenario: Unhandled exception with altered event details - When I run "UnhandledExceptionEventDetailChangeScenario" and relaunch the crashed app + Scenario: Crash exception with altered event details + When I configure the app to run in the "crash" state + And I run "UnhandledExceptionEventDetailChangeScenario" and relaunch the crashed app And I configure Bugsnag for "UnhandledExceptionApiKeyChangeScenario" And I wait to receive an error And the error payload field "events" is an array with 1 elements + And the exception "message" equals "something broke" + And the error payload field "apiKey" equals "0000111122223333aaaabbbbcccc9999" + And the error "Bugsnag-Api-Key" header equals "0000111122223333aaaabbbbcccc9999" + And the event "severity" equals "error" + And the event "context" equals "new-context" + And the event "groupingHash" equals "groupingHash1" + And the event "user.id" equals "abc" + And the event "user.email" equals "joe@test.com" + And the event "user.name" equals "Joe" + And the event "metaData.custom_data2.test_data" equals "this is test" + And the event "metaData.custom_data1" is null + And the event "metaData.custom_data2.data" is null + And the event "metaData.custom_data3.test data" equals "divert all available power to the crash reporter" + And event 0 contains the feature flag "beta" with variant "b" + And event 0 does not contain the feature flag "alpha" + And event 0 contains the feature flag "gamma" with no variant + And event 0 does not contain the feature flag "test1" + And event 0 contains the feature flag "test2" with no variant + + # app fields + And the event "unhandled" is false + And the event "app.binaryArch" equals "x86" + And the event "app.id" equals "12345" + And the event "app.releaseStage" equals "custom" + And the event "app.version" equals "1.2.3" + And the event "app.buildUUID" equals "12345678" + And the event "app.type" equals "android_custom" + And the event "app.versionCode" equals 123 + And the event "app.duration" equals 123456 + And the event "app.durationInForeground" equals 123456 + And the event "app.inForeground" is false + And the event "app.isLaunching" is false + + # device fields + And the event "device.id" equals "12345" + And the event "device.jailbroken" is true + And the event "device.locale" equals "en-UK" + And the event "device.totalMemory" equals 123456 + And the event "device.runtimeVersions.androidApiLevel" equals "30" + And the event "device.freeDisk" equals 123456 + And the event "device.freeMemory" equals 123456 + And the event "device.orientation" equals "portrait" + + # breadcrumbs fields + And the event "breadcrumbs.0.type" equals "error" + And the event "breadcrumbs.0.name" equals "new breadcrumb message" + And the event "breadcrumbs.0.metaData.foo" equals "data" + And the event "breadcrumbs.1.type" equals "error" + And the event "breadcrumbs.1.name" equals "Second breadcrumb message" + + Scenario: Unhandled exception with altered event details + When I configure the app to run in the "notify" state + And I run "UnhandledExceptionEventDetailChangeScenario" + And I wait to receive an error + And the error payload field "events" is an array with 1 elements And the exception "message" equals "UnhandledExceptionEventDetailChangeScenario" And the error payload field "apiKey" equals "0000111122223333aaaabbbbcccc9999" And the error "Bugsnag-Api-Key" header equals "0000111122223333aaaabbbbcccc9999" diff --git a/features/full_tests/multi_process.feature b/features/full_tests/multi_process.feature index d4995d3077..aa8e178a70 100644 --- a/features/full_tests/multi_process.feature +++ b/features/full_tests/multi_process.feature @@ -32,6 +32,8 @@ Feature: Reporting errors in multi process apps And the error payload field "events.0.user.name" equals "MultiProcessHandledExceptionScenario" And the error payload field "events.0.user.email" equals "foreground@example.com" + # Skipped pending PLAT-12145 + @skip Scenario: Unhandled JVM error And I configure the app to run in the "main-activity" state When I run "MultiProcessUnhandledExceptionScenario" and relaunch the crashed app diff --git a/features/smoke_tests/02_handled.feature b/features/smoke_tests/02_handled.feature index f14f3ec91a..5f2d5d7a51 100644 --- a/features/smoke_tests/02_handled.feature +++ b/features/smoke_tests/02_handled.feature @@ -22,9 +22,9 @@ Feature: Handled smoke tests # Stacktrace validation And the error payload field "events.0.exceptions.0.stacktrace" is a non-empty array And the event "exceptions.0.stacktrace.0.method" ends with "HandledJavaSmokeScenario.startScenario" - And the exception "stacktrace.0.file" equals "HandledJavaSmokeScenario.java" + And the exception "stacktrace.0.file" equals "SourceFile" # R8 minification alters the lineNumber, see the mapping file/source code for the original value - And the event "exceptions.0.stacktrace.0.lineNumber" equals 8 + And the event "exceptions.0.stacktrace.0.lineNumber" equals 56 And the event "exceptions.0.stacktrace.0.inProject" is true And the error payload field "events.0.projectPackages" is a non-empty array And the event "projectPackages.0" equals "com.bugsnag.android.mazerunner" @@ -133,9 +133,9 @@ Feature: Handled smoke tests # Stacktrace validation And the error payload field "events.0.exceptions.0.stacktrace" is a non-empty array And the event "exceptions.0.stacktrace.0.method" ends with "generateException" - And the exception "stacktrace.0.file" equals "Scenario.kt" + And the exception "stacktrace.0.file" equals "SourceFile" # R8 minification alters the lineNumber, see the mapping file/source code for the original value - And the event "exceptions.0.stacktrace.0.lineNumber" equals 1 + And the event "exceptions.0.stacktrace.0.lineNumber" equals 11 And the event "exceptions.0.stacktrace.0.inProject" is true # Overwritten App data diff --git a/features/smoke_tests/04_unhandled.feature b/features/smoke_tests/04_unhandled.feature index 31d183d5c1..d25c411172 100644 --- a/features/smoke_tests/04_unhandled.feature +++ b/features/smoke_tests/04_unhandled.feature @@ -25,9 +25,9 @@ Feature: Unhandled smoke tests # Stacktrace validation And the error payload field "events.0.exceptions.0.stacktrace" is a non-empty array And the event "exceptions.0.stacktrace.0.method" ends with "UnhandledJavaLoadedConfigScenario.startScenario" - And the exception "stacktrace.0.file" equals "UnhandledJavaLoadedConfigScenario.java" + And the exception "stacktrace.0.file" equals "SourceFile" # R8 minification alters the lineNumber, see the mapping file/source code for the original value - And the event "exceptions.0.stacktrace.0.lineNumber" equals 7 + And the event "exceptions.0.stacktrace.0.lineNumber" equals 41 And the event "exceptions.0.stacktrace.0.inProject" is true And the thread with name "main" contains the error reporting flag diff --git a/gradle.properties b/gradle.properties index f27b1d23a3..a3007565d8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ org.gradle.jvmargs=-Xmx4096m # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects org.gradle.parallel=true -VERSION_NAME=6.5.0 +VERSION_NAME=6.6.0 GROUP=com.bugsnag POM_SCM_URL=https://github.com/bugsnag/bugsnag-android POM_SCM_CONNECTION=scm:git@github.com:bugsnag/bugsnag-android.git diff --git a/scripts/run-connected-checks.rb b/scripts/run-connected-checks.rb new file mode 100755 index 0000000000..bac5605ba8 --- /dev/null +++ b/scripts/run-connected-checks.rb @@ -0,0 +1,69 @@ +#!/usr/bin/env ruby +require 'pty' +require 'open3' + +# Ensure API_LEVEL is set +raise('API_LEVEL environment variable must be set') unless ENV['API_LEVEL'] +target_api_level = ENV['API_LEVEL'] + +# Check if the appropriate AVD exists based on given API level +avd_exists = `avdmanager list avd -c | grep test-sdk-#{ENV['API_LEVEL']}`.strip + +if avd_exists.empty? + puts "AVD test-sdk-#{target_api_level} does not exist, creating it now" + # Determine if we're running on x86 or ARM + sys_arch = `uname -m`.strip + sys_arch = 'arm64-v8a' if sys_arch.eql?('arm64') + # Check to see if the appropriate SDK is installed + sdk_installed = `sdkmanager --list_installed | grep "system-images;android-#{target_api_level};google_apis;#{sys_arch}"`.strip + + if sdk_installed.empty? + # If not, install it + puts "The system image for API level #{target_api_level} is not installed, installing it now" + `sdkmanager "system-images;android-#{target_api_level};google_apis;#{sys_arch}"` + end + # Create the AVD + `avdmanager -s create avd -n test-sdk-#{target_api_level} -k "system-images;android-#{target_api_level};google_apis;#{sys_arch}"` +else + puts "AVD test-sdk-#{target_api_level} already exists, skipping creation" +end + +begin + emulator_pid = nil + emulator_lines = [] + emulator_thread = Thread.new do + PTY.spawn('emulator', '-avd', "test-sdk-#{target_api_level}", '-no-window', '-gpu', 'swiftshader_indirect', '-noaudio', '-no-boot-anim', '-camera-back', 'none', '-no-snapshot-load') do |stdout, _stdin, pid| + emulator_pid = pid + stdout.each do |line| + emulator_lines << line + puts line + end + end + end + + # Wait for the emulator to boot + start_time = Time.now + until emulator_lines.any? { |line| line.include?('Boot completed') } + if Time.now - start_time > 60 + raise 'Emulator did not boot in 60 seconds' + end + end + + puts 'Emulator booted successfully' + + # Run the connectedCheck tests + exit_status = nil + Open3.popen2e('./gradlew connectedCheck -x :bugsnag-benchmarks:connectedCheck') do |_stdin, stdout_stderr, wait_thr| + stdout_stderr.each { |line| puts line } + exit_status = wait_thr.value + end +ensure + # Stop the emulator + puts 'Stopping emulator process' + Process.kill('INT', emulator_pid) if emulator_pid + emulator_thread.join +end + +unless exit_status.success? + exit(exit_status.exitstatus) +end diff --git a/scripts/run-cpp-check.sh b/scripts/run-cpp-check.sh index 35213af7b5..1ce30903ac 100755 --- a/scripts/run-cpp-check.sh +++ b/scripts/run-cpp-check.sh @@ -1,2 +1,2 @@ -cppcheck --error-exitcode=2 --enable=warning,performance bugsnag-plugin-android-anr/src/main/jni && \ -cppcheck --error-exitcode=2 --enable=warning,performance bugsnag-plugin-android-ndk/src/main/jni -i bugsnag-plugin-android-ndk/src/main/jni/deps -i bugsnag-plugin-android-ndk/src/main/jni/external +cppcheck --error-exitcode=2 --enable=warning,performance --check-level=exhaustive bugsnag-plugin-android-anr/src/main/jni && \ +cppcheck --error-exitcode=2 --enable=warning,performance --check-level=exhaustive bugsnag-plugin-android-ndk/src/main/jni -i bugsnag-plugin-android-ndk/src/main/jni/deps -i bugsnag-plugin-android-ndk/src/main/jni/external