mirror of
https://github.com/fatedier/frp.git
synced 2026-10-15 03:45:57 +08:00
152 lines
4.9 KiB
Go
152 lines
4.9 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
|
)
|
|
|
|
type lifecycleListener struct {
|
|
net.Listener
|
|
accepted, closeEntered, closeResume chan struct{}
|
|
acceptOnce, closeOnce sync.Once
|
|
accepts, closes atomic.Int64
|
|
}
|
|
|
|
func (l *lifecycleListener) Accept() (net.Conn, error) {
|
|
l.accepts.Add(1)
|
|
l.acceptOnce.Do(func() { close(l.accepted) })
|
|
return l.Listener.Accept()
|
|
}
|
|
|
|
func (l *lifecycleListener) Close() error {
|
|
l.closes.Add(1)
|
|
err := l.Listener.Close()
|
|
if l.closeEntered != nil {
|
|
l.closeOnce.Do(func() { close(l.closeEntered) })
|
|
<-l.closeResume
|
|
}
|
|
return err
|
|
}
|
|
|
|
func freeLifecycleTCPPort(t *testing.T) int {
|
|
t.Helper()
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
defer l.Close()
|
|
return l.Addr().(*net.TCPAddr).Port
|
|
}
|
|
|
|
func newLifecycleTestService(t *testing.T, web bool) (*Service, *lifecycleListener, []string) {
|
|
t.Helper()
|
|
cfg := &v1.ServerConfig{
|
|
BindAddr: "127.0.0.1", ProxyBindAddr: "127.0.0.1",
|
|
}
|
|
if web {
|
|
cfg.WebServer = v1.WebServerConfig{Addr: "127.0.0.1", Port: freeLifecycleTCPPort(t)}
|
|
cfg.VhostHTTPPort = freeLifecycleTCPPort(t)
|
|
}
|
|
require.NoError(t, cfg.Complete())
|
|
cfg.BindPort = 0
|
|
_, err := validation.NewConfigValidator(nil).ValidateServerConfig(cfg)
|
|
require.NoError(t, err)
|
|
svr, err := NewService(cfg)
|
|
require.NoError(t, err)
|
|
l := &lifecycleListener{Listener: svr.listener, accepted: make(chan struct{})}
|
|
svr.listener = l
|
|
addresses := []string{l.Addr().String()}
|
|
if web {
|
|
addresses = append(addresses, svr.webServer.Address())
|
|
addresses = append(addresses, net.JoinHostPort(cfg.ProxyBindAddr, fmt.Sprint(cfg.VhostHTTPPort)))
|
|
}
|
|
assertLifecyclePorts(t, addresses, false)
|
|
return svr, l, addresses
|
|
}
|
|
|
|
func assertLifecyclePorts(t *testing.T, addresses []string, released bool) {
|
|
t.Helper()
|
|
for _, address := range addresses {
|
|
ln, err := net.Listen("tcp", address)
|
|
if ln != nil {
|
|
_ = ln.Close()
|
|
}
|
|
if released {
|
|
require.NoError(t, err, "listener %s was not released", address)
|
|
} else {
|
|
require.Error(t, err, "listener %s was not bound", address)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestServiceRunAfterEarlyShutdown(t *testing.T) {
|
|
for _, mode := range []string{"close before Run", "close before Run with web", "parent already cancelled"} {
|
|
t.Run(mode, func(t *testing.T) {
|
|
svr, listener, addresses := newLifecycleTestService(t, mode == "close before Run with web")
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
defer svr.Close()
|
|
if mode != "parent already cancelled" {
|
|
require.NoError(t, svr.Close())
|
|
assertLifecyclePorts(t, addresses, true)
|
|
} else {
|
|
cancel()
|
|
}
|
|
done := make(chan struct{})
|
|
go func() { svr.Run(ctx); close(done) }()
|
|
waitForSignal(t, done, "Run after early shutdown")
|
|
require.Zero(t, listener.accepts.Load(), "closed service started accepting")
|
|
require.Equal(t, int64(1), listener.closes.Load())
|
|
assertLifecyclePorts(t, addresses, true)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServiceCloseMarksClosedBeforeCleanup(t *testing.T) {
|
|
svr, listener, addresses := newLifecycleTestService(t, false)
|
|
listener.closeEntered, listener.closeResume = make(chan struct{}), make(chan struct{})
|
|
var releaseOnce sync.Once
|
|
release := func() { releaseOnce.Do(func() { close(listener.closeResume) }) }
|
|
defer release()
|
|
closeDone := make(chan struct{})
|
|
go func() { _ = svr.Close(); close(closeDone) }()
|
|
waitForSignal(t, listener.closeEntered, "Close cleanup barrier")
|
|
runDone := make(chan struct{})
|
|
go func() { svr.Run(context.Background()); close(runDone) }()
|
|
waitForSignal(t, runDone, "Run to observe closed before cleanup finishes")
|
|
require.Zero(t, listener.accepts.Load(), "Run started during cleanup")
|
|
assertLifecyclePorts(t, addresses, false)
|
|
release()
|
|
waitForSignal(t, closeDone, "Close cleanup to finish")
|
|
assertLifecyclePorts(t, addresses, true)
|
|
}
|
|
|
|
func TestServiceParentCancellationAndCloseShareCleanup(t *testing.T) {
|
|
svr, listener, addresses := newLifecycleTestService(t, false)
|
|
listener.closeEntered, listener.closeResume = make(chan struct{}), make(chan struct{})
|
|
var releaseOnce sync.Once
|
|
release := func() { releaseOnce.Do(func() { close(listener.closeResume) }) }
|
|
defer release()
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
runDone := make(chan struct{})
|
|
go func() { svr.Run(ctx); close(runDone) }()
|
|
waitForSignal(t, listener.accepted, "main listener Accept")
|
|
cancel()
|
|
waitForSignal(t, listener.closeEntered, "parent cancellation to close main listener")
|
|
closeDone := make(chan struct{})
|
|
go func() { _ = svr.Close(); close(closeDone) }()
|
|
release()
|
|
waitForSignal(t, closeDone, "external Close to finish")
|
|
waitForSignal(t, runDone, "Run cleanup to finish")
|
|
require.Equal(t, int64(1), listener.closes.Load(), "cleanup ran more than once")
|
|
assertLifecyclePorts(t, addresses, true)
|
|
}
|