Unverified Commit 3cfc17c2 authored by Mygod's avatar Mygod Committed by GitHub

Merge pull request #2089 from Mygod/coroutines

Refactor using coroutines
parents 1658ac57 2241c5aa
...@@ -4,7 +4,7 @@ apply plugin: 'com.github.ben-manes.versions' ...@@ -4,7 +4,7 @@ apply plugin: 'com.github.ben-manes.versions'
buildscript { buildscript {
ext { ext {
kotlinVersion = '1.3.11' kotlinVersion = '1.3.20'
minSdkVersion = 21 minSdkVersion = 21
sdkVersion = 28 sdkVersion = 28
compileSdkVersion = 28 compileSdkVersion = 28
......
...@@ -62,13 +62,13 @@ dependencies { ...@@ -62,13 +62,13 @@ dependencies {
api "android.arch.work:work-runtime-ktx:$workVersion" api "android.arch.work:work-runtime-ktx:$workVersion"
api "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion" api "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion"
api "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" api "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
api "androidx.preference:preference:1.0.0" api 'androidx.preference:preference:1.0.0'
api "androidx.room:room-runtime:$roomVersion" api "androidx.room:room-runtime:$roomVersion"
api 'com.crashlytics.sdk.android:crashlytics:2.9.8' api 'com.crashlytics.sdk.android:crashlytics:2.9.8'
api 'com.google.firebase:firebase-config:16.1.3' api 'com.google.firebase:firebase-config:16.1.3'
api 'com.google.firebase:firebase-core:16.0.6' api 'com.google.firebase:firebase-core:16.0.6'
api 'com.squareup.okhttp3:okhttp:3.12.1'
api "com.takisoft.preferencex:preferencex:$preferencexVersion" api "com.takisoft.preferencex:preferencex:$preferencexVersion"
api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.0'
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycleVersion" kapt "androidx.lifecycle:lifecycle-compiler:$lifecycleVersion"
kapt "androidx.room:room-compiler:$roomVersion" kapt "androidx.room:room-compiler:$roomVersion"
testImplementation "androidx.arch.core:core-testing:$lifecycleVersion" testImplementation "androidx.arch.core:core-testing:$lifecycleVersion"
......
...@@ -32,8 +32,6 @@ import android.content.IntentFilter ...@@ -32,8 +32,6 @@ import android.content.IntentFilter
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.UserManager import android.os.UserManager
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
...@@ -60,7 +58,6 @@ object Core { ...@@ -60,7 +58,6 @@ object Core {
lateinit var app: Application lateinit var app: Application
lateinit var configureIntent: (Context) -> PendingIntent lateinit var configureIntent: (Context) -> PendingIntent
val handler by lazy { Handler(Looper.getMainLooper()) }
val packageInfo: PackageInfo by lazy { getPackageInfo(app.packageName) } val packageInfo: PackageInfo by lazy { getPackageInfo(app.packageName) }
val deviceStorage by lazy { if (Build.VERSION.SDK_INT < 24) app else DeviceStorageApp(app) } val deviceStorage by lazy { if (Build.VERSION.SDK_INT < 24) app else DeviceStorageApp(app) }
val analytics: FirebaseAnalytics by lazy { FirebaseAnalytics.getInstance(deviceStorage) } val analytics: FirebaseAnalytics by lazy { FirebaseAnalytics.getInstance(deviceStorage) }
...@@ -106,7 +103,7 @@ object Core { ...@@ -106,7 +103,7 @@ object Core {
// handle data restored/crash // handle data restored/crash
if (Build.VERSION.SDK_INT >= 24 && DataStore.directBootAware && if (Build.VERSION.SDK_INT >= 24 && DataStore.directBootAware &&
app.getSystemService<UserManager>()?.isUserUnlocked == true) DirectBoot.flushTrafficStats() app.getSystemService<UserManager>()?.isUserUnlocked == true) DirectBoot.flushTrafficStats()
if (DataStore.tcpFastOpen && !TcpFastOpen.sendEnabled) TcpFastOpen.enableAsync() if (DataStore.tcpFastOpen && !TcpFastOpen.sendEnabled) TcpFastOpen.enableTimeout()
if (DataStore.publicStore.getLong(Key.assetUpdateTime, -1) != packageInfo.lastUpdateTime) { if (DataStore.publicStore.getLong(Key.assetUpdateTime, -1) != packageInfo.lastUpdateTime) {
val assetManager = app.assets val assetManager = app.assets
for (dir in arrayOf("acl", "overture")) for (dir in arrayOf("acl", "overture"))
......
...@@ -25,9 +25,9 @@ import android.content.Context ...@@ -25,9 +25,9 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.os.DeadObjectException import android.os.DeadObjectException
import android.os.Handler
import android.os.IBinder import android.os.IBinder
import android.os.RemoteException import android.os.RemoteException
import com.github.shadowsocks.Core
import com.github.shadowsocks.bg.ProxyService import com.github.shadowsocks.bg.ProxyService
import com.github.shadowsocks.bg.TransproxyService import com.github.shadowsocks.bg.TransproxyService
import com.github.shadowsocks.bg.VpnService import com.github.shadowsocks.bg.VpnService
...@@ -38,7 +38,8 @@ import com.github.shadowsocks.utils.Key ...@@ -38,7 +38,8 @@ import com.github.shadowsocks.utils.Key
/** /**
* This object should be compact as it will not get GC-ed. * This object should be compact as it will not get GC-ed.
*/ */
class ShadowsocksConnection(private var listenForDeath: Boolean = false) : ServiceConnection, IBinder.DeathRecipient { class ShadowsocksConnection(private val handler: Handler = Handler(), private var listenForDeath: Boolean = false) :
ServiceConnection, IBinder.DeathRecipient {
companion object { companion object {
val serviceClass get() = when (DataStore.serviceMode) { val serviceClass get() = when (DataStore.serviceMode) {
Key.modeProxy -> ProxyService::class Key.modeProxy -> ProxyService::class
...@@ -66,13 +67,13 @@ class ShadowsocksConnection(private var listenForDeath: Boolean = false) : Servi ...@@ -66,13 +67,13 @@ class ShadowsocksConnection(private var listenForDeath: Boolean = false) : Servi
private var callback: Callback? = null private var callback: Callback? = null
private val serviceCallback = object : IShadowsocksServiceCallback.Stub() { private val serviceCallback = object : IShadowsocksServiceCallback.Stub() {
override fun stateChanged(state: Int, profileName: String?, msg: String?) { override fun stateChanged(state: Int, profileName: String?, msg: String?) {
Core.handler.post { callback!!.stateChanged(state, profileName, msg) } handler.post { callback!!.stateChanged(state, profileName, msg) }
} }
override fun trafficUpdated(profileId: Long, stats: TrafficStats) { override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
Core.handler.post { callback!!.trafficUpdated(profileId, stats) } handler.post { callback!!.trafficUpdated(profileId, stats) }
} }
override fun trafficPersisted(profileId: Long) { override fun trafficPersisted(profileId: Long) {
Core.handler.post { callback!!.trafficPersisted(profileId) } handler.post { callback!!.trafficPersisted(profileId) }
} }
} }
private var binder: IBinder? = null private var binder: IBinder? = null
...@@ -110,7 +111,7 @@ class ShadowsocksConnection(private var listenForDeath: Boolean = false) : Servi ...@@ -110,7 +111,7 @@ class ShadowsocksConnection(private var listenForDeath: Boolean = false) : Servi
override fun binderDied() { override fun binderDied() {
service = null service = null
callback!!.onBinderDied() handler.post(callback!!::onBinderDied)
} }
private fun unregisterCallback() { private fun unregisterCallback() {
......
...@@ -39,8 +39,8 @@ import com.github.shadowsocks.plugin.PluginManager ...@@ -39,8 +39,8 @@ import com.github.shadowsocks.plugin.PluginManager
import com.github.shadowsocks.utils.Action import com.github.shadowsocks.utils.Action
import com.github.shadowsocks.utils.broadcastReceiver import com.github.shadowsocks.utils.broadcastReceiver
import com.github.shadowsocks.utils.printLog import com.github.shadowsocks.utils.printLog
import com.github.shadowsocks.utils.thread
import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.analytics.FirebaseAnalytics
import kotlinx.coroutines.*
import java.io.File import java.io.File
import java.net.UnknownHostException import java.net.UnknownHostException
import java.util.* import java.util.*
...@@ -62,21 +62,22 @@ object BaseService { ...@@ -62,21 +62,22 @@ object BaseService {
const val CONFIG_FILE_UDP = "shadowsocks-udp.conf" const val CONFIG_FILE_UDP = "shadowsocks-udp.conf"
class Data internal constructor(private val service: Interface) { class Data internal constructor(private val service: Interface) {
@Volatile var state = STOPPED var state = STOPPED
val processes = GuardedProcessPool() var processes: GuardedProcessPool? = null
@Volatile var proxy: ProxyInstance? = null var proxy: ProxyInstance? = null
@Volatile var udpFallback: ProxyInstance? = null var udpFallback: ProxyInstance? = null
var notification: ServiceNotification? = null var notification: ServiceNotification? = null
val closeReceiver = broadcastReceiver { _, intent -> val closeReceiver = broadcastReceiver { _, intent ->
when (intent.action) { when (intent.action) {
Action.RELOAD -> service.forceLoad() Action.RELOAD -> service.forceLoad()
else -> service.stopRunner(true) else -> service.stopRunner()
} }
} }
var closeReceiverRegistered = false var closeReceiverRegistered = false
val binder = Binder(this) val binder = Binder(this)
var connectingJob: Job? = null
fun changeState(s: Int, msg: String? = null) { fun changeState(s: Int, msg: String? = null) {
if (state == s && msg == null) return if (state == s && msg == null) return
...@@ -92,6 +93,7 @@ object BaseService { ...@@ -92,6 +93,7 @@ object BaseService {
class Binder(private var data: Data? = null) : IShadowsocksService.Stub(), AutoCloseable { class Binder(private var data: Data? = null) : IShadowsocksService.Stub(), AutoCloseable {
val callbacks = RemoteCallbackList<IShadowsocksServiceCallback>() val callbacks = RemoteCallbackList<IShadowsocksServiceCallback>()
private val bandwidthListeners = HashSet<IBinder>() // the binder is the real identifier private val bandwidthListeners = HashSet<IBinder>() // the binder is the real identifier
private val handler = Handler()
override fun getState(): Int = data!!.state override fun getState(): Int = data!!.state
override fun getProfileName(): String = data!!.proxy?.profile?.name ?: "Idle" override fun getProfileName(): String = data!!.proxy?.profile?.name ?: "Idle"
...@@ -101,17 +103,17 @@ object BaseService { ...@@ -101,17 +103,17 @@ object BaseService {
} }
private fun broadcast(work: (IShadowsocksServiceCallback) -> Unit) { private fun broadcast(work: (IShadowsocksServiceCallback) -> Unit) {
val n = callbacks.beginBroadcast() repeat(callbacks.beginBroadcast()) {
for (i in 0 until n) try { try {
work(callbacks.getBroadcastItem(i)) work(callbacks.getBroadcastItem(it))
} catch (e: Exception) { } catch (e: Exception) {
printLog(e) printLog(e)
} }
}
callbacks.finishBroadcast() callbacks.finishBroadcast()
} }
private fun registerTimeout() = private fun registerTimeout() = handler.postDelayed(this::onTimeout, 1000)
Core.handler.postAtTime(this::onTimeout, this, SystemClock.uptimeMillis() + 1000)
private fun onTimeout() { private fun onTimeout() {
val proxies = listOfNotNull(data!!.proxy, data!!.udpFallback) val proxies = listOfNotNull(data!!.proxy, data!!.udpFallback)
val stats = proxies val stats = proxies
...@@ -157,7 +159,7 @@ object BaseService { ...@@ -157,7 +159,7 @@ object BaseService {
override fun stopListeningForBandwidth(cb: IShadowsocksServiceCallback) { override fun stopListeningForBandwidth(cb: IShadowsocksServiceCallback) {
if (bandwidthListeners.remove(cb.asBinder()) && bandwidthListeners.isEmpty()) { if (bandwidthListeners.remove(cb.asBinder()) && bandwidthListeners.isEmpty()) {
Core.handler.removeCallbacksAndMessages(this) handler.removeCallbacksAndMessages(null)
} }
} }
...@@ -179,7 +181,7 @@ object BaseService { ...@@ -179,7 +181,7 @@ object BaseService {
override fun close() { override fun close() {
callbacks.kill() callbacks.kill()
Core.handler.removeCallbacksAndMessages(this) handler.removeCallbacksAndMessages(null)
data = null data = null
} }
} }
...@@ -193,26 +195,23 @@ object BaseService { ...@@ -193,26 +195,23 @@ object BaseService {
fun forceLoad() { fun forceLoad() {
val (profile, fallback) = Core.currentProfile val (profile, fallback) = Core.currentProfile
?: return stopRunner(true, (this as Context).getString(R.string.profile_empty)) ?: return stopRunner(false, (this as Context).getString(R.string.profile_empty))
if (profile.host.isEmpty() || profile.password.isEmpty() || if (profile.host.isEmpty() || profile.password.isEmpty() ||
fallback != null && (fallback.host.isEmpty() || fallback.password.isEmpty())) { fallback != null && (fallback.host.isEmpty() || fallback.password.isEmpty())) {
stopRunner(true, (this as Context).getString(R.string.proxy_empty)) stopRunner(false, (this as Context).getString(R.string.proxy_empty))
return return
} }
val s = data.state val s = data.state
when (s) { when (s) {
STOPPED -> startRunner() STOPPED -> startRunner()
CONNECTED -> { CONNECTED -> stopRunner(true)
stopRunner(false)
startRunner()
}
else -> Crashlytics.log(Log.WARN, tag, "Illegal state when invoking use: $s") else -> Crashlytics.log(Log.WARN, tag, "Illegal state when invoking use: $s")
} }
} }
fun buildAdditionalArguments(cmd: ArrayList<String>): ArrayList<String> = cmd fun buildAdditionalArguments(cmd: ArrayList<String>): ArrayList<String> = cmd
fun startNativeProcesses() { suspend fun startProcesses() {
val configRoot = (if (Build.VERSION.SDK_INT < 24 || app.getSystemService<UserManager>() val configRoot = (if (Build.VERSION.SDK_INT < 24 || app.getSystemService<UserManager>()
?.isUserUnlocked != false) app else Core.deviceStorage).noBackupFilesDir ?.isUserUnlocked != false) app else Core.deviceStorage).noBackupFilesDir
val udpFallback = data.udpFallback val udpFallback = data.udpFallback
...@@ -233,19 +232,25 @@ object BaseService { ...@@ -233,19 +232,25 @@ object BaseService {
else startService(Intent(this, javaClass)) else startService(Intent(this, javaClass))
} }
fun killProcesses() = data.processes.killAll() suspend fun killProcesses() {
data.processes?.run {
close()
data.processes = null
}
}
fun stopRunner(stopService: Boolean, msg: String? = null) { fun stopRunner(restart: Boolean = false, msg: String? = null) {
if (data.state == STOPPING) return
// channge the state // channge the state
val data = data
data.changeState(STOPPING) data.changeState(STOPPING)
GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) {
Core.analytics.logEvent("stop", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag))) Core.analytics.logEvent("stop", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag)))
killProcesses() killProcesses()
// clean up recevier // clean up recevier
this as Service this@Interface as Service
val data = data
if (data.closeReceiverRegistered) { if (data.closeReceiverRegistered) {
unregisterReceiver(data.closeReceiver) unregisterReceiver(data.closeReceiver)
data.closeReceiverRegistered = false data.closeReceiverRegistered = false
...@@ -265,7 +270,8 @@ object BaseService { ...@@ -265,7 +270,8 @@ object BaseService {
data.changeState(STOPPED, msg) data.changeState(STOPPED, msg)
// stop the service if nothing has bound to it // stop the service if nothing has bound to it
if (stopService) stopSelf() if (restart) startRunner() else stopSelf()
}
} }
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
...@@ -276,7 +282,7 @@ object BaseService { ...@@ -276,7 +282,7 @@ object BaseService {
if (profilePair == null) { if (profilePair == null) {
// gracefully shutdown: https://stackoverflow.com/q/47337857/2245107 // gracefully shutdown: https://stackoverflow.com/q/47337857/2245107
data.notification = createNotification("") data.notification = createNotification("")
stopRunner(true, getString(R.string.profile_empty)) stopRunner(false, getString(R.string.profile_empty))
return Service.START_NOT_STICKY return Service.START_NOT_STICKY
} }
val (profile, fallback) = profilePair val (profile, fallback) = profilePair
...@@ -298,15 +304,18 @@ object BaseService { ...@@ -298,15 +304,18 @@ object BaseService {
Core.analytics.logEvent("start", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag))) Core.analytics.logEvent("start", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag)))
data.changeState(CONNECTING) data.changeState(CONNECTING)
data.connectingJob = GlobalScope.launch(Dispatchers.Main) {
thread("$tag-Connecting") {
try { try {
proxy.init() proxy.init()
data.udpFallback?.init() data.udpFallback?.init()
// Clean up
killProcesses()
startNativeProcesses() killProcesses()
data.processes = GuardedProcessPool {
printLog(it)
data.connectingJob?.apply { runBlocking { cancelAndJoin() } }
stopRunner(false, it.localizedMessage)
}
startProcesses()
proxy.scheduleUpdate() proxy.scheduleUpdate()
data.udpFallback?.scheduleUpdate() data.udpFallback?.scheduleUpdate()
...@@ -314,12 +323,14 @@ object BaseService { ...@@ -314,12 +323,14 @@ object BaseService {
data.changeState(CONNECTED) data.changeState(CONNECTED)
} catch (_: UnknownHostException) { } catch (_: UnknownHostException) {
stopRunner(true, getString(R.string.invalid_server)) stopRunner(false, getString(R.string.invalid_server))
} catch (exc: Throwable) { } catch (exc: Throwable) {
if (exc !is PluginManager.PluginNotFoundException && exc !is VpnService.NullConnectionException) { if (exc !is PluginManager.PluginNotFoundException && exc !is VpnService.NullConnectionException) {
printLog(exc) printLog(exc)
} }
stopRunner(true, "${getString(R.string.service_failed)}: ${exc.localizedMessage}") stopRunner(false, "${getString(R.string.service_failed)}: ${exc.localizedMessage}")
} finally {
data.connectingJob = null
} }
} }
return Service.START_NOT_STICKY return Service.START_NOT_STICKY
......
...@@ -26,135 +26,109 @@ import android.system.ErrnoException ...@@ -26,135 +26,109 @@ import android.system.ErrnoException
import android.system.Os import android.system.Os
import android.system.OsConstants import android.system.OsConstants
import android.util.Log import android.util.Log
import androidx.annotation.MainThread
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.Core import com.github.shadowsocks.Core
import com.github.shadowsocks.utils.Commandline import com.github.shadowsocks.utils.Commandline
import com.github.shadowsocks.utils.thread import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.selects.select
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.util.concurrent.ArrayBlockingQueue import kotlin.concurrent.thread
import java.util.concurrent.atomic.AtomicReference
import java.util.concurrent.TimeUnit
class GuardedProcessPool { class GuardedProcessPool(private val onFatal: (IOException) -> Unit) : CoroutineScope {
companion object Dummy : IOException("Oopsie the developer has made a no-no") { companion object {
private const val TAG = "GuardedProcessPool" private const val TAG = "GuardedProcessPool"
private val ProcessImpl by lazy { Class.forName("java.lang.ProcessManager\$ProcessImpl") } private val pid by lazy {
private val pid by lazy { ProcessImpl.getDeclaredField("pid").apply { isAccessible = true } } Class.forName("java.lang.ProcessManager\$ProcessImpl").getDeclaredField("pid").apply { isAccessible = true }
private val exitValueMutex by lazy {
ProcessImpl.getDeclaredField("exitValueMutex").apply { isAccessible = true }
} }
} }
private inner class Guard(private val cmd: List<String>, private val onRestartCallback: (() -> Unit)?) { private inner class Guard(private val cmd: List<String>) {
val cmdName = File(cmd.first()).nameWithoutExtension private val abortChannel = Channel<Unit>()
val excQueue = ArrayBlockingQueue<IOException>(1) // ArrayBlockingQueue doesn't want null private var startTime: Long = -1
private var pushed = false private lateinit var process: Process
private fun streamLogger(input: InputStream, logger: (String, String) -> Int) = private fun streamLogger(input: InputStream, logger: (String) -> Unit) = try {
thread("StreamLogger-$cmdName") { input.bufferedReader().forEachLine(logger)
try {
input.bufferedReader().forEachLine { logger(TAG, it) }
} catch (_: IOException) { } // ignore } catch (_: IOException) { } // ignore
fun start() {
startTime = SystemClock.elapsedRealtime()
process = ProcessBuilder(cmd).directory(Core.deviceStorage.noBackupFilesDir).start()
} }
private fun pushException(ioException: IOException?) {
if (pushed) return suspend fun abort() {
excQueue.put(ioException ?: Dummy) if (!abortChannel.isClosedForSend) abortChannel.send(Unit)
pushed = true
} }
fun looper(host: HashSet<Thread>) { suspend fun looper(onRestartCallback: (() -> Unit)?) {
var process: Process? = null var running = true
val cmdName = File(cmd.first()).nameWithoutExtension
val exitChannel = Channel<Int>()
try { try {
var callback: (() -> Unit)? = null while (true) {
while (guardThreads.get() === host) { thread(name = "stderr-$cmdName") { streamLogger(process.errorStream) { Log.e(cmdName, it) } }
Crashlytics.log(Log.DEBUG, TAG, "start process: " + Commandline.toString(cmd)) thread(name = "stdout-$cmdName") {
val startTime = SystemClock.elapsedRealtime() runBlocking {
streamLogger(process.inputStream) { Log.i(cmdName, it) }
process = ProcessBuilder(cmd) exitChannel.send(process.waitFor()) // this thread also acts as a daemon thread for waitFor
.redirectErrorStream(true) }
.directory(Core.deviceStorage.noBackupFilesDir) }
.start() if (select {
abortChannel.onReceive { true } // prefer abort to save work
streamLogger(process.inputStream, Log::i) exitChannel.onReceive { false }
streamLogger(process.errorStream, Log::e) }) break
running = false
if (callback == null) callback = onRestartCallback else callback() if (SystemClock.elapsedRealtime() - startTime < 1000) throw IOException("$cmdName exits too fast")
Crashlytics.log(Log.DEBUG, TAG, "restart process: " + Commandline.toString(cmd))
pushException(null) start()
process.waitFor() running = true
onRestartCallback?.invoke()
if (SystemClock.elapsedRealtime() - startTime < 1000) {
Crashlytics.log(Log.WARN, TAG, "process exit too fast, stop guard: $cmdName")
break
}
} }
} catch (_: InterruptedException) {
Crashlytics.log(Log.DEBUG, TAG, "thread interrupt, destroy process: $cmdName")
} catch (e: IOException) { } catch (e: IOException) {
pushException(e) Crashlytics.log(Log.WARN, TAG, "error occurred. stop guard: " + Commandline.toString(cmd))
// calling callback without closing channel first will cause deadlock, therefore we defer it
GlobalScope.launch(Dispatchers.Main) { onFatal(e) }
} finally { } finally {
if (process != null) { abortChannel.close()
if (!running) return // process already exited, nothing to be done
if (Build.VERSION.SDK_INT < 24) { if (Build.VERSION.SDK_INT < 24) {
val pid = pid.get(process) as Int
try { try {
Os.kill(pid, OsConstants.SIGTERM) Os.kill(pid.get(process) as Int, OsConstants.SIGTERM)
} catch (e: ErrnoException) { } catch (e: ErrnoException) {
if (e.errno != OsConstants.ESRCH) throw e if (e.errno != OsConstants.ESRCH) throw e
} }
val mutex = exitValueMutex.get(process) as Object if (withTimeoutOrNull(500) { exitChannel.receive() } != null) return
synchronized(mutex) {
try {
process.exitValue()
} catch (e: IllegalThreadStateException) {
mutex.wait(500)
}
}
} }
process.destroy() // kill the process process.destroy() // kill the process
if (Build.VERSION.SDK_INT >= 26) { if (Build.VERSION.SDK_INT >= 26) {
val isKilled = process.waitFor(1L, TimeUnit.SECONDS) // wait for 1 second if (withTimeoutOrNull(1000) { exitChannel.receive() } != null) return
if (!isKilled) {
process.destroyForcibly() // Force to kill the process if it's still alive process.destroyForcibly() // Force to kill the process if it's still alive
} }
} exitChannel.receive()
process.waitFor() // ensure the process is destroyed
}
pushException(null)
} }
} }
} }
/** private val supervisor = SupervisorJob()
* This is an indication of which thread pool is being active. override val coroutineContext get() = Dispatchers.Main + supervisor
* Reading/writing this collection still needs an additional lock to prevent concurrent modification. private val guards = ArrayList<Guard>()
*/
private val guardThreads = AtomicReference<HashSet<Thread>>(HashSet())
fun start(cmd: List<String>, onRestartCallback: (() -> Unit)? = null): GuardedProcessPool { @MainThread
val guard = Guard(cmd, onRestartCallback) suspend fun start(cmd: List<String>, onRestartCallback: (() -> Unit)? = null) {
val guardThreads = guardThreads.get() Crashlytics.log(Log.DEBUG, TAG, "start process: " + Commandline.toString(cmd))
synchronized(guardThreads) { val guard = Guard(cmd)
guardThreads.add(thread("GuardThread-${guard.cmdName}") { guard.start()
guard.looper(guardThreads) guards += guard
}) launch(start = CoroutineStart.UNDISPATCHED) { guard.looper(onRestartCallback) }
}
val ioException = guard.excQueue.take()
if (ioException !== Dummy) throw ioException
return this
} }
fun killAll() { @MainThread
val guardThreads = guardThreads.getAndSet(HashSet()) suspend fun close() {
synchronized(guardThreads) { guards.forEach { it.abort() }
guardThreads.forEach { it.interrupt() } supervisor.children.forEach { it.join() } // we can't cancel the supervisor as we need it to do clean up
try {
guardThreads.forEach { it.join() }
} catch (_: InterruptedException) { }
}
} }
} }
...@@ -32,8 +32,8 @@ import org.json.JSONObject ...@@ -32,8 +32,8 @@ import org.json.JSONObject
object LocalDnsService { object LocalDnsService {
interface Interface : BaseService.Interface { interface Interface : BaseService.Interface {
override fun startNativeProcesses() { override suspend fun startProcesses() {
super.startNativeProcesses() super.startProcesses()
val data = data val data = data
val profile = data.proxy!!.profile val profile = data.proxy!!.profile
...@@ -86,7 +86,7 @@ object LocalDnsService { ...@@ -86,7 +86,7 @@ object LocalDnsService {
}) })
} }
if (!profile.udpdns) data.processes.start(buildAdditionalArguments(arrayListOf( if (!profile.udpdns) data.processes!!.start(buildAdditionalArguments(arrayListOf(
File(app.applicationInfo.nativeLibraryDir, Executable.OVERTURE).absolutePath, File(app.applicationInfo.nativeLibraryDir, Executable.OVERTURE).absolutePath,
"-c", buildOvertureConfig("overture.conf")))) "-c", buildOvertureConfig("overture.conf"))))
} }
......
...@@ -23,16 +23,19 @@ package com.github.shadowsocks.bg ...@@ -23,16 +23,19 @@ package com.github.shadowsocks.bg
import android.net.LocalServerSocket import android.net.LocalServerSocket
import android.net.LocalSocket import android.net.LocalSocket
import android.net.LocalSocketAddress import android.net.LocalSocketAddress
import android.system.ErrnoException
import android.system.Os
import android.system.OsConstants
import com.github.shadowsocks.utils.printLog import com.github.shadowsocks.utils.printLog
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
abstract class LocalSocketListener(protected val tag: String) : Thread(tag) { abstract class LocalSocketListener(name: String, socketFile: File) : Thread(name), AutoCloseable {
init { private val localSocket = LocalSocket().apply {
setUncaughtExceptionHandler { _, t -> printLog(t) } socketFile.delete() // It's a must-have to close and reuse previous local socket.
bind(LocalSocketAddress(socketFile.absolutePath, LocalSocketAddress.Namespace.FILESYSTEM))
} }
private val serverSocket = LocalServerSocket(localSocket.fileDescriptor)
protected abstract val socketFile: File
@Volatile @Volatile
private var running = true private var running = true
...@@ -40,29 +43,25 @@ abstract class LocalSocketListener(protected val tag: String) : Thread(tag) { ...@@ -40,29 +43,25 @@ abstract class LocalSocketListener(protected val tag: String) : Thread(tag) {
* Inherited class do not need to close input/output streams as they will be closed automatically. * Inherited class do not need to close input/output streams as they will be closed automatically.
*/ */
protected abstract fun accept(socket: LocalSocket) protected abstract fun accept(socket: LocalSocket)
final override fun run() { final override fun run() = localSocket.use {
socketFile.delete() // It's a must-have to close and reuse previous local socket.
LocalSocket().use { localSocket ->
val serverSocket = try {
localSocket.bind(LocalSocketAddress(socketFile.absolutePath, LocalSocketAddress.Namespace.FILESYSTEM))
LocalServerSocket(localSocket.fileDescriptor)
} catch (e: IOException) {
printLog(e)
return
}
while (running) { while (running) {
try { try {
serverSocket.accept() accept(serverSocket.accept())
} catch (e: IOException) { } catch (e: IOException) {
printLog(e) if (running) printLog(e)
continue continue
}?.use(this::accept)
} }
} }
} }
fun stopThread() { override fun close() {
running = false running = false
interrupt() // see also: https://issuetracker.google.com/issues/36945762#comment15
try {
Os.shutdown(localSocket.fileDescriptor, OsConstants.SHUT_RDWR)
} catch (e: ErrnoException) {
if (e.errno != OsConstants.EBADF) throw e // suppress fd already closed
}
join()
} }
} }
...@@ -30,16 +30,18 @@ import com.github.shadowsocks.database.ProfileManager ...@@ -30,16 +30,18 @@ import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.plugin.PluginConfiguration import com.github.shadowsocks.plugin.PluginConfiguration
import com.github.shadowsocks.plugin.PluginManager import com.github.shadowsocks.plugin.PluginManager
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.* import com.github.shadowsocks.utils.DirectBoot
import okhttp3.FormBody import com.github.shadowsocks.utils.parseNumericAddress
import okhttp3.OkHttpClient import com.github.shadowsocks.utils.signaturesCompat
import okhttp3.Request import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.net.HttpURLConnection
import java.net.InetAddress import java.net.InetAddress
import java.net.UnknownHostException import java.net.UnknownHostException
import java.security.MessageDigest import java.security.MessageDigest
import java.util.concurrent.TimeUnit
/** /**
* This class sets up environment for ss-local. * This class sets up environment for ss-local.
...@@ -50,25 +52,26 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro ...@@ -50,25 +52,26 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro
private val plugin = PluginConfiguration(profile.plugin ?: "").selectedOptions private val plugin = PluginConfiguration(profile.plugin ?: "").selectedOptions
val pluginPath by lazy { PluginManager.init(plugin) } val pluginPath by lazy { PluginManager.init(plugin) }
fun init() { suspend fun init() {
if (profile.host == "198.199.101.152") { if (profile.host == "198.199.101.152") {
val client = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
val mdg = MessageDigest.getInstance("SHA-1") val mdg = MessageDigest.getInstance("SHA-1")
mdg.update(Core.packageInfo.signaturesCompat.first().toByteArray()) mdg.update(Core.packageInfo.signaturesCompat.first().toByteArray())
val requestBody = FormBody.Builder() val conn = RemoteConfig.proxyUrl.openConnection() as HttpURLConnection
.add("sig", String(Base64.encode(mdg.digest(), 0))) conn.requestMethod = "POST"
.build() conn.doOutput = true
val request = Request.Builder()
.url(RemoteConfig.proxyUrl) val proxies = try {
.post(requestBody) withTimeout(30_000) {
.build() withContext(Dispatchers.IO) {
conn.outputStream.bufferedWriter().use {
val proxies = client.newCall(request).execute() it.write("sig=" + String(Base64.encode(mdg.digest(), Base64.DEFAULT)))
.body()!!.string().split('|').toMutableList() }
conn.inputStream.bufferedReader().readText()
}
}
} finally {
conn.disconnect()
}.split('|').toMutableList()
proxies.shuffle() proxies.shuffle()
val proxy = proxies.first().split(':') val proxy = proxies.first().split(':')
profile.host = proxy[0].trim() profile.host = proxy[0].trim()
...@@ -80,22 +83,16 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro ...@@ -80,22 +83,16 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro
if (route == Acl.CUSTOM_RULES) Acl.save(Acl.CUSTOM_RULES, Acl.customRules.flatten(10)) if (route == Acl.CUSTOM_RULES) Acl.save(Acl.CUSTOM_RULES, Acl.customRules.flatten(10))
// it's hard to resolve DNS on a specific interface so we'll do it here // it's hard to resolve DNS on a specific interface so we'll do it here
if (profile.host.parseNumericAddress() == null) { if (profile.host.parseNumericAddress() == null) profile.host = withTimeout(10_000) {
thread("ProxyInstance-resolve") { withContext(Dispatchers.IO) { InetAddress.getByName(profile.host).hostAddress }
// A WAR fix for Huawei devices that UnknownHostException cannot be caught correctly } ?: throw UnknownHostException()
try {
profile.host = InetAddress.getByName(profile.host).hostAddress ?: ""
} catch (_: UnknownHostException) { }
}.join(10 * 1000)
if (profile.host.parseNumericAddress() == null) throw UnknownHostException()
}
} }
/** /**
* Sensitive shadowsocks configuration file requires extra protection. It may be stored in encrypted storage or * Sensitive shadowsocks configuration file requires extra protection. It may be stored in encrypted storage or
* device storage, depending on which is currently available. * device storage, depending on which is currently available.
*/ */
fun start(service: BaseService.Interface, stat: File, configFile: File, extraFlag: String? = null) { suspend fun start(service: BaseService.Interface, stat: File, configFile: File, extraFlag: String? = null) {
trafficMonitor = TrafficMonitor(stat) trafficMonitor = TrafficMonitor(stat)
this.configFile = configFile this.configFile = configFile
...@@ -122,7 +119,7 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro ...@@ -122,7 +119,7 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro
if (DataStore.tcpFastOpen) cmd += "--fast-open" if (DataStore.tcpFastOpen) cmd += "--fast-open"
service.data.processes.start(cmd) service.data.processes!!.start(cmd)
} }
fun scheduleUpdate() { fun scheduleUpdate() {
......
...@@ -25,11 +25,12 @@ import androidx.core.os.bundleOf ...@@ -25,11 +25,12 @@ import androidx.core.os.bundleOf
import com.github.shadowsocks.Core import com.github.shadowsocks.Core
import com.github.shadowsocks.core.R import com.github.shadowsocks.core.R
import com.google.firebase.remoteconfig.FirebaseRemoteConfig import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import java.net.URL
object RemoteConfig { object RemoteConfig {
private val config = FirebaseRemoteConfig.getInstance().apply { setDefaults(R.xml.default_configs) } private val config = FirebaseRemoteConfig.getInstance().apply { setDefaults(R.xml.default_configs) }
val proxyUrl get() = config.getString("proxy_url") val proxyUrl get() = URL(config.getString("proxy_url"))
fun fetch() = config.fetch().addOnCompleteListener { fun fetch() = config.fetch().addOnCompleteListener {
if (it.isSuccessful) config.activateFetched() else { if (it.isSuccessful) config.activateFetched() else {
......
...@@ -23,18 +23,14 @@ package com.github.shadowsocks.bg ...@@ -23,18 +23,14 @@ package com.github.shadowsocks.bg
import android.net.LocalSocket import android.net.LocalSocket
import android.os.SystemClock import android.os.SystemClock
import com.github.shadowsocks.aidl.TrafficStats import com.github.shadowsocks.aidl.TrafficStats
import com.github.shadowsocks.utils.printLog
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
class TrafficMonitor(statFile: File) : AutoCloseable { class TrafficMonitor(statFile: File) : AutoCloseable {
private val thread = object : LocalSocketListener("TrafficMonitor") { private val thread = object : LocalSocketListener("TrafficMonitor", statFile) {
override val socketFile = statFile override fun accept(socket: LocalSocket) = socket.use {
override fun accept(socket: LocalSocket) {
try {
val buffer = ByteArray(16) val buffer = ByteArray(16)
if (socket.inputStream.read(buffer) != 16) throw IOException("Unexpected traffic stat length") if (socket.inputStream.read(buffer) != 16) throw IOException("Unexpected traffic stat length")
val stat = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN) val stat = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN)
...@@ -48,16 +44,12 @@ class TrafficMonitor(statFile: File) : AutoCloseable { ...@@ -48,16 +44,12 @@ class TrafficMonitor(statFile: File) : AutoCloseable {
current.rxTotal = rx current.rxTotal = rx
dirty = true dirty = true
} }
} catch (e: IOException) {
printLog(e)
}
} }
}.apply { start() } }.apply { start() }
val current = TrafficStats() val current = TrafficStats()
var out = TrafficStats() var out = TrafficStats()
private var timestampLast = 0L private var timestampLast = 0L
@Volatile
private var dirty = false private var dirty = false
fun requestUpdate(): Pair<TrafficStats, Boolean> { fun requestUpdate(): Pair<TrafficStats, Boolean> {
...@@ -87,5 +79,5 @@ class TrafficMonitor(statFile: File) : AutoCloseable { ...@@ -87,5 +79,5 @@ class TrafficMonitor(statFile: File) : AutoCloseable {
return Pair(out, updated) return Pair(out, updated)
} }
override fun close() = thread.stopThread() override fun close() = thread.close()
} }
...@@ -36,7 +36,7 @@ class TransproxyService : Service(), LocalDnsService.Interface { ...@@ -36,7 +36,7 @@ class TransproxyService : Service(), LocalDnsService.Interface {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int = override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId) super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
private fun startDNSTunnel() { private suspend fun startDNSTunnel() {
val proxy = data.proxy!! val proxy = data.proxy!!
val cmd = arrayListOf(File(applicationInfo.nativeLibraryDir, Executable.SS_TUNNEL).absolutePath, val cmd = arrayListOf(File(applicationInfo.nativeLibraryDir, Executable.SS_TUNNEL).absolutePath,
"-t", "10", "-t", "10",
...@@ -47,10 +47,10 @@ class TransproxyService : Service(), LocalDnsService.Interface { ...@@ -47,10 +47,10 @@ class TransproxyService : Service(), LocalDnsService.Interface {
// config is already built by BaseService.Interface // config is already built by BaseService.Interface
"-c", (data.udpFallback ?: proxy).configFile!!.absolutePath) "-c", (data.udpFallback ?: proxy).configFile!!.absolutePath)
if (DataStore.tcpFastOpen) cmd += "--fast-open" if (DataStore.tcpFastOpen) cmd += "--fast-open"
data.processes.start(cmd) data.processes!!.start(cmd)
} }
private fun startRedsocksDaemon() { private suspend fun startRedsocksDaemon() {
File(Core.deviceStorage.noBackupFilesDir, "redsocks.conf").writeText("""base { File(Core.deviceStorage.noBackupFilesDir, "redsocks.conf").writeText("""base {
log_debug = off; log_debug = off;
log_info = off; log_info = off;
...@@ -66,13 +66,13 @@ redsocks { ...@@ -66,13 +66,13 @@ redsocks {
type = socks5; type = socks5;
} }
""") """)
data.processes.start(listOf( data.processes!!.start(listOf(
File(applicationInfo.nativeLibraryDir, Executable.REDSOCKS).absolutePath, "-c", "redsocks.conf")) File(applicationInfo.nativeLibraryDir, Executable.REDSOCKS).absolutePath, "-c", "redsocks.conf"))
} }
override fun startNativeProcesses() { override suspend fun startProcesses() {
startRedsocksDaemon() startRedsocksDaemon()
super.startNativeProcesses() super.startProcesses()
if (data.proxy!!.profile.udpdns) startDNSTunnel() if (data.proxy!!.profile.udpdns) startDNSTunnel()
} }
......
...@@ -39,6 +39,7 @@ import com.github.shadowsocks.utils.Key ...@@ -39,6 +39,7 @@ import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.Subnet import com.github.shadowsocks.utils.Subnet
import com.github.shadowsocks.utils.parseNumericAddress import com.github.shadowsocks.utils.parseNumericAddress
import com.github.shadowsocks.utils.printLog import com.github.shadowsocks.utils.printLog
import kotlinx.coroutines.*
import java.io.Closeable import java.io.Closeable
import java.io.File import java.io.File
import java.io.FileDescriptor import java.io.FileDescriptor
...@@ -78,10 +79,15 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { ...@@ -78,10 +79,15 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
override fun close() = Os.close(fd) override fun close() = Os.close(fd)
} }
private inner class ProtectWorker : LocalSocketListener("ShadowsocksVpnThread") { private inner class ProtectWorker :
override val socketFile: File = File(Core.deviceStorage.noBackupFilesDir, "protect_path") LocalSocketListener("ShadowsocksVpnThread", File(Core.deviceStorage.noBackupFilesDir, "protect_path")),
CoroutineScope {
private val job = SupervisorJob()
override val coroutineContext get() = Dispatchers.IO + job + CoroutineExceptionHandler { _, t -> printLog(t) }
override fun accept(socket: LocalSocket) = try { override fun accept(socket: LocalSocket) {
launch {
socket.use {
socket.inputStream.read() socket.inputStream.read()
val fd = socket.ancillaryFileDescriptors!!.single()!! val fd = socket.ancillaryFileDescriptors!!.single()!!
CloseableFd(fd).use { CloseableFd(fd).use {
...@@ -96,8 +102,14 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { ...@@ -96,8 +102,14 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
} else protect(getInt.invoke(fd) as Int) } else protect(getInt.invoke(fd) as Int)
}) 0 else 1) }) 0 else 1)
} }
} catch (e: IOException) { }
printLog(e) }
}
suspend fun shutdown() {
job.cancel()
close()
job.join()
} }
} }
inner class NullConnectionException : NullPointerException() { inner class NullConnectionException : NullPointerException() {
...@@ -139,14 +151,14 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { ...@@ -139,14 +151,14 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
else -> super<LocalDnsService.Interface>.onBind(intent) else -> super<LocalDnsService.Interface>.onBind(intent)
} }
override fun onRevoke() = stopRunner(true) override fun onRevoke() = stopRunner()
override fun killProcesses() { override suspend fun killProcesses() {
if (listeningForDefaultNetwork) { if (listeningForDefaultNetwork) {
connectivity.unregisterNetworkCallback(defaultNetworkCallback) connectivity.unregisterNetworkCallback(defaultNetworkCallback)
listeningForDefaultNetwork = false listeningForDefaultNetwork = false
} }
worker?.stopThread() worker?.shutdown()
worker = null worker = null
super.killProcesses() super.killProcesses()
conn?.close() conn?.close()
...@@ -159,16 +171,14 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { ...@@ -159,16 +171,14 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
startActivity(Intent(this, VpnRequestActivity::class.java) startActivity(Intent(this, VpnRequestActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
else return super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId) else return super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
stopRunner(true) stopRunner()
return Service.START_NOT_STICKY return Service.START_NOT_STICKY
} }
override fun startNativeProcesses() { override suspend fun startProcesses() {
val worker = ProtectWorker() worker = ProtectWorker().apply { start() }
worker.start()
this.worker = worker
super.startNativeProcesses() super.startProcesses()
sendFd(startVpn()) sendFd(startVpn())
} }
...@@ -178,7 +188,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { ...@@ -178,7 +188,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
return cmd return cmd
} }
private fun startVpn(): FileDescriptor { private suspend fun startVpn(): FileDescriptor {
val profile = data.proxy!!.profile val profile = data.proxy!!.profile
val builder = Builder() val builder = Builder()
.setConfigureIntent(Core.configureIntent(this)) .setConfigureIntent(Core.configureIntent(this))
...@@ -247,11 +257,11 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { ...@@ -247,11 +257,11 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
cmd += "--dnsgw" cmd += "--dnsgw"
cmd += "127.0.0.1:${DataStore.portLocalDns}" cmd += "127.0.0.1:${DataStore.portLocalDns}"
} }
data.processes.start(cmd, onRestartCallback = { data.processes!!.start(cmd, onRestartCallback = {
try { try {
sendFd(conn.fileDescriptor) sendFd(conn.fileDescriptor)
} catch (e: ErrnoException) { } catch (e: ErrnoException) {
stopRunner(true, e.message) stopRunner(false, e.message)
} }
}) })
return conn.fileDescriptor return conn.fileDescriptor
......
...@@ -26,9 +26,9 @@ import androidx.lifecycle.ViewModel ...@@ -26,9 +26,9 @@ import androidx.lifecycle.ViewModel
import com.github.shadowsocks.Core import com.github.shadowsocks.Core
import com.github.shadowsocks.Core.app import com.github.shadowsocks.Core.app
import com.github.shadowsocks.acl.Acl import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.core.R import com.github.shadowsocks.core.R
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import kotlinx.coroutines.*
import java.io.IOException import java.io.IOException
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.InetSocketAddress import java.net.InetSocketAddress
...@@ -72,14 +72,12 @@ class HttpsTest : ViewModel() { ...@@ -72,14 +72,12 @@ class HttpsTest : ViewModel() {
} }
} }
private var testCount = 0 private var running: Pair<HttpURLConnection, Job>? = null
val status = MutableLiveData<Status>().apply { value = Status.Idle } val status = MutableLiveData<Status>().apply { value = Status.Idle }
fun testConnection() { fun testConnection() {
++testCount cancelTest()
status.value = Status.Testing status.value = Status.Testing
val id = testCount // it would change by other code
thread("ConnectionTest") {
val url = URL("https", when (Core.currentProfile!!.first.route) { val url = URL("https", when (Core.currentProfile!!.first.route) {
Acl.CHINALIST -> "www.qualcomm.cn" Acl.CHINALIST -> "www.qualcomm.cn"
else -> "www.google.com" else -> "www.google.com"
...@@ -90,7 +88,9 @@ class HttpsTest : ViewModel() { ...@@ -90,7 +88,9 @@ class HttpsTest : ViewModel() {
conn.setRequestProperty("Connection", "close") conn.setRequestProperty("Connection", "close")
conn.instanceFollowRedirects = false conn.instanceFollowRedirects = false
conn.useCaches = false conn.useCaches = false
val result = try { running = conn to GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) {
status.value = withContext(Dispatchers.IO) {
try {
val start = SystemClock.elapsedRealtime() val start = SystemClock.elapsedRealtime()
val code = conn.responseCode val code = conn.responseCode
val elapsed = SystemClock.elapsedRealtime() - start val elapsed = SystemClock.elapsedRealtime() - start
...@@ -101,12 +101,18 @@ class HttpsTest : ViewModel() { ...@@ -101,12 +101,18 @@ class HttpsTest : ViewModel() {
} finally { } finally {
conn.disconnect() conn.disconnect()
} }
if (testCount == id) status.postValue(result)
} }
} }
}
private fun cancelTest() = running?.let { (conn, job) ->
job.cancel() // ensure job is cancelled before interrupting
conn.disconnect()
running = null
}
fun invalidate() { fun invalidate() {
++testCount cancelTest()
status.value = Status.Idle status.value = Status.Idle
} }
} }
...@@ -20,6 +20,8 @@ ...@@ -20,6 +20,8 @@
package com.github.shadowsocks.utils package com.github.shadowsocks.utils
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
...@@ -58,5 +60,5 @@ object TcpFastOpen { ...@@ -58,5 +60,5 @@ object TcpFastOpen {
e.localizedMessage e.localizedMessage
} }
} }
fun enableAsync() = thread("TcpFastOpen") { enable() }.join(1000) fun enableTimeout() = runBlocking { withTimeoutOrNull(1000) { enable() } }
} }
...@@ -54,17 +54,6 @@ fun broadcastReceiver(callback: (Context, Intent) -> Unit): BroadcastReceiver = ...@@ -54,17 +54,6 @@ fun broadcastReceiver(callback: (Context, Intent) -> Unit): BroadcastReceiver =
override fun onReceive(context: Context, intent: Intent) = callback(context, intent) override fun onReceive(context: Context, intent: Intent) = callback(context, intent)
} }
/**
* Wrapper for kotlin.concurrent.thread that tracks uncaught exceptions.
*/
fun thread(name: String? = null, start: Boolean = true, isDaemon: Boolean = false,
contextClassLoader: ClassLoader? = null, priority: Int = -1, block: () -> Unit): Thread {
val thread = kotlin.concurrent.thread(false, isDaemon, contextClassLoader, name, priority, block)
thread.setUncaughtExceptionHandler { _, t -> printLog(t) }
if (start) thread.start()
return thread
}
val URLConnection.responseLength: Long val URLConnection.responseLength: Long
get() = if (Build.VERSION.SDK_INT >= 24) contentLengthLong else contentLength.toLong() get() = if (Build.VERSION.SDK_INT >= 24) contentLengthLong else contentLength.toLong()
......
...@@ -22,5 +22,3 @@ ...@@ -22,5 +22,3 @@
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile
-dontwarn com.google.android.gms.internal.** -dontwarn com.google.android.gms.internal.**
-dontwarn okhttp3.**
-dontwarn okio.**
...@@ -32,10 +32,10 @@ import android.content.pm.PackageInfo ...@@ -32,10 +32,10 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.view.* import android.view.*
import android.widget.ImageView import android.widget.ImageView
import android.widget.Switch import android.widget.Switch
import androidx.annotation.UiThread
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
...@@ -47,9 +47,8 @@ import com.github.shadowsocks.database.ProfileManager ...@@ -47,9 +47,8 @@ import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.preference.DataStore import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot import com.github.shadowsocks.utils.DirectBoot
import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.thread
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import java.util.concurrent.atomic.AtomicBoolean import kotlinx.coroutines.*
class AppManager : AppCompatActivity() { class AppManager : AppCompatActivity() {
companion object { companion object {
...@@ -58,13 +57,13 @@ class AppManager : AppCompatActivity() { ...@@ -58,13 +57,13 @@ class AppManager : AppCompatActivity() {
private var receiver: BroadcastReceiver? = null private var receiver: BroadcastReceiver? = null
private var cachedApps: List<PackageInfo>? = null private var cachedApps: List<PackageInfo>? = null
private fun getApps(pm: PackageManager) = synchronized(AppManager) { private suspend fun getApps(pm: PackageManager) = synchronized(AppManager) {
if (receiver == null) receiver = Core.listenForPackageChanges { if (receiver == null) receiver = Core.listenForPackageChanges {
synchronized(AppManager) { synchronized(AppManager) {
receiver = null receiver = null
cachedApps = null cachedApps = null
} }
AppManager.instance?.reloadApps() AppManager.instance?.loadApps()
} }
// Labels and icons can change on configuration (locale, etc.) changes, therefore they are not cached. // Labels and icons can change on configuration (locale, etc.) changes, therefore they are not cached.
val cachedApps = cachedApps ?: pm.getInstalledPackages(PackageManager.GET_PERMISSIONS) val cachedApps = cachedApps ?: pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
...@@ -72,7 +71,10 @@ class AppManager : AppCompatActivity() { ...@@ -72,7 +71,10 @@ class AppManager : AppCompatActivity() {
it.requestedPermissions?.contains(Manifest.permission.INTERNET) ?: false } it.requestedPermissions?.contains(Manifest.permission.INTERNET) ?: false }
this.cachedApps = cachedApps this.cachedApps = cachedApps
cachedApps cachedApps
}.map { ProxiedApp(pm, it.applicationInfo, it.packageName) } }.map {
yield()
ProxiedApp(pm, it.applicationInfo, it.packageName)
}
} }
private class ProxiedApp(private val pm: PackageManager, private val appInfo: ApplicationInfo, private class ProxiedApp(private val pm: PackageManager, private val appInfo: ApplicationInfo,
...@@ -106,17 +108,15 @@ class AppManager : AppCompatActivity() { ...@@ -106,17 +108,15 @@ class AppManager : AppCompatActivity() {
proxiedApps.add(item.packageName) proxiedApps.add(item.packageName)
check.isChecked = true check.isChecked = true
} }
if (!appsLoading.get()) {
DataStore.individual = proxiedApps.joinToString("\n") DataStore.individual = proxiedApps.joinToString("\n")
DataStore.dirty = true DataStore.dirty = true
} }
} }
}
private inner class AppsAdapter : RecyclerView.Adapter<AppViewHolder>() { private inner class AppsAdapter : RecyclerView.Adapter<AppViewHolder>() {
private var apps = listOf<ProxiedApp>() private var apps = listOf<ProxiedApp>()
fun reload() { suspend fun reload() {
apps = getApps(packageManager) apps = getApps(packageManager)
.sortedWith(compareBy({ !proxiedApps.contains(it.packageName) }, { it.name.toString() })) .sortedWith(compareBy({ !proxiedApps.contains(it.packageName) }, { it.name.toString() }))
} }
...@@ -132,38 +132,36 @@ class AppManager : AppCompatActivity() { ...@@ -132,38 +132,36 @@ class AppManager : AppCompatActivity() {
private lateinit var bypassSwitch: Switch private lateinit var bypassSwitch: Switch
private lateinit var appListView: RecyclerView private lateinit var appListView: RecyclerView
private lateinit var loadingView: View private lateinit var loadingView: View
private val appsLoading = AtomicBoolean()
private val handler = Handler()
private val clipboard by lazy { getSystemService<ClipboardManager>()!! } private val clipboard by lazy { getSystemService<ClipboardManager>()!! }
private var loader: Job? = null
private val shortAnimTime by lazy { resources.getInteger(android.R.integer.config_shortAnimTime).toLong() }
private fun View.crossFadeFrom(other: View) {
clearAnimation()
other.clearAnimation()
if (visibility == View.VISIBLE && other.visibility == View.GONE) return
alpha = 0F
visibility = View.VISIBLE
animate().alpha(1F).duration = shortAnimTime
other.animate().alpha(0F).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
other.visibility = View.GONE
}
}).duration = shortAnimTime
}
private fun initProxiedApps(str: String = DataStore.individual) { private fun initProxiedApps(str: String = DataStore.individual) {
proxiedApps = str.split('\n').toHashSet() proxiedApps = str.split('\n').toHashSet()
} }
private fun reloadApps() { @UiThread
if (!appsLoading.compareAndSet(true, false)) loadAppsAsync() private fun loadApps() {
} loader?.cancel()
private fun loadAppsAsync() { loader = GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) {
if (!appsLoading.compareAndSet(false, true)) return loadingView.crossFadeFrom(appListView)
appListView.visibility = View.GONE
loadingView.visibility = View.VISIBLE
thread("AppManager-loader") {
val adapter = appListView.adapter as AppsAdapter val adapter = appListView.adapter as AppsAdapter
do { withContext(Dispatchers.IO) { adapter.reload() }
appsLoading.set(true)
adapter.reload()
} while (!appsLoading.compareAndSet(true, false))
handler.post {
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
val shortAnimTime = resources.getInteger(android.R.integer.config_shortAnimTime) appListView.crossFadeFrom(loadingView)
appListView.alpha = 0F
appListView.visibility = View.VISIBLE
appListView.animate().alpha(1F).duration = shortAnimTime.toLong()
loadingView.animate().alpha(0F).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
loadingView.visibility = View.GONE
}
}).duration = shortAnimTime.toLong()
}
} }
} }
...@@ -199,7 +197,7 @@ class AppManager : AppCompatActivity() { ...@@ -199,7 +197,7 @@ class AppManager : AppCompatActivity() {
appListView.adapter = AppsAdapter() appListView.adapter = AppsAdapter()
instance = this instance = this
loadAppsAsync() loadApps()
} }
override fun onCreateOptionsMenu(menu: Menu?): Boolean { override fun onCreateOptionsMenu(menu: Menu?): Boolean {
...@@ -239,7 +237,7 @@ class AppManager : AppCompatActivity() { ...@@ -239,7 +237,7 @@ class AppManager : AppCompatActivity() {
DataStore.dirty = true DataStore.dirty = true
Snackbar.make(appListView, R.string.action_import_msg, Snackbar.LENGTH_LONG).show() Snackbar.make(appListView, R.string.action_import_msg, Snackbar.LENGTH_LONG).show()
initProxiedApps(apps) initProxiedApps(apps)
reloadApps() loadApps()
return true return true
} catch (_: IllegalArgumentException) { } } catch (_: IllegalArgumentException) { }
} }
...@@ -255,7 +253,7 @@ class AppManager : AppCompatActivity() { ...@@ -255,7 +253,7 @@ class AppManager : AppCompatActivity() {
override fun onDestroy() { override fun onDestroy() {
instance = null instance = null
handler.removeCallbacksAndMessages(null) loader?.cancel()
super.onDestroy() super.onDestroy()
} }
} }
...@@ -29,6 +29,7 @@ import android.nfc.NdefMessage ...@@ -29,6 +29,7 @@ import android.nfc.NdefMessage
import android.nfc.NfcAdapter import android.nfc.NfcAdapter
import android.os.Bundle import android.os.Bundle
import android.os.DeadObjectException import android.os.DeadObjectException
import android.os.Handler
import android.util.Log import android.util.Log
import android.view.KeyCharacterMap import android.view.KeyCharacterMap
import android.view.KeyEvent import android.view.KeyEvent
...@@ -124,7 +125,8 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref ...@@ -124,7 +125,8 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
else -> Core.startService() else -> Core.startService()
} }
private val connection = ShadowsocksConnection(true) private val handler = Handler()
private val connection = ShadowsocksConnection(handler, true)
override fun onServiceConnected(service: IShadowsocksService) = changeState(try { override fun onServiceConnected(service: IShadowsocksService) = changeState(try {
service.state service.state
} catch (_: DeadObjectException) { } catch (_: DeadObjectException) {
...@@ -132,12 +134,10 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref ...@@ -132,12 +134,10 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
}) })
override fun onServiceDisconnected() = changeState(BaseService.IDLE) override fun onServiceDisconnected() = changeState(BaseService.IDLE)
override fun onBinderDied() { override fun onBinderDied() {
Core.handler.post {
connection.disconnect(this) connection.disconnect(this)
Executable.killAll() Executable.killAll()
connection.connect(this, this) connection.connect(this, this)
} }
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when { when {
...@@ -167,7 +167,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref ...@@ -167,7 +167,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
fab.setOnClickListener { toggle() } fab.setOnClickListener { toggle() }
changeState(BaseService.IDLE) // reset everything to init state changeState(BaseService.IDLE) // reset everything to init state
Core.handler.post { connection.connect(this, this) } connection.connect(this, this)
DataStore.publicStore.registerChangeListener(this) DataStore.publicStore.registerChangeListener(this)
val intent = this.intent val intent = this.intent
...@@ -205,7 +205,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref ...@@ -205,7 +205,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) { override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) {
when (key) { when (key) {
Key.serviceMode -> Core.handler.post { Key.serviceMode -> handler.post {
connection.disconnect(this) connection.disconnect(this)
connection.connect(this, this) connection.connect(this, this)
} }
...@@ -280,6 +280,6 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref ...@@ -280,6 +280,6 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
DataStore.publicStore.unregisterChangeListener(this) DataStore.publicStore.unregisterChangeListener(this)
connection.disconnect(this) connection.disconnect(this)
BackupManager(this).dataChanged() BackupManager(this).dataChanged()
Core.handler.removeCallbacksAndMessages(null) handler.removeCallbacksAndMessages(null)
} }
} }
...@@ -55,11 +55,13 @@ ...@@ -55,11 +55,13 @@
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_height="fill_parent"
android:id="@+id/list" android:id="@+id/list"
android:visibility="gone"
app:fastScrollEnabled="true" app:fastScrollEnabled="true"
app:fastScrollHorizontalThumbDrawable="@drawable/fastscroll_thumb" app:fastScrollHorizontalThumbDrawable="@drawable/fastscroll_thumb"
app:fastScrollHorizontalTrackDrawable="@drawable/fastscroll_line" app:fastScrollHorizontalTrackDrawable="@drawable/fastscroll_line"
app:fastScrollVerticalThumbDrawable="@drawable/fastscroll_thumb" app:fastScrollVerticalThumbDrawable="@drawable/fastscroll_thumb"
app:fastScrollVerticalTrackDrawable="@drawable/fastscroll_line" app:fastScrollVerticalTrackDrawable="@drawable/fastscroll_line"
tools:listitem="@layout/layout_apps_item"/> tools:listitem="@layout/layout_apps_item"
tools:visibility="visible"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout> </LinearLayout>
...@@ -22,5 +22,3 @@ ...@@ -22,5 +22,3 @@
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile
-dontwarn com.google.android.gms.internal.** -dontwarn com.google.android.gms.internal.**
-dontwarn okhttp3.**
-dontwarn okio.**
...@@ -27,6 +27,7 @@ import android.content.Intent ...@@ -27,6 +27,7 @@ import android.content.Intent
import android.net.VpnService import android.net.VpnService
import android.os.Bundle import android.os.Bundle
import android.os.DeadObjectException import android.os.DeadObjectException
import android.os.Handler
import android.text.format.Formatter import android.text.format.Formatter
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
...@@ -135,7 +136,8 @@ class MainPreferenceFragment : LeanbackPreferenceFragment(), ShadowsocksConnecti ...@@ -135,7 +136,8 @@ class MainPreferenceFragment : LeanbackPreferenceFragment(), ShadowsocksConnecti
} }
} }
private val connection = ShadowsocksConnection(true) private val handler = Handler()
private val connection = ShadowsocksConnection(handler, true)
override fun onServiceConnected(service: IShadowsocksService) = changeState(try { override fun onServiceConnected(service: IShadowsocksService) = changeState(try {
service.state service.state
} catch (_: DeadObjectException) { } catch (_: DeadObjectException) {
...@@ -143,12 +145,10 @@ class MainPreferenceFragment : LeanbackPreferenceFragment(), ShadowsocksConnecti ...@@ -143,12 +145,10 @@ class MainPreferenceFragment : LeanbackPreferenceFragment(), ShadowsocksConnecti
}) })
override fun onServiceDisconnected() = changeState(BaseService.IDLE) override fun onServiceDisconnected() = changeState(BaseService.IDLE)
override fun onBinderDied() { override fun onBinderDied() {
Core.handler.post {
connection.disconnect(activity) connection.disconnect(activity)
Executable.killAll() Executable.killAll()
connection.connect(activity, this) connection.connect(activity, this)
} }
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.preferenceDataStore = DataStore.publicStore preferenceManager.preferenceDataStore = DataStore.publicStore
...@@ -232,7 +232,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragment(), ShadowsocksConnecti ...@@ -232,7 +232,7 @@ class MainPreferenceFragment : LeanbackPreferenceFragment(), ShadowsocksConnecti
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) { override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) {
when (key) { when (key) {
Key.serviceMode -> Core.handler.post { Key.serviceMode -> handler.post {
connection.disconnect(activity) connection.disconnect(activity)
connection.connect(activity, this) connection.connect(activity, this)
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment