q3rcon/cmd/q3rcon/main.go

191 lines
4.3 KiB
Go

package main
import (
"bufio"
"flag"
"fmt"
"io"
"os"
"runtime/debug"
"strings"
"time"
"github.com/charmbracelet/log"
"github.com/onyx-and-iris/q3rcon"
)
var version string // Version will be set at build time
func main() {
var exitCode int
// Defer exit with the final exit code
defer func() {
if exitCode != 0 {
os.Exit(exitCode)
}
}()
closer, err := run()
if closer != nil {
defer closer()
}
if err != nil {
log.Error(err)
exitCode = 1
}
}
// run executes the main logic of the application and returns a cleanup function and an error if any.
func run() (func(), error) {
var (
host string
port int
rconpass string
interactive bool
loglevel string
versionFlag bool
)
flag.StringVar(&host, "host", "localhost", "hostname of the gameserver")
flag.StringVar(&host, "h", "localhost", "hostname of the gameserver (shorthand)")
flag.IntVar(&port, "port", 28960, "port on which the gameserver resides, default is 28960")
flag.IntVar(
&port,
"p",
28960,
"port on which the gameserver resides, default is 28960 (shorthand)",
)
flag.StringVar(&rconpass, "rconpass", os.Getenv("RCON_PASS"), "rcon password of the gameserver")
flag.StringVar(
&rconpass,
"r",
os.Getenv("RCON_PASS"),
"rcon password of the gameserver (shorthand)",
)
flag.BoolVar(&interactive, "interactive", false, "run in interactive mode")
flag.BoolVar(&interactive, "i", false, "run in interactive mode")
flag.StringVar(&loglevel, "loglevel", "warn", "log level")
flag.StringVar(&loglevel, "l", "warn", "log level (shorthand)")
flag.BoolVar(&versionFlag, "version", false, "print version information and exit")
flag.BoolVar(&versionFlag, "v", false, "print version information and exit (shorthand)")
flag.Parse()
if versionFlag {
fmt.Printf("q3rcon version: %s\n", versionFromBuild())
return nil, nil
}
level, err := log.ParseLevel(loglevel)
if err != nil {
return nil, fmt.Errorf("invalid log level: %s", loglevel)
}
log.SetLevel(level)
if port < 1024 || port > 65535 {
return nil, fmt.Errorf("invalid port value, got: (%d) expected: in range 1024-65535", port)
}
if len(rconpass) < 8 {
return nil, fmt.Errorf(
"invalid rcon password, got: (%s) expected: at least 8 characters",
rconpass,
)
}
client, closer, err := connectRcon(host, port, rconpass)
if err != nil {
return nil, fmt.Errorf("failed to connect to rcon: %w", err)
}
if interactive {
fmt.Printf("Enter 'Q' to exit.\n>> ")
err := interactiveMode(client, os.Stdin)
if err != nil {
return closer, fmt.Errorf("interactive mode error: %w", err)
}
return closer, nil
}
commands := flag.Args()
if len(commands) == 0 {
log.Debug("no commands provided, defaulting to 'status'")
commands = append(commands, "status")
}
runCommands(client, commands)
return closer, nil
}
// versionFromBuild retrieves the version information from the build metadata.
func versionFromBuild() string {
if version == "" {
info, ok := debug.ReadBuildInfo()
if !ok {
return "(unable to read build info)"
}
version = strings.Split(info.Main.Version, "-")[0]
}
return version
}
func connectRcon(host string, port int, password string) (*q3rcon.Rcon, func(), error) {
client, err := q3rcon.New(host, port, password, q3rcon.WithTimeouts(map[string]time.Duration{
"map": time.Second,
"map_rotate": time.Second,
"map_restart": time.Second,
}))
if err != nil {
return nil, nil, err
}
closer := func() {
if err := client.Close(); err != nil {
log.Error(err)
}
}
return client, closer, nil
}
// runCommands runs the commands given in the flag.Args slice.
// If no commands are given, it defaults to running the "status" command.
func runCommands(client *q3rcon.Rcon, commands []string) {
for _, cmd := range commands {
resp, err := client.Send(cmd)
if err != nil {
log.Error(err)
continue
}
fmt.Print(resp)
}
}
// interactiveMode continuously reads from input until a quit signal is given.
func interactiveMode(client *q3rcon.Rcon, input io.Reader) error {
scanner := bufio.NewScanner(input)
for scanner.Scan() {
cmd := scanner.Text()
if strings.EqualFold(cmd, "Q") {
return nil
}
resp, err := client.Send(cmd)
if err != nil {
log.Error(err)
continue
}
fmt.Printf("%s>> ", resp)
}
if scanner.Err() != nil {
return scanner.Err()
}
return nil
}