From 938e2b4f06988e052535b431018eceefa603b7f9 Mon Sep 17 00:00:00 2001 From: yiguo Date: Sat, 4 Jul 2026 11:02:19 +0800 Subject: [PATCH 1/7] Document layered invoke env behavior --- README.md | 12 ++++++++++-- invoke.go | 9 ++++++--- invoke_test.go | 35 ++++++++++++++++++++++++++++++++++- readme/README.zh_CN.md | 10 ++++++++-- 4 files changed, 58 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 0cfadd07..01bbb879 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,12 @@ The response is a JSON object: ``` `env` is optional and only supports Xray-core environment variables that are -explicitly modeled by libXray: +explicitly modeled by libXray. Passing `env` sets process environment variables +and reloads Xray-core's process-global env-backed settings before the method +continues. It is primarily used for load-stage keys and runtime values that are +not known when the Xray JSON is generated. Reloadable runtime keys can also be +declared in the Xray config root `env` object, which is applied during config +build and overrides same-name external values. | JSON key | Meaning | | --- | --- | @@ -178,7 +183,10 @@ Design notes: 2. Unknown `env` keys are ignored and are not written to the process environment. 3. `env` only sets modeled, non-empty fields. Missing fields are not unset. 4. libXray does not restore previous environment values after a method returns. Callers must pass the required env fields on every request that depends on them. This avoids concurrent calls restoring stale values over newer values. -5. `SetTunFd` has been removed. Pass `xray.tun.fd` in the `env` object of the `runXray` request. +5. `xray.json.strict`, `xray.location.config`, and `xray.location.confdir` are load-stage keys and must stay outside the Xray JSON config. +6. Xray config root `env` does not accept those three load-stage keys. Other reloadable keys declared there override same-name values from `Invoke.env`. +7. `env` reloads Xray-core process-global env-backed settings. It does not provide per-instance environment isolation. +8. `SetTunFd` has been removed. Pass `xray.tun.fd` in the `env` object of the `runXray` request when the fd is only known at runtime. Supported methods: diff --git a/invoke.go b/invoke.go index 6f04737a..e729b7d3 100644 --- a/invoke.go +++ b/invoke.go @@ -26,7 +26,9 @@ func Invoke(requestJSON string) string { if err := validateAPIVersion(request.APIVersion); err != nil { return encodeInvokeResponse(nil, err) } - applyEnv(request.Env) + if err := applyEnv(request.Env); err != nil { + return encodeInvokeResponse(nil, err) + } switch request.Method { case LibXrayMethodGetFreePorts: @@ -63,9 +65,9 @@ func validateAPIVersion(version int) error { return errors.New("unsupported apiVersion") } -func applyEnv(env *LibXrayEnvJson) { +func applyEnv(env *LibXrayEnvJson) error { if env == nil { - return + return nil } setEnvIfNotEmpty(platform.ConfigLocation, env.ConfigLocation) setEnvIfNotEmpty(platform.ConfdirLocation, env.ConfdirLocation) @@ -81,6 +83,7 @@ func applyEnv(env *LibXrayEnvJson) { setEnvIfNotEmpty(platform.XUDPLog, env.XUDPLog) setEnvIfNotEmpty(platform.XUDPBaseKey, env.XUDPBaseKey) setEnvIfNotEmpty(platform.TunFdKey, env.TunFd) + return platform.ReloadEnvSettings() } func setEnvIfNotEmpty(key string, value string) { diff --git a/invoke_test.go b/invoke_test.go index b810f22d..53179afa 100644 --- a/invoke_test.go +++ b/invoke_test.go @@ -1,6 +1,7 @@ package libXray import ( + "bytes" "encoding/base64" "encoding/json" "os" @@ -8,6 +9,7 @@ import ( "testing" "github.com/xtls/xray-core/common/platform" + "github.com/xtls/xray-core/infra/conf/serial" ) type testResponse struct { @@ -282,7 +284,7 @@ func TestInvokeSetsAllSupportedEnvFields(t *testing.T) { {"buffer size", platform.BufferSize, "buffer-size-value"}, {"browser dialer", platform.BrowserDialerAddress, "browser-dialer-value"}, {"xudp log", platform.XUDPLog, "xudp-log-value"}, - {"xudp base key", platform.XUDPBaseKey, "xudp-base-key-value"}, + {"xudp base key", platform.XUDPBaseKey, base64.RawURLEncoding.EncodeToString(bytes.Repeat([]byte{7}, 32))}, {"tun fd", platform.TunFdKey, "123"}, } for _, tt := range tests { @@ -303,6 +305,37 @@ func TestInvokeSetsAllSupportedEnvFields(t *testing.T) { } } +func TestInvokeEnvReloadsXrayCoreSettings(t *testing.T) { + original, existed := os.LookupEnv(platform.UseStrictJSON) + _ = os.Setenv(platform.UseStrictJSON, "false") + platform.ReloadEnvSettings() + t.Cleanup(func() { + if existed { + _ = os.Setenv(platform.UseStrictJSON, original) + } else { + _ = os.Unsetenv(platform.UseStrictJSON) + } + platform.ReloadEnvSettings() + }) + + if serial.IsStrictJSONEnabled() { + t.Fatal("strict JSON should start disabled") + } + + response := invokeForTest( + t, + LibXrayMethodXrayVersion, + &LibXrayEnvJson{UseStrictJSON: "true"}, + nil, + ) + if !response.Success { + t.Fatalf("XrayVersion failed: %s", response.Err) + } + if !serial.IsStrictJSONEnabled() { + t.Fatal("Invoke.env did not reload strict JSON setting") + } +} + func TestInvokeUnknownMethod(t *testing.T) { response := invokeForTest(t, LibXrayMethod("unknown"), nil, nil) if response.Success { diff --git a/readme/README.zh_CN.md b/readme/README.zh_CN.md index 5adda226..34734f17 100644 --- a/readme/README.zh_CN.md +++ b/readme/README.zh_CN.md @@ -120,7 +120,10 @@ char* CGoInvoke(char* requestJSON); } ``` -`env` 是可选字段,只支持 libXray 显式建模的 Xray-core 环境变量: +`env` 是可选字段,只支持 libXray 显式建模的 Xray-core 环境变量。传入 `env` +会写入进程环境变量,并在 method 继续执行前 reload Xray-core 的进程全局 env-backed settings。 +它主要用于加载前 key,以及生成 Xray JSON 时还不知道的运行时值。可 reload 的运行时 key +也可以写在 Xray 配置根 `env` 对象中;配置根 `env` 会在 config build 阶段应用,并覆盖同名外部值。 | JSON key | 含义 | | --- | --- | @@ -145,7 +148,10 @@ char* CGoInvoke(char* requestJSON); 2. 未知 `env` key 会被忽略,不会写入进程环境变量。 3. `env` 只设置已建模的非空字段,缺失字段不会 unset。 4. libXray 不会在 method 结束后 restore 旧环境变量。调用方必须在每次依赖环境变量的请求中显式传入对应字段。这样可以避免并发调用时,一个请求恢复旧值覆盖另一个请求的新值。 -5. `SetTunFd` 已删除。请在 `runXray` 请求的 `env` 对象中传入 `xray.tun.fd`。 +5. `xray.json.strict`、`xray.location.config`、`xray.location.confdir` 属于加载前 key,必须保留在 Xray JSON 配置外部。 +6. Xray 配置根 `env` 不接受上述三个加载前 key;其他可 reload key 写在配置根 `env` 时会覆盖 `Invoke.env` 中的同名值。 +7. `env` reload 的是 Xray-core 进程全局 env-backed settings,不提供 per-instance 环境隔离。 +8. `SetTunFd` 已删除。如果 fd 只能在运行时获得,请在 `runXray` 请求的 `env` 对象中传入 `xray.tun.fd`。 支持的 method: From b696fb7efe87470a2093ec731819803ad8d2450e Mon Sep 17 00:00:00 2001 From: yiguo Date: Sat, 4 Jul 2026 12:04:30 +0800 Subject: [PATCH 2/7] Remove Invoke env support --- README.md | 48 +++----------- download_geo/main.go | 7 +- invoke.go | 39 +----------- invoke_model.go | 19 +----- invoke_test.go | 141 +++++++++++------------------------------ readme/README.zh_CN.md | 40 ++---------- 6 files changed, 57 insertions(+), 237 deletions(-) diff --git a/README.md b/README.md index 01bbb879..f0ff34f4 100644 --- a/README.md +++ b/README.md @@ -130,12 +130,6 @@ The request is a JSON object: { "apiVersion": 1, "method": "runXray", - "env": { - "xray.location.config": "/path/to/config.json", - "xray.location.asset": "/path/to/dat", - "xray.location.cert": "/path/to/dat", - "xray.tun.fd": "123" - }, "payload": { "configPath": "/path/to/config.json" } @@ -152,41 +146,17 @@ The response is a JSON object: } ``` -`env` is optional and only supports Xray-core environment variables that are -explicitly modeled by libXray. Passing `env` sets process environment variables -and reloads Xray-core's process-global env-backed settings before the method -continues. It is primarily used for load-stage keys and runtime values that are -not known when the Xray JSON is generated. Reloadable runtime keys can also be -declared in the Xray config root `env` object, which is applied during config -build and overrides same-name external values. - -| JSON key | Meaning | -| --- | --- | -| `xray.location.config` | Xray config file location | -| `xray.location.confdir` | Xray config directory location | -| `xray.location.asset` | Directory containing `geosite.dat`, `geoip.dat`, and custom GeoData files | -| `xray.location.cert` | Certificate directory used by Xray-core | -| `xray.buf.readv` | Xray-core readv buffer switch | -| `xray.buf.splice` | Xray-core splice buffer switch | -| `xray.vmess.padding` | VMess padding switch | -| `xray.cone.disabled` | Cone behavior switch | -| `xray.json.strict` | Strict JSON parsing switch | -| `xray.ray.buffer.size` | Ray buffer size | -| `xray.browser.dialer` | Browser dialer address | -| `xray.xudp.show` | XUDP log display switch | -| `xray.xudp.basekey` | XUDP base key | -| `xray.tun.fd` | TUN file descriptor for Android, iOS, and macOS packet tunnel integrations | - Design notes: -1. `env` is modeled as fixed fields in Go and Dart. It is not a free-form map. -2. Unknown `env` keys are ignored and are not written to the process environment. -3. `env` only sets modeled, non-empty fields. Missing fields are not unset. -4. libXray does not restore previous environment values after a method returns. Callers must pass the required env fields on every request that depends on them. This avoids concurrent calls restoring stale values over newer values. -5. `xray.json.strict`, `xray.location.config`, and `xray.location.confdir` are load-stage keys and must stay outside the Xray JSON config. -6. Xray config root `env` does not accept those three load-stage keys. Other reloadable keys declared there override same-name values from `Invoke.env`. -7. `env` reloads Xray-core process-global env-backed settings. It does not provide per-instance environment isolation. -8. `SetTunFd` has been removed. Pass `xray.tun.fd` in the `env` object of the `runXray` request when the fd is only known at runtime. +1. `Invoke` does not accept an `env` field. Runtime Xray-core environment + settings must be written into the Xray config root `env` object. +2. `xray.json.strict`, `xray.location.config`, and `xray.location.confdir` are + load-stage process environment variables and cannot be supplied through + `Invoke`. +3. `SetTunFd` has been removed. Write `xray.tun.fd` into the Xray config root + `env` object before calling `runXray` when the fd is only known at runtime. +4. `countGeoData` is not backed by an Xray config, so its `datDir` is passed in + the method payload. Supported methods: diff --git a/download_geo/main.go b/download_geo/main.go index b202c126..d625ecb3 100644 --- a/download_geo/main.go +++ b/download_geo/main.go @@ -66,6 +66,7 @@ func makeLoadGeoDataRequest(datDir string, name string, geoType string) (string, payload, err := json.Marshal(&libXray.CountGeoDataRequest{ Name: name, GeoType: geoType, + DatDir: datDir, }) if err != nil { return "", err @@ -73,11 +74,7 @@ func makeLoadGeoDataRequest(datDir string, name string, geoType string) (string, request := libXray.LibXrayInvokeRequest{ APIVersion: 1, Method: libXray.LibXrayMethodCountGeoData, - Env: &libXray.LibXrayEnvJson{ - AssetLocation: datDir, - CertLocation: datDir, - }, - Payload: payload, + Payload: payload, } data, err := json.Marshal(&request) if err != nil { diff --git a/invoke.go b/invoke.go index e729b7d3..6f1cbe6e 100644 --- a/invoke.go +++ b/invoke.go @@ -3,13 +3,11 @@ package libXray import ( "encoding/json" "errors" - "os" "github.com/xtls/libxray/geo" "github.com/xtls/libxray/nodep" "github.com/xtls/libxray/share" "github.com/xtls/libxray/xray" - "github.com/xtls/xray-core/common/platform" ) type invokeResponse struct { @@ -26,9 +24,6 @@ func Invoke(requestJSON string) string { if err := validateAPIVersion(request.APIVersion); err != nil { return encodeInvokeResponse(nil, err) } - if err := applyEnv(request.Env); err != nil { - return encodeInvokeResponse(nil, err) - } switch request.Method { case LibXrayMethodGetFreePorts: @@ -65,33 +60,6 @@ func validateAPIVersion(version int) error { return errors.New("unsupported apiVersion") } -func applyEnv(env *LibXrayEnvJson) error { - if env == nil { - return nil - } - setEnvIfNotEmpty(platform.ConfigLocation, env.ConfigLocation) - setEnvIfNotEmpty(platform.ConfdirLocation, env.ConfdirLocation) - setEnvIfNotEmpty(platform.AssetLocation, env.AssetLocation) - setEnvIfNotEmpty(platform.CertLocation, env.CertLocation) - setEnvIfNotEmpty(platform.UseReadV, env.UseReadV) - setEnvIfNotEmpty(platform.UseFreedomSplice, env.UseFreedomSplice) - setEnvIfNotEmpty(platform.UseVmessPadding, env.UseVmessPadding) - setEnvIfNotEmpty(platform.UseCone, env.UseCone) - setEnvIfNotEmpty(platform.UseStrictJSON, env.UseStrictJSON) - setEnvIfNotEmpty(platform.BufferSize, env.BufferSize) - setEnvIfNotEmpty(platform.BrowserDialerAddress, env.BrowserDialerAddress) - setEnvIfNotEmpty(platform.XUDPLog, env.XUDPLog) - setEnvIfNotEmpty(platform.XUDPBaseKey, env.XUDPBaseKey) - setEnvIfNotEmpty(platform.TunFdKey, env.TunFd) - return platform.ReloadEnvSettings() -} - -func setEnvIfNotEmpty(key string, value string) { - if value != "" { - _ = os.Setenv(key, value) - } -} - func decodePayload[T any](payload json.RawMessage) (T, error) { var request T if len(payload) == 0 { @@ -161,11 +129,10 @@ func invokeCountGeoData(payload json.RawMessage) string { if err != nil { return encodeInvokeNoDataResponse(err) } - datDir := platform.NewEnvFlag(platform.AssetLocation).GetValue(func() string { return "" }) - if datDir == "" { - return encodeInvokeNoDataResponse(errors.New("missing xray.location.asset")) + if request.DatDir == "" { + return encodeInvokeNoDataResponse(errors.New("missing datDir")) } - err = geo.CountGeoData(datDir, request.Name, request.GeoType) + err = geo.CountGeoData(request.DatDir, request.Name, request.GeoType) return encodeInvokeNoDataResponse(err) } diff --git a/invoke_model.go b/invoke_model.go index 5f369a94..66d2732d 100644 --- a/invoke_model.go +++ b/invoke_model.go @@ -22,27 +22,9 @@ const ( type LibXrayInvokeRequest struct { APIVersion int `json:"apiVersion,omitempty"` Method LibXrayMethod `json:"method,omitempty"` - Env *LibXrayEnvJson `json:"env,omitempty"` Payload json.RawMessage `json:"payload,omitempty"` } -type LibXrayEnvJson struct { - ConfigLocation string `json:"xray.location.config,omitempty"` - ConfdirLocation string `json:"xray.location.confdir,omitempty"` - AssetLocation string `json:"xray.location.asset,omitempty"` - CertLocation string `json:"xray.location.cert,omitempty"` - UseReadV string `json:"xray.buf.readv,omitempty"` - UseFreedomSplice string `json:"xray.buf.splice,omitempty"` - UseVmessPadding string `json:"xray.vmess.padding,omitempty"` - UseCone string `json:"xray.cone.disabled,omitempty"` - UseStrictJSON string `json:"xray.json.strict,omitempty"` - BufferSize string `json:"xray.ray.buffer.size,omitempty"` - BrowserDialerAddress string `json:"xray.browser.dialer,omitempty"` - XUDPLog string `json:"xray.xudp.show,omitempty"` - XUDPBaseKey string `json:"xray.xudp.basekey,omitempty"` - TunFd string `json:"xray.tun.fd,omitempty"` -} - type GetFreePortsRequest struct { Count int `json:"count,omitempty"` } @@ -66,6 +48,7 @@ type ConvertXrayJsonToShareLinksResponse struct { type CountGeoDataRequest struct { Name string `json:"name,omitempty"` GeoType string `json:"geoType,omitempty"` + DatDir string `json:"datDir,omitempty"` } type PingRequest struct { diff --git a/invoke_test.go b/invoke_test.go index 53179afa..d6d9e1d0 100644 --- a/invoke_test.go +++ b/invoke_test.go @@ -1,15 +1,11 @@ package libXray import ( - "bytes" "encoding/base64" "encoding/json" "os" "path/filepath" "testing" - - "github.com/xtls/xray-core/common/platform" - "github.com/xtls/xray-core/infra/conf/serial" ) type testResponse struct { @@ -18,7 +14,7 @@ type testResponse struct { Err string `json:"error,omitempty"` } -func invokeForTest(t *testing.T, method LibXrayMethod, env *LibXrayEnvJson, payload any) testResponse { +func invokeForTest(t *testing.T, method LibXrayMethod, payload any) testResponse { t.Helper() rawPayload, err := json.Marshal(payload) if err != nil { @@ -27,7 +23,6 @@ func invokeForTest(t *testing.T, method LibXrayMethod, env *LibXrayEnvJson, payl rawRequest, err := json.Marshal(&LibXrayInvokeRequest{ APIVersion: 1, Method: method, - Env: env, Payload: rawPayload, }) if err != nil { @@ -87,6 +82,20 @@ func writeConfigToFile(t *testing.T, config any, path string) { } } +func copyFileForTest(t *testing.T, src string, dst string) { + t.Helper() + data, err := os.ReadFile(src) + if err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Dir(dst), os.ModePerm); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(dst, data, 0o644); err != nil { + t.Fatal(err) + } +} + func testVmessPayloadBase64() string { const vmessJSON = `{"add":"127.0.0.1","port":1080,"id":"00000000-0000-0000-0000-000000000000","aid":0,"scy":"auto"}` return base64.StdEncoding.EncodeToString([]byte(vmessJSON)) @@ -173,7 +182,6 @@ func TestInvokeTestXray(t *testing.T) { response := invokeForTest( t, LibXrayMethodTestXray, - &LibXrayEnvJson{AssetLocation: filepath.Join(projectRoot, "dat"), CertLocation: filepath.Join(projectRoot, "dat")}, RunXrayRequest{ConfigPath: configPath}, ) if !response.Success { @@ -190,7 +198,6 @@ func TestInvokeRunXray(t *testing.T) { response := invokeForTest( t, LibXrayMethodRunXray, - &LibXrayEnvJson{AssetLocation: filepath.Join(projectRoot, "dat"), CertLocation: filepath.Join(projectRoot, "dat")}, RunXrayRequest{ConfigPath: configPath}, ) defer xrayStopForTest(t) @@ -201,7 +208,7 @@ func TestInvokeRunXray(t *testing.T) { } func TestInvokeXrayVersion(t *testing.T) { - response := invokeForTest(t, LibXrayMethodXrayVersion, nil, nil) + response := invokeForTest(t, LibXrayMethodXrayVersion, nil) if !response.Success { t.Fatalf("XrayVersion failed: %s", response.Err) } @@ -212,7 +219,7 @@ func TestInvokeXrayVersion(t *testing.T) { } func TestInvokeMapResponseShape(t *testing.T) { - response := invokeForTest(t, LibXrayMethodGetFreePorts, nil, GetFreePortsRequest{Count: 1}) + response := invokeForTest(t, LibXrayMethodGetFreePorts, GetFreePortsRequest{Count: 1}) if !response.Success { t.Fatalf("GetFreePorts failed: %s", response.Err) } @@ -221,7 +228,7 @@ func TestInvokeMapResponseShape(t *testing.T) { t.Fatalf("ports = %v", ports.Ports) } - response = invokeForTest(t, LibXrayMethodGetXrayState, nil, nil) + response = invokeForTest(t, LibXrayMethodGetXrayState, nil) if !response.Success { t.Fatalf("GetXrayState failed: %s", response.Err) } @@ -234,7 +241,6 @@ func TestInvokeMapResponseShape(t *testing.T) { response = invokeForTest( t, LibXrayMethodConvertXrayJsonToShareLinks, - nil, ConvertXrayJsonToShareLinksRequest{XrayJson: string(rawConfig)}, ) if !response.Success { @@ -246,98 +252,31 @@ func TestInvokeMapResponseShape(t *testing.T) { } } -func TestInvokeEnvOnlySetsProvidedFields(t *testing.T) { - t.Setenv(platform.AssetLocation, "initial-asset") - t.Setenv(platform.CertLocation, "initial-cert") - response := invokeForTest( - t, - LibXrayMethodXrayVersion, - &LibXrayEnvJson{AssetLocation: "updated-asset"}, - nil, - ) - if !response.Success { - t.Fatalf("XrayVersion failed: %s", response.Err) - } - if got := os.Getenv(platform.AssetLocation); got != "updated-asset" { - t.Fatalf("asset env = %q", got) - } - if got := os.Getenv(platform.CertLocation); got != "initial-cert" { - t.Fatalf("cert env = %q", got) - } -} - -func TestInvokeSetsAllSupportedEnvFields(t *testing.T) { - tests := []struct { - name string - key string - value string - }{ - {"config", platform.ConfigLocation, "config-value"}, - {"confdir", platform.ConfdirLocation, "confdir-value"}, - {"asset", platform.AssetLocation, "asset-value"}, - {"cert", platform.CertLocation, "cert-value"}, - {"readv", platform.UseReadV, "readv-value"}, - {"splice", platform.UseFreedomSplice, "splice-value"}, - {"vmess padding", platform.UseVmessPadding, "vmess-padding-value"}, - {"cone", platform.UseCone, "cone-value"}, - {"strict json", platform.UseStrictJSON, "strict-json-value"}, - {"buffer size", platform.BufferSize, "buffer-size-value"}, - {"browser dialer", platform.BrowserDialerAddress, "browser-dialer-value"}, - {"xudp log", platform.XUDPLog, "xudp-log-value"}, - {"xudp base key", platform.XUDPBaseKey, base64.RawURLEncoding.EncodeToString(bytes.Repeat([]byte{7}, 32))}, - {"tun fd", platform.TunFdKey, "123"}, - } - for _, tt := range tests { - t.Setenv(tt.key, "") - t.Run(tt.name, func(t *testing.T) { - requestJSON := `{"apiVersion":1,"method":"xrayVersion","env":{"` + tt.key + `":"` + tt.value + `"}}` - var response testResponse - if err := json.Unmarshal([]byte(Invoke(requestJSON)), &response); err != nil { - t.Fatal(err) - } - if !response.Success { - t.Fatalf("XrayVersion failed: %s", response.Err) - } - if got := os.Getenv(tt.key); got != tt.value { - t.Fatalf("%s = %q", tt.key, got) - } - }) - } -} - -func TestInvokeEnvReloadsXrayCoreSettings(t *testing.T) { - original, existed := os.LookupEnv(platform.UseStrictJSON) - _ = os.Setenv(platform.UseStrictJSON, "false") - platform.ReloadEnvSettings() - t.Cleanup(func() { - if existed { - _ = os.Setenv(platform.UseStrictJSON, original) - } else { - _ = os.Unsetenv(platform.UseStrictJSON) - } - platform.ReloadEnvSettings() - }) - - if serial.IsStrictJSONEnabled() { - t.Fatal("strict JSON should start disabled") - } +func TestInvokeCountGeoDataUsesPayloadDatDir(t *testing.T) { + projectRoot, _ := filepath.Abs(".") + datDir := t.TempDir() + copyFileForTest(t, filepath.Join(projectRoot, "dat", "geosite.dat"), filepath.Join(datDir, "geosite.dat")) response := invokeForTest( t, - LibXrayMethodXrayVersion, - &LibXrayEnvJson{UseStrictJSON: "true"}, - nil, + LibXrayMethodCountGeoData, + CountGeoDataRequest{ + Name: "geosite", + GeoType: "domain", + DatDir: datDir, + }, ) if !response.Success { - t.Fatalf("XrayVersion failed: %s", response.Err) + t.Fatalf("CountGeoData failed: %s", response.Err) } - if !serial.IsStrictJSONEnabled() { - t.Fatal("Invoke.env did not reload strict JSON setting") + requireNoDataObject(t, response) + if _, err := os.Stat(filepath.Join(datDir, "geosite.json")); err != nil { + t.Fatal(err) } } func TestInvokeUnknownMethod(t *testing.T) { - response := invokeForTest(t, LibXrayMethod("unknown"), nil, nil) + response := invokeForTest(t, LibXrayMethod("unknown"), nil) if response.Success { t.Fatal("unknown method should fail") } @@ -349,7 +288,6 @@ func TestInvokeAPIVersion(t *testing.T) { t.Fatalf("omitted apiVersion should default to v1: %s", response.Err) } - t.Setenv(platform.AssetLocation, "initial-asset") response = invokeRawForTest(t, `{"apiVersion":2,"method":"xrayVersion","env":{"xray.location.asset":"updated-asset"}}`) if response.Success { t.Fatal("unsupported apiVersion should fail") @@ -357,13 +295,10 @@ func TestInvokeAPIVersion(t *testing.T) { if got := string(response.Data); got != "null" { t.Fatalf("data = %s, want null", got) } - if got := os.Getenv(platform.AssetLocation); got != "initial-asset" { - t.Fatalf("asset env = %q", got) - } } func TestInvokeNoDataResponseShape(t *testing.T) { - response := invokeForTest(t, LibXrayMethodStopXray, nil, nil) + response := invokeForTest(t, LibXrayMethodStopXray, nil) if !response.Success { t.Fatalf("StopXray failed: %s", response.Err) } @@ -378,7 +313,7 @@ func TestInvokeNoDataResponseShape(t *testing.T) { } } -func TestInvokeIgnoresUnknownEnvField(t *testing.T) { +func TestInvokeIgnoresTopLevelEnv(t *testing.T) { const key = "XRAY_LIBXRAY_UNKNOWN_ENV_TEST" _ = os.Unsetenv(key) t.Cleanup(func() { _ = os.Unsetenv(key) }) @@ -388,16 +323,16 @@ func TestInvokeIgnoresUnknownEnvField(t *testing.T) { t.Fatal(err) } if !response.Success { - t.Fatalf("unknown env field should be ignored: %s", response.Err) + t.Fatalf("top-level env should be ignored: %s", response.Err) } if _, found := os.LookupEnv(key); found { - t.Fatal("unknown env field should not be set") + t.Fatal("top-level env should not be set") } } func xrayStopForTest(t *testing.T) { t.Helper() - response := invokeForTest(t, LibXrayMethodStopXray, nil, nil) + response := invokeForTest(t, LibXrayMethodStopXray, nil) if !response.Success { t.Fatalf("StopXray failed: %s", response.Err) } diff --git a/readme/README.zh_CN.md b/readme/README.zh_CN.md index 34734f17..ede80bd3 100644 --- a/readme/README.zh_CN.md +++ b/readme/README.zh_CN.md @@ -98,12 +98,6 @@ char* CGoInvoke(char* requestJSON); { "apiVersion": 1, "method": "runXray", - "env": { - "xray.location.config": "/path/to/config.json", - "xray.location.asset": "/path/to/dat", - "xray.location.cert": "/path/to/dat", - "xray.tun.fd": "123" - }, "payload": { "configPath": "/path/to/config.json" } @@ -120,38 +114,12 @@ char* CGoInvoke(char* requestJSON); } ``` -`env` 是可选字段,只支持 libXray 显式建模的 Xray-core 环境变量。传入 `env` -会写入进程环境变量,并在 method 继续执行前 reload Xray-core 的进程全局 env-backed settings。 -它主要用于加载前 key,以及生成 Xray JSON 时还不知道的运行时值。可 reload 的运行时 key -也可以写在 Xray 配置根 `env` 对象中;配置根 `env` 会在 config build 阶段应用,并覆盖同名外部值。 - -| JSON key | 含义 | -| --- | --- | -| `xray.location.config` | Xray 配置文件路径 | -| `xray.location.confdir` | Xray 配置目录路径 | -| `xray.location.asset` | 保存 `geosite.dat`、`geoip.dat` 和自定义 GeoData 的目录 | -| `xray.location.cert` | Xray-core 使用的证书目录 | -| `xray.buf.readv` | Xray-core readv buffer 开关 | -| `xray.buf.splice` | Xray-core splice buffer 开关 | -| `xray.vmess.padding` | VMess padding 开关 | -| `xray.cone.disabled` | Cone 行为开关 | -| `xray.json.strict` | 严格 JSON 解析开关 | -| `xray.ray.buffer.size` | Ray buffer size | -| `xray.browser.dialer` | Browser dialer 地址 | -| `xray.xudp.show` | XUDP 日志显示开关 | -| `xray.xudp.basekey` | XUDP base key | -| `xray.tun.fd` | Android、iOS、macOS Packet Tunnel 使用的 TUN 文件描述符 | - 设计决定: -1. `env` 在 Go 和 Dart 侧都是固定字段 model,不是自由 map。 -2. 未知 `env` key 会被忽略,不会写入进程环境变量。 -3. `env` 只设置已建模的非空字段,缺失字段不会 unset。 -4. libXray 不会在 method 结束后 restore 旧环境变量。调用方必须在每次依赖环境变量的请求中显式传入对应字段。这样可以避免并发调用时,一个请求恢复旧值覆盖另一个请求的新值。 -5. `xray.json.strict`、`xray.location.config`、`xray.location.confdir` 属于加载前 key,必须保留在 Xray JSON 配置外部。 -6. Xray 配置根 `env` 不接受上述三个加载前 key;其他可 reload key 写在配置根 `env` 时会覆盖 `Invoke.env` 中的同名值。 -7. `env` reload 的是 Xray-core 进程全局 env-backed settings,不提供 per-instance 环境隔离。 -8. `SetTunFd` 已删除。如果 fd 只能在运行时获得,请在 `runXray` 请求的 `env` 对象中传入 `xray.tun.fd`。 +1. `Invoke` 不再接受 `env` 字段。运行时 Xray-core 环境项必须写入 Xray 配置根 `env` 对象。 +2. `xray.json.strict`、`xray.location.config`、`xray.location.confdir` 属于加载前进程环境变量,不能通过 `Invoke` 传入。 +3. `SetTunFd` 已删除。如果 fd 只能在运行时获得,请在调用 `runXray` 前把 `xray.tun.fd` 写入 Xray 配置根 `env` 对象。 +4. `countGeoData` 不依赖 Xray 配置,因此通过 method payload 的 `datDir` 传入数据目录。 支持的 method: From 23783c4aebcb30567d0c90077cd2bc738aa8b3e5 Mon Sep 17 00:00:00 2001 From: yiguo Date: Mon, 6 Jul 2026 19:09:29 +0800 Subject: [PATCH 3/7] Return ping delay sentinel on failure --- invoke.go | 3 +++ invoke_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/invoke.go b/invoke.go index 6f1cbe6e..793bedd2 100644 --- a/invoke.go +++ b/invoke.go @@ -143,6 +143,9 @@ func invokePing(payload json.RawMessage) string { } delay, err := xray.Ping(request.ConfigPath, request.Timeout, request.URL, request.Proxy) if err != nil { + if delay == nodep.PingDelayError || delay == nodep.PingDelayTimeout { + return encodeInvokeResponse(&PingResponse{Delay: delay}, err) + } return encodeInvokeResponse(nil, err) } return encodeInvokeResponse(&PingResponse{Delay: delay}, nil) diff --git a/invoke_test.go b/invoke_test.go index d6d9e1d0..e956bf11 100644 --- a/invoke_test.go +++ b/invoke_test.go @@ -6,6 +6,8 @@ import ( "os" "path/filepath" "testing" + + "github.com/xtls/libxray/nodep" ) type testResponse struct { @@ -252,6 +254,28 @@ func TestInvokeMapResponseShape(t *testing.T) { } } +func TestInvokePingReturnsDelaySentinelOnXrayError(t *testing.T) { + response := invokeForTest( + t, + LibXrayMethodPing, + PingRequest{ + ConfigPath: filepath.Join(t.TempDir(), "missing.json"), + Timeout: 1, + URL: "https://example.com", + }, + ) + if response.Success { + t.Fatal("Ping should keep failure success state on Xray error") + } + if response.Err == "" { + t.Fatal("Ping failure should keep error text") + } + ping := decodeDataObject[PingResponse](t, response) + if ping.Delay != nodep.PingDelayError { + t.Fatalf("delay = %d, want %d", ping.Delay, nodep.PingDelayError) + } +} + func TestInvokeCountGeoDataUsesPayloadDatDir(t *testing.T) { projectRoot, _ := filepath.Abs(".") datDir := t.TempDir() From 102a418f6009ece342b2dceaab8a9e79d515e0c9 Mon Sep 17 00:00:00 2001 From: yiguo Date: Tue, 7 Jul 2026 20:25:00 +0800 Subject: [PATCH 4/7] Restore invoke env and desktop wrapper --- .github/workflows/build.yml | 2 ++ README.md | 25 +++++++++++------ build/app/build.py | 23 ++++++++++++++++ build/app/linux.py | 2 ++ build/app/windows.py | 2 ++ desktop_bin/main.go | 29 ++++++++++++++++++++ desktop_bin/run.go | 54 +++++++++++++++++++++++++++++++++++++ invoke.go | 19 +++++++++++++ invoke_model.go | 7 +++++ invoke_test.go | 41 +++++++++++++++++++++++++--- readme/README.zh_CN.md | 19 +++++++++---- 11 files changed, 207 insertions(+), 16 deletions(-) create mode 100644 desktop_bin/main.go create mode 100644 desktop_bin/run.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 85493c55..d23b54cb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -147,11 +147,13 @@ jobs: mkdir -p "dist/${{ matrix.artifact }}" cp linux_so/libXray.so "dist/${{ matrix.artifact }}/" cp linux_so/libXray.h "dist/${{ matrix.artifact }}/" + cp bin/xray "dist/${{ matrix.artifact }}/" ;; windows) mkdir -p "dist/${{ matrix.artifact }}" cp windows_dll/libXray.dll "dist/${{ matrix.artifact }}/" cp windows_dll/libXray.h "dist/${{ matrix.artifact }}/" + cp bin/xray.exe "dist/${{ matrix.artifact }}/" ;; android) mkdir -p "dist/${{ matrix.artifact }}" diff --git a/README.md b/README.md index f0ff34f4..ac5e19aa 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,9 @@ depends on git and go. By default, the build script does not clone [Xray-core](https://github.com/XTLS/Xray-core). It uses Go modules and pins Xray-core to tag `v26.6.27` (recorded by Go as the matching pseudo-version). Pass the optional `local` argument to use an existing local checkout at `../Xray-core` through a Go module `replace`. -Linux and Windows builds only produce the libXray shared library. Applications that need a standalone Xray executable should use official Xray-core release binaries. +Linux and Windows builds also produce a small desktop wrapper executable: +`bin/xray` or `bin/xray.exe`. The wrapper accepts `-configPath` pointing to a +`LibXrayInvokeRequest` JSON file whose method is `runXray`. ### Usage @@ -130,6 +132,11 @@ The request is a JSON object: { "apiVersion": 1, "method": "runXray", + "env": { + "xray.location.asset": "/path/to/dat", + "xray.location.cert": "/path/to/dat", + "xray.tun.fd": "123" + }, "payload": { "configPath": "/path/to/config.json" } @@ -148,14 +155,16 @@ The response is a JSON object: Design notes: -1. `Invoke` does not accept an `env` field. Runtime Xray-core environment - settings must be written into the Xray config root `env` object. -2. `xray.json.strict`, `xray.location.config`, and `xray.location.confdir` are +1. `Invoke.env` supports only fixed Xray-core runtime environment keys: + `xray.location.asset`, `xray.location.cert`, and `xray.tun.fd`. +2. Non-empty `env` fields are set on the process before the method runs. + Missing fields are not unset and old values are not restored. +3. `xray.json.strict`, `xray.location.config`, and `xray.location.confdir` are load-stage process environment variables and cannot be supplied through - `Invoke`. -3. `SetTunFd` has been removed. Write `xray.tun.fd` into the Xray config root - `env` object before calling `runXray` when the fd is only known at runtime. -4. `countGeoData` is not backed by an Xray config, so its `datDir` is passed in + `Invoke.env`. +4. `SetTunFd` has been removed. Use `Invoke.env["xray.tun.fd"]` when the fd is + only known at runtime. +5. `countGeoData` is not backed by an Xray config, so its `datDir` is passed in the method payload. Supported methods: diff --git a/build/app/build.py b/build/app/build.py index e7b3166f..be2c62bb 100644 --- a/build/app/build.py +++ b/build/app/build.py @@ -4,6 +4,7 @@ import subprocess from app.cmd import ( + create_dir_if_not_exists, delete_file_if_exists, delete_dir_if_exists, ) @@ -142,6 +143,28 @@ def before_build(self): def build(self): pass + def build_desktop_bin(self, bin_file: str): + bin_dir = os.path.join(self.lib_dir, "bin") + create_dir_if_not_exists(bin_dir) + output_file = os.path.join(bin_dir, bin_file) + run_env = os.environ.copy() + run_env["CGO_ENABLED"] = "0" + + cmd = [ + "go", + "build", + "-trimpath", + "-ldflags", + "-s -w", + f"-o={output_file}", + "./desktop_bin", + ] + os.chdir(self.lib_dir) + print(cmd) + ret = subprocess.run(cmd, env=run_env) + if ret.returncode != 0: + raise Exception("build_desktop_bin failed") + def after_build(self): pass diff --git a/build/app/linux.py b/build/app/linux.py index d0d69ee5..181c1279 100644 --- a/build/app/linux.py +++ b/build/app/linux.py @@ -13,6 +13,7 @@ def __init__(self, build_dir: str, use_local_xray_core: bool = False): create_dir_if_not_exists(self.framework_dir) self.lib_file = "libXray.so" self.lib_header_file = "libXray.h" + self.bin_file = "xray" def before_build(self): super().before_build() @@ -22,6 +23,7 @@ def build(self): self.before_build() self.build_linux() self.after_build() + self.build_desktop_bin(self.bin_file) self.revert_go_env() def build_linux(self): diff --git a/build/app/windows.py b/build/app/windows.py index 7b0353ae..9cb70a45 100644 --- a/build/app/windows.py +++ b/build/app/windows.py @@ -13,6 +13,7 @@ def __init__(self, build_dir: str, use_local_xray_core: bool = False): create_dir_if_not_exists(self.framework_dir) self.lib_file = "libXray.dll" self.lib_header_file = "libXray.h" + self.bin_file = "xray.exe" def before_build(self): super().before_build() @@ -22,6 +23,7 @@ def build(self): self.before_build() self.build_windows() self.after_build() + self.build_desktop_bin(self.bin_file) self.revert_go_env() def build_windows(self): diff --git a/desktop_bin/main.go b/desktop_bin/main.go new file mode 100644 index 00000000..a41c3a4d --- /dev/null +++ b/desktop_bin/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "flag" + "fmt" + "os" + "os/signal" + "runtime" + "runtime/debug" + "syscall" +) + +func main() { + configPath := flag.String("configPath", "config.json", "Path of libXray invoke request json") + flag.Parse() + + if err := runXray(*configPath); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + defer stopXray() + + runtime.GC() + debug.FreeOSMemory() + + osSignals := make(chan os.Signal, 1) + signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM) + <-osSignals +} diff --git a/desktop_bin/run.go b/desktop_bin/run.go new file mode 100644 index 00000000..afcf12fd --- /dev/null +++ b/desktop_bin/run.go @@ -0,0 +1,54 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "os" + + libXray "github.com/xtls/libxray" +) + +type invokeResponse struct { + Success bool `json:"success"` + Err string `json:"error"` +} + +func runXray(configPath string) error { + requestBytes, err := os.ReadFile(configPath) + if err != nil { + return err + } + + var request libXray.LibXrayInvokeRequest + if err := json.Unmarshal(requestBytes, &request); err != nil { + return err + } + if request.Method != libXray.LibXrayMethodRunXray { + return fmt.Errorf("unsupported method %q: desktop_bin only supports runXray", request.Method) + } + + responseText := libXray.Invoke(string(requestBytes)) + var response invokeResponse + if err := json.Unmarshal([]byte(responseText), &response); err != nil { + return err + } + if !response.Success { + if response.Err == "" { + response.Err = "runXray failed" + } + return errors.New(response.Err) + } + return nil +} + +func stopXray() { + requestBytes, err := json.Marshal(&libXray.LibXrayInvokeRequest{ + APIVersion: 1, + Method: libXray.LibXrayMethodStopXray, + }) + if err != nil { + return + } + _ = libXray.Invoke(string(requestBytes)) +} diff --git a/invoke.go b/invoke.go index 793bedd2..50bd9317 100644 --- a/invoke.go +++ b/invoke.go @@ -3,11 +3,13 @@ package libXray import ( "encoding/json" "errors" + "os" "github.com/xtls/libxray/geo" "github.com/xtls/libxray/nodep" "github.com/xtls/libxray/share" "github.com/xtls/libxray/xray" + "github.com/xtls/xray-core/common/platform" ) type invokeResponse struct { @@ -24,6 +26,7 @@ func Invoke(requestJSON string) string { if err := validateAPIVersion(request.APIVersion); err != nil { return encodeInvokeResponse(nil, err) } + applyEnv(request.Env) switch request.Method { case LibXrayMethodGetFreePorts: @@ -53,6 +56,22 @@ func Invoke(requestJSON string) string { } } +func applyEnv(env *LibXrayEnvJson) { + if env == nil { + return + } + setEnvIfNotEmpty(platform.AssetLocation, env.AssetLocation) + setEnvIfNotEmpty(platform.CertLocation, env.CertLocation) + setEnvIfNotEmpty(platform.TunFdKey, env.TunFd) +} + +func setEnvIfNotEmpty(key string, value string) { + if value == "" { + return + } + _ = os.Setenv(key, value) +} + func validateAPIVersion(version int) error { if version == 0 || version == 1 { return nil diff --git a/invoke_model.go b/invoke_model.go index 66d2732d..68c9efff 100644 --- a/invoke_model.go +++ b/invoke_model.go @@ -22,9 +22,16 @@ const ( type LibXrayInvokeRequest struct { APIVersion int `json:"apiVersion,omitempty"` Method LibXrayMethod `json:"method,omitempty"` + Env *LibXrayEnvJson `json:"env,omitempty"` Payload json.RawMessage `json:"payload,omitempty"` } +type LibXrayEnvJson struct { + AssetLocation string `json:"xray.location.asset,omitempty"` + CertLocation string `json:"xray.location.cert,omitempty"` + TunFd string `json:"xray.tun.fd,omitempty"` +} + type GetFreePortsRequest struct { Count int `json:"count,omitempty"` } diff --git a/invoke_test.go b/invoke_test.go index e956bf11..73acd173 100644 --- a/invoke_test.go +++ b/invoke_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/xtls/libxray/nodep" + "github.com/xtls/xray-core/common/platform" ) type testResponse struct { @@ -337,7 +338,28 @@ func TestInvokeNoDataResponseShape(t *testing.T) { } } -func TestInvokeIgnoresTopLevelEnv(t *testing.T) { +func TestInvokeAppliesSupportedEnv(t *testing.T) { + restoreEnv(t, platform.AssetLocation) + restoreEnv(t, platform.CertLocation) + restoreEnv(t, platform.TunFdKey) + + requestJSON := `{"apiVersion":1,"method":"xrayVersion","env":{"xray.location.asset":"/tmp/assets","xray.location.cert":"/tmp/certs","xray.tun.fd":"123"}}` + response := invokeRawForTest(t, requestJSON) + if !response.Success { + t.Fatalf("xrayVersion failed: %s", response.Err) + } + if got := os.Getenv(platform.AssetLocation); got != "/tmp/assets" { + t.Fatalf("%s = %q", platform.AssetLocation, got) + } + if got := os.Getenv(platform.CertLocation); got != "/tmp/certs" { + t.Fatalf("%s = %q", platform.CertLocation, got) + } + if got := os.Getenv(platform.TunFdKey); got != "123" { + t.Fatalf("%s = %q", platform.TunFdKey, got) + } +} + +func TestInvokeIgnoresUnknownEnvKeys(t *testing.T) { const key = "XRAY_LIBXRAY_UNKNOWN_ENV_TEST" _ = os.Unsetenv(key) t.Cleanup(func() { _ = os.Unsetenv(key) }) @@ -347,13 +369,26 @@ func TestInvokeIgnoresTopLevelEnv(t *testing.T) { t.Fatal(err) } if !response.Success { - t.Fatalf("top-level env should be ignored: %s", response.Err) + t.Fatalf("unknown env should be ignored: %s", response.Err) } if _, found := os.LookupEnv(key); found { - t.Fatal("top-level env should not be set") + t.Fatal("unknown env should not be set") } } +func restoreEnv(t *testing.T, key string) { + t.Helper() + oldValue, found := os.LookupEnv(key) + _ = os.Unsetenv(key) + t.Cleanup(func() { + if found { + _ = os.Setenv(key, oldValue) + } else { + _ = os.Unsetenv(key) + } + }) +} + func xrayStopForTest(t *testing.T) { t.Helper() response := invokeForTest(t, LibXrayMethodStopXray, nil) diff --git a/readme/README.zh_CN.md b/readme/README.zh_CN.md index ede80bd3..125ca776 100644 --- a/readme/README.zh_CN.md +++ b/readme/README.zh_CN.md @@ -18,7 +18,9 @@ 默认情况下,编译脚本不会 clone [Xray-core](https://github.com/XTLS/Xray-core),而是通过 Go modules 将 Xray-core 固定到 tag `v26.6.27`(Go 会记录为对应的 pseudo-version)。 传入可选参数 `local` 时,会通过 Go module `replace` 改用已有的本地仓库 `../Xray-core`。 -Linux 和 Windows 构建只输出 libXray 动态库。需要独立 Xray 可执行文件的应用应使用 Xray-core 官方发布的二进制。 +Linux 和 Windows 构建还会输出一个 desktop wrapper 可执行文件: +`bin/xray` 或 `bin/xray.exe`。wrapper 的 `-configPath` 参数指向一个 +`method` 为 `runXray` 的 `LibXrayInvokeRequest` JSON 文件。 ### 使用方式 @@ -98,6 +100,11 @@ char* CGoInvoke(char* requestJSON); { "apiVersion": 1, "method": "runXray", + "env": { + "xray.location.asset": "/path/to/dat", + "xray.location.cert": "/path/to/dat", + "xray.tun.fd": "123" + }, "payload": { "configPath": "/path/to/config.json" } @@ -116,10 +123,12 @@ char* CGoInvoke(char* requestJSON); 设计决定: -1. `Invoke` 不再接受 `env` 字段。运行时 Xray-core 环境项必须写入 Xray 配置根 `env` 对象。 -2. `xray.json.strict`、`xray.location.config`、`xray.location.confdir` 属于加载前进程环境变量,不能通过 `Invoke` 传入。 -3. `SetTunFd` 已删除。如果 fd 只能在运行时获得,请在调用 `runXray` 前把 `xray.tun.fd` 写入 Xray 配置根 `env` 对象。 -4. `countGeoData` 不依赖 Xray 配置,因此通过 method payload 的 `datDir` 传入数据目录。 +1. `Invoke.env` 只支持固定的 Xray-core runtime 环境项: + `xray.location.asset`、`xray.location.cert`、`xray.tun.fd`。 +2. 非空 `env` 字段会在 method 执行前写入进程环境变量。缺失字段不会 unset,也不会 restore 旧值。 +3. `xray.json.strict`、`xray.location.config`、`xray.location.confdir` 属于加载前进程环境变量,不能通过 `Invoke.env` 传入。 +4. `SetTunFd` 已删除。如果 fd 只能在运行时获得,请使用 `Invoke.env["xray.tun.fd"]`。 +5. `countGeoData` 不依赖 Xray 配置,因此通过 method payload 的 `datDir` 传入数据目录。 支持的 method: From 42efee4c989ebf205c7c409de429005324d03667 Mon Sep 17 00:00:00 2001 From: yiguo Date: Tue, 7 Jul 2026 21:22:02 +0800 Subject: [PATCH 5/7] Make geo data invoke test self contained --- invoke_test.go | 49 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/invoke_test.go b/invoke_test.go index 73acd173..90d6b3a2 100644 --- a/invoke_test.go +++ b/invoke_test.go @@ -8,7 +8,9 @@ import ( "testing" "github.com/xtls/libxray/nodep" + "github.com/xtls/xray-core/common/geodata" "github.com/xtls/xray-core/common/platform" + "google.golang.org/protobuf/proto" ) type testResponse struct { @@ -85,16 +87,36 @@ func writeConfigToFile(t *testing.T, config any, path string) { } } -func copyFileForTest(t *testing.T, src string, dst string) { +func writeGeoSiteDatForTest(t *testing.T, path string) { t.Helper() - data, err := os.ReadFile(src) + data, err := proto.Marshal(&geodata.GeoSiteList{ + Entry: []*geodata.GeoSite{ + { + Code: "TEST", + Domain: []*geodata.Domain{ + { + Type: geodata.Domain_Domain, + Value: "example.com", + Attribute: []*geodata.Domain_Attribute{ + { + Key: "ads", + TypedValue: &geodata.Domain_Attribute_BoolValue{ + BoolValue: true, + }, + }, + }, + }, + }, + }, + }, + }) if err != nil { t.Fatal(err) } - if err := os.MkdirAll(filepath.Dir(dst), os.ModePerm); err != nil { + if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil { t.Fatal(err) } - if err := os.WriteFile(dst, data, 0o644); err != nil { + if err := os.WriteFile(path, data, 0o644); err != nil { t.Fatal(err) } } @@ -278,9 +300,8 @@ func TestInvokePingReturnsDelaySentinelOnXrayError(t *testing.T) { } func TestInvokeCountGeoDataUsesPayloadDatDir(t *testing.T) { - projectRoot, _ := filepath.Abs(".") datDir := t.TempDir() - copyFileForTest(t, filepath.Join(projectRoot, "dat", "geosite.dat"), filepath.Join(datDir, "geosite.dat")) + writeGeoSiteDatForTest(t, filepath.Join(datDir, "geosite.dat")) response := invokeForTest( t, @@ -295,9 +316,23 @@ func TestInvokeCountGeoDataUsesPayloadDatDir(t *testing.T) { t.Fatalf("CountGeoData failed: %s", response.Err) } requireNoDataObject(t, response) - if _, err := os.Stat(filepath.Join(datDir, "geosite.json")); err != nil { + output, err := os.ReadFile(filepath.Join(datDir, "geosite.json")) + if err != nil { + t.Fatal(err) + } + if !json.Valid(output) { + t.Fatalf("geosite.json is not valid JSON: %s", output) + } + var list struct { + CategoryCount int `json:"categoryCount,omitempty"` + RuleCount int `json:"ruleCount,omitempty"` + } + if err := json.Unmarshal(output, &list); err != nil { t.Fatal(err) } + if list.CategoryCount != 1 || list.RuleCount != 1 { + t.Fatalf("unexpected geosite counts: %+v", list) + } } func TestInvokeUnknownMethod(t *testing.T) { From 48f7e8a3ed08e0707dda409cf619e230d4362720 Mon Sep 17 00:00:00 2001 From: yiguo Date: Tue, 7 Jul 2026 21:24:53 +0800 Subject: [PATCH 6/7] Clarify invoke env documentation --- README.md | 4 ++-- readme/README.zh_CN.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ac5e19aa..d8cc0a90 100644 --- a/README.md +++ b/README.md @@ -162,8 +162,8 @@ Design notes: 3. `xray.json.strict`, `xray.location.config`, and `xray.location.confdir` are load-stage process environment variables and cannot be supplied through `Invoke.env`. -4. `SetTunFd` has been removed. Use `Invoke.env["xray.tun.fd"]` when the fd is - only known at runtime. +4. `SetTunFd` has been removed. Set the `"xray.tun.fd"` key in the request + `env` object when the fd is only known at runtime. 5. `countGeoData` is not backed by an Xray config, so its `datDir` is passed in the method payload. diff --git a/readme/README.zh_CN.md b/readme/README.zh_CN.md index 125ca776..71ac6d76 100644 --- a/readme/README.zh_CN.md +++ b/readme/README.zh_CN.md @@ -127,7 +127,7 @@ char* CGoInvoke(char* requestJSON); `xray.location.asset`、`xray.location.cert`、`xray.tun.fd`。 2. 非空 `env` 字段会在 method 执行前写入进程环境变量。缺失字段不会 unset,也不会 restore 旧值。 3. `xray.json.strict`、`xray.location.config`、`xray.location.confdir` 属于加载前进程环境变量,不能通过 `Invoke.env` 传入。 -4. `SetTunFd` 已删除。如果 fd 只能在运行时获得,请使用 `Invoke.env["xray.tun.fd"]`。 +4. `SetTunFd` 已删除。如果 fd 只能在运行时获得,请在请求的 `env` 对象中设置 `"xray.tun.fd"`。 5. `countGeoData` 不依赖 Xray 配置,因此通过 method payload 的 `datDir` 传入数据目录。 支持的 method: From c9efaf0d0cfe70905d5b5374a4e5bb6f22fcee11 Mon Sep 17 00:00:00 2001 From: yiguo Date: Tue, 7 Jul 2026 21:30:48 +0800 Subject: [PATCH 7/7] Harden desktop wrapper errors --- desktop_bin/main.go | 13 +++++++++++-- desktop_bin/run.go | 17 ++++++++++++++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/desktop_bin/main.go b/desktop_bin/main.go index a41c3a4d..dafade96 100644 --- a/desktop_bin/main.go +++ b/desktop_bin/main.go @@ -11,14 +11,23 @@ import ( ) func main() { - configPath := flag.String("configPath", "config.json", "Path of libXray invoke request json") + configPath := flag.String("configPath", "", "Path of LibXrayInvokeRequest JSON") flag.Parse() + if *configPath == "" { + flag.Usage() + fmt.Fprintln(os.Stderr, "missing -configPath") + os.Exit(1) + } if err := runXray(*configPath); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } - defer stopXray() + defer func() { + if err := stopXray(); err != nil { + fmt.Fprintf(os.Stderr, "stopXray failed: %v\n", err) + } + }() runtime.GC() debug.FreeOSMemory() diff --git a/desktop_bin/run.go b/desktop_bin/run.go index afcf12fd..2c34b594 100644 --- a/desktop_bin/run.go +++ b/desktop_bin/run.go @@ -42,13 +42,24 @@ func runXray(configPath string) error { return nil } -func stopXray() { +func stopXray() error { requestBytes, err := json.Marshal(&libXray.LibXrayInvokeRequest{ APIVersion: 1, Method: libXray.LibXrayMethodStopXray, }) if err != nil { - return + return err + } + responseText := libXray.Invoke(string(requestBytes)) + var response invokeResponse + if err := json.Unmarshal([]byte(responseText), &response); err != nil { + return err } - _ = libXray.Invoke(string(requestBytes)) + if !response.Success { + if response.Err == "" { + response.Err = "stopXray failed" + } + return errors.New(response.Err) + } + return nil }