diff --git a/app/src/main/java/com/github/kr328/clash/LogcatService.kt b/app/src/main/java/com/github/kr328/clash/LogcatService.kt index 439d19d..5a36c52 100644 --- a/app/src/main/java/com/github/kr328/clash/LogcatService.kt +++ b/app/src/main/java/com/github/kr328/clash/LogcatService.kt @@ -31,9 +31,11 @@ import com.microsoft.appcenter.crashes.ingestion.models.ErrorAttachmentLog import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.selects.select +import java.io.DataInputStream import java.io.FileWriter import java.io.IOException import java.util.* +import kotlin.concurrent.thread import kotlin.math.max class LogcatService : Service(), CoroutineScope by MainScope(), IInterface { @@ -41,6 +43,7 @@ class LogcatService : Service(), CoroutineScope by MainScope(), IInterface { private const val NOTIFICATION_CHANNEL_ID = "clash_logcat_channel" private const val NOTIFICATION_ID = 256 private const val MAX_CACHE_COUNT = 200 + private const val LOG_LISTENER_KEY = "logcat_service" private const val LOG_CONTENT_FORMAT = "%d %s %s" @@ -57,39 +60,23 @@ class LogcatService : Service(), CoroutineScope by MainScope(), IInterface { private val entity = LogFile.generate() private val connection = object : ServiceConnection { + private var manager: IClashManager? = null + override fun onServiceDisconnected(name: ComponentName?) { - val dump = LogcatDumper.dump() - - logChannel.offer(LogEvent(LogEvent.Level.ERROR, "Clash Service Crashed")) - - dump.forEach { - logChannel.offer(LogEvent(LogEvent.Level.ERROR, it)) - } - - val textLog = ErrorAttachmentLog - .attachmentWithText(dump.joinToString(separator = "\n"), "logcat.txt") - - Crashes.trackError(RemoteException("Clash Service Crashed"), - null, listOf(textLog)) + manager?.unregisterLogListener(LOG_LISTENER_KEY) } override fun onServiceConnected(name: ComponentName?, service: IBinder?) { - val manager = IClashManager.Stub.asInterface(service) ?: return stopSelf() - - manager.openLogEvent(object : IStreamCallback.Stub() { - override fun complete() { - stopSelf() - } - - override fun completeExceptionally(reason: String?) { - stopSelf() - } + manager = IClashManager.Stub.asInterface(service) ?: return stopSelf() + manager?.registerLogListener(LOG_LISTENER_KEY, object: IStreamCallback.Stub() { + override fun complete() {} + override fun completeExceptionally(reason: String?) {} override fun send(data: ParcelableContainer?) { - val logEvent = (data?.data as LogEvent?) ?: return + data ?: return + data.data ?: return - if (!logChannel.offer(logEvent)) - Log.w("Drop log $logEvent") + logChannel.offer(data.data as LogEvent) } }) } @@ -115,6 +102,8 @@ class LogcatService : Service(), CoroutineScope by MainScope(), IInterface { cancel() + connection.onServiceDisconnected(null) + stopForeground(true) super.onDestroy() diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index ad403b2..094c0e6 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -2,7 +2,9 @@ package com.github.kr328.clash import android.app.Application import android.content.Context +import android.os.DeadObjectException import com.github.kr328.clash.core.Global +import com.github.kr328.clash.dump.LogcatDumper import com.github.kr328.clash.remote.Broadcasts import com.github.kr328.clash.remote.Remote import com.microsoft.appcenter.AppCenter @@ -32,6 +34,21 @@ class MainApplication : Application() { BuildConfig.APP_CENTER_KEY, Analytics::class.java, Crashes::class.java ) + + Crashes.setListener(object: AbstractCrashesListener() { + override fun getErrorAttachments(report: ErrorReport?): MutableIterable { + report ?: return mutableListOf() + + if ( !report.stackTrace.contains("DeadObjectException") ) + return mutableListOf() + + val logcat = LogcatDumper.dump().joinToString(separator = "\n") + + return mutableListOf( + ErrorAttachmentLog.attachmentWithText(logcat, "logcat.txt") + ) + } + }) } Remote.init(this) diff --git a/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt b/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt index 7974f03..eabb1a0 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt @@ -12,50 +12,38 @@ import kotlinx.coroutines.withContext class ClashClient(val service: IClashManager) { suspend fun setSelectProxy(name: String, proxy: String): Boolean = withContext(Dispatchers.IO) { - Utils.callRemote { - service.setSelectProxy(name, proxy) - } + service.setSelectProxy(name, proxy) } suspend fun startHealthCheck(group: String) = withContext(Dispatchers.IO) { - Utils.callRemote { - CompletableDeferred().apply { - service.startHealthCheck(group, object : IStreamCallback.Stub() { - override fun complete() { - this@apply.complete(Unit) - } + CompletableDeferred().apply { + service.startHealthCheck(group, object : IStreamCallback.Stub() { + override fun complete() { + this@apply.complete(Unit) + } - override fun completeExceptionally(reason: String?) { - this@apply.completeExceptionally(RemoteException(reason)) - } + override fun completeExceptionally(reason: String?) { + this@apply.completeExceptionally(RemoteException(reason)) + } - override fun send(data: ParcelableContainer?) {} - }) - } + override fun send(data: ParcelableContainer?) {} + }) } }.await() suspend fun queryAllProxyGroups(): List = withContext(Dispatchers.IO) { - Utils.callRemote { - service.queryAllProxies().list - } + service.queryAllProxies().list } suspend fun queryGeneral(): General = withContext(Dispatchers.IO) { - Utils.callRemote { - service.queryGeneral() - } + service.queryGeneral() } suspend fun queryBandwidth(): Long = withContext(Dispatchers.IO) { - Utils.callRemote { - service.queryBandwidth() - } - } + service.queryBandwidth() + } suspend fun setProxyMode(mode: General.Mode) = withContext(Dispatchers.IO) { - Utils.callRemote { - service.setProxyMode(mode.toString()) - } + service.setProxyMode(mode.toString()) } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/remote/ProfileClient.kt b/app/src/main/java/com/github/kr328/clash/remote/ProfileClient.kt index b3d3a08..bf70528 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/ProfileClient.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/ProfileClient.kt @@ -8,38 +8,26 @@ import kotlinx.coroutines.withContext class ProfileClient(private val service: IProfileService) { suspend fun queryProfiles(): Array = withContext(Dispatchers.IO) { - Utils.callRemote { - service.queryProfiles() - } + service.queryProfiles() } suspend fun queryActiveProfile(): ClashProfileEntity? = withContext(Dispatchers.IO) { - Utils.callRemote { - service.queryActiveProfile() - } + service.queryActiveProfile() } suspend fun enqueueRequest(request: ProfileRequest) = withContext(Dispatchers.IO) { - Utils.callRemote { - service.enqueueRequest(request) - } + service.enqueueRequest(request) } suspend fun setActiveProfile(id: Long) = withContext(Dispatchers.IO) { - Utils.callRemote { - service.setActiveProfile(id) - } + service.setActiveProfile(id) } suspend fun requestProfileEditUri(id: Long): String? = withContext(Dispatchers.IO) { - Utils.callRemote { - service.requestProfileEditUri(id) - } + service.requestProfileEditUri(id) } suspend fun commitProfileEditUri(uri: String) = withContext(Dispatchers.IO) { - Utils.callRemote { - service.commitProfileEditUri(uri) - } + service.commitProfileEditUri(uri) } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/remote/Utils.kt b/app/src/main/java/com/github/kr328/clash/remote/Utils.kt deleted file mode 100644 index bf0bf98..0000000 --- a/app/src/main/java/com/github/kr328/clash/remote/Utils.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.github.kr328.clash.remote - -import android.os.DeadObjectException -import com.github.kr328.clash.dump.LogcatDumper -import com.microsoft.appcenter.crashes.Crashes -import com.microsoft.appcenter.crashes.ingestion.models.ErrorAttachmentLog - -internal object Utils { - fun callRemote(block: () -> T): T { - return try { - block() - } - catch (e: DeadObjectException) { - val log = LogcatDumper.dump().joinToString(separator = "\n") - - val attachmentLog = ErrorAttachmentLog - .attachmentWithText(log, "logcat.txt") - - Crashes.trackError(e, null, listOf(attachmentLog)) - - throw e - } - } -} \ No newline at end of file diff --git a/core/src/main/golang/bridge/init.go b/core/src/main/golang/bridge/init.go index e833867..d35ea69 100644 --- a/core/src/main/golang/bridge/init.go +++ b/core/src/main/golang/bridge/init.go @@ -1,12 +1,24 @@ package bridge import ( + "sync" + "github.com/Dreamacro/clash/component/mmdb" C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/tunnel" "github.com/kr328/cfa/profile" ) +var ( + logCallback LogCallback + logSubscribe sync.Once +) + +type LogCallback interface { + OnLogEvent(level, payload string) +} + func LoadMMDB(data []byte) { dataClone := make([]byte, len(data)) copy(dataClone, data) @@ -26,3 +38,27 @@ func Reset() { func SetApplicationVersion(version string) { profile.ApplicationVersion = version } + +func SetLogCallback(callback LogCallback) { + logSubscribe.Do(func() { + go func() { + sub := log.Subscribe() + defer log.UnSubscribe(sub) + + for { + elm := <-sub + l := elm.(*log.Event) + + if l.LogLevel < log.Level() { + continue + } + + if cb := logCallback; cb != nil { + cb.OnLogEvent(l.LogLevel.String(), l.Payload) + } + } + }() + }) + + logCallback = callback +} diff --git a/core/src/main/golang/bridge/statistics.go b/core/src/main/golang/bridge/statistics.go index dc30757..265092a 100644 --- a/core/src/main/golang/bridge/statistics.go +++ b/core/src/main/golang/bridge/statistics.go @@ -1,16 +1,21 @@ package bridge import ( - "github.com/Dreamacro/clash/log" + "sync" + "github.com/Dreamacro/clash/tunnel" ) type EventPoll struct { + stop sync.Once + onStop func() } func (e *EventPoll) Stop() { - e.onStop() + e.stop.Do(func() { + e.onStop() + }) } type Traffic struct { @@ -40,35 +45,3 @@ func QueryTraffic() *Traffic { Download: down, } } - -func PollLogs(logs Logs) *EventPoll { - stopChannel := make(chan int, 1) - sub := log.Subscribe() - - go func() { - defer log.UnSubscribe(sub) - defer close(stopChannel) - defer log.Infoln("Logs Poll Stopped") - - for { - select { - case <-stopChannel: - return - case elm := <-sub: - l := elm.(*log.Event) - - if l.LogLevel < log.Level() { - break - } - - logs.OnEvent(l.Type(), l.Payload) - } - } - }() - - return &EventPoll{ - onStop: func() { - stopChannel <- 0 - }, - } -} diff --git a/core/src/main/golang/profile/download.go b/core/src/main/golang/profile/download.go index a4509e2..994f78c 100644 --- a/core/src/main/golang/profile/download.go +++ b/core/src/main/golang/profile/download.go @@ -83,10 +83,14 @@ func ReadAndCheck(fd int, output, baseDir string) error { } func SaveAndCheck(data []byte, output, baseDir string) error { - _, err := parseConfig(data, baseDir) + cfg, err := parseConfig(data, baseDir) if err != nil { return err } + for _, v := range cfg.Providers { + v.Destroy() + } + return ioutil.WriteFile(output, data, defaultFileMode) } diff --git a/core/src/main/golang/profile/load.go b/core/src/main/golang/profile/load.go index cd98278..2dfa0e9 100644 --- a/core/src/main/golang/profile/load.go +++ b/core/src/main/golang/profile/load.go @@ -107,7 +107,7 @@ func parseConfig(data []byte, baseDir string) (*config.Config, error) { raw.Experimental.Interface = "" raw.ExternalUI = "" raw.ExternalController = "" - raw.Rule = append([]string{fmt.Sprintf("IP-CIDR,%s,REJECT", tunAddress)}, raw.Rule...) + raw.Rule = append([]string{fmt.Sprintf("IP-CIDR,%s,REJECT,no-resolve", tunAddress)}, raw.Rule...) patchRawConfig(raw) diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index a92a3d7..d68b6ca 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -1,6 +1,7 @@ package com.github.kr328.clash.core import android.content.Context +import android.os.Messenger import bridge.Bridge import bridge.TunCallback import com.github.kr328.clash.core.event.EventStream @@ -18,6 +19,8 @@ import java.io.File import java.io.InputStream object Clash { + private val logReceivers = mutableMapOf Unit>() + private var initialized = false @Synchronized @@ -33,8 +36,6 @@ object Clash { Bridge.setHome(context.cacheDir.absolutePath) Bridge.setApplicationVersion(BuildConfig.VERSION_NAME) Bridge.reset() - - Log.d("MMDB loaded ${bytes.size}") } fun start() { @@ -52,10 +53,11 @@ object Clash { onNewSocket: (Int) -> Boolean, onTunStop: () -> Unit ) { - Bridge.startTunDevice(fd.toLong(), mtu.toLong(), dns, object: TunCallback { + Bridge.startTunDevice(fd.toLong(), mtu.toLong(), dns, object : TunCallback { override fun onCreateSocket(fd: Long) { onNewSocket(fd.toInt()) } + override fun onStop() { onTunStop() } @@ -143,14 +145,27 @@ object Clash { return Traffic(data.upload, data.download) } - fun openLogEvent(): EventStream { - return object : EventStream() { - val log = Bridge.pollLogs { level, payload -> - send(LogEvent(LogEvent.Level.fromString(level), payload)) - } + fun registerLogReceiver(key: String, receiver: (LogEvent) -> Unit) { + synchronized(logReceivers) { + logReceivers[key] = receiver - override fun onClose() { - log.stop() + Bridge.setLogCallback(this::onLogEvent) + } + } + + fun unregisterLogReceiver(key: String) { + synchronized(logReceivers) { + logReceivers.remove(key) + + if ( logReceivers.isEmpty() ) + Bridge.setLogCallback(null) + } + } + + private fun onLogEvent(level: String, payload: String) { + synchronized(logReceivers) { + logReceivers.forEach { + it.value(LogEvent(LogEvent.Level.fromString(level), payload)) } } } diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl index 700bd59..a1086b0 100644 --- a/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl @@ -16,9 +16,6 @@ interface IClashManager { long queryBandwidth(); // Events - void openLogEvent(IStreamCallback callback); - - // Settings - boolean putSetting(String key, String value); - String getSetting(String key); + void registerLogListener(String key, IStreamCallback callback); + void unregisterLogListener(String key); } diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt b/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt index deeccb9..8523889 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt @@ -1,16 +1,26 @@ package com.github.kr328.clash.service import android.content.Context +import android.os.ParcelFileDescriptor import androidx.core.content.edit import com.github.kr328.clash.core.Clash +import com.github.kr328.clash.core.event.LogEvent import com.github.kr328.clash.core.model.General import com.github.kr328.clash.core.model.ProxyGroupList +import com.github.kr328.clash.core.utils.Log import com.github.kr328.clash.service.data.ClashDatabase import com.github.kr328.clash.service.data.ClashProfileProxyEntity import com.github.kr328.clash.service.ipc.IStreamCallback import com.github.kr328.clash.service.ipc.ParcelableContainer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonConfiguration +import kotlinx.serialization.toUtf8Bytes +import java.io.DataOutputStream +import java.lang.Exception +import java.nio.channels.FileChannel +import kotlin.concurrent.thread class ClashManager(context: Context, parent: CoroutineScope) : IClashManager.Stub(), CoroutineScope by parent { @@ -42,37 +52,12 @@ class ClashManager(context: Context, parent: CoroutineScope) : return Clash.setSelectedProxy(proxy, selected) } - override fun putSetting(key: String?, value: String?): Boolean { - settings.edit(commit = false) { - putString(key, value) - } - return true - } - override fun queryBandwidth(): Long { val data = Clash.queryBandwidth() return data.download + data.upload } - override fun openLogEvent(callback: IStreamCallback?) { - require(callback != null) - - Clash.openLogEvent().apply { - callback.asBinder()?.linkToDeath({ - close() - }, 0) - - onEvent { - try { - callback.send(ParcelableContainer(it)) - } catch (e: Exception) { - close() - } - } - } - } - override fun startHealthCheck(group: String?, callback: IStreamCallback?) { require(group != null && callback != null) @@ -84,7 +69,27 @@ class ClashManager(context: Context, parent: CoroutineScope) : } } - override fun getSetting(key: String?): String? { - return settings.getString(key, null) + override fun registerLogListener(key: String?, callback: IStreamCallback?) { + requireNotNull(key) + requireNotNull(callback) + + callback.asBinder().linkToDeath({ + Clash.unregisterLogReceiver(key) + }, 0) + + Clash.registerLogReceiver(key) { + try { + callback.send(ParcelableContainer(it)) + } + catch (e: Exception) { + Clash.unregisterLogReceiver(key) + } + } + } + + override fun unregisterLogListener(key: String?) { + requireNotNull(key) + + Clash.unregisterLogReceiver(key) } } \ No newline at end of file