fix:win ltsc nullbug

#878
This commit is contained in:
samwaf
2026-07-14 17:18:43 +08:00
parent 0eee3182e0
commit a8a9172d57
8 changed files with 328 additions and 14 deletions
+11
View File
@@ -4,6 +4,7 @@ import (
"SamWaf/cache"
"SamWaf/common/gwebsocket"
"SamWaf/common/tasklog"
"SamWaf/common/wafexec"
"SamWaf/common/zlog"
"SamWaf/enums"
"SamWaf/global"
@@ -865,6 +866,7 @@ func (m *wafSystenService) run() {
cmd := exec.Command(global.GWAF_RUNTIME_CURRENT_EXEPATH, "restart")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
wafexec.FixStdin(cmd)
if err := cmd.Start(); err != nil {
zlog.Error("启动新进程失败:", err)
os.Exit(0)
@@ -876,6 +878,7 @@ func (m *wafSystenService) run() {
cmd := exec.Command(global.GWAF_RUNTIME_CURRENT_EXEPATH)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
wafexec.FixStdin(cmd)
if err := cmd.Start(); err != nil {
zlog.Error("启动新进程失败:", err)
os.Exit(0)
@@ -1069,6 +1072,14 @@ func main() {
pid := os.Getpid()
zlog.Debug("SamWaf Current PID:" + strconv.Itoa(pid))
// 环境自检:精简版 Windows(如 LTSC 纯净版)可能裁剪/禁用 Null 内核驱动,导致空设备不可用。
// os/exec 在标准流为 nil 时会去打开该设备,缺失则所有子进程(含 Worker)都起不来。
// SamWaf 已通过 wafexec 自动规避,这里只留一条线索:Supervisor 记在 supervisor.log、Worker 记在 log.log。
if !wafexec.NullDeviceAvailable() {
zlog.Warn("[环境] 系统空设备 " + os.DevNull + " 不可用(精简版 Windows 常见:Null 内核驱动被裁剪或禁用)。" +
"SamWaf 已自动规避,不影响运行;如需彻底恢复,请以管理员执行 `sc config Null start=system` 后重启系统。")
}
// 调试标识:响应头标记处理进程,便于验证升级时新旧 Worker 交替。
// 主开关在 config.yml 的 debug_worker_header(已由上方 LoadAndInitConfig 读入,Supervisor/Worker 均生效,
// 服务模式下务必用配置项——SCM/systemd 不会传入交互式 shell 的环境变量)。
+99
View File
@@ -0,0 +1,99 @@
// Package wafexec 统一子进程标准流的兜底处理。
//
// Go 的 os/exec 在 Cmd.Stdin/Stdout/Stderr 为 nil 时,会去打开系统空设备
// (os.DevNull:Windows 为 NUL,类 Unix 为 /dev/null),见 os/exec/exec.go 的
// childStdin / writerDescriptor。某些精简版 Windows(如 LTSC 纯净版)裁掉或禁用了
// Null 内核驱动,open NUL 直接失败,cmd.Start() 在创建进程前就报
// "open NUL: The system cannot find the file specified." —— 所有子进程全起不来
// (Supervisor 连首个 Worker 都拉不起来,SamWaf 完全无法启动)。
//
// 本包采用"探测降级":
// - 空设备可用(绝大多数机器) → 原样返回,行为与改造前完全一致,零回归;
// - 空设备不可用(精简系统) → 注入等价的替代流,让子进程照常启动。
//
// 约定:本项目内所有 exec.Cmd 在 Start/Run/Output/CombinedOutput 之前,
// 必须先经 FixStdin 或 FixStdio 处理,不得直接依赖空设备。
package wafexec
import (
"bytes"
"os"
"os/exec"
"sync"
)
// forceNoNullEnv 强制走"无空设备"降级分支的隐藏开关,仅用于在正常机器上验证降级路径。
// 该变量随 os.Environ() 自动继承给 spawn 出来的 Worker 子进程,无需额外传递。
const forceNoNullEnv = "SAMWAF_FORCE_NO_NULLDEV"
var (
nullOnce sync.Once
nullOK bool
)
// nullAvailable 惰性探测一次系统空设备是否可读可写,结果进程内缓存。
// 读写都要探:os/exec 对 Stdin 用 os.Open,对 Stdout/Stderr 用 os.OpenFile(O_WRONLY)。
func nullAvailable() bool {
nullOnce.Do(func() {
if os.Getenv(forceNoNullEnv) == "1" {
return // nullOK 保持 false
}
r, err := os.Open(os.DevNull)
if err != nil {
return
}
_ = r.Close()
w, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
if err != nil {
return
}
_ = w.Close()
nullOK = true
})
return nullOK
}
// NullDeviceAvailable 返回系统空设备是否可用,供启动自检使用。
func NullDeviceAvailable() bool { return nullAvailable() }
// FixStdin 仅在"空设备不可用"时,为 nil 的 Stdin 补一个立即 EOF 的读端;不动 Stdout/Stderr。
//
// 适用于随后要调 Output()/CombinedOutput()/StdoutPipe()/StderrPipe() 的场景 ——
// 这些方法要求 Stdout/Stderr 保持 nil 或由其自行接管,被提前占用会报 "exec: Stdout already set"。
//
// 替代流用 bytes.Reader 而非 os.Stdin:os.Stdin 在 Windows 服务模式下句柄可能无效,
// 前台模式下又会把真实控制台输入交给子进程(可能吞掉用户按键)。非 *os.File 的 Reader 会让
// os/exec 自建匿名管道,读端由 Start() 收尾关闭、写端由其拷贝 goroutine 拷完 0 字节后主动关闭,
// 因此即使调用方只调 os.Process.Wait() 而不调 cmd.Wait()(supervisor 就是如此)也不会泄漏句柄,
// 子进程读 stdin 立即 EOF,与 NUL 语义等价。
func FixStdin(cmd *exec.Cmd) *exec.Cmd {
if cmd == nil || nullAvailable() {
return cmd
}
if cmd.Stdin == nil {
cmd.Stdin = bytes.NewReader(nil)
}
return cmd
}
// FixStdio 在"空设备不可用"时补齐三个标准流。
// 仅用于"直接 Run()/Start() 且确定不会再取 Pipe/Output"的 Cmd。
//
// Stdout/Stderr 必须填 *os.File(os.Stdout/os.Stderr),不能填 io.Discard:
// io.Discard 不是 *os.File,os/exec 会为它新建匿名管道 + 拷贝 goroutine,而 cmd.Run()/Wait()
// 要等管道 EOF 才返回。像 `cmd /C xxx` 这类命令的孙进程会继承管道写端,孙进程不退出则管道
// 永不 EOF,Run() 将永久阻塞。*os.File 是直传句柄、零管道零 goroutine,与 NUL 行为等价;
// 服务模式下 os.Stdout 句柄为 0(子进程等于没有 stdout,天然丢弃),前台模式下输出到控制台。
func FixStdio(cmd *exec.Cmd) *exec.Cmd {
if cmd == nil || nullAvailable() {
return cmd
}
FixStdin(cmd)
if cmd.Stdout == nil {
cmd.Stdout = os.Stdout
}
if cmd.Stderr == nil {
cmd.Stderr = os.Stderr
}
return cmd
}
+172
View File
@@ -0,0 +1,172 @@
package wafexec
import (
"os"
"os/exec"
"runtime"
"sync"
"testing"
"time"
)
// resetProbe 重置空设备探测缓存,让下一次调用重新探测。
func resetProbe() {
nullOnce = sync.Once{}
nullOK = false
}
// trueCmd 返回一个立刻成功退出的短命命令。
func trueCmd() *exec.Cmd {
if runtime.GOOS == "windows" {
return exec.Command("cmd", "/c", "exit 0")
}
return exec.Command("true")
}
// echoCmd 返回一个会向 stdout 打印内容的命令。
func echoCmd(s string) *exec.Cmd {
if runtime.GOOS == "windows" {
return exec.Command("cmd", "/c", "echo "+s)
}
return exec.Command("echo", s)
}
// TestNoopWhenNullAvailable 空设备可用时必须原样返回,三个流保持 nil。
// 这是"正常机器零回归"的核心保证:os/exec 依旧走它原本的空设备路径。
func TestNoopWhenNullAvailable(t *testing.T) {
resetProbe()
if !NullDeviceAvailable() {
t.Skipf("本机 %s 不可用,跳过", os.DevNull)
}
cmd := trueCmd()
FixStdio(cmd)
if cmd.Stdin != nil || cmd.Stdout != nil || cmd.Stderr != nil {
t.Fatalf("空设备可用时不应改动标准流,实际 Stdin=%v Stdout=%v Stderr=%v",
cmd.Stdin, cmd.Stdout, cmd.Stderr)
}
}
// TestFixStdinWhenDegraded 降级时只补 Stdin,不碰 Stdout/Stderr(否则 Output/Pipe 路径会报 Stdout already set)。
func TestFixStdinWhenDegraded(t *testing.T) {
t.Setenv(forceNoNullEnv, "1")
resetProbe()
defer resetProbe()
cmd := trueCmd()
FixStdin(cmd)
if cmd.Stdin == nil {
t.Fatal("降级时 Stdin 应被补上")
}
if cmd.Stdout != nil || cmd.Stderr != nil {
t.Fatal("FixStdin 不应改动 Stdout/Stderr")
}
}
// TestOutputStillWorksWhenDegraded 降级后 Output() 仍能正常取到输出(防止误伤采集类命令)。
func TestOutputStillWorksWhenDegraded(t *testing.T) {
t.Setenv(forceNoNullEnv, "1")
resetProbe()
defer resetProbe()
out, err := FixStdin(echoCmd("samwaf")).Output()
if err != nil {
t.Fatalf("Output() 失败: %v", err)
}
if len(out) == 0 {
t.Fatal("Output() 未取到内容")
}
}
// TestStdoutIsFileWhenDegraded 降级时 Stdout/Stderr 必须是 *os.File。
//
// 回归断言:不能改成 io.Discard —— 非 *os.File 会让 os/exec 建匿名管道 + 拷贝 goroutine,
// 而 cmd.Run()/Wait() 要等管道 EOF 才返回;`cmd /C` 的孙进程会继承管道写端,
// 孙进程不退出则 Run() 永久阻塞(wafappengine 的 StopCmd/taskkill 会因此卡死优雅停止)。
func TestStdoutIsFileWhenDegraded(t *testing.T) {
t.Setenv(forceNoNullEnv, "1")
resetProbe()
defer resetProbe()
cmd := trueCmd()
FixStdio(cmd)
if _, ok := cmd.Stdout.(*os.File); !ok {
t.Fatalf("降级时 Stdout 必须是 *os.File(直传句柄,零管道),实际为 %T", cmd.Stdout)
}
if _, ok := cmd.Stderr.(*os.File); !ok {
t.Fatalf("降级时 Stderr 必须是 *os.File,实际为 %T", cmd.Stderr)
}
}
// TestStartWithoutCmdWait 降级后只调 os.Process.Wait()、不调 cmd.Wait() 也能正常收尾。
// 对齐 supervisor.spawn 的实际用法(它保存 cmd.Process,用 proc.Wait() 监护)。
func TestStartWithoutCmdWait(t *testing.T) {
t.Setenv(forceNoNullEnv, "1")
resetProbe()
defer resetProbe()
cmd := trueCmd()
FixStdio(cmd)
if err := cmd.Start(); err != nil {
t.Fatalf("Start() 失败: %v", err)
}
state, err := cmd.Process.Wait()
if err != nil {
t.Fatalf("proc.Wait() 失败: %v", err)
}
if state.ExitCode() != 0 {
t.Fatalf("子进程退出码应为 0,实际 %d", state.ExitCode())
}
}
// TestRunNotBlockedByGrandchild 降级后,即使子进程派生了长命孙进程,Run() 也必须立即返回。
//
// 这是 io.Discard 方案会踩的坑:非 *os.File 的 Stdout 会让 os/exec 建匿名管道,孙进程继承写端,
// Run()/Wait() 要等管道 EOF 才返回 → 孙进程活多久就阻塞多久。wafappengine 的 StopCmd/taskkill
// 正是 `cmd /C` 起的,一旦阻塞会直接卡死应用的优雅停止。用 *os.File 则零管道、不阻塞。
func TestRunNotBlockedByGrandchild(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("仅在 Windows 上验证 cmd /C 的孙进程场景")
}
t.Setenv(forceNoNullEnv, "1")
resetProbe()
defer resetProbe()
// cmd /C start /b 派生一个脱离 cmd.exe 自身生命周期的孙进程(存活约 8s)。
// 孙进程必须继承 Stdout —— 这正是 io.Discard 方案里"管道迟迟不 EOF"的成因,不能重定向掉。
cmd := FixStdio(exec.Command("cmd", "/C", "start /b ping -n 8 127.0.0.1"))
done := make(chan error, 1)
go func() { done <- cmd.Run() }()
select {
case <-done: // 期望:cmd.exe 一退出 Run() 立刻返回,不等孙进程
case <-time.After(3 * time.Second):
t.Fatal("Run() 被孙进程阻塞超过 3s —— Stdout 很可能被改成了非 *os.File(如 io.Discard),会卡死应用优雅停止")
}
}
// TestNeverOverrideExisting 已显式赋值的流一律不覆盖。
func TestNeverOverrideExisting(t *testing.T) {
t.Setenv(forceNoNullEnv, "1")
resetProbe()
defer resetProbe()
cmd := trueCmd()
cmd.Stdout = os.Stderr // 故意用 Stderr 占位,确认不被改写
FixStdio(cmd)
if cmd.Stdout != os.Stderr {
t.Fatal("已设置的 Stdout 不应被覆盖")
}
}
// TestNilCmdSafe 传 nil 不 panic。
func TestNilCmdSafe(t *testing.T) {
t.Setenv(forceNoNullEnv, "1")
resetProbe()
defer resetProbe()
if FixStdin(nil) != nil || FixStdio(nil) != nil {
t.Fatal("传 nil 应原样返回 nil")
}
}
+7 -4
View File
@@ -3,6 +3,7 @@
package firewall
import (
"SamWaf/common/wafexec"
"bufio"
"fmt"
"os"
@@ -45,7 +46,7 @@ type IPBlockInfo struct {
func (fw *FireWallEngine) IsFirewallEnabled() bool {
if runtime.GOOS == "linux" {
out, err := exec.Command("iptables", "-L").CombinedOutput()
out, err := wafexec.FixStdin(exec.Command("iptables", "-L")).CombinedOutput()
if err != nil {
return false
}
@@ -85,7 +86,7 @@ func (fw *FireWallEngine) checkAvailable() error {
}
}
out, err := exec.Command("iptables", "-S", "INPUT").CombinedOutput()
out, err := wafexec.FixStdin(exec.Command("iptables", "-S", "INPUT")).CombinedOutput()
if err != nil {
output := strings.TrimSpace(string(out))
if strings.Contains(output, "Permission denied") ||
@@ -104,6 +105,8 @@ func (fw *FireWallEngine) checkAvailable() error {
}
func (fw *FireWallEngine) executeCommand(cmd *exec.Cmd) (error, string) {
// 只补 Stdin:下面要取 StdoutPipe/StderrPipe,Stdout/Stderr 必须留给 os/exec 自己接管
wafexec.FixStdin(cmd)
stdout, err := cmd.StdoutPipe()
if err != nil {
fmt.Println(err)
@@ -136,7 +139,7 @@ func (fw *FireWallEngine) executeCommand(cmd *exec.Cmd) (error, string) {
// isIPInRules 直接用原始 IP 检查 iptables 中是否存在 DROP 规则,支持单 IP 和 CIDR
func (fw *FireWallEngine) isIPInRules(ip string) (bool, error) {
cmd := exec.Command("iptables-save")
cmd := wafexec.FixStdin(exec.Command("iptables-save"))
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("[ERROR] 获取iptables规则失败: %v\n", err)
@@ -285,7 +288,7 @@ func (fw *FireWallEngine) UnblockIPList(ips []string) (successCount int, failedI
// GetBlockedIPList 获取所有已封禁的IP列表
func (fw *FireWallEngine) GetBlockedIPList() ([]string, error) {
cmd := exec.Command("iptables-save")
cmd := wafexec.FixStdin(exec.Command("iptables-save"))
output, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("failed to get blocked IP list: %v", err)
+18 -3
View File
@@ -3,8 +3,10 @@
package firewall
import (
"SamWaf/common/wafexec"
"bufio"
"fmt"
"os"
"os/exec"
"strings"
"time"
@@ -52,6 +54,7 @@ type IPBlockInfo struct {
func (fw *FireWallEngine) IsFirewallEnabled() bool {
// 检查 pf 是否启用
cmd := exec.Command("pfctl", "-s", "info")
wafexec.FixStdin(cmd)
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("[WARN] 检查防火墙状态失败: %v\n", err)
@@ -74,6 +77,12 @@ func (fw *FireWallEngine) checkAvailable() error {
}
func (fw *FireWallEngine) executeCommand(cmd *exec.Cmd) (error error, printstr string) {
// 只补 Stdin:下面要取 StdoutPipe,Stdout 必须留给 os/exec 自己接管。
// Stderr 本函数不接管,无空设备时需显式兜底,否则 os/exec 会去打开空设备而失败。
wafexec.FixStdin(cmd)
if cmd.Stderr == nil && !wafexec.NullDeviceAvailable() {
cmd.Stderr = os.Stderr
}
stdout, err := cmd.StdoutPipe()
if err != nil {
fmt.Println(err)
@@ -104,6 +113,7 @@ func (fw *FireWallEngine) AddRule(ruleName, ipToAdd, action, proc, localport str
cmd := exec.Command("pfctl", "-t", PF_TABLE_NAME, "-T", "add", ipToAdd)
fmt.Printf("[DEBUG] 执行命令: pfctl -t %s -T add %s\n", PF_TABLE_NAME, ipToAdd)
wafexec.FixStdin(cmd)
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("[ERROR] 添加规则失败: %v, 输出: %s\n", err, string(output))
@@ -132,6 +142,7 @@ func (fw *FireWallEngine) DeleteRule(ruleName string) (bool, error) {
cmd := exec.Command("pfctl", "-t", PF_TABLE_NAME, "-T", "delete", ip)
fmt.Printf("[DEBUG] 执行命令: pfctl -t %s -T delete %s\n", PF_TABLE_NAME, ip)
wafexec.FixStdin(cmd)
output, err := cmd.CombinedOutput()
outputStr := string(output)
@@ -163,6 +174,7 @@ func (fw *FireWallEngine) IsRuleExists(ruleName string) (bool, error) {
// 检查 IP 是否在 table 中
cmd := exec.Command("pfctl", "-t", PF_TABLE_NAME, "-T", "show")
wafexec.FixStdin(cmd)
output, err := cmd.CombinedOutput()
if err != nil {
// table 可能不存在
@@ -220,6 +232,7 @@ func (fw *FireWallEngine) BlockIP(ip string, reason string) error {
// 添加 IP 到 pf table
cmd := exec.Command("pfctl", "-t", PF_TABLE_NAME, "-T", "add", ip)
wafexec.FixStdin(cmd)
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("[ERROR] 封禁IP失败: %s, error: %v, output: %s\n", ip, err, string(output))
@@ -245,6 +258,7 @@ func (fw *FireWallEngine) UnblockIP(ip string) error {
// 从 table 中删除 IP
cmd := exec.Command("pfctl", "-t", PF_TABLE_NAME, "-T", "delete", ip)
wafexec.FixStdin(cmd)
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("[ERROR] 解除IP封禁失败: %s, error: %v, output: %s\n", ip, err, string(output))
@@ -317,6 +331,7 @@ func (fw *FireWallEngine) GetBlockedIPList() ([]string, error) {
fmt.Printf("[DEBUG] 获取已封禁IP列表 (Mac)\n")
cmd := exec.Command("pfctl", "-t", PF_TABLE_NAME, "-T", "show")
wafexec.FixStdin(cmd)
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("[ERROR] 获取IP列表失败: %v\n", err)
@@ -382,7 +397,7 @@ func (fw *FireWallEngine) ensureTableExists() error {
fmt.Printf("[DEBUG] 确保pf table存在\n")
// 检查 table 是否存在
cmd := exec.Command("pfctl", "-t", PF_TABLE_NAME, "-T", "show")
cmd := wafexec.FixStdin(exec.Command("pfctl", "-t", PF_TABLE_NAME, "-T", "show"))
_, err := cmd.CombinedOutput()
if err == nil {
// table 已存在
@@ -397,7 +412,7 @@ func (fw *FireWallEngine) ensureTableExists() error {
// 尝试添加并立即删除一个临时IP来初始化表
tempIP := "127.0.0.254"
cmd = exec.Command("pfctl", "-t", PF_TABLE_NAME, "-T", "add", tempIP)
cmd = wafexec.FixStdin(exec.Command("pfctl", "-t", PF_TABLE_NAME, "-T", "add", tempIP))
_, err = cmd.CombinedOutput()
if err != nil {
fmt.Printf("[WARN] 初始化table失败: %v\n", err)
@@ -405,7 +420,7 @@ func (fw *FireWallEngine) ensureTableExists() error {
}
// 删除临时IP
cmd = exec.Command("pfctl", "-t", PF_TABLE_NAME, "-T", "delete", tempIP)
cmd = wafexec.FixStdin(exec.Command("pfctl", "-t", PF_TABLE_NAME, "-T", "delete", tempIP))
cmd.CombinedOutput()
return nil
+3
View File
@@ -3,6 +3,7 @@
package firewall
import (
"SamWaf/common/wafexec"
"bufio"
"fmt"
"os/exec"
@@ -91,6 +92,8 @@ func (fw *FireWallEngine) checkAvailable() error {
}
func (fw *FireWallEngine) executeCommand(cmd *exec.Cmd) (error error, printstr string) {
// 只补 Stdin:下面要取 StdoutPipe/StderrPipe,Stdout/Stderr 必须留给 os/exec 自己接管
wafexec.FixStdin(cmd)
stdout, err := cmd.StdoutPipe()
if err != nil {
fmt.Println(err)
+4
View File
@@ -10,6 +10,7 @@
package supervisor
import (
"SamWaf/common/wafexec"
"SamWaf/common/zlog"
"SamWaf/wafipc"
"SamWaf/wafupdate"
@@ -295,6 +296,9 @@ func (s *Supervisor) spawn(takeover bool) (int, error) {
cmd := exec.Command(s.opts.ExePath, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// Stdin 必须显式兜底:nil 时 os/exec 会去打开系统空设备(Windows 上是 NUL),
// 精简版系统缺失该设备会导致 Start() 直接失败,连首个 Worker 都拉不起来。
wafexec.FixStdin(cmd)
if err := cmd.Start(); err != nil {
return 0, err
}
+14 -7
View File
@@ -1,6 +1,7 @@
package wafappengine
import (
"SamWaf/common/wafexec"
"SamWaf/common/zlog"
"SamWaf/global"
"SamWaf/model"
@@ -150,6 +151,9 @@ func (e *WafAppEngine) startProcess(app model.WafApp, rt *wafappmodel.AppRuntime
}
cmd.Env = envs
// 只补 Stdin:下面要取 StdoutPipe/StderrPipe,Stdout/Stderr 必须留给 os/exec 自己接管
wafexec.FixStdin(cmd)
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("获取stdout失败: %w", err)
@@ -306,6 +310,9 @@ func (e *WafAppEngine) monitorApp(app model.WafApp, rt *wafappmodel.AppRuntime)
}
newCmd.Env = envs
// 只补 Stdin:下面要取 StdoutPipe/StderrPipe,Stdout/Stderr 必须留给 os/exec 自己接管
wafexec.FixStdin(newCmd)
stdoutPipe, _ := newCmd.StdoutPipe()
stderrPipe, _ := newCmd.StderrPipe()
if startErr := newCmd.Start(); startErr != nil {
@@ -374,11 +381,11 @@ func (e *WafAppEngine) StopApp(code string) error {
stopCmd = exec.Command("/bin/sh", "-c", app.StopCmd)
}
stopCmd.Dir = app.AppDir
_ = stopCmd.Run()
_ = wafexec.FixStdio(stopCmd).Run()
} else {
if runtime.GOOS == "windows" {
// 发送 WM_CLOSE 给进程,给其机会优雅退出(不带 /F)
exec.Command("taskkill", "/PID", strconv.Itoa(rt.Pid)).Run()
wafexec.FixStdio(exec.Command("taskkill", "/PID", strconv.Itoa(rt.Pid))).Run()
} else {
rt.Cmd.Process.Signal(os.Interrupt)
}
@@ -391,7 +398,7 @@ func (e *WafAppEngine) StopApp(code string) error {
case <-time.After(time.Duration(timeout) * time.Second):
if runtime.GOOS == "windows" {
// /F 强制 + /T 终止整棵子进程树,解决 cmd.exe 子进程孤儿问题
exec.Command("taskkill", "/F", "/T", "/PID", strconv.Itoa(rt.Pid)).Run()
wafexec.FixStdio(exec.Command("taskkill", "/F", "/T", "/PID", strconv.Itoa(rt.Pid))).Run()
} else {
if rt.Cmd.Process != nil {
rt.Cmd.Process.Kill()
@@ -586,7 +593,7 @@ func buildChildMapWindows(m map[int][]int) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 列名字母序:ParentProcessId 排在 ProcessId 前面
out, err := exec.CommandContext(ctx, "wmic", "process", "get", "ParentProcessId,ProcessId").Output()
out, err := wafexec.FixStdin(exec.CommandContext(ctx, "wmic", "process", "get", "ParentProcessId,ProcessId")).Output()
if err != nil {
return
}
@@ -644,7 +651,7 @@ func buildChildMapLinux(m map[int][]int) {
func fetchNetStatsWindows(pidSet map[int]bool) (*wafappmodel.NetStatsResult, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
out, err := exec.CommandContext(ctx, "netstat", "-ano").Output()
out, err := wafexec.FixStdin(exec.CommandContext(ctx, "netstat", "-ano")).Output()
if err != nil {
return nil, fmt.Errorf("netstat failed: %w", err)
}
@@ -709,7 +716,7 @@ func fetchNetStatsLinux(pidSet map[int]bool) (*wafappmodel.NetStatsResult, error
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// ss -tanp: TCP all-states, numeric, with-process
out, err := exec.CommandContext(ctx, "ss", "-tanp").Output()
out, err := wafexec.FixStdin(exec.CommandContext(ctx, "ss", "-tanp")).Output()
if err != nil {
// 降级到 netstat
return fetchNetStatsLinuxNetstat(pidSet)
@@ -777,7 +784,7 @@ func fetchNetStatsLinuxNetstat(pidSet map[int]bool) (*wafappmodel.NetStatsResult
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// netstat -tnap: TCP numeric all programs
out, err := exec.CommandContext(ctx, "netstat", "-tnap").Output()
out, err := wafexec.FixStdin(exec.CommandContext(ctx, "netstat", "-tnap")).Output()
if err != nil {
return nil, fmt.Errorf("netstat failed: %w", err)
}