3x-ui/web/runtime/local.go
MHSanaei 7cd26a0583
Some checks are pending
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
v3
2026-05-10 02:13:42 +02:00

137 lines
4.3 KiB
Go

package runtime
import (
"context"
"encoding/json"
"errors"
"sync"
"github.com/mhsanaei/3x-ui/v3/database/model"
"github.com/mhsanaei/3x-ui/v3/xray"
)
// LocalDeps wires the runtime to the panel's xray process and the
// service.XrayService restart trigger via callbacks. We use callbacks
// (not an interface to *service.XrayService) because the runtime
// package would otherwise cycle-import service.
type LocalDeps struct {
// APIPort returns the xray gRPC API port the local engine is
// currently listening on. Returns 0 when xray isn't running yet —
// callers should treat that as a transient error.
APIPort func() int
// SetNeedRestart trips the panel's "restart xray on next cron tick"
// flag. Mirrors how InboundController.addInbound calls
// xrayService.SetToNeedRestart() today.
SetNeedRestart func()
}
// Local implements Runtime against the panel's own xray process. Each
// call follows the existing inbound.go pattern: open a gRPC client,
// run one operation, close. Per-call init keeps the connection state
// scoped so a stuck call can't leak across operations.
type Local struct {
deps LocalDeps
// Serialise gRPC operations — xray's HandlerService isn't documented
// as concurrent-safe and the existing InboundService implicitly
// runs one op at a time per request. This matches that.
mu sync.Mutex
}
// NewLocal builds a Local runtime. deps.APIPort and deps.SetNeedRestart
// are required; callers that want a no-op restart can pass `func(){}`.
func NewLocal(deps LocalDeps) *Local {
return &Local{deps: deps}
}
func (l *Local) Name() string { return "local" }
// withAPI runs fn against a freshly-initialised XrayAPI client and
// guarantees Close() afterwards. Returns an error if the gRPC port
// isn't available yet (xray still starting / stopped).
func (l *Local) withAPI(fn func(api *xray.XrayAPI) error) error {
l.mu.Lock()
defer l.mu.Unlock()
port := l.deps.APIPort()
if port <= 0 {
return errors.New("local xray is not running")
}
var api xray.XrayAPI
if err := api.Init(port); err != nil {
return err
}
defer api.Close()
return fn(&api)
}
func (l *Local) AddInbound(_ context.Context, ib *model.Inbound) error {
body, err := json.MarshalIndent(ib.GenXrayInboundConfig(), "", " ")
if err != nil {
return err
}
return l.withAPI(func(api *xray.XrayAPI) error {
return api.AddInbound(body)
})
}
func (l *Local) DelInbound(_ context.Context, ib *model.Inbound) error {
return l.withAPI(func(api *xray.XrayAPI) error {
return api.DelInbound(ib.Tag)
})
}
func (l *Local) UpdateInbound(ctx context.Context, oldIb, newIb *model.Inbound) error {
// xray-core has no in-place inbound update — drop and re-add.
// Matches what InboundService.UpdateInbound did inline.
if err := l.DelInbound(ctx, oldIb); err != nil {
// Best-effort: continue to AddInbound so a transient remove
// failure (e.g. inbound already gone) doesn't strand us. The
// caller's needRestart fallback will reconcile from config.
_ = err
}
if !newIb.Enable {
// Disabled inbounds aren't pushed to xray; we already removed
// the old one above.
return nil
}
return l.AddInbound(ctx, newIb)
}
func (l *Local) AddUser(_ context.Context, ib *model.Inbound, userMap map[string]any) error {
return l.withAPI(func(api *xray.XrayAPI) error {
return api.AddUser(string(ib.Protocol), ib.Tag, userMap)
})
}
func (l *Local) RemoveUser(_ context.Context, ib *model.Inbound, email string) error {
return l.withAPI(func(api *xray.XrayAPI) error {
return api.RemoveUser(ib.Tag, email)
})
}
func (l *Local) RestartXray(_ context.Context) error {
if l.deps.SetNeedRestart != nil {
l.deps.SetNeedRestart()
}
return nil
}
// Reset methods are intentional no-ops for Local. The central DB UPDATE
// that runs in InboundService.Reset* before this call has already zeroed
// the counters that xray reads; on the next stats poll the gRPC service
// will pick up matching values. Pre-Phase-1 the panel never issued an
// xrayApi reset call here either — keeping the same shape avoids a
// behaviour change for single-panel users.
func (l *Local) ResetClientTraffic(_ context.Context, _ *model.Inbound, _ string) error {
return nil
}
func (l *Local) ResetInboundClientTraffics(_ context.Context, _ *model.Inbound) error {
return nil
}
func (l *Local) ResetAllTraffics(_ context.Context) error {
return nil
}