Add sub-command "-dump" to "run". (#2854)

* Add MarshalToJson().

* Add cmd arg -dump for printing out merged multiple json configs.

---------

Co-authored-by: nobody <nobody@nowhere.mars>
This commit is contained in:
nobody 2023-12-30 00:16:48 +08:00 committed by GitHub
parent 006cf491e5
commit 44bb83033f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 476 additions and 10 deletions

View File

@ -27,6 +27,11 @@ type generalLogger struct {
done *done.Instance done *done.Instance
} }
type serverityLogger struct {
inner *generalLogger
logLevel Severity
}
// NewLogger returns a generic log handler that can handle all type of messages. // NewLogger returns a generic log handler that can handle all type of messages.
func NewLogger(logWriterCreator WriterCreator) Handler { func NewLogger(logWriterCreator WriterCreator) Handler {
return &generalLogger{ return &generalLogger{
@ -37,6 +42,32 @@ func NewLogger(logWriterCreator WriterCreator) Handler {
} }
} }
func ReplaceWithSeverityLogger(serverity Severity) {
w := CreateStdoutLogWriter()
g := &generalLogger{
creator: w,
buffer: make(chan Message, 16),
access: semaphore.New(1),
done: done.New(),
}
s := &serverityLogger{
inner: g,
logLevel: serverity,
}
RegisterHandler(s)
}
func (l *serverityLogger) Handle(msg Message) {
switch msg := msg.(type) {
case *GeneralMessage:
if msg.Severity <= l.logLevel {
l.inner.Handle(msg)
}
default:
l.inner.Handle(msg)
}
}
func (l *generalLogger) run() { func (l *generalLogger) run() {
defer l.access.Signal() defer l.access.Signal()
@ -67,6 +98,7 @@ func (l *generalLogger) run() {
} }
func (l *generalLogger) Handle(msg Message) { func (l *generalLogger) Handle(msg Message) {
select { select {
case l.buffer <- msg: case l.buffer <- msg:
default: default:

173
common/reflect/marshal.go Normal file
View File

@ -0,0 +1,173 @@
package reflect
import (
"encoding/json"
"reflect"
"slices"
cserial "github.com/xtls/xray-core/common/serial"
)
func MarshalToJson(v interface{}) (string, bool) {
if itf := marshalInterface(v, true); itf != nil {
if b, err := json.MarshalIndent(itf, "", " "); err == nil {
return string(b[:]), true
}
}
return "", false
}
func marshalTypedMessage(v *cserial.TypedMessage, ignoreNullValue bool) interface{} {
tmsg, err := v.GetInstance()
if err != nil {
return nil
}
r := marshalInterface(tmsg, ignoreNullValue)
if msg, ok := r.(map[string]interface{}); ok {
msg["_TypedMessage_"] = v.Type
}
return r
}
func marshalSlice(v reflect.Value, ignoreNullValue bool) interface{} {
r := make([]interface{}, 0)
for i := 0; i < v.Len(); i++ {
rv := v.Index(i)
if rv.CanInterface() {
value := rv.Interface()
r = append(r, marshalInterface(value, ignoreNullValue))
}
}
return r
}
func marshalStruct(v reflect.Value, ignoreNullValue bool) interface{} {
r := make(map[string]interface{})
t := v.Type()
for i := 0; i < v.NumField(); i++ {
rv := v.Field(i)
if rv.CanInterface() {
ft := t.Field(i)
name := ft.Name
value := rv.Interface()
tv := marshalInterface(value, ignoreNullValue)
if tv != nil || !ignoreNullValue {
r[name] = tv
}
}
}
return r
}
func marshalMap(v reflect.Value, ignoreNullValue bool) interface{} {
// policy.level is map[uint32] *struct
kt := v.Type().Key()
vt := reflect.TypeOf((*interface{})(nil))
mt := reflect.MapOf(kt, vt)
r := reflect.MakeMap(mt)
for _, key := range v.MapKeys() {
rv := v.MapIndex(key)
if rv.CanInterface() {
iv := rv.Interface()
tv := marshalInterface(iv, ignoreNullValue)
if tv != nil || !ignoreNullValue {
r.SetMapIndex(key, reflect.ValueOf(&tv))
}
}
}
return r.Interface()
}
func marshalIString(v interface{}) (r string, ok bool) {
defer func() {
if err := recover(); err != nil {
r = ""
ok = false
}
}()
if iStringFn, ok := v.(interface{ String() string }); ok {
return iStringFn.String(), true
}
return "", false
}
func marshalKnownType(v interface{}, ignoreNullValue bool) (interface{}, bool) {
switch ty := v.(type) {
case cserial.TypedMessage:
return marshalTypedMessage(&ty, ignoreNullValue), true
case *cserial.TypedMessage:
return marshalTypedMessage(ty, ignoreNullValue), true
case map[string]json.RawMessage:
return ty, true
case []json.RawMessage:
return ty, true
case *json.RawMessage:
return ty, true
case json.RawMessage:
return ty, true
default:
return nil, false
}
}
var valueKinds = []reflect.Kind{
reflect.Bool,
reflect.Int,
reflect.Int8,
reflect.Int16,
reflect.Int32,
reflect.Int64,
reflect.Uint,
reflect.Uint8,
reflect.Uint16,
reflect.Uint32,
reflect.Uint64,
reflect.Uintptr,
reflect.Float32,
reflect.Float64,
reflect.Complex64,
reflect.Complex128,
reflect.String,
}
func isValueKind(kind reflect.Kind) bool {
return slices.Contains(valueKinds, kind)
}
func marshalInterface(v interface{}, ignoreNullValue bool) interface{} {
if r, ok := marshalKnownType(v, ignoreNullValue); ok {
return r
}
rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Ptr {
rv = rv.Elem()
}
k := rv.Kind()
if k == reflect.Invalid {
return nil
}
if isValueKind(k) {
return v
}
switch k {
case reflect.Struct:
return marshalStruct(rv, ignoreNullValue)
case reflect.Slice:
return marshalSlice(rv, ignoreNullValue)
case reflect.Array:
return marshalSlice(rv, ignoreNullValue)
case reflect.Map:
return marshalMap(rv, ignoreNullValue)
default:
break
}
if str, ok := marshalIString(v); ok {
return str
}
return nil
}

View File

@ -0,0 +1,187 @@
package reflect_test
import (
"bytes"
"encoding/json"
"strings"
"testing"
. "github.com/xtls/xray-core/common/reflect"
cserial "github.com/xtls/xray-core/common/serial"
iserial "github.com/xtls/xray-core/infra/conf/serial"
)
func TestMashalStruct(t *testing.T) {
type Foo = struct {
N int `json:"n"`
Np *int `json:"np"`
S string `json:"s"`
Arr *[]map[string]map[string]string `json:"arr"`
}
n := 1
np := &n
arr := make([]map[string]map[string]string, 0)
m1 := make(map[string]map[string]string, 0)
m2 := make(map[string]string, 0)
m2["hello"] = "world"
m1["foo"] = m2
arr = append(arr, m1)
f1 := Foo{
N: n,
Np: np,
S: "hello",
Arr: &arr,
}
s, ok1 := MarshalToJson(f1)
sp, ok2 := MarshalToJson(&f1)
if !ok1 || !ok2 || s != sp {
t.Error("marshal failed")
}
f2 := Foo{}
if json.Unmarshal([]byte(s), &f2) != nil {
t.Error("json unmarshal failed")
}
v := (*f2.Arr)[0]["foo"]["hello"]
if f1.N != f2.N || *(f1.Np) != *(f2.Np) || f1.S != f2.S || v != "world" {
t.Error("f1 not equal to f2")
}
}
func TestMarshalConfigJson(t *testing.T) {
buf := bytes.NewBufferString(getConfig())
config, err := iserial.DecodeJSONConfig(buf)
if err != nil {
t.Error("decode JSON config failed")
}
bc, err := config.Build()
if err != nil {
t.Error("build core config failed")
}
tmsg := cserial.ToTypedMessage(bc)
tc, ok := MarshalToJson(tmsg)
if !ok {
t.Error("marshal config failed")
}
// t.Log(tc)
keywords := []string{
"4784f9b8-a879-4fec-9718-ebddefa47750",
"bing.com",
"DomainStrategy",
"InboundTag",
"Level",
"Stats",
"UserDownlink",
"UserUplink",
"System",
"InboundDownlink",
"OutboundUplink",
}
for _, kw := range keywords {
if !strings.Contains(tc, kw) {
t.Error("marshaled config error")
}
}
}
func getConfig() string {
return `{
"log": {
"loglevel": "debug"
},
"stats": {},
"policy": {
"levels": {
"0": {
"statsUserUplink": true,
"statsUserDownlink": true
}
},
"system": {
"statsInboundUplink": true,
"statsInboundDownlink": true,
"statsOutboundUplink": true,
"statsOutboundDownlink": true
}
},
"inbounds": [
{
"tag": "agentin",
"protocol": "http",
"port": 8080,
"listen": "127.0.0.1",
"settings": {}
},
{
"listen": "127.0.0.1",
"port": 10085,
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1"
},
"tag": "api-in"
}
],
"api": {
"tag": "api",
"services": [
"HandlerService",
"StatsService"
]
},
"routing": {
"rules": [
{
"inboundTag": [
"api-in"
],
"outboundTag": "api",
"type": "field"
}
],
"domainStrategy": "AsIs"
},
"outbounds": [
{
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "1.2.3.4",
"port": 1234,
"users": [
{
"id": "4784f9b8-a879-4fec-9718-ebddefa47750",
"encryption": "none"
}
]
}
]
},
"tag": "agentout",
"streamSettings": {
"network": "ws",
"security": "none",
"wsSettings": {
"path": "/?ed=2048",
"headers": {
"Host": "bing.com"
}
}
}
}
]
}`
}

View File

@ -2,6 +2,7 @@ package core
import ( import (
"io" "io"
"slices"
"strings" "strings"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
@ -24,10 +25,14 @@ type ConfigLoader func(input interface{}) (*Config, error)
// ConfigBuilder is a builder to build core.Config from filenames and formats // ConfigBuilder is a builder to build core.Config from filenames and formats
type ConfigBuilder func(files []string, formats []string) (*Config, error) type ConfigBuilder func(files []string, formats []string) (*Config, error)
// ConfigMerger merge multiple json configs into on config
type ConfigsMerger func(files []string, formats []string) (string, error)
var ( var (
configLoaderByName = make(map[string]*ConfigFormat) configLoaderByName = make(map[string]*ConfigFormat)
configLoaderByExt = make(map[string]*ConfigFormat) configLoaderByExt = make(map[string]*ConfigFormat)
ConfigBuilderForFiles ConfigBuilder ConfigBuilderForFiles ConfigBuilder
ConfigMergedFormFiles ConfigsMerger
) )
// RegisterConfigLoader add a new ConfigLoader. // RegisterConfigLoader add a new ConfigLoader.
@ -49,6 +54,20 @@ func RegisterConfigLoader(format *ConfigFormat) error {
return nil return nil
} }
func GetMergedConfig(args cmdarg.Arg) (string, error) {
files := make([]string, 0)
formats := make([]string, 0)
supported := []string{"json", "yaml", "toml"}
for _, file := range args {
format := getFormat(file)
if slices.Contains(supported, format) {
files = append(files, file)
formats = append(formats, format)
}
}
return ConfigMergedFormFiles(files, formats)
}
func GetFormatByExtension(ext string) string { func GetFormatByExtension(ext string) string {
switch strings.ToLower(ext) { switch strings.ToLower(ext) {
case "pb", "protobuf": case "pb", "protobuf":

View File

@ -3,12 +3,25 @@ package serial
import ( import (
"io" "io"
creflect "github.com/xtls/xray-core/common/reflect"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/infra/conf" "github.com/xtls/xray-core/infra/conf"
"github.com/xtls/xray-core/main/confloader" "github.com/xtls/xray-core/main/confloader"
) )
func BuildConfig(files []string, formats []string) (*core.Config, error) { func MergeConfigFromFiles(files []string, formats []string) (string, error) {
c, err := mergeConfigs(files, formats)
if err != nil {
return "", err
}
if j, ok := creflect.MarshalToJson(c); ok {
return j, nil
}
return "", newError("marshal to json failed.").AtError()
}
func mergeConfigs(files []string, formats []string) (*conf.Config, error) {
cf := &conf.Config{} cf := &conf.Config{}
for i, file := range files { for i, file := range files {
newError("Reading config: ", file).AtInfo().WriteToLog() newError("Reading config: ", file).AtInfo().WriteToLog()
@ -26,7 +39,15 @@ func BuildConfig(files []string, formats []string) (*core.Config, error) {
} }
cf.Override(c, file) cf.Override(c, file)
} }
return cf.Build() return cf, nil
}
func BuildConfig(files []string, formats []string) (*core.Config, error) {
config, err := mergeConfigs(files, formats)
if err != nil {
return nil, err
}
return config.Build()
} }
type readerDecoder func(io.Reader) (*conf.Config, error) type readerDecoder func(io.Reader) (*conf.Config, error)
@ -39,4 +60,5 @@ func init() {
ReaderDecoderByFormat["toml"] = DecodeTOMLConfig ReaderDecoderByFormat["toml"] = DecodeTOMLConfig
core.ConfigBuilderForFiles = BuildConfig core.ConfigBuilderForFiles = BuildConfig
core.ConfigMergedFormFiles = MergeConfigFromFiles
} }

View File

@ -12,8 +12,10 @@ import (
"runtime/debug" "runtime/debug"
"strings" "strings"
"syscall" "syscall"
"time"
"github.com/xtls/xray-core/common/cmdarg" "github.com/xtls/xray-core/common/cmdarg"
clog "github.com/xtls/xray-core/common/log"
"github.com/xtls/xray-core/common/platform" "github.com/xtls/xray-core/common/platform"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/main/commands/base" "github.com/xtls/xray-core/main/commands/base"
@ -34,7 +36,9 @@ The -format=json flag sets the format of config files.
Default "auto". Default "auto".
The -test flag tells Xray to test config files only, The -test flag tells Xray to test config files only,
without launching the server without launching the server.
The -dump flag tells Xray to print the merged config.
`, `,
} }
@ -45,6 +49,7 @@ func init() {
var ( var (
configFiles cmdarg.Arg // "Config file for Xray.", the option is customed type, parse in main configFiles cmdarg.Arg // "Config file for Xray.", the option is customed type, parse in main
configDir string configDir string
dump = cmdRun.Flag.Bool("dump", false, "Dump merged config only, without launching Xray server.")
test = cmdRun.Flag.Bool("test", false, "Test config file only, without launching Xray server.") test = cmdRun.Flag.Bool("test", false, "Test config file only, without launching Xray server.")
format = cmdRun.Flag.String("format", "auto", "Format of input file.") format = cmdRun.Flag.String("format", "auto", "Format of input file.")
@ -61,6 +66,12 @@ var (
) )
func executeRun(cmd *base.Command, args []string) { func executeRun(cmd *base.Command, args []string) {
if *dump {
clog.ReplaceWithSeverityLogger(clog.Severity_Warning)
errCode := dumpConfig()
os.Exit(errCode)
}
printVersion() printVersion()
server, err := startXray() server, err := startXray()
if err != nil { if err != nil {
@ -97,6 +108,18 @@ func executeRun(cmd *base.Command, args []string) {
} }
} }
func dumpConfig() int {
files := getConfigFilePath(false)
if config, err := core.GetMergedConfig(files); err != nil {
fmt.Println(err)
time.Sleep(1 * time.Second)
return 23
} else {
fmt.Print(config)
}
return 0
}
func fileExists(file string) bool { func fileExists(file string) bool {
info, err := os.Stat(file) info, err := os.Stat(file)
return err == nil && !info.IsDir() return err == nil && !info.IsDir()
@ -139,12 +162,16 @@ func readConfDir(dirPath string) {
} }
} }
func getConfigFilePath() cmdarg.Arg { func getConfigFilePath(verbose bool) cmdarg.Arg {
if dirExists(configDir) { if dirExists(configDir) {
log.Println("Using confdir from arg:", configDir) if verbose {
log.Println("Using confdir from arg:", configDir)
}
readConfDir(configDir) readConfDir(configDir)
} else if envConfDir := platform.GetConfDirPath(); dirExists(envConfDir) { } else if envConfDir := platform.GetConfDirPath(); dirExists(envConfDir) {
log.Println("Using confdir from env:", envConfDir) if verbose {
log.Println("Using confdir from env:", envConfDir)
}
readConfDir(envConfDir) readConfDir(envConfDir)
} }
@ -155,17 +182,23 @@ func getConfigFilePath() cmdarg.Arg {
if workingDir, err := os.Getwd(); err == nil { if workingDir, err := os.Getwd(); err == nil {
configFile := filepath.Join(workingDir, "config.json") configFile := filepath.Join(workingDir, "config.json")
if fileExists(configFile) { if fileExists(configFile) {
log.Println("Using default config: ", configFile) if verbose {
log.Println("Using default config: ", configFile)
}
return cmdarg.Arg{configFile} return cmdarg.Arg{configFile}
} }
} }
if configFile := platform.GetConfigurationPath(); fileExists(configFile) { if configFile := platform.GetConfigurationPath(); fileExists(configFile) {
log.Println("Using config from env: ", configFile) if verbose {
log.Println("Using config from env: ", configFile)
}
return cmdarg.Arg{configFile} return cmdarg.Arg{configFile}
} }
log.Println("Using config from STDIN") if verbose {
log.Println("Using config from STDIN")
}
return cmdarg.Arg{"stdin:"} return cmdarg.Arg{"stdin:"}
} }
@ -178,7 +211,7 @@ func getConfigFormat() string {
} }
func startXray() (core.Server, error) { func startXray() (core.Server, error) {
configFiles := getConfigFilePath() configFiles := getConfigFilePath(true)
// config, err := core.LoadConfig(getConfigFormat(), configFiles[0], configFiles) // config, err := core.LoadConfig(getConfigFormat(), configFiles[0], configFiles)