change crash reporter
fix crash on log polling fix crash on udp write back fix goroutine leak fix default rule inject
This commit is contained in:
12 files changed
+162
-174
No files matched your search
@@ -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()
|
||||
|
||||
@@ -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<ErrorAttachmentLog> {
|
||||
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)
|
||||
|
||||
@@ -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<Unit>().apply {
|
||||
service.startHealthCheck(group, object : IStreamCallback.Stub() {
|
||||
override fun complete() {
|
||||
this@apply.complete(Unit)
|
||||
}
|
||||
CompletableDeferred<Unit>().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<ProxyGroup> = 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())
|
||||
}
|
||||
}
|
||||
@@ -8,38 +8,26 @@ import kotlinx.coroutines.withContext
|
||||
|
||||
class ProfileClient(private val service: IProfileService) {
|
||||
suspend fun queryProfiles(): Array<ClashProfileEntity> = 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)
|
||||
}
|
||||
}
|
||||
@@ -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 <T>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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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<String, (LogEvent) -> 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<LogEvent> {
|
||||
return object : EventStream<LogEvent>() {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user