app: core-shizuku — dual-path shell-tier executor + probe, wired into the app
Ports the prober's validated Shizuku tier: AIDL UserService, the build-4 dual-path ShizukuRunner (UserService bind where it works, legacy newProcess reflection fallback where it doesn't — exec_path records which), and ShizukuProbe running the shell command battery, emitting a shizuku-tier link.ip_monitor Test with per-device dumps as evidence. Self-degrades to UNSUPPORTED without Shizuku. Wired into RunViewModel (sets tiers.shizuku); app APK assembles. On-device verification deferred — no device reachable at build time (flaky LAN dropped the tablet, phone debugging off). Expect UserService on OnePlus, newProcess on Lenovo per the prober. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9809ae57b4
commit
aaed22dd3f
@@ -355,3 +355,20 @@ Wireless-adb beacon (tools/adb-beacon) made this practical: both devices self-re
|
|||||||
rotating wireless-debug port to fmr:443; a PC connector keeps adb connected. Debugged live
|
rotating wireless-debug port to fmr:443; a PC connector keeps adb connected. Debugged live
|
||||||
against the restricted LAN (cleartext policy, egress filtering, shared-LAN mDNS crossing, fast
|
against the restricted LAN (cleartext policy, egress filtering, shared-LAN mDNS crossing, fast
|
||||||
port rotation) — all handled.
|
port rotation) — all handled.
|
||||||
|
|
||||||
|
## App: core-shizuku (shell tier) built + wired (2026-07-31)
|
||||||
|
Ported the prober's validated Shizuku executor into the app as a library module:
|
||||||
|
- AIDL IUserService + UserService (runs `sh -c` as shell/root in the Shizuku-spawned process),
|
||||||
|
Shizuku provider merged into the app manifest.
|
||||||
|
- **ShizukuRunner: the build-4 dual-path executor** — bind the UserService (25s + retry) where it
|
||||||
|
works (OnePlus 7/7), fall back to the legacy `Shizuku.newProcess` reflection API where it never
|
||||||
|
binds (Lenovo). `exec_path` records which path ran.
|
||||||
|
- ShizukuProbe: runs the shell battery (ip neigh / ip -6 route / ip addr / ip monitor /
|
||||||
|
network_stack DHCP / wifi dump) and emits a shizuku-tier `link.ip_monitor` Test with the raw
|
||||||
|
per-device dumps as evidence + commands_ok/exec_path metrics. Self-degrades to UNSUPPORTED when
|
||||||
|
Shizuku isn't running.
|
||||||
|
Wired into RunViewModel (runs after app-tier probes; sets tiers.shizuku). App APK assembles clean.
|
||||||
|
On-device test deferred: at build time no device was reachable (tablet wifi/beacon dropped on the
|
||||||
|
churning LAN; phone wireless debugging disabled to stop reconnect notifications). Will verify on a
|
||||||
|
device later — expecting UserService on the OnePlus, newProcess fallback on the Lenovo, per the
|
||||||
|
prober.
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ dependencies {
|
|||||||
implementation(project(":core-protocol"))
|
implementation(project(":core-protocol"))
|
||||||
implementation(project(":core-engine"))
|
implementation(project(":core-engine"))
|
||||||
implementation(project(":core-probe"))
|
implementation(project(":core-probe"))
|
||||||
|
implementation(project(":core-shizuku"))
|
||||||
|
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
implementation(libs.kotlinx.coroutines.android)
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import app.echo_lot.probe.LinkSnapshotProbe
|
|||||||
import app.echo_lot.probe.NetworkInventory
|
import app.echo_lot.probe.NetworkInventory
|
||||||
import app.echo_lot.probe.Probe
|
import app.echo_lot.probe.Probe
|
||||||
import app.echo_lot.probe.ProbeIds
|
import app.echo_lot.probe.ProbeIds
|
||||||
|
import app.echo_lot.shizuku.ShizukuProbe
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -87,6 +88,20 @@ class RunViewModel(app: Application) : AndroidViewModel(app) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shizuku shell tier — self-degrades to UNSUPPORTED when Shizuku isn't running.
|
||||||
|
step("shizuku.command_battery")
|
||||||
|
val shizukuTest = try {
|
||||||
|
ShizukuProbe().run(ctx, ids::uuid, ids::monoNs)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Test(
|
||||||
|
id = ids.uuid(), type = TestType.LINK_IP_MONITOR, tier = Tier.SHIZUKU,
|
||||||
|
startedMonoNs = ids.monoNs(), endedMonoNs = ids.monoNs(),
|
||||||
|
status = TestStatus.FAILED, error = TestError("uncaught", t.message ?: t.javaClass.simpleName),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
tests.add(shizukuTest)
|
||||||
|
val shizukuAvailable = shizukuTest.status == TestStatus.OK || shizukuTest.status == TestStatus.PARTIAL
|
||||||
|
|
||||||
val findings = deriveFindings(tests)
|
val findings = deriveFindings(tests)
|
||||||
val summary = Verdicts.derive(tests, findings)
|
val summary = Verdicts.derive(tests, findings)
|
||||||
|
|
||||||
@@ -102,7 +117,7 @@ class RunViewModel(app: Application) : AndroidViewModel(app) {
|
|||||||
manufacturer = Build.MANUFACTURER, model = Build.MODEL,
|
manufacturer = Build.MANUFACTURER, model = Build.MODEL,
|
||||||
androidSdk = Build.VERSION.SDK_INT, androidRelease = Build.VERSION.RELEASE,
|
androidSdk = Build.VERSION.SDK_INT, androidRelease = Build.VERSION.RELEASE,
|
||||||
),
|
),
|
||||||
tiers = Tiers(app = true),
|
tiers = Tiers(app = true, shizuku = shizukuAvailable),
|
||||||
),
|
),
|
||||||
networks = networks,
|
networks = networks,
|
||||||
tests = tests,
|
tests = tests,
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 Echolot contributors
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
// AGP 9 built-in Kotlin (no kotlin.android — see core-probe note).
|
||||||
|
alias(libs.plugins.android.library)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Shizuku (shell-tier) executor + probe. Ported from the validated
|
||||||
|
// echolot-prober with the build-4 dual-path fix: bind the UserService when it
|
||||||
|
// works (OnePlus), fall back to the legacy newProcess API when it doesn't
|
||||||
|
// (Lenovo). Emits a core-measurement Test.
|
||||||
|
android {
|
||||||
|
namespace = "app.echo_lot.shizuku"
|
||||||
|
compileSdk = 36
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 26
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
aidl = true // IUserService
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":core-measurement"))
|
||||||
|
implementation(libs.shizuku.api)
|
||||||
|
implementation(libs.shizuku.provider)
|
||||||
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- SPDX-FileCopyrightText: 2026 Echolot contributors
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later -->
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<uses-permission android:name="moe.shizuku.manager.permission.API_V23" />
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<!-- Shizuku binder provider (merged into the host app). -->
|
||||||
|
<provider
|
||||||
|
android:name="rikka.shizuku.ShizukuProvider"
|
||||||
|
android:authorities="${applicationId}.shizuku"
|
||||||
|
android:multiprocess="false"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 Echolot contributors
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
package app.echo_lot.shizuku;
|
||||||
|
|
||||||
|
interface IUserService {
|
||||||
|
void destroy();
|
||||||
|
String exec(String command, int timeoutMs);
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 Echolot contributors
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package app.echo_lot.shizuku
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import app.echo_lot.measurement.Test
|
||||||
|
import app.echo_lot.measurement.TestStatus
|
||||||
|
import app.echo_lot.measurement.TestType
|
||||||
|
import app.echo_lot.measurement.Tier
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Shizuku shell-tier probe: runs the privileged command battery (neighbor table, RA routes
|
||||||
|
* with lifetimes, netlink monitor, IpClient DHCP logs, wifi dump) that the app UID cannot, and
|
||||||
|
* captures the real per-device dump formats the production parsers must handle. Emitted as a
|
||||||
|
* shizuku-tier `link.ip_monitor` test (the representative shell-tier link test); `exec_path`
|
||||||
|
* records whether the UserService or the newProcess fallback carried it.
|
||||||
|
*/
|
||||||
|
class ShizukuProbe {
|
||||||
|
val type = TestType.LINK_IP_MONITOR
|
||||||
|
val tier = Tier.SHIZUKU
|
||||||
|
|
||||||
|
private val battery = listOf(
|
||||||
|
"id" to "id",
|
||||||
|
"ip_neigh" to "ip neigh show",
|
||||||
|
"ip6_route" to "ip -6 route show table all",
|
||||||
|
"ip_addr" to "ip addr show",
|
||||||
|
"ip_monitor" to "(ping -c 2 -W 1 \$(ip route show default | head -1 | cut -d' ' -f3) >/dev/null 2>&1 &); timeout 5 ip monitor all || true",
|
||||||
|
"dhcp_log" to "dumpsys network_stack 2>/dev/null | grep -iA2 -m 20 -e dhcp -e 'IpClient' || true",
|
||||||
|
"wifi_dump" to "dumpsys wifi 2>/dev/null | grep -iA1 -m 20 -e 'mDhcpResults' -e 'Gateway' -e 'DNS' || true",
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Runs the battery and returns a Test. [uuid]/[monoNs] come from the run's id/clock source. */
|
||||||
|
suspend fun run(context: Context, uuid: () -> String, monoNs: () -> Long): Test = withContext(Dispatchers.IO) {
|
||||||
|
val id = uuid()
|
||||||
|
val started = monoNs()
|
||||||
|
val runner = ShizukuRunner(context)
|
||||||
|
val st = runner.status()
|
||||||
|
|
||||||
|
fun envelope(status: TestStatus, evidence: kotlinx.serialization.json.JsonObject, metrics: kotlinx.serialization.json.JsonObject? = null) =
|
||||||
|
Test(id = id, type = type, tier = tier, startedMonoNs = started, endedMonoNs = monoNs(),
|
||||||
|
status = status, evidence = evidence, metrics = metrics)
|
||||||
|
|
||||||
|
if (!st.binderAlive) {
|
||||||
|
return@withContext envelope(TestStatus.UNSUPPORTED, buildJsonObject {
|
||||||
|
put("binder_alive", false); put("detail", "Shizuku not running")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!st.permissionGranted && !runner.requestPermission()) {
|
||||||
|
return@withContext envelope(TestStatus.UNSUPPORTED, buildJsonObject {
|
||||||
|
put("binder_alive", true); put("permission", false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
val batch = runner.execBatch(battery, timeoutMs = 6000)
|
||||||
|
var ok = 0
|
||||||
|
val evidence = buildJsonObject {
|
||||||
|
put("binder_alive", true); put("version", st.version); put("runs_as", st.uidName)
|
||||||
|
put("exec_path", batch.execPath)
|
||||||
|
for ((key, out) in batch.results) {
|
||||||
|
val trimmed = out.trim().take(1200)
|
||||||
|
put(key, trimmed)
|
||||||
|
if (!out.startsWith("SHIZUKU_") && !out.startsWith("EXEC_") &&
|
||||||
|
!out.startsWith("NEWPROCESS_") && out.isNotBlank()
|
||||||
|
) ok++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val metrics = buildJsonObject {
|
||||||
|
put("commands_ok", ok); put("commands_total", battery.size); put("exec_path", batch.execPath)
|
||||||
|
}
|
||||||
|
val status = when {
|
||||||
|
ok >= 4 -> TestStatus.OK
|
||||||
|
ok >= 1 -> TestStatus.PARTIAL
|
||||||
|
else -> TestStatus.FAILED
|
||||||
|
}
|
||||||
|
envelope(status, evidence, metrics)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 Echolot contributors
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package app.echo_lot.shizuku
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.ServiceConnection
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.IBinder
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import rikka.shizuku.Shizuku
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thin wrapper over the Shizuku binder + a bound UserService, with a dual execution path proven
|
||||||
|
* necessary on real hardware (echolot-prober):
|
||||||
|
* - UserService bind works on the OnePlus 15 (7/7 commands);
|
||||||
|
* - it never binds on the Lenovo TB330FU (two 25 s attempts, binder alive, permission granted),
|
||||||
|
* so this falls back to the legacy `Shizuku.newProcess` remote-process API via reflection.
|
||||||
|
* core-shizuku must not assume UserService works everywhere.
|
||||||
|
*/
|
||||||
|
class ShizukuRunner(private val context: Context) {
|
||||||
|
|
||||||
|
data class Status(
|
||||||
|
val binderAlive: Boolean,
|
||||||
|
val version: Int,
|
||||||
|
val uidName: String,
|
||||||
|
val permissionGranted: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class BatchResult(val execPath: String, val results: LinkedHashMap<String, String>)
|
||||||
|
|
||||||
|
fun status(): Status {
|
||||||
|
val alive = runCatching { Shizuku.pingBinder() }.getOrDefault(false)
|
||||||
|
val version = if (alive) runCatching { Shizuku.getVersion() }.getOrDefault(-1) else -1
|
||||||
|
val uid = if (alive) runCatching { Shizuku.getUid() }.getOrDefault(-1) else -1
|
||||||
|
val uidName = when (uid) {
|
||||||
|
0 -> "root(0)"; 2000 -> "shell(2000)"; -1 -> "unknown"; else -> "uid=$uid"
|
||||||
|
}
|
||||||
|
val granted = alive && runCatching {
|
||||||
|
Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED
|
||||||
|
}.getOrDefault(false)
|
||||||
|
return Status(alive, version, uidName, granted)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun requestPermission(): Boolean {
|
||||||
|
val s = status()
|
||||||
|
if (!s.binderAlive) return false
|
||||||
|
if (s.permissionGranted) return true
|
||||||
|
val deferred = CompletableDeferred<Boolean>()
|
||||||
|
val code = 0xE1
|
||||||
|
val listener = object : Shizuku.OnRequestPermissionResultListener {
|
||||||
|
override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) {
|
||||||
|
if (requestCode == code) {
|
||||||
|
Shizuku.removeRequestPermissionResultListener(this)
|
||||||
|
deferred.complete(grantResult == PackageManager.PERMISSION_GRANTED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Shizuku.addRequestPermissionResultListener(listener)
|
||||||
|
runCatching { Shizuku.requestPermission(code) }
|
||||||
|
return withTimeoutOrNull(30000) { deferred.await() } ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
private val serviceArgs = Shizuku.UserServiceArgs(
|
||||||
|
ComponentName(context.packageName, UserService::class.java.name),
|
||||||
|
).daemon(false).processNameSuffix("echolot").version(1)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind the UserService once for the whole battery (per-command binding races Shizuku's
|
||||||
|
* unbind — 3/7 spurious timeouts on the OnePlus). 25 s + one retry; on total failure, fall
|
||||||
|
* back to newProcess. Returns which path ran + one output per command.
|
||||||
|
*/
|
||||||
|
suspend fun execBatch(commands: List<Pair<String, String>>, timeoutMs: Int = 6000): BatchResult {
|
||||||
|
val out = LinkedHashMap<String, String>()
|
||||||
|
val bound = CompletableDeferred<IUserService?>()
|
||||||
|
val conn = object : ServiceConnection {
|
||||||
|
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
|
||||||
|
bound.complete(if (binder != null && binder.pingBinder()) IUserService.Stub.asInterface(binder) else null)
|
||||||
|
}
|
||||||
|
override fun onServiceDisconnected(name: ComponentName?) {}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Shizuku.bindUserService(serviceArgs, conn)
|
||||||
|
var svc = withTimeoutOrNull(25_000) { bound.await() }
|
||||||
|
if (svc == null) {
|
||||||
|
runCatching { Shizuku.unbindUserService(serviceArgs, conn, true) }
|
||||||
|
Shizuku.bindUserService(serviceArgs, conn)
|
||||||
|
svc = withTimeoutOrNull(25_000) { bound.await() }
|
||||||
|
}
|
||||||
|
if (svc == null) return execBatchViaNewProcess(commands, timeoutMs)
|
||||||
|
for ((key, cmd) in commands) {
|
||||||
|
out[key] = try {
|
||||||
|
withTimeoutOrNull(timeoutMs.toLong() + 4000) { svc.exec(cmd, timeoutMs) } ?: "EXEC_TIMEOUT"
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
"SHIZUKU_ERROR: ${e.message ?: e.javaClass.simpleName}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
commands.forEach { (key, _) -> out.putIfAbsent(key, "SHIZUKU_ERROR: ${e.message ?: e.javaClass.simpleName}") }
|
||||||
|
} finally {
|
||||||
|
runCatching { Shizuku.unbindUserService(serviceArgs, conn, true) }
|
||||||
|
}
|
||||||
|
return BatchResult("UserService", out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Legacy Shizuku.newProcess (private, reflection) — carries the Lenovo where UserService won't
|
||||||
|
* bind. Whether it works per device is itself evidence core-shizuku records. */
|
||||||
|
private fun execBatchViaNewProcess(commands: List<Pair<String, String>>, timeoutMs: Int): BatchResult {
|
||||||
|
val out = LinkedHashMap<String, String>()
|
||||||
|
val method = runCatching {
|
||||||
|
Shizuku::class.java.getDeclaredMethod(
|
||||||
|
"newProcess", Array<String>::class.java, Array<String>::class.java, String::class.java,
|
||||||
|
).apply { isAccessible = true }
|
||||||
|
}.getOrNull()
|
||||||
|
if (method == null) {
|
||||||
|
commands.forEach { (key, _) -> out[key] = "SHIZUKU_BIND_TIMEOUT; newProcess API absent" }
|
||||||
|
return BatchResult("UserService bind failed; newProcess absent", out)
|
||||||
|
}
|
||||||
|
for ((key, cmd) in commands) {
|
||||||
|
out[key] = runCatching {
|
||||||
|
val proc = method.invoke(null, arrayOf("sh", "-c", cmd), null, null) as Process
|
||||||
|
val output = StringBuilder()
|
||||||
|
val reader = Thread {
|
||||||
|
runCatching {
|
||||||
|
proc.inputStream.bufferedReader().forEachLine { line ->
|
||||||
|
if (output.length < 64 * 1024) output.append(line).append('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.start(); reader.join(timeoutMs.toLong())
|
||||||
|
if (reader.isAlive) proc.destroy()
|
||||||
|
output.toString().ifBlank { "EXEC_TIMEOUT(newProcess)" }
|
||||||
|
}.getOrElse { e ->
|
||||||
|
val cause = (e as? java.lang.reflect.InvocationTargetException)?.cause ?: e
|
||||||
|
"NEWPROCESS_ERROR: ${cause.message ?: cause.javaClass.simpleName}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return BatchResult("newProcess fallback (UserService bind failed)", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 Echolot contributors
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package app.echo_lot.shizuku
|
||||||
|
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs inside the Shizuku-spawned process (uid = shell 2000, or root). This process has the shell
|
||||||
|
* SELinux domain, so commands here read `ip neigh`, `ip monitor`, `dumpsys network_stack`, etc. —
|
||||||
|
* things the app UID cannot. The implicit no-arg primary constructor is the one Shizuku requires.
|
||||||
|
*/
|
||||||
|
class UserService : IUserService.Stub() {
|
||||||
|
|
||||||
|
override fun destroy() {
|
||||||
|
exitProcess(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun exec(command: String, timeoutMs: Int): String {
|
||||||
|
return try {
|
||||||
|
val proc = ProcessBuilder("sh", "-c", command).redirectErrorStream(true).start()
|
||||||
|
val output = StringBuilder()
|
||||||
|
val reader = proc.inputStream.bufferedReader()
|
||||||
|
val deadline = System.currentTimeMillis() + timeoutMs.coerceIn(500, 30000)
|
||||||
|
val readerThread = Thread {
|
||||||
|
runCatching {
|
||||||
|
reader.forEachLine { line -> if (output.length < 64 * 1024) output.append(line).append('\n') }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
readerThread.start()
|
||||||
|
while (readerThread.isAlive && System.currentTimeMillis() < deadline) Thread.sleep(20)
|
||||||
|
if (proc.isAlive) proc.destroy()
|
||||||
|
"uid=" + android.os.Process.myUid() + "\n" + output.toString()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
"EXEC_ERROR: ${e.message ?: e.javaClass.simpleName}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,5 +25,6 @@ include(":core-protocol")
|
|||||||
include(":core-measurement")
|
include(":core-measurement")
|
||||||
include(":core-engine")
|
include(":core-engine")
|
||||||
include(":core-probe")
|
include(":core-probe")
|
||||||
|
include(":core-shizuku")
|
||||||
include(":app")
|
include(":app")
|
||||||
include(":adb-beacon")
|
include(":adb-beacon")
|
||||||
|
|||||||
Reference in New Issue
Block a user