mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-09 09:06:36 +08:00
fix lint
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
// Package config loads and persists relay daemon configuration.
|
||||
package config
|
||||
|
||||
import (
|
||||
@@ -12,8 +13,16 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/edge/nodeip"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultHeartbeatInterval = 10 * time.Second
|
||||
defaultRequestTimeout = 10 * time.Second
|
||||
configFilePerm = 0o600
|
||||
)
|
||||
|
||||
// MillisecondDuration is a JSON-friendly duration type shared with edge config.
|
||||
type MillisecondDuration = edgeconfig.MillisecondDuration
|
||||
|
||||
// Config holds relay daemon settings loaded from file and environment.
|
||||
type Config struct {
|
||||
ServerURL string `json:"server_url"`
|
||||
AgentToken string `json:"agent_token"`
|
||||
@@ -28,8 +37,9 @@ type Config struct {
|
||||
configPath string
|
||||
}
|
||||
|
||||
// Load reads configuration from path, applying environment overrides and defaults.
|
||||
func Load(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
data, err := os.ReadFile(path) //nolint:gosec // path is the relay config file from startup
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
@@ -105,10 +115,10 @@ func applyDefaults(cfg *Config, baseDir string) {
|
||||
cfg.StatePath = filepath.Join(cfg.DataDir, "relay-state.json")
|
||||
}
|
||||
if cfg.HeartbeatInterval <= 0 {
|
||||
cfg.HeartbeatInterval = MillisecondDuration(10 * time.Second)
|
||||
cfg.HeartbeatInterval = MillisecondDuration(defaultHeartbeatInterval)
|
||||
}
|
||||
if cfg.RequestTimeout <= 0 {
|
||||
cfg.RequestTimeout = MillisecondDuration(10 * time.Second)
|
||||
cfg.RequestTimeout = MillisecondDuration(defaultRequestTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +135,7 @@ func validate(cfg *Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitialAuthToken returns the agent or discovery token used for authentication.
|
||||
func (cfg *Config) InitialAuthToken() string {
|
||||
if cfg == nil {
|
||||
return ""
|
||||
@@ -135,6 +146,7 @@ func (cfg *Config) InitialAuthToken() string {
|
||||
return strings.TrimSpace(cfg.DiscoveryToken)
|
||||
}
|
||||
|
||||
// Save writes the current configuration back to the loaded config path.
|
||||
func (cfg *Config) Save() error {
|
||||
if cfg == nil {
|
||||
return errors.New("config 不能为空")
|
||||
@@ -146,7 +158,5 @@ func (cfg *Config) Save() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(cfg.configPath, data, 0o644)
|
||||
return os.WriteFile(cfg.configPath, data, configFilePerm)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
package config
|
||||
|
||||
// Version is the relay daemon build version string.
|
||||
var Version = "dev"
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Package frps manages the lifecycle of the frps reverse-proxy process:
|
||||
// rendering its TOML config, supervising the child process with exponential-
|
||||
// backoff restarts, and exposing runtime status to the heartbeat subsystem.
|
||||
package frps
|
||||
|
||||
import (
|
||||
@@ -15,6 +18,19 @@ import (
|
||||
service "github.com/Rain-kl/Wavelet/pkg/protocol"
|
||||
)
|
||||
|
||||
const (
|
||||
statusUnhealthy = "unhealthy"
|
||||
|
||||
defaultFrpsWebServerPort = 17500
|
||||
frpsSupervisorPollInterval = 100 * time.Millisecond
|
||||
frpsOrphanProcessCleanupDelay = 500 * time.Millisecond
|
||||
frpsDataDirPerm = 0o750
|
||||
frpsConfigFilePerm = 0o600
|
||||
frpsPidFilePerm = 0o600
|
||||
)
|
||||
|
||||
// Manager controls a single frps child process. It renders TOML configuration,
|
||||
// supervises the process with automatic restarts, and reports runtime status.
|
||||
type Manager struct {
|
||||
frpsPath string
|
||||
dataDir string
|
||||
@@ -31,6 +47,7 @@ type Manager struct {
|
||||
stopping bool
|
||||
}
|
||||
|
||||
// RuntimeStatus is a snapshot of the frps process state at the time of the call.
|
||||
type RuntimeStatus struct {
|
||||
Status string
|
||||
LastError string
|
||||
@@ -41,6 +58,8 @@ type RuntimeStatus struct {
|
||||
ProcessAlive bool
|
||||
}
|
||||
|
||||
// NewManager constructs a Manager that will run frpsPath and store its PID file
|
||||
// and generated configuration under dataDir.
|
||||
func NewManager(frpsPath string, dataDir string, agentToken string) *Manager {
|
||||
return &Manager{
|
||||
frpsPath: frpsPath,
|
||||
@@ -52,11 +71,9 @@ func NewManager(frpsPath string, dataDir string, agentToken string) *Manager {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) GetVersion() string {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, m.frpsPath, "-v")
|
||||
// GetVersion executes frps -v and returns the trimmed version string.
|
||||
func (m *Manager) GetVersion(ctx context.Context) string {
|
||||
cmd := exec.CommandContext(ctx, m.frpsPath, "-v") //nolint:gosec // frpsPath is the configured trusted frps binary location
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
slog.Error("failed to get frps version", "error", err)
|
||||
@@ -65,12 +82,16 @@ func (m *Manager) GetVersion() string {
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
// GetStatus returns the current status string (e.g. "healthy", "unhealthy") under
|
||||
// a read lock.
|
||||
func (m *Manager) GetStatus() string {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.status
|
||||
}
|
||||
|
||||
// GetRuntimeStatus returns a RuntimeStatus snapshot without blocking the
|
||||
// supervisor goroutine for more than the duration of a read lock.
|
||||
func (m *Manager) GetRuntimeStatus() RuntimeStatus {
|
||||
m.mu.RLock()
|
||||
status := m.status
|
||||
@@ -89,7 +110,10 @@ func (m *Manager) GetRuntimeStatus() RuntimeStatus {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) UpdateConfig(cfg *service.RelayConfig) {
|
||||
// UpdateConfig applies a new RelayConfig. If the configuration has not changed
|
||||
// and frps is already running, this is a no-op. Otherwise the existing process
|
||||
// is killed and a new supervisor goroutine is started.
|
||||
func (m *Manager) UpdateConfig(ctx context.Context, cfg *service.RelayConfig) {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
@@ -109,11 +133,11 @@ func (m *Manager) UpdateConfig(cfg *service.RelayConfig) {
|
||||
generation := m.generation
|
||||
if err := m.renderConfig(cfg); err != nil {
|
||||
slog.Error("failed to render frps config", "error", err)
|
||||
m.status = "unhealthy"
|
||||
m.status = statusUnhealthy
|
||||
m.lastError = err.Error()
|
||||
return
|
||||
}
|
||||
go m.supervise(generation)
|
||||
go m.supervise(ctx, generation)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -132,27 +156,27 @@ func (m *Manager) UpdateConfig(cfg *service.RelayConfig) {
|
||||
|
||||
if err := m.renderConfig(cfg); err != nil {
|
||||
slog.Error("failed to render frps config", "error", err)
|
||||
m.status = "unhealthy"
|
||||
m.status = statusUnhealthy
|
||||
m.lastError = err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
go m.supervise(generation)
|
||||
go m.supervise(ctx, generation)
|
||||
}
|
||||
|
||||
func (m *Manager) renderConfig(cfg *service.RelayConfig) error {
|
||||
if err := os.MkdirAll(m.dataDir, 0755); err != nil {
|
||||
if err := os.MkdirAll(m.dataDir, frpsDataDirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(fmt.Sprintf("bindPort = %d\n", cfg.BindPort))
|
||||
fmt.Fprintf(&buf, "bindPort = %d\n", cfg.BindPort)
|
||||
if cfg.VhostHTTPPort > 0 {
|
||||
buf.WriteString(fmt.Sprintf("vhostHTTPPort = %d\n", cfg.VhostHTTPPort))
|
||||
fmt.Fprintf(&buf, "vhostHTTPPort = %d\n", cfg.VhostHTTPPort)
|
||||
}
|
||||
if cfg.AuthToken != "" {
|
||||
buf.WriteString("[auth]\n")
|
||||
buf.WriteString("method = \"token\"\n")
|
||||
buf.WriteString(fmt.Sprintf("token = \"%s\"\n", cfg.AuthToken))
|
||||
fmt.Fprintf(&buf, "token = \"%s\"\n", cfg.AuthToken)
|
||||
}
|
||||
|
||||
// WebServer configuration
|
||||
@@ -162,19 +186,20 @@ func (m *Manager) renderConfig(cfg *service.RelayConfig) error {
|
||||
} else {
|
||||
buf.WriteString("addr = \"127.0.0.1\"\n")
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf("port = %d\n", 17500))
|
||||
fmt.Fprintf(&buf, "port = %d\n", defaultFrpsWebServerPort)
|
||||
buf.WriteString("user = \"admin\"\n")
|
||||
|
||||
password := m.agentToken
|
||||
if password == "" {
|
||||
password = "admin"
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf("password = \"%s\"\n", password))
|
||||
fmt.Fprintf(&buf, "password = \"%s\"\n", password)
|
||||
|
||||
return os.WriteFile(m.configPath, buf.Bytes(), 0644)
|
||||
return os.WriteFile(m.configPath, buf.Bytes(), frpsConfigFilePerm)
|
||||
}
|
||||
|
||||
func (m *Manager) supervise(generation uint64) {
|
||||
func (m *Manager) supervise(ctx context.Context, generation uint64) {
|
||||
procCtx := context.WithoutCancel(ctx)
|
||||
backoff := 1 * time.Second
|
||||
const maxBackoff = 60 * time.Second
|
||||
|
||||
@@ -187,13 +212,13 @@ func (m *Manager) supervise(generation uint64) {
|
||||
|
||||
ensureNoOrphanProcess(m.pidPath)
|
||||
|
||||
cmd := exec.Command(m.frpsPath, "-c", m.configPath)
|
||||
cmd := exec.CommandContext(procCtx, m.frpsPath, "-c", m.configPath) //nolint:gosec // frpsPath and configPath are managed trusted locations
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
m.status = "unhealthy"
|
||||
m.status = statusUnhealthy
|
||||
m.lastError = fmt.Sprintf("failed to start: %v", err)
|
||||
slog.Error("failed to start frps", "error", err, "generation", generation)
|
||||
m.mu.Unlock()
|
||||
@@ -201,14 +226,14 @@ func (m *Manager) supervise(generation uint64) {
|
||||
if !m.sleepOrInterrupt(generation, backoff) {
|
||||
return
|
||||
}
|
||||
backoff = backoff * 2
|
||||
backoff *= 2
|
||||
if backoff > maxBackoff {
|
||||
backoff = maxBackoff
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
_ = os.WriteFile(m.pidPath, []byte(fmt.Sprintf("%d", cmd.Process.Pid)), 0644)
|
||||
_ = os.WriteFile(m.pidPath, []byte(fmt.Sprintf("%d", cmd.Process.Pid)), frpsPidFilePerm)
|
||||
|
||||
m.cmd = cmd
|
||||
m.status = "healthy"
|
||||
@@ -222,7 +247,7 @@ func (m *Manager) supervise(generation uint64) {
|
||||
m.mu.Lock()
|
||||
if m.cmd == cmd {
|
||||
m.cmd = nil
|
||||
m.status = "unhealthy"
|
||||
m.status = statusUnhealthy
|
||||
if waitErr != nil {
|
||||
m.lastError = fmt.Sprintf("exited with error: %v", waitErr)
|
||||
} else {
|
||||
@@ -244,7 +269,7 @@ func (m *Manager) supervise(generation uint64) {
|
||||
if !m.sleepOrInterrupt(generation, backoff) {
|
||||
return
|
||||
}
|
||||
backoff = backoff * 2
|
||||
backoff *= 2
|
||||
if backoff > maxBackoff {
|
||||
backoff = maxBackoff
|
||||
}
|
||||
@@ -252,24 +277,24 @@ func (m *Manager) supervise(generation uint64) {
|
||||
}
|
||||
|
||||
func (m *Manager) sleepOrInterrupt(generation uint64, d time.Duration) bool {
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
ticker := time.NewTicker(frpsSupervisorPollInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
deadline := time.Now().Add(d)
|
||||
for time.Now().Before(deadline) {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
m.mu.RLock()
|
||||
interrupted := m.stopping || m.generation != generation
|
||||
m.mu.RUnlock()
|
||||
if interrupted {
|
||||
return false
|
||||
}
|
||||
<-ticker.C
|
||||
m.mu.RLock()
|
||||
interrupted := m.stopping || m.generation != generation
|
||||
m.mu.RUnlock()
|
||||
if interrupted {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Stop signals the supervisor to cease restarting frps and kills the running
|
||||
// process if one exists.
|
||||
func (m *Manager) Stop() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
@@ -280,11 +305,11 @@ func (m *Manager) Stop() {
|
||||
m.cmd = nil
|
||||
}
|
||||
_ = os.Remove(m.pidPath)
|
||||
m.status = "unhealthy"
|
||||
m.status = statusUnhealthy
|
||||
}
|
||||
|
||||
func ensureNoOrphanProcess(pidPath string) {
|
||||
data, err := os.ReadFile(pidPath)
|
||||
data, err := os.ReadFile(pidPath) //nolint:gosec // pidPath is a managed internal path, not user input
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -300,7 +325,7 @@ func ensureNoOrphanProcess(pidPath string) {
|
||||
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)
|
||||
time.Sleep(frpsOrphanProcessCleanupDelay)
|
||||
}
|
||||
_ = os.Remove(pidPath)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package frps
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -93,7 +94,7 @@ func TestStartProcessSuccess(t *testing.T) {
|
||||
WebServerEnabled: false,
|
||||
}
|
||||
|
||||
m.UpdateConfig(cfg)
|
||||
m.UpdateConfig(context.Background(), cfg)
|
||||
|
||||
assertStatusEventually(t, m, "healthy", 2*time.Second)
|
||||
|
||||
@@ -117,7 +118,7 @@ func TestStartProcessFailureAndBackoff(t *testing.T) {
|
||||
WebServerEnabled: false,
|
||||
}
|
||||
|
||||
m.UpdateConfig(cfg)
|
||||
m.UpdateConfig(context.Background(), cfg)
|
||||
|
||||
assertStatusEventually(t, m, "unhealthy", 2*time.Second)
|
||||
|
||||
@@ -158,7 +159,7 @@ func TestUnexpectedExitAndAutorestart(t *testing.T) {
|
||||
WebServerEnabled: false,
|
||||
}
|
||||
|
||||
m.UpdateConfig(cfg)
|
||||
m.UpdateConfig(context.Background(), cfg)
|
||||
|
||||
assertStatusEventually(t, m, "unhealthy", 2*time.Second)
|
||||
|
||||
@@ -189,7 +190,7 @@ func TestBackoffReset(t *testing.T) {
|
||||
WebServerEnabled: false,
|
||||
}
|
||||
|
||||
m.UpdateConfig(cfg)
|
||||
m.UpdateConfig(context.Background(), cfg)
|
||||
|
||||
// Crashed once, backoff is 2s
|
||||
assertStatusEventually(t, m, "unhealthy", 2*time.Second)
|
||||
@@ -232,7 +233,7 @@ func TestImmediateRestartOnSameConfigDeadProcess(t *testing.T) {
|
||||
WebServerEnabled: false,
|
||||
}
|
||||
|
||||
m.UpdateConfig(cfg)
|
||||
m.UpdateConfig(context.Background(), cfg)
|
||||
|
||||
// Let it crash
|
||||
assertStatusEventually(t, m, "unhealthy", 2*time.Second)
|
||||
@@ -241,7 +242,7 @@ func TestImmediateRestartOnSameConfigDeadProcess(t *testing.T) {
|
||||
writeControl(t, dir, 0, 5)
|
||||
|
||||
// Send same config block to trigger immediate restart bypass of backoff sleep
|
||||
m.UpdateConfig(cfg)
|
||||
m.UpdateConfig(context.Background(), cfg)
|
||||
|
||||
// Check if it started immediately
|
||||
assertStatusEventually(t, m, "healthy", 2*time.Second)
|
||||
@@ -261,7 +262,7 @@ func TestSupervisorGenerationInterrupt(t *testing.T) {
|
||||
WebServerEnabled: false,
|
||||
}
|
||||
|
||||
m.UpdateConfig(cfg)
|
||||
m.UpdateConfig(context.Background(), cfg)
|
||||
|
||||
assertStatusEventually(t, m, "healthy", 2*time.Second)
|
||||
|
||||
@@ -281,7 +282,7 @@ func TestSupervisorGenerationInterrupt(t *testing.T) {
|
||||
AuthToken: "test-auth",
|
||||
WebServerEnabled: false,
|
||||
}
|
||||
m.UpdateConfig(cfg2)
|
||||
m.UpdateConfig(context.Background(), cfg2)
|
||||
|
||||
assertStatusEventually(t, m, "healthy", 2*time.Second)
|
||||
|
||||
@@ -338,7 +339,7 @@ func TestUpdateConfigKillsOrphanProcessBeforeRestart(t *testing.T) {
|
||||
WebServerEnabled: false,
|
||||
}
|
||||
|
||||
m.UpdateConfig(cfg)
|
||||
m.UpdateConfig(context.Background(), cfg)
|
||||
|
||||
assertCommandExitedEventually(t, orphan, 2*time.Second)
|
||||
assertStatusEventually(t, m, "healthy", 2*time.Second)
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
// Package heartbeat sends periodic relay node status to the control plane.
|
||||
package heartbeat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
edgeheartbeat "github.com/Rain-kl/Wavelet/internal/apps/edge/heartbeat"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/relay/config"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/relay/frps"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/relay/httpclient"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/relay/observability"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/relay/state"
|
||||
edgeheartbeat "github.com/Rain-kl/Wavelet/internal/apps/edge/heartbeat"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/relay/updater"
|
||||
service "github.com/Rain-kl/Wavelet/pkg/protocol"
|
||||
)
|
||||
|
||||
// Service sends periodic heartbeat payloads to the server, updates the frps
|
||||
// configuration from the server response, and triggers auto-update checks.
|
||||
type Service struct {
|
||||
client *httpclient.Client
|
||||
frpsManager *frps.Manager
|
||||
@@ -22,6 +25,8 @@ type Service struct {
|
||||
updater *updater.Service
|
||||
}
|
||||
|
||||
// New constructs a Service using the provided HTTP client, frps manager,
|
||||
// configuration, and persistent state store.
|
||||
func New(client *httpclient.Client, manager *frps.Manager, cfg *config.Config, stateStore *state.Store) *Service {
|
||||
return &Service{
|
||||
client: client,
|
||||
@@ -32,6 +37,7 @@ func New(client *httpclient.Client, manager *frps.Manager, cfg *config.Config, s
|
||||
}
|
||||
}
|
||||
|
||||
// Run starts the heartbeat loop and blocks until ctx is cancelled.
|
||||
func (s *Service) Run(ctx context.Context) {
|
||||
edgeheartbeat.RunLoop(ctx, s.config.HeartbeatInterval.Duration(), s.doHeartbeat)
|
||||
}
|
||||
@@ -42,7 +48,7 @@ func (s *Service) doHeartbeat(ctx context.Context) {
|
||||
runtimeStatus := s.frpsManager.GetRuntimeStatus()
|
||||
payload := service.RelayHeartbeatPayload{
|
||||
Version: config.Version,
|
||||
ExtVersion: s.frpsManager.GetVersion(),
|
||||
ExtVersion: s.frpsManager.GetVersion(ctx),
|
||||
RelayStatus: runtimeStatus.Status,
|
||||
FrpsConnCount: runtimeStatus.Connections,
|
||||
FrpsProxyCount: runtimeStatus.ProxyCount,
|
||||
@@ -63,10 +69,12 @@ func (s *Service) doHeartbeat(ctx context.Context) {
|
||||
slog.Debug("heartbeat succeeded")
|
||||
|
||||
// Update configs if changed
|
||||
s.frpsManager.UpdateConfig(resp.RelayConfig)
|
||||
if resp != nil {
|
||||
s.frpsManager.UpdateConfig(ctx, resp.RelayConfig)
|
||||
|
||||
if resp != nil && resp.RelaySettings != nil {
|
||||
edgeheartbeat.TryAutoUpdate(ctx, s.updater, relaySettingsToAutoUpdate(resp.RelaySettings), "relay")
|
||||
if resp.RelaySettings != nil {
|
||||
edgeheartbeat.TryAutoUpdate(ctx, s.updater, relaySettingsToAutoUpdate(resp.RelaySettings), "relay")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package httpclient provides an HTTP client for relay control-plane communication.
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
@@ -8,21 +9,25 @@ import (
|
||||
service "github.com/Rain-kl/Wavelet/pkg/protocol"
|
||||
)
|
||||
|
||||
// APIResponse wraps a control-plane API response with an optional error message.
|
||||
type APIResponse[T any] struct {
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
Data T `json:"data"`
|
||||
}
|
||||
|
||||
// Client sends authenticated requests to the relay control-plane API.
|
||||
type Client struct {
|
||||
base *edgehttp.Client
|
||||
}
|
||||
|
||||
// New creates a relay HTTP client with the given base URL, token, and timeout.
|
||||
func New(baseURL string, token string, timeout time.Duration) *Client {
|
||||
return &Client{
|
||||
base: edgehttp.New(baseURL, token, timeout, "X-Agent-Token"),
|
||||
}
|
||||
}
|
||||
|
||||
// Heartbeat sends a relay heartbeat payload to the control plane.
|
||||
func (c *Client) Heartbeat(ctx context.Context, payload service.RelayHeartbeatPayload) (*service.RelayHeartbeatResponse, error) {
|
||||
resp := APIResponse[service.RelayHeartbeatResponse]{}
|
||||
if err := c.base.PostJSON(ctx, "/api/v1/relay/heartbeat", payload, &resp); err != nil {
|
||||
@@ -34,6 +39,7 @@ func (c *Client) Heartbeat(ctx context.Context, payload service.RelayHeartbeatPa
|
||||
return &resp.Data, nil
|
||||
}
|
||||
|
||||
// SetToken updates the authentication token used for API requests.
|
||||
func (c *Client) SetToken(token string) {
|
||||
c.base.SetToken(token)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package observability collects relay node profile data for heartbeat reporting.
|
||||
package observability
|
||||
|
||||
import (
|
||||
@@ -16,6 +17,8 @@ import (
|
||||
service "github.com/Rain-kl/Wavelet/pkg/protocol"
|
||||
)
|
||||
|
||||
// BuildProfile collects the node's system profile and returns it only when the
|
||||
// fingerprint has changed since the last heartbeat, avoiding redundant uploads.
|
||||
func BuildProfile(cfg *config.Config, stateStore *state.Store) *service.AgentNodeSystemProfile {
|
||||
profile := collectProfile(cfg)
|
||||
if profile == nil || stateStore == nil {
|
||||
@@ -36,6 +39,9 @@ func BuildProfile(cfg *config.Config, stateStore *state.Store) *service.AgentNod
|
||||
return profile
|
||||
}
|
||||
|
||||
// BuildSnapshot captures a point-in-time metric snapshot including memory,
|
||||
// disk, network I/O, and CPU usage computed from a delta against the last
|
||||
// persisted CPU stat.
|
||||
func BuildSnapshot(cfg *config.Config, stateStore *state.Store) *service.AgentNodeMetricSnapshot {
|
||||
now := time.Now().UTC()
|
||||
metric := &service.AgentNodeMetricSnapshot{CapturedAtUnix: now.Unix()}
|
||||
@@ -67,6 +73,8 @@ func BuildSnapshot(cfg *config.Config, stateStore *state.Store) *service.AgentNo
|
||||
return metric
|
||||
}
|
||||
|
||||
// BuildHealthEvents converts a RuntimeStatus into a list of health events.
|
||||
// An empty slice is returned when frps is healthy.
|
||||
func BuildHealthEvents(status frps.RuntimeStatus) []service.AgentNodeHealthEvent {
|
||||
if strings.TrimSpace(status.Status) == "healthy" {
|
||||
return []service.AgentNodeHealthEvent{}
|
||||
@@ -110,4 +118,4 @@ func fingerprintProfile(profile *service.AgentNodeSystemProfile) string {
|
||||
}
|
||||
sum := sha256.Sum256(raw)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package relay implements the relay node daemon runtime loop.
|
||||
package relay
|
||||
|
||||
import (
|
||||
@@ -15,15 +16,17 @@ import (
|
||||
service "github.com/Rain-kl/Wavelet/pkg/protocol"
|
||||
)
|
||||
|
||||
// Runner manages the relay process.
|
||||
type Runner struct {
|
||||
Config *config.Config
|
||||
StateStore *state.Store
|
||||
HeartbeatService *heartbeat.Service
|
||||
FrpsManager *frps.Manager
|
||||
WebSocketService *wsclient.Client
|
||||
HttpClient *httpclient.Client
|
||||
HTTPClient *httpclient.Client
|
||||
}
|
||||
|
||||
// Run starts the relay process by initiating the heartbeat and WS reconnection loop.
|
||||
func (r *Runner) Run(ctx context.Context) error {
|
||||
go r.HeartbeatService.Run(ctx)
|
||||
|
||||
@@ -41,7 +44,7 @@ type relayWSHandler struct {
|
||||
runner *Runner
|
||||
}
|
||||
|
||||
func (h *relayWSHandler) OnConnect(ctx context.Context) error {
|
||||
func (h *relayWSHandler) OnConnect(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -53,7 +56,7 @@ func (h *relayWSHandler) HandleMessage(ctx context.Context, msg wsclient.WSMessa
|
||||
slog.Error("failed to unmarshal relay_config", "error", err)
|
||||
return nil
|
||||
}
|
||||
h.runner.FrpsManager.UpdateConfig(&cfg)
|
||||
h.runner.FrpsManager.UpdateConfig(ctx, &cfg)
|
||||
default:
|
||||
slog.Debug("ignored unknown ws message type", "type", msg.Type)
|
||||
}
|
||||
@@ -71,4 +74,4 @@ func (r *Runner) handleConnection(ctx context.Context, conn edgerunner.WSConnect
|
||||
return
|
||||
}
|
||||
_ = wsConn.RunReceiveLoop(ctx, &relayWSHandler{runner: r})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package state persists relay runtime state to disk.
|
||||
package state
|
||||
|
||||
import (
|
||||
@@ -7,11 +8,15 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
const relayStateFilePerm = 0o600
|
||||
|
||||
// Store reads and writes relay state from a JSON file.
|
||||
type Store struct {
|
||||
path string
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// State holds persisted relay metrics and authentication state.
|
||||
type State struct {
|
||||
LastAuthToken string `json:"last_auth_token"`
|
||||
LastProfileFingerprint string `json:"last_profile_fingerprint"`
|
||||
@@ -20,12 +25,14 @@ type State struct {
|
||||
LastMetricAtUnix int64 `json:"last_metric_at_unix"`
|
||||
}
|
||||
|
||||
// NewStore creates a state store backed by the file at path.
|
||||
func NewStore(path string) *Store {
|
||||
return &Store{
|
||||
path: path,
|
||||
}
|
||||
}
|
||||
|
||||
// Load reads the persisted state from disk, returning an empty state if the file is missing.
|
||||
func (s *Store) Load() (*State, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
@@ -45,6 +52,7 @@ func (s *Store) Load() (*State, error) {
|
||||
return &state, nil
|
||||
}
|
||||
|
||||
// Save writes state to disk.
|
||||
func (s *Store) Save(state *State) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -55,5 +63,5 @@ func (s *Store) Save(state *State) error {
|
||||
}
|
||||
|
||||
slog.Debug("saving relay state")
|
||||
return os.WriteFile(s.path, data, 0644)
|
||||
return os.WriteFile(s.path, data, relayStateFilePerm)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package updater provides relay self-update integration with the edge updater.
|
||||
package updater
|
||||
|
||||
import (
|
||||
@@ -5,13 +6,17 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/relay/config"
|
||||
)
|
||||
|
||||
// Service is an alias for the edge updater Service.
|
||||
type Service = edgeupdater.Service
|
||||
|
||||
// UpdateOptions is an alias for the edge updater UpdateOptions.
|
||||
type UpdateOptions = edgeupdater.UpdateOptions
|
||||
|
||||
// New creates and initializes a new updater Service for the relay.
|
||||
func New() *Service {
|
||||
return edgeupdater.New(edgeupdater.Config{
|
||||
LocalVersion: config.Version,
|
||||
AssetPrefix: "openflare-relay",
|
||||
LogLabel: "relay",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package wsclient provides a WebSocket client for relay control-plane communication.
|
||||
package wsclient
|
||||
|
||||
import (
|
||||
@@ -7,24 +8,33 @@ import (
|
||||
edgews "github.com/Rain-kl/Wavelet/internal/apps/edge/wsclient"
|
||||
)
|
||||
|
||||
// WSMessage is a WebSocket message exchanged with the control plane.
|
||||
type WSMessage = edgews.WSMessage
|
||||
|
||||
// MessageHandler processes incoming WebSocket messages.
|
||||
type MessageHandler = edgews.MessageHandler
|
||||
|
||||
// Connection represents an active WebSocket connection.
|
||||
type Connection = edgews.Connection
|
||||
|
||||
// Client connects to the relay WebSocket endpoint on the control plane.
|
||||
type Client struct {
|
||||
inner *edgews.Client
|
||||
}
|
||||
|
||||
// New creates a WebSocket client for the relay control-plane endpoint.
|
||||
func New(baseURL, token string, timeout time.Duration) *Client {
|
||||
return &Client{
|
||||
inner: edgews.New(edgews.PresetRelay, baseURL, token, timeout),
|
||||
}
|
||||
}
|
||||
|
||||
// SetToken updates the authentication token used for the WebSocket connection.
|
||||
func (c *Client) SetToken(token string) {
|
||||
c.inner.SetToken(token)
|
||||
}
|
||||
|
||||
// Connect establishes a WebSocket connection to the control plane.
|
||||
func (c *Client) Connect(ctx context.Context) (*Connection, error) {
|
||||
return c.inner.Connect(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user