mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 10:29:58 +02:00
feat(diagnostics): harden ingestion and delivery
This commit is contained in:
@@ -1,15 +1,13 @@
|
||||
package com.vnidrop.app.diagnostics
|
||||
|
||||
import kotlinx.cinterop.BetaInteropApi
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.addressOf
|
||||
import kotlinx.cinterop.convert
|
||||
import kotlinx.cinterop.usePinned
|
||||
import platform.Foundation.NSData
|
||||
import platform.Foundation.NSFileManager
|
||||
import platform.Foundation.NSString
|
||||
import platform.Foundation.NSUTF8StringEncoding
|
||||
import platform.Foundation.create
|
||||
import platform.Foundation.dataUsingEncoding
|
||||
import platform.Foundation.dataWithContentsOfFile
|
||||
import platform.Foundation.writeToFile
|
||||
import platform.posix.memcpy
|
||||
@@ -25,10 +23,11 @@ private class IosPendingCrashStore(
|
||||
private val directory = appDataDir.trimEnd('/') + "/diagnostics/crashes"
|
||||
|
||||
override fun write(report: CrashReport) {
|
||||
if (!isValidDiagnosticId(report.id)) return
|
||||
ensureDirectory()
|
||||
val path = "$directory/${report.id}.crash"
|
||||
val payload = CrashReportCodec.encode(report)
|
||||
val data = (payload as NSString).dataUsingEncoding(NSUTF8StringEncoding) ?: return
|
||||
val data = payload.encodeToByteArray().toNSData()
|
||||
data.writeToFile(path, atomically = true)
|
||||
}
|
||||
|
||||
@@ -46,14 +45,47 @@ private class IosPendingCrashStore(
|
||||
}
|
||||
|
||||
override fun delete(id: String) {
|
||||
if (!isValidDiagnosticId(id)) return
|
||||
fileManager.removeItemAtPath("$directory/$id.crash", null)
|
||||
}
|
||||
|
||||
override fun prune(olderThanTimestampMillis: Long, maxCount: Int) {
|
||||
require(maxCount > 0) { "maxCount must be positive" }
|
||||
ensureDirectory()
|
||||
val reports = fileManager.contentsOfDirectoryAtPath(directory, null).orEmpty()
|
||||
.filterIsInstance<String>()
|
||||
.filter { it.endsWith(".crash") }
|
||||
.mapNotNull { name ->
|
||||
val path = "$directory/$name"
|
||||
val report = NSData.dataWithContentsOfFile(path)
|
||||
?.toUtf8String()
|
||||
?.let(CrashReportCodec::decode)
|
||||
if (report == null) {
|
||||
fileManager.removeItemAtPath(path, null)
|
||||
null
|
||||
} else {
|
||||
name to report
|
||||
}
|
||||
}
|
||||
.sortedByDescending { (_, report) -> report.timestampMillis }
|
||||
reports.forEachIndexed { index, (name, report) ->
|
||||
if (index >= maxCount || report.timestampMillis < olderThanTimestampMillis) {
|
||||
fileManager.removeItemAtPath("$directory/$name", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureDirectory() {
|
||||
fileManager.createDirectoryAtPath(directory, withIntermediateDirectories = true, attributes = null, error = null)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
|
||||
private fun ByteArray.toNSData(): NSData =
|
||||
usePinned { pinned ->
|
||||
NSData.create(bytes = pinned.addressOf(0), length = size.toULong())
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
private fun NSData.toUtf8String(): String {
|
||||
val size = length.toInt()
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.vnidrop.app.diagnostics
|
||||
|
||||
import kotlinx.cinterop.BetaInteropApi
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.addressOf
|
||||
import kotlinx.cinterop.convert
|
||||
import kotlinx.cinterop.usePinned
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import platform.Foundation.NSData
|
||||
import platform.Foundation.NSHTTPURLResponse
|
||||
import platform.Foundation.NSMutableURLRequest
|
||||
import platform.Foundation.NSURL
|
||||
import platform.Foundation.NSURLSession
|
||||
import platform.Foundation.create
|
||||
import platform.Foundation.dataTaskWithRequest
|
||||
import platform.Foundation.setHTTPBody
|
||||
import platform.Foundation.setHTTPMethod
|
||||
import platform.Foundation.setValue
|
||||
import platform.posix.memcpy
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
actual suspend fun platformHttpPost(
|
||||
url: String,
|
||||
headers: Map<String, String>,
|
||||
bodyUtf8: String,
|
||||
): PlatformHttpResponse = suspendCancellableCoroutine { cont ->
|
||||
val nsUrl = NSURL.URLWithString(url)
|
||||
if (nsUrl == null) {
|
||||
cont.resume(PlatformHttpResponse(statusCode = 0, body = "invalid_url"))
|
||||
return@suspendCancellableCoroutine
|
||||
}
|
||||
val request = NSMutableURLRequest.requestWithURL(nsUrl).apply {
|
||||
setHTTPMethod("POST")
|
||||
setValue("application/json; charset=utf-8", forHTTPHeaderField = "Content-Type")
|
||||
headers.forEach { (key, value) ->
|
||||
setValue(value, forHTTPHeaderField = key)
|
||||
}
|
||||
setHTTPBody(bodyUtf8.encodeToByteArray().toNSData())
|
||||
}
|
||||
val task = NSURLSession.sharedSession.dataTaskWithRequest(request) { data, response, error ->
|
||||
if (!cont.isActive) return@dataTaskWithRequest
|
||||
if (error != null) {
|
||||
val message = error.localizedDescription
|
||||
cont.resume(PlatformHttpResponse(statusCode = 0, body = message))
|
||||
return@dataTaskWithRequest
|
||||
}
|
||||
val http = response as? NSHTTPURLResponse
|
||||
val status = http?.statusCode?.toInt() ?: 0
|
||||
val body = data?.toUtf8String().orEmpty()
|
||||
cont.resume(PlatformHttpResponse(statusCode = status, body = body))
|
||||
}
|
||||
cont.invokeOnCancellation { task.cancel() }
|
||||
task.resume()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
|
||||
private fun ByteArray.toNSData(): NSData =
|
||||
usePinned { pinned ->
|
||||
NSData.create(bytes = pinned.addressOf(0), length = size.toULong())
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
private fun NSData.toUtf8String(): String {
|
||||
val size = length.toInt()
|
||||
if (size == 0) return ""
|
||||
val result = ByteArray(size)
|
||||
val source = bytes ?: return ""
|
||||
result.usePinned { pinned ->
|
||||
memcpy(pinned.addressOf(0), source, size.convert())
|
||||
}
|
||||
return result.decodeToString()
|
||||
}
|
||||
Reference in New Issue
Block a user