mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +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)
|
||||
// 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 {
|
||||
sig := <-sigCh
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"unsafe"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/net"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var SIGUSR1 = syscall.Signal(0)
|
||||
|
|
@ -18,7 +19,6 @@ func GetConnectionCount(proto string) (int, error) {
|
|||
if proto != "tcp" && proto != "udp" {
|
||||
return 0, errors.New("invalid protocol")
|
||||
}
|
||||
|
||||
stats, err := net.Connections(proto)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
|
@ -39,7 +39,9 @@ func GetUDPCount() (int, error) {
|
|||
// --- CPU Utilization (Windows native) ---
|
||||
|
||||
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")
|
||||
|
||||
cpuMu sync.Mutex
|
||||
|
|
@ -49,32 +51,25 @@ var (
|
|||
hasLast bool
|
||||
)
|
||||
|
||||
type filetime struct {
|
||||
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 {
|
||||
func ftToUint64(ft windows.Filetime) uint64 {
|
||||
return (uint64(ft.HighDateTime) << 32) | uint64(ft.LowDateTime)
|
||||
}
|
||||
|
||||
// CPUPercentRaw returns the instantaneous total CPU utilization percentage using
|
||||
// Windows GetSystemTimes across all logical processors. The first call returns 0
|
||||
// as it initializes the baseline. Subsequent calls compute deltas.
|
||||
// CPUPercentRaw returns instantaneous total CPU utilization across all
|
||||
// logical processors via Windows GetSystemTimes. The first call returns 0
|
||||
// while it initializes the baseline; subsequent calls compute deltas.
|
||||
func CPUPercentRaw() (float64, error) {
|
||||
var idleFT, kernelFT, userFT filetime
|
||||
var idleFT, kernelFT, userFT windows.Filetime
|
||||
r1, _, e1 := procGetSystemTimes.Call(
|
||||
uintptr(unsafe.Pointer(&idleFT)),
|
||||
uintptr(unsafe.Pointer(&kernelFT)),
|
||||
uintptr(unsafe.Pointer(&userFT)),
|
||||
)
|
||||
if r1 == 0 { // failure
|
||||
if e1 != nil {
|
||||
return 0, e1
|
||||
if r1 == 0 {
|
||||
if errno, _ := e1.(syscall.Errno); errno != 0 {
|
||||
return 0, errno
|
||||
}
|
||||
return 0, syscall.GetLastError()
|
||||
return 0, errors.New("GetSystemTimes failed")
|
||||
}
|
||||
|
||||
idle := ftToUint64(idleFT)
|
||||
|
|
@ -96,7 +91,6 @@ func CPUPercentRaw() (float64, error) {
|
|||
kernelDelta := kernel - lastKernel
|
||||
userDelta := user - lastUser
|
||||
|
||||
// Update for next call
|
||||
lastIdle = idle
|
||||
lastKernel = kernel
|
||||
lastUser = user
|
||||
|
|
@ -105,11 +99,10 @@ func CPUPercentRaw() (float64, error) {
|
|||
if total == 0 {
|
||||
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
|
||||
|
||||
pct := float64(busy) / float64(total) * 100.0
|
||||
// lower bound not needed; ratios of uint64 are non-negative
|
||||
if pct > 100 {
|
||||
pct = 100
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package global
|
|||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
_ "unsafe"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
|
|
@ -11,6 +12,9 @@ import (
|
|||
var (
|
||||
webServer WebServer
|
||||
subServer SubServer
|
||||
|
||||
restartHookMu sync.RWMutex
|
||||
restartHook func()
|
||||
)
|
||||
|
||||
// WebServer interface defines methods for accessing the web server instance.
|
||||
|
|
@ -44,3 +48,24 @@ func SetSubServer(s SubServer) {
|
|||
func GetSubServer() 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/logger"
|
||||
"github.com/mhsanaei/3x-ui/v3/web/global"
|
||||
)
|
||||
|
||||
// PanelService provides business logic for panel management operations.
|
||||
|
|
@ -35,14 +36,21 @@ const (
|
|||
)
|
||||
|
||||
func (s *PanelService) RestartPanel(delay time.Duration) error {
|
||||
p, err := os.FindProcess(syscall.Getpid())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
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 {
|
||||
logger.Error("panel restart: FindProcess failed:", err)
|
||||
return
|
||||
}
|
||||
if err := p.Signal(syscall.SIGHUP); err != nil {
|
||||
logger.Error("failed to send SIGHUP signal:", err)
|
||||
}
|
||||
}()
|
||||
|
|
|
|||
|
|
@ -368,6 +368,8 @@ func (p *process) startCommand(cmd *exec.Cmd) error {
|
|||
return err
|
||||
}
|
||||
|
||||
attachChildLifetime(cmd)
|
||||
|
||||
go p.waitForCommand(cmd)
|
||||
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