feat: Optimize urltest and remove useless features

This commit is contained in:
unknown 2024-07-25 14:56:21 +03:30
parent 7b9dff74b1
commit c5da6e88de
No known key found for this signature in database
GPG Key ID: C2CA486E4F771093
16 changed files with 747 additions and 851 deletions

View File

@ -51,15 +51,18 @@ namespace NekoGui {
}
}
// Common
std::shared_ptr<BuildConfigResult> BuildConfig(const std::shared_ptr<ProxyEntity> &ent, bool forTest, bool forExport) {
std::shared_ptr<BuildConfigResult> BuildConfig(const std::shared_ptr<ProxyEntity> &ent, bool forTest, bool forExport, int chainID) {
auto result = std::make_shared<BuildConfigResult>();
auto status = std::make_shared<BuildConfigStatus>();
status->ent = ent;
status->result = result;
status->forTest = forTest;
status->forExport = forExport;
status->chainID = chainID;
auto customBean = dynamic_cast<NekoGui_fmt::CustomBean *>(ent->bean.get());
if (customBean != nullptr && customBean->core == "internal-full") {
@ -74,6 +77,80 @@ namespace NekoGui {
return result;
}
std::shared_ptr<BuildTestConfigResult> BuildTestConfig(QList<std::shared_ptr<ProxyEntity>> profiles) {
auto results = std::make_shared<BuildTestConfigResult>();
auto idx = 1;
QJsonArray outboundArray = {
QJsonObject{
{"type", "direct"},
{"tag", "direct"}
},
QJsonObject{
{"type", "block"},
{"tag", "block"}
},
QJsonObject{
{"type", "dns"},
{"tag", "dns-out"}
}
};
QJsonArray directDomainArray;
for (const auto &item: profiles) {
auto res = BuildConfig(item, true, false, idx++);
if (!res->error.isEmpty()) {
results->error = res->error;
return results;
}
if (item->CustomBean() != nullptr && item->CustomBean()->core == "internal-full") {
res->coreConfig["inbounds"] = QJsonArray();
results->fullConfigs[item->id] = QJsonObject2QString(res->coreConfig, true);
continue;
}
// not full config, process it
if (results->coreConfig.isEmpty()) {
results->coreConfig = res->coreConfig;
}
// add the direct dns domains
for (const auto &rule: res->coreConfig["dns"].toObject()["rules"].toArray()) {
if (rule.toObject().contains("domain")) {
for (const auto &domain: rule.toObject()["domain"].toArray()) {
directDomainArray.append(domain);
}
}
}
// now we add the outbounds of the current config to the final one
auto outbounds = res->coreConfig["outbounds"].toArray();
if (outbounds.isEmpty()) {
results->error = QString("outbounds is empty for %1").arg(item->bean->name);
return results;
}
auto tag = outbounds[0].toObject()["tag"].toString();
results->outboundTags << tag;
results->tag2entID.insert(tag, item->id);
for (const auto &outboundRef: outbounds) {
auto outbound = outboundRef.toObject();
if (outbound["tag"] == "direct" || outbound["tag"] == "block" || outbound["tag"] == "dns-out") continue;
outboundArray.append(outbound);
}
}
results->coreConfig["outbounds"] = outboundArray;
auto dnsObj = results->coreConfig["dns"].toObject();
auto dnsRulesObj = QJsonArray();
if (!directDomainArray.empty()) {
dnsRulesObj += QJsonObject{
{"domain", directDomainArray},
{"server", "dns-direct"}
};
}
dnsObj["rules"] = dnsRulesObj;
results->coreConfig["dns"] = dnsObj;
return results;
}
QString BuildChain(int chainId, const std::shared_ptr<BuildConfigStatus> &status) {
auto group = profileManager->GetGroup(status->ent->gid);
if (group == nullptr) {
@ -128,7 +205,7 @@ namespace NekoGui {
}
// BuildChain
QString chainTagOut = BuildChainInternal(0, ents, status);
QString chainTagOut = BuildChainInternal(chainId, ents, status);
// Chain ent traffic stat
if (ents.length() > 1) {
@ -154,21 +231,16 @@ namespace NekoGui {
// profile2 (in) (global) tag g-(id)
// profile1 tag (chainTag)-(id)
// profile0 (out) tag (chainTag)-(id) / single: chainTag=g-(id)
auto tagOut = chainTag + "-" + Int2String(ent->id);
// needGlobal: can only contain one?
bool needGlobal = false;
auto tagOut = chainTag + "-" + Int2String(ent->id) + "-" + Int2String(index);
// first profile set as global
auto isFirstProfile = index == ents.length() - 1;
if (isFirstProfile) {
needGlobal = true;
tagOut = "g-" + Int2String(ent->id);
tagOut = "g-" + Int2String(ent->id) + "-" + Int2String(index);
}
// last profile set as "proxy"
if (chainId == 0 && index == 0) {
needGlobal = false;
tagOut = "proxy";
}
@ -177,13 +249,6 @@ namespace NekoGui {
status->result->ignoreConnTag << tagOut;
}
if (needGlobal) {
if (status->globalProfiles.contains(ent->id)) {
continue;
}
status->globalProfiles += ent->id;
}
if (index > 0) {
// chain rules: past
if (pastExternalStat == 0) {
@ -433,7 +498,7 @@ namespace NekoGui {
}
// Outbounds
auto tagProxy = BuildChain(0, status);
auto tagProxy = BuildChain(status->chainID, status);
if (!status->result->error.isEmpty()) return;
// direct & block & dns-out
@ -465,7 +530,7 @@ namespace NekoGui {
if (dataStore->spmode_vpn) {
routeObj["auto_detect_interface"] = true;
}
routeObj["final"] = dataStore->routing->def_outbound;
if (!status->forTest) routeObj["final"] = dataStore->routing->def_outbound;
auto routeChain = NekoGui::profileManager->GetRouteChain(NekoGui::dataStore->routing->current_route_id);
if (routeChain == nullptr) {

View File

@ -16,16 +16,23 @@ namespace NekoGui {
std::list<std::shared_ptr<NekoGui_fmt::ExternalBuildResult>> extRs;
};
class BuildTestConfigResult {
public:
QString error;
QMap<int, QString> fullConfigs;
QMap<QString, int> tag2entID;
QJsonObject coreConfig;
QStringList outboundTags;
};
class BuildConfigStatus {
public:
std::shared_ptr<BuildConfigResult> result;
std::shared_ptr<ProxyEntity> ent;
int chainID = 0;
bool forTest;
bool forExport;
// priv
QList<int> globalProfiles;
// xxList is V2Ray format string list
QStringList domainListDNSDirect;
@ -37,7 +44,9 @@ namespace NekoGui {
QJsonArray outbounds;
};
std::shared_ptr<BuildConfigResult> BuildConfig(const std::shared_ptr<ProxyEntity> &ent, bool forTest, bool forExport);
std::shared_ptr<BuildTestConfigResult> BuildTestConfig(QList<std::shared_ptr<ProxyEntity>> profiles);
std::shared_ptr<BuildConfigResult> BuildConfig(const std::shared_ptr<ProxyEntity> &ent, bool forTest, bool forExport, int chainID = 0);
void BuildConfigSingBox(const std::shared_ptr<BuildConfigStatus> &status);

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"github.com/Mahdi-zarei/sing-box-extra/boxbox"
"github.com/sagernet/sing-box/common/settings"
"github.com/sagernet/sing/common/metadata"
"strings"
@ -13,12 +14,9 @@ import (
"grpc_server/gen"
"github.com/Mahdi-zarei/sing-box-extra/boxapi"
"github.com/Mahdi-zarei/sing-box-extra/boxbox"
"github.com/Mahdi-zarei/sing-box-extra/boxmain"
"github.com/matsuridayo/libneko/neko_common"
"github.com/matsuridayo/libneko/neko_log"
"github.com/matsuridayo/libneko/speedtest"
"log"
"github.com/sagernet/sing-box/option"
@ -86,53 +84,66 @@ func (s *server) Stop(ctx context.Context, in *gen.EmptyReq) (out *gen.ErrorResp
return
}
func (s *server) Test(ctx context.Context, in *gen.TestReq) (out *gen.TestResp, _ error) {
func (s *server) Test(ctx context.Context, in *gen.TestReq) (*gen.TestResp, error) {
var testInstance *boxbox.Box
var cancel context.CancelFunc
var err error
out = &gen.TestResp{Ms: 0}
defer func() {
var twice = true
if in.TestCurrent {
if instance == nil {
return &gen.TestResp{Results: []*gen.URLTestResp{{
OutboundTag: "proxy",
LatencyMs: 0,
Error: "Instance is not running",
}}}, nil
}
testInstance = instance
twice = false
} else {
testInstance, cancel, err = boxmain.Create([]byte(in.Config), true)
if err != nil {
out.Error = err.Error()
return nil, err
}
}()
if in.Mode == gen.TestMode_UrlTest {
var i *boxbox.Box
var cancel context.CancelFunc
if in.Config != nil {
// Test instance
i, cancel, err = boxmain.Create([]byte(in.Config.CoreConfig), true)
if i != nil {
defer i.Close()
defer cancel()
}
if err != nil {
return
}
} else {
// Test running instance
i = instance
if i == nil {
return
}
}
// Latency
out.Ms, err = speedtest.UrlTest(boxapi.CreateProxyHttpClient(i), in.Url, in.Timeout)
} else if in.Mode == gen.TestMode_TcpPing {
out.Ms, err = speedtest.TcpPing(in.Address, in.Timeout)
} else if in.Mode == gen.TestMode_FullTest {
i, cancel, err := boxmain.Create([]byte(in.Config.CoreConfig), true)
if i != nil {
defer i.Close()
defer cancel()
}
if err != nil {
return
}
return grpc_server.DoFullTest(ctx, in, i)
defer cancel()
defer testInstance.Close()
}
return
outboundTags := in.OutboundTags
if in.UseDefaultOutbound || in.TestCurrent {
outbound, err := testInstance.Router().DefaultOutbound("tcp")
if err != nil {
return nil, err
}
outboundTags = []string{outbound.Tag()}
}
var maxConcurrency = in.MaxConcurrency
if maxConcurrency >= 500 || maxConcurrency == 0 {
maxConcurrency = MaxConcurrentTests
}
results := BatchURLTest(testCtx, testInstance, outboundTags, in.Url, int(maxConcurrency), twice)
res := make([]*gen.URLTestResp, 0)
for idx, data := range results {
errStr := ""
if data.Error != nil {
errStr = data.Error.Error()
}
res = append(res, &gen.URLTestResp{
OutboundTag: outboundTags[idx],
LatencyMs: int32(data.Duration.Milliseconds()),
Error: errStr,
})
}
return &gen.TestResp{Results: res}, nil
}
func (s *server) StopTest(ctx context.Context, in *gen.EmptyReq) (*gen.EmptyResp, error) {
cancelTests()
testCtx, cancelTests = context.WithCancel(context.Background())
return &gen.EmptyResp{}, nil
}
func (s *server) QueryStats(ctx context.Context, in *gen.QueryStatsReq) (out *gen.QueryStatsResp, _ error) {

View File

@ -1,6 +1,7 @@
package main
import (
"context"
"fmt"
_ "unsafe"
@ -14,6 +15,7 @@ func main() {
fmt.Println("sing-box:", boxbox.Version)
fmt.Println()
testCtx, cancelTests = context.WithCancel(context.Background())
grpc_server.RunCore(setupCore, &server{})
return
}

View File

@ -0,0 +1,99 @@
package main
import (
"context"
"errors"
"github.com/Mahdi-zarei/sing-box-extra/boxbox"
"github.com/sagernet/sing/common/metadata"
"net"
"net/http"
"sync"
"time"
)
var testCtx context.Context
var cancelTests context.CancelFunc
const URLTestTimeout = 3 * time.Second
const MaxConcurrentTests = 100
type URLTestResult struct {
Duration time.Duration
Error error
}
func BatchURLTest(ctx context.Context, i *boxbox.Box, outboundTags []string, url string, maxConcurrency int, twice bool) []*URLTestResult {
router := i.Router()
resMap := make(map[string]*URLTestResult)
resAccess := sync.Mutex{}
limiter := make(chan struct{}, maxConcurrency)
wg := &sync.WaitGroup{}
wg.Add(len(outboundTags))
for _, tag := range outboundTags {
select {
case <-ctx.Done():
wg.Done()
resAccess.Lock()
resMap[tag] = &URLTestResult{
Duration: 0,
Error: errors.New("test aborted"),
}
resAccess.Unlock()
default:
time.Sleep(2 * time.Millisecond) // don't spawn goroutines too quickly
limiter <- struct{}{}
go func(t string) {
defer wg.Done()
outbound, found := router.Outbound(t)
if !found {
panic("no outbound with tag " + t + " found")
}
client := &http.Client{
Transport: &http.Transport{
DialContext: func(_ context.Context, network string, addr string) (net.Conn, error) {
return outbound.DialContext(ctx, "tcp", metadata.ParseSocksaddr(addr))
},
},
Timeout: URLTestTimeout,
}
// to properly measure muxed configs, let's do the test twice
duration, err := urlTest(ctx, client, url)
if err == nil && twice {
duration, err = urlTest(ctx, client, url)
}
resAccess.Lock()
resMap[t] = &URLTestResult{
Duration: duration,
Error: err,
}
resAccess.Unlock()
<-limiter
}(tag)
}
}
wg.Wait()
res := make([]*URLTestResult, 0, len(outboundTags))
for _, tag := range outboundTags {
res = append(res, resMap[tag])
}
return res
}
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 {
return 0, err
}
resp, err := client.Do(req)
if err != nil {
return 0, err
}
_ = resp.Body.Close()
return time.Since(begin), nil
}

View File

@ -1,180 +0,0 @@
package grpc_server
import (
"context"
"encoding/hex"
"fmt"
"grpc_server/gen"
"io"
"log"
"math"
"net"
"net/http"
"strings"
"time"
"github.com/matsuridayo/libneko/neko_common"
"github.com/matsuridayo/libneko/speedtest"
)
const (
KiB = 1024
MiB = 1024 * KiB
)
func getBetweenStr(str, start, end string) string {
n := strings.Index(str, start)
if n == -1 {
n = 0
}
str = string([]byte(str)[n:])
m := strings.Index(str, end)
if m == -1 {
m = len(str)
}
str = string([]byte(str)[:m])
return str[len(start):]
}
func DoFullTest(ctx context.Context, in *gen.TestReq, instance interface{}) (out *gen.TestResp, _ error) {
out = &gen.TestResp{}
httpClient := neko_common.CreateProxyHttpClient(instance)
// Latency
var latency string
if in.FullLatency {
t, _ := speedtest.UrlTest(httpClient, in.Url, in.Timeout)
out.Ms = t
if t > 0 {
latency = fmt.Sprint(t, "ms")
} else {
latency = "Error"
}
}
// UDP Latency
var udpLatency string
if in.FullUdpLatency {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
result := make(chan string)
go func() {
var startTime = time.Now()
pc, err := neko_common.DialContext(ctx, instance, "udp", "8.8.8.8:53")
if err == nil {
defer pc.Close()
dnsPacket, _ := hex.DecodeString("0000010000010000000000000377777706676f6f676c6503636f6d0000010001")
_, err = pc.Write(dnsPacket)
if err == nil {
var buf [1400]byte
_, err = pc.Read(buf[:])
}
}
if err == nil {
var endTime = time.Now()
result <- fmt.Sprint(endTime.Sub(startTime).Abs().Milliseconds(), "ms")
} else {
log.Println("UDP Latency test error:", err)
result <- "Error"
}
close(result)
}()
select {
case <-ctx.Done():
udpLatency = "Timeout"
case r := <-result:
udpLatency = r
}
cancel()
}
// 入口 IP
var in_ip string
if in.FullInOut {
_in_ip, err := net.ResolveIPAddr("ip", in.InAddress)
if err == nil {
in_ip = _in_ip.String()
} else {
in_ip = err.Error()
}
}
// 出口 IP
var out_ip string
if in.FullInOut {
resp, err := httpClient.Get("https://www.cloudflare.com/cdn-cgi/trace")
if err == nil {
b, _ := io.ReadAll(resp.Body)
out_ip = getBetweenStr(string(b), "ip=", "\n")
resp.Body.Close()
} else {
out_ip = "Error"
}
}
// 下载
var speed string
if in.FullSpeed {
if in.FullSpeedTimeout <= 0 {
in.FullSpeedTimeout = 30
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(in.FullSpeedTimeout))
result := make(chan string)
var bodyClose io.Closer
go func() {
req, _ := http.NewRequestWithContext(ctx, "GET", in.FullSpeedUrl, nil)
resp, err := httpClient.Do(req)
if err == nil && resp != nil && resp.Body != nil {
bodyClose = resp.Body
defer resp.Body.Close()
timeStart := time.Now()
n, _ := io.Copy(io.Discard, resp.Body)
timeEnd := time.Now()
duration := math.Max(timeEnd.Sub(timeStart).Seconds(), 0.000001)
resultSpeed := (float64(n) / duration) / MiB
result <- fmt.Sprintf("%.2fMiB/s", resultSpeed)
} else {
result <- "Error"
}
close(result)
}()
select {
case <-ctx.Done():
speed = "Timeout"
case s := <-result:
speed = s
}
cancel()
if bodyClose != nil {
bodyClose.Close()
}
}
fr := make([]string, 0)
if latency != "" {
fr = append(fr, fmt.Sprintf("Latency: %s", latency))
}
if udpLatency != "" {
fr = append(fr, fmt.Sprintf("UDPLatency: %s", udpLatency))
}
if speed != "" {
fr = append(fr, fmt.Sprintf("Speed: %s", speed))
}
if in_ip != "" {
fr = append(fr, fmt.Sprintf("In: %s", in_ip))
}
if out_ip != "" {
fr = append(fr, fmt.Sprintf("Out: %s", out_ip))
}
out.FullReport = strings.Join(fr, " / ")
return
}

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@ service LibcoreService {
rpc Start(LoadConfigReq) returns (ErrorResp) {}
rpc Stop(EmptyReq) returns (ErrorResp) {}
rpc Test(TestReq) returns (TestResp) {}
rpc StopTest(EmptyReq) returns (EmptyResp);
rpc QueryStats(QueryStatsReq) returns (QueryStatsResp) {}
rpc ListConnections(EmptyReq) returns (ListConnectionsResp) {}
//
@ -36,37 +37,23 @@ message LoadConfigReq {
bool disable_stats = 4;
}
enum TestMode {
TcpPing = 0;
UrlTest = 1;
FullTest = 2;
message URLTestResp {
string outbound_tag = 1;
int32 latency_ms = 2;
string error = 3;
}
message TestReq {
TestMode mode = 1;
int32 timeout = 6;
// TcpPing
string address = 2;
// UrlTest
LoadConfigReq config = 3;
string inbound = 4;
string url = 5;
// FullTest
string in_address = 7;
bool full_latency = 8;
bool full_speed = 9;
string full_speed_url = 13;
int32 full_speed_timeout = 14;
bool full_in_out = 10;
bool full_udp_latency = 12;
//
bool full_nat = 11 [deprecated = true];
string config = 1;
repeated string outbound_tags = 2;
bool use_default_outbound = 3;
string url = 4;
bool test_current = 5;
int32 max_concurrency = 6;
}
message TestResp {
string error = 1;
int32 ms = 2;
string full_report = 3;
repeated URLTestResp results = 1;
}
message QueryStatsReq{

View File

@ -24,6 +24,7 @@ const (
LibcoreService_Start_FullMethodName = "/libcore.LibcoreService/Start"
LibcoreService_Stop_FullMethodName = "/libcore.LibcoreService/Stop"
LibcoreService_Test_FullMethodName = "/libcore.LibcoreService/Test"
LibcoreService_StopTest_FullMethodName = "/libcore.LibcoreService/StopTest"
LibcoreService_QueryStats_FullMethodName = "/libcore.LibcoreService/QueryStats"
LibcoreService_ListConnections_FullMethodName = "/libcore.LibcoreService/ListConnections"
LibcoreService_GetGeoIPList_FullMethodName = "/libcore.LibcoreService/GetGeoIPList"
@ -42,6 +43,7 @@ type LibcoreServiceClient interface {
Start(ctx context.Context, in *LoadConfigReq, opts ...grpc.CallOption) (*ErrorResp, error)
Stop(ctx context.Context, in *EmptyReq, opts ...grpc.CallOption) (*ErrorResp, error)
Test(ctx context.Context, in *TestReq, opts ...grpc.CallOption) (*TestResp, error)
StopTest(ctx context.Context, in *EmptyReq, opts ...grpc.CallOption) (*EmptyResp, error)
QueryStats(ctx context.Context, in *QueryStatsReq, opts ...grpc.CallOption) (*QueryStatsResp, error)
ListConnections(ctx context.Context, in *EmptyReq, opts ...grpc.CallOption) (*ListConnectionsResp, error)
GetGeoIPList(ctx context.Context, in *EmptyReq, opts ...grpc.CallOption) (*GetGeoIPListResponse, error)
@ -104,6 +106,15 @@ func (c *libcoreServiceClient) Test(ctx context.Context, in *TestReq, opts ...gr
return out, nil
}
func (c *libcoreServiceClient) StopTest(ctx context.Context, in *EmptyReq, opts ...grpc.CallOption) (*EmptyResp, error) {
out := new(EmptyResp)
err := c.cc.Invoke(ctx, LibcoreService_StopTest_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *libcoreServiceClient) QueryStats(ctx context.Context, in *QueryStatsReq, opts ...grpc.CallOption) (*QueryStatsResp, error) {
out := new(QueryStatsResp)
err := c.cc.Invoke(ctx, LibcoreService_QueryStats_FullMethodName, in, out, opts...)
@ -176,6 +187,7 @@ type LibcoreServiceServer interface {
Start(context.Context, *LoadConfigReq) (*ErrorResp, error)
Stop(context.Context, *EmptyReq) (*ErrorResp, error)
Test(context.Context, *TestReq) (*TestResp, error)
StopTest(context.Context, *EmptyReq) (*EmptyResp, error)
QueryStats(context.Context, *QueryStatsReq) (*QueryStatsResp, error)
ListConnections(context.Context, *EmptyReq) (*ListConnectionsResp, error)
GetGeoIPList(context.Context, *EmptyReq) (*GetGeoIPListResponse, error)
@ -205,6 +217,9 @@ func (UnimplementedLibcoreServiceServer) Stop(context.Context, *EmptyReq) (*Erro
func (UnimplementedLibcoreServiceServer) Test(context.Context, *TestReq) (*TestResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method Test not implemented")
}
func (UnimplementedLibcoreServiceServer) StopTest(context.Context, *EmptyReq) (*EmptyResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method StopTest not implemented")
}
func (UnimplementedLibcoreServiceServer) QueryStats(context.Context, *QueryStatsReq) (*QueryStatsResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method QueryStats not implemented")
}
@ -329,6 +344,24 @@ func _LibcoreService_Test_Handler(srv interface{}, ctx context.Context, dec func
return interceptor(ctx, in, info, handler)
}
func _LibcoreService_StopTest_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(EmptyReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(LibcoreServiceServer).StopTest(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: LibcoreService_StopTest_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(LibcoreServiceServer).StopTest(ctx, req.(*EmptyReq))
}
return interceptor(ctx, in, info, handler)
}
func _LibcoreService_QueryStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(QueryStatsReq)
if err := dec(in); err != nil {
@ -482,6 +515,10 @@ var LibcoreService_ServiceDesc = grpc.ServiceDesc{
MethodName: "Test",
Handler: _LibcoreService_Test_Handler,
},
{
MethodName: "StopTest",
Handler: _LibcoreService_StopTest_Handler,
},
{
MethodName: "QueryStats",
Handler: _LibcoreService_QueryStats_Handler,

View File

@ -91,6 +91,8 @@ func RunCore(setupCore func(), server gen.LibcoreServiceServer) {
s := grpc.NewServer(
grpc.StreamInterceptor(grpc_auth.StreamServerInterceptor(auther.Authenticate)),
grpc.UnaryInterceptor(grpc_auth.UnaryServerInterceptor(auther.Authenticate)),
grpc.MaxRecvMsgSize(1024*1024*1024), // 1 gigaByte
grpc.MaxSendMsgSize(1024*1024*1024), // 1 gigaByte
)
gen.RegisterLibcoreServiceServer(s, server)

View File

@ -66,9 +66,7 @@ namespace QtGrpc {
QNetworkRequest request(callUrl);
// request.setAttribute(QNetworkRequest::CacheSaveControlAttribute, false);
// request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
request.setAttribute(QNetworkRequest::Http2DirectAttribute, true);
#endif
request.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String{"application/grpc"});
request.setRawHeader("Cache-Control", "no-store");
request.setRawHeader(GrpcAcceptEncodingHeader, QByteArray{"identity,deflate,gzip"});
@ -268,6 +266,19 @@ namespace NekoGui_rpc {
}
}
void Client::StopTests(bool *rpcOK) {
const libcore::EmptyReq req;
libcore::EmptyResp resp;
auto status = make_grpc_channel()->Call("StopTest", req, &resp);
if (status == QNetworkReply::NoError) {
*rpcOK = true;
} else {
NOT_OK
}
}
libcore::UpdateResp Client::Update(bool *rpcOK, const libcore::UpdateReq &request) {
libcore::UpdateResp reply;
auto status = default_grpc_channel->Call("Update", request, &reply);

View File

@ -28,6 +28,8 @@ namespace NekoGui_rpc {
libcore::TestResp Test(bool *rpcOK, const libcore::TestReq &request);
void StopTests(bool *rpcOK);
libcore::UpdateResp Update(bool *rpcOK, const libcore::UpdateReq &request);
QStringList GetGeoList(bool *rpcOK, GeoRuleSetType mode);

View File

@ -1,7 +1,6 @@
#include "./ui_mainwindow.h"
#include "mainwindow.h"
#include "fmt/Preset.hpp"
#include "db/ProfileFilter.hpp"
#include "db/ConfigBuilder.hpp"
#include "sub/GroupUpdater.hpp"
@ -68,7 +67,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
}
themeManager->ApplyTheme(NekoGui::dataStore->theme);
ui->setupUi(this);
speedTestThreadPool->setMaxThreadCount(NekoGui::dataStore->test_concurrent);
speedTestThreadPool->setMaxThreadCount(10); // constant value
//
connect(ui->menu_start, &QAction::triggered, this, [=]() { neko_start(); });
connect(ui->menu_stop, &QAction::triggered, this, [=]() { neko_stop(); });
@ -323,37 +322,28 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
neko_set_spmode_vpn(false);
});
connect(ui->menu_qr, &QAction::triggered, this, [=]() { display_qr_link(false); });
connect(ui->menu_tcp_ping, &QAction::triggered, this, [=]() { speedtest_current_group(0); });
connect(ui->menu_url_test, &QAction::triggered, this, [=]() { speedtest_current_group(1); });
connect(ui->menu_full_test, &QAction::triggered, this, [=]() { speedtest_current_group(2); });
connect(ui->menu_stop_testing, &QAction::triggered, this, [=]() { speedtest_current_group(114514); });
connect(ui->menu_server, &QMenu::aboutToShow, this, [=](){
if (!speedtestRunning.tryLock()) {
ui->menu_server->addAction(ui->menu_stop_testing);
return;
} else {
speedtestRunning.unlock();
ui->menu_server->removeAction(ui->menu_stop_testing);
}
});
connect(ui->actionUrl_Test_Selected, &QAction::triggered, this, [=]() {
speedtest_current_group(get_now_selected_list());
});
connect(ui->actionUrl_Test_Group, &QAction::triggered, this, [=]() {
speedtest_current_group(NekoGui::profileManager->CurrentGroup()->ProfilesWithOrder());
});
connect(ui->menu_stop_testing, &QAction::triggered, this, [=]() { stopSpeedTests(); });
//
auto set_selected_or_group = [=](int mode) {
// 0=group 1=select 2=unknown(menu is hide)
ui->menu_server->setProperty("selected_or_group", mode);
};
auto move_tests_to_menu = [=](bool menuCurrent_Select) {
return [=] {
if (menuCurrent_Select) {
ui->menuCurrent_Select->insertAction(ui->actionfake_4, ui->menu_tcp_ping);
ui->menuCurrent_Select->insertAction(ui->actionfake_4, ui->menu_url_test);
ui->menuCurrent_Select->insertAction(ui->actionfake_4, ui->menu_full_test);
ui->menuCurrent_Select->insertAction(ui->actionfake_4, ui->menu_stop_testing);
ui->menuCurrent_Select->insertAction(ui->actionfake_4, ui->menu_clear_test_result);
ui->menuCurrent_Select->insertAction(ui->actionfake_4, ui->menu_resolve_domain);
} else {
ui->menuCurrent_Group->insertAction(ui->actionfake_5, ui->menu_tcp_ping);
ui->menuCurrent_Group->insertAction(ui->actionfake_5, ui->menu_url_test);
ui->menuCurrent_Group->insertAction(ui->actionfake_5, ui->menu_full_test);
ui->menuCurrent_Group->insertAction(ui->actionfake_5, ui->menu_stop_testing);
ui->menuCurrent_Group->insertAction(ui->actionfake_5, ui->menu_clear_test_result);
ui->menuCurrent_Group->insertAction(ui->actionfake_5, ui->menu_resolve_domain);
}
set_selected_or_group(menuCurrent_Select ? 1 : 0);
};
};
connect(ui->menuCurrent_Select, &QMenu::aboutToShow, this, move_tests_to_menu(true));
connect(ui->menuCurrent_Group, &QMenu::aboutToShow, this, move_tests_to_menu(false));
connect(ui->menu_server, &QMenu::aboutToHide, this, [=] {
setTimeout([=] { set_selected_or_group(2); }, this, 200);
});
@ -911,11 +901,10 @@ void MainWindow::refresh_proxy_list_impl(const int &id, GroupSortAction groupSor
ui->proxyListTable->setRowCount(0);
// 添加行
int row = -1;
for (const auto &[id, profile]: NekoGui::profileManager->profiles) {
if (NekoGui::dataStore->current_group != profile->gid) continue;
for (const auto ent: NekoGui::profileManager->GetGroup(NekoGui::dataStore->current_group)->ProfilesWithOrder()) {
row++;
ui->proxyListTable->insertRow(row);
ui->proxyListTable->row2Id += id;
ui->proxyListTable->row2Id += ent->id;
}
}

View File

@ -190,9 +190,11 @@ private:
static void setup_grpc();
void speedtest_current_group(int mode);
void speedtest_current_group(const QList<std::shared_ptr<NekoGui::ProxyEntity>>& profiles);
void RunSpeedTest(const std::shared_ptr<NekoGui::ProxyEntity>& ent, int mode, const QStringList& full_test_flags);
void stopSpeedTests();
void RunSpeedTest(const QString& config, bool useDefault, const QStringList& outboundTags, const QMap<QString, int>& tag2entID, int entID = -1);
void url_test_current();

View File

@ -38,10 +38,10 @@
</size>
</property>
<property name="popupMode">
<enum>QToolButton::InstantPopup</enum>
<enum>QToolButton::ToolButtonPopupMode::InstantPopup</enum>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextUnderIcon</enum>
<enum>Qt::ToolButtonStyle::ToolButtonTextUnderIcon</enum>
</property>
</widget>
</item>
@ -60,10 +60,10 @@
</size>
</property>
<property name="popupMode">
<enum>QToolButton::InstantPopup</enum>
<enum>QToolButton::ToolButtonPopupMode::InstantPopup</enum>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextUnderIcon</enum>
<enum>Qt::ToolButtonStyle::ToolButtonTextUnderIcon</enum>
</property>
</widget>
</item>
@ -82,10 +82,10 @@
</size>
</property>
<property name="popupMode">
<enum>QToolButton::InstantPopup</enum>
<enum>QToolButton::ToolButtonPopupMode::InstantPopup</enum>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextUnderIcon</enum>
<enum>Qt::ToolButtonStyle::ToolButtonTextUnderIcon</enum>
</property>
</widget>
</item>
@ -104,10 +104,10 @@
</size>
</property>
<property name="popupMode">
<enum>QToolButton::InstantPopup</enum>
<enum>QToolButton::ToolButtonPopupMode::InstantPopup</enum>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextUnderIcon</enum>
<enum>Qt::ToolButtonStyle::ToolButtonTextUnderIcon</enum>
</property>
</widget>
</item>
@ -138,7 +138,7 @@
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -160,7 +160,7 @@
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
@ -192,19 +192,19 @@
<item row="0" column="0">
<widget class="MyTableWidget" name="proxyListTable">
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
<enum>Qt::ContextMenuPolicy::CustomContextMenu</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
<set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
<enum>QAbstractItemView::SelectionBehavior::SelectRows</enum>
</property>
<property name="horizontalScrollMode">
<enum>QAbstractItemView::ScrollPerPixel</enum>
<enum>QAbstractItemView::ScrollMode::ScrollPerPixel</enum>
</property>
<property name="wordWrap">
<bool>false</bool>
@ -274,7 +274,7 @@
<item>
<widget class="QTextBrowser" name="masterLogBrowser">
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
<enum>Qt::ContextMenuPolicy::CustomContextMenu</enum>
</property>
<property name="openLinks">
<bool>false</bool>
@ -300,10 +300,10 @@
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Maximum</enum>
<enum>QSizePolicy::Policy::Maximum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -337,7 +337,7 @@
<x>0</x>
<y>0</y>
<width>800</width>
<height>25</height>
<height>33</height>
</rect>
</property>
<widget class="QMenu" name="menu_program">
@ -404,29 +404,6 @@
<addaction name="menu_copy_links"/>
<addaction name="menu_copy_links_nkr"/>
</widget>
<widget class="QMenu" name="menuCurrent_Group">
<property name="title">
<string>Current Group</string>
</property>
<addaction name="actionfake_5"/>
<addaction name="menu_tcp_ping"/>
<addaction name="menu_url_test"/>
<addaction name="menu_full_test"/>
<addaction name="menu_stop_testing"/>
<addaction name="menu_clear_test_result"/>
<addaction name="menu_resolve_domain"/>
<addaction name="separator"/>
<addaction name="menu_remove_unavailable"/>
<addaction name="menu_delete_repeat"/>
<addaction name="separator"/>
<addaction name="menu_update_subscription"/>
</widget>
<widget class="QMenu" name="menuCurrent_Select">
<property name="title">
<string>Current Select</string>
</property>
<addaction name="actionfake_4"/>
</widget>
<addaction name="menu_add_from_input"/>
<addaction name="menu_add_from_clipboard"/>
<addaction name="separator"/>
@ -440,12 +417,8 @@
<addaction name="menu_delete"/>
<addaction name="separator"/>
<addaction name="menu_share_item"/>
<addaction name="separator"/>
<addaction name="menuCurrent_Select"/>
<addaction name="menuCurrent_Group"/>
<addaction name="separator"/>
<addaction name="menu_profile_debug_info"/>
<addaction name="separator"/>
<addaction name="actionUrl_Test_Selected"/>
<addaction name="actionUrl_Test_Group"/>
</widget>
<addaction name="menu_program"/>
<addaction name="menu_preferences"/>
@ -779,6 +752,16 @@
<string>Stop Testing</string>
</property>
</action>
<action name="actionUrl_Test_Selected">
<property name="text">
<string>Url Test Selected</string>
</property>
</action>
<action name="actionUrl_Test_Group">
<property name="text">
<string>Url Test Group</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>

View File

@ -8,12 +8,10 @@
#include "ui/widget/MessageBoxTimer.h"
#include <QTimer>
#include <QThread>
#include <QInputDialog>
#include <QPushButton>
#include <QDesktopServices>
#include <QMessageBox>
#include <QDialogButtonBox>
// ext core
@ -49,7 +47,7 @@ void MainWindow::setup_grpc() {
runOnNewThread([=] { NekoGui_traffic::trafficLooper->Loop(); });
}
void MainWindow::RunSpeedTest(const std::shared_ptr<NekoGui::ProxyEntity>& ent, int mode, const QStringList& full_test_flags) {
void MainWindow::RunSpeedTest(const QString& config, bool useDefault, const QStringList& outboundTags, const QMap<QString, int>& tag2entID, int entID) {
if (stopSpeedtest.load()) {
MW_show_log("Profile test aborted");
return;
@ -59,46 +57,13 @@ void MainWindow::RunSpeedTest(const std::shared_ptr<NekoGui::ProxyEntity>& ent,
QSemaphore extSem;
libcore::TestReq req;
req.set_mode((libcore::TestMode) mode);
req.set_timeout(3000);
req.set_url(NekoGui::dataStore->test_latency_url.toStdString());
if (mode == libcore::TestMode::UrlTest || mode == libcore::FullTest) {
auto c = BuildConfig(ent, true, false);
if (!c->error.isEmpty()) {
ent->full_test_report = c->error;
ent->Save();
auto profileId = ent->id;
runOnUiThread([this, profileId] {
refresh_proxy_list(profileId);
});
}
//
if (!c->extRs.empty()) {
runOnUiThread(
[&] {
extCs = CreateExtCFromExtR(c->extRs, true);
QThread::msleep(500);
extSem.release();
},
DS_cores);
extSem.acquire();
}
//
auto config = new libcore::LoadConfigReq;
config->set_core_config(QJsonObject2QString(c->coreConfig, true).toStdString());
req.set_allocated_config(config);
req.set_in_address(ent->bean->serverAddress.toStdString());
req.set_full_latency(full_test_flags.contains("1"));
req.set_full_udp_latency(full_test_flags.contains("2"));
req.set_full_speed(full_test_flags.contains("3"));
req.set_full_in_out(full_test_flags.contains("4"));
req.set_full_speed_url(NekoGui::dataStore->test_download_url.toStdString());
req.set_full_speed_timeout(NekoGui::dataStore->test_download_timeout);
} else if (mode == libcore::TcpPing) {
req.set_address(ent->bean->DisplayAddress().toStdString());
for (const auto &item: outboundTags) {
req.add_outbound_tags(item.toStdString());
}
req.set_config(config.toStdString());
req.set_url(NekoGui::dataStore->test_latency_url.toStdString());
req.set_use_default_outbound(useDefault);
req.set_max_concurrency(NekoGui::dataStore->test_concurrent);
bool rpcOK;
auto result = defaultClient->Test(&rpcOK, req);
@ -117,29 +82,33 @@ void MainWindow::RunSpeedTest(const std::shared_ptr<NekoGui::ProxyEntity>& ent,
//
if (!rpcOK) return;
if (result.error().empty()) {
ent->latency = result.ms();
if (ent->latency == 0) ent->latency = 1; // nekoray use 0 to represents not tested
} else {
ent->latency = -1;
}
ent->full_test_report = result.full_report().c_str(); // higher priority
ent->Save();
for (const auto &res: result.results()) {
if (!tag2entID.empty()) {
entID = tag2entID.count(QString(res.outbound_tag().c_str())) == 0 ? -1 : tag2entID[QString(res.outbound_tag().c_str())];
}
if (entID == -1) {
MW_show_log("Something is very wrong, the subject ent cannot be found!");
continue;
}
if (!result.error().empty()) {
MW_show_log(tr("[%1] test error: %2").arg(ent->bean->DisplayTypeAndName(), result.error().c_str()));
}
auto ent = NekoGui::profileManager->GetProfile(entID);
if (ent == nullptr) {
MW_show_log("Profile manager data is corrupted, try again.");
continue;
}
auto profileId = ent->id;
runOnUiThread([this, profileId] {
refresh_proxy_list(profileId);
});
if (res.error().empty()) {
ent->latency = res.latency_ms();
} else {
ent->latency = 0;
MW_show_log(tr("[%1] test error: %2").arg(ent->bean->DisplayTypeAndName(), res.error().c_str()));
}
ent->Save();
}
}
void MainWindow::speedtest_current_group(int mode) {
// menu_stop_testing mode == 114514, TODO use proper constants
if (mode == 114514) {
stopSpeedtest.store(true);
void MainWindow::speedtest_current_group(const QList<std::shared_ptr<NekoGui::ProxyEntity>>& profiles) {
if (profiles.isEmpty()) {
return;
}
if (!speedtestRunning.tryLock()) {
@ -147,95 +116,72 @@ void MainWindow::speedtest_current_group(int mode) {
return;
}
speedTestThreadPool->setMaxThreadCount(NekoGui::dataStore->test_concurrent);
runOnNewThread([this, profiles]() {
auto buildObject = NekoGui::BuildTestConfig(profiles);
auto profiles = get_selected_or_group();
if (profiles.isEmpty()) {
speedtestRunning.unlock();
return;
}
QStringList full_test_flags;
if (mode == libcore::FullTest) {
auto w = new QDialog(this);
auto layout = new QVBoxLayout(w);
w->setWindowTitle(tr("Test Options"));
//
auto l1 = new QCheckBox(tr("Latency"));
auto l2 = new QCheckBox(tr("UDP latency"));
auto l3 = new QCheckBox(tr("Download speed"));
auto l4 = new QCheckBox(tr("In and Out IP"));
//
auto box = new QDialogButtonBox;
box->setOrientation(Qt::Horizontal);
box->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok);
connect(box, &QDialogButtonBox::accepted, w, &QDialog::accept);
connect(box, &QDialogButtonBox::rejected, w, &QDialog::reject);
//
layout->addWidget(l1);
layout->addWidget(l2);
layout->addWidget(l3);
layout->addWidget(l4);
layout->addWidget(box);
if (w->exec() != QDialog::Accepted) {
w->deleteLater();
speedtestRunning.unlock();
return;
}
//
if (l1->isChecked()) full_test_flags << "1";
if (l2->isChecked()) full_test_flags << "2";
if (l3->isChecked()) full_test_flags << "3";
if (l4->isChecked()) full_test_flags << "4";
//
w->deleteLater();
if (full_test_flags.isEmpty()) {
speedtestRunning.unlock();
return;
}
}
runOnNewThread([profiles, full_test_flags, mode, this]() {
std::atomic<int> counter(0);
stopSpeedtest.store(false);
for (const auto &item: profiles) {
auto func = [&item, full_test_flags, mode, this, &counter, profiles]() {
MainWindow::RunSpeedTest(item, mode, full_test_flags);
auto testCount = buildObject->fullConfigs.size() + (!buildObject->outboundTags.empty());
for (const auto &entID: buildObject->fullConfigs.keys()) {
auto configStr = buildObject->fullConfigs[entID];
auto func = [this, &counter, testCount, configStr, entID]() {
MainWindow::RunSpeedTest(configStr, true, {}, {}, entID);
counter++;
if (counter.load() == profiles.size()) {
if (counter.load() == testCount) {
speedtestRunning.unlock();
}
};
speedTestThreadPool->start(func);
}
if (!buildObject->outboundTags.empty()) {
auto func = [this, &buildObject, &counter, testCount]() {
MainWindow::RunSpeedTest(QJsonObject2QString(buildObject->coreConfig, false), false, buildObject->outboundTags, buildObject->tag2entID);
counter++;
if (counter.load() == testCount) {
speedtestRunning.unlock();
}
};
speedTestThreadPool->start(func);
}
speedtestRunning.lock();
speedtestRunning.unlock();
MW_show_log("Speedtest finished!");
runOnUiThread([=]{
refresh_proxy_list();
MW_show_log("Speedtest finished!");
});
});
}
void MainWindow::stopSpeedTests() {
bool ok;
defaultClient->StopTests(&ok);
if (!ok) {
MW_show_log("Failed to stop tests");
}
}
void MainWindow::url_test_current() {
last_test_time = QTime::currentTime();
ui->label_running->setText(tr("Testing"));
runOnNewThread([=] {
libcore::TestReq req;
req.set_mode(libcore::UrlTest);
req.set_timeout(3000);
req.set_test_current(true);
req.set_url(NekoGui::dataStore->test_latency_url.toStdString());
bool rpcOK;
auto result = defaultClient->Test(&rpcOK, req);
if (!rpcOK) return;
auto latency = result.ms();
auto latency = result.results()[0].latency_ms();
last_test_time = QTime::currentTime();
runOnUiThread([=] {
if (!result.error().empty()) {
MW_show_log(QString("UrlTest error: %1").arg(result.error().c_str()));
if (!result.results()[0].error().empty()) {
MW_show_log(QString("UrlTest error: %1").arg(result.results()[0].error().c_str()));
}
if (latency <= 0) {
ui->label_running->setText(tr("Test Result") + ": " + tr("Unavailable"));