fix(xray): reuse bundled container binary through symlinks

Package the image's Xray executable once and make the working path a
relocatable symlink. Reuse that executable for latest and matching explicit
pins, replacing duplicate working copies instead of copying it again.

Restore image selections with an atomic symlink replacement. Downloads
replace the working symlink without overwriting its target, preserving the
image binary for fallback and unpinning. Avoid self-referencing links when
the working and bundled paths identify the same file.

Keep container resource lookup compatible with the former executable
directory, including geodata, relative certificates, Lua script search
precedence, and environment overrides. Leave native processes unchanged.

Exercise the real packaging script after relocation, real child-process
resource lookup, and version-switching lifecycles without nested containers.
This commit is contained in:
ergolyam 2026-10-15 23:41:33 +03:00
parent 50b7763f07
commit e8d445a045
9 changed files with 543 additions and 13 deletions

View file

@ -31,12 +31,13 @@ if [ -z "$MTG_MULTI_VER" ]; then
echo "DockerInit: could not resolve the latest mtg-multi release tag" >&2
exit 1
fi
mkdir -p build/bin
mkdir -p build/bin build/xray-image
cd build/bin
curl -sfLRO "https://github.com/XTLS/Xray-core/releases/download/v26.10.10/Xray-linux-${ARCH}.zip"
unzip "Xray-linux-${ARCH}.zip"
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
mv xray "xray-linux-${FNAME}"
mv xray "../xray-image/xray-linux-${FNAME}"
ln -s "../xray-image/xray-linux-${FNAME}" "xray-linux-${FNAME}"
# mtg-multi (MTProto sidecar) ships prebuilt release binaries for every target
# we package, so download and unpack the matching one instead of compiling.
case $FNAME in

View file

@ -46,7 +46,6 @@ RUN apk add --no-cache --update \
openssl
COPY --from=builder /app/build/ /app/
COPY --from=builder /app/build/bin/xray-* /app/xray-image/
COPY --from=builder /app/DockerEntrypoint.sh /app/
COPY --from=builder /app/x-ui.sh /usr/bin/x-ui
COPY --from=builder /app/internal/web/translation /app/internal/web/translation

129
docker_init_test.go Normal file
View file

