@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
29
mobile/src/main/java/be/mygod/vpnhotspot/util/Services.kt
Normal file
29
mobile/src/main/java/be/mygod/vpnhotspot/util/Services.kt
Normal 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>()!! }
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user