add url test
This commit is contained in:
16 files changed
+270
-66
No files matched your search
@@ -1,6 +1,7 @@
|
||||
package com.github.kr328.clash
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import com.github.kr328.clash.core.utils.Log
|
||||
import com.google.firebase.FirebaseApp
|
||||
|
||||
@@ -9,6 +10,14 @@ class MainApplication : Application() {
|
||||
const val KEY_PROXY_MODE = "key_proxy_mode"
|
||||
const val PROXY_MODE_VPN = "vpn"
|
||||
const val PROXY_MODE_PROXY_ONLY = "proxy_only"
|
||||
|
||||
lateinit var instance: MainApplication
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
|
||||
instance = this
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
|
||||
@@ -2,7 +2,9 @@ package com.github.kr328.clash
|
||||
|
||||
import android.os.Bundle
|
||||
import com.github.kr328.clash.adapter.ProxyAdapter
|
||||
import com.github.kr328.clash.callback.IUrlTestCallback
|
||||
import com.github.kr328.clash.core.model.ProxyPacket
|
||||
import com.github.kr328.clash.core.utils.Log
|
||||
import com.github.kr328.clash.model.ListProxy
|
||||
import kotlinx.android.synthetic.main.activity_proxies.*
|
||||
|
||||
@@ -14,11 +16,7 @@ class ProxyActivity : BaseActivity() {
|
||||
setSupportActionBar(activity_proxies_toolbar)
|
||||
|
||||
activity_proxies_list.also {
|
||||
it.adapter = ProxyAdapter(this) { p, s ->
|
||||
runClash { clash ->
|
||||
clash.setSelectProxy(p, s)
|
||||
}
|
||||
}
|
||||
it.adapter = ProxyAdapter(this, this::setProxySelected, this::urlTest)
|
||||
it.layoutManager = (it.adapter!! as ProxyAdapter).getLayoutManager()
|
||||
}
|
||||
|
||||
@@ -29,6 +27,51 @@ class ProxyActivity : BaseActivity() {
|
||||
refreshList()
|
||||
}
|
||||
|
||||
private fun setProxySelected(name: String, selected: String) {
|
||||
runClash {
|
||||
it.setSelectProxy(name, selected)
|
||||
}
|
||||
}
|
||||
|
||||
private fun urlTest(position: Int, size: Int) {
|
||||
val adapter = (activity_proxies_list.adapter as ProxyAdapter)
|
||||
|
||||
runClash {
|
||||
val proxies = adapter.elements.subList(position, position+size)
|
||||
.filterIsInstance<ListProxy.ListProxyItem>()
|
||||
.mapIndexed { index, proxy -> proxy.name to IndexedValue(index, proxy)}
|
||||
.toMap()
|
||||
|
||||
for ((_, p) in proxies) {
|
||||
Log.i("$p -> ${p.index}")
|
||||
|
||||
p.value.delay = -1
|
||||
}
|
||||
|
||||
it.startUrlTest(proxies.keys.toTypedArray(), object: IUrlTestCallback.Stub() {
|
||||
override fun onResult(proxy: String?, delay: Long) {
|
||||
if ( proxy == null ) {
|
||||
(adapter.elements[position] as ListProxy.ListProxyHeader).urlTest = false
|
||||
|
||||
runOnUiThread {
|
||||
adapter.notifyItemRangeChanged(position, size)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
val p = proxies[proxy] ?: return
|
||||
|
||||
p.value.delay = delay
|
||||
|
||||
runOnUiThread {
|
||||
adapter.notifyItemChanged(position + p.index + 1)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshList() {
|
||||
if (!activity_proxies_swipe.isRefreshing)
|
||||
activity_proxies_swipe.isRefreshing = true
|
||||
|
||||
@@ -13,7 +13,9 @@ import com.github.kr328.clash.core.model.ProxyPacket
|
||||
import com.github.kr328.clash.model.ListProxy
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
|
||||
class ProxyAdapter(private val context: Context, val listener: (String, String) -> Unit) :
|
||||
class ProxyAdapter(private val context: Context,
|
||||
private val onSelect: (String, String) -> Unit,
|
||||
private val onUrlTest: (Int, Int) -> Unit) :
|
||||
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
var elements: List<ListProxy> = emptyList()
|
||||
var clickable: Boolean = false
|
||||
@@ -108,7 +110,7 @@ class ProxyAdapter(private val context: Context, val listener: (String, String)
|
||||
notifyItemChanged(old)
|
||||
notifyItemChanged(position)
|
||||
|
||||
listener(element.header.name, element.name)
|
||||
onSelect(element.header.name, element.name)
|
||||
}
|
||||
} else {
|
||||
holder.card.setOnClickListener(null)
|
||||
@@ -119,20 +121,45 @@ class ProxyAdapter(private val context: Context, val listener: (String, String)
|
||||
holder.name.text = current.name
|
||||
holder.type.text = current.type
|
||||
holder.delay.text =
|
||||
if (current.delay > 0) {
|
||||
current.delay.toString()
|
||||
} else {
|
||||
if (current.header.type != ProxyPacket.Type.SELECT)
|
||||
"N/A"
|
||||
else
|
||||
""
|
||||
when {
|
||||
current.header.urlTest && current.delay < 0 -> "..."
|
||||
current.delay < 0 -> "N/A"
|
||||
current.delay > 0 -> {
|
||||
current.delay.toString()
|
||||
}
|
||||
else -> {
|
||||
if (current.header.type != ProxyPacket.Type.SELECT)
|
||||
"N/A"
|
||||
else
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun bindHeaderView(holder: HeaderHolder, current: ListProxy.ListProxyHeader) {
|
||||
holder.name.text = current.name
|
||||
holder.test.visibility =
|
||||
if (current.type == ProxyPacket.Type.SELECT) View.VISIBLE else View.GONE
|
||||
holder.test.setOnClickListener {
|
||||
if ( current.urlTest )
|
||||
return@setOnClickListener
|
||||
|
||||
val indexed = elements.withIndex()
|
||||
.filter { it.value is ListProxy.ListProxyHeader }
|
||||
.filterIsInstance<IndexedValue<ListProxy.ListProxyHeader>>()
|
||||
|
||||
val headerIndex = indexed.indexOfFirst {
|
||||
it.value === current
|
||||
}.takeIf { it >= 0 } ?: return@setOnClickListener
|
||||
val header = indexed[headerIndex]
|
||||
|
||||
indexed[headerIndex].value.urlTest = true
|
||||
|
||||
val position = header.index
|
||||
val size = (indexed.getOrNull(headerIndex + 1)?.index ?: elements.size) - header.index
|
||||
|
||||
notifyItemRangeChanged(position, size)
|
||||
onUrlTest(position, size)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,13 @@ package com.github.kr328.clash.model
|
||||
import com.github.kr328.clash.core.model.ProxyPacket
|
||||
|
||||
interface ListProxy {
|
||||
data class ListProxyHeader(val name: String, val type: ProxyPacket.Type, var now: Int) :
|
||||
data class ListProxyHeader(val name: String, val type: ProxyPacket.Type, var now: Int, var urlTest: Boolean = false) :
|
||||
ListProxy
|
||||
|
||||
data class ListProxyItem(
|
||||
val name: String,
|
||||
val type: String,
|
||||
val delay: Long,
|
||||
var delay: Long,
|
||||
val header: ListProxyHeader
|
||||
) : ListProxy
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.github.kr328.clash.utils
|
||||
|
||||
import android.util.TypedValue
|
||||
import com.github.kr328.clash.MainApplication
|
||||
|
||||
val Int.dp: Float
|
||||
get() {
|
||||
return TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
this.toFloat(),
|
||||
MainApplication.instance.resources.displayMetrics
|
||||
)
|
||||
}
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
kotlin_version = '1.3.60'
|
||||
kotlin_version = '1.3.61'
|
||||
room_version = '2.2.2'
|
||||
}
|
||||
repositories {
|
||||
@@ -14,7 +14,7 @@ buildscript {
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.5.2'
|
||||
classpath 'com.android.tools.build:gradle:3.5.3'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||
classpath 'com.google.gms:google-services:4.3.3'
|
||||
|
||||
@@ -2,7 +2,7 @@ apply plugin: 'java'
|
||||
apply plugin: 'kotlin'
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.3.60'
|
||||
ext.kotlin_version = '1.3.61'
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
|
||||
A "github.com/Dreamacro/clash/adapters/outbound"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
"github.com/Dreamacro/clash/tunnel"
|
||||
"github.com/kr328/cfa/profile"
|
||||
)
|
||||
|
||||
@@ -54,31 +52,8 @@ func handleProfileReload(client *net.UnixConn) {
|
||||
return
|
||||
}
|
||||
|
||||
proxies := tunnel.Instance().Proxies()
|
||||
|
||||
for k, v := range payload.Selected {
|
||||
p := proxies[k]
|
||||
|
||||
if p == nil {
|
||||
response.InvalidSelected = append(response.InvalidSelected, k)
|
||||
continue
|
||||
}
|
||||
|
||||
proxy, ok := p.(*A.Proxy)
|
||||
if !ok {
|
||||
response.InvalidSelected = append(response.InvalidSelected, k)
|
||||
continue
|
||||
}
|
||||
|
||||
selector, ok := proxy.ProxyAdapter.(*A.Selector)
|
||||
if !ok {
|
||||
response.InvalidSelected = append(response.InvalidSelected, k)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Infoln("Set selector " + k + " -> " + v)
|
||||
|
||||
selector.Set(v)
|
||||
setProxySelect(k, v)
|
||||
}
|
||||
|
||||
log.Infoln("Profile " + payload.Path + " loaded")
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/log"
|
||||
"github.com/Dreamacro/clash/proxy"
|
||||
@@ -95,8 +98,6 @@ func handleQueryProxies(client *net.UnixConn) {
|
||||
}
|
||||
|
||||
func handleSetProxy(client *net.UnixConn) {
|
||||
proxies := tunnel.Instance().Proxies()
|
||||
|
||||
var Request struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
@@ -119,28 +120,101 @@ func handleSetProxy(client *net.UnixConn) {
|
||||
|
||||
json.Unmarshal(buf, &Request)
|
||||
|
||||
p := proxies[Request.Key]
|
||||
if err := setProxySelect(Request.Key, Request.Value); err != nil {
|
||||
Response.Error = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
func handleUrlTest(client *net.UnixConn) {
|
||||
var request struct {
|
||||
URL string `json:"url"`
|
||||
Timeout int `json:"timeout"`
|
||||
Proxies []string `json:"proxies"`
|
||||
}
|
||||
|
||||
request.Proxies = make([]string, 0)
|
||||
|
||||
buf, err := readCommandPacket(client)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if json.Unmarshal(buf, &request) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Name string `json:"name"`
|
||||
Delay int `json:"delay"`
|
||||
}
|
||||
|
||||
proxies := tunnel.Instance().Proxies()
|
||||
channel := make(chan *Response, len(request.Proxies))
|
||||
|
||||
for _, p := range request.Proxies {
|
||||
go func(p string) {
|
||||
proxy := proxies[p]
|
||||
if proxy == nil {
|
||||
channel <- nil
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(request.Timeout)*time.Millisecond)
|
||||
|
||||
defer cancel()
|
||||
|
||||
delay, err := proxies[p].URLTest(ctx, request.URL)
|
||||
if err != nil {
|
||||
channel <- nil
|
||||
return
|
||||
}
|
||||
|
||||
channel <- &Response{
|
||||
Name: p,
|
||||
Delay: int(delay),
|
||||
}
|
||||
}(p)
|
||||
}
|
||||
|
||||
for range request.Proxies {
|
||||
response := <-channel
|
||||
|
||||
if response != nil {
|
||||
buf, _ := json.Marshal(&response)
|
||||
|
||||
writeCommandPacket(client, buf)
|
||||
}
|
||||
}
|
||||
|
||||
writeCommandPacket(client, nil)
|
||||
|
||||
log.Infoln("URLTest exited")
|
||||
}
|
||||
|
||||
func setProxySelect(name, selected string) error {
|
||||
proxies := tunnel.Instance().Proxies()
|
||||
|
||||
p := proxies[name]
|
||||
|
||||
if p == nil {
|
||||
Response.Error = "Unknown proxy " + Request.Key
|
||||
return
|
||||
return errors.New("Unknown proxy " + name)
|
||||
}
|
||||
|
||||
proxy, ok := p.(*A.Proxy)
|
||||
if !ok {
|
||||
Response.Error = "Invalid proxy " + Request.Key
|
||||
return
|
||||
return errors.New("Invalid proxy " + name)
|
||||
}
|
||||
|
||||
selector, ok := proxy.ProxyAdapter.(*A.Selector)
|
||||
if !ok {
|
||||
Response.Error = "Not selector"
|
||||
return
|
||||
return errors.New("Not selector")
|
||||
}
|
||||
|
||||
log.Infoln("Set selector " + Request.Key + " -> " + Request.Value)
|
||||
log.Infoln("Set selector " + name + " -> " + selected)
|
||||
|
||||
if err := selector.Set(Request.Value); err != nil {
|
||||
Response.Error = err.Error()
|
||||
if err := selector.Set(selected); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -19,6 +19,7 @@ const (
|
||||
commandPullBandwidth uint32 = 8
|
||||
commandSetProxy uint32 = 9
|
||||
commandQueryGeneral uint32 = 10
|
||||
commandUrlTest uint32 = 11
|
||||
)
|
||||
|
||||
var handlers map[uint32]func(*net.UnixConn) = make(map[uint32]func(*net.UnixConn))
|
||||
@@ -35,6 +36,7 @@ func init() {
|
||||
handlers[commandPullBandwidth] = handlePullBandwidthEvent // event.go
|
||||
handlers[commandSetProxy] = handleSetProxy // proxies.go
|
||||
handlers[commandQueryGeneral] = handleQueryGeneral // proxies.go
|
||||
handlers[commandUrlTest] = handleUrlTest // proxies.go
|
||||
}
|
||||
|
||||
// Start local control server
|
||||
|
||||
@@ -6,12 +6,11 @@ import com.github.kr328.clash.core.event.BandwidthEvent
|
||||
import com.github.kr328.clash.core.event.LogEvent
|
||||
import com.github.kr328.clash.core.event.ProcessEvent
|
||||
import com.github.kr328.clash.core.event.SpeedEvent
|
||||
import com.github.kr328.clash.core.model.GeneralPacket
|
||||
import com.github.kr328.clash.core.model.LoadProfilePacket
|
||||
import com.github.kr328.clash.core.model.RawProxyPacket
|
||||
import com.github.kr328.clash.core.model.SetProxyPacket
|
||||
import com.github.kr328.clash.core.model.*
|
||||
import com.github.kr328.clash.core.utils.Log
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
import kotlinx.serialization.stringify
|
||||
import java.io.File
|
||||
import java.io.FileDescriptor
|
||||
import java.io.IOException
|
||||
@@ -33,8 +32,12 @@ class Clash(
|
||||
const val COMMAND_PULL_BANDWIDTH = 8
|
||||
const val COMMAND_SET_PROXY = 9
|
||||
const val COMMAND_QUERY_GENERAL = 10
|
||||
const val COMMAND_URL_TEST = 11
|
||||
|
||||
const val PING_REPLY = 233
|
||||
|
||||
const val DEFAULT_URL_TEST_TIMEOUT = 5000
|
||||
const val DEFAULT_URL_TEST_URL = "https://www.gstatic.com/generate_204"
|
||||
}
|
||||
|
||||
val process = ClashProcess(context, clashDir, controllerPath, listener)
|
||||
@@ -153,6 +156,27 @@ class Clash(
|
||||
}
|
||||
}
|
||||
|
||||
fun startUrlTest(proxies: List<String>, callback: (String, Long) -> Unit) {
|
||||
runControl(COMMAND_URL_TEST) { _, input, output ->
|
||||
output.writeString(Json(JsonConfiguration.Stable)
|
||||
.stringify(UrlTestPacket.Request.serializer(),
|
||||
UrlTestPacket.Request(proxies, DEFAULT_URL_TEST_TIMEOUT, DEFAULT_URL_TEST_URL)))
|
||||
|
||||
while ( true ) {
|
||||
val data = input.readString()
|
||||
if ( data.isEmpty() )
|
||||
return@runControl
|
||||
|
||||
val response = Json(JsonConfiguration.Stable)
|
||||
.parse(UrlTestPacket.Response.serializer(), data)
|
||||
|
||||
callback(response.name, response.delay)
|
||||
}
|
||||
}
|
||||
|
||||
Log.i("Url test exited")
|
||||
}
|
||||
|
||||
fun startTunDevice(fd: FileDescriptor, mtu: Int) {
|
||||
runControl(COMMAND_TUN_START) { socket, _, output ->
|
||||
socket.setFileDescriptorsForSend(arrayOf(fd))
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.github.kr328.clash.core.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
class UrlTestPacket {
|
||||
@Serializable
|
||||
data class Request(val proxies: List<String>, val timeout: Int, val url: String)
|
||||
@Serializable
|
||||
data class Response(val name: String, val delay: Long)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.github.kr328.clash.callback;
|
||||
|
||||
interface IUrlTestCallback {
|
||||
void onResult(String proxy, long delay);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import com.github.kr328.clash.service.IClashEventObserver;
|
||||
import com.github.kr328.clash.service.IClashEventService;
|
||||
import com.github.kr328.clash.service.IClashProfileService;
|
||||
import com.github.kr328.clash.service.IClashSettingService;
|
||||
import com.github.kr328.clash.callback.IUrlTestCallback;
|
||||
import com.github.kr328.clash.core.event.Event;
|
||||
import com.github.kr328.clash.core.model.Packet;
|
||||
|
||||
@@ -26,4 +27,5 @@ interface IClashService {
|
||||
// Query
|
||||
ProxyPacket queryAllProxies();
|
||||
GeneralPacket queryGeneral();
|
||||
void startUrlTest(in String[] proxies, IUrlTestCallback callback);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import java.util.concurrent.Executors
|
||||
|
||||
class ClashEventService(private val master: Master) : IClashEventService.Stub() {
|
||||
interface Master {
|
||||
fun requireEvent(event: Int)
|
||||
fun acquireEvent(event: Int)
|
||||
fun releaseEvent(event: Int)
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ class ClashEventService(private val master: Master) : IClashEventService.Stub()
|
||||
}.toSet()
|
||||
val rel = EVENT_SET - req
|
||||
|
||||
req.forEach(master::requireEvent)
|
||||
req.forEach(master::acquireEvent)
|
||||
rel.forEach(master::releaseEvent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.*
|
||||
import com.github.kr328.clash.callback.IUrlTestCallback
|
||||
import com.github.kr328.clash.core.Clash
|
||||
import com.github.kr328.clash.core.ClashProcess
|
||||
import com.github.kr328.clash.core.event.*
|
||||
@@ -15,6 +16,7 @@ import com.github.kr328.clash.core.utils.Log
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class ClashService : Service(), IClashEventObserver, ClashEventService.Master,
|
||||
ClashProfileService.Master, ClashEventPuller.Master {
|
||||
@@ -66,6 +68,24 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master,
|
||||
}
|
||||
}
|
||||
|
||||
override fun startUrlTest(proxies: Array<out String>?, callback: IUrlTestCallback?) {
|
||||
require(proxies != null && callback != null)
|
||||
|
||||
thread {
|
||||
try {
|
||||
clash.startUrlTest(proxies.toList()) { name, delay ->
|
||||
callback.onResult(name, delay)
|
||||
}
|
||||
}
|
||||
catch (e: Exception) {
|
||||
Log.w("Url test failure", e)
|
||||
}
|
||||
|
||||
callback.onResult(null, -1)
|
||||
// Ignore exceptions
|
||||
}
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
try {
|
||||
clash.process.start()
|
||||
@@ -134,7 +154,7 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master,
|
||||
}
|
||||
}
|
||||
|
||||
override fun requireEvent(event: Int) {
|
||||
override fun acquireEvent(event: Int) {
|
||||
if (clash.process.getProcessStatus() == ProcessEvent.STOPPED)
|
||||
return
|
||||
|
||||
@@ -146,7 +166,6 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master,
|
||||
Event.EVENT_BANDWIDTH ->
|
||||
puller.startBandwidthPull()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun releaseEvent(event: Int) {
|
||||
|
||||
Reference in new issue
Block a user