librootkotlinx

Fixes #14, #27, #114, #117.
This commit is contained in:
Mygod
2020-06-21 05:33:39 +08:00
parent 7b1f610f9a
commit ad218d7ec6
51 changed files with 1781 additions and 574 deletions

View File

@@ -1,127 +1,50 @@
package be.mygod.vpnhotspot.util
import android.os.Looper
import androidx.annotation.WorkerThread
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.*
import be.mygod.librootkotlinx.RootServer
import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.root.RoutingCommands
import kotlinx.coroutines.runBlocking
import timber.log.Timber
import java.util.*
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
import kotlin.collections.ArrayList
import kotlin.concurrent.withLock
class RootSession : AutoCloseable {
companion object {
private val monitor = ReentrantLock()
private fun onUnlock() {
if (monitor.holdCount == 1) instance?.startTimeoutLocked()
}
private fun unlock() {
onUnlock()
monitor.unlock()
}
private var instance: RootSession? = null
private fun ensureInstance(): RootSession {
var instance = instance
if (instance == null || !instance.isAlive) instance = RootSession().also { RootSession.instance = it }
return instance
}
fun <T> use(operation: (RootSession) -> T) = monitor.withLock {
val instance = ensureInstance()
instance.haltTimeoutLocked()
operation(instance).also { onUnlock() }
}
fun <T> use(operation: (RootSession) -> T) = monitor.withLock { operation(RootSession()) }
fun beginTransaction(): Transaction {
monitor.lock()
val instance = try {
ensureInstance()
RootSession()
} catch (e: RuntimeException) {
unlock()
monitor.unlock()
throw e
}
instance.haltTimeoutLocked()
return instance.Transaction()
}
@WorkerThread
fun trimMemory() = monitor.withLock {
val instance = instance ?: return
instance.haltTimeoutLocked()
instance.close()
}
fun checkOutput(command: String, result: Shell.Result, out: Boolean = result.out.isNotEmpty(),
err: Boolean = result.err.isNotEmpty()): String {
val msg = StringBuilder("$command exited with ${result.code}")
if (out) result.out.forEach { msg.append("\n$it") }
if (err) result.err.forEach { msg.append("\nE $it") }
if (!result.isSuccess || out || err) throw UnexpectedOutputException(msg.toString(), result)
return msg.toString()
}
}
class UnexpectedOutputException(msg: String, val result: Shell.Result) : RuntimeException(msg)
init {
check(Looper.getMainLooper().thread != Thread.currentThread()) {
"Unable to initialize shell in main thread" // https://github.com/topjohnwu/libsu/issues/33
}
}
private val shell = Shell.newInstance("su")
private val stdout = ArrayList<String>()
private val stderr = ArrayList<String>()
private val isAlive get() = shell.isAlive
private var server: RootServer? = runBlocking { RootManager.acquire() }
override fun close() {
shell.close()
if (instance == this) instance = null
}
private var timeoutJob: Job? = null
private fun startTimeoutLocked() {
check(timeoutJob == null)
timeoutJob = GlobalScope.launch(start = CoroutineStart.UNDISPATCHED) {
delay(TimeUnit.MINUTES.toMillis(5))
monitor.withLock {
close()
timeoutJob = null
}
}
}
private fun haltTimeoutLocked() {
timeoutJob?.cancel()
timeoutJob = null
server = null
server?.let { runBlocking { RootManager.release(it) } }
}
/**
* Don't care about the results, but still sync.
*/
fun submit(command: String) {
val result = execQuiet(command)
val err = result.err.joinToString("\n") { "E $it" }.trim()
val out = result.out.joinToString("\n").trim()
if (result.code != 0 || err.isNotEmpty() || out.isNotEmpty()) {
Timber.v("$command exited with ${result.code}")
if (err.isNotEmpty()) Timber.v(err)
if (out.isNotEmpty()) Timber.v(out)
}
}
fun submit(command: String) = execQuiet(command).message(listOf(command))?.let { Timber.v(it) }
fun execQuiet(command: String, redirect: Boolean = false): Shell.Result {
stdout.clear()
return shell.newJob().add(command).to(stdout, if (redirect) stdout else {
stderr.clear()
stderr
}).exec()
fun execQuiet(command: String, redirect: Boolean = false) = runBlocking {
server!!.execute(RoutingCommands.Process(listOf("sh", "-c", command), redirect))
}
fun exec(command: String) = checkOutput(command, execQuiet(command))
fun exec(command: String) = execQuiet(command).check(listOf(command))
fun execOut(command: String): String {
val result = execQuiet(command)
checkOutput(command, result, false)
return result.out.joinToString("\n")
result.check(listOf(command), false)
return result.out
}
/**
@@ -130,13 +53,13 @@ class RootSession : AutoCloseable {
inner class Transaction {
private val revertCommands = LinkedList<String>()
fun exec(command: String, revert: String? = null) = checkOutput(command, execQuiet(command, revert))
fun execQuiet(command: String, revert: String? = null): Shell.Result {
fun exec(command: String, revert: String? = null) = execQuiet(command, revert).check(listOf(command))
fun execQuiet(command: String, revert: String? = null): RoutingCommands.ProcessResult {
if (revert != null) revertCommands.addFirst(revert) // add first just in case exec fails
return this@RootSession.execQuiet(command)
}
fun commit() = unlock()
fun commit() = monitor.unlock()
fun revert() {
if (revertCommands.isEmpty()) return
@@ -145,15 +68,14 @@ class RootSession : AutoCloseable {
val shell = if (locked) this@RootSession else {
monitor.lock()
locked = true
ensureInstance()
RootSession()
}
shell.haltTimeoutLocked()
revertCommands.forEach { shell.submit(it) }
} catch (e: RuntimeException) { // if revert fails, it should fail silently
} catch (e: RuntimeException) { // if revert fails, it should fail silently
Timber.d(e)
} finally {
revertCommands.clear()
if (locked) unlock() // commit
if (locked) monitor.unlock() // commit
}
}

View File

@@ -0,0 +1,29 @@
package be.mygod.vpnhotspot.util
import android.annotation.SuppressLint
import android.content.Context
import android.net.ConnectivityManager
import android.net.wifi.WifiManager
import android.net.wifi.p2p.WifiP2pManager
import android.util.Log
import androidx.core.content.getSystemService
import timber.log.Timber
@SuppressLint("LogNotTimber")
object Services {
lateinit var context: Context
fun init(context: Context) {
this.context = context
}
val connectivity by lazy { context.getSystemService<ConnectivityManager>()!! }
val p2p by lazy {
try {
context.getSystemService<WifiP2pManager>()
} catch (e: RuntimeException) {
if (android.os.Process.myUid() == 0) Log.w("WifiP2pManager", e) else Timber.w(e)
null
}
}
val wifi by lazy { context.getSystemService<WifiManager>()!! }
}

View File

@@ -25,12 +25,15 @@ import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.widget.SmartSnackbar
import java.lang.invoke.MethodHandles
import java.lang.reflect.InvocationHandler
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
import java.net.InetAddress
import java.net.NetworkInterface
import java.net.SocketException
val Throwable.readableMessage get() = localizedMessage ?: javaClass.name
val Throwable.readableMessage: String get() = if (this is InvocationTargetException) {
targetException.readableMessage
} else localizedMessage ?: javaClass.name
/**
* This is a hack: we wrap longs around in 1 billion and such. Hopefully every language counts in base 10 and this works