From fd8b3ce164a73e2085772c419b8880c927e89007 Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:57:13 +0800 Subject: [PATCH] Split delay/stats helpers into utils file Moves outbound traffic and delay-related helper methods out of `libv2ray_main.go` into a new `libv2ray_utils.go` to keep core lifecycle code focused. The refactor also trims now-unused imports in the main file, removes the old `QueryStats` helper, and bumps `libVersion` from 39 to 40. --- libv2ray_main.go | 186 +--------------------------------------------- libv2ray_utils.go | 176 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+), 185 deletions(-) create mode 100644 libv2ray_utils.go diff --git a/libv2ray_main.go b/libv2ray_main.go index 1941b71..05ad11c 100644 --- a/libv2ray_main.go +++ b/libv2ray_main.go @@ -1,25 +1,18 @@ package libv2ray import ( - "context" - "errors" "fmt" "io" "log" - "net" - "net/http" "os" "path/filepath" "strconv" "strings" "sync" - "time" coreapplog "github.com/xtls/xray-core/app/log" corecommlog "github.com/xtls/xray-core/common/log" - corenet "github.com/xtls/xray-core/common/net" corefilesystem "github.com/xtls/xray-core/common/platform/filesystem" - "github.com/xtls/xray-core/common/serial" core "github.com/xtls/xray-core/core" corestats "github.com/xtls/xray-core/features/stats" coreserial "github.com/xtls/xray-core/infra/conf/serial" @@ -35,7 +28,7 @@ const ( xudpBaseKey = "xray.xudp.basekey" tunFdKey = "xray.tun.fd" browserDialerAddress = "xray.browser.dialer" - libVersion = 39 // Library version, update here only + libVersion = 40 // Library version, update here only ) // CoreController represents a controller for managing Xray core instance lifecycle @@ -141,95 +134,6 @@ func (x *CoreController) StopLoop() error { return nil } -// QueryStats retrieves and resets traffic statistics for a specific outbound tag and direction -// Returns the accumulated traffic value and resets the counter to zero -// Returns 0 if the stats manager is not initialized or the counter doesn't exist -func (x *CoreController) QueryStats(tag string, direct string) int64 { - if x.statsManager == nil { - return 0 - } - counter := x.statsManager.GetCounter(fmt.Sprintf("outbound>>>%s>>>traffic>>>%s", tag, direct)) - if counter == nil { - return 0 - } - return counter.Set(0) -} - -// QueryAllOutboundTrafficStats retrieves and resets all outbound traffic counters. -// Returns a single-line text in format: tag,direction,value;tag,direction,value; -// Returns an empty string if the stats manager is not initialized or no counters exist. -func (x *CoreController) QueryAllOutboundTrafficStats() string { - if x.statsManager == nil { - return "" - } - - var b strings.Builder - - x.statsManager.VisitCounters(func(name string, counter corestats.Counter) bool { - parts := strings.Split(name, ">>>") - if len(parts) != 4 || parts[0] != "outbound" || parts[2] != "traffic" { - return true - } - - tag := parts[1] - direct := parts[3] - value := counter.Set(0) - if value <= 0 { - return true // Skip counters with non-positive values - } - - b.WriteString(tag) - b.WriteByte(',') - b.WriteString(direct) - b.WriteByte(',') - b.WriteString(strconv.FormatInt(value, 10)) - b.WriteByte(';') - return true - }) - return b.String() -} - -// MeasureDelay measures network latency to a specified URL through the current core instance -// Uses a 12-second timeout context and returns the round-trip time in milliseconds -// An error is returned if the connection fails or returns an unexpected status -func (x *CoreController) MeasureDelay(url string) (int64, error) { - ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second) - defer cancel() - - return measureInstDelay(ctx, x.coreInstance, url) -} - -// MeasureOutboundDelay measures the outbound delay for a given configuration and URL -func MeasureOutboundDelay(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) - } - - // Simplify config for testing - config.Inbound = nil - var essentialApp []*serial.TypedMessage - for _, app := range config.App { - if app.Type == "xray.app.proxyman.OutboundConfig" || - app.Type == "xray.app.dispatcher.Config" || - app.Type == "xray.app.log.Config" { - essentialApp = append(essentialApp, app) - } - } - config.App = essentialApp - - inst, err := core.New(config) - if err != nil { - return -1, fmt.Errorf("instance creation failed: %w", err) - } - - if err := inst.Start(); err != nil { - return -1, fmt.Errorf("startup failed: %w", err) - } - defer inst.Close() - return measureInstDelay(context.Background(), inst, url) -} - // CheckVersionX returns the library and Xray versions func CheckVersionX() string { return fmt.Sprintf("Lib v%d, Xray-core v%s", libVersion, core.Version()) @@ -282,94 +186,6 @@ func (x *CoreController) doStartLoop(configContent string) error { return nil } -// measureInstDelay measures the delay for an instance to a given URL -func measureInstDelay(ctx context.Context, inst *core.Instance, url string) (int64, error) { - if inst == nil { - return -1, errors.New("core instance is nil") - } - - if url == "" { - url = "https://www.google.com/generate_204" - } - - tr := &http.Transport{ - TLSHandshakeTimeout: 6 * time.Second, - DisableKeepAlives: false, - DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - dest, err := corenet.ParseDestination(fmt.Sprintf("%s:%s", network, addr)) - if err != nil { - return nil, err - } - return core.Dial(ctx, inst, dest) - }, - } - - client := &http.Client{ - Transport: tr, - Timeout: 12 * time.Second, - } - - var minDuration int64 = -1 - success := false - var lastErr error - - // Close idle connections to ensure the temporary instance can be closed safely - defer tr.CloseIdleConnections() - - // Add exception handling and increase retry attempts - const attempts = 2 - for i := 0; i < attempts; i++ { - select { - case <-ctx.Done(): - // Return immediately when context is canceled - if !success { - return -1, ctx.Err() - } - return minDuration, nil - default: - // Continue execution - } - - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) - if err != nil { - lastErr = fmt.Errorf("failed to create HTTP request: %w", err) - continue - } - - start := time.Now() - resp, err := client.Do(req) - if err != nil { - lastErr = err - continue - } - - // Read and close body immediately to allow connection reuse for the next attempt - _, err = io.Copy(io.Discard, resp.Body) - resp.Body.Close() - - if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { - lastErr = fmt.Errorf("invalid status: %s", resp.Status) - continue - } - - if err != nil { - lastErr = fmt.Errorf("failed to read response body: %w", err) - continue - } - - duration := time.Since(start).Milliseconds() - if !success || duration < minDuration { - minDuration = duration - } - - success = true - } - if !success { - return -1, lastErr - } - return minDuration, nil -} - // Log writer implementation func (w *consoleLogWriter) Write(s string) error { w.logger.Print(s) diff --git a/libv2ray_utils.go b/libv2ray_utils.go new file mode 100644 index 0000000..342e516 --- /dev/null +++ b/libv2ray_utils.go @@ -0,0 +1,176 @@ +package libv2ray + +import ( + "context" + "errors" + "fmt" + "io" + "net" + "net/http" + "strconv" + "strings" + "time" + + corenet "github.com/xtls/xray-core/common/net" + "github.com/xtls/xray-core/common/serial" + core "github.com/xtls/xray-core/core" + corestats "github.com/xtls/xray-core/features/stats" + coreserial "github.com/xtls/xray-core/infra/conf/serial" +) + +// QueryAllOutboundTrafficStats retrieves and resets all outbound traffic counters. +// Returns a single-line text in format: tag,direction,value;tag,direction,value; +// Returns an empty string if the stats manager is not initialized or no counters exist. +func (x *CoreController) QueryAllOutboundTrafficStats() string { + if x.statsManager == nil { + return "" + } + + var b strings.Builder + + x.statsManager.VisitCounters(func(name string, counter corestats.Counter) bool { + parts := strings.Split(name, ">>>") + if len(parts) != 4 || parts[0] != "outbound" || parts[2] != "traffic" { + return true + } + + tag := parts[1] + direct := parts[3] + value := counter.Set(0) + if value <= 0 { + return true + } + + b.WriteString(tag) + b.WriteByte(',') + b.WriteString(direct) + b.WriteByte(',') + b.WriteString(strconv.FormatInt(value, 10)) + b.WriteByte(';') + return true + }) + return b.String() +} + +// MeasureDelay measures network latency to a specified URL through the current core instance +// Uses a 12-second timeout context and returns the round-trip time in milliseconds +// An error is returned if the connection fails or returns an unexpected status +func (x *CoreController) MeasureDelay(url string) (int64, error) { + ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second) + defer cancel() + + return measureInstDelay(ctx, x.coreInstance, url) +} + +// MeasureOutboundDelay measures the outbound delay for a given configuration and URL +func MeasureOutboundDelay(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) + } + + config.Inbound = nil + var essentialApp []*serial.TypedMessage + for _, app := range config.App { + if app.Type == "xray.app.proxyman.OutboundConfig" || + app.Type == "xray.app.dispatcher.Config" || + app.Type == "xray.app.log.Config" { + essentialApp = append(essentialApp, app) + } + } + config.App = essentialApp + + inst, err := core.New(config) + if err != nil { + return -1, fmt.Errorf("instance creation failed: %w", err) + } + + if err := inst.Start(); err != nil { + return -1, fmt.Errorf("startup failed: %w", err) + } + defer inst.Close() + return measureInstDelay(context.Background(), inst, url) +} + +// measureInstDelay measures the delay for an instance to a given URL +func measureInstDelay(ctx context.Context, inst *core.Instance, url string) (int64, error) { + if inst == nil { + return -1, errors.New("core instance is nil") + } + + if url == "" { + url = "https://www.google.com/generate_204" + } + + tr := &http.Transport{ + TLSHandshakeTimeout: 6 * time.Second, + DisableKeepAlives: false, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + dest, err := corenet.ParseDestination(fmt.Sprintf("%s:%s", network, addr)) + if err != nil { + return nil, err + } + return core.Dial(ctx, inst, dest) + }, + } + + client := &http.Client{ + Transport: tr, + Timeout: 12 * time.Second, + } + + var minDuration int64 = -1 + success := false + var lastErr error + + defer tr.CloseIdleConnections() + + const attempts = 2 + for i := 0; i < attempts; i++ { + select { + case <-ctx.Done(): + if !success { + return -1, ctx.Err() + } + return minDuration, nil + default: + } + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + lastErr = fmt.Errorf("failed to create HTTP request: %w", err) + continue + } + + start := time.Now() + resp, err := client.Do(req) + if err != nil { + lastErr = err + continue + } + + _, err = io.Copy(io.Discard, resp.Body) + resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { + lastErr = fmt.Errorf("invalid status: %s", resp.Status) + continue + } + + if err != nil { + lastErr = fmt.Errorf("failed to read response body: %w", err) + continue + } + + duration := time.Since(start).Milliseconds() + if !success || duration < minDuration { + minDuration = duration + } + + success = true + } + if !success { + return -1, lastErr + } + return minDuration, nil +}