add custom timeout to url and speed test

This commit is contained in:
Nova 2025-10-03 14:42:55 +03:30
parent 1c4429f4aa
commit 808c4f31a9
9 changed files with 170 additions and 108 deletions

View File

@ -53,6 +53,7 @@ message TestReq {
optional string url = 4 [default = ""];
optional bool test_current = 5 [default = false];
optional int32 max_concurrency = 6 [default = 0];
optional int32 test_timeout_ms = 7 [default = 0];
}
message TestResp {
@ -98,6 +99,7 @@ message SpeedTestRequest {
optional bool test_upload = 6 [default = false];
optional bool simple_download = 7 [default = false];
optional string simple_download_addr = 8 [default = ""];
optional int32 timeout_ms = 9 [default = 0];
}
message SpeedTestResult {

View File

@ -182,7 +182,7 @@ func (s *server) Test(in *gen.TestReq, out *gen.TestResp) error {
if maxConcurrency >= 500 || maxConcurrency == 0 {
maxConcurrency = MaxConcurrentTests
}
results := BatchURLTest(testCtx, testInstance, outboundTags, *in.Url, int(maxConcurrency), twice)
results := BatchURLTest(testCtx, testInstance, outboundTags, *in.Url, int(maxConcurrency), twice, time.Duration(*in.TestTimeoutMs)*time.Millisecond)
res := make([]*gen.URLTestResp, 0)
for idx, data := range results {
@ -339,7 +339,7 @@ func (s *server) SpeedTest(in *gen.SpeedTestRequest, out *gen.SpeedTestResponse)
outboundTags = []string{outbound.Tag()}
}
results := BatchSpeedTest(testCtx, testInstance, outboundTags, *in.TestDownload, *in.TestUpload, *in.SimpleDownload, *in.SimpleDownloadAddr)
results := BatchSpeedTest(testCtx, testInstance, outboundTags, *in.TestDownload, *in.TestUpload, *in.SimpleDownload, *in.SimpleDownloadAddr, time.Duration(*in.TimeoutMs)*time.Millisecond)
res := make([]*gen.SpeedTestResult, 0)
for _, data := range results {

View File

@ -24,6 +24,7 @@ var SpTQuerier SpeedTestResultQuerier
var URLReporter URLTestReporter
const URLTestTimeout = 3 * time.Second
const FetchServersTimeout = 8 * time.Second
const MaxConcurrentTests = 100
type URLTestResult struct {
@ -84,7 +85,10 @@ func (s *SpeedTestResultQuerier) setIsRunning(isRunning bool) {
s.isRunning = isRunning
}
func BatchURLTest(ctx context.Context, i *boxbox.Box, outboundTags []string, url string, maxConcurrency int, twice bool) []*URLTestResult {
func BatchURLTest(ctx context.Context, i *boxbox.Box, outboundTags []string, url string, maxConcurrency int, twice bool, timeout time.Duration) []*URLTestResult {
if timeout <= 0 {
timeout = URLTestTimeout
}
outbounds := service.FromContext[adapter.OutboundManager](i.Context())
resMap := make(map[string]*URLTestResult)
resAccess := sync.Mutex{}
@ -113,11 +117,11 @@ func BatchURLTest(ctx context.Context, i *boxbox.Box, outboundTags []string, url
}
client := &http.Client{
Transport: &http.Transport{
DialContext: func(_ context.Context, network string, addr string) (net.Conn, error) {
DialContext: func(ctx context.Context, network string, addr string) (net.Conn, error) {
return outbound.DialContext(ctx, "tcp", metadata.ParseSocksaddr(addr))
},
},
Timeout: URLTestTimeout,
Timeout: timeout,
}
// to properly measure muxed configs, let's do the test twice
duration, err := urlTest(ctx, client, url)
@ -148,8 +152,6 @@ func BatchURLTest(ctx context.Context, i *boxbox.Box, outboundTags []string, url
}
func urlTest(ctx context.Context, client *http.Client, url string) (time.Duration, error) {
ctx, cancel := context.WithTimeout(ctx, URLTestTimeout)
defer cancel()
begin := time.Now()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
@ -169,7 +171,7 @@ func getNetDialer(dialer func(ctx context.Context, network string, destination m
}
}
func BatchSpeedTest(ctx context.Context, i *boxbox.Box, outboundTags []string, testDl, testUl bool, simpleDL bool, simpleAddress string) []*SpeedTestResult {
func BatchSpeedTest(ctx context.Context, i *boxbox.Box, outboundTags []string, testDl, testUl bool, simpleDL bool, simpleAddress string, timeout time.Duration) []*SpeedTestResult {
outbounds := service.FromContext[adapter.OutboundManager](i.Context())
results := make([]*SpeedTestResult, 0)
@ -189,9 +191,9 @@ func BatchSpeedTest(ctx context.Context, i *boxbox.Box, outboundTags []string, t
var err error
if simpleDL {
err = simpleDownloadTest(ctx, getNetDialer(outbound.DialContext), res, simpleAddress)
err = simpleDownloadTest(ctx, getNetDialer(outbound.DialContext), res, simpleAddress, timeout)
} else {
err = speedTestWithDialer(ctx, getNetDialer(outbound.DialContext), res, testDl, testUl)
err = speedTestWithDialer(ctx, getNetDialer(outbound.DialContext), res, testDl, testUl, timeout)
}
if err != nil && !errors.Is(err, context.Canceled) {
res.Error = err
@ -208,14 +210,17 @@ func BatchSpeedTest(ctx context.Context, i *boxbox.Box, outboundTags []string, t
return results
}
func simpleDownloadTest(ctx context.Context, dialer func(ctx context.Context, network string, address string) (net.Conn, error), res *SpeedTestResult, testURL string) error {
func simpleDownloadTest(ctx context.Context, dialer func(ctx context.Context, network string, address string) (net.Conn, error), res *SpeedTestResult, testURL string, timeout time.Duration) error {
if timeout <= 0 {
timeout = URLTestTimeout
}
client := &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network string, addr string) (net.Conn, error) {
return dialer(ctx, network, addr)
},
},
Timeout: URLTestTimeout,
Timeout: timeout,
}
res.ServerName = "N/A"
@ -268,13 +273,15 @@ func simpleDownloadTest(ctx context.Context, dialer func(ctx context.Context, ne
}
}
func speedTestWithDialer(ctx context.Context, dialer func(ctx context.Context, network string, address string) (net.Conn, error), res *SpeedTestResult, testDl, testUl bool) error {
func speedTestWithDialer(ctx context.Context, dialer func(ctx context.Context, network string, address string) (net.Conn, error), res *SpeedTestResult, testDl, testUl bool, timeout time.Duration) error {
clt := speedtest.New(speedtest.WithUserConfig(&speedtest.UserConfig{
DialContextFunc: dialer,
PingMode: speedtest.HTTP,
MaxConnections: 8,
}))
srv, err := clt.FetchServerListContext(ctx)
fetchCtx, cancel := context.WithTimeout(ctx, FetchServersTimeout)
defer cancel()
srv, err := clt.FetchServerListContext(fetchCtx)
if err != nil {
return err
}
@ -297,14 +304,18 @@ func speedTestWithDialer(ctx context.Context, dialer func(ctx context.Context, n
go func() {
defer func() { close(done) }()
if testDl {
err = srv[0].DownloadTestContext(ctx)
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
err = srv[0].DownloadTestContext(timeoutCtx)
if err != nil && !errors.Is(err, context.Canceled) {
res.Error = err
return
}
}
if testUl {
err = srv[0].UploadTestContext(ctx)
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
err = srv[0].UploadTestContext(timeoutCtx)
if err != nil && !errors.Is(err, context.Canceled) {
res.Error = err
return

View File

@ -79,6 +79,7 @@ namespace Configs {
// Misc
QString log_level = "info";
QString test_latency_url = "http://cp.cloudflare.com/";
int url_test_timeout_ms = 3000;
bool disable_tray = false;
int test_concurrent = 10;
bool disable_traffic_stats = false;
@ -99,6 +100,7 @@ namespace Configs {
bool enable_stats = true;
int stats_tab = 0; // either connection or log
int speed_test_mode = TestConfig::FULL;
int speed_test_timeout_ms = 3000;
QString simple_dl_url = "http://cachefly.cachefly.net/1mb.test";
bool allow_beta_update = false;

View File

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>728</width>
<width>767</width>
<height>472</height>
</rect>
</property>
@ -20,7 +20,7 @@
<string>Basic Settings</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="8" column="3">
<item row="9" column="3">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
@ -165,27 +165,52 @@
</item>
<item>
<widget class="QGroupBox" name="groupBox1">
<layout class="QHBoxLayout" name="horizontalLayout_9" stretch="0,8,1,1">
<layout class="QVBoxLayout" name="verticalLayout_7">
<item>
<widget class="QLabel" name="label_13">
<property name="text">
<string>Latency Test URL</string>
</property>
<widget class="QWidget" name="widget_2" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_12">
<item>
<widget class="QLabel" name="label_13">
<property name="text">
<string>Latency Test URL</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="test_latency_url"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLineEdit" name="test_latency_url"/>
</item>
<item>
<widget class="QLabel" name="label_14">
<property name="text">
<string>Concurrent</string>
</property>
<widget class="QWidget" name="widget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_9">
<item>
<widget class="QLabel" name="label_14">
<property name="text">
<string>Concurrent</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="test_concurrent"/>
</item>
<item>
<widget class="QLabel" name="label_22">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Timeout for URLtest in ms&lt;br/&gt;Note that muxed connections take a much longer time for their initial request, and setting this value too low will cause the test to falsely report that the config is not working&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Timeout</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="url_timeout"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLineEdit" name="test_concurrent"/>
</item>
</layout>
</widget>
</item>
@ -260,6 +285,19 @@
</item>
</widget>
</item>
<item>
<widget class="QLabel" name="label_19">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;timeout in milliseconds&lt;br/&gt;applies to all tests individually&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Timeout</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="test_timeout"/>
</item>
</layout>
</item>
<item>
@ -534,80 +572,80 @@
<string>Subscription</string>
</property>
<layout class="QGridLayout" name="gridLayout_10">
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QCheckBox" name="sub_auto_update_enable">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Enable</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_21">
<property name="text">
<string>Interval (minute, invalid if less than 30)</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="sub_auto_update">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="1">
<widget class="MyLineEdit" name="user_agent"/>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="sub_clear">
<property name="text">
<string>Clear servers before updating subscription</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="sub_send_hwid">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;HWID=%1&lt;/p&gt;&lt;p&gt;OS=%2&lt;/p&gt;&lt;p&gt;OS Version=%3&lt;/p&gt;&lt;p&gt;Model=%4&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Enable sending HWID, device model, and OS version when updating subscription</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_20">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Automatic update</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>User Agent</string>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QCheckBox" name="sub_auto_update_enable">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Enable</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_21">
<property name="text">
<string>Interval (minute, invalid if less than 30)</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="sub_auto_update">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="1">
<widget class="MyLineEdit" name="user_agent"/>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="sub_clear">
<property name="text">
<string>Clear servers before updating subscription</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="sub_send_hwid">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;HWID=%1&lt;/p&gt;&lt;p&gt;OS=%2&lt;/p&gt;&lt;p&gt;OS Version=%3&lt;/p&gt;&lt;p&gt;Model=%4&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Enable sending HWID, device model, and OS version when updating subscription</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_20">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Automatic update</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>User Agent</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View File

@ -318,6 +318,8 @@ namespace Configs {
_add(new configItem("use_mozilla_certs", &use_mozilla_certs, itemType::boolean));
_add(new configItem("allow_beta_update", &allow_beta_update, itemType::boolean));
_add(new configItem("adblock_enable", &adblock_enable, itemType::boolean));
_add(new configItem("speed_test_timeout_ms", &speed_test_timeout_ms, itemType::integer));
_add(new configItem("url_test_timeout_ms", &url_test_timeout_ms, itemType::integer));
}
void DataStore::UpdateStartedId(int id) {

View File

@ -2384,6 +2384,7 @@ void MainWindow::loadShortcuts()
auto mp = Configs::dataStore->shortcuts->shortcuts;
for (QList<QAction *> actions = findChildren<QAction *>(); QAction *action : actions)
{
if (action->data().isNull() || action->data().toString().isEmpty()) continue;
if (mp.count(action->data().toString()) == 0) action->setShortcut(QKeySequence());
else action->setShortcut(mp[action->data().toString()]);
}

View File

@ -43,6 +43,7 @@ void MainWindow::runURLTest(const QString& config, bool useDefault, const QStrin
req.url = Configs::dataStore->test_latency_url.toStdString();
req.use_default_outbound = useDefault;
req.max_concurrency = Configs::dataStore->test_concurrent;
req.test_timeout_ms = Configs::dataStore->url_test_timeout_ms;
auto done = new QMutex;
done->lock();
@ -51,7 +52,7 @@ void MainWindow::runURLTest(const QString& config, bool useDefault, const QStrin
bool ok;
while (true)
{
QThread::msleep(1500);
QThread::msleep(200);
if (done->try_lock()) break;
auto resp = defaultClient->QueryURLTest(&ok);
if (!ok || resp.results.empty())
@ -285,6 +286,7 @@ void MainWindow::runSpeedTest(const QString& config, bool useDefault, bool testC
req.simple_download = speedtestConf == Configs::TestConfig::SIMPLEDL;
req.simple_download_addr = Configs::dataStore->simple_dl_url.toStdString();
req.test_current = testCurrent;
req.timeout_ms = Configs::dataStore->speed_test_timeout_ms;
// loop query result
auto doneMu = new QMutex;

View File

@ -36,7 +36,9 @@ DialogBasicSettings::DialogBasicSettings(QWidget *parent)
D_LOAD_INT(test_concurrent)
D_LOAD_STRING(test_latency_url)
D_LOAD_BOOL(disable_tray)
ui->url_timeout->setText(Int2String(Configs::dataStore->url_test_timeout_ms));
ui->speedtest_mode->setCurrentIndex(Configs::dataStore->speed_test_mode);
ui->test_timeout->setText(Int2String(Configs::dataStore->speed_test_timeout_ms));
ui->simple_down_url->setText(Configs::dataStore->simple_dl_url);
ui->allow_beta->setChecked(Configs::dataStore->allow_beta_update);
@ -184,6 +186,8 @@ void DialogBasicSettings::accept() {
Configs::dataStore->proxy_scheme = ui->proxy_scheme->currentText().toLower();
Configs::dataStore->speed_test_mode = ui->speedtest_mode->currentIndex();
Configs::dataStore->simple_dl_url = ui->simple_down_url->text();
Configs::dataStore->url_test_timeout_ms = ui->url_timeout->text().toInt();
Configs::dataStore->speed_test_timeout_ms = ui->test_timeout->text().toInt();
Configs::dataStore->allow_beta_update = ui->allow_beta->isChecked();
// Style