@ -0,0 +1,129 @@
package main
import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"io/fs"
"os"
"os/exec"
"path/filepath"
"runtime"
"slices"
"strings"
"testing"
)
func TestDockerInitPackagesOneXrayBinaryWithRelocatableSymlink(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("DockerInit uses a Unix shell and symlinks")
}
script, err := filepath.Abs("DockerInit.sh")
if err != nil {
t.Fatal(err)
}
root := t.TempDir()
write := func(name string, data []byte, mode fs.FileMode) string {
t.Helper()
path := filepath.Join(root, name)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, data, mode); err != nil {
t.Fatal(err)
}
return path
}
var xrayArchive bytes.Buffer
zw := zip.NewWriter(&xrayArchive)
header := &zip.FileHeader{Name: "xray"}
header.SetMode(0o755)
entry, err := zw.CreateHeader(header)
if err != nil {
t.Fatal(err)
}
if _, err := entry.Write([]byte("#!/bin/sh\nprintf 'Xray 26.10.10 fixture\\n'\n")); err != nil {
t.Fatal(err)
}
if err := zw.Close(); err != nil {
t.Fatal(err)
}
xrayArchivePath := write("xray.zip", xrayArchive.Bytes(), 0o600)
var mtgArchive bytes.Buffer
gz := gzip.NewWriter(&mtgArchive)
tw := tar.NewWriter(gz)
const mtg = "#!/bin/sh\nexit 0\n"
if err := tw.WriteHeader(&tar.Header{Name: "mtg-multi-1.0.0-linux-amd64/mtg-multi", Mode: 0o755, Size: int64(len(mtg))}); err != nil {
t.Fatal(err)
}
if _, err := tw.Write([]byte(mtg)); err != nil {
t.Fatal(err)
}
if err := tw.Close(); err != nil {
t.Fatal(err)
}
if err := gz.Close(); err != nil {
t.Fatal(err)
}
mtgArchivePath := write("mtg.tar.gz", mtgArchive.Bytes(), 0o600)
write("tools/curl", []byte(`#!/bin/sh
for arg in "$@"; do url=$arg; done
case "$url" in
https://api.github.com/repos/mhsanaei/mtg-multi/releases/latest)
printf '{"tag_name":"v1.0.0"}\n'
;;
*/Xray-*.zip)
cp "$XRAY_ARCHIVE_FIXTURE" "${url##*/}"
;;
*/mtg-multi-*.tar.gz)
cp "$MTG_ARCHIVE_FIXTURE" "${url##*/}"
;;
*/geoip.dat|*/geosite.dat)
target=${url##*/}
if [ "$1" = '-sfLRo' ]; then target=$2; fi
printf 'geodata fixture\n' > "$target"
;;
*) exit 1 ;;
esac
`), 0o755)
cmd := exec.Command("sh", script, "amd64")
cmd.Dir = root
cmd.Env = append(os.Environ(), "PATH="+filepath.Join(root, "tools")+string(os.PathListSeparator)+os.Getenv("PATH"),
"XRAY_ARCHIVE_FIXTURE="+xrayArchivePath, "MTG_ARCHIVE_FIXTURE="+mtgArchivePath)
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("DockerInit: %v\n%s", err, output)
}
app := filepath.Join(root, "app")
if err := os.Rename(filepath.Join(root, "build"), app); err != nil {
t.Fatal(err)
}
activePath := filepath.Join(app, "bin", "xray-linux-amd64")
target, err := os.Readlink(activePath)
if err != nil {
t.Fatalf("working Xray must be a symlink: %v", err)
}
if filepath.IsAbs(target) {
t.Fatalf("build symlink is tied to the builder path: %q", target)
}
output, err := exec.Command(activePath, "-version").CombinedOutput()
if err != nil || string(output) != "Xray 26.10.10 fixture\n" {
t.Fatalf("relocated Xray = %q, %v", output, err)
}
var binaries []string
if err := filepath.WalkDir(app, func(path string, entry fs.DirEntry, err error) error {
if err == nil && entry.Type().IsRegular() && strings.HasPrefix(entry.Name(), "xray") {
relative, relErr := filepath.Rel(app, path)
if relErr != nil {
return relErr
}
binaries = append(binaries, relative)
}
return err
}); err != nil {
t.Fatal(err)
}
if !slices.Equal(binaries, []string{filepath.Join("xray-image", "xray-linux-amd64")}) {
t.Fatalf("physical Xray binaries = %q, want only xray-image/xray-linux-amd64", binaries)
}
}

View file

