Compare commits

..
2 Commits
Author SHA1 Message Date
fatedier 8d1ab7d585 test/e2e: guard Process against double-Start and Stop-before-Start
Add started flag to prevent double-Start panics and allow Stop to
return immediately when the process was never started. Use sync.Once
for closing the done channel as defense-in-depth against double close.
2026-03-09 01:44:54 +08:00
fatedier f32bec9f4d test/e2e: optimize RunFrps/RunFrpc with process exit detection
Refactor Process to track subprocess lifecycle via a done channel,
replacing direct cmd.Wait() in Stop() to avoid double-Wait races.
RunFrps/RunFrpc now use select on the done channel instead of fixed
sleeps, allowing short-lived processes (verify, startup failures) to
return immediately while preserving existing timeout behavior for
long-running daemons.
2026-03-09 00:41:00 +08:00
2 changed files with 7 additions and 91 deletions
+3 -42
View File
@@ -9,7 +9,6 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/fatedier/frp/pkg/config"
flog "github.com/fatedier/frp/pkg/util/log" flog "github.com/fatedier/frp/pkg/util/log"
"github.com/fatedier/frp/test/e2e/framework/consts" "github.com/fatedier/frp/test/e2e/framework/consts"
"github.com/fatedier/frp/test/e2e/pkg/process" "github.com/fatedier/frp/test/e2e/pkg/process"
@@ -62,22 +61,9 @@ func (f *Framework) RunProcesses(serverTemplate string, clientTemplates []string
err = p.Start() err = p.Start()
ExpectNoError(err) ExpectNoError(err)
} }
// Wait for each client's proxies to register with frps. // frpc needs time to connect and register proxies with frps.
// If any client has no proxies (e.g. visitor-only), fall back to sleep if len(clientProcesses) > 0 {
// for the remaining time since visitors have no deterministic readiness signal. time.Sleep(1500 * time.Millisecond)
allConfirmed := len(clientProcesses) > 0
start := time.Now()
for i, p := range clientProcesses {
configPath := f.clientConfPaths[len(f.clientConfPaths)-len(clientProcesses)+i]
if !waitForClientProxyReady(configPath, p, 5*time.Second) {
allConfirmed = false
}
}
if len(clientProcesses) > 0 && !allConfirmed {
remaining := 1500*time.Millisecond - time.Since(start)
if remaining > 0 {
time.Sleep(remaining)
}
} }
return serverProcess, clientProcesses return serverProcess, clientProcesses
@@ -119,31 +105,6 @@ func (f *Framework) GenerateConfigFile(content string) string {
return path return path
} }
// waitForClientProxyReady parses the client config to extract proxy names,
// then waits for each proxy's "start proxy success" log in the process output.
// Returns true only if proxies were expected and all registered successfully.
func waitForClientProxyReady(configPath string, p *process.Process, timeout time.Duration) bool {
_, proxyCfgs, _, _, err := config.LoadClientConfig(configPath, false)
if err != nil || len(proxyCfgs) == 0 {
return false
}
// Use a single deadline so the total wait across all proxies does not exceed timeout.
deadline := time.Now().Add(timeout)
for _, cfg := range proxyCfgs {
remaining := time.Until(deadline)
if remaining <= 0 {
return false
}
name := cfg.GetBaseConfig().Name
pattern := fmt.Sprintf("[%s] start proxy success", name)
if err := p.WaitForOutput(pattern, 1, remaining); err != nil {
return false
}
}
return true
}
// WaitForTCPReady polls a TCP address until a connection succeeds or timeout. // WaitForTCPReady polls a TCP address until a connection succeeds or timeout.
func WaitForTCPReady(addr string, timeout time.Duration) error { func WaitForTCPReady(addr string, timeout time.Duration) error {
if timeout <= 0 { if timeout <= 0 {
+4 -49
View File
@@ -4,37 +4,15 @@ import (
"bytes" "bytes"
"context" "context"
"errors" "errors"
"fmt"
"os/exec" "os/exec"
"strings"
"sync" "sync"
"time"
) )
// SafeBuffer is a thread-safe wrapper around bytes.Buffer.
// It is safe to call Write and String concurrently.
type SafeBuffer struct {
mu sync.Mutex
buf bytes.Buffer
}
func (b *SafeBuffer) Write(p []byte) (int, error) {
b.mu.Lock()
defer b.mu.Unlock()
return b.buf.Write(p)
}
func (b *SafeBuffer) String() string {
b.mu.Lock()
defer b.mu.Unlock()
return b.buf.String()
}
type Process struct { type Process struct {
cmd *exec.Cmd cmd *exec.Cmd
cancel context.CancelFunc cancel context.CancelFunc
errorOutput *SafeBuffer errorOutput *bytes.Buffer
stdOutput *SafeBuffer stdOutput *bytes.Buffer
done chan struct{} done chan struct{}
closeOne sync.Once closeOne sync.Once
@@ -58,8 +36,8 @@ func NewWithEnvs(path string, params []string, envs []string) *Process {
cancel: cancel, cancel: cancel,
done: make(chan struct{}), done: make(chan struct{}),
} }
p.errorOutput = &SafeBuffer{} p.errorOutput = bytes.NewBufferString("")
p.stdOutput = &SafeBuffer{} p.stdOutput = bytes.NewBufferString("")
cmd.Stderr = p.errorOutput cmd.Stderr = p.errorOutput
cmd.Stdout = p.stdOutput cmd.Stdout = p.stdOutput
return p return p
@@ -123,26 +101,3 @@ func (p *Process) Output() string {
func (p *Process) SetBeforeStopHandler(fn func()) { func (p *Process) SetBeforeStopHandler(fn func()) {
p.beforeStopHandler = fn p.beforeStopHandler = fn
} }
// WaitForOutput polls the combined process output until the pattern is found
// count time(s) or the timeout is reached. It also returns early if the process exits.
func (p *Process) WaitForOutput(pattern string, count int, timeout time.Duration) error {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
output := p.Output()
if strings.Count(output, pattern) >= count {
return nil
}
select {
case <-p.Done():
// Process exited, check one last time.
output = p.Output()
if strings.Count(output, pattern) >= count {
return nil
}
return fmt.Errorf("process exited before %d occurrence(s) of %q found", count, pattern)
case <-time.After(25 * time.Millisecond):
}
}
return fmt.Errorf("timeout waiting for %d occurrence(s) of %q", count, pattern)
}