That fuck shit the fascists are using
at master 502 lines 15 kB view raw
1package org.tm.archive.database 2 3import android.annotation.SuppressLint 4import android.app.Application 5import android.database.Cursor 6import net.zetetic.database.sqlcipher.SQLiteDatabase 7import net.zetetic.database.sqlcipher.SQLiteOpenHelper 8import org.signal.core.util.CursorUtil 9import org.signal.core.util.SqlUtil 10import org.signal.core.util.Stopwatch 11import org.signal.core.util.delete 12import org.signal.core.util.deleteAll 13import org.signal.core.util.exists 14import org.signal.core.util.getTableRowCount 15import org.signal.core.util.insertInto 16import org.signal.core.util.logging.Log 17import org.signal.core.util.mebiBytes 18import org.signal.core.util.readToList 19import org.signal.core.util.readToSingleInt 20import org.signal.core.util.requireLong 21import org.signal.core.util.requireNonNullString 22import org.signal.core.util.select 23import org.signal.core.util.update 24import org.signal.core.util.withinTransaction 25import org.tm.archive.crash.CrashConfig 26import org.tm.archive.crypto.DatabaseSecret 27import org.tm.archive.crypto.DatabaseSecretProvider 28import org.tm.archive.database.model.LogEntry 29import java.io.Closeable 30import kotlin.math.abs 31import kotlin.time.Duration.Companion.days 32 33/** 34 * Stores logs. 35 * 36 * Logs are very performance critical. Even though this database is written to on a low-priority background thread, we want to keep throughput high and ensure 37 * that we aren't creating excess garbage. 38 * 39 * This is it's own separate physical database, so it cannot do joins or queries with any other tables. 40 */ 41class LogDatabase private constructor( 42 application: Application, 43 databaseSecret: DatabaseSecret 44) : 45 SQLiteOpenHelper( 46 application, 47 DATABASE_NAME, 48 databaseSecret.asString(), 49 null, 50 DATABASE_VERSION, 51 0, 52 SqlCipherDeletingErrorHandler(DATABASE_NAME), 53 SqlCipherDatabaseHook(), 54 true 55 ), 56 SignalDatabaseOpenHelper { 57 58 companion object { 59 private val TAG = Log.tag(LogDatabase::class.java) 60 61 private const val DATABASE_VERSION = 4 62 private const val DATABASE_NAME = "signal-logs.db" 63 64 @SuppressLint("StaticFieldLeak") // We hold an Application context, not a view context 65 @Volatile 66 private var instance: LogDatabase? = null 67 68 @JvmStatic 69 fun getInstance(context: Application): LogDatabase { 70 if (instance == null) { 71 synchronized(LogDatabase::class.java) { 72 if (instance == null) { 73 SqlCipherLibraryLoader.load() 74 instance = LogDatabase(context, DatabaseSecretProvider.getOrCreateDatabaseSecret(context)) 75 } 76 } 77 } 78 return instance!! 79 } 80 } 81 82 @get:JvmName("logs") 83 val logs: LogTable by lazy { LogTable(this) } 84 85 @get:JvmName("crashes") 86 val crashes: CrashTable by lazy { CrashTable(this) } 87 88 @get:JvmName("anrs") 89 val anrs: AnrTable by lazy { AnrTable(this) } 90 91 override fun onCreate(db: SQLiteDatabase) { 92 Log.i(TAG, "onCreate()") 93 94 db.execSQL(LogTable.CREATE_TABLE) 95 db.execSQL(CrashTable.CREATE_TABLE) 96 db.execSQL(AnrTable.CREATE_TABLE) 97 98 LogTable.CREATE_INDEXES.forEach { db.execSQL(it) } 99 CrashTable.CREATE_INDEXES.forEach { db.execSQL(it) } 100 } 101 102 override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { 103 Log.i(TAG, "onUpgrade($oldVersion, $newVersion)") 104 105 if (oldVersion < 2) { 106 db.execSQL("DROP TABLE log") 107 db.execSQL("CREATE TABLE log (_id INTEGER PRIMARY KEY, created_at INTEGER, keep_longer INTEGER DEFAULT 0, body TEXT, size INTEGER)") 108 db.execSQL("CREATE INDEX keep_longer_index ON log (keep_longer)") 109 db.execSQL("CREATE INDEX log_created_at_keep_longer_index ON log (created_at, keep_longer)") 110 } 111 112 if (oldVersion < 3) { 113 db.execSQL("CREATE TABLE crash (_id INTEGER PRIMARY KEY, created_at INTEGER, name TEXT, message TEXT, stack_trace TEXT, last_prompted_at INTEGER)") 114 db.execSQL("CREATE INDEX crash_created_at ON crash (created_at)") 115 db.execSQL("CREATE INDEX crash_name_message ON crash (name, message)") 116 } 117 118 if (oldVersion < 4) { 119 db.execSQL("CREATE TABLE anr (_id INTEGER PRIMARY KEY, created_at INTEGER NOT NULL, thread_dump TEXT NOT NULL)") 120 } 121 } 122 123 override fun onOpen(db: SQLiteDatabase) { 124 db.setForeignKeyConstraintsEnabled(true) 125 } 126 127 override fun getSqlCipherDatabase(): SQLiteDatabase { 128 return writableDatabase 129 } 130 131 class LogTable(private val openHelper: LogDatabase) { 132 companion object { 133 const val TABLE_NAME = "log" 134 const val ID = "_id" 135 const val CREATED_AT = "created_at" 136 const val KEEP_LONGER = "keep_longer" 137 const val BODY = "body" 138 const val SIZE = "size" 139 140 const val CREATE_TABLE = """ 141 CREATE TABLE $TABLE_NAME ( 142 $ID INTEGER PRIMARY KEY, 143 $CREATED_AT INTEGER, 144 $KEEP_LONGER INTEGER DEFAULT 0, 145 $BODY TEXT, 146 $SIZE INTEGER 147 ) 148 """ 149 150 val CREATE_INDEXES = arrayOf( 151 "CREATE INDEX keep_longer_index ON $TABLE_NAME ($KEEP_LONGER)", 152 "CREATE INDEX log_created_at_keep_longer_index ON $TABLE_NAME ($CREATED_AT, $KEEP_LONGER)" 153 ) 154 155 val MAX_FILE_SIZE = 20L.mebiBytes.inWholeBytes 156 val DEFAULT_LIFESPAN = 3.days.inWholeMilliseconds 157 val LONGER_LIFESPAN = 21.days.inWholeMilliseconds 158 } 159 160 private val readableDatabase: SQLiteDatabase get() = openHelper.readableDatabase 161 private val writableDatabase: SQLiteDatabase get() = openHelper.writableDatabase 162 163 fun insert(logs: List<LogEntry>, currentTime: Long) { 164 writableDatabase.withinTransaction { db -> 165 logs.forEach { log -> 166 db.insertInto(TABLE_NAME) 167 .values( 168 CREATED_AT to log.createdAt, 169 KEEP_LONGER to if (log.keepLonger) 1 else 0, 170 BODY to log.body, 171 SIZE to log.body.length 172 ) 173 .run() 174 } 175 176 db.delete(TABLE_NAME) 177 .where("($CREATED_AT < ? AND $KEEP_LONGER = 0) OR ($CREATED_AT < ? AND $KEEP_LONGER = 1)", currentTime - DEFAULT_LIFESPAN, currentTime - LONGER_LIFESPAN) 178 .run() 179 } 180 } 181 182 fun getAllBeforeTime(time: Long): Reader { 183 return readableDatabase 184 .select(BODY) 185 .from(TABLE_NAME) 186 .where("$CREATED_AT < $time") 187 .run() 188 .toReader() 189 } 190 191 fun getRangeBeforeTime(start: Int, length: Int, time: Long): List<String> { 192 return readableDatabase 193 .select(BODY) 194 .from(TABLE_NAME) 195 .where("$CREATED_AT < $time") 196 .limit(limit = length, offset = start) 197 .run() 198 .readToList { it.requireNonNullString(BODY) } 199 } 200 201 fun trimToSize() { 202 val currentTime = System.currentTimeMillis() 203 val stopwatch = Stopwatch("trim") 204 205 val sizeOfSpecialLogs: Long = getSize("$KEEP_LONGER = ?", arrayOf("1")) 206 val remainingSize = MAX_FILE_SIZE - sizeOfSpecialLogs 207 208 stopwatch.split("keepers-size") 209 210 if (remainingSize <= 0) { 211 if (abs(remainingSize) > MAX_FILE_SIZE / 2) { 212 // Not only are KEEP_LONGER logs putting us over the storage limit, it's doing it by a lot! Delete half. 213 val logCount = readableDatabase.getTableRowCount(TABLE_NAME) 214 writableDatabase.execSQL("DELETE FROM $TABLE_NAME WHERE $ID < (SELECT MAX($ID) FROM (SELECT $ID FROM $TABLE_NAME LIMIT ${logCount / 2}))") 215 } else { 216 writableDatabase 217 .delete(TABLE_NAME) 218 .where("$KEEP_LONGER = 0") 219 } 220 return 221 } 222 223 val sizeDiffThreshold = MAX_FILE_SIZE * 0.01 224 225 var lhs: Long = currentTime - DEFAULT_LIFESPAN 226 var rhs: Long = currentTime 227 var mid: Long = 0 228 var sizeOfChunk: Long 229 230 while (lhs < rhs - 2) { 231 mid = (lhs + rhs) / 2 232 sizeOfChunk = getSize("$CREATED_AT > ? AND $CREATED_AT < ? AND $KEEP_LONGER = ?", SqlUtil.buildArgs(mid, currentTime, 0)) 233 234 if (sizeOfChunk > remainingSize) { 235 lhs = mid 236 } else if (sizeOfChunk < remainingSize) { 237 if (remainingSize - sizeOfChunk < sizeDiffThreshold) { 238 break 239 } else { 240 rhs = mid 241 } 242 } else { 243 break 244 } 245 } 246 247 stopwatch.split("binary-search") 248 249 writableDatabase.delete(TABLE_NAME, "$CREATED_AT < ? AND $KEEP_LONGER = ?", SqlUtil.buildArgs(mid, 0)) 250 251 stopwatch.split("delete") 252 stopwatch.stop(TAG) 253 } 254 255 fun getLogCountBeforeTime(time: Long): Int { 256 return readableDatabase 257 .select("COUNT(*)") 258 .from(TABLE_NAME) 259 .where("$CREATED_AT < $time") 260 .run() 261 .readToSingleInt() 262 } 263 264 fun clearKeepLonger() { 265 writableDatabase 266 .delete(TABLE_NAME) 267 .where("$KEEP_LONGER = 1") 268 .run() 269 } 270 271 fun clearAll() { 272 writableDatabase.deleteAll(TABLE_NAME) 273 } 274 275 private fun getSize(query: String?, args: Array<String>?): Long { 276 readableDatabase.query(TABLE_NAME, arrayOf("SUM($SIZE)"), query, args, null, null, null).use { cursor -> 277 return if (cursor.moveToFirst()) { 278 cursor.getLong(0) 279 } else { 280 0 281 } 282 } 283 } 284 285 private fun Cursor.toReader(): CursorReader { 286 return CursorReader(this) 287 } 288 289 interface Reader : Iterator<String>, Closeable 290 291 class CursorReader(private val cursor: Cursor) : Reader { 292 override fun hasNext(): Boolean { 293 return !cursor.isLast && cursor.count > 0 294 } 295 296 override fun next(): String { 297 cursor.moveToNext() 298 return CursorUtil.requireString(cursor, LogTable.BODY) 299 } 300 301 override fun close() { 302 cursor.close() 303 } 304 } 305 } 306 307 class CrashTable(private val openHelper: LogDatabase) { 308 companion object { 309 const val TABLE_NAME = "crash" 310 const val ID = "_id" 311 const val CREATED_AT = "created_at" 312 const val NAME = "name" 313 const val MESSAGE = "message" 314 const val STACK_TRACE = "stack_trace" 315 const val LAST_PROMPTED_AT = "last_prompted_at" 316 317 const val CREATE_TABLE = """ 318 CREATE TABLE $TABLE_NAME ( 319 $ID INTEGER PRIMARY KEY, 320 $CREATED_AT INTEGER, 321 $NAME TEXT, 322 $MESSAGE TEXT, 323 $STACK_TRACE TEXT, 324 $LAST_PROMPTED_AT INTEGER 325 ) 326 """ 327 328 val CREATE_INDEXES = arrayOf( 329 "CREATE INDEX crash_created_at ON $TABLE_NAME ($CREATED_AT)", 330 "CREATE INDEX crash_name_message ON $TABLE_NAME ($NAME, $MESSAGE)" 331 ) 332 } 333 334 private val readableDatabase: SQLiteDatabase get() = openHelper.readableDatabase 335 private val writableDatabase: SQLiteDatabase get() = openHelper.writableDatabase 336 337 fun saveCrash(createdAt: Long, name: String, message: String?, stackTrace: String) { 338 writableDatabase 339 .insertInto(TABLE_NAME) 340 .values( 341 CREATED_AT to createdAt, 342 NAME to name, 343 MESSAGE to message, 344 STACK_TRACE to stackTrace, 345 LAST_PROMPTED_AT to 0 346 ) 347 .run() 348 349 trimToSize() 350 } 351 352 /** 353 * Returns true if crashes exists that 354 * (1) match any of the provided crash patterns 355 * (2) have not been prompted within the [promptThreshold] 356 */ 357 fun anyMatch(patterns: Collection<CrashConfig.CrashPattern>, promptThreshold: Long): Boolean { 358 for (pattern in patterns) { 359 val (query, args) = pattern.asLikeQuery() 360 361 val found = readableDatabase 362 .exists(TABLE_NAME) 363 .where("$query AND $LAST_PROMPTED_AT < $promptThreshold", args) 364 .run() 365 366 if (found) { 367 return true 368 } 369 } 370 371 return false 372 } 373 374 /** 375 * Marks all crashes that match any of the provided patterns as being prompted at the provided [promptedAt] time. 376 */ 377 fun markAsPrompted(patterns: Collection<CrashConfig.CrashPattern>, promptedAt: Long) { 378 for (pattern in patterns) { 379 val (query, args) = pattern.asLikeQuery() 380 381 readableDatabase 382 .update(TABLE_NAME) 383 .values(LAST_PROMPTED_AT to promptedAt) 384 .where(query, args) 385 .run() 386 } 387 } 388 389 fun trimToSize() { 390 // Delete crashes older than 30 days 391 val threshold = System.currentTimeMillis() - 30.days.inWholeMilliseconds 392 writableDatabase 393 .delete(TABLE_NAME) 394 .where("$CREATED_AT < $threshold") 395 .run() 396 397 // Only keep 100 most recent crashes to prevent crash loops from filling up the disk 398 writableDatabase 399 .delete(TABLE_NAME) 400 .where("$ID NOT IN (SELECT $ID FROM $TABLE_NAME ORDER BY $CREATED_AT DESC LIMIT 100)") 401 .run() 402 } 403 404 fun clear() { 405 writableDatabase.deleteAll(TABLE_NAME) 406 } 407 408 private fun CrashConfig.CrashPattern.asLikeQuery(): Pair<String, Array<String>> { 409 val query = StringBuilder() 410 var args = arrayOf<String>() 411 412 if (namePattern != null) { 413 query.append("$NAME LIKE ?") 414 args += "%$namePattern%" 415 } 416 417 if (messagePattern != null) { 418 if (query.isNotEmpty()) { 419 query.append(" AND ") 420 } 421 query.append("$MESSAGE LIKE ?") 422 args += "%$messagePattern%" 423 } 424 425 if (stackTracePattern != null) { 426 if (query.isNotEmpty()) { 427 query.append(" AND ") 428 } 429 query.append("$STACK_TRACE LIKE ?") 430 args += "%$stackTracePattern%" 431 } 432 433 return query.toString() to args 434 } 435 } 436 437 class AnrTable(private val openHelper: LogDatabase) { 438 companion object { 439 const val TABLE_NAME = "anr" 440 const val ID = "_id" 441 const val CREATED_AT = "created_at" 442 const val THREAD_DUMP = "thread_dump" 443 444 const val CREATE_TABLE = """ 445 CREATE TABLE $TABLE_NAME ( 446 $ID INTEGER PRIMARY KEY, 447 $CREATED_AT INTEGER NOT NULL, 448 $THREAD_DUMP TEXT NOT NULL 449 ) 450 """ 451 } 452 453 private val readableDatabase: SQLiteDatabase get() = openHelper.readableDatabase 454 private val writableDatabase: SQLiteDatabase get() = openHelper.writableDatabase 455 456 fun save(currentTime: Long, threadDumps: String) { 457 writableDatabase 458 .insertInto(TABLE_NAME) 459 .values( 460 CREATED_AT to currentTime, 461 THREAD_DUMP to threadDumps 462 ) 463 .run() 464 465 val count = writableDatabase 466 .delete(TABLE_NAME) 467 .where( 468 """ 469 $ID NOT IN (SELECT $ID FROM $TABLE_NAME ORDER BY $CREATED_AT DESC LIMIT 10) 470 """.trimIndent() 471 ) 472 .run() 473 474 if (count > 0) { 475 Log.i(TAG, "Deleted $count old ANRs") 476 } 477 } 478 479 fun getAll(): List<AnrRecord> { 480 return readableDatabase 481 .select() 482 .from(TABLE_NAME) 483 .run() 484 .readToList { cursor -> 485 AnrRecord( 486 createdAt = cursor.requireLong(CREATED_AT), 487 threadDump = cursor.requireNonNullString(THREAD_DUMP) 488 ) 489 } 490 .sortedBy { it.createdAt } 491 } 492 493 fun clear() { 494 writableDatabase.deleteAll(TABLE_NAME) 495 } 496 497 data class AnrRecord( 498 val createdAt: Long, 499 val threadDump: String 500 ) 501 } 502}