Skip to content

Commit 8865d70

Browse files
committed
feat: kill old xray process
1 parent 32e51ca commit 8865d70

File tree

6 files changed

+557
-4
lines changed

6 files changed

+557
-4
lines changed

backend/xray/core.go

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"path/filepath"
1313
"regexp"
1414
"sync"
15+
"time"
1516

1617
nodeLogger "github.com/pasarguard/node/logger"
1718
)
@@ -22,6 +23,7 @@ type Core struct {
2223
configPath string
2324
version string
2425
process *exec.Cmd
26+
processPID int
2527
restarting bool
2628
logsChan chan string
2729
logger *nodeLogger.Logger
@@ -130,10 +132,18 @@ func (c *Core) Start(xConfig *Config, debugMode bool) error {
130132
return errors.New("xray is started already")
131133
}
132134

133-
// Force kill any orphaned process before starting new one
135+
// Clean up any orphaned xray processes before starting new one
136+
if err := c.cleanupOrphanedProcesses(); err != nil {
137+
log.Printf("warning: failed to cleanup orphaned processes: %v", err)
138+
}
139+
140+
// Force kill any orphaned process in this Core instance before starting new one
134141
if c.process != nil && c.process.Process != nil {
142+
pid := c.process.Process.Pid
135143
_ = c.process.Process.Kill()
144+
_ = killProcessTree(pid)
136145
c.process = nil
146+
c.processPID = 0
137147
}
138148

139149
cmd := exec.Command(c.executablePath, "-c", "stdin:")
@@ -162,6 +172,7 @@ func (c *Core) Start(xConfig *Config, debugMode bool) error {
162172
return err
163173
}
164174
c.process = cmd
175+
c.processPID = cmd.Process.Pid
165176

166177
// Wait for the process to exit to prevent zombie processes
167178
go func() {
@@ -187,9 +198,36 @@ func (c *Core) Stop() {
187198
}
188199

189200
if c.process != nil && c.process.Process != nil {
201+
pid := c.process.Process.Pid
202+
c.processPID = pid
203+
204+
// Kill the process
190205
_ = c.process.Process.Kill()
206+
207+
// Wait for process to terminate with timeout
208+
done := make(chan error, 1)
209+
go func() {
210+
done <- c.process.Wait()
211+
}()
212+
213+
select {
214+
case <-done:
215+
// Process terminated
216+
case <-time.After(5 * time.Second):
217+
// Timeout - try force kill
218+
log.Printf("xray process %d did not terminate within timeout, force killing", pid)
219+
_ = killProcessTree(pid)
220+
}
221+
222+
// Verify process is actually dead
223+
if err := verifyProcessDead(pid); err != nil {
224+
log.Printf("warning: xray process %d may still be running: %v", pid, err)
225+
// Try one more time to kill it
226+
_ = killProcessTree(pid)
227+
}
191228
}
192229
c.process = nil
230+
c.processPID = 0
193231

194232
if c.cancelFunc != nil {
195233
c.cancelFunc()
@@ -231,3 +269,62 @@ func (c *Core) Logs() chan string {
231269
defer c.mu.Unlock()
232270
return c.logsChan
233271
}
272+
273+
// ProcessInfo holds information about a process
274+
type ProcessInfo struct {
275+
PID int
276+
PPID int
277+
IsZombie bool
278+
}
279+
280+
// cleanupOrphanedProcesses finds and kills xray processes that are:
281+
// 1. Zombie processes (orphaned from their parent)
282+
// 2. Processes where the node itself is the parent (PPID matches node PID)
283+
func (c *Core) cleanupOrphanedProcesses() error {
284+
processes, err := findXrayProcesses(c.executablePath)
285+
if err != nil {
286+
return fmt.Errorf("failed to find xray processes: %w", err)
287+
}
288+
289+
currentPID := 0
290+
if c.process != nil && c.process.Process != nil {
291+
currentPID = c.process.Process.Pid
292+
}
293+
294+
// Get current node process PID
295+
nodePID := os.Getpid()
296+
297+
killedCount := 0
298+
for _, procInfo := range processes {
299+
// Skip current process if it exists
300+
if procInfo.PID == currentPID {
301+
continue
302+
}
303+
304+
// Only kill processes that are:
305+
// 1. Zombie processes
306+
// 2. Processes where node is the parent (PPID matches node PID)
307+
shouldKill := false
308+
if procInfo.IsZombie {
309+
log.Printf("found zombie xray process %d (PPID: %d), killing it", procInfo.PID, procInfo.PPID)
310+
shouldKill = true
311+
} else if procInfo.PPID == nodePID {
312+
log.Printf("found orphaned xray process %d with node as parent (PPID: %d), killing it", procInfo.PID, procInfo.PPID)
313+
shouldKill = true
314+
}
315+
316+
if shouldKill {
317+
if err := killProcessTree(procInfo.PID); err != nil {
318+
log.Printf("warning: failed to kill orphaned process %d: %v", procInfo.PID, err)
319+
} else {
320+
killedCount++
321+
}
322+
}
323+
}
324+
325+
if killedCount > 0 {
326+
log.Printf("cleaned up %d orphaned xray process(es)", killedCount)
327+
}
328+
329+
return nil
330+
}

backend/xray/core_windows.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@ import (
88
)
99

1010
// setProcAttributes sets Windows-specific process attributes for proper process management
11+
// We use CREATE_NEW_PROCESS_GROUP to allow sending signals to the process group,
12+
// but we ensure proper cleanup in killProcessTree to handle child processes
1113
func setProcAttributes(cmd *exec.Cmd) {
1214
cmd.SysProcAttr = &syscall.SysProcAttr{
1315
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
16+
// NoInheritHandles ensures child processes don't inherit handles unnecessarily
17+
NoInheritHandles: false,
1418
}
1519
}

backend/xray/process_unix.go

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
//go:build !windows
2+
3+
package xray
4+
5+
import (
6+
"fmt"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
"strconv"
11+
"strings"
12+
"syscall"
13+
)
14+
15+
// findXrayProcesses finds all running xray processes by executable path
16+
// Returns process information including PID, PPID, and zombie state
17+
func findXrayProcesses(executablePath string) ([]ProcessInfo, error) {
18+
absPath, err := filepath.Abs(executablePath)
19+
if err != nil {
20+
return nil, err
21+
}
22+
23+
// Use ps to find processes with PID, PPID, state, and command
24+
cmd := exec.Command("ps", "-eo", "pid,ppid,state,comm")
25+
output, err := cmd.Output()
26+
if err != nil {
27+
return nil, fmt.Errorf("failed to list processes: %w", err)
28+
}
29+
30+
var processes []ProcessInfo
31+
lines := strings.Split(string(output), "\n")
32+
executableName := filepath.Base(absPath)
33+
34+
for _, line := range lines {
35+
line = strings.TrimSpace(line)
36+
if line == "" || strings.HasPrefix(line, "PID") {
37+
continue
38+
}
39+
40+
fields := strings.Fields(line)
41+
if len(fields) < 4 {
42+
continue
43+
}
44+
45+
pidStr := fields[0]
46+
ppidStr := fields[1]
47+
state := fields[2]
48+
comm := fields[3]
49+
50+
// Check if this is an xray process
51+
if comm != executableName {
52+
continue
53+
}
54+
55+
pid, err := strconv.Atoi(pidStr)
56+
if err != nil {
57+
continue
58+
}
59+
60+
ppid, err := strconv.Atoi(ppidStr)
61+
if err != nil {
62+
continue
63+
}
64+
65+
// Verify it's actually the same executable by checking full path
66+
procPath, err := getProcessPath(pid)
67+
if err != nil {
68+
continue
69+
}
70+
71+
procAbsPath, err := filepath.Abs(procPath)
72+
if err != nil {
73+
continue
74+
}
75+
76+
if procAbsPath == absPath {
77+
// Check if process is zombie (state 'Z' in ps output)
78+
isZombie := state == "Z" || state == "z"
79+
80+
processes = append(processes, ProcessInfo{
81+
PID: pid,
82+
PPID: ppid,
83+
IsZombie: isZombie,
84+
})
85+
}
86+
}
87+
88+
return processes, nil
89+
}
90+
91+
// getProcessPath gets the full path of a process by PID on Unix
92+
func getProcessPath(pid int) (string, error) {
93+
// Read from /proc/PID/exe symlink
94+
procPath := fmt.Sprintf("/proc/%d/exe", pid)
95+
path, err := os.Readlink(procPath)
96+
if err != nil {
97+
return "", fmt.Errorf("failed to read process path: %w", err)
98+
}
99+
return path, nil
100+
}
101+
102+
// killProcessTree kills a process and all its children on Unix
103+
func killProcessTree(pid int) error {
104+
proc, err := os.FindProcess(pid)
105+
if err != nil {
106+
return fmt.Errorf("failed to find process %d: %w", pid, err)
107+
}
108+
109+
// Try graceful termination first (SIGTERM)
110+
err = proc.Signal(syscall.SIGTERM)
111+
if err != nil {
112+
// Process might already be dead
113+
}
114+
115+
// Wait a bit for graceful shutdown
116+
// Note: We can't easily wait here without blocking, so we'll just try SIGKILL after
117+
118+
// Get process group ID and kill the whole group
119+
// First, try to get the pgid
120+
pgid, err := getProcessGroupID(pid)
121+
if err == nil && pgid != 0 {
122+
// Kill the entire process group
123+
_ = syscall.Kill(-pgid, syscall.SIGTERM)
124+
// Give it a moment
125+
// Then force kill
126+
_ = syscall.Kill(-pgid, syscall.SIGKILL)
127+
}
128+
129+
// Also kill the specific process
130+
_ = proc.Signal(syscall.SIGKILL)
131+
132+
// Verify it's dead
133+
if !isProcessRunning(pid) {
134+
return nil
135+
}
136+
137+
return fmt.Errorf("process %d is still running after kill attempt", pid)
138+
}
139+
140+
// getProcessGroupID gets the process group ID for a process
141+
func getProcessGroupID(pid int) (int, error) {
142+
// Read from /proc/PID/stat
143+
statPath := fmt.Sprintf("/proc/%d/stat", pid)
144+
data, err := os.ReadFile(statPath)
145+
if err != nil {
146+
return 0, err
147+
}
148+
149+
// Parse stat file - format: pid comm state ppid pgrp ...
150+
fields := strings.Fields(string(data))
151+
if len(fields) < 5 {
152+
return 0, fmt.Errorf("invalid stat format")
153+
}
154+
155+
pgid, err := strconv.Atoi(fields[4])
156+
if err != nil {
157+
return 0, err
158+
}
159+
160+
return pgid, nil
161+
}
162+
163+
// verifyProcessDead checks if a process is actually dead
164+
func verifyProcessDead(pid int) error {
165+
if !isProcessRunning(pid) {
166+
return nil
167+
}
168+
return fmt.Errorf("process %d is still running", pid)
169+
}
170+
171+
// isProcessRunning checks if a process is still running
172+
func isProcessRunning(pid int) bool {
173+
proc, err := os.FindProcess(pid)
174+
if err != nil {
175+
return false
176+
}
177+
178+
// Try to signal the process - if it fails, it's likely dead
179+
err = proc.Signal(syscall.Signal(0))
180+
return err == nil
181+
}
182+
183+
// isProcessZombie checks if a process is a zombie on Unix
184+
// This is already handled in findXrayProcesses by checking the state field
185+
// But we provide this function for consistency
186+
func isProcessZombie(pid int) bool {
187+
// Read from /proc/PID/stat to get process state
188+
statPath := fmt.Sprintf("/proc/%d/stat", pid)
189+
data, err := os.ReadFile(statPath)
190+
if err != nil {
191+
return false
192+
}
193+
194+
// Parse stat file - format: pid comm state ppid ...
195+
fields := strings.Fields(string(data))
196+
if len(fields) < 3 {
197+
return false
198+
}
199+
200+
// State is the 3rd field (index 2)
201+
// 'Z' means zombie process
202+
state := fields[2]
203+
return state == "Z" || state == "z"
204+
}
205+

0 commit comments

Comments
 (0)