@ -115,7 +115,7 @@ func localXrayBinaryPath(version string) string {
if !supportedXrayVersion(version) {
return ""
}
for _, path := range []string{xray.GetBinaryPath(), bundledXrayBinaryPath()} {
for _, path := range []string{bundledXrayBinaryPath(), xray.GetBinaryPath()} {
if xrayBinaryVersion(path) == strings.TrimPrefix(version, "v") {
return path
}
@ -146,12 +146,31 @@ func bundledXrayBinaryPath() string {
}
func restoreImageXray() error {
file, err := os.Open(bundledXrayBinaryPath())
imagePath, err := filepath.Abs(bundledXrayBinaryPath())
if err != nil {
return fmt.Errorf("open bundled Xray: %w", err)
return err
}
defer file.Close()
return replaceXrayBinary(file, xray.GetBinaryPath())
image, err := os.Stat(imagePath)
if err != nil {
return fmt.Errorf("stat bundled Xray: %w", err)
}
target := xray.GetBinaryPath()
if active, err := os.Stat(target); err == nil && os.SameFile(image, active) {
return nil
}
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
return err
}
tmpDir, err := os.MkdirTemp(filepath.Dir(target), ".xray-link-*")
if err != nil {
return err
}
defer os.RemoveAll(tmpDir)
linkPath := filepath.Join(tmpDir, xray.GetBinaryName())
if err := os.Symlink(imagePath, linkPath); err != nil {
return err
}
return os.Rename(linkPath, target)
}
func replaceXrayBinary(source io.Reader, target string) error {

View file

@ -134,6 +134,28 @@ func assertXrayVersionStarts(t *testing.T, root, want string) {
}
}
func assertXrayImageLink(t *testing.T, root string) {
t.Helper()
link, err := os.Lstat(xray.GetBinaryPath())
if err != nil {
t.Fatal(err)
}
if link.Mode()&os.ModeSymlink == 0 {
t.Fatalf("working Xray mode = %v, want a symlink to the bundled binary", link.Mode())
}
active, err := os.Stat(xray.GetBinaryPath())
if err != nil {
t.Fatal(err)
}
bundled, err := os.Stat(filepath.Join(root, "xray-image", xray.GetBinaryName()))
if err != nil {
t.Fatal(err)
}
if !os.SameFile(active, bundled) {
t.Fatal("working and bundled Xray paths refer to different physical files")
}
}
func TestUpdateXrayPersistsOnlyContainerSelections(t *testing.T) {
for _, container := range []string{"true", "false"} {
t.Run(container, func(t *testing.T) {
@ -171,6 +193,7 @@ func TestContainerXrayUpdateUsesLocalVersionWithoutNetwork(t *testing.T) {
name, installed, bundled string
}{
{"bundled version", "26.10.9", "26.10.10"},
{"duplicate image version", "26.10.10", "26.10.10"},
{"installed version", "26.10.10", "26.10.11"},
} {
t.Run(tc.name, func(t *testing.T) {
@ -186,6 +209,9 @@ func TestContainerXrayUpdateUsesLocalVersionWithoutNetwork(t *testing.T) {
}
assertXrayVersionStarts(t, root, "26.10.10\n")
assertStoredXrayVersion(t, "v26.10.10")
if tc.bundled == "26.10.10" {
assertXrayImageLink(t, root)
}
})
}
}
@ -229,23 +255,56 @@ func TestXrayVersionListKeepsImageChoiceWhenGitHubIsOffline(t *testing.T) {
func TestXrayLatestRestoresImageOfflineAndDeletesPin(t *testing.T) {
root := setupXrayVersionTest(t)
requests := serveXrayVersion(t, "26.10.10")
writeXrayVersionBinary(t, filepath.Join(root, "xray-image", xray.GetBinaryName()), "26.10.11")
bundledPath := filepath.Join(root, "xray-image", xray.GetBinaryName())
writeXrayVersionBinary(t, bundledPath, "26.10.11")
bundledBefore, err := os.ReadFile(bundledPath)
if err != nil {
t.Fatal(err)
}
svc := &ServerService{}
if err := svc.UpdateXray("latest"); err != nil {
t.Fatal(err)
}
assertXrayImageLink(t, root)
assertXrayVersionStarts(t, root, "26.10.11\n")
if err := svc.UpdateXray("v26.10.10"); err != nil {
t.Fatal(err)
}
assertXrayVersionStarts(t, root, "26.10.10\n")
assertXrayVersionStarts(t, root, "26.10.11\n26.10.10\n")
bundledAfter, err := os.ReadFile(bundledPath)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(bundledBefore, bundledAfter) {
t.Fatal("installing a pinned release overwrote the bundled symlink target")
}
requests.Store(0)
if err := svc.UpdateXray("latest"); err != nil {
t.Fatal(err)
}
assertStoredXrayVersion(t, "")
assertXrayVersionStarts(t, root, "26.10.10\n26.10.11\n")
assertXrayImageLink(t, root)
assertXrayVersionStarts(t, root, "26.10.11\n26.10.10\n26.10.11\n")
if got := requests.Load(); got != 0 {
t.Fatalf("latest made %d network requests, want none", got)
}
}
func TestContainerXrayRestoreDoesNotLinkBundledBinaryToItself(t *testing.T) {
root := setupXrayVersionTest(t)
t.Setenv("XUI_BIN_FOLDER", filepath.Join(root, "xray-image"))
writeXrayVersionBinary(t, xray.GetBinaryPath(), "26.10.11")
svc := &ServerService{}
if err := svc.RestoreXrayVersion(); err != nil {
t.Fatal(err)
}
if err := svc.RestartXrayService(); err != nil {
t.Fatal(err)
}
assertXrayVersionStarts(t, root, "26.10.11\n")
assertStoredXrayVersion(t, "")
}
func TestContainerXrayPinSurvivesRecreationAndRestartBeforeFirstLaunch(t *testing.T) {
root := setupXrayVersionTest(t)
requests := serveXrayVersion(t, "26.10.10")
@ -333,6 +392,9 @@ func TestContainerXrayStartupFallbackPreservesPreferenceAndNativeBinary(t *testi
}
assertXrayVersionStarts(t, root, tc.wantVersion+"\n")
assertStoredXrayVersion(t, tc.pin)
if tc.container == "true" {
assertXrayImageLink(t, root)
}
if got := requests.Load(); got != tc.wantRequests {
t.Fatalf("startup requests = %d, want %d", got, tc.wantRequests)
}

View file

@ -13,6 +13,7 @@ import (
)
func TestMain(m *testing.M) {
runXrayAssetProcessHelper()
// ComputeHotDiff logs the section that blocks a hot apply; the package
// logger must exist before any test exercises a blocked path.
xuilogger.InitLogger(logging.ERROR)

View file

@ -320,7 +320,7 @@ func (p *Process) SetConfig(config *Config) {
// keeping it in step after a hot apply (Start only writes it on a cold start).
func (p *Process) PersistConfig() error {
p.mu.RLock()
data, err := json.MarshalIndent(p.config, "", " ")
data, err := marshalProcessConfig(p.config)
path := p.configPath
p.mu.RUnlock()
if err != nil {
@ -619,7 +619,7 @@ func (p *process) Start() (err error) {
}
}()
data, err := json.MarshalIndent(p.config, "", " ")
data, err := marshalProcessConfig(p.config)
if err != nil {
return common.NewErrorf("Failed to generate XRAY configuration files: %v", err)
}
@ -639,6 +639,12 @@ func (p *process) Start() (err error) {
}
cmd := exec.CommandContext(context.Background(), GetBinaryPath(), "-c", configPath)
if os.Getenv("XUI_IN_DOCKER") == "true" {
cmd.Env, err = containerXrayEnv()
if err != nil {
return err
}
}
cmd.Stdout = p.logWriter
cmd.Stderr = p.logWriter

View file

@ -0,0 +1,216 @@
package xray
import (
"encoding/json"
"fmt"
"os"
"os/signal"
"path/filepath"
"runtime"
"syscall"
"testing"
"time"
"github.com/xtls/xray-core/common/platform"
"github.com/xtls/xray-core/common/platform/filesystem"
)
func runXrayAssetProcessHelper() {
if os.Getenv("XUI_TEST_XRAY_ASSET_HELPER") == "1" {
if len(os.Args) > 1 && os.Args[1] == "-version" {
fmt.Println("Xray 26.10.10 fixture")
os.Exit(0)
}
stopped := make(chan os.Signal, 1)
signal.Notify(stopped, syscall.SIGTERM)
data, err := readXrayProcessResources()
if err != nil {
data = fmt.Appendf(nil, "asset lookup failed: %v", err)
}
if writeErr := os.WriteFile(os.Getenv("XUI_TEST_XRAY_ASSET_RESULT"), data, 0o600); writeErr != nil {
fmt.Fprintln(os.Stderr, writeErr)
os.Exit(1)
}
if err != nil {
os.Exit(1)
}
<-stopped
os.Exit(0)
}
}
func readXrayProcessResources() ([]byte, error) {
data, err := os.ReadFile(os.Args[2])
if err != nil {
return nil, err
}
var c Config
if err := json.Unmarshal(data, &c); err != nil {
return nil, err
}
var env map[string]string
if len(c.Env) > 0 {
if err := json.Unmarshal(c.Env, &env); err != nil {
return nil, err
}
}
for key, value := range env {
if err := os.Setenv(key, value); err != nil {
return nil, err
}
}
data, err = os.ReadFile(platform.GetAssetLocation("geosite.dat"))
if err != nil {
return nil, err
}
cert, err := filesystem.ReadCert("certificate.pem")
if err != nil {
return nil, err
}
data = append(append(data, '\n'), cert...)
for _, section := range [][]byte{c.DNSConfig, c.RouterConfig} {
var cfg struct{ Script string }
if len(section) == 0 {
continue
}
if err := json.Unmarshal(section, &cfg); err != nil {
return nil, err
}
if cfg.Script == "" {
continue
}
path, err := platform.ResolveLuaFile(cfg.Script)
if err != nil {
return nil, err
}
script, err := os.ReadFile(path)
if err != nil {
return nil, err
}
data = append(append(data, '\n'), script...)
}
return data, nil
}
func TestSymlinkedContainerXrayFindsConfiguredResources(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("test executable uses Unix signals and symlinks")
}
executable, err := os.Executable()
if err != nil {
t.Fatal(err)
}
binary, err := os.ReadFile(executable)
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
name, container, override, want string
scriptLocation string
}{
{name: "container default", container: "true", want: "geodata in bin\ncertificate in bin"},
{name: "container explicit assets", container: "true", override: "XRAY_LOCATION_ASSET", want: "configured shared geodata\ncertificate in bin"},
{name: "container dotted asset setting", container: "true", override: "xray.location.asset", want: "configured shared geodata\ncertificate in bin"},
{name: "container explicit certificates", container: "true", override: "XRAY_LOCATION_CERT", want: "geodata in bin\nconfigured shared certificate"},
{name: "container dotted certificate setting", container: "true", override: "xray.location.cert", want: "geodata in bin\nconfigured shared certificate"},
{name: "native default", container: "false", want: "geodata in bin\ncertificate in bin"},
{name: "container scripts beside working binary", container: "true", scriptLocation: "bin", want: "geodata in bin\ncertificate in bin\ndns in bin\nrouting in bin"},
{name: "container scripts prefer working directory", container: "true", scriptLocation: "cwd", want: "geodata in bin\ncertificate in bin\ndns in cwd\nrouting in cwd"},
{name: "container scripts prefer config environment", container: "true", scriptLocation: "env", want: "geodata in bin\ncertificate in bin\ndns in shared\nrouting in shared"},
{name: "native scripts beside binary", container: "false", scriptLocation: "bin", want: "geodata in bin\ncertificate in bin\ndns in bin\nrouting in bin"},
} {
t.Run(tc.name, func(t *testing.T) {
root := t.TempDir()
binDir := filepath.Join(root, "bin")
imageDir := filepath.Join(root, "xray-image")
sharedDir := filepath.Join(root, "shared-assets")
for _, dir := range []string{binDir, imageDir, sharedDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
}
for _, key := range []string{"XRAY_LOCATION_ASSET", "xray.location.asset", "XRAY_LOCATION_CERT", "xray.location.cert", "XRAY_LOCATION_CONFIG", "xray.location.config", "XRAY_LOCATION_CONFDIR", "xray.location.confdir"} {
t.Setenv(key, "")
if err := os.Unsetenv(key); err != nil {
t.Fatal(err)
}
}
t.Setenv("XUI_BIN_FOLDER", binDir)
t.Setenv("XUI_LOG_FOLDER", filepath.Join(root, "log"))
t.Setenv("XUI_IN_DOCKER", tc.container)
t.Setenv("XUI_TEST_XRAY_ASSET_HELPER", "1")
resultPath := filepath.Join(root, "asset-result")
t.Setenv("XUI_TEST_XRAY_ASSET_RESULT", resultPath)
if tc.override != "" {
t.Setenv(tc.override, sharedDir)
}
for path, content := range map[string]string{
filepath.Join(binDir, "geosite.dat"): "geodata in bin",
filepath.Join(sharedDir, "geosite.dat"): "configured shared geodata",
filepath.Join(binDir, "certificate.pem"): "certificate in bin",
filepath.Join(sharedDir, "certificate.pem"): "configured shared certificate",
} {
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatal(err)
}
}
physicalPath := GetBinaryPath()
if tc.container == "true" {
physicalPath = filepath.Join(imageDir, GetBinaryName())
}
if err := os.WriteFile(physicalPath, binary, 0o755); err != nil {
t.Fatal(err)
}
if tc.container == "true" {
if err := os.Symlink(physicalPath, GetBinaryPath()); err != nil {
t.Fatal(err)
}
}
c := &Config{}
if tc.scriptLocation != "" {
t.Chdir(root)
c.DNSConfig = []byte(`{"script":"dns.lua"}`)
c.RouterConfig = []byte(`{"script":"routing.lua"}`)
locations := map[string]string{binDir: "bin"}
if tc.scriptLocation == "cwd" || tc.scriptLocation == "env" {
locations[root] = "cwd"
}
if tc.scriptLocation == "env" {
locations[sharedDir] = "shared"
t.Setenv("XRAY_LOCATION_CONFDIR", binDir)
c.Env, err = json.Marshal(map[string]string{"XRAY_LOCATION_CONFDIR": sharedDir})
if err != nil {
t.Fatal(err)
}
}
for dir, label := range locations {
for _, name := range []string{"dns", "routing"} {
if err := os.WriteFile(filepath.Join(dir, name+".lua"), []byte(name+" in "+label), 0o600); err != nil {
t.Fatal(err)
}
}
}
}
p := NewProcess(c)
t.Cleanup(func() { _ = p.Stop() })
if err := p.Start(); err != nil {
t.Fatal(err)
}
deadline := time.Now().Add(3 * time.Second)
var result []byte
for time.Now().Before(deadline) {
result, _ = os.ReadFile(resultPath)
if len(result) > 0 {
break
}
time.Sleep(10 * time.Millisecond)
}
if string(result) != tc.want {
t.Fatalf("Xray resource lookup = %q, want %q", result, tc.want)
}
if tc.scriptLocation != "" && (string(c.DNSConfig) != `{"script":"dns.lua"}` || string(c.RouterConfig) != `{"script":"routing.lua"}`) {
t.Fatalf("starting Xray mutated its config snapshot: dns=%s routing=%s", c.DNSConfig, c.RouterConfig)
}
})
}
}

View file

@ -0,0 +1,97 @@
package xray
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"github.com/xtls/xray-core/common/platform"
"github.com/mhsanaei/3x-ui/v3/internal/config"
"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
)
func containerXrayEnv() ([]string, error) {
binDir, err := filepath.Abs(config.GetBinFolderPath())
if err != nil {
return nil, err
}
env := os.Environ()
// A symlink changes os.Executable's directory; keep resource lookup beside the working path.
for _, name := range []string{platform.AssetLocation, platform.CertLocation} {
alternate := platform.NormalizeEnvName(name)
if _, exists := os.LookupEnv(name); exists {
continue
}
if _, exists := os.LookupEnv(alternate); !exists {
env = append(env, alternate+"="+binDir)
}
}
return env, nil
}
func marshalProcessConfig(c *Config) ([]byte, error) {
if os.Getenv("XUI_IN_DOCKER") == "true" && c != nil {
var env map[string]string
if len(c.Env) == 0 || json.Unmarshal(c.Env, &env) == nil {
lookup := func(name string) string {
for _, key := range []string{name, platform.NormalizeEnvName(name)} {
if value, exists := env[key]; exists {
return value
}
if value, exists := os.LookupEnv(key); exists {
return value
}
}
return ""
}
dirs := []string{lookup(platform.ConfdirLocation), lookup(platform.ConfigLocation), ".", config.GetBinFolderPath()}
prepared := *c
prepared.DNSConfig = resolveContainerLuaScript(c.DNSConfig, dirs)
prepared.RouterConfig = resolveContainerLuaScript(c.RouterConfig, dirs)
c = &prepared
}
}
return json.MarshalIndent(c, "", " ")
}
func resolveContainerLuaScript(section json_util.RawMessage, dirs []string) json_util.RawMessage {
var cfg struct{ Script string }
if len(section) == 0 || json.Unmarshal(section, &cfg) != nil || cfg.Script == "" || filepath.IsAbs(cfg.Script) {
return section
}
// Preserve Xray's lookup order, with the working binary directory as its final fallback.
for _, dir := range dirs {
if dir == "" {
continue
}
path, err := filepath.Abs(filepath.Join(dir, cfg.Script))
if err != nil {
return section
}
info, err := os.Stat(path)
if os.IsNotExist(err) {
continue
}
if err != nil || !info.Mode().IsRegular() {
return section
}
var fields map[string]json.RawMessage
if json.Unmarshal(section, &fields) != nil {
return section
}
for key := range fields {
if strings.EqualFold(key, "script") {
delete(fields, key)
}
}
fields["script"], _ = json.Marshal(path)
data, err := json.Marshal(fields)
if err != nil {
return section
}
return data
}
return section
}