mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-29 06:31:58 +00: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:
1 parent
40cea0c2fe
commit
ad470681ef
9 files changed
+67
-11
No files matched your search
@@ -2,7 +2,6 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -78,6 +77,9 @@ func currentXrayProcess() *xray.Process {
|
|||||||
return 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.
|
// XrayService provides business logic for Xray process management.
|
||||||
// It handles starting, stopping, restarting Xray, and managing its configuration.
|
// It handles starting, stopping, restarting Xray, and managing its configuration.
|
||||||
type XrayService struct {
|
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) {
|
func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic, error) {
|
||||||
process := currentXrayProcess()
|
process := currentXrayProcess()
|
||||||
if process == nil || !process.IsRunning() {
|
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)
|
logger.Debug("Attempted to fetch Xray traffic, but Xray is not running:", err)
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -1355,7 +1357,7 @@ func (s *XrayService) OverrideBalancer(tag, target string) error {
|
|||||||
defer lock.Unlock()
|
defer lock.Unlock()
|
||||||
process := currentXrayProcess()
|
process := currentXrayProcess()
|
||||||
if process == nil || !process.IsRunning() {
|
if process == nil || !process.IsRunning() {
|
||||||
return errors.New("xray is not running")
|
return ErrXrayNotRunning
|
||||||
}
|
}
|
||||||
if err := s.xrayAPI.Init(process.GetAPIPort()); err != nil {
|
if err := s.xrayAPI.Init(process.GetAPIPort()); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1369,7 +1371,7 @@ func (s *XrayService) OverrideBalancer(tag, target string) error {
|
|||||||
func (s *XrayService) TestRoute(req xray.RouteTestRequest) (*xray.RouteTestResult, error) {
|
func (s *XrayService) TestRoute(req xray.RouteTestRequest) (*xray.RouteTestResult, error) {
|
||||||
process := currentXrayProcess()
|
process := currentXrayProcess()
|
||||||
if process == nil || !process.IsRunning() {
|
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 {
|
if err := s.xrayAPI.Init(process.GetAPIPort()); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1611,7 +1613,7 @@ func (s *XrayService) StopXray() error {
|
|||||||
if process != nil && process.IsRunning() {
|
if process != nil && process.IsRunning() {
|
||||||
return process.Stop()
|
return process.Stop()
|
||||||
}
|
}
|
||||||
return errors.New("xray is not running")
|
return ErrXrayNotRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetToNeedRestart marks that Xray needs to be restarted.
|
// 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()
|
amneziawgnet.GetOutboundManager().StopAll()
|
||||||
}
|
}
|
||||||
if stopXray {
|
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)
|
err2 = common.Combine(err2, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -811,7 +811,10 @@ func (s *Server) stop(stopXray bool, stopTgBot bool) error {
|
|||||||
s.wsHub.Stop()
|
s.wsHub.Stop()
|
||||||
}
|
}
|
||||||
if s.listener != nil {
|
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)
|
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"
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -75,7 +74,7 @@ func TestGracefulShutdownPersistsFinalTuicTraffic(t *testing.T) {
|
|||||||
t.Fatal("TUIC listener did not start")
|
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)
|
t.Fatalf("graceful server shutdown: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -227,6 +227,8 @@ var (
|
|||||||
xrayVersionTimeout = 5 * time.Second
|
xrayVersionTimeout = 5 * time.Second
|
||||||
// OnCrash is called when xray crashes unexpectedly. Set from web layer.
|
// OnCrash is called when xray crashes unexpectedly. Set from web layer.
|
||||||
OnCrash func(err error)
|
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.
|
// 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.
|
// Stop terminates the running Xray process.
|
||||||
func (p *process) Stop() error {
|
func (p *process) Stop() error {
|
||||||
if !p.IsRunning() {
|
if !p.IsRunning() {
|
||||||
return errors.New("xray is not running")
|
return ErrXrayNotRunning
|
||||||
}
|
}
|
||||||
p.intentionalStop.Store(true)
|
p.intentionalStop.Store(true)
|
||||||
|
|
||||||
@@ -768,7 +770,7 @@ func (p *process) Stop() error {
|
|||||||
cmd := p.cmd
|
cmd := p.cmd
|
||||||
p.mu.RUnlock()
|
p.mu.RUnlock()
|
||||||
if cmd == nil || cmd.Process == nil {
|
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
|
// 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) {
|
func TestStopKillsProcessThatIgnoresSIGTERM(t *testing.T) {
|
||||||
initProcessTestLogger(t)
|
initProcessTestLogger(t)
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ ExecStart=/usr/local/x-ui/x-ui
|
|||||||
ExecReload=/bin/kill -USR1 $MAINPID
|
ExecReload=/bin/kill -USR1 $MAINPID
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
|
KillMode=mixed
|
||||||
|
|
||||||
# The panel intentionally stays root: it supervises the Xray child processes,
|
# The panel intentionally stays root: it supervises the Xray child processes,
|
||||||
# edits netfilter state and reads TLS private keys. These settings only bound
|
# 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
|
ExecReload=/bin/kill -USR1 $MAINPID
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
|
KillMode=mixed
|
||||||
|
|
||||||
# The panel intentionally stays root: it supervises the Xray child processes,
|
# The panel intentionally stays root: it supervises the Xray child processes,
|
||||||
# edits netfilter state and reads TLS private keys. These settings only bound
|
# 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
|
ExecReload=/bin/kill -USR1 $MAINPID
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
|
KillMode=mixed
|
||||||
|
|
||||||
# The panel intentionally stays root: it supervises the Xray child processes,
|
# The panel intentionally stays root: it supervises the Xray child processes,
|
||||||
# edits netfilter state and reads TLS private keys. These settings only bound
|
# edits netfilter state and reads TLS private keys. These settings only bound
|
||||||
|
|||||||
Reference in new issue
Block a user