| package agent
|
|
|
| import (
|
| "context"
|
| "errors"
|
| "log/slog"
|
| "net"
|
| "os"
|
| "os/signal"
|
| "strings"
|
| "syscall"
|
| "time"
|
|
|
| "github.com/henrygd/beszel/agent/health"
|
| "github.com/henrygd/beszel/agent/utils"
|
| "github.com/henrygd/beszel/internal/entities/system"
|
| )
|
|
|
|
|
|
|
|
|
| type ConnectionManager struct {
|
| agent *Agent
|
| State ConnectionState
|
| eventChan chan ConnectionEvent
|
| wsClient *WebSocketClient
|
| serverOptions ServerOptions
|
| wsTicker *time.Ticker
|
| isConnecting bool
|
| ConnectionType system.ConnectionType
|
| }
|
|
|
|
|
| type ConnectionState uint8
|
|
|
|
|
| type ConnectionEvent uint8
|
|
|
|
|
| const (
|
| Disconnected ConnectionState = iota
|
| WebSocketConnected
|
| SSHConnected
|
| )
|
|
|
|
|
| const (
|
| WebSocketConnect ConnectionEvent = iota
|
| WebSocketDisconnect
|
| SSHConnect
|
| SSHDisconnect
|
| )
|
|
|
| const wsTickerInterval = 10 * time.Second
|
|
|
|
|
| func newConnectionManager(agent *Agent) *ConnectionManager {
|
| cm := &ConnectionManager{
|
| agent: agent,
|
| State: Disconnected,
|
| }
|
| return cm
|
| }
|
|
|
|
|
| func (c *ConnectionManager) startWsTicker() {
|
| if c.wsTicker == nil {
|
| c.wsTicker = time.NewTicker(wsTickerInterval)
|
| } else {
|
| c.wsTicker.Reset(wsTickerInterval)
|
| }
|
| }
|
|
|
|
|
| func (c *ConnectionManager) stopWsTicker() {
|
| if c.wsTicker != nil {
|
| c.wsTicker.Stop()
|
| }
|
| }
|
|
|
|
|
|
|
| func (c *ConnectionManager) Start(serverOptions ServerOptions) error {
|
| if c.eventChan != nil {
|
| return errors.New("already started")
|
| }
|
|
|
| wsClient, err := newWebSocketClient(c.agent)
|
| if err != nil {
|
| slog.Warn("Error creating WebSocket client", "err", err)
|
| }
|
| c.wsClient = wsClient
|
|
|
| c.serverOptions = serverOptions
|
| c.eventChan = make(chan ConnectionEvent, 1)
|
|
|
|
|
| sigCtx, stopSignals := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
| defer stopSignals()
|
|
|
| c.startWsTicker()
|
| c.connect()
|
|
|
|
|
| _ = health.Update()
|
| healthTicker := time.Tick(90 * time.Second)
|
|
|
| for {
|
| select {
|
| case connectionEvent := <-c.eventChan:
|
| c.handleEvent(connectionEvent)
|
| case <-c.wsTicker.C:
|
| _ = c.startWebSocketConnection()
|
| case <-healthTicker:
|
| _ = health.Update()
|
| case <-sigCtx.Done():
|
| slog.Info("Shutting down", "cause", context.Cause(sigCtx))
|
| return c.stop()
|
| }
|
| }
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| func (c *ConnectionManager) stop() error {
|
| _ = c.agent.StopServer()
|
| c.closeWebSocket()
|
| return health.CleanUp()
|
| }
|
|
|
|
|
| func (c *ConnectionManager) handleEvent(event ConnectionEvent) {
|
| switch event {
|
| case WebSocketConnect:
|
| c.handleStateChange(WebSocketConnected)
|
| case SSHConnect:
|
| c.handleStateChange(SSHConnected)
|
| case WebSocketDisconnect:
|
| if c.State == WebSocketConnected {
|
| c.handleStateChange(Disconnected)
|
| }
|
| case SSHDisconnect:
|
| if c.State == SSHConnected {
|
| c.handleStateChange(Disconnected)
|
| }
|
| }
|
| }
|
|
|
|
|
|
|
| func (c *ConnectionManager) handleStateChange(newState ConnectionState) {
|
| if c.State == newState {
|
| return
|
| }
|
| c.State = newState
|
| switch newState {
|
| case WebSocketConnected:
|
| slog.Info("WebSocket connected", "host", c.wsClient.hubURL.Host)
|
| c.ConnectionType = system.ConnectionTypeWebSocket
|
| c.stopWsTicker()
|
| _ = c.agent.StopServer()
|
| c.isConnecting = false
|
| case SSHConnected:
|
|
|
| slog.Info("SSH connection established")
|
| c.ConnectionType = system.ConnectionTypeSSH
|
| c.stopWsTicker()
|
| c.isConnecting = false
|
| case Disconnected:
|
| c.ConnectionType = system.ConnectionTypeNone
|
| if c.isConnecting {
|
|
|
| return
|
| }
|
| c.isConnecting = true
|
| slog.Warn("Disconnected from hub")
|
|
|
| c.closeWebSocket()
|
|
|
| go c.connect()
|
| }
|
| }
|
|
|
|
|
|
|
| func (c *ConnectionManager) connect() {
|
| c.isConnecting = true
|
| defer func() {
|
| c.isConnecting = false
|
| }()
|
|
|
| if c.wsClient != nil && time.Since(c.wsClient.lastConnectAttempt) < 5*time.Second {
|
| time.Sleep(5 * time.Second)
|
| }
|
|
|
|
|
| err := c.startWebSocketConnection()
|
| if err != nil {
|
| if shouldExitOnErr(err) {
|
| time.Sleep(2 * time.Second)
|
| _ = c.stop()
|
| os.Exit(1)
|
| }
|
| if c.State == Disconnected {
|
| c.startSSHServer()
|
| c.startWsTicker()
|
| }
|
| }
|
| }
|
|
|
|
|
| func (c *ConnectionManager) startWebSocketConnection() error {
|
| if c.State != Disconnected {
|
| return errors.New("already connected")
|
| }
|
| if c.wsClient == nil {
|
| return errors.New("WebSocket client not initialized")
|
| }
|
| if time.Since(c.wsClient.lastConnectAttempt) < 5*time.Second {
|
| return errors.New("already connecting")
|
| }
|
|
|
| err := c.wsClient.Connect()
|
| if err != nil {
|
| slog.Warn("WebSocket connection failed", "err", err)
|
| c.closeWebSocket()
|
| }
|
| return err
|
| }
|
|
|
|
|
| func (c *ConnectionManager) startSSHServer() {
|
| if c.State == Disconnected {
|
| go c.agent.StartServer(c.serverOptions)
|
| }
|
| }
|
|
|
|
|
| func (c *ConnectionManager) closeWebSocket() {
|
| if c.wsClient != nil {
|
| c.wsClient.Close()
|
| }
|
| }
|
|
|
|
|
|
|
| func shouldExitOnErr(err error) bool {
|
| if val, _ := utils.GetEnv("EXIT_ON_DNS_ERROR"); val == "true" {
|
| if opErr, ok := errors.AsType[*net.OpError](err); ok {
|
| return strings.Contains(opErr.Err.Error(), "lookup")
|
| }
|
| }
|
| return false
|
| }
|
|
|