mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 20:54:14 +00:00
fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
072d266f50
commit
d97ce19f30
7 changed files with 134 additions and 27 deletions
8
main.go
8
main.go
|
|
@ -73,7 +73,13 @@ func runWebServer() {
|
||||||
|
|
||||||
sigCh := make(chan os.Signal, 1)
|
sigCh := make(chan os.Signal, 1)
|
||||||
// Trap shutdown signals
|
// Trap shutdown signals
|
||||||
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, sys.SIGUSR1)
|
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, sys.SIGUSR1, os.Interrupt)
|
||||||
|
global.SetRestartHook(func() {
|
||||||
|
select {
|
||||||
|
case sigCh <- syscall.SIGHUP:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
})
|
||||||
for {
|
for {
|
||||||
sig := <-sigCh
|
sig := <-sigCh
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/shirou/gopsutil/v4/net"
|
"github.com/shirou/gopsutil/v4/net"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
)
|
)
|
||||||
|
|
||||||
var SIGUSR1 = syscall.Signal(0)
|
var SIGUSR1 = syscall.Signal(0)
|
||||||
|
|
@ -18,7 +19,6 @@ func GetConnectionCount(proto string) (int, error) {
|
||||||
if proto != "tcp" && proto != "udp" {
|
if proto != "tcp" && proto != "udp" {
|
||||||
return 0, errors.New("invalid protocol")
|
return 0, errors.New("invalid protocol")
|
||||||
}
|
}
|
||||||
|
|
||||||
stats, err := net.Connections(proto)
|
stats, err := net.Connections(proto)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
|
|
@ -39,7 +39,9 @@ func GetUDPCount() (int, error) {
|
||||||
// --- CPU Utilization (Windows native) ---
|
// --- CPU Utilization (Windows native) ---
|
||||||
|
|
||||||
var (
|
var (
|
||||||
modKernel32 = syscall.NewLazyDLL("kernel32.dll")
|
// NewLazySystemDLL forces the load from %SystemRoot%\System32 so a
|
||||||
|
// kernel32.dll planted next to the binary can't hijack the call.
|
||||||
|
modKernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||||
procGetSystemTimes = modKernel32.NewProc("GetSystemTimes")
|
procGetSystemTimes = modKernel32.NewProc("GetSystemTimes")
|
||||||
|
|
||||||
cpuMu sync.Mutex
|
cpuMu sync.Mutex
|
||||||
|
|
@ -49,32 +51,25 @@ var (
|
||||||
hasLast bool
|
hasLast bool
|
||||||
)
|
)
|
||||||
|
|
||||||
type filetime struct {
|
func ftToUint64(ft windows.Filetime) uint64 {
|
||||||
LowDateTime uint32
|
|
||||||
HighDateTime uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
// ftToUint64 converts a Windows FILETIME-like struct to a uint64 for
|
|
||||||
// arithmetic and delta calculations used by CPUPercentRaw.
|
|
||||||
func ftToUint64(ft filetime) uint64 {
|
|
||||||
return (uint64(ft.HighDateTime) << 32) | uint64(ft.LowDateTime)
|
return (uint64(ft.HighDateTime) << 32) | uint64(ft.LowDateTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CPUPercentRaw returns the instantaneous total CPU utilization percentage using
|
// CPUPercentRaw returns instantaneous total CPU utilization across all
|
||||||
// Windows GetSystemTimes across all logical processors. The first call returns 0
|
// logical processors via Windows GetSystemTimes. The first call returns 0
|
||||||
// as it initializes the baseline. Subsequent calls compute deltas.
|
// while it initializes the baseline; subsequent calls compute deltas.
|
||||||
func CPUPercentRaw() (float64, error) {
|
func CPUPercentRaw() (float64, error) {
|
||||||
var idleFT, kernelFT, userFT filetime
|
var idleFT, kernelFT, userFT windows.Filetime
|
||||||
r1, _, e1 := procGetSystemTimes.Call(
|
r1, _, e1 := procGetSystemTimes.Call(
|
||||||
uintptr(unsafe.Pointer(&idleFT)),
|
uintptr(unsafe.Pointer(&idleFT)),
|
||||||
uintptr(unsafe.Pointer(&kernelFT)),
|
uintptr(unsafe.Pointer(&kernelFT)),
|
||||||
uintptr(unsafe.Pointer(&userFT)),
|
uintptr(unsafe.Pointer(&userFT)),
|
||||||
)
|
)
|
||||||
if r1 == 0 { // failure
|
if r1 == 0 {
|
||||||
if e1 != nil {
|
if errno, _ := e1.(syscall.Errno); errno != 0 {
|
||||||
return 0, e1
|
return 0, errno
|
||||||
}
|
}
|
||||||
return 0, syscall.GetLastError()
|
return 0, errors.New("GetSystemTimes failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
idle := ftToUint64(idleFT)
|
idle := ftToUint64(idleFT)
|
||||||
|
|
@ -96,7 +91,6 @@ func CPUPercentRaw() (float64, error) {
|
||||||
kernelDelta := kernel - lastKernel
|
kernelDelta := kernel - lastKernel
|
||||||
userDelta := user - lastUser
|
userDelta := user - lastUser
|
||||||
|
|
||||||
// Update for next call
|
|
||||||
lastIdle = idle
|
lastIdle = idle
|
||||||
lastKernel = kernel
|
lastKernel = kernel
|
||||||
lastUser = user
|
lastUser = user
|
||||||
|
|
@ -105,11 +99,10 @@ func CPUPercentRaw() (float64, error) {
|
||||||
if total == 0 {
|
if total == 0 {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
// On Windows, kernel time includes idle time; busy = total - idle
|
// kernel time includes idle on Windows; busy = total - idle
|
||||||
busy := total - idleDelta
|
busy := total - idleDelta
|
||||||
|
|
||||||
pct := float64(busy) / float64(total) * 100.0
|
pct := float64(busy) / float64(total) * 100.0
|
||||||
// lower bound not needed; ratios of uint64 are non-negative
|
|
||||||
if pct > 100 {
|
if pct > 100 {
|
||||||
pct = 100
|
pct = 100
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package global
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"sync"
|
||||||
_ "unsafe"
|
_ "unsafe"
|
||||||
|
|
||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
|
|
@ -11,6 +12,9 @@ import (
|
||||||
var (
|
var (
|
||||||
webServer WebServer
|
webServer WebServer
|
||||||
subServer SubServer
|
subServer SubServer
|
||||||
|
|
||||||
|
restartHookMu sync.RWMutex
|
||||||
|
restartHook func()
|
||||||
)
|
)
|
||||||
|
|
||||||
// WebServer interface defines methods for accessing the web server instance.
|
// WebServer interface defines methods for accessing the web server instance.
|
||||||
|
|
@ -44,3 +48,24 @@ func SetSubServer(s SubServer) {
|
||||||
func GetSubServer() SubServer {
|
func GetSubServer() SubServer {
|
||||||
return subServer
|
return subServer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetRestartHook registers a callback that triggers an in-process panel
|
||||||
|
// restart. main.go sets this up to push SIGHUP into its own signal channel
|
||||||
|
// so the restart path works on Windows (where p.Signal(SIGHUP) is unsupported).
|
||||||
|
func SetRestartHook(fn func()) {
|
||||||
|
restartHookMu.Lock()
|
||||||
|
defer restartHookMu.Unlock()
|
||||||
|
restartHook = fn
|
||||||
|
}
|
||||||
|
|
||||||
|
// TriggerRestart fires the registered restart hook. Returns false if none is set.
|
||||||
|
func TriggerRestart() bool {
|
||||||
|
restartHookMu.RLock()
|
||||||
|
fn := restartHook
|
||||||
|
restartHookMu.RUnlock()
|
||||||
|
if fn == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
fn()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import (
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/v3/config"
|
"github.com/mhsanaei/3x-ui/v3/config"
|
||||||
"github.com/mhsanaei/3x-ui/v3/logger"
|
"github.com/mhsanaei/3x-ui/v3/logger"
|
||||||
|
"github.com/mhsanaei/3x-ui/v3/web/global"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PanelService provides business logic for panel management operations.
|
// PanelService provides business logic for panel management operations.
|
||||||
|
|
@ -35,14 +36,21 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *PanelService) RestartPanel(delay time.Duration) error {
|
func (s *PanelService) RestartPanel(delay time.Duration) error {
|
||||||
p, err := os.FindProcess(syscall.Getpid())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(delay)
|
time.Sleep(delay)
|
||||||
err := p.Signal(syscall.SIGHUP)
|
if global.TriggerRestart() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
logger.Error("panel restart: no restart hook registered (SIGHUP unsupported on Windows)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p, err := os.FindProcess(syscall.Getpid())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.Error("panel restart: FindProcess failed:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := p.Signal(syscall.SIGHUP); err != nil {
|
||||||
logger.Error("failed to send SIGHUP signal:", err)
|
logger.Error("failed to send SIGHUP signal:", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
|
||||||
|
|
@ -368,6 +368,8 @@ func (p *process) startCommand(cmd *exec.Cmd) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attachChildLifetime(cmd)
|
||||||
|
|
||||||
go p.waitForCommand(cmd)
|
go p.waitForCommand(cmd)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
7
xray/process_other.go
Normal file
7
xray/process_other.go
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package xray
|
||||||
|
|
||||||
|
import "os/exec"
|
||||||
|
|
||||||
|
func attachChildLifetime(_ *exec.Cmd) {}
|
||||||
66
xray/process_windows.go
Normal file
66
xray/process_windows.go
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package xray
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"sync"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/mhsanaei/3x-ui/v3/logger"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
killOnExitJobOnce sync.Once
|
||||||
|
killOnExitJob windows.Handle
|
||||||
|
killOnExitJobErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
func ensureKillOnExitJob() (windows.Handle, error) {
|
||||||
|
killOnExitJobOnce.Do(func() {
|
||||||
|
h, err := windows.CreateJobObject(nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
killOnExitJobErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
info := windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION{
|
||||||
|
BasicLimitInformation: windows.JOBOBJECT_BASIC_LIMIT_INFORMATION{
|
||||||
|
LimitFlags: windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err = windows.SetInformationJobObject(
|
||||||
|
h,
|
||||||
|
windows.JobObjectExtendedLimitInformation,
|
||||||
|
uintptr(unsafe.Pointer(&info)),
|
||||||
|
uint32(unsafe.Sizeof(info)),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
windows.CloseHandle(h)
|
||||||
|
killOnExitJobErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
killOnExitJob = h
|
||||||
|
})
|
||||||
|
return killOnExitJob, killOnExitJobErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func attachChildLifetime(cmd *exec.Cmd) {
|
||||||
|
if cmd == nil || cmd.Process == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
job, err := ensureKillOnExitJob()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("xray: kill-on-exit job unavailable:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h, err := windows.OpenProcess(windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE, false, uint32(cmd.Process.Pid))
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("xray: OpenProcess for job attach failed:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer windows.CloseHandle(h)
|
||||||
|
if err := windows.AssignProcessToJobObject(job, h); err != nil {
|
||||||
|
logger.Warning("xray: AssignProcessToJobObject failed:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue