manager: replace Runtime.exec with getRootShell for command execution

This commit is contained in:
ShirkNeko
2025-07-01 02:48:09 +08:00
parent 1fd13d9d8d
commit be14da387e
2 changed files with 45 additions and 53 deletions

View File

@@ -5,11 +5,8 @@ import android.content.Intent
import android.util.Log import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -19,7 +16,6 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -27,7 +23,6 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.sukisu.ultra.ui.component.* import com.sukisu.ultra.ui.component.*
@@ -37,13 +32,10 @@ import com.sukisu.ultra.ui.util.*
import java.io.File import java.io.File
import androidx.core.content.edit import androidx.core.content.edit
import com.sukisu.ultra.R import com.sukisu.ultra.R
import java.io.BufferedReader
import java.io.FileInputStream import java.io.FileInputStream
import java.io.InputStreamReader
import java.net.* import java.net.*
import android.app.Activity import android.app.Activity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
/** /**
* KPM 管理界面 * KPM 管理界面
@@ -54,7 +46,6 @@ import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
@Destination<RootGraph> @Destination<RootGraph>
@Composable @Composable
fun KpmScreen( fun KpmScreen(
navigator: DestinationsNavigator,
viewModel: KpmViewModel = viewModel() viewModel: KpmViewModel = viewModel()
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -92,18 +83,17 @@ fun KpmScreen(
LaunchedEffect(tempFileForInstall) { LaunchedEffect(tempFileForInstall) {
tempFileForInstall?.let { tempFile -> tempFileForInstall?.let { tempFile ->
try { try {
val command = arrayOf("su", "-c", "strings ${tempFile.absolutePath} | grep 'name='") val shell = getRootShell()
val process = Runtime.getRuntime().exec(command) val command = "strings ${tempFile.absolutePath} | grep 'name='"
val inputStream = process.inputStream val result = shell.newJob().add(command).to(ArrayList(), null).exec()
val reader = BufferedReader(InputStreamReader(inputStream)) if (result.isSuccess) {
var line: String? for (line in result.out) {
while (reader.readLine().also { line = it } != null) { if (line.startsWith("name=")) {
if (line!!.startsWith("name=")) { moduleName = line.substringAfter("name=").trim()
moduleName = line.substringAfter("name=").trim() break
break }
} }
} }
process.waitFor()
} catch (e: Exception) { } catch (e: Exception) {
Log.e("KsuCli", "Failed to get module name: ${e.message}", e) Log.e("KsuCli", "Failed to get module name: ${e.message}", e)
} }
@@ -434,18 +424,17 @@ private suspend fun handleModuleInstall(
) { ) {
var moduleId: String? = null var moduleId: String? = null
try { try {
val command = arrayOf("su", "-c", "strings ${tempFile.absolutePath} | grep 'name='") val shell = getRootShell()
val process = Runtime.getRuntime().exec(command) val command = "strings ${tempFile.absolutePath} | grep 'name='"
val inputStream = process.inputStream val result = shell.newJob().add(command).to(ArrayList(), null).exec()
val reader = BufferedReader(InputStreamReader(inputStream)) if (result.isSuccess) {
var line: String? for (line in result.out) {
while (reader.readLine().also { line = it } != null) { if (line.startsWith("name=")) {
if (line!!.startsWith("name=")) { moduleId = line.substringAfter("name=").trim()
moduleId = line.substringAfter("name=").trim() break
break }
} }
} }
process.waitFor()
} catch (e: Exception) { } catch (e: Exception) {
Log.e("KsuCli", "Failed to get module ID from strings command: ${e.message}", e) Log.e("KsuCli", "Failed to get module ID from strings command: ${e.message}", e)
} }
@@ -464,8 +453,9 @@ private suspend fun handleModuleInstall(
try { try {
if (isEmbed) { if (isEmbed) {
Runtime.getRuntime().exec(arrayOf("su", "-c", "mkdir -p /data/adb/kpm")).waitFor() val shell = getRootShell()
Runtime.getRuntime().exec(arrayOf("su", "-c", "cp ${tempFile.absolutePath} $targetPath")).waitFor() shell.newJob().add("mkdir -p /data/adb/kpm").exec()
shell.newJob().add("cp ${tempFile.absolutePath} $targetPath").exec()
} }
val loadResult = loadKpmModule(tempFile.absolutePath) val loadResult = loadKpmModule(tempFile.absolutePath)
@@ -509,8 +499,9 @@ private suspend fun handleModuleUninstall(
val moduleFilePath = "/data/adb/kpm/$moduleFileName" val moduleFilePath = "/data/adb/kpm/$moduleFileName"
val fileExists = try { val fileExists = try {
val result = Runtime.getRuntime().exec(arrayOf("su", "-c", "ls /data/adb/kpm/$moduleFileName")).waitFor() == 0 val shell = getRootShell()
result val result = shell.newJob().add("ls /data/adb/kpm/$moduleFileName").exec()
result.isSuccess
} catch (e: Exception) { } catch (e: Exception) {
Log.e("KsuCli", "Failed to check module file existence: ${e.message}", e) Log.e("KsuCli", "Failed to check module file existence: ${e.message}", e)
snackBarHost.showSnackbar( snackBarHost.showSnackbar(
@@ -519,6 +510,7 @@ private suspend fun handleModuleUninstall(
) )
false false
} }
val confirmResult = confirmDialog.awaitConfirm( val confirmResult = confirmDialog.awaitConfirm(
title = confirmTitle, title = confirmTitle,
content = confirmContent, content = confirmContent,
@@ -539,7 +531,8 @@ private suspend fun handleModuleUninstall(
} }
if (fileExists) { if (fileExists) {
Runtime.getRuntime().exec(arrayOf("su", "-c", "rm $moduleFilePath")).waitFor() val shell = getRootShell()
shell.newJob().add("rm $moduleFilePath").exec()
} }
viewModel.fetchModuleList() viewModel.fetchModuleList()
@@ -710,29 +703,31 @@ private fun KpmModuleItem(
} }
private fun checkStringsCommand(tempFile: File): Int { private fun checkStringsCommand(tempFile: File): Int {
val command = arrayOf("su", "-c", "strings ${tempFile.absolutePath} | grep -E 'name=|version=|license=|author='") val shell = getRootShell()
val process = Runtime.getRuntime().exec(command) val command = "strings ${tempFile.absolutePath} | grep -E 'name=|version=|license=|author='"
val inputStream = process.inputStream val result = shell.newJob().add(command).to(ArrayList(), null).exec()
val reader = BufferedReader(InputStreamReader(inputStream))
var line: String? if (!result.isSuccess) {
return 0
}
var matchCount = 0 var matchCount = 0
val keywords = listOf("name=", "version=", "license=", "author=") val keywords = listOf("name=", "version=", "license=", "author=")
var nameExists = false var nameExists = false
while (reader.readLine().also { line = it } != null) { for (line in result.out) {
if (!nameExists && line!!.startsWith("name=")) { if (!nameExists && line.startsWith("name=")) {
nameExists = true nameExists = true
matchCount++ matchCount++
} else if (nameExists) { } else if (nameExists) {
for (keyword in keywords) { for (keyword in keywords) {
if (line!!.startsWith(keyword)) { if (line.startsWith(keyword)) {
matchCount++ matchCount++
break break
} }
} }
} }
} }
process.waitFor()
return if (nameExists) matchCount else 0 return if (nameExists) matchCount else 0
} }

View File

@@ -15,11 +15,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.sukisu.ultra.ui.util.HanziToPinyin import com.sukisu.ultra.ui.util.HanziToPinyin
import com.sukisu.ultra.ui.util.listModules import com.sukisu.ultra.ui.util.listModules
import com.sukisu.ultra.ui.util.getRootShell
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.io.BufferedReader
import java.io.InputStreamReader
import java.text.Collator import java.text.Collator
import java.text.DecimalFormat import java.text.DecimalFormat
import java.util.Locale import java.util.Locale
@@ -405,14 +404,12 @@ class ModuleSizeCache(context: Context) {
*/ */
private fun calculateModuleFolderSize(dirId: String): Long { private fun calculateModuleFolderSize(dirId: String): Long {
return try { return try {
val process = Runtime.getRuntime().exec(arrayOf("su", "-c", "du -sb /data/adb/modules/$dirId")) val shell = getRootShell()
val reader = BufferedReader(InputStreamReader(process.inputStream)) val command = "du -sb /data/adb/modules/$dirId"
val output = reader.readLine() val result = shell.newJob().add(command).to(ArrayList(), null).exec()
process.waitFor()
reader.close()
if (output != null) { if (result.isSuccess && result.out.isNotEmpty()) {
val sizeStr = output.split("\t").firstOrNull() val sizeStr = result.out.firstOrNull()?.split("\t")?.firstOrNull()
sizeStr?.toLongOrNull() ?: 0L sizeStr?.toLongOrNull() ?: 0L
} else { } else {
0L 0L