merge feat/agent-split into main (V2.3.0)
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
// Command xui-agent is the standalone Linux build of the 3x-ui node
|
||||
// agent (agent/panel split, phase 1). Until the split, nodes ran the
|
||||
// panel binary under the "agent" subcommand, shipping the whole web
|
||||
// panel and the embedded frontend to every node. This entry links only
|
||||
// the agent's own dependency closure (internal/agent plus config,
|
||||
// logger, selfupdate, xray-api types and two util leaves), so the node
|
||||
// artifact is a fraction of the panel binary while remaining a drop-in
|
||||
// replacement for it:
|
||||
//
|
||||
// - invocation is unchanged: service units run
|
||||
// "xui-agent agent [-config /etc/x-ui/agent.json]" (the bare
|
||||
// "-config" form is also accepted for manual runs);
|
||||
// - agent.json, the traffic baseline and the other state files keep
|
||||
// their names, locations and formats;
|
||||
// - the wire protocol with the master (register / heartbeat /
|
||||
// config / traffic / probe-report / update intent) is byte-for-
|
||||
// byte the internal/agent implementation the dual-mode binary ran.
|
||||
//
|
||||
// The agent lifecycle wiring below mirrors main.go's agent branch
|
||||
// exactly — the same internal/agent package does the work either way,
|
||||
// and cmd/xui-agent-windows is the Windows sibling of this entry.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/agent"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/config"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
||||
|
||||
"github.com/op/go-logging"
|
||||
)
|
||||
|
||||
func main() {
|
||||
args := os.Args[1:]
|
||||
|
||||
// Version self-report: the agent's own display version (A-form),
|
||||
// from the same build-time displayVersion injection the panel
|
||||
// uses. Kept ahead of the subcommand so it works however the
|
||||
// binary was invoked.
|
||||
if len(args) > 0 {
|
||||
switch args[0] {
|
||||
case "-v", "--version", "version":
|
||||
fmt.Println(config.GetAgentDisplayVersion())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Service units (systemd / OpenRC / procd / nohup fallback in
|
||||
// install-agent.sh) invoke the binary with the historical "agent"
|
||||
// subcommand; consume it so those units keep working unchanged.
|
||||
if len(args) > 0 && args[0] == "agent" {
|
||||
args = args[1:]
|
||||
} else if len(args) > 0 && args[0][0] != '-' {
|
||||
fmt.Fprintf(os.Stderr, "xui-agent: unknown subcommand %q (this binary only runs the node agent)\n", args[0])
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
runAgent(args)
|
||||
}
|
||||
|
||||
// runAgent is main.go's agent branch, verbatim in behaviour: same
|
||||
// flags, same defaults, same shutdown wiring.
|
||||
func runAgent(args []string) {
|
||||
agentCmd := flag.NewFlagSet("agent", flag.ExitOnError)
|
||||
var configPath string
|
||||
agentCmd.StringVar(&configPath, "config", "/etc/x-ui/agent.json", "Path to agent config file")
|
||||
agentCmd.Parse(args)
|
||||
|
||||
// Agent mode always uses the panel's directory layout for the
|
||||
// xray binary, geo files and config.json. Scoped to agent mode
|
||||
// only so the panel's own relative "bin" default is untouched.
|
||||
if os.Getenv("XUI_BIN_FOLDER") == "" {
|
||||
_ = os.Setenv("XUI_BIN_FOLDER", agent.DefaultBinFolder)
|
||||
}
|
||||
|
||||
cfg, err := agent.LoadConfig(configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load agent config from %s: %v", configPath, err)
|
||||
}
|
||||
|
||||
// The acknowledged traffic baseline persists next to the agent
|
||||
// config (its state home, e.g. /etc/x-ui), unless the config
|
||||
// names a path.
|
||||
if cfg.BaselinePath == "" {
|
||||
cfg.BaselinePath = filepath.Join(filepath.Dir(configPath), "agent-traffic-baseline.json")
|
||||
}
|
||||
|
||||
// Initialize logger
|
||||
logger.InitLogger(logging.INFO)
|
||||
log.Printf("Starting 3x-ui agent (node: %s)", cfg.NodeName)
|
||||
|
||||
ag := agent.New(cfg)
|
||||
|
||||
// Handle shutdown signals
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-sigCh
|
||||
log.Println("Shutting down agent...")
|
||||
ag.Stop()
|
||||
}()
|
||||
|
||||
if err := ag.Run(); err != nil {
|
||||
log.Fatalf("Agent error: %v", err)
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -81,8 +81,13 @@ esac
|
||||
# sing-box 核心在 NexusGate 仓的专用发行里(见下方 SINGBOX_BASE)。
|
||||
# 注意:本脚本是 V2 版——只装 sing-box 运行时;主控未升 V2 前不要用它
|
||||
# 重装节点(节点升级走面板一键更新,不需要本脚本)。
|
||||
#
|
||||
# agent/主控拆分(期一):下载的是 agent 专用产物 x-ui-agent-linux-<arch>
|
||||
# (独立入口 cmd/xui-agent,不再是主控双模式二进制)。该资产自拆分版
|
||||
# 发行起才有——RELEASE_BASE 的升版由发版步完成,指向拆分版即可用;
|
||||
# 若需装拆分前的旧版节点,显式 XUI_URL 指回 x-ui-linux-<arch> 即可。
|
||||
RELEASE_BASE="https://git.881218.xyz/rogue/3x-ui-agent/releases/download/V2.2.1"
|
||||
XUI_URL="${XUI_URL:-${RELEASE_BASE}/x-ui-linux-${BIN_ARCH}}"
|
||||
XUI_URL="${XUI_URL:-${RELEASE_BASE}/x-ui-agent-linux-${BIN_ARCH}}"
|
||||
|
||||
ROOT="${XUI_ROOT:-}"
|
||||
XUI_HOME="${ROOT}/usr/local/x-ui"
|
||||
|
||||
@@ -151,7 +151,12 @@ func (a *Agent) runUpdate(intent updateIntent) {
|
||||
fail(fmt.Errorf("locate own binary: %w", err))
|
||||
return
|
||||
}
|
||||
asset, err := selfupdate.AssetName(runtime.GOARCH)
|
||||
// Agent/panel split (phase 1): the agent downloads the standalone
|
||||
// agent asset, not the panel binary. An agent build predating the
|
||||
// split still asks for the panel asset (AssetName) — the two are
|
||||
// interchangeable at the same path, so each generation converges
|
||||
// on the form its own build requests.
|
||||
asset, err := selfupdate.AgentAssetName(runtime.GOARCH)
|
||||
if err != nil {
|
||||
fail(err)
|
||||
return
|
||||
|
||||
@@ -22,7 +22,9 @@ import (
|
||||
|
||||
func fakeReleaseServer(t *testing.T, payload []byte, goodSum bool, hits *atomic.Int32) *httptest.Server {
|
||||
t.Helper()
|
||||
asset, err := selfupdate.AssetName(runtime.GOARCH)
|
||||
// The agent requests the standalone agent asset (agent/panel
|
||||
// split); the fake release only serves that name.
|
||||
asset, err := selfupdate.AgentAssetName(runtime.GOARCH)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -20,8 +20,10 @@ import (
|
||||
)
|
||||
|
||||
// DefaultBaseURL is the release download base of the fork's own Gitea.
|
||||
// A release tag directory hangs directly below it:
|
||||
// <base><tag>/x-ui-linux-<arch> plus a "<asset>.sha256" sidecar.
|
||||
// A release tag directory hangs directly below it, holding the panel
|
||||
// assets (<base><tag>/x-ui-linux-<arch>) and — since the agent/panel
|
||||
// split — the standalone agent assets (x-ui-agent-linux-<arch>),
|
||||
// each with a "<asset>.sha256" sidecar.
|
||||
const DefaultBaseURL = "https://git.881218.xyz/rogue/3x-ui-agent/releases/download/"
|
||||
|
||||
// LatestReleaseAPI is the Gitea API endpoint for the newest release of the
|
||||
@@ -41,7 +43,9 @@ func BaseURL() string {
|
||||
return DefaultBaseURL
|
||||
}
|
||||
|
||||
// AssetName maps a GOARCH to the release asset name published for it.
|
||||
// AssetName maps a GOARCH to the release asset name published for it:
|
||||
// the panel (dual-mode) binary. The panel's own native self-update
|
||||
// downloads this asset; pre-split agents consumed it too.
|
||||
func AssetName(goarch string) (string, error) {
|
||||
switch goarch {
|
||||
case "amd64":
|
||||
@@ -53,6 +57,24 @@ func AssetName(goarch string) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// AgentAssetName maps a GOARCH to the standalone agent asset published
|
||||
// since the agent/panel split (phase 1). A split agent's one-click
|
||||
// update downloads this asset — never the panel binary — so a node
|
||||
// keeps its slim form across updates. Releases predating the split do
|
||||
// not carry these assets; an intent pointing at such a tag fails the
|
||||
// download and the agent keeps running its old binary (the designed
|
||||
// failure mode of runUpdate).
|
||||
func AgentAssetName(goarch string) (string, error) {
|
||||
switch goarch {
|
||||
case "amd64":
|
||||
return "x-ui-agent-linux-amd64", nil
|
||||
case "arm64":
|
||||
return "x-ui-agent-linux-arm64", nil
|
||||
default:
|
||||
return "", fmt.Errorf("no agent release asset for architecture %q", goarch)
|
||||
}
|
||||
}
|
||||
|
||||
// LatestReleaseURLEnv overrides the release-lookup endpoint as a whole
|
||||
// (mirrors, tests), analogous to BaseURLEnv.
|
||||
const LatestReleaseURLEnv = "XUI_RELEASE_LATEST_URL"
|
||||
|
||||
@@ -47,6 +47,18 @@ func TestAssetName(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentAssetName(t *testing.T) {
|
||||
if name, err := AgentAssetName("amd64"); err != nil || name != "x-ui-agent-linux-amd64" {
|
||||
t.Fatalf("amd64: %q, %v", name, err)
|
||||
}
|
||||
if name, err := AgentAssetName("arm64"); err != nil || name != "x-ui-agent-linux-arm64" {
|
||||
t.Fatalf("arm64: %q, %v", name, err)
|
||||
}
|
||||
if _, err := AgentAssetName("riscv64"); err == nil {
|
||||
t.Fatal("unknown arch must error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwapExecutableKeepsBackup(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
exe := filepath.Join(dir, "xui-agent")
|
||||
|
||||
Reference in New Issue
Block a user