Skip to content

Commit 102a418

Browse files
author
yiguo
committed
Restore invoke env and desktop wrapper
1 parent 23783c4 commit 102a418

11 files changed

Lines changed: 207 additions & 16 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,13 @@ jobs:
147147
mkdir -p "dist/${{ matrix.artifact }}"
148148
cp linux_so/libXray.so "dist/${{ matrix.artifact }}/"
149149
cp linux_so/libXray.h "dist/${{ matrix.artifact }}/"
150+
cp bin/xray "dist/${{ matrix.artifact }}/"
150151
;;
151152
windows)
152153
mkdir -p "dist/${{ matrix.artifact }}"
153154
cp windows_dll/libXray.dll "dist/${{ matrix.artifact }}/"
154155
cp windows_dll/libXray.h "dist/${{ matrix.artifact }}/"
156+
cp bin/xray.exe "dist/${{ matrix.artifact }}/"
155157
;;
156158
android)
157159
mkdir -p "dist/${{ matrix.artifact }}"

README.md

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ depends on git and go.
4242

4343
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).
4444
Pass the optional `local` argument to use an existing local checkout at `../Xray-core` through a Go module `replace`.
45-
Linux and Windows builds only produce the libXray shared library. Applications that need a standalone Xray executable should use official Xray-core release binaries.
45+
Linux and Windows builds also produce a small desktop wrapper executable:
46+
`bin/xray` or `bin/xray.exe`. The wrapper accepts `-configPath` pointing to a
47+
`LibXrayInvokeRequest` JSON file whose method is `runXray`.
4648

4749
### Usage
4850

@@ -130,6 +132,11 @@ The request is a JSON object:
130132
{
131133
"apiVersion": 1,
132134
"method": "runXray",
135+
"env": {
136+
"xray.location.asset": "/path/to/dat",
137+
"xray.location.cert": "/path/to/dat",
138+
"xray.tun.fd": "123"
139+
},
133140
"payload": {
134141
"configPath": "/path/to/config.json"
135142
}
@@ -148,14 +155,16 @@ The response is a JSON object:
148155

149156
Design notes:
150157

151-
1. `Invoke` does not accept an `env` field. Runtime Xray-core environment
152-
settings must be written into the Xray config root `env` object.
153-
2. `xray.json.strict`, `xray.location.config`, and `xray.location.confdir` are
158+
1. `Invoke.env` supports only fixed Xray-core runtime environment keys:
159+
`xray.location.asset`, `xray.location.cert`, and `xray.tun.fd`.
160+
2. Non-empty `env` fields are set on the process before the method runs.
161+
Missing fields are not unset and old values are not restored.
162+
3. `xray.json.strict`, `xray.location.config`, and `xray.location.confdir` are
154163
load-stage process environment variables and cannot be supplied through
155-
`Invoke`.
156-
3. `SetTunFd` has been removed. Write `xray.tun.fd` into the Xray config root
157-
`env` object before calling `runXray` when the fd is only known at runtime.
158-
4. `countGeoData` is not backed by an Xray config, so its `datDir` is passed in
164+
`Invoke.env`.
165+
4. `SetTunFd` has been removed. Use `Invoke.env["xray.tun.fd"]` when the fd is
166+
only known at runtime.
167+
5. `countGeoData` is not backed by an Xray config, so its `datDir` is passed in
159168
the method payload.
160169

161170
Supported methods:

build/app/build.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import subprocess
55

66
from app.cmd import (
7+
create_dir_if_not_exists,
78
delete_file_if_exists,
89
delete_dir_if_exists,
910
)
@@ -142,6 +143,28 @@ def before_build(self):
142143
def build(self):
143144
pass
144145

146+
def build_desktop_bin(self, bin_file: str):
147+
bin_dir = os.path.join(self.lib_dir, "bin")
148+
create_dir_if_not_exists(bin_dir)
149+
output_file = os.path.join(bin_dir, bin_file)
150+
run_env = os.environ.copy()
151+
run_env["CGO_ENABLED"] = "0"
152+
153+
cmd = [
154+
"go",
155+
"build",
156+
"-trimpath",
157+
"-ldflags",
158+
"-s -w",
159+
f"-o={output_file}",
160+
"./desktop_bin",
161+
]
162+
os.chdir(self.lib_dir)
163+
print(cmd)
164+
ret = subprocess.run(cmd, env=run_env)
165+
if ret.returncode != 0:
166+
raise Exception("build_desktop_bin failed")
167+
145168
def after_build(self):
146169
pass
147170

build/app/linux.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def __init__(self, build_dir: str, use_local_xray_core: bool = False):
1313
create_dir_if_not_exists(self.framework_dir)
1414
self.lib_file = "libXray.so"
1515
self.lib_header_file = "libXray.h"
16+
self.bin_file = "xray"
1617

1718
def before_build(self):
1819
super().before_build()
@@ -22,6 +23,7 @@ def build(self):
2223
self.before_build()
2324
self.build_linux()
2425
self.after_build()
26+
self.build_desktop_bin(self.bin_file)
2527
self.revert_go_env()
2628

2729
def build_linux(self):

build/app/windows.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def __init__(self, build_dir: str, use_local_xray_core: bool = False):
1313
create_dir_if_not_exists(self.framework_dir)
1414
self.lib_file = "libXray.dll"
1515
self.lib_header_file = "libXray.h"
16+
self.bin_file = "xray.exe"
1617

1718
def before_build(self):
1819
super().before_build()
@@ -22,6 +23,7 @@ def build(self):
2223
self.before_build()
2324
self.build_windows()
2425
self.after_build()
26+
self.build_desktop_bin(self.bin_file)
2527
self.revert_go_env()
2628

2729
def build_windows(self):

desktop_bin/main.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"os"
7+
"os/signal"
8+
"runtime"
9+
"runtime/debug"
10+
"syscall"
11+
)
12+
13+
func main() {
14+
configPath := flag.String("configPath", "config.json", "Path of libXray invoke request json")
15+
flag.Parse()
16+
17+
if err := runXray(*configPath); err != nil {
18+
fmt.Fprintln(os.Stderr, err)
19+
os.Exit(1)
20+
}
21+
defer stopXray()
22+
23+
runtime.GC()
24+
debug.FreeOSMemory()
25+
26+
osSignals := make(chan os.Signal, 1)
27+
signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM)
28+
<-osSignals
29+
}

