perf(core): database corruption

This commit is contained in:
rhunk
2023-11-09 20:31:53 +01:00
parent 4bf421441b
commit 691510235b
3 changed files with 104 additions and 78 deletions

View File

@ -17,6 +17,7 @@ class CleanCache : AbstractAction() {
"databases/journal.db", "databases/journal.db",
"databases/arroyo.db", "databases/arroyo.db",
"databases/arroyo.db-wal", "databases/arroyo.db-wal",
"databases/arroyo.db-shm",
"databases/native_content_manager/*" "databases/native_content_manager/*"
) )
} }

View File

@ -1,89 +1,119 @@
package me.rhunk.snapenhance.core.database package me.rhunk.snapenhance.core.database
import android.annotation.SuppressLint import android.database.Cursor
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteDatabaseCorruptException
import me.rhunk.snapenhance.common.database.DatabaseObject import me.rhunk.snapenhance.common.database.DatabaseObject
import me.rhunk.snapenhance.common.database.impl.ConversationMessage import me.rhunk.snapenhance.common.database.impl.ConversationMessage
import me.rhunk.snapenhance.common.database.impl.FriendFeedEntry import me.rhunk.snapenhance.common.database.impl.FriendFeedEntry
import me.rhunk.snapenhance.common.database.impl.FriendInfo import me.rhunk.snapenhance.common.database.impl.FriendInfo
import me.rhunk.snapenhance.common.database.impl.StoryEntry import me.rhunk.snapenhance.common.database.impl.StoryEntry
import me.rhunk.snapenhance.common.database.impl.UserConversationLink import me.rhunk.snapenhance.common.database.impl.UserConversationLink
import me.rhunk.snapenhance.common.util.ktx.getInteger
import me.rhunk.snapenhance.common.util.ktx.getStringOrNull import me.rhunk.snapenhance.common.util.ktx.getStringOrNull
import me.rhunk.snapenhance.core.ModContext import me.rhunk.snapenhance.core.ModContext
import me.rhunk.snapenhance.core.logger.CoreLogger
import me.rhunk.snapenhance.core.manager.Manager import me.rhunk.snapenhance.core.manager.Manager
import java.lang.ref.WeakReference import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper
import java.io.File
inline fun <T> SQLiteDatabase.performOperation(crossinline query: SQLiteDatabase.() -> T?): T? {
synchronized(this) {
if (!isOpen) {
return null
}
return runCatching {
query()
}.onFailure {
CoreLogger.xposedLog("Database operation failed", it)
}.getOrNull()
}
}
@SuppressLint("Range")
class DatabaseAccess( class DatabaseAccess(
private val context: ModContext private val context: ModContext
) : Manager { ) : Manager {
private val mainDb by lazy { openLocalDatabase("main.db") }
private val arroyoDb by lazy { openLocalDatabase("arroyo.db") }
private inline fun <T> SQLiteDatabase.performOperation(crossinline query: SQLiteDatabase.() -> T?): T? {
return runCatching {
query()
}.onFailure {
context.log.error("Database operation failed", it)
}.getOrNull()
}
private var hasShownDatabaseError = false
private fun showDatabaseError(databasePath: String, throwable: Throwable) {
if (hasShownDatabaseError) return
hasShownDatabaseError = true
context.runOnUiThread {
if (context.mainActivity == null) return@runOnUiThread
ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity)
.setTitle("SnapEnhance")
.setMessage("Failed to query $databasePath database!\n\n${throwable.localizedMessage}\n\nRestarting Snapchat may fix this issue. If the issue persists, try to clean the app data and cache.")
.setPositiveButton("Restart Snapchat") { _, _ ->
File(databasePath).takeIf { it.exists() }?.delete()
context.softRestartApp()
}
.setNegativeButton("Dismiss") { dialog, _ ->
dialog.dismiss()
}.show()
}
}
private fun SQLiteDatabase.safeRawQuery(query: String, args: Array<String>? = null): Cursor? {
return runCatching {
rawQuery(query, args)
}.onFailure {
if (it !is SQLiteDatabaseCorruptException) {
context.log.error("Failed to execute query $query", it)
showDatabaseError(this.path, it)
return@onFailure
}
context.log.warn("Database ${this.path} is corrupted!")
context.androidContext.deleteDatabase(this.path)
showDatabaseError(this.path, it)
}.getOrNull()
}
private val dmOtherParticipantCache by lazy { private val dmOtherParticipantCache by lazy {
(openArroyo().performOperation { (arroyoDb?.performOperation {
rawQuery( safeRawQuery(
"SELECT client_conversation_id, user_id FROM user_conversation WHERE conversation_type = 0 AND user_id != ?", "SELECT client_conversation_id, user_id FROM user_conversation WHERE conversation_type = 0 AND user_id != ?",
arrayOf(myUserId) arrayOf(myUserId)
).use { query -> )?.use { query ->
val participants = mutableMapOf<String, String?>() val participants = mutableMapOf<String, String?>()
if (!query.moveToFirst()) { if (!query.moveToFirst()) {
return@performOperation null return@performOperation null
} }
do { do {
participants[query.getString(query.getColumnIndex("client_conversation_id"))] = participants[query.getStringOrNull("client_conversation_id")!!] = query.getStringOrNull("user_id")!!
query.getString(query.getColumnIndex("user_id"))
} while (query.moveToNext()) } while (query.moveToNext())
participants participants
} }
} ?: emptyMap()).toMutableMap() } ?: emptyMap()).toMutableMap()
} }
private var databaseWeakMap = mutableMapOf<String, WeakReference<SQLiteDatabase>?>() private fun openLocalDatabase(fileName: String): SQLiteDatabase? {
val dbPath = context.androidContext.getDatabasePath(fileName)
private fun openLocalDatabase(fileName: String): SQLiteDatabase { if (!dbPath.exists()) return null
if (databaseWeakMap.containsKey(fileName)) {
val database = databaseWeakMap[fileName]?.get()
if (database != null && database.isOpen) return database
}
return runCatching { return runCatching {
SQLiteDatabase.openDatabase( SQLiteDatabase.openDatabase(
context.androidContext.getDatabasePath(fileName).absolutePath, dbPath.absolutePath,
null, null,
SQLiteDatabase.OPEN_READONLY SQLiteDatabase.OPEN_READONLY or SQLiteDatabase.NO_LOCALIZED_COLLATORS
)?.also { )
databaseWeakMap[fileName] = WeakReference(it)
}
}.onFailure { }.onFailure {
context.log.error("Failed to open database $fileName, restarting!", it) context.log.error("Failed to open database $fileName!", it)
}.getOrNull() ?: throw IllegalStateException("Failed to open database $fileName") showDatabaseError(dbPath.absolutePath, it)
}.getOrNull()
} }
private fun openMain() = openLocalDatabase("main.db") fun hasMain(): Boolean = mainDb?.isOpen == true
private fun openArroyo() = openLocalDatabase("arroyo.db") fun hasArroyo(): Boolean = arroyoDb?.isOpen == true
fun hasMain(): Boolean = context.androidContext.getDatabasePath("main.db").exists() fun finalize() {
fun hasArroyo(): Boolean = context.androidContext.getDatabasePath("arroyo.db").exists() mainDb?.close()
arroyoDb?.close()
context.log.verbose("Database closed")
}
private fun <T : DatabaseObject> readDatabaseObject( private fun <T : DatabaseObject> SQLiteDatabase.readDatabaseObject(
obj: T, obj: T,
database: SQLiteDatabase,
table: String, table: String,
where: String, where: String,
args: Array<String> args: Array<String>
): T? = database.rawQuery("SELECT * FROM $table WHERE $where", args).use { ): T? = this.safeRawQuery("SELECT * FROM $table WHERE $where", args)?.use {
if (!it.moveToFirst()) { if (!it.moveToFirst()) {
return null return null
} }
@ -96,10 +126,9 @@ class DatabaseAccess(
} }
fun getFeedEntryByUserId(userId: String): FriendFeedEntry? { fun getFeedEntryByUserId(userId: String): FriendFeedEntry? {
return openMain().performOperation { return mainDb?.performOperation {
readDatabaseObject( readDatabaseObject(
FriendFeedEntry(), FriendFeedEntry(),
this,
"FriendsFeedView", "FriendsFeedView",
"friendUserId = ?", "friendUserId = ?",
arrayOf(userId) arrayOf(userId)
@ -108,10 +137,10 @@ class DatabaseAccess(
} }
val myUserId by lazy { val myUserId by lazy {
openArroyo().performOperation { arroyoDb?.performOperation {
rawQuery(buildString { safeRawQuery(buildString {
append("SELECT value FROM required_values WHERE key = 'USERID'") append("SELECT value FROM required_values WHERE key = 'USERID'")
}, null).use { query -> }, null)?.use { query ->
if (!query.moveToFirst()) { if (!query.moveToFirst()) {
return@performOperation null return@performOperation null
} }
@ -121,10 +150,9 @@ class DatabaseAccess(
} }
fun getFeedEntryByConversationId(conversationId: String): FriendFeedEntry? { fun getFeedEntryByConversationId(conversationId: String): FriendFeedEntry? {
return openMain().performOperation { return mainDb?.performOperation {
readDatabaseObject( readDatabaseObject(
FriendFeedEntry(), FriendFeedEntry(),
this,
"FriendsFeedView", "FriendsFeedView",
"key = ?", "key = ?",
arrayOf(conversationId) arrayOf(conversationId)
@ -133,10 +161,9 @@ class DatabaseAccess(
} }
fun getFriendInfo(userId: String): FriendInfo? { fun getFriendInfo(userId: String): FriendInfo? {
return openMain().performOperation { return mainDb?.performOperation {
readDatabaseObject( readDatabaseObject(
FriendInfo(), FriendInfo(),
this,
"FriendWithUsername", "FriendWithUsername",
"userId = ?", "userId = ?",
arrayOf(userId) arrayOf(userId)
@ -145,11 +172,11 @@ class DatabaseAccess(
} }
fun getFeedEntries(limit: Int): List<FriendFeedEntry> { fun getFeedEntries(limit: Int): List<FriendFeedEntry> {
return openMain().performOperation { return mainDb?.performOperation {
rawQuery( safeRawQuery(
"SELECT * FROM FriendsFeedView ORDER BY _id LIMIT ?", "SELECT * FROM FriendsFeedView ORDER BY _id LIMIT ?",
arrayOf(limit.toString()) arrayOf(limit.toString())
).use { query -> )?.use { query ->
val list = mutableListOf<FriendFeedEntry>() val list = mutableListOf<FriendFeedEntry>()
while (query.moveToNext()) { while (query.moveToNext()) {
val friendFeedEntry = FriendFeedEntry() val friendFeedEntry = FriendFeedEntry()
@ -164,10 +191,9 @@ class DatabaseAccess(
} }
fun getConversationMessageFromId(clientMessageId: Long): ConversationMessage? { fun getConversationMessageFromId(clientMessageId: Long): ConversationMessage? {
return openArroyo().performOperation { return arroyoDb?.performOperation {
readDatabaseObject( readDatabaseObject(
ConversationMessage(), ConversationMessage(),
this,
"conversation_message", "conversation_message",
"client_message_id = ?", "client_message_id = ?",
arrayOf(clientMessageId.toString()) arrayOf(clientMessageId.toString())
@ -176,24 +202,23 @@ class DatabaseAccess(
} }
fun getConversationType(conversationId: String): Int? { fun getConversationType(conversationId: String): Int? {
return openArroyo().performOperation { return arroyoDb?.performOperation {
rawQuery( safeRawQuery(
"SELECT conversation_type FROM user_conversation WHERE client_conversation_id = ?", "SELECT conversation_type FROM user_conversation WHERE client_conversation_id = ?",
arrayOf(conversationId) arrayOf(conversationId)
).use { query -> )?.use { query ->
if (!query.moveToFirst()) { if (!query.moveToFirst()) {
return@performOperation null return@performOperation null
} }
query.getInt(query.getColumnIndex("conversation_type")) query.getInteger("conversation_type")
} }
} }
} }
fun getConversationLinkFromUserId(userId: String): UserConversationLink? { fun getConversationLinkFromUserId(userId: String): UserConversationLink? {
return openArroyo().performOperation { return arroyoDb?.performOperation {
readDatabaseObject( readDatabaseObject(
UserConversationLink(), UserConversationLink(),
this,
"user_conversation", "user_conversation",
"user_id = ? AND conversation_type = 0", "user_id = ? AND conversation_type = 0",
arrayOf(userId) arrayOf(userId)
@ -203,17 +228,17 @@ class DatabaseAccess(
fun getDMOtherParticipant(conversationId: String): String? { fun getDMOtherParticipant(conversationId: String): String? {
if (dmOtherParticipantCache.containsKey(conversationId)) return dmOtherParticipantCache[conversationId] if (dmOtherParticipantCache.containsKey(conversationId)) return dmOtherParticipantCache[conversationId]
return openArroyo().performOperation { return arroyoDb?.performOperation {
rawQuery( safeRawQuery(
"SELECT user_id FROM user_conversation WHERE client_conversation_id = ? AND conversation_type = 0", "SELECT user_id FROM user_conversation WHERE client_conversation_id = ? AND conversation_type = 0",
arrayOf(conversationId) arrayOf(conversationId)
).use { query -> )?.use { query ->
val participants = mutableListOf<String>() val participants = mutableListOf<String>()
if (!query.moveToFirst()) { if (!query.moveToFirst()) {
return@performOperation null return@performOperation null
} }
do { do {
participants.add(query.getString(query.getColumnIndex("user_id"))) participants.add(query.getStringOrNull("user_id")!!)
} while (query.moveToNext()) } while (query.moveToNext())
participants.firstOrNull { it != myUserId } participants.firstOrNull { it != myUserId }
} }
@ -222,23 +247,23 @@ class DatabaseAccess(
fun getStoryEntryFromId(storyId: String): StoryEntry? { fun getStoryEntryFromId(storyId: String): StoryEntry? {
return openMain().performOperation { return mainDb?.performOperation {
readDatabaseObject(StoryEntry(), this, "Story", "storyId = ?", arrayOf(storyId)) readDatabaseObject(StoryEntry(), "Story", "storyId = ?", arrayOf(storyId))
} }
} }
fun getConversationParticipants(conversationId: String): List<String>? { fun getConversationParticipants(conversationId: String): List<String>? {
return openArroyo().performOperation { return arroyoDb?.performOperation {
rawQuery( safeRawQuery(
"SELECT user_id FROM user_conversation WHERE client_conversation_id = ?", "SELECT user_id FROM user_conversation WHERE client_conversation_id = ?",
arrayOf(conversationId) arrayOf(conversationId)
).use { )?.use {
if (!it.moveToFirst()) { if (!it.moveToFirst()) {
return@performOperation null return@performOperation null
} }
val participants = mutableListOf<String>() val participants = mutableListOf<String>()
do { do {
participants.add(it.getString(it.getColumnIndex("user_id"))) participants.add(it.getStringOrNull("user_id")!!)
} while (it.moveToNext()) } while (it.moveToNext())
participants participants
} }
@ -249,11 +274,11 @@ class DatabaseAccess(
conversationId: String, conversationId: String,
limit: Int limit: Int
): List<ConversationMessage>? { ): List<ConversationMessage>? {
return openArroyo().performOperation { return arroyoDb?.performOperation {
rawQuery( safeRawQuery(
"SELECT * FROM conversation_message WHERE client_conversation_id = ? ORDER BY creation_timestamp DESC LIMIT ?", "SELECT * FROM conversation_message WHERE client_conversation_id = ? ORDER BY creation_timestamp DESC LIMIT ?",
arrayOf(conversationId, limit.toString()) arrayOf(conversationId, limit.toString())
).use { query -> )?.use { query ->
if (!query.moveToFirst()) { if (!query.moveToFirst()) {
return@performOperation null return@performOperation null
} }
@ -269,7 +294,7 @@ class DatabaseAccess(
} }
fun getAddSource(userId: String): String? { fun getAddSource(userId: String): String? {
return openMain().performOperation { return mainDb?.performOperation {
rawQuery( rawQuery(
"SELECT addSource FROM FriendWhoAddedMe WHERE userId = ?", "SELECT addSource FROM FriendWhoAddedMe WHERE userId = ?",
arrayOf(userId) arrayOf(userId)

View File

@ -57,13 +57,13 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C
} }
} }
val myUserId = context.database.myUserId
context.classCache.feedEntry.hookConstructor(HookStage.AFTER) { param -> context.classCache.feedEntry.hookConstructor(HookStage.AFTER) { param ->
val instance = param.thisObject<Any>() val instance = param.thisObject<Any>()
val interactionInfo = instance.getObjectFieldOrNull("mInteractionInfo") ?: return@hookConstructor val interactionInfo = instance.getObjectFieldOrNull("mInteractionInfo") ?: return@hookConstructor
val messages = (interactionInfo.getObjectFieldOrNull("mMessages") as? List<*>)?.map { Message(it) } ?: return@hookConstructor val messages = (interactionInfo.getObjectFieldOrNull("mMessages") as? List<*>)?.map { Message(it) } ?: return@hookConstructor
val conversationId = SnapUUID(instance.getObjectFieldOrNull("mConversationId") ?: return@hookConstructor).toString() val conversationId = SnapUUID(instance.getObjectFieldOrNull("mConversationId") ?: return@hookConstructor).toString()
val myUserId = context.database.myUserId
feedCachedSnapMessages[conversationId] = messages.filter { msg -> feedCachedSnapMessages[conversationId] = messages.filter { msg ->
msg.messageMetadata?.seenBy?.none { it.toString() == myUserId } == true msg.messageMetadata?.seenBy?.none { it.toString() == myUserId } == true