Migrate from NoisySu to RootSession
Fix #24. Note that just like before, IpMonitor doesn't use RootSession.
This commit is contained in:
@@ -109,7 +109,7 @@ Undocumented system binaries are all bundled and executable:
|
|||||||
* `echo`;
|
* `echo`;
|
||||||
* `ip` (`link monitor neigh rule` with proper output format);
|
* `ip` (`link monitor neigh rule` with proper output format);
|
||||||
* `iptables` (with correct version corresponding to API level);
|
* `iptables` (with correct version corresponding to API level);
|
||||||
* `su` (needs to support `-c` argument).
|
* `su`.
|
||||||
|
|
||||||
If some of these are unavailable, you can alternatively install a recent version (v1.28.1 or higher) of Busybox.
|
If some of these are unavailable, you can alternatively install a recent version (v1.28.1 or higher) of Busybox.
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ dependencies {
|
|||||||
implementation 'com.android.billingclient:billing:1.1'
|
implementation 'com.android.billingclient:billing:1.1'
|
||||||
implementation 'com.crashlytics.sdk.android:crashlytics:2.9.5'
|
implementation 'com.crashlytics.sdk.android:crashlytics:2.9.5'
|
||||||
implementation 'com.github.luongvo:BadgeView:1.1.5'
|
implementation 'com.github.luongvo:BadgeView:1.1.5'
|
||||||
|
implementation 'com.github.topjohnwu:libsu:2.0.1'
|
||||||
implementation "com.google.android.material:material:$androidxVersion"
|
implementation "com.google.android.material:material:$androidxVersion"
|
||||||
implementation 'com.linkedin.dexmaker:dexmaker-mockito:2.19.1'
|
implementation 'com.linkedin.dexmaker:dexmaker-mockito:2.19.1'
|
||||||
implementation 'com.takisoft.preferencex:preferencex:1.0.0-alpha2'
|
implementation 'com.takisoft.preferencex:preferencex:1.0.0-alpha2'
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import be.mygod.vpnhotspot.widget.SmartSnackbar
|
|||||||
import com.crashlytics.android.Crashlytics
|
import com.crashlytics.android.Crashlytics
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.InterfaceAddress
|
import java.net.InterfaceAddress
|
||||||
import java.net.SocketException
|
|
||||||
|
|
||||||
class LocalOnlyInterfaceManager(val downstream: String) : UpstreamMonitor.Callback {
|
class LocalOnlyInterfaceManager(val downstream: String) : UpstreamMonitor.Callback {
|
||||||
private var routing: Routing? = null
|
private var routing: Routing? = null
|
||||||
@@ -21,13 +20,13 @@ class LocalOnlyInterfaceManager(val downstream: String) : UpstreamMonitor.Callba
|
|||||||
override fun onAvailable(ifname: String, dns: List<InetAddress>) {
|
override fun onAvailable(ifname: String, dns: List<InetAddress>) {
|
||||||
val routing = routing
|
val routing = routing
|
||||||
initRouting(ifname, if (routing == null) null else {
|
initRouting(ifname, if (routing == null) null else {
|
||||||
routing.stop()
|
routing.revert()
|
||||||
routing.hostAddress
|
routing.hostAddress
|
||||||
}, dns)
|
}, dns)
|
||||||
}
|
}
|
||||||
override fun onLost() {
|
override fun onLost() {
|
||||||
val routing = routing ?: return
|
val routing = routing ?: return
|
||||||
routing.stop()
|
routing.revert()
|
||||||
initRouting(null, routing.hostAddress, emptyList())
|
initRouting(null, routing.hostAddress, emptyList())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,9 +38,10 @@ class LocalOnlyInterfaceManager(val downstream: String) : UpstreamMonitor.Callba
|
|||||||
|
|
||||||
private fun initRouting(upstream: String? = null, owner: InterfaceAddress? = null,
|
private fun initRouting(upstream: String? = null, owner: InterfaceAddress? = null,
|
||||||
dns: List<InetAddress> = this.dns) {
|
dns: List<InetAddress> = this.dns) {
|
||||||
try {
|
|
||||||
this.dns = dns
|
this.dns = dns
|
||||||
|
try {
|
||||||
this.routing = Routing(upstream, downstream, owner).apply {
|
this.routing = Routing(upstream, downstream, owner).apply {
|
||||||
|
try {
|
||||||
val strict = app.strict
|
val strict = app.strict
|
||||||
if (strict && upstream == null) return@apply // in this case, nothing to be done
|
if (strict && upstream == null) return@apply // in this case, nothing to be done
|
||||||
if (app.dhcpWorkaround) dhcpWorkaround()
|
if (app.dhcpWorkaround) dhcpWorkaround()
|
||||||
@@ -50,9 +50,14 @@ class LocalOnlyInterfaceManager(val downstream: String) : UpstreamMonitor.Callba
|
|||||||
forward(strict)
|
forward(strict)
|
||||||
if (app.masquerade) masquerade(strict)
|
if (app.masquerade) masquerade(strict)
|
||||||
dnsRedirect(dns)
|
dnsRedirect(dns)
|
||||||
if (!start()) SmartSnackbar.make(R.string.noisy_su_failure).show()
|
} catch (e: Exception) {
|
||||||
|
revert()
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
commit()
|
||||||
}
|
}
|
||||||
} catch (e: SocketException) {
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
SmartSnackbar.make(e.localizedMessage).show()
|
SmartSnackbar.make(e.localizedMessage).show()
|
||||||
Crashlytics.logException(e)
|
Crashlytics.logException(e)
|
||||||
routing = null
|
routing = null
|
||||||
@@ -62,6 +67,6 @@ class LocalOnlyInterfaceManager(val downstream: String) : UpstreamMonitor.Callba
|
|||||||
fun stop() {
|
fun stop() {
|
||||||
UpstreamMonitor.unregisterCallback(this)
|
UpstreamMonitor.unregisterCallback(this)
|
||||||
app.cleanRoutings -= this
|
app.cleanRoutings -= this
|
||||||
routing?.stop()
|
routing?.revert()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,10 +176,17 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
|||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
if (status != Status.IDLE) return START_NOT_STICKY
|
if (status != Status.IDLE) return START_NOT_STICKY
|
||||||
status = Status.STARTING
|
status = Status.STARTING
|
||||||
val matcher = WifiP2pManagerHelper.patternNetworkInfo.matcher(
|
val out = try {
|
||||||
loggerSu("exec dumpsys ${Context.WIFI_P2P_SERVICE}") ?: "")
|
RootSession.use { it.execOut("dumpsys ${Context.WIFI_P2P_SERVICE}") }
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
Crashlytics.logException(e)
|
||||||
|
startFailure(e.localizedMessage)
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
val matcher = WifiP2pManagerHelper.patternNetworkInfo.matcher(out)
|
||||||
when {
|
when {
|
||||||
!matcher.find() -> startFailure(getString(R.string.root_unavailable))
|
!matcher.find() -> startFailure(out)
|
||||||
matcher.group(2) == "true" -> {
|
matcher.group(2) == "true" -> {
|
||||||
unregisterReceiver()
|
unregisterReceiver()
|
||||||
registerReceiver(receiver, intentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION,
|
registerReceiver(receiver, intentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import be.mygod.vpnhotspot.net.Routing
|
|||||||
import be.mygod.vpnhotspot.net.UpstreamMonitor
|
import be.mygod.vpnhotspot.net.UpstreamMonitor
|
||||||
import be.mygod.vpnhotspot.preference.AlwaysAutoCompleteEditTextPreferenceDialogFragmentCompat
|
import be.mygod.vpnhotspot.preference.AlwaysAutoCompleteEditTextPreferenceDialogFragmentCompat
|
||||||
import be.mygod.vpnhotspot.preference.SharedPreferenceDataStore
|
import be.mygod.vpnhotspot.preference.SharedPreferenceDataStore
|
||||||
import be.mygod.vpnhotspot.util.loggerSuStream
|
import be.mygod.vpnhotspot.util.RootSession
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import com.crashlytics.android.Crashlytics
|
import com.crashlytics.android.Crashlytics
|
||||||
import com.takisoft.preferencex.PreferenceFragmentCompat
|
import com.takisoft.preferencex.PreferenceFragmentCompat
|
||||||
@@ -35,8 +35,15 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
|||||||
}
|
}
|
||||||
boot.isChecked = BootReceiver.enabled
|
boot.isChecked = BootReceiver.enabled
|
||||||
findPreference("service.clean").setOnPreferenceClickListener {
|
findPreference("service.clean").setOnPreferenceClickListener {
|
||||||
if (Routing.clean() == null) SmartSnackbar.make(R.string.root_unavailable).show()
|
val cleaned = try {
|
||||||
else app.cleanRoutings()
|
Routing.clean()
|
||||||
|
true
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
SmartSnackbar.make(e.localizedMessage).show()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
if (cleaned) app.cleanRoutings()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
findPreference("misc.logcat").setOnPreferenceClickListener {
|
findPreference("misc.logcat").setOnPreferenceClickListener {
|
||||||
@@ -82,8 +89,9 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
|||||||
|logcat -d
|
|logcat -d
|
||||||
""".trimMargin())
|
""".trimMargin())
|
||||||
try {
|
try {
|
||||||
loggerSuStream(commands.toString())?.use { it.copyTo(out) }
|
out.write(RootSession.use { it.execQuiet(commands.toString(), true).out }
|
||||||
} catch (e: IOException) {
|
.joinToString("\n").toByteArray())
|
||||||
|
} catch (e: Exception) {
|
||||||
e.printStackTrace(writer)
|
e.printStackTrace(writer)
|
||||||
Crashlytics.logException(e)
|
Crashlytics.logException(e)
|
||||||
writer.flush()
|
writer.flush()
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import be.mygod.vpnhotspot.util.broadcastReceiver
|
|||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import com.crashlytics.android.Crashlytics
|
import com.crashlytics.android.Crashlytics
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.SocketException
|
|
||||||
|
|
||||||
class TetheringService : IpNeighbourMonitoringService(), UpstreamMonitor.Callback {
|
class TetheringService : IpNeighbourMonitoringService(), UpstreamMonitor.Callback {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -34,7 +33,7 @@ class TetheringService : IpNeighbourMonitoringService(), UpstreamMonitor.Callbac
|
|||||||
private val receiver = broadcastReceiver { _, intent ->
|
private val receiver = broadcastReceiver { _, intent ->
|
||||||
synchronized(routings) {
|
synchronized(routings) {
|
||||||
for (iface in routings.keys - TetheringManager.getTetheredIfaces(intent.extras!!))
|
for (iface in routings.keys - TetheringManager.getTetheredIfaces(intent.extras!!))
|
||||||
routings.remove(iface)?.stop()
|
routings.remove(iface)?.revert()
|
||||||
updateRoutingsLocked()
|
updateRoutingsLocked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,13 +60,13 @@ class TetheringService : IpNeighbourMonitoringService(), UpstreamMonitor.Callbac
|
|||||||
val upstream = upstream
|
val upstream = upstream
|
||||||
val disableIpv6 = app.pref.getBoolean("service.disableIpv6", false)
|
val disableIpv6 = app.pref.getBoolean("service.disableIpv6", false)
|
||||||
if (upstream != null || app.strict || disableIpv6) {
|
if (upstream != null || app.strict || disableIpv6) {
|
||||||
var failed = false
|
|
||||||
val iterator = routings.iterator()
|
val iterator = routings.iterator()
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
val (downstream, value) = iterator.next()
|
val (downstream, value) = iterator.next()
|
||||||
if (value != null) if (value.upstream == upstream) continue else value.stop()
|
if (value != null) if (value.upstream == upstream) continue else value.revert()
|
||||||
try {
|
try {
|
||||||
routings[downstream] = Routing(upstream, downstream).apply {
|
routings[downstream] = Routing(upstream, downstream).apply {
|
||||||
|
try {
|
||||||
if (app.dhcpWorkaround) dhcpWorkaround()
|
if (app.dhcpWorkaround) dhcpWorkaround()
|
||||||
// system tethering already has working forwarding rules
|
// system tethering already has working forwarding rules
|
||||||
// so it doesn't make sense to add additional forwarding rules
|
// so it doesn't make sense to add additional forwarding rules
|
||||||
@@ -78,16 +77,20 @@ class TetheringService : IpNeighbourMonitoringService(), UpstreamMonitor.Callbac
|
|||||||
if (app.masquerade) masquerade()
|
if (app.masquerade) masquerade()
|
||||||
if (upstream != null) dnsRedirect(dns)
|
if (upstream != null) dnsRedirect(dns)
|
||||||
if (disableIpv6) disableIpv6()
|
if (disableIpv6) disableIpv6()
|
||||||
if (!start()) failed = true
|
} catch (e: Exception) {
|
||||||
|
revert()
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
commit()
|
||||||
}
|
}
|
||||||
} catch (e: SocketException) {
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
Crashlytics.logException(e)
|
Crashlytics.logException(e)
|
||||||
|
SmartSnackbar.make(e.localizedMessage).show()
|
||||||
iterator.remove()
|
iterator.remove()
|
||||||
failed = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (failed) SmartSnackbar.make(R.string.noisy_su_failure).show()
|
|
||||||
}
|
}
|
||||||
updateNotification()
|
updateNotification()
|
||||||
}
|
}
|
||||||
@@ -101,7 +104,7 @@ class TetheringService : IpNeighbourMonitoringService(), UpstreamMonitor.Callbac
|
|||||||
val iface = intent.getStringExtra(EXTRA_ADD_INTERFACE)
|
val iface = intent.getStringExtra(EXTRA_ADD_INTERFACE)
|
||||||
synchronized(routings) {
|
synchronized(routings) {
|
||||||
if (iface != null) routings[iface] = null
|
if (iface != null) routings[iface] = null
|
||||||
routings.remove(intent.getStringExtra(EXTRA_REMOVE_INTERFACE))?.stop()
|
routings.remove(intent.getStringExtra(EXTRA_REMOVE_INTERFACE))?.revert()
|
||||||
updateRoutingsLocked()
|
updateRoutingsLocked()
|
||||||
}
|
}
|
||||||
} else if (routings.isEmpty()) stopSelf(startId)
|
} else if (routings.isEmpty()) stopSelf(startId)
|
||||||
@@ -120,7 +123,7 @@ class TetheringService : IpNeighbourMonitoringService(), UpstreamMonitor.Callbac
|
|||||||
this.dns = emptyList()
|
this.dns = emptyList()
|
||||||
synchronized(routings) {
|
synchronized(routings) {
|
||||||
for ((iface, routing) in routings) {
|
for ((iface, routing) in routings) {
|
||||||
routing?.stop()
|
routing?.revert()
|
||||||
routings[iface] = null
|
routings[iface] = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,17 +21,10 @@ abstract class IpMonitor : Runnable {
|
|||||||
private var monitor: Process? = null
|
private var monitor: Process? = null
|
||||||
private var pool: ScheduledExecutorService? = null
|
private var pool: ScheduledExecutorService? = null
|
||||||
|
|
||||||
init {
|
private fun handleProcess(process: Process): Int {
|
||||||
thread("${javaClass.simpleName}-input") {
|
val err = thread("${javaClass.simpleName}-error") {
|
||||||
// monitor may get rejected by SELinux
|
|
||||||
val monitor = ProcessBuilder("sh", "-c",
|
|
||||||
"ip monitor $monitoredObject || exec su -c 'exec ip monitor $monitoredObject'")
|
|
||||||
.redirectErrorStream(true)
|
|
||||||
.start()
|
|
||||||
this.monitor = monitor
|
|
||||||
thread("${javaClass.simpleName}-error") {
|
|
||||||
try {
|
try {
|
||||||
monitor.errorStream.bufferedReader().forEachLine {
|
process.errorStream.bufferedReader().forEachLine {
|
||||||
Crashlytics.log(Log.ERROR, javaClass.simpleName, it)
|
Crashlytics.log(Log.ERROR, javaClass.simpleName, it)
|
||||||
}
|
}
|
||||||
} catch (_: InterruptedIOException) { } catch (e: IOException) {
|
} catch (_: InterruptedIOException) { } catch (e: IOException) {
|
||||||
@@ -40,18 +33,27 @@ abstract class IpMonitor : Runnable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
monitor.inputStream.bufferedReader().forEachLine(this::processLine)
|
process.inputStream.bufferedReader().forEachLine(this::processLine)
|
||||||
monitor.waitFor()
|
} catch (_: InterruptedIOException) { } catch (e: IOException) {
|
||||||
if (monitor.exitValue() == 0) return@thread
|
e.printStackTrace()
|
||||||
|
Crashlytics.logException(e)
|
||||||
|
}
|
||||||
|
err.join()
|
||||||
|
process.waitFor()
|
||||||
|
return process.exitValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
thread("${javaClass.simpleName}-input") {
|
||||||
|
// monitor may get rejected by SELinux
|
||||||
|
if (handleProcess(ProcessBuilder("ip", "monitor", monitoredObject).start()) == 0) return@thread
|
||||||
|
if (handleProcess(ProcessBuilder("su", "-c", "exec ip monitor $monitoredObject").start()) == 0)
|
||||||
|
return@thread
|
||||||
Crashlytics.log(Log.WARN, javaClass.simpleName, "Failed to set up monitor, switching to polling")
|
Crashlytics.log(Log.WARN, javaClass.simpleName, "Failed to set up monitor, switching to polling")
|
||||||
Crashlytics.logException(MonitorFailure())
|
Crashlytics.logException(MonitorFailure())
|
||||||
val pool = Executors.newScheduledThreadPool(1)
|
val pool = Executors.newScheduledThreadPool(1)
|
||||||
pool.scheduleAtFixedRate(this, 1, 1, TimeUnit.SECONDS)
|
pool.scheduleAtFixedRate(this, 1, 1, TimeUnit.SECONDS)
|
||||||
this.pool = pool
|
this.pool = pool
|
||||||
} catch (_: InterruptedIOException) { } catch (e: IOException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
Crashlytics.logException(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,15 @@ package be.mygod.vpnhotspot.net
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.R
|
import be.mygod.vpnhotspot.R
|
||||||
|
import be.mygod.vpnhotspot.util.RootSession
|
||||||
import be.mygod.vpnhotspot.util.debugLog
|
import be.mygod.vpnhotspot.util.debugLog
|
||||||
import be.mygod.vpnhotspot.util.noisySu
|
|
||||||
import java.net.*
|
import java.net.*
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A transaction wrapper that helps set up routing environment.
|
||||||
|
*
|
||||||
|
* Once revert is called, this object no longer serves any purpose.
|
||||||
|
*/
|
||||||
class Routing(val upstream: String?, private val downstream: String, ownerAddress: InterfaceAddress? = null) {
|
class Routing(val upstream: String?, private val downstream: String, ownerAddress: InterfaceAddress? = null) {
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
@@ -18,17 +22,22 @@ class Routing(val upstream: String?, private val downstream: String, ownerAddres
|
|||||||
*/
|
*/
|
||||||
private val IPTABLES = if (Build.VERSION.SDK_INT >= 26) "iptables -w 1" else "iptables -w"
|
private val IPTABLES = if (Build.VERSION.SDK_INT >= 26) "iptables -w 1" else "iptables -w"
|
||||||
|
|
||||||
fun clean() = noisySu(
|
fun clean() = RootSession.use {
|
||||||
"$IPTABLES -t nat -F PREROUTING",
|
it.submit("$IPTABLES -t nat -F PREROUTING")
|
||||||
"quiet while $IPTABLES -D FORWARD -j vpnhotspot_fwd; do done",
|
it.submit("while $IPTABLES -D FORWARD -j vpnhotspot_fwd; do done")
|
||||||
"$IPTABLES -F vpnhotspot_fwd",
|
it.submit("$IPTABLES -F vpnhotspot_fwd")
|
||||||
"$IPTABLES -X vpnhotspot_fwd",
|
it.submit("$IPTABLES -X vpnhotspot_fwd")
|
||||||
"quiet while $IPTABLES -t nat -D POSTROUTING -j vpnhotspot_masquerade; do done",
|
it.submit("while $IPTABLES -t nat -D POSTROUTING -j vpnhotspot_masquerade; do done")
|
||||||
"$IPTABLES -t nat -F vpnhotspot_masquerade",
|
it.submit("$IPTABLES -t nat -F vpnhotspot_masquerade")
|
||||||
"$IPTABLES -t nat -X vpnhotspot_masquerade",
|
it.submit("$IPTABLES -t nat -X vpnhotspot_masquerade")
|
||||||
"quiet while ip rule del priority 17900; do done",
|
it.submit("while ip rule del priority 17900; do done")
|
||||||
"quiet while ip rule del iif lo uidrange 0-0 lookup local_network priority 11000; do done",
|
it.submit("while ip rule del iif lo uidrange 0-0 lookup local_network priority 11000; do done")
|
||||||
report = false)
|
}
|
||||||
|
|
||||||
|
fun RootSession.Transaction.iptablesAdd(content: String, table: String = "filter") =
|
||||||
|
exec("$IPTABLES -t $table -A $content", "$IPTABLES -t $table -D $content")
|
||||||
|
fun RootSession.Transaction.iptablesInsert(content: String, table: String = "filter") =
|
||||||
|
exec("$IPTABLES -t $table -I $content", "$IPTABLES -t $table -D $content")
|
||||||
}
|
}
|
||||||
|
|
||||||
class InterfaceNotFoundException : SocketException() {
|
class InterfaceNotFoundException : SocketException() {
|
||||||
@@ -37,18 +46,13 @@ class Routing(val upstream: String?, private val downstream: String, ownerAddres
|
|||||||
|
|
||||||
val hostAddress = ownerAddress ?: NetworkInterface.getByName(downstream)?.interfaceAddresses?.asSequence()
|
val hostAddress = ownerAddress ?: NetworkInterface.getByName(downstream)?.interfaceAddresses?.asSequence()
|
||||||
?.singleOrNull { it.address is Inet4Address } ?: throw InterfaceNotFoundException()
|
?.singleOrNull { it.address is Inet4Address } ?: throw InterfaceNotFoundException()
|
||||||
private val startScript = LinkedList<String>()
|
private val transaction = RootSession.beginTransaction()
|
||||||
private val stopScript = LinkedList<String>()
|
|
||||||
var started = false
|
var started = false
|
||||||
|
|
||||||
fun ipForward() {
|
fun ipForward() = transaction.exec("echo 1 >/proc/sys/net/ipv4/ip_forward")
|
||||||
startScript.add("echo 1 >/proc/sys/net/ipv4/ip_forward")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun disableIpv6() {
|
fun disableIpv6() = transaction.exec("echo 1 >/proc/sys/net/ipv6/conf/$downstream/disable_ipv6",
|
||||||
startScript.add("echo 1 >/proc/sys/net/ipv6/conf/$downstream/disable_ipv6")
|
"echo 0 >/proc/sys/net/ipv6/conf/$downstream/disable_ipv6")
|
||||||
stopScript.add("echo 0 >/proc/sys/net/ipv6/conf/$downstream/disable_ipv6")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Since Android 5.0, RULE_PRIORITY_TETHERING = 18000.
|
* Since Android 5.0, RULE_PRIORITY_TETHERING = 18000.
|
||||||
@@ -58,63 +62,50 @@ class Routing(val upstream: String?, private val downstream: String, ownerAddres
|
|||||||
*/
|
*/
|
||||||
fun rule() {
|
fun rule() {
|
||||||
if (upstream != null) {
|
if (upstream != null) {
|
||||||
startScript.add("ip rule add from all iif $downstream lookup $upstream priority 17900")
|
transaction.exec("ip rule add from all iif $downstream lookup $upstream priority 17900",
|
||||||
// by the time stopScript is called, table entry for upstream may already get removed
|
// by the time stopScript is called, table entry for upstream may already get removed
|
||||||
stopScript.addFirst("ip rule del from all iif $downstream priority 17900")
|
"ip rule del from all iif $downstream priority 17900")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun forward(strict: Boolean = true) {
|
fun forward(strict: Boolean = true) {
|
||||||
startScript.add("quiet $IPTABLES -N vpnhotspot_fwd 2>/dev/null")
|
transaction.execQuiet("$IPTABLES -N vpnhotspot_fwd")
|
||||||
|
transaction.iptablesInsert("FORWARD -j vpnhotspot_fwd")
|
||||||
if (strict) {
|
if (strict) {
|
||||||
if (upstream != null) {
|
if (upstream != null) {
|
||||||
startScript.add("$IPTABLES -A vpnhotspot_fwd -i $upstream -o $downstream -m state --state ESTABLISHED,RELATED -j ACCEPT")
|
transaction.iptablesAdd("vpnhotspot_fwd -i $upstream -o $downstream -m state --state ESTABLISHED,RELATED -j ACCEPT")
|
||||||
startScript.add("$IPTABLES -A vpnhotspot_fwd -i $downstream -o $upstream -j ACCEPT")
|
transaction.iptablesAdd("vpnhotspot_fwd -i $downstream -o $upstream -j ACCEPT")
|
||||||
stopScript.addFirst("$IPTABLES -D vpnhotspot_fwd -i $upstream -o $downstream -m state --state ESTABLISHED,RELATED -j ACCEPT")
|
|
||||||
stopScript.addFirst("$IPTABLES -D vpnhotspot_fwd -i $downstream -o $upstream -j ACCEPT")
|
|
||||||
} // else nothing needs to be done
|
} // else nothing needs to be done
|
||||||
} else {
|
} else {
|
||||||
// for not strict mode, allow downstream packets to be redirected to anywhere
|
// for not strict mode, allow downstream packets to be redirected to anywhere
|
||||||
// because we don't wanna keep track of default network changes
|
// because we don't wanna keep track of default network changes
|
||||||
startScript.add("$IPTABLES -A vpnhotspot_fwd -o $downstream -m state --state ESTABLISHED,RELATED -j ACCEPT")
|
transaction.iptablesAdd("vpnhotspot_fwd -o $downstream -m state --state ESTABLISHED,RELATED -j ACCEPT")
|
||||||
startScript.add("$IPTABLES -A vpnhotspot_fwd -i $downstream -j ACCEPT")
|
transaction.iptablesAdd("vpnhotspot_fwd -i $downstream -j ACCEPT")
|
||||||
stopScript.addFirst("$IPTABLES -D vpnhotspot_fwd -o $downstream -m state --state ESTABLISHED,RELATED -j ACCEPT")
|
|
||||||
stopScript.addFirst("$IPTABLES -D vpnhotspot_fwd -i $downstream -j ACCEPT")
|
|
||||||
}
|
}
|
||||||
startScript.add("$IPTABLES -I FORWARD -j vpnhotspot_fwd")
|
|
||||||
stopScript.addFirst("$IPTABLES -D FORWARD -j vpnhotspot_fwd")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun overrideSystemRules() {
|
fun overrideSystemRules() = transaction.iptablesAdd("vpnhotspot_fwd -i $downstream -j DROP")
|
||||||
startScript.add("$IPTABLES -A vpnhotspot_fwd -i $downstream -j DROP")
|
|
||||||
stopScript.addFirst("$IPTABLES -D vpnhotspot_fwd -i $downstream -j DROP")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun masquerade(strict: Boolean = true) {
|
fun masquerade(strict: Boolean = true) {
|
||||||
val hostSubnet = "${hostAddress.address.hostAddress}/${hostAddress.networkPrefixLength}"
|
val hostSubnet = "${hostAddress.address.hostAddress}/${hostAddress.networkPrefixLength}"
|
||||||
startScript.add("quiet $IPTABLES -t nat -N vpnhotspot_masquerade 2>/dev/null")
|
transaction.execQuiet("$IPTABLES -t nat -N vpnhotspot_masquerade")
|
||||||
|
transaction.iptablesInsert("POSTROUTING -j vpnhotspot_masquerade", "nat")
|
||||||
// note: specifying -i wouldn't work for POSTROUTING
|
// note: specifying -i wouldn't work for POSTROUTING
|
||||||
if (strict) {
|
if (strict) {
|
||||||
if (upstream != null) {
|
if (upstream != null) {
|
||||||
startScript.add("$IPTABLES -t nat -A vpnhotspot_masquerade -s $hostSubnet -o $upstream -j MASQUERADE")
|
transaction.iptablesAdd("vpnhotspot_masquerade -s $hostSubnet -o $upstream -j MASQUERADE", "nat")
|
||||||
stopScript.addFirst("$IPTABLES -t nat -D vpnhotspot_masquerade -s $hostSubnet -o $upstream -j MASQUERADE")
|
|
||||||
} // else nothing needs to be done
|
} // else nothing needs to be done
|
||||||
} else {
|
} else {
|
||||||
startScript.add("$IPTABLES -t nat -A vpnhotspot_masquerade -s $hostSubnet -j MASQUERADE")
|
transaction.iptablesAdd("vpnhotspot_masquerade -s $hostSubnet -j MASQUERADE", "nat")
|
||||||
stopScript.addFirst("$IPTABLES -t nat -D vpnhotspot_masquerade -s $hostSubnet -j MASQUERADE")
|
|
||||||
}
|
}
|
||||||
startScript.add("$IPTABLES -t nat -I POSTROUTING -j vpnhotspot_masquerade")
|
|
||||||
stopScript.addFirst("$IPTABLES -t nat -D POSTROUTING -j vpnhotspot_masquerade")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dnsRedirect(dnses: List<InetAddress>) {
|
fun dnsRedirect(dnses: List<InetAddress>) {
|
||||||
val hostAddress = hostAddress.address.hostAddress
|
val hostAddress = hostAddress.address.hostAddress
|
||||||
val dns = dnses.firstOrNull { it is Inet4Address }?.hostAddress ?: app.pref.getString("service.dns", "8.8.8.8")
|
val dns = dnses.firstOrNull { it is Inet4Address }?.hostAddress ?: app.pref.getString("service.dns", "8.8.8.8")
|
||||||
debugLog("Routing", "Using $dns from ($dnses)")
|
debugLog("Routing", "Using $dns from ($dnses)")
|
||||||
startScript.add("$IPTABLES -t nat -A PREROUTING -i $downstream -p tcp -d $hostAddress --dport 53 -j DNAT --to-destination $dns")
|
transaction.iptablesAdd("PREROUTING -i $downstream -p tcp -d $hostAddress --dport 53 -j DNAT --to-destination $dns", "nat")
|
||||||
startScript.add("$IPTABLES -t nat -A PREROUTING -i $downstream -p udp -d $hostAddress --dport 53 -j DNAT --to-destination $dns")
|
transaction.iptablesAdd("PREROUTING -i $downstream -p udp -d $hostAddress --dport 53 -j DNAT --to-destination $dns", "nat")
|
||||||
stopScript.addFirst("$IPTABLES -t nat -D PREROUTING -i $downstream -p tcp -d $hostAddress --dport 53 -j DNAT --to-destination $dns")
|
|
||||||
stopScript.addFirst("$IPTABLES -t nat -D PREROUTING -i $downstream -p udp -d $hostAddress --dport 53 -j DNAT --to-destination $dns")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -125,20 +116,9 @@ class Routing(val upstream: String?, private val downstream: String, ownerAddres
|
|||||||
*
|
*
|
||||||
* Source: https://android.googlesource.com/platform/system/netd/+/b9baf26/server/RouteController.cpp#57
|
* Source: https://android.googlesource.com/platform/system/netd/+/b9baf26/server/RouteController.cpp#57
|
||||||
*/
|
*/
|
||||||
fun dhcpWorkaround() {
|
fun dhcpWorkaround() = transaction.exec("ip rule add iif lo uidrange 0-0 lookup local_network priority 11000",
|
||||||
startScript.add("ip rule add iif lo uidrange 0-0 lookup local_network priority 11000")
|
"ip rule del iif lo uidrange 0-0 lookup local_network priority 11000")
|
||||||
stopScript.addFirst("ip rule del iif lo uidrange 0-0 lookup local_network priority 11000")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun start(): Boolean {
|
fun commit() = transaction.commit()
|
||||||
if (started) return true
|
fun revert() = transaction.revert()
|
||||||
started = true
|
|
||||||
if (noisySu(startScript) != true) stop()
|
|
||||||
return started
|
|
||||||
}
|
|
||||||
fun stop(): Boolean {
|
|
||||||
if (!started) return true
|
|
||||||
started = false
|
|
||||||
return noisySu(stopScript, false) == true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import android.os.Parcel
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.util.loggerSu
|
import be.mygod.vpnhotspot.util.RootSession
|
||||||
import be.mygod.vpnhotspot.util.noisySu
|
|
||||||
import com.crashlytics.android.Crashlytics
|
import com.crashlytics.android.Crashlytics
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -29,6 +28,7 @@ class P2pSupplicantConfiguration(private val initContent: String? = null) : Parc
|
|||||||
* PSK parser can be found here: https://android.googlesource.com/platform/external/wpa_supplicant_8/+/d2986c2/wpa_supplicant/config.c#488
|
* PSK parser can be found here: https://android.googlesource.com/platform/external/wpa_supplicant_8/+/d2986c2/wpa_supplicant/config.c#488
|
||||||
*/
|
*/
|
||||||
private val pskParser = "^[\\r\\t ]*psk=(ext:|\"(.*)\"|\"(.*)|[0-9a-fA-F]{64}\$)".toRegex(RegexOption.MULTILINE)
|
private val pskParser = "^[\\r\\t ]*psk=(ext:|\"(.*)\"|\"(.*)|[0-9a-fA-F]{64}\$)".toRegex(RegexOption.MULTILINE)
|
||||||
|
private val whitespaceMatcher = "\\s+".toRegex()
|
||||||
|
|
||||||
private val confPath = if (Build.VERSION.SDK_INT >= 28)
|
private val confPath = if (Build.VERSION.SDK_INT >= 28)
|
||||||
"/data/vendor/wifi/wpa/p2p_supplicant.conf" else "/data/misc/wifi/p2p_supplicant.conf"
|
"/data/vendor/wifi/wpa/p2p_supplicant.conf" else "/data/misc/wifi/p2p_supplicant.conf"
|
||||||
@@ -40,12 +40,12 @@ class P2pSupplicantConfiguration(private val initContent: String? = null) : Parc
|
|||||||
}
|
}
|
||||||
override fun describeContents() = 0
|
override fun describeContents() = 0
|
||||||
|
|
||||||
private val contentDelegate = lazy { initContent ?: loggerSu("exec cat $confPath") }
|
private val contentDelegate = lazy { initContent ?: RootSession.use { it.execOut("cat $confPath") } }
|
||||||
private val content by contentDelegate
|
private val content by contentDelegate
|
||||||
|
|
||||||
fun readPsk(handler: ((RuntimeException) -> Unit)? = null): String? {
|
fun readPsk(handler: ((RuntimeException) -> Unit)? = null): String? {
|
||||||
return try {
|
return try {
|
||||||
val match = pskParser.findAll(content ?: return null).single()
|
val match = pskParser.findAll(content).single()
|
||||||
if (match.groups[2] == null && match.groups[3] == null) "" else {
|
if (match.groups[2] == null && match.groups[3] == null) "" else {
|
||||||
// only one will match and hold non-empty value
|
// only one will match and hold non-empty value
|
||||||
val result = match.groupValues[2] + match.groupValues[3]
|
val result = match.groupValues[2] + match.groupValues[3]
|
||||||
@@ -64,8 +64,7 @@ class P2pSupplicantConfiguration(private val initContent: String? = null) : Parc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(config: WifiConfiguration): Boolean? {
|
fun update(config: WifiConfiguration) {
|
||||||
val content = content ?: return null
|
|
||||||
val tempFile = File.createTempFile("vpnhotspot-", ".conf", app.cacheDir)
|
val tempFile = File.createTempFile("vpnhotspot-", ".conf", app.cacheDir)
|
||||||
try {
|
try {
|
||||||
var ssidFound = 0
|
var ssidFound = 0
|
||||||
@@ -86,13 +85,18 @@ class P2pSupplicantConfiguration(private val initContent: String? = null) : Parc
|
|||||||
}
|
}
|
||||||
if (ssidFound != 1 || pskFound != 1) {
|
if (ssidFound != 1 || pskFound != 1) {
|
||||||
Crashlytics.log(Log.WARN, TAG, "Invalid conf ($ssidFound, $pskFound): $content")
|
Crashlytics.log(Log.WARN, TAG, "Invalid conf ($ssidFound, $pskFound): $content")
|
||||||
Crashlytics.logException(InvalidConfigurationError())
|
if (ssidFound == 0 || pskFound == 0) throw InvalidConfigurationError()
|
||||||
|
else Crashlytics.logException(InvalidConfigurationError())
|
||||||
}
|
}
|
||||||
if (ssidFound == 0 || pskFound == 0) return false
|
|
||||||
// pkill not available on Lollipop. Source: https://android.googlesource.com/platform/system/core/+/master/shell_and_utilities/README.md
|
// pkill not available on Lollipop. Source: https://android.googlesource.com/platform/system/core/+/master/shell_and_utilities/README.md
|
||||||
return noisySu("cat ${tempFile.absolutePath} > $confPath",
|
RootSession.use {
|
||||||
if (Build.VERSION.SDK_INT >= 23) "pkill wpa_supplicant"
|
it.exec("cat ${tempFile.absolutePath} > $confPath")
|
||||||
else "set `ps | grep wpa_supplicant`; kill \$2")
|
if (Build.VERSION.SDK_INT >= 23) it.exec("pkill wpa_supplicant") else {
|
||||||
|
val result = it.execOut("ps | grep wpa_supplicant").split(whitespaceMatcher)
|
||||||
|
check(result.size >= 2) { "wpa_supplicant not found, please toggle Airplane mode manually" }
|
||||||
|
it.exec("kill ${result[1]}")
|
||||||
|
}
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (!tempFile.delete()) tempFile.deleteOnExit()
|
if (!tempFile.delete()) tempFile.deleteOnExit()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import be.mygod.vpnhotspot.App.Companion.app
|
|||||||
import be.mygod.vpnhotspot.R
|
import be.mygod.vpnhotspot.R
|
||||||
import be.mygod.vpnhotspot.manage.TetheringFragment
|
import be.mygod.vpnhotspot.manage.TetheringFragment
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
|
import com.crashlytics.android.Crashlytics
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
@@ -85,13 +86,14 @@ class WifiP2pDialogFragment : DialogFragment(), TextWatcher, DialogInterface.OnC
|
|||||||
|
|
||||||
override fun onClick(dialog: DialogInterface?, which: Int) {
|
override fun onClick(dialog: DialogInterface?, which: Int) {
|
||||||
when (which) {
|
when (which) {
|
||||||
DialogInterface.BUTTON_POSITIVE -> when (configurer.update(config!!)) {
|
DialogInterface.BUTTON_POSITIVE -> try {
|
||||||
true -> {
|
configurer.update(config!!)
|
||||||
app.handler.postDelayed((targetFragment as TetheringFragment).adapter.repeaterManager
|
app.handler.postDelayed((targetFragment as TetheringFragment).adapter.repeaterManager
|
||||||
.binder!!::requestGroupUpdate, 1000)
|
.binder!!::requestGroupUpdate, 1000)
|
||||||
}
|
} catch (e: RuntimeException) {
|
||||||
false -> SmartSnackbar.make(R.string.noisy_su_failure).show()
|
e.printStackTrace()
|
||||||
null -> SmartSnackbar.make(R.string.root_unavailable).show()
|
Crashlytics.logException(e)
|
||||||
|
SmartSnackbar.make(e.localizedMessage).show()
|
||||||
}
|
}
|
||||||
DialogInterface.BUTTON_NEUTRAL ->
|
DialogInterface.BUTTON_NEUTRAL ->
|
||||||
(targetFragment as TetheringFragment).adapter.repeaterManager.binder!!.resetCredentials()
|
(targetFragment as TetheringFragment).adapter.repeaterManager.binder!!.resetCredentials()
|
||||||
|
|||||||
@@ -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)
|
|
||||||
107
mobile/src/main/java/be/mygod/vpnhotspot/util/RootSession.kt
Normal file
107
mobile/src/main/java/be/mygod/vpnhotspot/util/RootSession.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user