desktop_bin/run.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"os"
8+
9+
libXray "github.com/xtls/libxray"
10+
)
11+
12+
type invokeResponse struct {
13+
Success bool `json:"success"`
14+
Err string `json:"error"`
15+
}
16+
17+
func runXray(configPath string) error {
18+
requestBytes, err := os.ReadFile(configPath)
19+
if err != nil {
20+
return err
21+
}
22+
23+
var request libXray.LibXrayInvokeRequest
24+
if err := json.Unmarshal(requestBytes, &request); err != nil {
25+
return err
26+
}
27+
if request.Method != libXray.LibXrayMethodRunXray {
28+
return fmt.Errorf("unsupported method %q: desktop_bin only supports runXray", request.Method)
29+
}
30+
31+
responseText := libXray.Invoke(string(requestBytes))
32+
var response invokeResponse
33+
if err := json.Unmarshal([]byte(responseText), &response); err != nil {
34+
return err
35+
}
36+
if !response.Success {
37+
if response.Err == "" {
38+
response.Err = "runXray failed"
39+
}
40+
return errors.New(response.Err)
41+
}
42+
return nil
43+
}
44+
45+
func stopXray() {
46+
requestBytes, err := json.Marshal(&libXray.LibXrayInvokeRequest{
47+
APIVersion: 1,
48+
Method: libXray.LibXrayMethodStopXray,
49+
})
50+
if err != nil {
51+
return
52+
}
53+
_ = libXray.Invoke(string(requestBytes))
54+
}

invoke.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ package libXray
33
import (
44
"encoding/json"
55
"errors"
6+
"os"
67

78
"github.com/xtls/libxray/geo"
89
"github.com/xtls/libxray/nodep"
910
"github.com/xtls/libxray/share"
1011
"github.com/xtls/libxray/xray"
12+
"github.com/xtls/xray-core/common/platform"
1113
)
1214

