mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-05 23:26:38 +08:00
refactor(repo): consolidate openflare-server to root and move subprojects to internal/apps
- Merge all files inside openflare-server to the repository root directory. - Relocate agent, relay, and flared subprojects from internal/ to internal/apps/. - Combine docker-compose files and update build context paths to root. - Update GitHub workflows and Dockerfiles to refer to new directories and package names. - Rewrite Go package imports across all files. - Resolve database renew test race condition and clean up docs.
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
package frpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/flared/config"
|
||||
service "github.com/Rain-kl/Wavelet/pkg/protocol"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
cfg *config.Config
|
||||
processes map[string]*Process
|
||||
mu sync.RWMutex
|
||||
|
||||
currentVersion string
|
||||
currentChecksum string
|
||||
}
|
||||
|
||||
type Process struct {
|
||||
RelayID string
|
||||
Cmd *exec.Cmd
|
||||
Cancel context.CancelFunc
|
||||
Status string
|
||||
StartTime time.Time
|
||||
LastError string
|
||||
}
|
||||
|
||||
func NewManager(cfg *config.Config) *Manager {
|
||||
return &Manager{
|
||||
cfg: cfg,
|
||||
processes: make(map[string]*Process),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) GetVersion() string {
|
||||
cmd := exec.Command(m.cfg.FrpcPath, "-v")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "unknown"
|
||||
}
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
func (m *Manager) GetConnectedRelays() []service.FlaredConnectedRelay {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
result := make([]service.FlaredConnectedRelay, 0, len(m.processes))
|
||||
for relayID, proc := range m.processes {
|
||||
result = append(result, service.FlaredConnectedRelay{
|
||||
RelayNodeID: relayID,
|
||||
Status: proc.Status,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (m *Manager) GetCurrentConfigVersion() string {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.currentVersion
|
||||
}
|
||||
|
||||
func (m *Manager) GetCurrentConfigChecksum() string {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.currentChecksum
|
||||
}
|
||||
|
||||
func (m *Manager) UpdateConfig(ctx context.Context, newConfig *service.FlaredTunnelConfigResponse) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if newConfig == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
versionChanged := newConfig.Version != m.currentVersion || newConfig.Checksum != m.currentChecksum
|
||||
if versionChanged {
|
||||
slog.Info("applying new tunnel config", "version", newConfig.Version)
|
||||
} else {
|
||||
slog.Debug("tunnel config version unchanged, ensuring processes are running", "version", newConfig.Version)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(m.cfg.DataDir, 0o755); err != nil {
|
||||
return fmt.Errorf("create data dir failed: %w", err)
|
||||
}
|
||||
|
||||
activeRelays := make(map[string]struct{})
|
||||
|
||||
for _, relay := range newConfig.Relays {
|
||||
activeRelays[relay.RelayNodeID] = struct{}{}
|
||||
tomlContent := buildFrpcToml(relay, newConfig.Proxies)
|
||||
configPath := filepath.Join(m.cfg.DataDir, fmt.Sprintf("frpc_%s.toml", relay.RelayNodeID))
|
||||
|
||||
needsRestart := false
|
||||
existingData, err := os.ReadFile(configPath)
|
||||
if err != nil || string(existingData) != tomlContent {
|
||||
// 配置文件不存在或内容有变化,需要写入并重启
|
||||
needsRestart = true
|
||||
}
|
||||
|
||||
if needsRestart {
|
||||
if err := os.WriteFile(configPath, []byte(tomlContent), 0o644); err != nil {
|
||||
slog.Error("failed to write frpc config", "relay_id", relay.RelayNodeID, "error", err)
|
||||
continue
|
||||
}
|
||||
m.restartProcess(ctx, relay.RelayNodeID, configPath)
|
||||
} else if _, ok := m.processes[relay.RelayNodeID]; !ok {
|
||||
// 配置未变但进程不存在(如重启后),直接启动进程
|
||||
slog.Info("frpc process missing, starting", "relay_id", relay.RelayNodeID)
|
||||
m.restartProcess(ctx, relay.RelayNodeID, configPath)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop obsolete processes
|
||||
for relayID, proc := range m.processes {
|
||||
if _, ok := activeRelays[relayID]; !ok {
|
||||
slog.Info("stopping obsolete frpc process", "relay_id", relayID)
|
||||
proc.Cancel()
|
||||
pidPath := filepath.Join(m.cfg.DataDir, fmt.Sprintf("frpc_%s.pid", relayID))
|
||||
_ = os.Remove(pidPath)
|
||||
delete(m.processes, relayID)
|
||||
}
|
||||
}
|
||||
|
||||
if versionChanged {
|
||||
m.currentVersion = newConfig.Version
|
||||
m.currentChecksum = newConfig.Checksum
|
||||
return m.saveState()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) restartProcess(ctx context.Context, relayID string, configPath string) {
|
||||
pidPath := filepath.Join(m.cfg.DataDir, fmt.Sprintf("frpc_%s.pid", relayID))
|
||||
if proc, ok := m.processes[relayID]; ok {
|
||||
proc.Cancel()
|
||||
_ = os.Remove(pidPath)
|
||||
}
|
||||
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
procCtx, cancel := context.WithCancel(ctx)
|
||||
proc := &Process{
|
||||
RelayID: relayID,
|
||||
Cancel: cancel,
|
||||
Status: "starting",
|
||||
StartTime: time.Now(),
|
||||
}
|
||||
m.processes[relayID] = proc
|
||||
|
||||
go func() {
|
||||
backoff := 1 * time.Second
|
||||
const maxBackoff = 60 * time.Second
|
||||
|
||||
for {
|
||||
m.mu.Lock()
|
||||
if procCtx.Err() != nil {
|
||||
m.mu.Unlock()
|
||||
return
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
ensureNoOrphanProcess(pidPath)
|
||||
|
||||
cmd := exec.CommandContext(procCtx, m.cfg.FrpcPath, "-c", configPath)
|
||||
|
||||
m.mu.Lock()
|
||||
proc.Cmd = cmd
|
||||
proc.Status = "running"
|
||||
m.mu.Unlock()
|
||||
|
||||
startedAt := time.Now()
|
||||
err := cmd.Start()
|
||||
if err == nil {
|
||||
_ = os.WriteFile(pidPath, []byte(fmt.Sprintf("%d", cmd.Process.Pid)), 0o644)
|
||||
err = cmd.Wait()
|
||||
}
|
||||
_ = os.Remove(pidPath)
|
||||
|
||||
m.mu.Lock()
|
||||
if procCtx.Err() != nil {
|
||||
proc.Status = "stopped"
|
||||
m.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
proc.LastError = err.Error()
|
||||
proc.Status = "error"
|
||||
slog.Error("frpc process exited unexpectedly", "relay_id", relayID, "error", err)
|
||||
} else {
|
||||
proc.Status = "stopped"
|
||||
proc.LastError = "exited unexpectedly with code 0"
|
||||
slog.Warn("frpc process exited unexpectedly with code 0", "relay_id", relayID)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
if time.Since(startedAt) >= 10*time.Second {
|
||||
backoff = 1 * time.Second
|
||||
}
|
||||
|
||||
select {
|
||||
case <-procCtx.Done():
|
||||
return
|
||||
case <-time.After(backoff):
|
||||
backoff = backoff * 2
|
||||
if backoff > maxBackoff {
|
||||
backoff = maxBackoff
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (m *Manager) Stop() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
for relayID, proc := range m.processes {
|
||||
if proc != nil && proc.Cancel != nil {
|
||||
proc.Cancel()
|
||||
}
|
||||
pidPath := filepath.Join(m.cfg.DataDir, fmt.Sprintf("frpc_%s.pid", relayID))
|
||||
_ = os.Remove(pidPath)
|
||||
delete(m.processes, relayID)
|
||||
}
|
||||
}
|
||||
|
||||
func buildFrpcToml(relay service.FlaredRelayInfo, proxies []service.FlaredProxyEntry) string {
|
||||
var buf bytes.Buffer
|
||||
|
||||
host, port := parseAddr(relay.Address)
|
||||
|
||||
buf.WriteString(fmt.Sprintf(`serverAddr = "%s"
|
||||
serverPort = %s
|
||||
`, host, port))
|
||||
|
||||
if relay.AuthToken != "" {
|
||||
buf.WriteString(fmt.Sprintf(`auth.method = "token"
|
||||
auth.token = "%s"
|
||||
`, relay.AuthToken))
|
||||
}
|
||||
|
||||
if relay.ProxyURL != "" {
|
||||
buf.WriteString(fmt.Sprintf(`transport.proxyURL = "%s"
|
||||
`, relay.ProxyURL))
|
||||
}
|
||||
|
||||
buf.WriteString("\n")
|
||||
|
||||
for _, proxy := range proxies {
|
||||
buf.WriteString(fmt.Sprintf("[[proxies]]\nname = \"%s\"\ntype = \"%s\"\nlocalIP = \"%s\"\nlocalPort = %d\n",
|
||||
proxy.Name, proxy.Type, proxy.LocalAddr, proxy.LocalPort))
|
||||
if len(proxy.CustomDomains) > 0 {
|
||||
buf.WriteString(fmt.Sprintf("customDomains = [\"%s\"]\n", strings.Join(proxy.CustomDomains, "\", \"")))
|
||||
}
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func parseAddr(addr string) (string, string) {
|
||||
addr = strings.TrimSpace(addr)
|
||||
if addr == "" {
|
||||
return "127.0.0.1", "7000"
|
||||
}
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err == nil {
|
||||
return strings.Trim(host, "[]"), port
|
||||
}
|
||||
lastColon := strings.LastIndex(addr, ":")
|
||||
if lastColon > 0 && strings.Count(addr, ":") == 1 {
|
||||
return addr[:lastColon], addr[lastColon+1:]
|
||||
}
|
||||
return addr, "7000"
|
||||
}
|
||||
|
||||
// State persistence
|
||||
type ManagerState struct {
|
||||
Version string
|
||||
Checksum string
|
||||
}
|
||||
|
||||
func (m *Manager) saveState() error {
|
||||
state := ManagerState{
|
||||
Version: m.currentVersion,
|
||||
Checksum: m.currentChecksum,
|
||||
}
|
||||
data, err := json.MarshalIndent(state, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(m.cfg.StatePath, data, 0o644)
|
||||
}
|
||||
|
||||
func (m *Manager) LoadState() error {
|
||||
data, err := os.ReadFile(m.cfg.StatePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
var state ManagerState
|
||||
if err := json.Unmarshal(data, &state); err != nil {
|
||||
return err
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.currentVersion = state.Version
|
||||
m.currentChecksum = state.Checksum
|
||||
m.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureNoOrphanProcess(pidPath string) {
|
||||
data, err := os.ReadFile(pidPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var pid int
|
||||
if _, err := fmt.Sscanf(string(data), "%d", &pid); err != nil {
|
||||
return
|
||||
}
|
||||
if pid <= 0 {
|
||||
return
|
||||
}
|
||||
process, err := os.FindProcess(pid)
|
||||
if err == nil && process != nil {
|
||||
slog.Warn("attempting to kill potentially orphan process", "pid", pid, "pid_path", pidPath)
|
||||
_ = process.Kill()
|
||||
// Wait a little bit to ensure the OS has reclaimed ports
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
_ = os.Remove(pidPath)
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
package frpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/flared/config"
|
||||
service "github.com/Rain-kl/Wavelet/pkg/protocol"
|
||||
)
|
||||
|
||||
// Helper to write control file for the dummy script
|
||||
func writeControl(t *testing.T, dir string, exitCode int, delaySeconds int) {
|
||||
controlPath := filepath.Join(dir, "control.txt")
|
||||
content := fmt.Sprintf("%d %d\n", exitCode, delaySeconds)
|
||||
err := os.WriteFile(controlPath, []byte(content), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write control file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Setup a dummy executable script that reads control.txt to decide exit code and sleep duration
|
||||
func setupDummyScript(t *testing.T) (string, string) {
|
||||
dir := t.TempDir()
|
||||
scriptPath := filepath.Join(dir, "dummy_frpc")
|
||||
|
||||
// On macOS/Linux, we write a shell script
|
||||
scriptContent := fmt.Sprintf(`#!/bin/sh
|
||||
control_file="%s/control.txt"
|
||||
EXIT_CODE=0
|
||||
DELAY=0
|
||||
if [ -f "$control_file" ]; then
|
||||
read -r EXIT_CODE DELAY < "$control_file"
|
||||
fi
|
||||
if [ -n "$DELAY" ] && [ "$DELAY" -gt 0 ] 2>/dev/null; then
|
||||
sleep "$DELAY"
|
||||
fi
|
||||
exit "${EXIT_CODE:-0}"
|
||||
`, dir)
|
||||
|
||||
err := os.WriteFile(scriptPath, []byte(scriptContent), 0755)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write dummy script: %v", err)
|
||||
}
|
||||
|
||||
return scriptPath, dir
|
||||
}
|
||||
|
||||
// Helper to poll for status to eliminate timing flakiness in tests
|
||||
func assertStatusEventually(t *testing.T, m *Manager, relayID string, expectedStatus string, timeout time.Duration) {
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
m.mu.RLock()
|
||||
proc, ok := m.processes[relayID]
|
||||
m.mu.RUnlock()
|
||||
if ok && proc.Status == expectedStatus {
|
||||
return
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
m.mu.RLock()
|
||||
proc, ok := m.processes[relayID]
|
||||
var got string
|
||||
var errStr string
|
||||
if ok {
|
||||
got = proc.Status
|
||||
errStr = proc.LastError
|
||||
} else {
|
||||
got = "not_found"
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
t.Fatalf("expected status eventually %s, got %s (err: %s)", expectedStatus, got, errStr)
|
||||
}
|
||||
|
||||
func assertCommandExitedEventually(t *testing.T, cmd *exec.Cmd, timeout time.Duration) {
|
||||
t.Helper()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- cmd.Wait()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(timeout):
|
||||
t.Fatalf("expected process pid=%d to exit within %s", cmd.Process.Pid, timeout)
|
||||
case <-done:
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartProcessSuccess(t *testing.T) {
|
||||
scriptPath, dir := setupDummyScript(t)
|
||||
writeControl(t, dir, 0, 5) // exit code 0, sleep 5s
|
||||
|
||||
cfg := &config.Config{
|
||||
ServerURL: "http://localhost:8080",
|
||||
TunnelToken: "test-token",
|
||||
FrpcPath: scriptPath,
|
||||
DataDir: dir,
|
||||
StatePath: filepath.Join(dir, "flared-state.json"),
|
||||
}
|
||||
|
||||
m := NewManager(cfg)
|
||||
|
||||
newConfig := &service.FlaredTunnelConfigResponse{
|
||||
Version: "1",
|
||||
Checksum: "sum1",
|
||||
Relays: []service.FlaredRelayInfo{
|
||||
{
|
||||
RelayNodeID: "relay-1",
|
||||
Address: "127.0.0.1:7000",
|
||||
AuthToken: "auth-1",
|
||||
},
|
||||
},
|
||||
Proxies: nil,
|
||||
}
|
||||
|
||||
err := m.UpdateConfig(context.Background(), newConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to UpdateConfig: %v", err)
|
||||
}
|
||||
|
||||
assertStatusEventually(t, m, "relay-1", "running", 4*time.Second)
|
||||
|
||||
m.mu.RLock()
|
||||
proc := m.processes["relay-1"]
|
||||
m.mu.RUnlock()
|
||||
|
||||
proc.Cancel()
|
||||
assertStatusEventually(t, m, "relay-1", "stopped", 4*time.Second) // wait for clean stop
|
||||
}
|
||||
|
||||
func TestStartProcessFailureAndBackoff(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
invalidScriptPath := filepath.Join(dir, "non_existent_frpc")
|
||||
|
||||
cfg := &config.Config{
|
||||
ServerURL: "http://localhost:8080",
|
||||
TunnelToken: "test-token",
|
||||
FrpcPath: invalidScriptPath,
|
||||
DataDir: dir,
|
||||
StatePath: filepath.Join(dir, "flared-state.json"),
|
||||
}
|
||||
|
||||
m := NewManager(cfg)
|
||||
newConfig := &service.FlaredTunnelConfigResponse{
|
||||
Version: "1",
|
||||
Checksum: "sum1",
|
||||
Relays: []service.FlaredRelayInfo{
|
||||
{
|
||||
RelayNodeID: "relay-1",
|
||||
Address: "127.0.0.1:7000",
|
||||
AuthToken: "auth-1",
|
||||
},
|
||||
},
|
||||
Proxies: nil,
|
||||
}
|
||||
|
||||
_ = m.UpdateConfig(context.Background(), newConfig)
|
||||
|
||||
assertStatusEventually(t, m, "relay-1", "error", 4*time.Second)
|
||||
|
||||
// Correct the path to dummy script
|
||||
scriptPath, _ := setupDummyScript(t)
|
||||
writeControl(t, filepath.Dir(scriptPath), 0, 5)
|
||||
|
||||
m.mu.Lock()
|
||||
m.cfg.FrpcPath = scriptPath
|
||||
m.mu.Unlock()
|
||||
|
||||
// Wait for backoff retry (1s backoff)
|
||||
assertStatusEventually(t, m, "relay-1", "running", 4*time.Second)
|
||||
|
||||
m.mu.RLock()
|
||||
proc := m.processes["relay-1"]
|
||||
m.mu.RUnlock()
|
||||
proc.Cancel()
|
||||
}
|
||||
|
||||
func TestUnexpectedExit0CPUProtection(t *testing.T) {
|
||||
scriptPath, dir := setupDummyScript(t)
|
||||
// Start with immediate exit code 0
|
||||
writeControl(t, dir, 0, 0)
|
||||
|
||||
cfg := &config.Config{
|
||||
ServerURL: "http://localhost:8080",
|
||||
TunnelToken: "test-token",
|
||||
FrpcPath: scriptPath,
|
||||
DataDir: dir,
|
||||
StatePath: filepath.Join(dir, "flared-state.json"),
|
||||
}
|
||||
|
||||
m := NewManager(cfg)
|
||||
newConfig := &service.FlaredTunnelConfigResponse{
|
||||
Version: "1",
|
||||
Checksum: "sum1",
|
||||
Relays: []service.FlaredRelayInfo{
|
||||
{
|
||||
RelayNodeID: "relay-1",
|
||||
Address: "127.0.0.1:7000",
|
||||
AuthToken: "auth-1",
|
||||
},
|
||||
},
|
||||
Proxies: nil,
|
||||
}
|
||||
|
||||
_ = m.UpdateConfig(context.Background(), newConfig)
|
||||
|
||||
assertStatusEventually(t, m, "relay-1", "stopped", 4*time.Second)
|
||||
|
||||
m.mu.RLock()
|
||||
proc := m.processes["relay-1"]
|
||||
if !strings.Contains(proc.LastError, "exited unexpectedly with code 0") {
|
||||
t.Errorf("expected LastError to record exit status 0 warning, got %s", proc.LastError)
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
proc.Cancel()
|
||||
}
|
||||
|
||||
func TestBackoffReset(t *testing.T) {
|
||||
scriptPath, dir := setupDummyScript(t)
|
||||
// Rapid exit code 1 to increase backoff
|
||||
writeControl(t, dir, 1, 0)
|
||||
|
||||
cfg := &config.Config{
|
||||
ServerURL: "http://localhost:8080",
|
||||
TunnelToken: "test-token",
|
||||
FrpcPath: scriptPath,
|
||||
DataDir: dir,
|
||||
StatePath: filepath.Join(dir, "flared-state.json"),
|
||||
}
|
||||
|
||||
m := NewManager(cfg)
|
||||
newConfig := &service.FlaredTunnelConfigResponse{
|
||||
Version: "1",
|
||||
Checksum: "sum1",
|
||||
Relays: []service.FlaredRelayInfo{
|
||||
{
|
||||
RelayNodeID: "relay-1",
|
||||
Address: "127.0.0.1:7000",
|
||||
AuthToken: "auth-1",
|
||||
},
|
||||
},
|
||||
Proxies: nil,
|
||||
}
|
||||
|
||||
_ = m.UpdateConfig(context.Background(), newConfig)
|
||||
|
||||
// Wait to crash
|
||||
assertStatusEventually(t, m, "relay-1", "error", 4*time.Second)
|
||||
|
||||
// Now make it run successfully for 11 seconds (exit code 0, sleep 11s)
|
||||
writeControl(t, dir, 0, 11)
|
||||
|
||||
// Wait for next retry to start running
|
||||
assertStatusEventually(t, m, "relay-1", "running", 4*time.Second)
|
||||
|
||||
// Wait for process to run for 10.5 seconds to trigger backoff reset
|
||||
time.Sleep(10500 * time.Millisecond)
|
||||
|
||||
// Now make it crash again (exit code 1, sleep 0s)
|
||||
writeControl(t, dir, 1, 0)
|
||||
|
||||
// Wait for it to finish and crash
|
||||
assertStatusEventually(t, m, "relay-1", "error", 4*time.Second)
|
||||
|
||||
// It crashed. Since it ran for > 10s, backoff should have been reset to 1s.
|
||||
// We make it healthy again (exit code 0, sleep 5)
|
||||
writeControl(t, dir, 0, 5)
|
||||
|
||||
// Wait 1.5 seconds. If backoff was reset to 1s, it should be running now.
|
||||
assertStatusEventually(t, m, "relay-1", "running", 4*time.Second)
|
||||
|
||||
m.mu.RLock()
|
||||
proc := m.processes["relay-1"]
|
||||
m.mu.RUnlock()
|
||||
proc.Cancel()
|
||||
}
|
||||
|
||||
func TestUpdateConfigKillsOrphanProcessBeforeRestart(t *testing.T) {
|
||||
scriptPath, dir := setupDummyScript(t)
|
||||
writeControl(t, dir, 0, 5)
|
||||
|
||||
cfg := &config.Config{
|
||||
ServerURL: "http://localhost:8080",
|
||||
TunnelToken: "test-token",
|
||||
FrpcPath: scriptPath,
|
||||
DataDir: dir,
|
||||
StatePath: filepath.Join(dir, "flared-state.json"),
|
||||
}
|
||||
|
||||
m := NewManager(cfg)
|
||||
|
||||
orphan := exec.Command("sh", "-c", "sleep 30")
|
||||
if err := orphan.Start(); err != nil {
|
||||
t.Fatalf("failed to start orphan process: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if orphan.Process != nil {
|
||||
_ = orphan.Process.Kill()
|
||||
}
|
||||
})
|
||||
|
||||
pidPath := filepath.Join(dir, "frpc_relay-1.pid")
|
||||
if err := os.WriteFile(pidPath, []byte(fmt.Sprintf("%d", orphan.Process.Pid)), 0o644); err != nil {
|
||||
t.Fatalf("failed to seed orphan pid file: %v", err)
|
||||
}
|
||||
|
||||
newConfig := &service.FlaredTunnelConfigResponse{
|
||||
Version: "1",
|
||||
Checksum: "sum1",
|
||||
Relays: []service.FlaredRelayInfo{
|
||||
{
|
||||
RelayNodeID: "relay-1",
|
||||
Address: "127.0.0.1:7000",
|
||||
AuthToken: "auth-1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := m.UpdateConfig(context.Background(), newConfig); err != nil {
|
||||
t.Fatalf("failed to UpdateConfig: %v", err)
|
||||
}
|
||||
|
||||
assertCommandExitedEventually(t, orphan, 2*time.Second)
|
||||
assertStatusEventually(t, m, "relay-1", "running", 4*time.Second)
|
||||
|
||||
m.mu.RLock()
|
||||
proc := m.processes["relay-1"]
|
||||
m.mu.RUnlock()
|
||||
proc.Cancel()
|
||||
}
|
||||
|
||||
func TestStopCancelsRunningProcesses(t *testing.T) {
|
||||
scriptPath, dir := setupDummyScript(t)
|
||||
writeControl(t, dir, 0, 30)
|
||||
|
||||
cfg := &config.Config{
|
||||
ServerURL: "http://localhost:8080",
|
||||
TunnelToken: "test-token",
|
||||
FrpcPath: scriptPath,
|
||||
DataDir: dir,
|
||||
StatePath: filepath.Join(dir, "flared-state.json"),
|
||||
}
|
||||
|
||||
m := NewManager(cfg)
|
||||
newConfig := &service.FlaredTunnelConfigResponse{
|
||||
Version: "1",
|
||||
Checksum: "sum1",
|
||||
Relays: []service.FlaredRelayInfo{
|
||||
{
|
||||
RelayNodeID: "relay-1",
|
||||
Address: "127.0.0.1:7000",
|
||||
AuthToken: "auth-1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := m.UpdateConfig(context.Background(), newConfig); err != nil {
|
||||
t.Fatalf("failed to UpdateConfig: %v", err)
|
||||
}
|
||||
|
||||
assertStatusEventually(t, m, "relay-1", "running", 4*time.Second)
|
||||
|
||||
m.mu.RLock()
|
||||
proc := m.processes["relay-1"]
|
||||
if proc == nil || proc.Cmd == nil {
|
||||
m.mu.RUnlock()
|
||||
t.Fatal("expected running process to have a command handle")
|
||||
}
|
||||
cmd := proc.Cmd
|
||||
m.mu.RUnlock()
|
||||
|
||||
m.Stop()
|
||||
assertCommandExitedEventually(t, cmd, 2*time.Second)
|
||||
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
if len(m.processes) != 0 {
|
||||
t.Fatalf("expected no managed processes after stop, got %d", len(m.processes))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user