prober build 4: newProcess fallback when the Shizuku UserService won't bind
Build 3 settled it: the Lenovo TB330FU/A15 never spawns the UserService
(two 25s bind attempts, binder alive, permission granted). Build 4 falls
back to the legacy Shizuku.newProcess remote-process API via reflection
and records exec_path in the evidence — whether that path works per
device is itself the capability question core-shizuku needs answered.
Also archives both build-3 reports: phone 7/7 incl. provoked NEIGH
transitions in ip_monitor; traceroute.udp4 handled a silent hop ("*")
correctly on both devices.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0ed36d9103
commit
60989b67ef
@@ -60,10 +60,14 @@ class ShizukuProbe : Probe {
|
||||
var ok = 0
|
||||
// One bind for the whole battery — per-command binding raced Shizuku's unbind and
|
||||
// produced spurious SHIZUKU_BIND_TIMEOUTs (3/7 on the OnePlus 15).
|
||||
for ((key, out) in runner.execBatch(battery, timeoutMs = 6000)) {
|
||||
val batch = runner.execBatch(battery, timeoutMs = 6000)
|
||||
ev["exec_path"] = batch.execPath
|
||||
for ((key, out) in batch.results) {
|
||||
// Truncate each excerpt so evidence stays readable; full capture is future work.
|
||||
ev[key] = out.trim().take(1200)
|
||||
if (!out.startsWith("SHIZUKU_") && !out.startsWith("EXEC_") && out.isNotBlank()) ok++
|
||||
if (!out.startsWith("SHIZUKU_") && !out.startsWith("EXEC_") &&
|
||||
!out.startsWith("NEWPROCESS_") && out.isNotBlank()
|
||||
) ok++
|
||||
}
|
||||
val verdict = if (ok >= 4) Verdict.SUPPORTED else if (ok >= 1) Verdict.PARTIAL else Verdict.UNSUPPORTED
|
||||
ProbeResult.of(this@ShizukuProbe, verdict,
|
||||
|
||||
@@ -67,17 +67,27 @@ class ShizukuRunner(private val context: Context) {
|
||||
.processNameSuffix("prober")
|
||||
.version(1)
|
||||
|
||||
/** How the batch actually executed — recorded as evidence by the probe. */
|
||||
data class BatchResult(
|
||||
val execPath: String,
|
||||
val results: LinkedHashMap<String, String>,
|
||||
)
|
||||
|
||||
/**
|
||||
* Bind the UserService ONCE, run every command against it, then unbind. The first device run
|
||||
* (OnePlus 15) showed why per-command binding is wrong: unbind of command N races the bind of
|
||||
* command N+1 inside Shizuku, and 3/7 commands died on SHIZUKU_BIND_TIMEOUT.
|
||||
*
|
||||
* If the UserService never binds (Lenovo TB330FU/A15: two 25 s attempts, spawn never
|
||||
* happens), falls back to the legacy Shizuku.newProcess remote-process API via reflection —
|
||||
* itself a capability question worth answering per device.
|
||||
*
|
||||
* Returns one output (or error string) per command, keyed like the input.
|
||||
*/
|
||||
suspend fun execBatch(
|
||||
commands: List<Pair<String, String>>,
|
||||
timeoutMs: Int = 8000,
|
||||
): LinkedHashMap<String, String> {
|
||||
): BatchResult {
|
||||
val out = LinkedHashMap<String, String>()
|
||||
val bound = CompletableDeferred<IUserService?>()
|
||||
val conn = object : ServiceConnection {
|
||||
@@ -100,8 +110,7 @@ class ShizukuRunner(private val context: Context) {
|
||||
svc = withTimeoutOrNull(25_000) { bound.await() }
|
||||
}
|
||||
if (svc == null) {
|
||||
commands.forEach { (key, _) -> out[key] = "SHIZUKU_BIND_TIMEOUT" }
|
||||
return out
|
||||
return execBatchViaNewProcess(commands, timeoutMs)
|
||||
}
|
||||
for ((key, cmd) in commands) {
|
||||
out[key] = try {
|
||||
@@ -118,6 +127,50 @@ class ShizukuRunner(private val context: Context) {
|
||||
} finally {
|
||||
runCatching { Shizuku.unbindUserService(serviceArgs, conn, true) }
|
||||
}
|
||||
return out
|
||||
return BatchResult("UserService", out)
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy path: Shizuku.newProcess (private in API 13, reached via reflection — repo
|
||||
* convention for uncertain APIs). Returns a java.lang.Process whose stdout is read with a
|
||||
* deadline. Whether this path works per device/Shizuku build is itself evidence the
|
||||
* production core-shizuku module needs.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user