Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package me.weishu.kernelsu

import android.app.Application
import android.system.Os
import com.topjohnwu.superuser.Shell
import me.weishu.kernelsu.ui.util.createRootShellBuilder
import okhttp3.Cache
import okhttp3.OkHttpClient
import java.io.File
Expand All @@ -16,6 +18,8 @@ class KernelSUApplication : Application() {
override fun onCreate() {
super.onCreate()
ksuApp = this
Shell.setDefaultBuilder(createRootShellBuilder(true))
Shell.enableVerboseLogging = BuildConfig.DEBUG

val webroot = File(dataDir, "webroot")
if (!webroot.exists()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import dev.chrisbanes.haze.HazeTint
import dev.chrisbanes.haze.hazeSource
import kotlinx.coroutines.launch
import me.weishu.kernelsu.Natives
import me.weishu.kernelsu.ksuApp
import me.weishu.kernelsu.ui.component.BottomBar
import me.weishu.kernelsu.ui.screen.HomePager
import me.weishu.kernelsu.ui.screen.ModulePager
Expand All @@ -59,7 +58,7 @@ class MainActivity : ComponentActivity() {

super.onCreate(savedInstanceState)

val isManager = Natives.becomeManager(ksuApp.packageName)
val isManager = Natives.becomeManager(packageName)
if (isManager) install()

setContent {
Expand Down
142 changes: 55 additions & 87 deletions manager/app/src/main/java/me/weishu/kernelsu/ui/util/KsuCli.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import com.topjohnwu.superuser.ShellUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import me.weishu.kernelsu.BuildConfig
import me.weishu.kernelsu.Natives
import me.weishu.kernelsu.ksuApp
import org.json.JSONArray
Expand All @@ -29,26 +28,15 @@ import java.io.File
*/
private const val TAG = "KsuCli"

private fun getKsuDaemonPath(): String {
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libksud.so"
private val ksuDaemonPath by lazy {
"${ksuApp.applicationInfo.nativeLibraryDir}${File.separator}libksud.so"
}

data class FlashResult(val code: Int, val err: String, val showReboot: Boolean) {
constructor(result: Shell.Result, showReboot: Boolean) : this(result.code, result.err.joinToString("\n"), showReboot)
constructor(result: Shell.Result) : this(result, result.isSuccess)
}

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(
globalMnt: Boolean = false,
block: Shell.() -> T
Expand All @@ -68,37 +56,39 @@ fun Uri.getFileName(context: Context): String? {
return fileName
}

fun createRootShell(globalMnt: Boolean = false): Shell {
Shell.enableVerboseLogging = BuildConfig.DEBUG
val builder = Shell.Builder.create()
return try {
if (globalMnt) {
builder.build(getKsuDaemonPath(), "debug", "su", "-g")
} else {
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 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 {
return runCatching {
createRootShellBuilder(globalMnt).build()
}.getOrElse { e ->
Log.w(TAG, "su failed: ", e)
Shell.Builder.create().apply {
Copy link

Copilot AI Sep 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The fallback shell creation logic should be consistent with the primary shell builder. Consider using createRootShellBuilder(globalMnt).setFlags() to maintain consistency.

Suggested change
Shell.Builder.create().apply {
createRootShellBuilder(globalMnt).apply {

Copilot uses AI. Check for mistakes.
if (globalMnt) setFlags(Shell.FLAG_MOUNT_MASTER)
}.build()
}
}

fun execKsud(args: String, newShell: Boolean = false): Boolean {
return if (newShell) {
withNewRootShell {
ShellUtils.fastCmdResult(this, "${getKsuDaemonPath()} $args")
ShellUtils.fastCmdResult(this, "$ksuDaemonPath $args")
}
} else {
ShellUtils.fastCmdResult(getRootShell(), "${getKsuDaemonPath()} $args")
ShellUtils.fastCmdResult("$ksuDaemonPath $args")
}
}

Expand All @@ -110,19 +100,17 @@ fun install() {
}

fun listModules(): String {
val shell = getRootShell()

val out =
shell.newJob().add("${getKsuDaemonPath()} module list").to(ArrayList(), null).exec().out
Shell.cmd("$ksuDaemonPath module list").to(ArrayList(), null).exec().out
return out.joinToString("\n").ifBlank { "[]" }
}

fun getModuleCount(): Int {
val result = listModules()
runCatching {
return runCatching {
val array = JSONArray(result)
return array.length()
}.getOrElse { return 0 }
array.length()
}.getOrDefault(0)
}

fun getSuperuserCount(): Int {
Expand Down Expand Up @@ -182,7 +170,7 @@ fun flashModule(
this?.copyTo(output)
}
val cmd = "module install ${file.absolutePath}"
val result = flashWithIO("${getKsuDaemonPath()} $cmd", onStdout, onStderr)
val result = flashWithIO("$ksuDaemonPath $cmd", onStdout, onStderr)
Log.i("KernelSU", "install module $uri result: $result")

file.delete()
Expand All @@ -208,7 +196,7 @@ fun runModuleAction(
}
}

val result = shell.newJob().add("${getKsuDaemonPath()} module action $moduleId")
val result = shell.newJob().add("$ksuDaemonPath module action $moduleId")
.to(stdoutCallback, stderrCallback).exec()
Log.i("KernelSU", "Module runAction result: $result")

Expand All @@ -219,15 +207,15 @@ fun restoreBoot(
onStdout: (String) -> Unit, onStderr: (String) -> Unit
): FlashResult {
val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so")
val result = flashWithIO("${getKsuDaemonPath()} boot-restore -f --magiskboot $magiskboot", onStdout, onStderr)
val result = flashWithIO("$ksuDaemonPath boot-restore -f --magiskboot $magiskboot", onStdout, onStderr)
return FlashResult(result)
}

fun uninstallPermanently(
onStdout: (String) -> Unit, onStderr: (String) -> Unit
): FlashResult {
val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so")
val result = flashWithIO("${getKsuDaemonPath()} uninstall --magiskboot $magiskboot", onStdout, onStderr)
val result = flashWithIO("$ksuDaemonPath uninstall --magiskboot $magiskboot", onStdout, onStderr)
return FlashResult(result)
}

Expand Down Expand Up @@ -304,7 +292,7 @@ fun installBoot(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
cmd += " -o $downloadsDir"

val result = flashWithIO("${getKsuDaemonPath()} $cmd", onStdout, onStderr)
val result = flashWithIO("$ksuDaemonPath $cmd", onStdout, onStderr)
Log.i("KernelSU", "install boot result: ${result.isSuccess}")

bootFile?.delete()
Expand All @@ -315,120 +303,100 @@ fun installBoot(
}

fun reboot(reason: String = "") {
val shell = getRootShell()
if (reason == "recovery") {
// KEYCODE_POWER = 26, hide incorrect "Factory data reset" message
ShellUtils.fastCmd(shell, "/system/bin/input keyevent 26")
ShellUtils.fastCmd("/system/bin/input keyevent 26")
}
ShellUtils.fastCmd(shell, "/system/bin/svc power reboot $reason || /system/bin/reboot $reason")
ShellUtils.fastCmd("/system/bin/svc power reboot $reason || /system/bin/reboot $reason")
}

fun rootAvailable(): Boolean {
val shell = getRootShell()
return shell.isRoot
}
fun rootAvailable() = Shell.isAppGrantedRoot() == true

fun isAbDevice(): Boolean {
val shell = getRootShell()
return ShellUtils.fastCmd(shell, "getprop ro.build.ab_update").trim().toBoolean()
return ShellUtils.fastCmd("getprop ro.build.ab_update").trim().toBoolean()
}

fun isInitBoot(): Boolean {
return !Os.uname().release.contains("android12-")
}

suspend fun getCurrentKmi(): String = withContext(Dispatchers.IO) {
val shell = getRootShell()
val cmd = "boot-info current-kmi"
ShellUtils.fastCmd(shell, "${getKsuDaemonPath()} $cmd")
ShellUtils.fastCmd("$ksuDaemonPath $cmd")
}

suspend fun getSupportedKmis(): List<String> = withContext(Dispatchers.IO) {
val shell = getRootShell()
val cmd = "boot-info supported-kmi"
val out = shell.newJob().add("${getKsuDaemonPath()} $cmd").to(ArrayList(), null).exec().out
val out = Shell.cmd("$ksuDaemonPath $cmd").to(ArrayList(), null).exec().out
out.filter { it.isNotBlank() }.map { it.trim() }
}

fun overlayFsAvailable(): Boolean {
val shell = getRootShell()
// check /proc/filesystems
return ShellUtils.fastCmdResult(shell, "cat /proc/filesystems | grep overlay")
return ShellUtils.fastCmdResult("cat /proc/filesystems | grep overlay")
}

fun hasMagisk(): Boolean {
val shell = getRootShell(true)
val result = shell.newJob().add("which magisk").exec()
Log.i(TAG, "has magisk: ${result.isSuccess}")
return result.isSuccess
val result = ShellUtils.fastCmdResult("which magisk")
Log.i(TAG, "has magisk: $result")
return result
}

fun isSepolicyValid(rules: String?): Boolean {
if (rules == null) {
return true
}
val shell = getRootShell()
val result =
shell.newJob().add("${getKsuDaemonPath()} sepolicy check '$rules'").to(ArrayList(), null)
Shell.cmd("$ksuDaemonPath sepolicy check '$rules'").to(ArrayList(), null)
.exec()
return result.isSuccess
}

fun getSepolicy(pkg: String): String {
val shell = getRootShell()
val result =
shell.newJob().add("${getKsuDaemonPath()} profile get-sepolicy $pkg").to(ArrayList(), null)
Shell.cmd("$ksuDaemonPath profile get-sepolicy $pkg").to(ArrayList(), null)
.exec()
Log.i(TAG, "code: ${result.code}, out: ${result.out}, err: ${result.err}")
return result.out.joinToString("\n")
}

fun setSepolicy(pkg: String, rules: String): Boolean {
val shell = getRootShell()
val result = shell.newJob().add("${getKsuDaemonPath()} profile set-sepolicy $pkg '$rules'")
val result = Shell.cmd("$ksuDaemonPath profile set-sepolicy $pkg '$rules'")
.to(ArrayList(), null).exec()
Log.i(TAG, "set sepolicy result: ${result.code}")
return result.isSuccess
}

fun listAppProfileTemplates(): List<String> {
val shell = getRootShell()
return shell.newJob().add("${getKsuDaemonPath()} profile list-templates").to(ArrayList(), null)
return Shell.cmd("$ksuDaemonPath profile list-templates").to(ArrayList(), null)
.exec().out
}

fun getAppProfileTemplate(id: String): String {
val shell = getRootShell()
return shell.newJob().add("${getKsuDaemonPath()} profile get-template '${id}'")
return Shell.cmd("$ksuDaemonPath profile get-template '${id}'")
.to(ArrayList(), null).exec().out.joinToString("\n")
}

fun setAppProfileTemplate(id: String, template: String): Boolean {
val shell = getRootShell()
val escapedTemplate = template.replace("\"", "\\\"")
val cmd = """${getKsuDaemonPath()} profile set-template "$id" "$escapedTemplate'""""
return shell.newJob().add(cmd)
val cmd = """$ksuDaemonPath profile set-template "$id" "$escapedTemplate'""""
return Shell.cmd(cmd)
.to(ArrayList(), null).exec().isSuccess
}

fun deleteAppProfileTemplate(id: String): Boolean {
val shell = getRootShell()
return shell.newJob().add("${getKsuDaemonPath()} profile delete-template '${id}'")
return Shell.cmd("$ksuDaemonPath profile delete-template '${id}'")
.to(ArrayList(), null).exec().isSuccess
}

fun forceStopApp(packageName: String) {
val shell = getRootShell()
val result = shell.newJob().add("am force-stop $packageName").exec()
val result = Shell.cmd("am force-stop $packageName").exec()
Log.i(TAG, "force stop $packageName result: $result")
}

fun launchApp(packageName: String) {

val shell = getRootShell()
val result =
shell.newJob()
.add("cmd package resolve-activity --brief $packageName | tail -n 1 | xargs cmd activity start-activity -n")
Shell.cmd("cmd package resolve-activity --brief $packageName | tail -n 1 | xargs cmd activity start-activity -n")
.exec()
Log.i(TAG, "launch $packageName result: $result")
}
Expand Down
Loading