mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 10:29:58 +02:00
feat(shared): add opt-in diagnostics client with compile-time kill switch
Ship client-side telemetry, crash capture, and bug-report assembly behind a user opt-in, with NoOp transport until Cloudflare is wired. Builds can set vnidrop.diagnostics.included=false to omit the Share-diagnostics UI and disable the telemetry/crash auto-upload stack while keeping bug reports.
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
package com.vnidrop.app.diagnostics
|
||||
|
||||
import java.io.File
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
actual fun createPendingCrashStore(appDataDir: String): PendingCrashStore =
|
||||
JvmPendingCrashStore(appDataDir)
|
||||
|
||||
private class JvmPendingCrashStore(
|
||||
appDataDir: String,
|
||||
) : PendingCrashStore {
|
||||
private val directory = File(appDataDir, "diagnostics/crashes")
|
||||
|
||||
@Synchronized
|
||||
override fun write(report: CrashReport) {
|
||||
directory.mkdirs()
|
||||
File(directory, "${report.id}.crash").writeText(CrashReportCodec.encode(report), StandardCharsets.UTF_8)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun list(): List<CrashReport> {
|
||||
if (!directory.isDirectory) return emptyList()
|
||||
return directory
|
||||
.listFiles { file -> file.isFile && file.name.endsWith(".crash") }
|
||||
.orEmpty()
|
||||
.sortedByDescending { it.lastModified() }
|
||||
.mapNotNull { file ->
|
||||
runCatching { CrashReportCodec.decode(file.readText(StandardCharsets.UTF_8)) }.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun delete(id: String) {
|
||||
File(directory, "$id.crash").delete()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.vnidrop.app.diagnostics
|
||||
|
||||
actual fun installPlatformCrashHook(onCrash: (Throwable) -> Unit) {
|
||||
val previous = Thread.getDefaultUncaughtExceptionHandler()
|
||||
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
||||
runCatching { onCrash(throwable) }
|
||||
previous?.uncaughtException(thread, throwable)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.vnidrop.app.logging
|
||||
|
||||
import java.io.File
|
||||
import java.io.RandomAccessFile
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
actual fun createPlatformLogStore(appDataDir: String, policy: LogRotationPolicy): PlatformLogStore =
|
||||
@@ -37,6 +38,32 @@ private class JvmPlatformLogStore(
|
||||
.map { file -> LogFileInfo(file.name, file.absolutePath, file.length(), file.lastModified()) }
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun readLatest(maxBytes: Long): String {
|
||||
if (maxBytes <= 0) return ""
|
||||
directory.mkdirs()
|
||||
val files = listOf(activeFile) +
|
||||
(1..policy.maxFiles).map { File(directory, "app.$it.log") }
|
||||
val chunks = ArrayList<ByteArray>()
|
||||
var remaining = maxBytes
|
||||
for (file in files) {
|
||||
if (remaining <= 0 || !file.isFile) continue
|
||||
val slice = readTail(file, remaining)
|
||||
if (slice.isEmpty()) continue
|
||||
chunks.add(0, slice)
|
||||
remaining -= slice.size.toLong()
|
||||
}
|
||||
if (chunks.isEmpty()) return ""
|
||||
val total = chunks.sumOf { it.size }
|
||||
val out = ByteArray(total)
|
||||
var offset = 0
|
||||
for (chunk in chunks) {
|
||||
chunk.copyInto(out, offset)
|
||||
offset += chunk.size
|
||||
}
|
||||
return String(out, StandardCharsets.UTF_8)
|
||||
}
|
||||
|
||||
private fun rotate() {
|
||||
if (policy.maxFiles == 0) {
|
||||
activeFile.delete()
|
||||
@@ -54,3 +81,23 @@ private class JvmPlatformLogStore(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun readTail(file: File, maxBytes: Long): ByteArray {
|
||||
if (!file.isFile || file.length() == 0L || maxBytes <= 0) return ByteArray(0)
|
||||
val length = file.length()
|
||||
val start = (length - maxBytes).coerceAtLeast(0L)
|
||||
val size = (length - start).toInt()
|
||||
RandomAccessFile(file, "r").use { raf ->
|
||||
raf.seek(start)
|
||||
val bytes = ByteArray(size)
|
||||
raf.readFully(bytes)
|
||||
if (start == 0L) return bytes
|
||||
// Align to the next full line when we start mid-file.
|
||||
val newline = bytes.indexOf('\n'.code.toByte())
|
||||
return if (newline in 0 until bytes.lastIndex) {
|
||||
bytes.copyOfRange(newline + 1, bytes.size)
|
||||
} else {
|
||||
bytes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user