Revert "manager: Optimized import, optimized all libsu shell calls, and fixed WebUI memory leaks #369

This reverts commit c3533861f2.
This commit is contained in:
ShirkNeko
2025-09-13 20:18:30 +08:00
parent 0c578e4518
commit cead5b03f4
9 changed files with 253 additions and 166 deletions

View File

@@ -12,8 +12,6 @@ import android.os.Bundle
import coil.Coil import coil.Coil
import coil.ImageLoader import coil.ImageLoader
import com.dergoogler.mmrl.platform.Platform import com.dergoogler.mmrl.platform.Platform
import com.sukisu.ultra.ui.util.createRootShellBuilder
import com.topjohnwu.superuser.Shell
import me.zhanghai.android.appiconloader.coil.AppIconFetcher import me.zhanghai.android.appiconloader.coil.AppIconFetcher
import me.zhanghai.android.appiconloader.coil.AppIconKeyer import me.zhanghai.android.appiconloader.coil.AppIconKeyer
import java.io.File import java.io.File
@@ -88,8 +86,6 @@ class KernelSUApplication : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
ksuApp = this ksuApp = this
Shell.setDefaultBuilder(createRootShellBuilder(true))
Shell.enableVerboseLogging = BuildConfig.DEBUG
// 注册Activity生命周期回调 // 注册Activity生命周期回调
registerActivityLifecycleCallbacks(activityLifecycleCallbacks) registerActivityLifecycleCallbacks(activityLifecycleCallbacks)

View File