1315
type invokeResponse struct {
@@ -24,6 +26,7 @@ func Invoke(requestJSON string) string {
2426
if err := validateAPIVersion(request.APIVersion); err != nil {
2527
return encodeInvokeResponse(nil, err)
2628
}
29+
applyEnv(request.Env)
2730

2831
switch request.Method {
2932
case LibXrayMethodGetFreePorts:
@@ -53,6 +56,22 @@ func Invoke(requestJSON string) string {
5356
}
5457
}
5558

59+
func applyEnv(env *LibXrayEnvJson) {
60+
if env == nil {
61+
return
62+
}
63+
setEnvIfNotEmpty(platform.AssetLocation, env.AssetLocation)
64+
setEnvIfNotEmpty(platform.CertLocation, env.CertLocation)
65+
setEnvIfNotEmpty(platform.TunFdKey, env.TunFd)
66+
}
67+
68+
func setEnvIfNotEmpty(key string, value string) {
69+
if value == "" {
70+
return
71+
}
72+
_ = os.Setenv(key, value)
73+
}
74+
5675
func validateAPIVersion(version int) error {
5776
if version == 0 || version == 1 {
5877
return nil

invoke_model.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,16 @@ const (
2222
type LibXrayInvokeRequest struct {
2323
APIVersion int `json:"apiVersion,omitempty"`
2424
Method LibXrayMethod `json:"method,omitempty"`
25+
Env *LibXrayEnvJson `json:"env,omitempty"`
2526
Payload json.RawMessage `json:"payload,omitempty"`
2627
}
2728

29+
type LibXrayEnvJson struct {
30+
AssetLocation string `json:"xray.location.asset,omitempty"`
31+
CertLocation string `json:"xray.location.cert,omitempty"`
32+
TunFd string `json:"xray.tun.fd,omitempty"`
33+
}
34+
2835
type GetFreePortsRequest struct {
2936
Count int `json:"count,omitempty"`
3037
}

invoke_test.go

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"testing"
99

1010
"github.com/xtls/libxray/nodep"
11+
"github.com/xtls/xray-core/common/platform"
1112
)
1213

1314
type testResponse struct {
@@ -337,7 +338,28 @@ func TestInvokeNoDataResponseShape(t *testing.T) {
337338
}
338339
}
339340

340-
func TestInvokeIgnoresTopLevelEnv(t *testing.T) {
341+
func TestInvokeAppliesSupportedEnv(t *testing.T) {
342+
restoreEnv(t, platform.AssetLocation)
343+
restoreEnv(t, platform.CertLocation)
344+
restoreEnv(t, platform.TunFdKey)
345+
346+
requestJSON := `{"apiVersion":1,"method":"xrayVersion","env":{"xray.location.asset":"/tmp/assets","xray.location.cert":"/tmp/certs","xray.tun.fd":"123"}}`
347+
response := invokeRawForTest(t, requestJSON)
348+
if !response.Success {
349+
t.Fatalf("xrayVersion failed: %s", response.Err)
350+
}
351+
if got := os.Getenv(platform.AssetLocation); got != "/tmp/assets" {
352+
t.Fatalf("%s = %q", platform.AssetLocation, got)
353+
}
354+
if got := os.Getenv(platform.CertLocation); got != "/tmp/certs" {
355+
t.Fatalf("%s = %q", platform.CertLocation, got)
356+
}
357+
if got := os.Getenv(platform.TunFdKey); got != "123" {
358+
t.Fatalf("%s = %q", platform.TunFdKey, got)
359+
}
360+
}
361+
362+
func TestInvokeIgnoresUnknownEnvKeys(t *testing.T) {
341363
const key = "XRAY_LIBXRAY_UNKNOWN_ENV_TEST"
342364
_ = os.Unsetenv(key)
343365
t.Cleanup(func() { _ = os.Unsetenv(key) })
@@ -347,13 +369,26 @@ func TestInvokeIgnoresTopLevelEnv(t *testing.T) {
347369
t.Fatal(err)
348370
}
349371
if !response.Success {
350-
t.Fatalf("top-level env should be ignored: %s", response.Err)
372+
t.Fatalf("unknown env should be ignored: %s", response.Err)
351373
}
352374
if _, found := os.LookupEnv(key); found {
353-
t.Fatal("top-level env should not be set")
375+
t.Fatal("unknown env should not be set")
354376
}
355377
}
356378

379+
func restoreEnv(t *testing.T, key string) {
380+
t.Helper()
381+
oldValue, found := os.LookupEnv(key)
382+
_ = os.Unsetenv(key)
383+
t.Cleanup(func() {
384+
if found {
385+
_ = os.Setenv(key, oldValue)
386+
} else {
387+
_ = os.Unsetenv(key)
388+
}
389+
})
390+
}
391+
357392
func xrayStopForTest(t *testing.T) {
358393
t.Helper()
359394
response := invokeForTest(t, LibXrayMethodStopXray, nil)

0 commit comments

Comments
 (0)