mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-24 06:45:01 +03:00
fix(web): stop clean shutdowns logging false errors, and let the panel stop its children (#6884)
A systemd stop or restart logged "Server shutdown failed: use of closed network connection | xray is not running" on every clean exit. httpServer.Shutdown already closes the listener, so the second Close's net.ErrClosed is now ignored, and StopXray's not-running case is a sentinel (ErrXrayNotRunning) that Stop treats as done. The shipped units now use KillMode=mixed, so SIGTERM reaches only the panel, which stops Xray and the sidecars itself; systemd still SIGKILLs whatever is left after the stop timeout.
This commit is contained in:
@@ -2,7 +2,6 @@ package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@@ -78,6 +77,9 @@ func currentXrayProcess() *xray.Process {
|
||||
return process
|
||||
}
|
||||
|
||||
// ErrXrayNotRunning reports an operation that requires a running core when xray is stopped.
|
||||
var ErrXrayNotRunning = xray.ErrXrayNotRunning
|
||||
|
||||
// XrayService provides business logic for Xray process management.
|
||||
// It handles starting, stopping, restarting Xray, and managing its configuration.
|
||||
type XrayService struct {
|
||||
@@ -1245,7 +1247,7 @@ func stripDisabledRules(routerCfg json_util.RawMessage) json_util.RawMessage {
|
||||
func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic, error) {
|
||||
process := currentXrayProcess()
|
||||
if process == nil || !process.IsRunning() {
|
||||
err := errors.New("xray is not running")
|
||||
err := ErrXrayNotRunning
|
||||
logger.Debug("Attempted to fetch Xray traffic, but Xray is not running:", err)
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -1355,7 +1357,7 @@ func (s *XrayService) OverrideBalancer(tag, target string) error {
|
||||
defer lock.Unlock()
|
||||
process := currentXrayProcess()
|
||||
if process == nil || !process.IsRunning() {
|
||||
return errors.New("xray is not running")
|
||||
return ErrXrayNotRunning
|
||||
}
|
||||
if err := s.xrayAPI.Init(process.GetAPIPort()); err != nil {
|
||||
return err
|
||||
@@ -1369,7 +1371,7 @@ func (s *XrayService) OverrideBalancer(tag, target string) error {
|
||||
func (s *XrayService) TestRoute(req xray.RouteTestRequest) (*xray.RouteTestResult, error) {
|
||||
process := currentXrayProcess()
|
||||
if process == nil || !process.IsRunning() {
|
||||
return nil, errors.New("xray is not running")
|
||||
return nil, ErrXrayNotRunning
|
||||
}
|
||||
if err := s.xrayAPI.Init(process.GetAPIPort()); err != nil {
|
||||
return nil, err
|
||||
@@ -1611,7 +1613,7 @@ func (s *XrayService) StopXray() error {
|
||||
if process != nil && process.IsRunning() {
|
||||
return process.Stop()
|
||||
}
|
||||
return errors.New("xray is not running")
|
||||
return ErrXrayNotRunning
|
||||
}
|
||||
|
||||
// SetToNeedRestart marks that Xray needs to be restarted.
|
||||
|
||||
+5
-2
@@ -787,7 +787,7 @@ func (s *Server) stop(stopXray bool, stopTgBot bool) error {
|
||||
amneziawgnet.GetOutboundManager().StopAll()
|
||||
}
|
||||
if stopXray {
|
||||
if err := s.xrayService.StopXray(); err != nil {
|
||||
if err := s.xrayService.StopXray(); err != nil && !errors.Is(err, service.ErrXrayNotRunning) {
|
||||
err2 = common.Combine(err2, err)
|
||||
}
|
||||
}
|
||||
@@ -811,7 +811,10 @@ func (s *Server) stop(stopXray bool, stopTgBot bool) error {
|
||||
s.wsHub.Stop()
|
||||
}
|
||||
if s.listener != nil {
|
||||
err1 = common.Combine(err1, s.listener.Close())
|
||||
// httpServer.Shutdown already closes the listener; ignore closed errors.
|
||||
if err := s.listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
|
||||
err1 = common.Combine(err1, err)
|
||||
}
|
||||
}
|
||||
return common.Combine(err1, err2)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestServerStopWhenXrayNotRunningSucceeds(t *testing.T) {
|
||||
t.Setenv("XUI_DB_FOLDER", t.TempDir())
|
||||
s := NewServer()
|
||||
if err := s.Stop(); err != nil {
|
||||
t.Fatalf("expected nil error when stopping server with unstarted xray, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerStopPanelOnlyWithPreclosedListenerSucceeds(t *testing.T) {
|
||||
t.Setenv("XUI_DB_FOLDER", t.TempDir())
|
||||
s := NewServer()
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("net.Listen failed: %v", err)
|
||||
}
|
||||
s.listener = ln
|
||||
s.httpServer = &http.Server{}
|
||||
go func() { _ = s.httpServer.Serve(ln) }()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
_ = s.httpServer.Shutdown(shutdownCtx)
|
||||
|
||||
if err := s.StopPanelOnly(); err != nil {
|
||||
t.Fatalf("expected nil error when listener is already closed, got: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"math/big"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -75,7 +74,7 @@ func TestGracefulShutdownPersistsFinalTuicTraffic(t *testing.T) {
|
||||
t.Fatal("TUIC listener did not start")
|
||||
}
|
||||
|
||||
if err := NewServer().Stop(); err != nil && !strings.Contains(err.Error(), "xray is not running") {
|
||||
if err := NewServer().Stop(); err != nil {
|
||||
t.Fatalf("graceful server shutdown: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -227,6 +227,8 @@ var (
|
||||
xrayVersionTimeout = 5 * time.Second
|
||||
// OnCrash is called when xray crashes unexpectedly. Set from web layer.
|
||||
OnCrash func(err error)
|
||||
// ErrXrayNotRunning reports an operation that requires a running core when xray is stopped.
|
||||
ErrXrayNotRunning = errors.New("xray is not running")
|
||||
)
|
||||
|
||||
// newProcess creates a new internal process struct for Xray.
|
||||
@@ -758,7 +760,7 @@ func (p *process) waitForCommand(cmd *exec.Cmd, done chan struct{}) {
|
||||
// Stop terminates the running Xray process.
|
||||
func (p *process) Stop() error {
|
||||
if !p.IsRunning() {
|
||||
return errors.New("xray is not running")
|
||||
return ErrXrayNotRunning
|
||||
}
|
||||
p.intentionalStop.Store(true)
|
||||
|
||||
@@ -768,7 +770,7 @@ func (p *process) Stop() error {
|
||||
cmd := p.cmd
|
||||
p.mu.RUnlock()
|
||||
if cmd == nil || cmd.Process == nil {
|
||||
return errors.New("xray is not running")
|
||||
return ErrXrayNotRunning
|
||||
}
|
||||
|
||||
// Remove temporary config file used for test runs so main config is never touched
|
||||
|
||||
@@ -96,6 +96,15 @@ func TestIntentionalStopDoesNotRecordExitError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStopWhenNotRunningReturnsErrXrayNotRunning(t *testing.T) {
|
||||
initProcessTestLogger(t)
|
||||
|
||||
p := NewProcess(&Config{})
|
||||
if err := p.Stop(); !errors.Is(err, ErrXrayNotRunning) {
|
||||
t.Fatalf("Stop() on unstarted process err = %v, want ErrXrayNotRunning", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStopKillsProcessThatIgnoresSIGTERM(t *testing.T) {
|
||||
initProcessTestLogger(t)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ ExecStart=/usr/local/x-ui/x-ui
|
||||
ExecReload=/bin/kill -USR1 $MAINPID
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
KillMode=mixed
|
||||
|
||||
# The panel intentionally stays root: it supervises the Xray child processes,
|
||||
# edits netfilter state and reads TLS private keys. These settings only bound
|
||||
|
||||
@@ -14,6 +14,7 @@ ExecStart=/usr/local/x-ui/x-ui
|
||||
ExecReload=/bin/kill -USR1 $MAINPID
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
KillMode=mixed
|
||||
|
||||
# The panel intentionally stays root: it supervises the Xray child processes,
|
||||
# edits netfilter state and reads TLS private keys. These settings only bound
|
||||
|
||||
@@ -14,6 +14,7 @@ ExecStart=/usr/local/x-ui/x-ui
|
||||
ExecReload=/bin/kill -USR1 $MAINPID
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
KillMode=mixed
|
||||
|
||||
# The panel intentionally stays root: it supervises the Xray child processes,
|
||||
# edits netfilter state and reads TLS private keys. These settings only bound
|
||||
|
||||
Reference in New Issue
Block a user