Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,15 @@
<data android:scheme="naive+https" />
<data android:scheme="naive+quic" />
<data android:scheme="hysteria" />
<data android:scheme="hysteria2" />
<data android:scheme="hy2" />
<data android:scheme="vless" />
<data android:scheme="anytls" />
<data android:scheme="snell" />
<data android:scheme="masterdns" />
<data android:scheme="olcrtc" />
<data android:scheme="tuic" />
<data android:scheme="juicity" />
Comment thread
greptile-apps[bot] marked this conversation as resolved.

</intent-filter>

Expand Down
46 changes: 32 additions & 14 deletions app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,11 @@ class BaseService {
profileId > 0L && SagerDatabase.proxyDao.getById(profileId) != null ->
DataStore.selectedProxy = profileId
}
onMainDispatcher { reloadInner(reloadStopGeneration) }
// Compute the in-place selector decision here (off the main thread) so
// reloadInner() does no DAO reads on the UI thread (Plan 027). null tag =>
// no fast-path (fall through to the state machine).
val selectorTag = resolveSelectorReloadTag()
onMainDispatcher { reloadInner(reloadStopGeneration, selectorTag) }
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Expand All @@ -245,7 +249,7 @@ class BaseService {
}
}

private fun reloadInner(reloadStopGeneration: Long) {
private fun reloadInner(reloadStopGeneration: Long, selectorTag: String?) {
// A stop raced the async refresh: drop this stale reload so it can't restart a service
// the user stopped. (A Connecting->Connected transition does NOT bump stopGeneration,
// so an in-flight legitimate reload still applies.)
Expand All @@ -258,16 +262,13 @@ class BaseService {
// Only take the in-place selector fast-path when fully Connected: during Connecting
// data.proxy is set but proxy.init() may not have built config/box yet, and during
// Stopping the box is being torn down — touching them would throw or act on a dead
// instance. In those states fall through to the state machine below.
if (s == State.Connected && canReloadSelector()) {
val ent = SagerDatabase.proxyDao.getById(DataStore.selectedProxy)
val tag = data.proxy!!.config.profileTagMap[ent?.id] ?: ""
if (tag.isNotBlank() && ent != null) {
// select from GUI
data.proxy!!.box.selectOutbound(tag)
// or select from webui
// => selector_OnProxySelected
}
// instance. In those states fall through to the state machine below. selectorTag was
// resolved off the main thread by the caller (null => no fast-path).
if (s == State.Connected && selectorTag != null && selectorTag.isNotBlank()) {
// select from GUI
data.proxy!!.box.selectOutbound(selectorTag)
// or select from webui
// => selector_OnProxySelected
return
}
when {
Expand All @@ -277,6 +278,18 @@ class BaseService {
}
}

// Off-main-thread resolver for reloadInner's in-place selector fast-path. Returns the
// outbound tag to select, or null if the selector fast-path does not apply. Does all the
// DAO reads (proxy/group) so reloadInner touches no DB on the UI thread.
fun resolveSelectorReloadTag(): String? {
if (data.state != State.Connected) return null
if (!canReloadSelector()) return null
val ent = SagerDatabase.proxyDao.getById(DataStore.selectedProxy) ?: return null
val proxy = data.proxy ?: return null
val tag = proxy.config.profileTagMap[ent.id] ?: ""
return tag.ifBlank { null }
}

fun canReloadSelector(): Boolean {
val running = data.proxy?.lastSelectorGroupId ?: -1L
if (running < 0L) return false
Expand Down Expand Up @@ -535,11 +548,16 @@ class BaseService {

return runOnMainDispatcher {
try {
data.notification = createNotification(ServiceNotification.genTitle(profile))
// Reuse the title computed during ProxyInstance construction (off the main
// thread); calling genTitle() here would do a groupDao read on the main thread.
data.notification = createNotification(proxy.displayProfileName)

Executable.killAll() // clean up old processes
preInit()
proxy.init()
// buildConfig() (via proxy.init()) does synchronous group/profile DAO reads;
// run it off the main thread so it works with the main-thread-DB allowance
// removed (Plan 027). init() is suspend and does not touch the UI.
onDefaultDispatcher { proxy.init() }
DataStore.currentProfile = profile.id

proxy.processes = GuardedProcessPool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.nekohasekai.sagernet.bg.ServiceNotification
import io.nekohasekai.sagernet.database.ProxyEntity
import io.nekohasekai.sagernet.ktx.Logs
import io.nekohasekai.sagernet.utils.Commandline
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.runBlocking
import moe.matsuri.nb4a.utils.JavaUtil
Expand Down Expand Up @@ -70,7 +71,10 @@ class ProxyInstance(profile: ProxyEntity, var service: BaseService.Interface? =

override fun close() {
super.close()
runBlocking {
// Teardown is called on the main thread; the final traffic flush in looper.stop() does
// synchronous DAO writes, so run the blocking body on a background dispatcher to keep it
// off the UI thread (Plan 027 — main-thread-DB allowance removed).
runBlocking(Dispatchers.Default) {
looper?.stop()
looper = null
}
Expand Down
Loading