mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
修复 frpc 进程生命周期真 bug(agent 生产代码):exec.CommandContext 默认只杀直接子进程,被杀 shell 的孤儿 sleep 继续持有 stderr 管道,cmd.Wait() 阻塞到其自然退出(Stop/重启可挂起秒级)。改 Setpgid 进程组 + Kill(-pid) 整组击杀。连带修复两个测试 bug(Manager 拥有 Cmd 的并发 Wait 竞态 → Signal(0) 探测;ssl_renew 用 miniredis 替代 init() 创建的真实 redis 客户端)。go test ./internal/... ./pkg/... 全绿,checks.sh 升级为真实测试门禁。
Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_exhaustive":0,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_vetx_total":0,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"measure_s":40}
This commit is contained in:
@@ -195,6 +195,17 @@ func (m *Manager) restartProcess(ctx context.Context, relayID string, configPath
|
||||
var stderrBuf bytes.Buffer
|
||||
cmd.Stderr = &stderrBuf
|
||||
|
||||
// frpc 及其中间子进程必须整体随上下文终止:CommandContext 默认只杀
|
||||
// 直接子进程,孤儿孙进程会继续持有 stderr 管道导致 cmd.Wait 阻塞到其
|
||||
// 自然退出。这里为 frpc 单独建进程组并整组 SIGKILL。
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
cmd.Cancel = func() error {
|
||||
if cmd.Process == nil {
|
||||
return nil
|
||||
}
|
||||
return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
proc.Cmd = cmd
|
||||
proc.Status = "running"
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -80,6 +81,7 @@ func assertStatusEventually(t *testing.T, m *Manager, relayID string, expectedSt
|
||||
t.Fatalf("expected status eventually %s, got %s (err: %s)", expectedStatus, got, errStr)
|
||||
}
|
||||
|
||||
// assertCommandExitedEventually 等待测试自建进程退出(本测试持有其 Wait 权)。
|
||||
func assertCommandExitedEventually(t *testing.T, cmd *exec.Cmd, timeout time.Duration) {
|
||||
t.Helper()
|
||||
|
||||
@@ -95,6 +97,22 @@ func assertCommandExitedEventually(t *testing.T, cmd *exec.Cmd, timeout time.Dur
|
||||
}
|
||||
}
|
||||
|
||||
// assertManagedCommandExitedEventually 探测受管进程是否已退出。不能对其调用
|
||||
// Wait —— Wait 由 Manager 拥有,测试并发 Wait 会与 os/exec 的 ctxResult
|
||||
// 通道竞争而永久挂起;Signal(0) 在进程被 Manager 收割后即报错。
|
||||
func assertManagedCommandExitedEventually(t *testing.T, cmd *exec.Cmd, timeout time.Duration) {
|
||||
t.Helper()
|
||||
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
if err := cmd.Process.Signal(syscall.Signal(0)); err != nil {
|
||||
return
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("expected managed process pid=%d to exit within %s", cmd.Process.Pid, timeout)
|
||||
}
|
||||
|
||||
func TestStartProcessSuccess(t *testing.T) {
|
||||
scriptPath, dir := setupDummyScript(t)
|
||||
writeControl(t, dir, 0, 5) // exit code 0, sleep 5s
|
||||
@@ -380,7 +398,7 @@ func TestStopCancelsRunningProcesses(t *testing.T) {
|
||||
m.mu.RUnlock()
|
||||
|
||||
m.Stop()
|
||||
assertCommandExitedEventually(t, cmd, 2*time.Second)
|
||||
assertManagedCommandExitedEventually(t, cmd, 2*time.Second)
|
||||
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
@@ -8,17 +8,16 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/repository"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/tls"
|
||||
"github.com/Rain-kl/Wavelet/internal/infra/config"
|
||||
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
|
||||
"github.com/Rain-kl/Wavelet/internal/infra/task"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/Rain-kl/Wavelet/internal/repository"
|
||||
"github.com/Rain-kl/Wavelet/internal/testhelper"
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func setupSSLRenewTestDB(t *testing.T) func() {
|
||||
@@ -26,18 +25,23 @@ func setupSSLRenewTestDB(t *testing.T) func() {
|
||||
|
||||
task.RegisterTaskMeta(tls.SSLSingleRenewMeta)
|
||||
|
||||
sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, sqliteDB.AutoMigrate(&model.TLSCertificate{}, &model.TaskExecution{}))
|
||||
_, mr, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
require.NoError(t, db.DB(nil).AutoMigrate(&model.TLSCertificate{}, &model.TaskExecution{}))
|
||||
|
||||
// task 包 init() 会按配置创建指向真实 Redis 的客户端;测试显式改用
|
||||
// miniredis(与 executor_test 一致),避免依赖本地 redis 实例。
|
||||
oldClient := task.AsynqClient
|
||||
task.AsynqClient = asynq.NewClient(asynq.RedisClientOpt{Addr: mr.Addr()})
|
||||
t.Cleanup(func() {
|
||||
_ = task.AsynqClient.Close()
|
||||
task.AsynqClient = oldClient
|
||||
})
|
||||
|
||||
db.SetDB(sqliteDB)
|
||||
oldSecret := config.Config.App.SessionSecret
|
||||
config.Config.App.SessionSecret = "test_session_secret_for_ssl_renew"
|
||||
return func() {
|
||||
db.SetDB(nil)
|
||||
config.Config.App.SessionSecret = oldSecret
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user