Migrate from NoisySu to RootSession

Fix #24. Note that just like before, IpMonitor doesn't use RootSession.
This commit is contained in:
Mygod
2018-09-06 15:36:34 +08:00
parent aa624708bb
commit 823ae9633b
12 changed files with 273 additions and 205 deletions

View File

@@ -1,51 +0,0 @@
package be.mygod.vpnhotspot.util
import android.util.Log
import be.mygod.vpnhotspot.App.Companion.app
import com.crashlytics.android.Crashlytics
import java.io.IOException
import java.io.InputStream
private const val NOISYSU_TAG = "NoisySU"
private const val NOISYSU_SUFFIX = "SUCCESS\n"
private class SuFailure(msg: String?) : RuntimeException(msg)
fun loggerSuStream(command: String): InputStream? {
val process = try {
ProcessBuilder("su", "-c", command)
.directory(app.deviceStorage.cacheDir)
.redirectErrorStream(true)
.start()
} catch (e: IOException) {
e.printStackTrace()
Crashlytics.logException(e)
return null
}
return process.inputStream
}
fun loggerSu(command: String): String? {
val stream = loggerSuStream(command) ?: return null
return try {
stream.bufferedReader().readText()
} catch (e: IOException) {
e.printStackTrace()
Crashlytics.logException(e)
null
}
}
fun noisySu(commands: Iterable<String>, report: Boolean = true): Boolean? {
var out = loggerSu("""function noisy() { "$@" || echo "$@" exited with $?; }
${commands.joinToString("\n") { if (it.startsWith("quiet ")) it.substring(6) else "noisy $it" }}
echo $NOISYSU_SUFFIX""")
val result = if (out == null) null else out == NOISYSU_SUFFIX
out = out?.removeSuffix(NOISYSU_SUFFIX)
if (!out.isNullOrBlank()) {
Crashlytics.log(Log.INFO, NOISYSU_TAG, out)
if (report) Crashlytics.logException(SuFailure(out))
}
return result
}
fun noisySu(vararg commands: String, report: Boolean = true) = noisySu(commands.asIterable(), report)

View File

@@ -0,0 +1,107 @@
package be.mygod.vpnhotspot.util
import android.util.Log
import com.crashlytics.android.Crashlytics
import com.topjohnwu.superuser.Shell
import java.util.*
import java.util.concurrent.locks.ReentrantLock
import kotlin.collections.ArrayList
import kotlin.concurrent.withLock
class RootSession {
companion object {
const val TAG = "RootSession"
const val INIT_CHECKPOINT = "$TAG initialized successfully"
private val monitor = ReentrantLock()
private var instance: RootSession? = null
private fun ensureInstance(): RootSession {
var instance = instance
if (instance == null) instance = RootSession().also { RootSession.instance = it }
return instance
}
fun <T> use(operation: (RootSession) -> T) = monitor.withLock { operation(ensureInstance()) }
fun beginTransaction(): Transaction {
monitor.lock()
return try {
ensureInstance()
} catch (e: RuntimeException) {
monitor.unlock()
throw e
}.Transaction()
}
}
class UnexpectedOutputException(msg: String) : RuntimeException(msg)
private fun checkOutput(command: String, result: Shell.Result, out: Boolean = result.out.isNotEmpty(),
err: Boolean = stderr.isNotEmpty()) {
if (result.isSuccess && !out && !err) return
val msg = StringBuilder("$command exited with ${result.code}")
if (out) result.out.forEach { msg.append("\n$it") }
// TODO bug: https://github.com/topjohnwu/libsu/pull/23
if (err) stderr.forEach { msg.append("\nE $it") }
throw UnexpectedOutputException(msg.toString())
}
private val shell = Shell.newInstance("su")
private val stdout = ArrayList<String>()
private val stderr = ArrayList<String>()
init {
// check basic shell functionality very basically
val result = execQuiet("echo $INIT_CHECKPOINT")
checkOutput("echo", result, result.out.joinToString("\n").trim() != INIT_CHECKPOINT)
}
/**
* Don't care about the results, but still sync.
*/
fun submit(command: String) {
val result = execQuiet(command)
if (result.code != 0) Crashlytics.log(Log.VERBOSE, TAG, "$command exited with ${result.code}")
var msg = stderr.joinToString("\n").trim()
if (msg.isNotEmpty()) Crashlytics.log(Log.VERBOSE, TAG, msg)
msg = result.out.joinToString("\n").trim()
if (msg.isNotEmpty()) Crashlytics.log(Log.VERBOSE, TAG, msg)
}
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 exec(command: String) = checkOutput(command, execQuiet(command))
fun execOut(command: String): String {
val result = execQuiet(command)
checkOutput(command, result, false)
return result.out.joinToString("\n")
}
/**
* This transaction is different from what you may have in mind since you can revert it after committing it.
*/
inner class Transaction {
private val revertCommands = LinkedList<String>()
fun exec(command: String, revert: String? = null) {
if (revert != null) revertCommands.addFirst(revert) // add first just in case exec fails
this@RootSession.exec(command)
}
fun execQuiet(command: String) = this@RootSession.execQuiet(command)
fun commit() = monitor.unlock()
fun revert() {
if (revertCommands.isEmpty()) return
val shell = if (monitor.isHeldByCurrentThread) this@RootSession else {
monitor.lock()
ensureInstance()
}
revertCommands.forEach { shell.submit(it) }
revertCommands.clear()
monitor.unlock() // commit
}
}
}