@@ -1,6 +1,5 @@
package com.sukisu.ultra.ui.screen package com.sukisu.ultra.ui.screen
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.Log import android.util.Log
@@ -18,27 +17,25 @@ 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.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
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
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.edit
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.sukisu.ultra.R
import com.sukisu.ultra.ui.component.*
import com.sukisu.ultra.ui.theme.getCardColors
import com.sukisu.ultra.ui.theme.getCardElevation
import com.sukisu.ultra.ui.util.loadKpmModule
import com.sukisu.ultra.ui.util.unloadKpmModule
import com.sukisu.ultra.ui.viewmodel.KpmViewModel
import com.topjohnwu.superuser.Shell
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.theme.*
import com.sukisu.ultra.ui.viewmodel.KpmViewModel
import com.sukisu.ultra.ui.util.*
import java.io.File import java.io.File
import androidx.core.content.edit
import com.sukisu.ultra.R
import java.io.FileInputStream import java.io.FileInputStream
import java.net.URLEncoder import java.net.*
import android.app.Activity
import androidx.compose.ui.res.painterResource
/** /**
* KPM 管理界面 * KPM 管理界面
@@ -86,8 +83,9 @@ fun KpmScreen(
LaunchedEffect(tempFileForInstall) { LaunchedEffect(tempFileForInstall) {
tempFileForInstall?.let { tempFile -> tempFileForInstall?.let { tempFile ->
try { try {
val shell = getRootShell()
val command = "strings ${tempFile.absolutePath} | grep 'name='" val command = "strings ${tempFile.absolutePath} | grep 'name='"
val result = Shell.cmd(command).to(ArrayList(), null).exec() val result = shell.newJob().add(command).to(ArrayList(), null).exec()
if (result.isSuccess) { if (result.isSuccess) {
for (line in result.out) { for (line in result.out) {
if (line.startsWith("name=")) { if (line.startsWith("name=")) {
@@ -426,8 +424,9 @@ private suspend fun handleModuleInstall(
) { ) {
var moduleId: String? = null var moduleId: String? = null
try { try {
val shell = getRootShell()
val command = "strings ${tempFile.absolutePath} | grep 'name='" val command = "strings ${tempFile.absolutePath} | grep 'name='"
val result = Shell.cmd(command).to(ArrayList(), null).exec() val result = shell.newJob().add(command).to(ArrayList(), null).exec()
if (result.isSuccess) { if (result.isSuccess) {
for (line in result.out) { for (line in result.out) {
if (line.startsWith("name=")) { if (line.startsWith("name=")) {
@@ -454,8 +453,9 @@ private suspend fun handleModuleInstall(
try { try {
if (isEmbed) { if (isEmbed) {
Shell.cmd("mkdir -p /data/adb/kpm").exec() val shell = getRootShell()
Shell.cmd("cp ${tempFile.absolutePath} $targetPath").exec() 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)
@@ -499,7 +499,8 @@ private suspend fun handleModuleUninstall(
val moduleFilePath = "/data/adb/kpm/$moduleFileName" val moduleFilePath = "/data/adb/kpm/$moduleFileName"
val fileExists = try { val fileExists = try {
val result = Shell.cmd("ls /data/adb/kpm/$moduleFileName").exec() val shell = getRootShell()
val result = shell.newJob().add("ls /data/adb/kpm/$moduleFileName").exec()
result.isSuccess 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)
@@ -530,7 +531,8 @@ private suspend fun handleModuleUninstall(
} }
if (fileExists) { if (fileExists) {
Shell.cmd("rm $moduleFilePath").exec() val shell = getRootShell()
shell.newJob().add("rm $moduleFilePath").exec()
} }
viewModel.fetchModuleList() viewModel.fetchModuleList()
@@ -701,8 +703,9 @@ private fun KpmModuleItem(
} }
private fun checkStringsCommand(tempFile: File): Int { private fun checkStringsCommand(tempFile: File): Int {
val shell = getRootShell()
val command = "strings ${tempFile.absolutePath} | grep -E 'name=|version=|license=|author='" val command = "strings ${tempFile.absolutePath} | grep -E 'name=|version=|license=|author='"
val result = Shell.cmd(command).to(ArrayList(), null).exec() val result = shell.newJob().add(command).to(ArrayList(), null).exec()
if (!result.isSuccess) { if (!result.isSuccess) {
return 0 return 0

View File

@@ -10,15 +10,15 @@ import android.os.SystemClock
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.system.Os import android.system.Os
import android.util.Log import android.util.Log
import com.sukisu.ultra.Natives
import com.sukisu.ultra.ksuApp
import com.topjohnwu.superuser.CallbackList import com.topjohnwu.superuser.CallbackList
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils import com.topjohnwu.superuser.ShellUtils
import com.topjohnwu.superuser.io.SuFile
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import com.sukisu.ultra.BuildConfig
import com.sukisu.ultra.Natives
import com.sukisu.ultra.ksuApp
import org.json.JSONArray import org.json.JSONArray
import java.io.File import java.io.File
@@ -29,8 +29,19 @@ import java.io.File
*/ */
private const val TAG = "KsuCli" private const val TAG = "KsuCli"
private val ksuDaemonPath by lazy { private fun getKsuDaemonPath(): String {
"${ksuApp.applicationInfo.nativeLibraryDir}${File.separator}libzakozako.so" return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libzakozako.so"
}
object KsuCli {
val SHELL: Shell = createRootShell()
val GLOBAL_MNT_SHELL: Shell = createRootShell(true)
}
fun getRootShell(globalMnt: Boolean = false): Shell {
return if (globalMnt) KsuCli.GLOBAL_MNT_SHELL else {
KsuCli.SHELL
}
} }
inline fun <T> withNewRootShell( inline fun <T> withNewRootShell(
@@ -52,39 +63,37 @@ fun Uri.getFileName(context: Context): String? {
return fileName return fileName
} }
fun createRootShellBuilder(globalMnt: Boolean = false): Shell.Builder {
return Shell.Builder.create().run {
val cmd = buildString {
append("$ksuDaemonPath debug su")
if (globalMnt) append(" -g")
append(" || ")
append("su")
if (globalMnt) append(" --mount-master")
append(" || ")
append("sh")
}
setCommands("sh", "-c", cmd)
}
}
fun createRootShell(globalMnt: Boolean = false): Shell { fun createRootShell(globalMnt: Boolean = false): Shell {
return runCatching { Shell.enableVerboseLogging = BuildConfig.DEBUG
createRootShellBuilder(globalMnt).build() val builder = Shell.Builder.create()
}.getOrElse { e -> return try {
Log.w(TAG, "su failed: ", e) if (globalMnt) {
Shell.Builder.create().apply { builder.build(getKsuDaemonPath(), "debug", "su", "-g")
if (globalMnt) setFlags(Shell.FLAG_MOUNT_MASTER) } else {
}.build() builder.build(getKsuDaemonPath(), "debug", "su")
}
} catch (e: Throwable) {
Log.w(TAG, "ksu failed: ", e)
try {
if (globalMnt) {
builder.build("su", "-mm")
} else {
builder.build("su")
}
} catch (e: Throwable) {
Log.e(TAG, "su failed: ", e)
builder.build("sh")
}
} }
} }
fun execKsud(args: String, newShell: Boolean = false): Boolean { fun execKsud(args: String, newShell: Boolean = false): Boolean {
return if (newShell) { return if (newShell) {
withNewRootShell { withNewRootShell {
ShellUtils.fastCmdResult(this, "$ksuDaemonPath $args") ShellUtils.fastCmdResult(this, "${getKsuDaemonPath()} $args")
} }
} else { } else {
ShellUtils.fastCmdResult("$ksuDaemonPath $args") ShellUtils.fastCmdResult(getRootShell(), "${getKsuDaemonPath()} $args")
} }
} }
@@ -96,15 +105,19 @@ fun install() {
} }
fun listModules(): String { fun listModules(): String {
val shell = getRootShell()
val out = val out =
Shell.cmd("$ksuDaemonPath module list").to(ArrayList(), null).exec().out shell.newJob().add("${getKsuDaemonPath()} module list").to(ArrayList(), null).exec().out
return out.joinToString("\n").ifBlank { "[]" } return out.joinToString("\n").ifBlank { "[]" }
} }
fun getModuleCount(): Int { fun getModuleCount(): Int {
return runCatching { val result = listModules()
JSONArray(listModules()).length() runCatching {
}.getOrDefault(0) val array = JSONArray(result)
return array.length()
}.getOrElse { return 0 }
} }
fun getSuperuserCount(): Int { fun getSuperuserCount(): Int {
@@ -172,7 +185,7 @@ fun flashModule(
this?.copyTo(output) this?.copyTo(output)
} }
val cmd = "module install ${file.absolutePath}" val cmd = "module install ${file.absolutePath}"
val result = flashWithIO("$ksuDaemonPath $cmd", onStdout, onStderr) val result = flashWithIO("${getKsuDaemonPath()} $cmd", onStdout, onStderr)
Log.i("KernelSU", "install module $uri result: $result") Log.i("KernelSU", "install module $uri result: $result")
file.delete() file.delete()
@@ -199,7 +212,7 @@ fun runModuleAction(
} }
} }
val result = shell.newJob().add("$ksuDaemonPath module action $moduleId") val result = shell.newJob().add("${getKsuDaemonPath()} module action $moduleId")
.to(stdoutCallback, stderrCallback).exec() .to(stdoutCallback, stderrCallback).exec()
Log.i("KernelSU", "Module runAction result: $result") Log.i("KernelSU", "Module runAction result: $result")
@@ -211,7 +224,7 @@ fun restoreBoot(
): Boolean { ): Boolean {
val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libzakoboot.so") val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libzakoboot.so")
val result = flashWithIO( val result = flashWithIO(
"$ksuDaemonPath boot-restore -f --magiskboot $magiskboot", "${getKsuDaemonPath()} boot-restore -f --magiskboot $magiskboot",
onStdout, onStdout,
onStderr onStderr
) )
@@ -224,7 +237,7 @@ fun uninstallPermanently(
): Boolean { ): Boolean {
val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libzakoboot.so") val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libzakoboot.so")
val result = val result =
flashWithIO("$ksuDaemonPath uninstall --magiskboot $magiskboot", onStdout, onStderr) flashWithIO("${getKsuDaemonPath()} uninstall --magiskboot $magiskboot", onStdout, onStderr)
onFinish(result.isSuccess, result.code) onFinish(result.isSuccess, result.code)
return result.isSuccess return result.isSuccess
} }
@@ -299,7 +312,7 @@ fun installBoot(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
cmd += " -o $downloadsDir" cmd += " -o $downloadsDir"
val result = flashWithIO("$ksuDaemonPath $cmd", onStdout, onStderr) val result = flashWithIO("${getKsuDaemonPath()} $cmd", onStdout, onStderr)
Log.i("KernelSU", "install boot result: ${result.isSuccess}") Log.i("KernelSU", "install boot result: ${result.isSuccess}")
bootFile?.delete() bootFile?.delete()
@@ -311,17 +324,22 @@ fun installBoot(
} }
fun reboot(reason: String = "") { fun reboot(reason: String = "") {
val shell = getRootShell()
if (reason == "recovery") { if (reason == "recovery") {
// KEYCODE_POWER = 26, hide incorrect "Factory data reset" message // KEYCODE_POWER = 26, hide incorrect "Factory data reset" message
ShellUtils.fastCmdResult("/system/bin/input keyevent 26") ShellUtils.fastCmd(shell, "/system/bin/input keyevent 26")
} }
ShellUtils.fastCmdResult("/system/bin/svc power reboot $reason || /system/bin/reboot $reason") ShellUtils.fastCmd(shell, "/system/bin/svc power reboot $reason || /system/bin/reboot $reason")
} }
fun rootAvailable() = Shell.isAppGrantedRoot() == true fun rootAvailable(): Boolean {
val shell = getRootShell()
return shell.isRoot
}
fun isAbDevice(): Boolean { fun isAbDevice(): Boolean {
return ShellUtils.fastCmd("getprop ro.build.ab_update").trim().toBoolean() val shell = getRootShell()
return ShellUtils.fastCmd(shell, "getprop ro.build.ab_update").trim().toBoolean()
} }
fun isInitBoot(): Boolean { fun isInitBoot(): Boolean {
@@ -329,77 +347,91 @@ fun isInitBoot(): Boolean {
} }
suspend fun getCurrentKmi(): String = withContext(Dispatchers.IO) { suspend fun getCurrentKmi(): String = withContext(Dispatchers.IO) {
val shell = getRootShell()
val cmd = "boot-info current-kmi" val cmd = "boot-info current-kmi"
ShellUtils.fastCmd("$ksuDaemonPath $cmd") ShellUtils.fastCmd(shell, "${getKsuDaemonPath()} $cmd")
} }
suspend fun getSupportedKmis(): List<String> = withContext(Dispatchers.IO) { suspend fun getSupportedKmis(): List<String> = withContext(Dispatchers.IO) {
val shell = getRootShell()
val cmd = "boot-info supported-kmi" val cmd = "boot-info supported-kmi"
val out = Shell.cmd("$ksuDaemonPath $cmd").to(ArrayList(), null).exec().out val out = shell.newJob().add("${getKsuDaemonPath()} $cmd").to(ArrayList(), null).exec().out
out.filter { it.isNotBlank() }.map { it.trim() } out.filter { it.isNotBlank() }.map { it.trim() }
} }
fun hasMagisk(): Boolean { fun hasMagisk(): Boolean {
val result = ShellUtils.fastCmdResult("which magisk") val shell = getRootShell(true)
Log.i(TAG, "has magisk: $result") val result = shell.newJob().add("which magisk").exec()
return result Log.i(TAG, "has magisk: ${result.isSuccess}")
return result.isSuccess
} }
fun isSepolicyValid(rules: String?): Boolean { fun isSepolicyValid(rules: String?): Boolean {
if (rules == null) { if (rules == null) {
return true return true
} }
val shell = getRootShell()
val result = val result =
Shell.cmd("$ksuDaemonPath sepolicy check '$rules'").to(ArrayList(), null) shell.newJob().add("${getKsuDaemonPath()} sepolicy check '$rules'").to(ArrayList(), null)
.exec() .exec()
return result.isSuccess return result.isSuccess
} }
fun getSepolicy(pkg: String): String { fun getSepolicy(pkg: String): String {
val shell = getRootShell()
val result = val result =
Shell.cmd("$ksuDaemonPath profile get-sepolicy $pkg").to(ArrayList(), null) shell.newJob().add("${getKsuDaemonPath()} profile get-sepolicy $pkg").to(ArrayList(), null)
.exec() .exec()
Log.i(TAG, "code: ${result.code}, out: ${result.out}, err: ${result.err}") Log.i(TAG, "code: ${result.code}, out: ${result.out}, err: ${result.err}")
return result.out.joinToString("\n") return result.out.joinToString("\n")
} }
fun setSepolicy(pkg: String, rules: String): Boolean { fun setSepolicy(pkg: String, rules: String): Boolean {
val result = Shell.cmd("$ksuDaemonPath profile set-sepolicy $pkg '$rules'") val shell = getRootShell()
val result = shell.newJob().add("${getKsuDaemonPath()} profile set-sepolicy $pkg '$rules'")
.to(ArrayList(), null).exec() .to(ArrayList(), null).exec()
Log.i(TAG, "set sepolicy result: ${result.code}") Log.i(TAG, "set sepolicy result: ${result.code}")
return result.isSuccess return result.isSuccess
} }
fun listAppProfileTemplates(): List<String> { fun listAppProfileTemplates(): List<String> {
return Shell.cmd("$ksuDaemonPath profile list-templates").to(ArrayList(), null) val shell = getRootShell()
return shell.newJob().add("${getKsuDaemonPath()} profile list-templates").to(ArrayList(), null)
.exec().out .exec().out
} }
fun getAppProfileTemplate(id: String): String { fun getAppProfileTemplate(id: String): String {
return Shell.cmd("$ksuDaemonPath profile get-template '${id}'") val shell = getRootShell()
return shell.newJob().add("${getKsuDaemonPath()} profile get-template '${id}'")
.to(ArrayList(), null).exec().out.joinToString("\n") .to(ArrayList(), null).exec().out.joinToString("\n")
} }
fun setAppProfileTemplate(id: String, template: String): Boolean { fun setAppProfileTemplate(id: String, template: String): Boolean {
val shell = getRootShell()
val escapedTemplate = template.replace("\"", "\\\"") val escapedTemplate = template.replace("\"", "\\\"")
val cmd = """$ksuDaemonPath profile set-template "$id" "$escapedTemplate'"""" val cmd = """${getKsuDaemonPath()} profile set-template "$id" "$escapedTemplate'""""
return Shell.cmd(cmd) return shell.newJob().add(cmd)
.to(ArrayList(), null).exec().isSuccess .to(ArrayList(), null).exec().isSuccess
} }
fun deleteAppProfileTemplate(id: String): Boolean { fun deleteAppProfileTemplate(id: String): Boolean {
return Shell.cmd("$ksuDaemonPath profile delete-template '${id}'") val shell = getRootShell()
return shell.newJob().add("${getKsuDaemonPath()} profile delete-template '${id}'")
.to(ArrayList(), null).exec().isSuccess .to(ArrayList(), null).exec().isSuccess
} }
fun forceStopApp(packageName: String) { fun forceStopApp(packageName: String) {
val result = Shell.cmd("am force-stop $packageName").exec() val shell = getRootShell()
val result = shell.newJob().add("am force-stop $packageName").exec()
Log.i(TAG, "force stop $packageName result: $result") Log.i(TAG, "force stop $packageName result: $result")
} }
fun launchApp(packageName: String) { fun launchApp(packageName: String) {
val shell = getRootShell()
val result = val result =
Shell.cmd("cmd package resolve-activity --brief $packageName | tail -n 1 | xargs cmd activity start-activity -n") shell.newJob()
.add("cmd package resolve-activity --brief $packageName | tail -n 1 | xargs cmd activity start-activity -n")
.exec() .exec()
Log.i(TAG, "launch $packageName result: $result") Log.i(TAG, "launch $packageName result: $result")
} }
@@ -409,90 +441,131 @@ fun restartApp(packageName: String) {
launchApp(packageName) launchApp(packageName)
} }
val suSFSDaemonPath by lazy { fun getSuSFSDaemonPath(): String {
"${ksuApp.applicationInfo.nativeLibraryDir}${File.separator}libzakozakozako.so" return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libzakozakozako.so"
} }
fun getSuSFS(): String { fun getSuSFS(): String {
return ShellUtils.fastCmd("$suSFSDaemonPath support") val shell = getRootShell()
val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} support")
return result
} }
fun getSuSFSVersion(): String { fun getSuSFSVersion(): String {
return ShellUtils.fastCmd("$suSFSDaemonPath version") val shell = getRootShell()
val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} version")
return result
} }
fun getSuSFSVariant(): String { fun getSuSFSVariant(): String {
return ShellUtils.fastCmd("$suSFSDaemonPath variant") val shell = getRootShell()
val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} variant")
return result
} }
fun getSuSFSFeatures(): String { fun getSuSFSFeatures(): String {
return ShellUtils.fastCmd("$suSFSDaemonPath features") val shell = getRootShell()
val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} features")
return result
} }
fun susfsSUS_SU_0(): String { fun susfsSUS_SU_0(): String {
return ShellUtils.fastCmd("$suSFSDaemonPath sus_su 0") val shell = getRootShell()
val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} sus_su 0")
return result
} }
fun susfsSUS_SU_2(): String { fun susfsSUS_SU_2(): String {
return ShellUtils.fastCmd("$suSFSDaemonPath sus_su 2") val shell = getRootShell()
val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} sus_su 2")
return result
} }
fun susfsSUS_SU_Mode(): String { fun susfsSUS_SU_Mode(): String {
return ShellUtils.fastCmd("$suSFSDaemonPath sus_su mode") val shell = getRootShell()
val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} sus_su mode")
return result
} }
val kpmmgrPath by lazy { fun getKpmmgrPath(): String {
"${ksuApp.applicationInfo.nativeLibraryDir}${File.separator}libkpmmgr.so" return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libkpmmgr.so"
} }
fun loadKpmModule(path: String, args: String? = null): String { fun loadKpmModule(path: String, args: String? = null): String {
return ShellUtils.fastCmd("$kpmmgrPath load $path ${args ?: ""}") val shell = getRootShell()
val cmd = "${getKpmmgrPath()} load $path ${args ?: ""}"
return ShellUtils.fastCmd(shell, cmd)
} }
fun unloadKpmModule(name: String): String { fun unloadKpmModule(name: String): String {
return ShellUtils.fastCmd("$kpmmgrPath unload $name") val shell = getRootShell()
val cmd = "${getKpmmgrPath()} unload $name"
return ShellUtils.fastCmd(shell, cmd)
} }
fun getKpmModuleCount(): Int { fun getKpmModuleCount(): Int {
val result = ShellUtils.fastCmd("$kpmmgrPath num") val shell = getRootShell()
val cmd = "${getKpmmgrPath()} num"
val result = ShellUtils.fastCmd(shell, cmd)
return result.trim().toIntOrNull() ?: 0 return result.trim().toIntOrNull() ?: 0
} }
fun runCmd(cmd: String): String { fun runCmd(shell: Shell, cmd: String): String {
return Shell.cmd(cmd) return shell.newJob()
.add(cmd)
.to(mutableListOf<String>(), null) .to(mutableListOf<String>(), null)
.exec().out .exec().out
.joinToString("\n") .joinToString("\n")
} }
fun listKpmModules(): String { fun listKpmModules(): String {
return runCmd("$kpmmgrPath list").trim() val shell = getRootShell()
val cmd = "${getKpmmgrPath()} list"
return try {
runCmd(shell, cmd).trim()
} catch (e: Exception) {
Log.e(TAG, "Failed to list KPM modules", e)
""
}
} }
fun getKpmModuleInfo(name: String): String { fun getKpmModuleInfo(name: String): String {
return runCmd("$kpmmgrPath info $name").trim() val shell = getRootShell()
val cmd = "${getKpmmgrPath()} info $name"
return try {
runCmd(shell, cmd).trim()
} catch (e: Exception) {
Log.e(TAG, "Failed to get KPM module info: $name", e)
""
}
} }
fun controlKpmModule(name: String, args: String? = null): Int { fun controlKpmModule(name: String, args: String? = null): Int {
val result = runCmd("""$kpmmgrPath control $name "${args ?: ""}"""") val shell = getRootShell()
val cmd = """${getKpmmgrPath()} control $name "${args ?: ""}""""
val result = runCmd(shell, cmd)
return result.trim().toIntOrNull() ?: -1 return result.trim().toIntOrNull() ?: -1
} }
fun getKpmVersion(): String { fun getKpmVersion(): String {
return ShellUtils.fastCmd("$kpmmgrPath version").trim() val shell = getRootShell()
val cmd = "${getKpmmgrPath()} version"
val result = ShellUtils.fastCmd(shell, cmd)
return result.trim()
} }
fun getZygiskImplement(): String { fun getZygiskImplement(): String {
val shell = getRootShell()
val zygiskPath = "/data/adb/modules/zygisksu" val zygiskPath = "/data/adb/modules/zygisksu"
val rezygiskPath = "/data/adb/modules/rezygisk" val rezygiskPath = "/data/adb/modules/rezygisk"
val result = when { val result = if (ShellUtils.fastCmdResult(shell, "test -f $zygiskPath/module.prop && test ! -f $zygiskPath/disable")) {
SuFile(zygiskPath, "module.prop").exists() && !SuFile(zygiskPath, "disable").exists() -> ShellUtils.fastCmd(shell, "grep '^name=' $zygiskPath/module.prop | cut -d'=' -f2")
ShellUtils.fastCmd("grep '^name=' $zygiskPath/module.prop | cut -d'=' -f2") } else if (ShellUtils.fastCmdResult(shell, "test -f $rezygiskPath/module.prop && test ! -f $rezygiskPath/disable")) {
SuFile(rezygiskPath, "module.prop").exists() && !SuFile(rezygiskPath, "disable").exists() -> ShellUtils.fastCmd(shell, "grep '^name=' $rezygiskPath/module.prop | cut -d'=' -f2")
ShellUtils.fastCmd("grep '^name=' $rezygiskPath/module.prop | cut -d'=' -f2") } else {
else -> "None" "None"
}.trim() }
Log.i(TAG, "Zygisk implement: $result") Log.i(TAG, "Zygisk implement: $result")
return result return result
} }

View File

@@ -3,10 +3,9 @@ package com.sukisu.ultra.ui.util
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.system.Os import android.system.Os
import com.topjohnwu.superuser.ShellUtils
import com.sukisu.ultra.Natives import com.sukisu.ultra.Natives
import com.sukisu.ultra.ui.screen.getManagerVersion import com.sukisu.ultra.ui.screen.getManagerVersion
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils
import java.io.File import java.io.File
import java.io.FileWriter import java.io.FileWriter
import java.io.PrintWriter import java.io.PrintWriter
@@ -39,28 +38,30 @@ fun getBugreportFile(context: Context): File {
val bootConfig = File(bugreportDir, "boot_config.txt") val bootConfig = File(bugreportDir, "boot_config.txt")
val kernelConfig = File(bugreportDir, "defconfig.gz") val kernelConfig = File(bugreportDir, "defconfig.gz")
Shell.cmd("dmesg > ${dmesgFile.absolutePath}").exec() val shell = getRootShell(true)
Shell.cmd("logcat -d > ${logcatFile.absolutePath}").exec()
Shell.cmd("tar -czf ${tombstonesFile.absolutePath} -C /data/tombstones .").exec()
Shell.cmd("tar -czf ${dropboxFile.absolutePath} -C /data/system/dropbox .").exec()
Shell.cmd("tar -czf ${pstoreFile.absolutePath} -C /sys/fs/pstore .").exec()
Shell.cmd("tar -czf ${diagFile.absolutePath} -C /data/vendor/diag . --exclude=./minidump.gz").exec()
Shell.cmd("tar -czf ${oplusFile.absolutePath} -C /mnt/oplus/op2/media/log/boot_log/ .").exec()
Shell.cmd("tar -czf ${bootlogFile.absolutePath} -C /data/adb/ksu/log .").exec()
Shell.cmd("cat /proc/1/mountinfo > ${mountsFile.absolutePath}").exec() shell.newJob().add("dmesg > ${dmesgFile.absolutePath}").exec()
Shell.cmd("cat /proc/filesystems > ${fileSystemsFile.absolutePath}").exec() shell.newJob().add("logcat -d > ${logcatFile.absolutePath}").exec()
Shell.cmd("busybox tree /data/adb > ${adbFileTree.absolutePath}").exec() shell.newJob().add("tar -czf ${tombstonesFile.absolutePath} -C /data/tombstones .").exec()
Shell.cmd("ls -alRZ /data/adb > ${adbFileDetails.absolutePath}").exec() shell.newJob().add("tar -czf ${dropboxFile.absolutePath} -C /data/system/dropbox .").exec()
Shell.cmd("du -sh /data/adb/ksu/* > ${ksuFileSize.absolutePath}").exec() shell.newJob().add("tar -czf ${pstoreFile.absolutePath} -C /sys/fs/pstore .").exec()
Shell.cmd("cp /data/system/packages.list ${appListFile.absolutePath}").exec() shell.newJob().add("tar -czf ${diagFile.absolutePath} -C /data/vendor/diag . --exclude=./minidump.gz").exec()
Shell.cmd("getprop > ${propFile.absolutePath}").exec() shell.newJob().add("tar -czf ${oplusFile.absolutePath} -C /mnt/oplus/op2/media/log/boot_log/ .").exec()
Shell.cmd("cp /data/adb/ksu/.allowlist ${allowListFile.absolutePath}").exec() shell.newJob().add("tar -czf ${bootlogFile.absolutePath} -C /data/adb/ksu/log .").exec()
Shell.cmd("cp /proc/modules ${procModules.absolutePath}").exec()
Shell.cmd("cp /proc/bootconfig ${bootConfig.absolutePath}").exec()
Shell.cmd("cp /proc/config.gz ${kernelConfig.absolutePath}").exec()
val selinux = ShellUtils.fastCmd("getenforce") shell.newJob().add("cat /proc/1/mountinfo > ${mountsFile.absolutePath}").exec()
shell.newJob().add("cat /proc/filesystems > ${fileSystemsFile.absolutePath}").exec()
shell.newJob().add("busybox tree /data/adb > ${adbFileTree.absolutePath}").exec()
shell.newJob().add("ls -alRZ /data/adb > ${adbFileDetails.absolutePath}").exec()
shell.newJob().add("du -sh /data/adb/ksu/* > ${ksuFileSize.absolutePath}").exec()
shell.newJob().add("cp /data/system/packages.list ${appListFile.absolutePath}").exec()
shell.newJob().add("getprop > ${propFile.absolutePath}").exec()
shell.newJob().add("cp /data/adb/ksu/.allowlist ${allowListFile.absolutePath}").exec()
shell.newJob().add("cp /proc/modules ${procModules.absolutePath}").exec()
shell.newJob().add("cp /proc/bootconfig ${bootConfig.absolutePath}").exec()
shell.newJob().add("cp /proc/config.gz ${kernelConfig.absolutePath}").exec()
val selinux = ShellUtils.fastCmd(shell, "getenforce")
// basic information // basic information
val buildInfo = File(bugreportDir, "basic.txt") val buildInfo = File(bugreportDir, "basic.txt")
@@ -101,9 +102,9 @@ fun getBugreportFile(context: Context): File {
val targetFile = File(context.cacheDir, "KernelSU_bugreport_${current}.tar.gz") val targetFile = File(context.cacheDir, "KernelSU_bugreport_${current}.tar.gz")
Shell.cmd("tar czf ${targetFile.absolutePath} -C ${bugreportDir.absolutePath} .").exec() shell.newJob().add("tar czf ${targetFile.absolutePath} -C ${bugreportDir.absolutePath} .").exec()
Shell.cmd("rm -rf ${bugreportDir.absolutePath}").exec() shell.newJob().add("rm -rf ${bugreportDir.absolutePath}").exec()
Shell.cmd("chmod 0644 ${targetFile.absolutePath}").exec() shell.newJob().add("chmod 0644 ${targetFile.absolutePath}").exec()
return targetFile return targetFile
} }

View File

@@ -4,7 +4,6 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import com.sukisu.ultra.Natives import com.sukisu.ultra.Natives
import com.topjohnwu.superuser.Shell
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
@@ -125,17 +124,18 @@ object ModuleVerificationManager {
// 为指定模块创建验证标志文件 // 为指定模块创建验证标志文件
fun createVerificationFlag(moduleId: String): Boolean { fun createVerificationFlag(moduleId: String): Boolean {
return try { return try {
val shell = getRootShell()
val flagFilePath = "$VERIFICATION_FLAGS_DIR/$moduleId" val flagFilePath = "$VERIFICATION_FLAGS_DIR/$moduleId"
// 确保目录存在 // 确保目录存在
val createDirCommand = "mkdir -p '$VERIFICATION_FLAGS_DIR'" val createDirCommand = "mkdir -p '$VERIFICATION_FLAGS_DIR'"
Shell.cmd(createDirCommand).exec() shell.newJob().add(createDirCommand).exec()
// 创建验证标志文件,写入验证时间戳 // 创建验证标志文件,写入验证时间戳
val timestamp = System.currentTimeMillis() val timestamp = System.currentTimeMillis()
val command = "echo '$timestamp' > '$flagFilePath'" val command = "echo '$timestamp' > '$flagFilePath'"
val result = Shell.cmd(command).exec() val result = shell.newJob().add(command).exec()
if (result.isSuccess) { if (result.isSuccess) {
Log.d(TAG, "验证标志文件创建成功: $flagFilePath") Log.d(TAG, "验证标志文件创建成功: $flagFilePath")
@@ -152,10 +152,11 @@ object ModuleVerificationManager {
fun removeVerificationFlag(moduleId: String): Boolean { fun removeVerificationFlag(moduleId: String): Boolean {
return try { return try {
val shell = getRootShell()
val flagFilePath = "$VERIFICATION_FLAGS_DIR/$moduleId" val flagFilePath = "$VERIFICATION_FLAGS_DIR/$moduleId"
val command = "rm -f '$flagFilePath'" val command = "rm -f '$flagFilePath'"
val result = Shell.cmd(command).exec() val result = shell.newJob().add(command).exec()
if (result.isSuccess) { if (result.isSuccess) {
Log.d(TAG, "验证标志文件移除成功: $flagFilePath") Log.d(TAG, "验证标志文件移除成功: $flagFilePath")
@@ -172,10 +173,11 @@ object ModuleVerificationManager {
fun getVerificationTimestamp(moduleId: String): Long { fun getVerificationTimestamp(moduleId: String): Long {
return try { return try {
val shell = getRootShell()
val flagFilePath = "$VERIFICATION_FLAGS_DIR/$moduleId" val flagFilePath = "$VERIFICATION_FLAGS_DIR/$moduleId"
val command = "cat '$flagFilePath' 2>/dev/null || echo '0'" val command = "cat '$flagFilePath' 2>/dev/null || echo '0'"
val result = Shell.cmd(command).to(ArrayList(), null).exec() val result = shell.newJob().add(command).to(ArrayList(), null).exec()
if (result.isSuccess && result.out.isNotEmpty()) { if (result.isSuccess && result.out.isNotEmpty()) {
val timestampStr = result.out.firstOrNull()?.trim() ?: "0" val timestampStr = result.out.firstOrNull()?.trim() ?: "0"
@@ -193,11 +195,12 @@ object ModuleVerificationManager {
if (moduleIds.isEmpty()) return emptyMap() if (moduleIds.isEmpty()) return emptyMap()
return try { return try {
val shell = getRootShell()
val result = mutableMapOf<String, Boolean>() val result = mutableMapOf<String, Boolean>()
// 确保目录存在 // 确保目录存在
val createDirCommand = "mkdir -p '$VERIFICATION_FLAGS_DIR'" val createDirCommand = "mkdir -p '$VERIFICATION_FLAGS_DIR'"
Shell.cmd(createDirCommand).exec() shell.newJob().add(createDirCommand).exec()
// 批量检查所有模块的验证标志文件 // 批量检查所有模块的验证标志文件
val commands = moduleIds.map { moduleId -> val commands = moduleIds.map { moduleId ->
@@ -205,7 +208,7 @@ object ModuleVerificationManager {
} }
val command = commands.joinToString(" && ") val command = commands.joinToString(" && ")
val shellResult = Shell.cmd(command).to(ArrayList(), null).exec() val shellResult = shell.newJob().add(command).to(ArrayList(), null).exec()
if (shellResult.isSuccess) { if (shellResult.isSuccess) {
shellResult.out.forEach { line -> shellResult.out.forEach { line ->

View File

@@ -7,17 +7,22 @@ import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.core.content.edit
import com.dergoogler.mmrl.platform.Platform.Companion.context import com.dergoogler.mmrl.platform.Platform.Companion.context
import com.sukisu.ultra.Natives import com.sukisu.ultra.Natives
import com.sukisu.ultra.R import com.sukisu.ultra.R
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.* import kotlinx.coroutines.Dispatchers
import org.json.JSONObject import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import androidx.core.content.edit
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import org.json.JSONObject
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@@ -425,10 +430,12 @@ object SuSFSManager {
async(Dispatchers.IO) { async(Dispatchers.IO) {
val dataPath = "$MEDIA_DATA_PATH/${appInfo.packageName}" val dataPath = "$MEDIA_DATA_PATH/${appInfo.packageName}"
val exists = try { val exists = try {
val shell = getRootShell()
val outputList = mutableListOf<String>() val outputList = mutableListOf<String>()
val errorList = mutableListOf<String>() val errorList = mutableListOf<String>()
val result = Shell.cmd("[ -d \"$dataPath\" ] && echo 'exists' || echo 'not_exists'") val result = shell.newJob()
.add("[ -d \"$dataPath\" ] && echo 'exists' || echo 'not_exists'")
.to(outputList, errorList) .to(outputList, errorList)
.exec() .exec()

View File

@@ -7,26 +7,26 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.core.content.edit
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.dergoogler.mmrl.platform.model.ModuleConfig import com.dergoogler.mmrl.platform.model.ModuleConfig
import com.dergoogler.mmrl.platform.model.ModuleConfig.Companion.asModuleConfig import com.dergoogler.mmrl.platform.model.ModuleConfig.Companion.asModuleConfig
import com.sukisu.ultra.ui.util.HanziToPinyin
import com.sukisu.ultra.ui.util.ModuleVerificationManager
import com.sukisu.ultra.ui.util.listModules
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers 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.listModules
import com.sukisu.ultra.ui.util.getRootShell
import com.sukisu.ultra.ui.util.ModuleVerificationManager
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.text.Collator import java.text.Collator
import java.text.DecimalFormat import java.text.DecimalFormat
import java.util.* import java.util.Locale
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.math.log10 import kotlin.math.log10
import kotlin.math.pow import kotlin.math.pow
import androidx.core.content.edit
/** /**
* @author ShirkNeko * @author ShirkNeko
@@ -452,8 +452,9 @@ class ModuleSizeCache(context: Context) {
*/ */
private fun calculateModuleFolderSize(dirId: String): Long { private fun calculateModuleFolderSize(dirId: String): Long {
return try { return try {
val shell = getRootShell()
val command = "du -sb /data/adb/modules/$dirId" val command = "du -sb /data/adb/modules/$dirId"
val result = Shell.cmd(command).to(ArrayList(), null).exec() val result = shell.newJob().add(command).to(ArrayList(), null).exec()
if (result.isSuccess && result.out.isNotEmpty()) { if (result.isSuccess && result.out.isNotEmpty()) {
val sizeStr = result.out.firstOrNull()?.split("\t")?.firstOrNull() val sizeStr = result.out.firstOrNull()?.split("\t")?.firstOrNull()

View File

@@ -1,13 +1,15 @@
package io.sukisu.ultra; package io.sukisu.ultra;
import com.topjohnwu.superuser.Shell;
import java.util.ArrayList; import java.util.ArrayList;
import com.sukisu.ultra.ui.util.KsuCli;
public class UltraShellHelper { public class UltraShellHelper {
public static String runCmd(String cmds) { public static String runCmd(String cmds) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
for(String str : Shell.cmd(cmds) for(String str : KsuCli.INSTANCE.getGLOBAL_MNT_SHELL()
.newJob()
.add(cmds)
.to(new ArrayList<>(), null) .to(new ArrayList<>(), null)
.exec() .exec()
.getOut()) { .getOut()) {
@@ -16,6 +18,11 @@ public class UltraShellHelper {
return sb.toString(); return sb.toString();
} }
public static boolean isPathExists(String path) {
String result = runCmd("test -f '" + path + "' && echo 'exists'");
return result.contains("exists");
}
public static void CopyFileTo(String path, String target) { public static void CopyFileTo(String path, String target) {
runCmd("cp -f '" + path + "' '" + target + "' 2>&1"); runCmd("cp -f '" + path + "' '" + target + "' 2>&1");
} }

View File

@@ -1,7 +1,5 @@
package io.sukisu.ultra; package io.sukisu.ultra;
import com.topjohnwu.superuser.io.SuFile;
import static com.sukisu.ultra.ui.util.KsuCliKt.getKpmmgrPath; import static com.sukisu.ultra.ui.util.KsuCliKt.getKpmmgrPath;
import static com.sukisu.ultra.ui.util.KsuCliKt.getSuSFSDaemonPath; import static com.sukisu.ultra.ui.util.KsuCliKt.getSuSFSDaemonPath;
@@ -13,17 +11,15 @@ public class UltraToolInstall {
@SuppressLint("SetWorldReadable") @SuppressLint("SetWorldReadable")
@SuppressWarnings("ResultOfMethodCallIgnored") @SuppressWarnings("ResultOfMethodCallIgnored")
public static void tryToInstall() { public static void tryToInstall() {
SuFile KpmmgrFile = new SuFile(OUTSIDE_KPMMGR_PATH); String kpmmgrPath = getKpmmgrPath();
if (KpmmgrFile.exists()) { if (UltraShellHelper.isPathExists(OUTSIDE_KPMMGR_PATH)) {
UltraShellHelper.CopyFileTo(getKpmmgrPath(), OUTSIDE_KPMMGR_PATH); UltraShellHelper.CopyFileTo(kpmmgrPath, OUTSIDE_KPMMGR_PATH);
KpmmgrFile.setReadable(true, false); UltraShellHelper.runCmd("chmod a+rx " + OUTSIDE_KPMMGR_PATH);
KpmmgrFile.setExecutable(true, false);
} }
SuFile SuSFSDaemonFile = new SuFile(OUTSIDE_SUSFSD_PATH); String SuSFSDaemonPath = getSuSFSDaemonPath();
if (SuSFSDaemonFile.exists()) { if (UltraShellHelper.isPathExists(OUTSIDE_SUSFSD_PATH)) {
UltraShellHelper.CopyFileTo(getSuSFSDaemonPath(), OUTSIDE_SUSFSD_PATH); UltraShellHelper.CopyFileTo(SuSFSDaemonPath, OUTSIDE_SUSFSD_PATH);
SuSFSDaemonFile.setReadable(true, false); UltraShellHelper.runCmd("chmod a+rx " + OUTSIDE_SUSFSD_PATH);
SuSFSDaemonFile.setExecutable(true, false);
} }
} }
} }