using System.Collections.Concurrent; using System.Diagnostics; using System.Net; using System.Net.Sockets; namespace ServiceLib.Services { public class SpeedtestService { private static readonly string _tag = "SpeedtestService"; private Config? _config; private Action? _updateFunc; private static readonly ConcurrentBag _lstExitLoop = new(); public SpeedtestService(Config config, Action updateFunc) { _config = config; _updateFunc = updateFunc; } public void RunLoop(ESpeedActionType actionType, List selecteds) { Task.Run(async () => { await RunAsync(actionType, selecteds); await ProfileExHandler.Instance.SaveTo(); UpdateFunc("", ResUI.SpeedtestingCompleted); FileManager.DeleteExpiredFiles(Utils.GetBinConfigPath(), DateTime.Now.AddHours(-1)); }); } public void ExitLoop() { if (_lstExitLoop.Count > 0) { UpdateFunc("", ResUI.SpeedtestingStop); _lstExitLoop.Clear(); } } private async Task RunAsync(ESpeedActionType actionType, List selecteds) { var exitLoopKey = Utils.GetGuid(false); _lstExitLoop.Add(exitLoopKey); var lstSelected = GetClearItem(actionType, selecteds); switch (actionType) { case ESpeedActionType.Tcping: await RunTcpingAsync(lstSelected); break; case ESpeedActionType.Realping: await RunRealPingBatchAsync(lstSelected, exitLoopKey); break; case ESpeedActionType.Speedtest: await RunMixedTestAsync(lstSelected, 1, true, exitLoopKey); break; case ESpeedActionType.Mixedtest: await RunMixedTestAsync(lstSelected, _config.SpeedTestItem.MixedConcurrencyCount, true, exitLoopKey); break; } } private List GetClearItem(ESpeedActionType actionType, List selecteds) { var lstSelected = new List(); foreach (var it in selecteds) { if (it.ConfigType == EConfigType.Custom) { continue; } if (it.Port <= 0) { continue; } lstSelected.Add(new ServerTestItem() { IndexId = it.IndexId, Address = it.Address, Port = it.Port, ConfigType = it.ConfigType, QueueNum = selecteds.IndexOf(it) }); } //clear test result foreach (var it in lstSelected) { switch (actionType) { case ESpeedActionType.Tcping: case ESpeedActionType.Realping: UpdateFunc(it.IndexId, ResUI.Speedtesting, ""); ProfileExHandler.Instance.SetTestDelay(it.IndexId, 0); break; case ESpeedActionType.Speedtest: UpdateFunc(it.IndexId, "", ResUI.SpeedtestingWait); ProfileExHandler.Instance.SetTestSpeed(it.IndexId, 0); break; case ESpeedActionType.Mixedtest: UpdateFunc(it.IndexId, ResUI.Speedtesting, ResUI.SpeedtestingWait); ProfileExHandler.Instance.SetTestDelay(it.IndexId, 0); ProfileExHandler.Instance.SetTestSpeed(it.IndexId, 0); break; } } return lstSelected; } private async Task RunTcpingAsync(List selecteds) { List tasks = []; foreach (var it in selecteds) { if (it.ConfigType == EConfigType.Custom) { continue; } tasks.Add(Task.Run(async () => { try { var responseTime = await GetTcpingTime(it.Address, it.Port); ProfileExHandler.Instance.SetTestDelay(it.IndexId, responseTime); UpdateFunc(it.IndexId, responseTime.ToString()); } catch (Exception ex) { Logging.SaveLog(_tag, ex); } })); } Task.WaitAll([.. tasks]); await Task.CompletedTask; } private async Task RunRealPingBatchAsync(List lstSelected, string exitLoopKey, int pageSize = 0) { if (pageSize <= 0) { pageSize = lstSelected.Count < Global.SpeedTestPageSize ? lstSelected.Count : Global.SpeedTestPageSize; } var lstTest = GetTestBatchItem(lstSelected, pageSize); List lstFailed = new(); foreach (var lst in lstTest) { var ret = await RunRealPingAsync(lst, exitLoopKey); if (ret == false) { lstFailed.AddRange(lst); } await Task.Delay(100); } //Retest the failed part var pageSizeNext = pageSize / 2; if (lstFailed.Count > 0 && pageSizeNext > 0) { if (_lstExitLoop.Any(p => p == exitLoopKey) == false) { UpdateFunc("", ResUI.SpeedtestingSkip); return; } UpdateFunc("", string.Format(ResUI.SpeedtestingTestFailedPart, lstFailed.Count)); if (pageSizeNext > _config.SpeedTestItem.MixedConcurrencyCount) { await RunRealPingBatchAsync(lstFailed, exitLoopKey, pageSizeNext); } else { await RunMixedTestAsync(lstSelected, _config.SpeedTestItem.MixedConcurrencyCount, false, exitLoopKey); } } } private async Task RunRealPingAsync(List selecteds, string exitLoopKey) { var pid = -1; try { pid = await CoreHandler.Instance.LoadCoreConfigSpeedtest(selecteds); if (pid < 0) { return false; } var downloadHandle = new DownloadService(); List tasks = new(); foreach (var it in selecteds) { if (!it.AllowTest) { continue; } if (it.ConfigType == EConfigType.Custom) { continue; } tasks.Add(Task.Run(async () => { await DoRealPing(downloadHandle, it); })); } Task.WaitAll(tasks.ToArray()); } catch (Exception ex) { Logging.SaveLog(_tag, ex); } finally { if (pid > 0) { await ProcUtils.ProcessKill(pid); } } return true; } private async Task RunMixedTestAsync(List selecteds, int concurrencyCount, bool blSpeedTest, string exitLoopKey) { using var concurrencySemaphore = new SemaphoreSlim(concurrencyCount); var downloadHandle = new DownloadService(); List tasks = new(); foreach (var it in selecteds) { if (_lstExitLoop.Any(p => p == exitLoopKey) == false) { UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip); continue; } if (it.ConfigType == EConfigType.Custom) { continue; } await concurrencySemaphore.WaitAsync(); tasks.Add(Task.Run(async () => { var pid = -1; try { pid = await CoreHandler.Instance.LoadCoreConfigSpeedtest(it); if (pid > 0) { await Task.Delay(500); var delay = await DoRealPing(downloadHandle, it); if (blSpeedTest) { if (delay > 0) { await DoSpeedTest(downloadHandle, it); } else { UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip); } } } else { UpdateFunc(it.IndexId, "", ResUI.FailedToRunCore); } } catch (Exception ex) { Logging.SaveLog(_tag, ex); } finally { if (pid > 0) { await ProcUtils.ProcessKill(pid); } concurrencySemaphore.Release(); } })); } Task.WaitAll(tasks.ToArray()); } private async Task DoRealPing(DownloadService downloadHandle, ServerTestItem it) { var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}"); var responseTime = await downloadHandle.GetRealPingTime(_config.SpeedTestItem.SpeedPingTestUrl, webProxy, 10); ProfileExHandler.Instance.SetTestDelay(it.IndexId, responseTime); UpdateFunc(it.IndexId, responseTime.ToString()); return responseTime; } private async Task DoSpeedTest(DownloadService downloadHandle, ServerTestItem it) { UpdateFunc(it.IndexId, "", ResUI.Speedtesting); var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}"); var url = _config.SpeedTestItem.SpeedTestUrl; var timeout = _config.SpeedTestItem.SpeedTestTimeout; await downloadHandle.DownloadDataAsync(url, webProxy, timeout, (success, msg) => { decimal.TryParse(msg, out var dec); if (dec > 0) { ProfileExHandler.Instance.SetTestSpeed(it.IndexId, dec); } UpdateFunc(it.IndexId, "", msg); }); } private async Task GetTcpingTime(string url, int port) { var responseTime = -1; try { if (!IPAddress.TryParse(url, out var ipAddress)) { var ipHostInfo = await Dns.GetHostEntryAsync(url); ipAddress = ipHostInfo.AddressList.First(); } var timer = Stopwatch.StartNew(); IPEndPoint endPoint = new(ipAddress, port); using Socket clientSocket = new(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); var result = clientSocket.BeginConnect(endPoint, null, null); if (!result.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(5))) { throw new TimeoutException("connect timeout (5s): " + url); } clientSocket.EndConnect(result); timer.Stop(); responseTime = (int)timer.Elapsed.TotalMilliseconds; } catch (Exception ex) { Logging.SaveLog(_tag, ex); } return responseTime; } private List> GetTestBatchItem(List lstSelected, int pageSize) { List> lstTest = new(); var lst1 = lstSelected.Where(t => t.ConfigType is not (EConfigType.Hysteria2 or EConfigType.TUIC or EConfigType.WireGuard)).ToList(); var lst2 = lstSelected.Where(t => t.ConfigType is EConfigType.Hysteria2 or EConfigType.TUIC or EConfigType.WireGuard).ToList(); for (var num = 0; num < (int)Math.Ceiling(lst1.Count * 1.0 / pageSize); num++) { lstTest.Add(lst1.Skip(num * pageSize).Take(pageSize).ToList()); } for (var num = 0; num < (int)Math.Ceiling(lst2.Count * 1.0 / pageSize); num++) { lstTest.Add(lst2.Skip(num * pageSize).Take(pageSize).ToList()); } return lstTest; } private void UpdateFunc(string indexId, string delay, string speed = "") { _updateFunc?.Invoke(new() { IndexId = indexId, Delay = delay, Speed = speed }); if (indexId.IsNotEmpty() && speed.IsNotEmpty()) { ProfileExHandler.Instance.SetTestMessage(indexId, speed); } } } }