2026-02-12 18:59:15 -08:00
|
|
|
package rcon
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
2026-02-17 09:01:41 -08:00
|
|
|
"log/slog"
|
2026-02-12 18:59:15 -08:00
|
|
|
"os"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/gorcon/rcon"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Client represents an RCON client for Minecraft server interaction
|
|
|
|
|
type Client struct {
|
|
|
|
|
conn *rcon.Conn
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// New creates a new RCON client
|
|
|
|
|
func New(address, password string) (*Client, error) {
|
|
|
|
|
conn, err := rcon.Dial(address, password)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to connect to RCON: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &Client{
|
|
|
|
|
conn: conn,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Execute executes a command on the Minecraft server
|
|
|
|
|
func (c *Client) Execute(command string) (string, error) {
|
|
|
|
|
if c.conn == nil {
|
|
|
|
|
return "", fmt.Errorf("RCON connection not established")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response, err := c.conn.Execute(command)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", fmt.Errorf("failed to execute command '%s': %w", command, err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 09:01:41 -08:00
|
|
|
slog.Info("executed command", "cmd", command, "resp", response)
|
|
|
|
|
|
2026-02-12 18:59:15 -08:00
|
|
|
return response, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Close closes the RCON connection
|
|
|
|
|
func (c *Client) Close() error {
|
|
|
|
|
if c.conn != nil {
|
|
|
|
|
return c.conn.Close()
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ping checks if the RCON connection is alive
|
|
|
|
|
func (c *Client) Ping() error {
|
|
|
|
|
_, err := c.Execute("help")
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 12:09:24 -08:00
|
|
|
func (c *Client) Say(msg string) error {
|
|
|
|
|
_, err := c.Execute("/say " + msg)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 18:59:15 -08:00
|
|
|
// GetServerInfo returns basic server information
|
|
|
|
|
func (c *Client) GetServerInfo() (string, error) {
|
|
|
|
|
return c.Execute("version")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ConnectWithTimeout attempts to connect with a timeout
|
|
|
|
|
func ConnectWithTimeout(address, password string, timeout time.Duration) (*Client, error) {
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
// Create a channel to receive the connection result
|
|
|
|
|
connChan := make(chan *Client, 1)
|
|
|
|
|
errChan := make(chan error, 1)
|
|
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
|
client, err := New(address, password)
|
|
|
|
|
if err != nil {
|
|
|
|
|
errChan <- err
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
connChan <- client
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case client := <-connChan:
|
|
|
|
|
return client, nil
|
|
|
|
|
case err := <-errChan:
|
|
|
|
|
return nil, err
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return nil, fmt.Errorf("timeout connecting to RCON: %w", ctx.Err())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetEnvCredentials returns RCON address and password from environment variables
|
|
|
|
|
func GetEnvCredentials() (string, string, error) {
|
|
|
|
|
address := os.Getenv("RCON_ADDRESS")
|
|
|
|
|
if address == "" {
|
|
|
|
|
return "", "", fmt.Errorf("RCON_ADDRESS environment variable not set")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
password := os.Getenv("RCON_PASSWORD")
|
|
|
|
|
if password == "" {
|
|
|
|
|
return "", "", fmt.Errorf("RCON_PASSWORD environment variable not set")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return address, password, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewFromEnv creates a new RCON client using environment variables
|
|
|
|
|
func NewFromEnv() (*Client, error) {
|
|
|
|
|
address, password, err := GetEnvCredentials()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
client, err := New(address, password)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to create RCON client from environment: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return client, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HealthCheck verifies the RCON connection is working properly
|
|
|
|
|
func (c *Client) HealthCheck() error {
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return fmt.Errorf("health check timeout")
|
|
|
|
|
default:
|
|
|
|
|
if err := c.Ping(); err != nil {
|
|
|
|
|
return fmt.Errorf("health check failed: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ExecuteWithTimeout executes a command with a timeout
|
|
|
|
|
func (c *Client) ExecuteWithTimeout(command string, timeout time.Duration) (string, error) {
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
// Create a channel to receive the response
|
|
|
|
|
responseChan := make(chan string, 1)
|
|
|
|
|
errChan := make(chan error, 1)
|
|
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
|
response, err := c.Execute(command)
|
|
|
|
|
if err != nil {
|
|
|
|
|
errChan <- err
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
responseChan <- response
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case response := <-responseChan:
|
|
|
|
|
return response, nil
|
|
|
|
|
case err := <-errChan:
|
|
|
|
|
return "", err
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return "", fmt.Errorf("timeout executing command '%s': %w", command, ctx.Err())
|
|
|
|
|
}
|
|
|
|
|
}
|