mirror of
https://github.com/openlibrecommunity/olcng.git
synced 2026-07-03 14:05:17 +02:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ed4eaf6bd0 | |||
| 6ed13ed6fd | |||
| 4ddda4902f | |||
| ba53203dbc | |||
| 146f20e13b | |||
| 38554d4e55 | |||
| 23725a3210 | |||
| 50f435f250 | |||
| 80572e566c |
@@ -170,7 +170,9 @@ func (x *CoreController) MeasureDelay(url string) (int64, error) {
|
||||
|
||||
// MeasureOutboundDelay measures the outbound delay for a given configuration and URL
|
||||
func MeasureOutboundDelay(ConfigureFileContent string, url string) (int64, error) {
|
||||
return measureOutboundDelayInternal(ConfigureFileContent, url)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
|
||||
defer cancel()
|
||||
return measureOutboundDelayInternal(ctx, ConfigureFileContent, url)
|
||||
}
|
||||
|
||||
// MeasureOutboundDelayBatch measures the outbound delay for multiple configurations in parallel
|
||||
@@ -190,28 +192,46 @@ func MeasureOutboundDelayBatch(itemsJson string, url string, callback PingCallba
|
||||
return
|
||||
}
|
||||
|
||||
// Semaphore to limit concurrency (max 128 concurrent tests)
|
||||
sem := make(chan struct{}, 128)
|
||||
var wg sync.WaitGroup
|
||||
// Use a worker pool to process items
|
||||
// Performance tuning: 24 concurrency
|
||||
concurrency := 24
|
||||
if len(items) < concurrency {
|
||||
concurrency = len(items)
|
||||
}
|
||||
|
||||
itemChan := make(chan PingItem, len(items))
|
||||
for _, item := range items {
|
||||
wg.Add(1)
|
||||
go func(it PingItem) {
|
||||
defer wg.Done()
|
||||
sem <- struct{}{}
|
||||
defer func() { <-sem }()
|
||||
itemChan <- item
|
||||
}
|
||||
close(itemChan)
|
||||
|
||||
delay, _ := measureOutboundDelayInternal(it.Config, url)
|
||||
if callback != nil {
|
||||
callback.OnResult(it.Guid, delay)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(concurrency)
|
||||
|
||||
for i := 0; i < concurrency; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for it := range itemChan {
|
||||
// Set a reasonable timeout for each individual test
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
|
||||
delay, _ := measureOutboundDelayInternal(ctx, it.Config, url)
|
||||
cancel() // cancel context as soon as we have a result
|
||||
|
||||
if callback != nil {
|
||||
callback.OnResult(it.Guid, delay)
|
||||
}
|
||||
|
||||
// Small sleep to prevent system congestion
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}(item)
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func measureOutboundDelayInternal(ConfigureFileContent string, url string) (int64, error) {
|
||||
func measureOutboundDelayInternal(ctx context.Context, ConfigureFileContent string, url string) (int64, error) {
|
||||
config, err := coreserial.LoadJSONConfig(strings.NewReader(ConfigureFileContent))
|
||||
if err != nil {
|
||||
return -1, fmt.Errorf("config load error: %w", err)
|
||||
@@ -237,8 +257,28 @@ func measureOutboundDelayInternal(ConfigureFileContent string, url string) (int6
|
||||
if err := inst.Start(); err != nil {
|
||||
return -1, fmt.Errorf("startup failed: %w", err)
|
||||
}
|
||||
defer inst.Close()
|
||||
return measureInstDelay(context.Background(), inst, url)
|
||||
|
||||
// Measure delay
|
||||
delay, err := measureInstDelay(ctx, inst, url)
|
||||
|
||||
// Close instance with a short timeout to prevent hanging the worker
|
||||
closeCtx, closeCancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer closeCancel()
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
inst.Close()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// Closed successfully
|
||||
case <-closeCtx.Done():
|
||||
// Close timed out, move on
|
||||
}
|
||||
|
||||
return delay, err
|
||||
}
|
||||
|
||||
// CheckVersionX returns the library and Xray versions
|
||||
@@ -295,7 +335,7 @@ func measureInstDelay(ctx context.Context, inst *core.Instance, url string) (int
|
||||
|
||||
tr := &http.Transport{
|
||||
TLSHandshakeTimeout: 6 * time.Second,
|
||||
DisableKeepAlives: false,
|
||||
DisableKeepAlives: true,
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
dest, err := corenet.ParseDestination(fmt.Sprintf("%s:%s", network, addr))
|
||||
if err != nil {
|
||||
@@ -307,7 +347,7 @@ func measureInstDelay(ctx context.Context, inst *core.Instance, url string) (int
|
||||
|
||||
client := &http.Client{
|
||||
Transport: tr,
|
||||
Timeout: 5 * time.Second,
|
||||
Timeout: 6 * time.Second,
|
||||
}
|
||||
|
||||
if url == "" {
|
||||
@@ -323,7 +363,7 @@ func measureInstDelay(ctx context.Context, inst *core.Instance, url string) (int
|
||||
success := false
|
||||
var lastErr error
|
||||
|
||||
// Use 2 attempts as requested by user
|
||||
// Use 2 attempts
|
||||
const attempts = 2
|
||||
for i := 0; i < attempts; i++ {
|
||||
select {
|
||||
@@ -344,8 +384,8 @@ func measureInstDelay(ctx context.Context, inst *core.Instance, url string) (int
|
||||
continue
|
||||
}
|
||||
|
||||
// Read body and close resp immediately
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
// Limit reading to 64KB to prevent OOM
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
|
||||
resp.Body.Close()
|
||||
|
||||
if err != nil {
|
||||
@@ -358,11 +398,14 @@ func measureInstDelay(ctx context.Context, inst *core.Instance, url string) (int
|
||||
continue
|
||||
}
|
||||
|
||||
// Strict IP check: the body must contain a valid IP address
|
||||
ipStr := strings.TrimSpace(string(body))
|
||||
if net.ParseIP(ipStr) == nil {
|
||||
lastErr = fmt.Errorf("response body is not a valid IP: %s", ipStr)
|
||||
continue
|
||||
// Relaxed IP check: only if the URL seems like an IP check service
|
||||
isIPCheckUrl := strings.Contains(strings.ToLower(url), "ip")
|
||||
if isIPCheckUrl {
|
||||
ipStr := strings.TrimSpace(string(body))
|
||||
if net.ParseIP(ipStr) == nil {
|
||||
lastErr = fmt.Errorf("response body is not a valid IP: %s", ipStr)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
duration := time.Since(start).Milliseconds()
|
||||
|
||||
@@ -42,8 +42,18 @@ open class FmtBase {
|
||||
* @return a map of query parameters
|
||||
*/
|
||||
fun getQueryParam(uri: URI): Map<String, String> {
|
||||
return uri.rawQuery.split("&")
|
||||
.associate { it.split("=").let { (k, v) -> k to Utils.decodeURIComponent(v) } }
|
||||
return uri.rawQuery.orEmpty().split("&")
|
||||
.mapNotNull {
|
||||
val parts = it.split("=", limit = 2)
|
||||
if (parts.size == 2) {
|
||||
parts[0] to Utils.decodeURIComponent(parts[1])
|
||||
} else if (parts.isNotEmpty() && parts[0].isNotEmpty()) {
|
||||
parts[0] to ""
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
.toMap()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,12 +40,18 @@ class RealPingWorkerService(
|
||||
// Prepare configurations for batch test and shuffle for better async feel
|
||||
val items =
|
||||
guids.shuffled().mapNotNull { guid ->
|
||||
val configResult =
|
||||
V2rayConfigManager.getV2rayConfig4Speedtest(context, guid)
|
||||
if (configResult.status) {
|
||||
PingItem(guid, configResult.content)
|
||||
} else {
|
||||
// Notify failure immediately for invalid configs
|
||||
try {
|
||||
val configResult =
|
||||
V2rayConfigManager.getV2rayConfig4Speedtest(context, guid)
|
||||
if (configResult.status) {
|
||||
PingItem(guid, configResult.content)
|
||||
} else {
|
||||
// Notify failure immediately for invalid configs
|
||||
reportResult(guid, -1L)
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e(AppConfig.TAG, "Failed to prepare config for $guid", e)
|
||||
reportResult(guid, -1L)
|
||||
null
|
||||
}
|
||||
@@ -67,9 +73,13 @@ class RealPingWorkerService(
|
||||
)
|
||||
}
|
||||
|
||||
onFinish("0")
|
||||
if (job.isActive) {
|
||||
onFinish("0")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
onFinish("-1")
|
||||
if (job.isActive) {
|
||||
onFinish("-1")
|
||||
}
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
@@ -77,15 +87,22 @@ class RealPingWorkerService(
|
||||
}
|
||||
|
||||
private fun reportResult(guid: String, delay: Long) {
|
||||
val finished = finishedCount.incrementAndGet()
|
||||
val total = guids.size
|
||||
if (!job.isActive) return
|
||||
|
||||
// Notify UI about the individual result
|
||||
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_SUCCESS, Pair(guid, delay))
|
||||
// Launch in scope to unblock Go worker immediately
|
||||
scope.launch {
|
||||
val finished = finishedCount.incrementAndGet()
|
||||
val total = guids.size
|
||||
|
||||
// Notify UI about progress
|
||||
val left = total - finished
|
||||
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_NOTIFY, "$left / $total")
|
||||
// Notify UI about the individual result
|
||||
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_SUCCESS, Pair(guid, delay))
|
||||
|
||||
// Throttle progress updates: every 10 items or the very last one
|
||||
if (finished % 10 == 0 || finished == total) {
|
||||
val left = total - finished
|
||||
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_NOTIFY, "$left / $total")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
|
||||
@@ -15,6 +15,8 @@ import xyz.zarazaex.olc.databinding.ItemRecyclerFooterBinding
|
||||
import xyz.zarazaex.olc.databinding.ItemRecyclerMainBinding
|
||||
import xyz.zarazaex.olc.dto.ProfileItem
|
||||
import xyz.zarazaex.olc.dto.ServersCache
|
||||
import xyz.zarazaex.olc.extension.toastError
|
||||
import xyz.zarazaex.olc.extension.toastSuccess
|
||||
import xyz.zarazaex.olc.handler.AngConfigManager
|
||||
import xyz.zarazaex.olc.handler.MmkvManager
|
||||
import xyz.zarazaex.olc.helper.ItemTouchHelperAdapter
|
||||
@@ -200,6 +202,14 @@ class MainRecyclerAdapter(
|
||||
mainViewModel.reloadServerList()
|
||||
}
|
||||
|
||||
holder.itemMainBinding.ivCopy.setOnClickListener {
|
||||
if (AngConfigManager.share2Clipboard(context, guid) == 0) {
|
||||
context.toastSuccess(R.string.toast_success)
|
||||
} else {
|
||||
context.toastError(R.string.toast_failure)
|
||||
}
|
||||
}
|
||||
|
||||
holder.itemMainBinding.infoContainer.setOnClickListener {
|
||||
adapterListener?.onSelectServer(guid)
|
||||
}
|
||||
|
||||
@@ -110,6 +110,18 @@
|
||||
android:padding="@dimen/padding_spacing_dp8"
|
||||
android:src="@drawable/ic_star_empty" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_copy"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="@dimen/padding_spacing_dp8"
|
||||
android:src="@drawable/ic_copy"
|
||||
app:tint="?attr/colorAccent" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
|
||||
Reference in New Issue
Block a user