Skip to content

Commit

Permalink
Send logs & crash reports to stash server (#339)
Browse files Browse the repository at this point in the history
Because gathering logs and information about app crashes is so
difficult, this PR adds the ability to gather that information and send
it to the user's Stash server if the companion plugin is installed.

The logs are only ever sent to the user's own stash server to help
protect privacy.

If the app crashes, a dialog will appear asking whether or not to send a
crash report to the server. The report will be logged server-side in Log
tab of Settings (eg http://192.168.1.5:9999/settings?tab=logs). This can
be disabled in advanced settings (ie the dialog will never appear).

PR to add the plugin to the community scripts:
stashapp/CommunityScripts#381
  • Loading branch information
damontecres authored Aug 2, 2024
1 parent e45f3b1 commit ac164c5
Show file tree
Hide file tree
Showing 17 changed files with 551 additions and 87 deletions.
12 changes: 12 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ android {
}
}

buildFeatures {
buildConfig = true
}

defaultConfig {
applicationId = "com.github.damontecres.stashapp"
minSdk = 23
Expand Down Expand Up @@ -130,6 +134,7 @@ tasks.preBuild.dependsOn("generateStrings")

val mediaVersion = "1.3.1"
val glideVersion = "4.16.0"
val acraVersion = "5.11.3"

dependencies {
implementation("androidx.core:core-ktx:1.13.1")
Expand All @@ -139,6 +144,7 @@ dependencies {
implementation("com.github.bumptech.glide:glide:$glideVersion")
implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion")
implementation("androidx.leanback:leanback-tab:1.1.0-beta01")
implementation("androidx.test.ext:junit-ktx:1.2.1")
kapt("com.github.bumptech.glide:compiler:$glideVersion")
implementation("com.caverock:androidsvg-aar:1.4")

Expand All @@ -162,6 +168,12 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1")
implementation("io.noties.markwon:core:4.6.2")

implementation("ch.acra:acra-http:$acraVersion")
implementation("ch.acra:acra-dialog:$acraVersion")
implementation("ch.acra:acra-limiter:$acraVersion")
compileOnly("com.google.auto.service:auto-service-annotations:1.1.1")
kapt("com.google.auto.service:auto-service:1.1.1")

testImplementation("androidx.test:core-ktx:1.6.1")
testImplementation("junit:junit:4.13.2")
testImplementation("org.mockito:mockito-core:5.9.0")
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/graphql/Configuration.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ query Configuration {
}
ui
}
version {
version
hash
build_time
}
plugins {
id
name
version
}
}

query ConfigurationUI {
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/graphql/RunPluginTask.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mutation RunPluginTask($plugin_id: ID!, $task_name: String!, $args_map: Map) {
runPluginTask(plugin_id: $plugin_id, task_name: $task_name, args_map: $args_map)
}
78 changes: 74 additions & 4 deletions app/src/main/java/com/github/damontecres/stashapp/DebugActivity.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
package com.github.damontecres.stashapp

import android.graphics.Color
import android.graphics.Typeface
import android.os.Bundle
import android.view.Gravity
import android.view.View
import android.widget.TableLayout
import android.widget.TableRow
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import com.github.damontecres.stashapp.playback.CodecSupport
import com.github.damontecres.stashapp.util.ServerPreferences
import com.github.damontecres.stashapp.util.StashClient
import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler
import com.github.damontecres.stashapp.util.StashServer
import com.github.damontecres.stashapp.util.plugin.CompanionPlugin
import kotlinx.coroutines.launch

class DebugActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
Expand All @@ -34,6 +42,8 @@ class DebugActivity : FragmentActivity() {
val prefTable = view.findViewById<TableLayout>(R.id.preferences_table)
val serverPrefTable = view.findViewById<TableLayout>(R.id.server_prefs_table)
val formatSupportedTable = view.findViewById<TableLayout>(R.id.supported_formats_table)
val otherTable = view.findViewById<TableLayout>(R.id.other_table)
val logTextView = view.findViewById<TextView>(R.id.logs)

val prefManager = PreferenceManager.getDefaultSharedPreferences(requireContext()).all
prefManager.keys.sorted().forEach {
Expand All @@ -42,9 +52,10 @@ class DebugActivity : FragmentActivity() {
}
prefTable.isStretchAllColumns = true

val serverPrefs = ServerPreferences(requireContext()).preferences.all
serverPrefs.keys.sorted().forEach {
val row = createRow(it, serverPrefs[it].toString())
val serverPrefs = ServerPreferences(requireContext())
val serverPrefsRaw = serverPrefs.preferences.all
serverPrefsRaw.keys.sorted().forEach {
val row = createRow(it, serverPrefsRaw[it].toString())
serverPrefTable.addView(row)
}
serverPrefTable.isStretchAllColumns = true
Expand All @@ -69,6 +80,54 @@ class DebugActivity : FragmentActivity() {
),
)
formatSupportedTable.isStretchAllColumns = true

val server = StashServer.getCurrentStashServer(requireContext())
if (server != null) {
otherTable.addView(
createRow(
"Current server URL",
server.url,
),
)
otherTable.addView(
createRow(
"Current server API Key",
server.apiKey,
),
)
otherTable.addView(
createRow(
"Current server URL (resolved endpoint)",
StashClient.cleanServerUrl(server.url),
),
)
otherTable.addView(
createRow(
"Current server URL (root)",
StashClient.getServerRoot(server.url),
),
)
}
otherTable.addView(
createRow(
"User-Agent",
StashClient.createUserAgent(requireContext()),
),
)
otherTable.isStretchAllColumns = true

viewLifecycleOwner.lifecycleScope.launch(
StashCoroutineExceptionHandler {
Toast.makeText(
requireContext(),
"Exception getting logs: ${it.message}",
Toast.LENGTH_LONG,
)
},
) {
val logs = CompanionPlugin.getLogCatLines(true).joinToString("\n")
logTextView.text = logs
}
}

private fun createRow(
Expand All @@ -95,8 +154,19 @@ class DebugActivity : FragmentActivity() {
// keyView.maxLines=8
// keyView.maxWidth=400

val isApiKey = key.contains("apikey", true) || key.contains("api key", true)

val valueView = TextView(requireContext())
valueView.text = if (key.contains("apikey", true)) "*****" else value
valueView.text =
if (isApiKey && value != null
) {
value.take(4) + "..." + value.takeLast(8)
} else {
value
}
if (isApiKey) {
valueView.typeface = Typeface.MONOSPACE
}
valueView.textSize = TABLE_TEXT_SIZE
valueView.setTextColor(Color.WHITE)
valueView.textAlignment = TextView.TEXT_ALIGNMENT_VIEW_START
Expand Down
70 changes: 33 additions & 37 deletions app/src/main/java/com/github/damontecres/stashapp/MainFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import androidx.leanback.widget.ListRowPresenter
import androidx.leanback.widget.SparseArrayObjectAdapter
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import com.github.damontecres.stashapp.api.ConfigurationQuery
import com.github.damontecres.stashapp.api.ServerInfoQuery
import com.github.damontecres.stashapp.api.fragment.ImageData
import com.github.damontecres.stashapp.api.fragment.SlimSceneData
Expand Down Expand Up @@ -295,44 +294,41 @@ class MainFragment : BrowseSupportFragment() {
?: Version.MINIMUM_STASH_VERSION,
)

val query = ConfigurationQuery()
val config = queryEngine.executeQuery(query).data?.configuration
ServerPreferences(requireContext()).updatePreferences(config, serverInfo)
val config = queryEngine.getServerConfiguration()
ServerPreferences(requireContext()).updatePreferences(config)

if (config?.ui != null) {
val ui = config.ui
val frontPageContent =
(ui as Map<String, *>).getCaseInsensitive("frontPageContent") as List<Map<String, *>>
val pageSize =
PreferenceManager.getDefaultSharedPreferences(requireContext())
.getInt(getString(R.string.pref_key_page_size), 25)
val frontPageParser =
FrontPageParser(queryEngine, filterParser, pageSize)
val jobs = frontPageParser.parse(frontPageContent)
jobs.forEachIndexed { index, job ->
job.await().let { row ->
if (row.successful) {
val rowData = row.data!!
filterList.set(index, rowData.filter)
val ui = config.configuration.ui
val frontPageContent =
(ui as Map<String, *>).getCaseInsensitive("frontPageContent") as List<Map<String, *>>
val pageSize =
PreferenceManager.getDefaultSharedPreferences(requireContext())
.getInt(getString(R.string.pref_key_page_size), 25)
val frontPageParser =
FrontPageParser(queryEngine, filterParser, pageSize)
val jobs = frontPageParser.parse(frontPageContent)
jobs.forEachIndexed { index, job ->
job.await().let { row ->
if (row.successful) {
val rowData = row.data!!
filterList.set(index, rowData.filter)

val adapter = ArrayObjectAdapter(StashPresenter.SELECTOR)
adapter.addAll(0, rowData.data)
adapter.add(rowData.filter)
adapters.add(adapter)
withContext(Dispatchers.Main) {
rowsAdapter.set(
index,
ListRow(HeaderItem(rowData.name), adapter),
)
}
} else if (row.result == FrontPageParser.FrontPageRowResult.ERROR) {
withContext(Dispatchers.Main) {
Toast.makeText(
requireContext(),
"Error loading row $index on front page",
Toast.LENGTH_SHORT,
).show()
}
val adapter = ArrayObjectAdapter(StashPresenter.SELECTOR)
adapter.addAll(0, rowData.data)
adapter.add(rowData.filter)
adapters.add(adapter)
withContext(Dispatchers.Main) {
rowsAdapter.set(
index,
ListRow(HeaderItem(rowData.name), adapter),
)
}
} else if (row.result == FrontPageParser.FrontPageRowResult.ERROR) {
withContext(Dispatchers.Main) {
Toast.makeText(
requireContext(),
"Error loading row $index on front page",
Toast.LENGTH_SHORT,
).show()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler
import com.github.damontecres.stashapp.util.StashServer
import com.github.damontecres.stashapp.util.UpdateChecker
import com.github.damontecres.stashapp.util.cacheDurationPrefToDuration
import com.github.damontecres.stashapp.util.plugin.CompanionPlugin
import com.github.damontecres.stashapp.util.testStashConnection
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -286,41 +287,54 @@ class SettingsFragment : LeanbackSettingsFragmentCompat() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
val serverPref = findPreference<Preference>(PREF_STASH_URL)!!
requireActivity().supportFragmentManager.addOnBackStackChangedListener {
val currentServer = StashServer.getCurrentStashServer(requireContext())
serverPref.summary = currentServer?.url
if (requireActivity().supportFragmentManager.backStackEntryCount == 0) {
refresh()
}
}
}
}

override fun onResume() {
super.onResume()
refresh()
}

private fun refresh() {
Log.v(TAG, "refresh")
val currentServer = StashServer.getCurrentStashServer(requireContext())
findPreference<Preference>(PREF_STASH_URL)!!.summary = currentServer?.url

val sendLogsPref = findPreference<LongClickPreference>("sendLogs")!!
sendLogsPref.isEnabled = false
sendLogsPref.summary = "Checking for companion plugin..."

viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
ServerPreferences(requireContext()).updatePreferences()
val serverPrefs = ServerPreferences(requireContext()).updatePreferences()
if (serverPrefs.companionPluginInstalled) {
sendLogsPref.isEnabled = true
sendLogsPref.summary = "Send a copy of recent app logs to your current server"
sendLogsPref.setOnPreferenceClickListener {
viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
CompanionPlugin.sendLogCat(requireContext(), false)
}
true
}
sendLogsPref.setOnLongClickListener {
viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
CompanionPlugin.sendLogCat(requireContext(), true)
}
true
}
} else {
sendLogsPref.isEnabled = false
sendLogsPref.summary = "Companion plugin not installed"
}
}
}

override fun onStop() {
super.onStop()
}

private fun setServers() {
val manager = PreferenceManager.getDefaultSharedPreferences(requireContext())
val keys =
manager.all.keys.filter { it.startsWith(SERVER_PREF_PREFIX) }.sorted().toList()
val values = keys.map { manager.all[it].toString() }.toList()

serverKeys = values
serverValues = keys
}

companion object {
const val TAG = "SettingsFragment"
private const val TAG = "SettingsFragment"

const val SERVER_PREF_PREFIX = "server_"
const val SERVER_APIKEY_PREF_PREFIX = "apikey_"
Expand Down
Loading

0 comments on commit ac164c5

Please sign in to comment.