Update dependencies
This commit is contained in:
@@ -188,8 +188,6 @@ Greylisted/blacklisted APIs or internal constants: (some constants are hardcoded
|
|||||||
* (since API 30) `Lcom/android/server/wifi/WifiContext;->ACTION_RESOURCES_APK:Ljava/lang/String;`
|
* (since API 30) `Lcom/android/server/wifi/WifiContext;->ACTION_RESOURCES_APK:Ljava/lang/String;`
|
||||||
* (since API 29) `Lcom/android/server/wifi/p2p/WifiP2pServiceImpl;->ANONYMIZED_DEVICE_ADDRESS:Ljava/lang/String;`
|
* (since API 29) `Lcom/android/server/wifi/p2p/WifiP2pServiceImpl;->ANONYMIZED_DEVICE_ADDRESS:Ljava/lang/String;`
|
||||||
* (since API 30) `Lcom/android/server/SystemServer;->TETHERING_CONNECTOR_CLASS:Ljava/lang/String;`
|
* (since API 30) `Lcom/android/server/SystemServer;->TETHERING_CONNECTOR_CLASS:Ljava/lang/String;`
|
||||||
* (since API 29) `Ldalvik/system/VMRuntime;->getRuntime()Ldalvik/system/VMRuntime;,core-platform-api,unsupported`
|
|
||||||
* (since API 29) `Ldalvik/system/VMRuntime;->setHiddenApiExemptions([Ljava/lang/String;)V,blocked,core-platform-api`
|
|
||||||
* (since API 26) `Ljava/lang/invoke/MethodHandles$Lookup;-><init>(Ljava/lang/Class;I)V,unsupported`
|
* (since API 26) `Ljava/lang/invoke/MethodHandles$Lookup;-><init>(Ljava/lang/Class;I)V,unsupported`
|
||||||
* (since API 26) `Ljava/lang/invoke/MethodHandles$Lookup;->ALL_MODES:I,lo-prio,max-target-o`
|
* (since API 26) `Ljava/lang/invoke/MethodHandles$Lookup;->ALL_MODES:I,lo-prio,max-target-o`
|
||||||
* (prior to API 29) `Ljava/net/InetAddress;->parseNumericAddress(Ljava/lang/String;)Ljava/net/InetAddress;,core-platform-api,max-target-p`
|
* (prior to API 29) `Ljava/net/InetAddress;->parseNumericAddress(Ljava/lang/String;)Ljava/net/InetAddress;,core-platform-api,max-target-p`
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ buildscript {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath(kotlin("gradle-plugin", "1.7.10"))
|
classpath(kotlin("gradle-plugin", "1.7.10"))
|
||||||
classpath("com.android.tools.build:gradle:7.3.0-beta05")
|
classpath("com.android.tools.build:gradle:7.4.0-beta01")
|
||||||
classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.1")
|
classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.2")
|
||||||
classpath("com.google.android.gms:oss-licenses-plugin:0.10.5")
|
classpath("com.google.android.gms:oss-licenses-plugin:0.10.5")
|
||||||
classpath("com.google.gms:google-services:4.3.13")
|
classpath("com.google.gms:google-services:4.3.14")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -69,25 +69,26 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
val lifecycleVersion = "2.5.0-rc01"
|
val lifecycleVersion = "2.5.1"
|
||||||
val roomVersion = "2.5.0-alpha02"
|
val roomVersion = "2.5.0-alpha03"
|
||||||
|
|
||||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.0")
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.0")
|
||||||
kapt("androidx.room:room-compiler:$roomVersion")
|
kapt("androidx.room:room-compiler:$roomVersion")
|
||||||
implementation(kotlin("stdlib-jdk8"))
|
implementation(kotlin("stdlib-jdk8"))
|
||||||
implementation("androidx.browser:browser:1.4.0")
|
implementation("androidx.browser:browser:1.4.0")
|
||||||
implementation("androidx.core:core-ktx:1.8.0")
|
implementation("androidx.core:core-ktx:1.9.0")
|
||||||
implementation("androidx.fragment:fragment-ktx:1.5.0")
|
implementation("androidx.fragment:fragment-ktx:1.5.3")
|
||||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion")
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
||||||
implementation("androidx.preference:preference:1.2.0")
|
implementation("androidx.preference:preference:1.2.0")
|
||||||
implementation("androidx.room:room-ktx:$roomVersion")
|
implementation("androidx.room:room-ktx:$roomVersion")
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
|
implementation("be.mygod.librootkotlinx:librootkotlinx:1.0.0")
|
||||||
implementation("com.android.billingclient:billing-ktx:5.0.0")
|
implementation("com.android.billingclient:billing-ktx:5.0.0")
|
||||||
implementation("com.google.android.gms:play-services-oss-licenses:17.0.0")
|
implementation("com.google.android.gms:play-services-oss-licenses:17.0.0")
|
||||||
implementation("com.google.android.material:material:1.7.0-alpha03")
|
implementation("com.google.android.material:material:1.7.0-rc01")
|
||||||
implementation("com.google.firebase:firebase-analytics-ktx:21.1.0")
|
implementation("com.google.firebase:firebase-analytics-ktx:21.1.1")
|
||||||
implementation("com.google.firebase:firebase-crashlytics:18.2.11")
|
implementation("com.google.firebase:firebase-crashlytics:18.2.13")
|
||||||
implementation("com.google.zxing:core:3.5.0")
|
implementation("com.google.zxing:core:3.5.0")
|
||||||
implementation("com.jakewharton.timber:timber:5.0.1")
|
implementation("com.jakewharton.timber:timber:5.0.1")
|
||||||
implementation("com.linkedin.dexmaker:dexmaker:2.28.3")
|
implementation("com.linkedin.dexmaker:dexmaker:2.28.3")
|
||||||
|
|||||||
@@ -1,131 +0,0 @@
|
|||||||
package be.mygod.librootkotlinx
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Debug
|
|
||||||
import android.os.Process
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
object AppProcess {
|
|
||||||
/**
|
|
||||||
* Based on: https://android.googlesource.com/platform/bionic/+/aff9a34/linker/linker.cpp#3397
|
|
||||||
*/
|
|
||||||
@get:RequiresApi(28)
|
|
||||||
val genericLdConfigFilePath: String get() {
|
|
||||||
"/system/etc/ld.config.$currentInstructionSet.txt".let { if (File(it).isFile) return it }
|
|
||||||
if (Build.VERSION.SDK_INT >= 30) "/linkerconfig/ld.config.txt".let {
|
|
||||||
if (File(it).isFile) return it
|
|
||||||
Logger.me.w("Failed to find generated linker configuration from \"$it\"")
|
|
||||||
}
|
|
||||||
if (isVndkLite) {
|
|
||||||
"/system/etc/ld.config.vndk_lite.txt".let { if (File(it).isFile) return it }
|
|
||||||
} else when (vndkVersion) {
|
|
||||||
"", "current" -> { }
|
|
||||||
else -> "/system/etc/ld.config.$vndkVersion.txt".let { if (File(it).isFile) return it }
|
|
||||||
}
|
|
||||||
return "/system/etc/ld.config.txt"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Based on: https://android.googlesource.com/platform/bionic/+/30f2f05/linker/linker_config.cpp#182
|
|
||||||
*/
|
|
||||||
@RequiresApi(26)
|
|
||||||
fun findLinkerSection(lines: Sequence<String>, binaryRealPath: String): String {
|
|
||||||
for (untrimmed in lines) {
|
|
||||||
val line = untrimmed.substringBefore('#').trim()
|
|
||||||
if (line.isEmpty()) continue
|
|
||||||
if (line[0] == '[' && line.last() == ']') break
|
|
||||||
if (line.contains("+=")) continue
|
|
||||||
val chunks = line.split('=', limit = 2)
|
|
||||||
if (chunks.size < 2) {
|
|
||||||
Logger.me.w("warning: couldn't parse invalid format: $line (ignoring this line)")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var (name, value) = chunks.map { it.trim() }
|
|
||||||
if (!name.startsWith("dir.")) {
|
|
||||||
Logger.me.w("warning: unexpected property name \"$name\", " +
|
|
||||||
"expected format dir.<section_name> (ignoring this line)")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (value.endsWith('/')) value = value.dropLast(1)
|
|
||||||
if (value.isEmpty()) {
|
|
||||||
Logger.me.w("warning: property value is empty (ignoring this line)")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
value = File(value).canonicalPath
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Logger.me.i("warning: path \"$value\" couldn't be resolved: ${e.message}")
|
|
||||||
}
|
|
||||||
if (binaryRealPath.startsWith(value) && binaryRealPath[value.length] == '/') return name.substring(4)
|
|
||||||
}
|
|
||||||
throw IllegalArgumentException("No valid linker section found")
|
|
||||||
}
|
|
||||||
|
|
||||||
val myExe get() = "/proc/${Process.myPid()}/exe"
|
|
||||||
val myExeCanonical get() = try {
|
|
||||||
File("/proc/self/exe").canonicalPath
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Logger.me.i("warning: couldn't resolve self exe: ${e.message}")
|
|
||||||
"/system/bin/app_process"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try to guess whether enabling relocation would work best.
|
|
||||||
* It seems some Android 5-7 devices give random permission denials without relocation.
|
|
||||||
* See also VPNHotspot#173.
|
|
||||||
*/
|
|
||||||
val shouldRelocateHeuristics get() = Build.VERSION.SDK_INT < 26 || myExeCanonical.startsWith("/data/")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To workaround Samsung's stupid kernel patch that prevents exec, we need to relocate exe outside of /data.
|
|
||||||
* See also: https://github.com/Chainfire/librootjava/issues/19
|
|
||||||
*
|
|
||||||
* @return The script to be executed to perform relocation and the relocated binary path.
|
|
||||||
*/
|
|
||||||
fun relocateScript(token: String): Pair<StringBuilder, String> {
|
|
||||||
val script = StringBuilder()
|
|
||||||
val (baseDir, relocated) = if (Build.VERSION.SDK_INT < 29) "/dev" to "/dev/app_process_$token" else {
|
|
||||||
val apexPath = "/apex/$token"
|
|
||||||
script.appendLine("[ -d $apexPath ] || " +
|
|
||||||
"mkdir $apexPath && " +
|
|
||||||
// we need to mount a new tmpfs to override noexec flag
|
|
||||||
"mount -t tmpfs -o size=1M tmpfs $apexPath || exit 1")
|
|
||||||
// unfortunately native ld.config.txt only recognizes /data,/system,/system_ext as system directories;
|
|
||||||
// to link correctly, we need to add our path to the linker config too
|
|
||||||
val ldConfig = "$apexPath/etc/ld.config.txt"
|
|
||||||
val masterLdConfig = genericLdConfigFilePath
|
|
||||||
val section = try {
|
|
||||||
File(masterLdConfig).useLines { findLinkerSection(it, myExeCanonical) }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.me.w("Failed to locate system section", e)
|
|
||||||
"system"
|
|
||||||
}
|
|
||||||
script.appendLine("[ -f $ldConfig ] || " +
|
|
||||||
"mkdir -p $apexPath/etc && " +
|
|
||||||
"echo dir.$section = $apexPath >$ldConfig && " +
|
|
||||||
"cat $masterLdConfig >>$ldConfig || exit 1")
|
|
||||||
"$apexPath/bin" to "$apexPath/bin/app_process"
|
|
||||||
}
|
|
||||||
script.appendLine("[ -f $relocated ] || " +
|
|
||||||
"mkdir -p $baseDir && " +
|
|
||||||
"cp $myExe $relocated && " +
|
|
||||||
"chmod 700 $relocated || exit 1")
|
|
||||||
return script to relocated
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute the shell script line that exec into the corresponding [clazz].
|
|
||||||
* Extra params can be simply appended to the string.
|
|
||||||
*/
|
|
||||||
fun launchString(packageCodePath: String, clazz: String, appProcess: String, niceName: String? = null): String {
|
|
||||||
val debugParams = if (Debug.isDebuggerConnected()) when (Build.VERSION.SDK_INT) {
|
|
||||||
in 29..Int.MAX_VALUE -> "-XjdwpProvider:adbconnection"
|
|
||||||
28 -> "-XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y -Xcompiler-option --debuggable"
|
|
||||||
else -> "-Xrunjdwp:transport=dt_android_adb,suspend=n,server=y -Xcompiler-option --debuggable"
|
|
||||||
} else ""
|
|
||||||
val extraParams = if (niceName != null) " --nice-name=$niceName" else ""
|
|
||||||
return "CLASSPATH=$packageCodePath exec $appProcess $debugParams /system/bin$extraParams $clazz"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package be.mygod.librootkotlinx
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
|
|
||||||
interface Logger {
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* Override this variable to change default behavior,
|
|
||||||
* which is to print to [android.util.Log] under tag "RootServer" except for [d].
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
var me = object : Logger { }
|
|
||||||
|
|
||||||
private const val TAG = "RootServer"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun d(m: String?, t: Throwable? = null) { }
|
|
||||||
fun e(m: String?, t: Throwable? = null) {
|
|
||||||
Log.e(TAG, m, t)
|
|
||||||
}
|
|
||||||
fun i(m: String?, t: Throwable? = null) {
|
|
||||||
Log.i(TAG, m, t)
|
|
||||||
}
|
|
||||||
fun w(m: String?, t: Throwable? = null) {
|
|
||||||
Log.w(TAG, m, t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,503 +0,0 @@
|
|||||||
package be.mygod.librootkotlinx
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Looper
|
|
||||||
import android.os.Parcelable
|
|
||||||
import android.os.RemoteException
|
|
||||||
import android.system.ErrnoException
|
|
||||||
import android.system.Os
|
|
||||||
import android.system.OsConstants
|
|
||||||
import androidx.collection.LongSparseArray
|
|
||||||
import androidx.collection.valueIterator
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.channels.*
|
|
||||||
import java.io.*
|
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.CountDownLatch
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
class RootServer {
|
|
||||||
private sealed class Callback(private val server: RootServer, private val index: Long,
|
|
||||||
protected val classLoader: ClassLoader?) {
|
|
||||||
var active = true
|
|
||||||
|
|
||||||
abstract fun cancel()
|
|
||||||
abstract fun shouldRemove(result: Byte): Boolean
|
|
||||||
abstract operator fun invoke(input: DataInputStream, result: Byte)
|
|
||||||
fun sendClosed() = server.execute(CancelCommand(index))
|
|
||||||
|
|
||||||
private fun initException(targetClass: Class<*>, message: String): Throwable {
|
|
||||||
@Suppress("NAME_SHADOWING")
|
|
||||||
var targetClass = targetClass
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
// try to find a message constructor
|
|
||||||
return targetClass.getDeclaredConstructor(String::class.java).newInstance(message) as Throwable
|
|
||||||
} catch (_: ReflectiveOperationException) { }
|
|
||||||
targetClass = targetClass.superclass
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun makeRemoteException(cause: Throwable, message: String? = null) =
|
|
||||||
if (cause is CancellationException) cause else RemoteException(message).initCause(cause)
|
|
||||||
protected fun DataInputStream.readException(result: Byte) = when (result.toInt()) {
|
|
||||||
EX_GENERIC -> {
|
|
||||||
val message = readUTF()
|
|
||||||
val name = message.split(':', limit = 2)[0]
|
|
||||||
makeRemoteException(initException(try {
|
|
||||||
classLoader?.loadClass(name)
|
|
||||||
} catch (_: ClassNotFoundException) {
|
|
||||||
null
|
|
||||||
} ?: Class.forName(name), message), message)
|
|
||||||
}
|
|
||||||
EX_PARCELABLE -> makeRemoteException(readParcelable<Parcelable>(classLoader) as Throwable)
|
|
||||||
EX_SERIALIZABLE -> makeRemoteException(readSerializable(classLoader) as Throwable)
|
|
||||||
else -> throw IllegalArgumentException("Unexpected result $result")
|
|
||||||
}
|
|
||||||
|
|
||||||
class Ordinary(server: RootServer, index: Long, classLoader: ClassLoader?,
|
|
||||||
private val callback: CompletableDeferred<Parcelable?>) : Callback(server, index, classLoader) {
|
|
||||||
override fun cancel() = callback.cancel()
|
|
||||||
override fun shouldRemove(result: Byte) = true
|
|
||||||
override fun invoke(input: DataInputStream, result: Byte) {
|
|
||||||
if (result.toInt() == SUCCESS) callback.complete(input.readParcelable(classLoader))
|
|
||||||
else callback.completeExceptionally(input.readException(result))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Channel(server: RootServer, index: Long, classLoader: ClassLoader?,
|
|
||||||
private val channel: SendChannel<Parcelable?>) : Callback(server, index, classLoader) {
|
|
||||||
val finish: CompletableDeferred<Unit> = CompletableDeferred()
|
|
||||||
override fun cancel() = finish.cancel()
|
|
||||||
override fun shouldRemove(result: Byte) = result.toInt() != SUCCESS
|
|
||||||
override fun invoke(input: DataInputStream, result: Byte) {
|
|
||||||
when (result.toInt()) {
|
|
||||||
SUCCESS -> channel.trySend(input.readParcelable(classLoader)).onClosed {
|
|
||||||
active = false
|
|
||||||
sendClosed()
|
|
||||||
finish.completeExceptionally(it
|
|
||||||
?: ClosedSendChannelException("Channel was closed normally"))
|
|
||||||
return
|
|
||||||
}.onFailure { throw it!! } // the channel we are supporting should never block
|
|
||||||
CHANNEL_CONSUMED -> finish.complete(Unit)
|
|
||||||
else -> finish.completeExceptionally(input.readException(result))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LaunchException(cause: Throwable) : RuntimeException("Failed to launch root daemon", cause)
|
|
||||||
class UnexpectedExitException : RemoteException("Root process exited unexpectedly")
|
|
||||||
|
|
||||||
private lateinit var process: Process
|
|
||||||
/**
|
|
||||||
* Thread safety: needs to be protected by callbackLookup.
|
|
||||||
*/
|
|
||||||
private lateinit var output: DataOutputStream
|
|
||||||
|
|
||||||
@Volatile
|
|
||||||
var active = false
|
|
||||||
private var counter = 0L
|
|
||||||
private var callbackListenerExit: Deferred<Unit>? = null
|
|
||||||
private val callbackLookup = LongSparseArray<Callback>()
|
|
||||||
|
|
||||||
private fun readUnexpectedStderr(): String? {
|
|
||||||
if (!this::process.isInitialized) return null
|
|
||||||
Logger.me.d("Attempting to read stderr")
|
|
||||||
var available = process.errorStream.available()
|
|
||||||
return if (available <= 0) null else String(ByteArrayOutputStream().apply {
|
|
||||||
try {
|
|
||||||
while (available > 0) {
|
|
||||||
val bytes = ByteArray(available)
|
|
||||||
val len = process.errorStream.read(bytes)
|
|
||||||
if (len < 0) throw EOFException() // should not happen
|
|
||||||
write(bytes, 0, len)
|
|
||||||
available = process.errorStream.available()
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Logger.me.w("Reading stderr was cut short", e)
|
|
||||||
}
|
|
||||||
}.toByteArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun BufferedReader.lookForToken(token: String) {
|
|
||||||
while (true) {
|
|
||||||
val line = readLine() ?: throw EOFException()
|
|
||||||
if (line.endsWith(token)) {
|
|
||||||
val extraLength = line.length - token.length
|
|
||||||
if (extraLength > 0) Logger.me.w(line.substring(0, extraLength))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
Logger.me.w(line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun doInit(context: Context, shouldRelocate: Boolean, niceName: String) {
|
|
||||||
try {
|
|
||||||
val (reader, writer) = try {
|
|
||||||
process = ProcessBuilder("su").start()
|
|
||||||
val token1 = UUID.randomUUID().toString()
|
|
||||||
val writer = DataOutputStream(process.outputStream.buffered())
|
|
||||||
writer.writeBytes("echo $token1\n")
|
|
||||||
writer.flush()
|
|
||||||
val reader = process.inputStream.bufferedReader()
|
|
||||||
reader.lookForToken(token1)
|
|
||||||
Logger.me.d("Root shell initialized")
|
|
||||||
reader to writer
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw NoShellException(e)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
val token2 = UUID.randomUUID().toString()
|
|
||||||
writer.writeBytes(if (shouldRelocate) {
|
|
||||||
val persistence = File(context.codeCacheDir, ".librootkotlinx-uuid")
|
|
||||||
val uuid = context.packageName + '@' + try {
|
|
||||||
persistence.readText()
|
|
||||||
} catch (_: FileNotFoundException) {
|
|
||||||
UUID.randomUUID().toString().also { persistence.writeText(it) }
|
|
||||||
}
|
|
||||||
val (script, relocated) = AppProcess.relocateScript(uuid)
|
|
||||||
script.appendLine(AppProcess.launchString(context.packageCodePath, RootServer::class.java.name,
|
|
||||||
relocated, niceName) + " $token2")
|
|
||||||
script.toString()
|
|
||||||
} else {
|
|
||||||
AppProcess.launchString(context.packageCodePath, RootServer::class.java.name, AppProcess.myExe,
|
|
||||||
niceName) + " $token2\n"
|
|
||||||
})
|
|
||||||
writer.flush()
|
|
||||||
reader.lookForToken(token2) // wait for ready signal
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw LaunchException(e)
|
|
||||||
}
|
|
||||||
output = writer
|
|
||||||
require(!active)
|
|
||||||
active = true
|
|
||||||
Logger.me.d("Root server initialized")
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
readUnexpectedStderr()?.let { Logger.me.e(it) }
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Logger.me.e("Failed to read from stderr", e) // avoid the real exception being swallowed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun callbackSpin() {
|
|
||||||
val input = DataInputStream(process.inputStream.buffered())
|
|
||||||
while (active) {
|
|
||||||
val index = try {
|
|
||||||
input.readLong()
|
|
||||||
} catch (_: EOFException) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
val result = input.readByte()
|
|
||||||
val callback = synchronized(callbackLookup) {
|
|
||||||
if (active) (callbackLookup[index] ?: error("Empty callback #$index")).also {
|
|
||||||
if (it.shouldRemove(result)) {
|
|
||||||
callbackLookup.remove(index)
|
|
||||||
it.active = false
|
|
||||||
}
|
|
||||||
} else null
|
|
||||||
} ?: break
|
|
||||||
Logger.me.d("Received callback #$index: $result")
|
|
||||||
callback(input, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize a RootServer synchronously, can throw a lot of exceptions.
|
|
||||||
*
|
|
||||||
* @param context Any [Context] from the app.
|
|
||||||
* @param shouldRelocate Whether app process should be copied first. See also [AppProcess.shouldRelocateHeuristics].
|
|
||||||
* @param niceName Name to call the rooted Java process.
|
|
||||||
*/
|
|
||||||
suspend fun init(context: Context, shouldRelocate: Boolean = false,
|
|
||||||
niceName: String = "${context.packageName}:root") {
|
|
||||||
withContext(Dispatchers.IO) { doInit(context, shouldRelocate, niceName) }
|
|
||||||
callbackListenerExit = GlobalScope.async(Dispatchers.IO) {
|
|
||||||
val errorReader = async(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
process.errorStream.bufferedReader().forEachLine(Logger.me::w)
|
|
||||||
} catch (_: IOException) { }
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
callbackSpin()
|
|
||||||
if (active) throw UnexpectedExitException()
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
process.destroy()
|
|
||||||
throw e
|
|
||||||
} finally {
|
|
||||||
Logger.me.d("Waiting for exit")
|
|
||||||
withContext(NonCancellable) { errorReader.await() }
|
|
||||||
process.waitFor()
|
|
||||||
closeInternal(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Caller should check for active.
|
|
||||||
*/
|
|
||||||
private fun sendLocked(command: Parcelable) {
|
|
||||||
try {
|
|
||||||
output.writeParcelable(command)
|
|
||||||
output.flush()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
if (e.isEBADF) throw CancellationException().initCause(e) else throw e
|
|
||||||
} catch (e: ErrnoException) {
|
|
||||||
if (e.errno == OsConstants.EPIPE) throw CancellationException().initCause(e) else throw e
|
|
||||||
}
|
|
||||||
Logger.me.d("Sent #$counter: $command")
|
|
||||||
counter++
|
|
||||||
}
|
|
||||||
|
|
||||||
fun execute(command: RootCommandOneWay) = synchronized(callbackLookup) { if (active) sendLocked(command) }
|
|
||||||
@Throws(RemoteException::class)
|
|
||||||
suspend inline fun <reified T : Parcelable?> execute(command: RootCommand<T>) =
|
|
||||||
execute(command, T::class.java.classLoader)
|
|
||||||
@Throws(RemoteException::class)
|
|
||||||
suspend fun <T : Parcelable?> execute(command: RootCommand<T>, classLoader: ClassLoader?): T {
|
|
||||||
val future = CompletableDeferred<T>()
|
|
||||||
val callback = synchronized(callbackLookup) {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val callback = Callback.Ordinary(this, counter, classLoader, future as CompletableDeferred<Parcelable?>)
|
|
||||||
if (active) {
|
|
||||||
callbackLookup.append(counter, callback)
|
|
||||||
sendLocked(command)
|
|
||||||
} else future.cancel()
|
|
||||||
callback
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return future.await()
|
|
||||||
} finally {
|
|
||||||
if (callback.active) callback.sendClosed()
|
|
||||||
callback.active = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
|
||||||
@Throws(RemoteException::class)
|
|
||||||
inline fun <reified T : Parcelable?> create(command: RootCommandChannel<T>, scope: CoroutineScope) =
|
|
||||||
create(command, scope, T::class.java.classLoader)
|
|
||||||
@ExperimentalCoroutinesApi
|
|
||||||
@Throws(RemoteException::class)
|
|
||||||
fun <T : Parcelable?> create(command: RootCommandChannel<T>, scope: CoroutineScope,
|
|
||||||
classLoader: ClassLoader?) = scope.produce<T>(
|
|
||||||
SupervisorJob(), command.capacity.also {
|
|
||||||
when (it) {
|
|
||||||
Channel.UNLIMITED, Channel.CONFLATED -> { }
|
|
||||||
else -> throw IllegalArgumentException("Unsupported channel capacity $it")
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
val callback = synchronized(callbackLookup) {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val callback = Callback.Channel(this@RootServer, counter, classLoader, this as SendChannel<Parcelable?>)
|
|
||||||
if (active) {
|
|
||||||
callbackLookup.append(counter, callback)
|
|
||||||
sendLocked(command)
|
|
||||||
} else callback.finish.cancel()
|
|
||||||
callback
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
callback.finish.await()
|
|
||||||
} finally {
|
|
||||||
if (callback.active) callback.sendClosed()
|
|
||||||
callback.active = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun closeInternal(fromWorker: Boolean = false) = synchronized(callbackLookup) {
|
|
||||||
if (active) {
|
|
||||||
active = false
|
|
||||||
Logger.me.d(if (fromWorker) "Shutting down from worker" else "Shutting down from client")
|
|
||||||
try {
|
|
||||||
sendLocked(Shutdown())
|
|
||||||
output.close()
|
|
||||||
process.outputStream.close()
|
|
||||||
} catch (_: CancellationException) {
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Logger.me.w("send Shutdown failed", e)
|
|
||||||
}
|
|
||||||
Logger.me.d("Client closed")
|
|
||||||
}
|
|
||||||
if (fromWorker) {
|
|
||||||
for (callback in callbackLookup.valueIterator()) callback.cancel()
|
|
||||||
callbackLookup.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Shutdown the instance gracefully.
|
|
||||||
*/
|
|
||||||
suspend fun close() {
|
|
||||||
closeInternal()
|
|
||||||
val callbackListenerExit = callbackListenerExit ?: return
|
|
||||||
try {
|
|
||||||
withTimeout(10000) { callbackListenerExit.await() }
|
|
||||||
} catch (e: TimeoutCancellationException) {
|
|
||||||
Logger.me.w("Closing the instance has timed out", e)
|
|
||||||
if (Build.VERSION.SDK_INT < 26) process.destroy() else if (process.isAlive) process.destroyForcibly()
|
|
||||||
} catch (e: UnexpectedExitException) {
|
|
||||||
Logger.me.w(e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val SUCCESS = 0
|
|
||||||
private const val EX_GENERIC = 1
|
|
||||||
private const val EX_PARCELABLE = 2
|
|
||||||
private const val EX_SERIALIZABLE = 4
|
|
||||||
private const val CHANNEL_CONSUMED = 3
|
|
||||||
|
|
||||||
private fun DataInputStream.readByteArray() = ByteArray(readInt()).also { readFully(it) }
|
|
||||||
|
|
||||||
private inline fun <reified T : Parcelable> DataInputStream.readParcelable(
|
|
||||||
classLoader: ClassLoader? = T::class.java.classLoader) = readByteArray().toParcelable<T>(classLoader)
|
|
||||||
private fun DataOutputStream.writeParcelable(data: Parcelable?, parcelableFlags: Int = 0) {
|
|
||||||
val bytes = data.toByteArray(parcelableFlags)
|
|
||||||
writeInt(bytes.size)
|
|
||||||
write(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun DataInputStream.readSerializable(classLoader: ClassLoader?) =
|
|
||||||
object : ObjectInputStream(ByteArrayInputStream(readByteArray())) {
|
|
||||||
override fun resolveClass(desc: ObjectStreamClass) = Class.forName(desc.name, false, classLoader)
|
|
||||||
}.readObject()
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun main(args: Array<String>) {
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
|
||||||
Logger.me.e("Uncaught exception from $thread", throwable)
|
|
||||||
throwable.printStackTrace() // stderr will be read by listener
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
rootMain(args)
|
|
||||||
exitProcess(0) // there might be other non-daemon threads
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun DataOutputStream.pushThrowable(callback: Long, e: Throwable) {
|
|
||||||
writeLong(callback)
|
|
||||||
if (e is Parcelable) {
|
|
||||||
writeByte(EX_PARCELABLE)
|
|
||||||
writeParcelable(e)
|
|
||||||
} else try {
|
|
||||||
val bytes = ByteArrayOutputStream().apply {
|
|
||||||
ObjectOutputStream(this).use { it.writeObject(e) }
|
|
||||||
}.toByteArray()
|
|
||||||
writeByte(EX_SERIALIZABLE)
|
|
||||||
writeInt(bytes.size)
|
|
||||||
write(bytes)
|
|
||||||
} catch (_: NotSerializableException) {
|
|
||||||
writeByte(EX_GENERIC)
|
|
||||||
writeUTF(e.stackTraceToString())
|
|
||||||
}
|
|
||||||
flush()
|
|
||||||
}
|
|
||||||
private fun DataOutputStream.pushResult(callback: Long, result: Parcelable?) {
|
|
||||||
writeLong(callback)
|
|
||||||
writeByte(SUCCESS)
|
|
||||||
writeParcelable(result)
|
|
||||||
flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun rootMain(args: Array<String>) {
|
|
||||||
require(args.isNotEmpty())
|
|
||||||
val mainInitialized = CountDownLatch(1)
|
|
||||||
val main = Thread({
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
Looper.prepareMainLooper()
|
|
||||||
mainInitialized.countDown()
|
|
||||||
Looper.loop()
|
|
||||||
}, "main")
|
|
||||||
main.start()
|
|
||||||
val job = Job()
|
|
||||||
val defaultWorker by lazy {
|
|
||||||
mainInitialized.await()
|
|
||||||
CoroutineScope(Dispatchers.Main.immediate + job)
|
|
||||||
}
|
|
||||||
val callbackWorker by lazy {
|
|
||||||
mainInitialized.await()
|
|
||||||
Dispatchers.IO.limitedParallelism(1)
|
|
||||||
}
|
|
||||||
// access to cancellables shall be wrapped in defaultWorker
|
|
||||||
val cancellables = LongSparseArray<() -> Unit>()
|
|
||||||
|
|
||||||
// thread safety: usage of output should be guarded by callbackWorker
|
|
||||||
val output = DataOutputStream(FileOutputStream(Os.dup(FileDescriptor.out)).buffered().apply {
|
|
||||||
// prevent future write attempts to System.out, possibly from Samsung changes (again)
|
|
||||||
Os.dup2(FileDescriptor.err, OsConstants.STDOUT_FILENO)
|
|
||||||
System.setOut(System.err)
|
|
||||||
val writer = writer()
|
|
||||||
writer.appendLine(args[0]) // echo ready signal
|
|
||||||
writer.flush()
|
|
||||||
})
|
|
||||||
// thread safety: usage of input should be in main thread
|
|
||||||
val input = DataInputStream(System.`in`.buffered())
|
|
||||||
var counter = 0L
|
|
||||||
Logger.me.d("Server entering main loop")
|
|
||||||
loop@ while (true) {
|
|
||||||
val command = try {
|
|
||||||
input.readParcelable<Parcelable>(RootServer::class.java.classLoader)
|
|
||||||
} catch (_: EOFException) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
val callback = counter
|
|
||||||
Logger.me.d("Received #$callback: $command")
|
|
||||||
when (command) {
|
|
||||||
is CancelCommand -> defaultWorker.launch { cancellables[command.index]?.invoke() }
|
|
||||||
is RootCommandOneWay -> defaultWorker.launch {
|
|
||||||
try {
|
|
||||||
command.execute()
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.me.e("Unexpected exception in RootCommandOneWay ($command.javaClass.simpleName)", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is RootCommand<*> -> {
|
|
||||||
val commandJob = Job()
|
|
||||||
defaultWorker.launch(commandJob) {
|
|
||||||
cancellables.append(callback) { commandJob.cancel() }
|
|
||||||
val result = try {
|
|
||||||
val result = command.execute();
|
|
||||||
{ output.pushResult(callback, result) }
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
val worker = { output.pushThrowable(callback, e) }
|
|
||||||
worker
|
|
||||||
} finally {
|
|
||||||
cancellables.remove(callback)
|
|
||||||
}
|
|
||||||
withContext(callbackWorker + NonCancellable) { result() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is RootCommandChannel<*> -> defaultWorker.launch {
|
|
||||||
val result = try {
|
|
||||||
coroutineScope {
|
|
||||||
command.create(this).also {
|
|
||||||
cancellables.append(callback) { it.cancel() }
|
|
||||||
}.consumeEach { result ->
|
|
||||||
withContext(callbackWorker) { output.pushResult(callback, result) }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext") {
|
|
||||||
output.writeByte(CHANNEL_CONSUMED)
|
|
||||||
output.writeLong(callback)
|
|
||||||
output.flush()
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
val worker = { output.pushThrowable(callback, e) }
|
|
||||||
worker
|
|
||||||
} finally {
|
|
||||||
cancellables.remove(callback)
|
|
||||||
}
|
|
||||||
withContext(callbackWorker + NonCancellable) { result() }
|
|
||||||
}
|
|
||||||
is Shutdown -> break@loop
|
|
||||||
else -> throw IllegalArgumentException("Unrecognized input: $command")
|
|
||||||
}
|
|
||||||
counter++
|
|
||||||
}
|
|
||||||
job.cancel()
|
|
||||||
Logger.me.d("Clean up initiated before exit. Jobs: ${job.children.joinToString()}")
|
|
||||||
if (runBlocking { withTimeoutOrNull(5000) { job.join() } } == null) {
|
|
||||||
Logger.me.w("Clean up timeout: ${job.children.joinToString()}")
|
|
||||||
} else Logger.me.d("Clean up finished, exiting")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
package be.mygod.librootkotlinx
|
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This object manages creation of [RootServer] and times them out automagically, with default timeout of 5 minutes.
|
|
||||||
*/
|
|
||||||
abstract class RootSession {
|
|
||||||
protected abstract suspend fun initServer(server: RootServer)
|
|
||||||
/**
|
|
||||||
* Timeout to close [RootServer] in milliseconds.
|
|
||||||
*/
|
|
||||||
protected open val timeout get() = TimeUnit.MINUTES.toMillis(5)
|
|
||||||
protected open val timeoutContext: CoroutineContext get() = Dispatchers.Default
|
|
||||||
|
|
||||||
private val mutex = Mutex()
|
|
||||||
private var server: RootServer? = null
|
|
||||||
private var timeoutJob: Job? = null
|
|
||||||
private var usersCount = 0L
|
|
||||||
private var closePending = false
|
|
||||||
|
|
||||||
private suspend fun ensureServerLocked(): RootServer {
|
|
||||||
server?.let {
|
|
||||||
if (it.active) return it
|
|
||||||
usersCount = 0
|
|
||||||
closeLocked()
|
|
||||||
}
|
|
||||||
check(usersCount == 0L) { "Unexpected $server, $usersCount" }
|
|
||||||
val server = RootServer()
|
|
||||||
try {
|
|
||||||
initServer(server)
|
|
||||||
this.server = server
|
|
||||||
return server
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
try {
|
|
||||||
server.close()
|
|
||||||
} catch (eClose: Throwable) {
|
|
||||||
e.addSuppressed(eClose)
|
|
||||||
}
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun closeLocked() {
|
|
||||||
closePending = false
|
|
||||||
val server = server
|
|
||||||
this.server = null
|
|
||||||
server?.close()
|
|
||||||
}
|
|
||||||
private fun startTimeoutLocked() {
|
|
||||||
check(timeoutJob == null)
|
|
||||||
timeoutJob = GlobalScope.launch(timeoutContext, CoroutineStart.UNDISPATCHED) {
|
|
||||||
delay(timeout)
|
|
||||||
mutex.withLock {
|
|
||||||
check(usersCount == 0L)
|
|
||||||
timeoutJob = null
|
|
||||||
closeLocked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun haltTimeoutLocked() {
|
|
||||||
timeoutJob?.cancel()
|
|
||||||
timeoutJob = null
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun acquire() = withContext(NonCancellable) {
|
|
||||||
mutex.withLock {
|
|
||||||
haltTimeoutLocked()
|
|
||||||
closePending = false
|
|
||||||
ensureServerLocked().also { ++usersCount }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
suspend fun release(server: RootServer) = withContext(NonCancellable) {
|
|
||||||
mutex.withLock {
|
|
||||||
if (this@RootSession.server != server) return@withLock // outdated reference
|
|
||||||
require(usersCount > 0)
|
|
||||||
when {
|
|
||||||
!server.active -> {
|
|
||||||
usersCount = 0
|
|
||||||
closeLocked()
|
|
||||||
return@withLock
|
|
||||||
}
|
|
||||||
--usersCount > 0L -> return@withLock
|
|
||||||
closePending -> closeLocked()
|
|
||||||
else -> startTimeoutLocked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
suspend inline fun <T> use(block: (RootServer) -> T): T {
|
|
||||||
val server = acquire()
|
|
||||||
try {
|
|
||||||
return block(server)
|
|
||||||
} finally {
|
|
||||||
release(server)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun closeExisting() = mutex.withLock {
|
|
||||||
if (usersCount > 0) closePending = true else {
|
|
||||||
haltTimeoutLocked()
|
|
||||||
closeLocked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package be.mygod.librootkotlinx
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import androidx.annotation.MainThread
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
|
||||||
import kotlinx.coroutines.channels.ReceiveChannel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
interface RootCommand<Result : Parcelable?> : Parcelable {
|
|
||||||
/**
|
|
||||||
* If a throwable was thrown, it will be wrapped in RemoteException only if it implements [Parcelable].
|
|
||||||
*/
|
|
||||||
@MainThread
|
|
||||||
suspend fun execute(): Result
|
|
||||||
}
|
|
||||||
|
|
||||||
typealias RootCommandNoResult = RootCommand<Parcelable?>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute a command and discards its result, even if an exception occurs.
|
|
||||||
*
|
|
||||||
* If you want to catch exception, use e.g. [RootCommandNoResult] and return null.
|
|
||||||
*/
|
|
||||||
interface RootCommandOneWay : Parcelable {
|
|
||||||
@MainThread
|
|
||||||
suspend fun execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RootCommandChannel<T : Parcelable?> : Parcelable {
|
|
||||||
/**
|
|
||||||
* The capacity of the channel that is returned by [create] to be used by client.
|
|
||||||
* Only [Channel.UNLIMITED] and [Channel.CONFLATED] is supported for now to avoid blocking the entire connection.
|
|
||||||
*/
|
|
||||||
val capacity: Int get() = Channel.UNLIMITED
|
|
||||||
|
|
||||||
@MainThread
|
|
||||||
fun create(scope: CoroutineScope): ReceiveChannel<T>
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
internal data class CancelCommand(val index: Long) : RootCommandOneWay {
|
|
||||||
override suspend fun execute() = error("Internal implementation")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
internal class Shutdown : Parcelable
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
@file:JvmName("Utils")
|
|
||||||
|
|
||||||
package be.mygod.librootkotlinx
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Parcel
|
|
||||||
import android.os.Parcelable
|
|
||||||
import android.system.ErrnoException
|
|
||||||
import android.system.OsConstants
|
|
||||||
import android.util.*
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class NoShellException(cause: Throwable) : Exception("Root missing", cause)
|
|
||||||
|
|
||||||
internal val currentInstructionSet by lazy {
|
|
||||||
val classVMRuntime = Class.forName("dalvik.system.VMRuntime")
|
|
||||||
val runtime = classVMRuntime.getDeclaredMethod("getRuntime").invoke(null)
|
|
||||||
classVMRuntime.getDeclaredMethod("getCurrentInstructionSet").invoke(runtime) as String
|
|
||||||
}
|
|
||||||
|
|
||||||
private val classSystemProperties by lazy { Class.forName("android.os.SystemProperties") }
|
|
||||||
@get:RequiresApi(26)
|
|
||||||
internal val isVndkLite by lazy {
|
|
||||||
classSystemProperties.getDeclaredMethod("getBoolean", String::class.java, Boolean::class.java).invoke(null,
|
|
||||||
"ro.vndk.lite", false) as Boolean
|
|
||||||
}
|
|
||||||
@get:RequiresApi(26)
|
|
||||||
internal val vndkVersion by lazy {
|
|
||||||
classSystemProperties.getDeclaredMethod("get", String::class.java, String::class.java).invoke(null,
|
|
||||||
"ro.vndk.version", "") as String
|
|
||||||
}
|
|
||||||
|
|
||||||
val systemContext by lazy {
|
|
||||||
val classActivityThread = Class.forName("android.app.ActivityThread")
|
|
||||||
val activityThread = classActivityThread.getMethod("systemMain").invoke(null)
|
|
||||||
classActivityThread.getMethod("getSystemContext").invoke(activityThread) as Context
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableByte(val value: Byte) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableShort(val value: Short) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableInt(val value: Int) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableLong(val value: Long) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableFloat(val value: Float) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableDouble(val value: Double) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableBoolean(val value: Boolean) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableString(val value: String) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableByteArray(val value: ByteArray) : Parcelable {
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as ParcelableByteArray
|
|
||||||
|
|
||||||
if (!value.contentEquals(other.value)) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return value.contentHashCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableIntArray(val value: IntArray) : Parcelable {
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as ParcelableIntArray
|
|
||||||
|
|
||||||
if (!value.contentEquals(other.value)) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return value.contentHashCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableLongArray(val value: LongArray) : Parcelable {
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as ParcelableLongArray
|
|
||||||
|
|
||||||
if (!value.contentEquals(other.value)) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return value.contentHashCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableFloatArray(val value: FloatArray) : Parcelable {
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as ParcelableFloatArray
|
|
||||||
|
|
||||||
if (!value.contentEquals(other.value)) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return value.contentHashCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableDoubleArray(val value: DoubleArray) : Parcelable {
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as ParcelableDoubleArray
|
|
||||||
|
|
||||||
if (!value.contentEquals(other.value)) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return value.contentHashCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableBooleanArray(val value: BooleanArray) : Parcelable {
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as ParcelableBooleanArray
|
|
||||||
|
|
||||||
if (!value.contentEquals(other.value)) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return value.contentHashCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableStringArray(val value: Array<String>) : Parcelable {
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as ParcelableStringArray
|
|
||||||
|
|
||||||
if (!value.contentEquals(other.value)) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return value.contentHashCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableStringList(val value: List<String>) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableSparseIntArray(val value: SparseIntArray) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableSparseLongArray(val value: SparseLongArray) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableSparseBooleanArray(val value: SparseBooleanArray) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableCharSequence(val value: CharSequence) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableSize(val value: Size) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableSizeF(val value: SizeF) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableArray(val value: Array<Parcelable?>) : Parcelable {
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as ParcelableArray
|
|
||||||
|
|
||||||
if (!value.contentEquals(other.value)) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return value.contentHashCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class ParcelableList(val value: List<Parcelable?>) : Parcelable
|
|
||||||
|
|
||||||
@SuppressLint("Recycle")
|
|
||||||
inline fun <T> useParcel(block: (Parcel) -> T) = Parcel.obtain().run {
|
|
||||||
try {
|
|
||||||
block(this)
|
|
||||||
} finally {
|
|
||||||
recycle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Parcelable?.toByteArray(parcelableFlags: Int = 0) = useParcel { p ->
|
|
||||||
p.writeParcelable(this, parcelableFlags)
|
|
||||||
p.marshall()
|
|
||||||
}
|
|
||||||
inline fun <reified T : Parcelable> ByteArray.toParcelable(classLoader: ClassLoader? = T::class.java.classLoader) =
|
|
||||||
useParcel { p ->
|
|
||||||
p.unmarshall(this, 0, size)
|
|
||||||
p.setDataPosition(0)
|
|
||||||
p.readParcelable<T>(classLoader)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stream closed caused in NullOutputStream
|
|
||||||
val IOException.isEBADF get() = (cause as? ErrnoException)?.errno == OsConstants.EBADF ||
|
|
||||||
message?.lowercase(Locale.ENGLISH) == "stream closed"
|
|
||||||
Reference in New Issue
Block a user