6 Commits

Author SHA1 Message Date
3be7ddb36b ensure we return closer from run() 2026-02-16 00:11:50 +00:00
c22b07808f upd imgs 2026-02-15 19:37:41 +00:00
c015770c2c add v0.4.0 to CHANGELOG 2026-02-15 18:35:52 +00:00
cd15e89837 add macos target to goreleaser config 2026-02-15 18:31:25 +00:00
c3e8013c4f make the example implementation a little more useful with a timeouts map. 2026-02-15 18:29:14 +00:00
89dd2d2eb1 replace exitOnError with deferred exit function.
this ensures the closer() cleanup function is always called
2026-02-15 18:24:59 +00:00
6 changed files with 93 additions and 37 deletions

View File

@@ -22,6 +22,7 @@ builds:
goos:
- linux
- windows
- darwin
goarch:
- amd64

View File

@@ -11,6 +11,18 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
- [x]
# [0.4.0] - 2026-02-15
### Added
- macos build to releases
### Changed
- exitOnError() removed in favour of a [deferred exit function](https://github.com/onyx-and-iris/q3rcon/blob/cd15e8983726177d6edd985a8bf3d7f4e0d7f346/cmd/q3rcon/main.go#L21), this ensures the closer() cleanup function is always called.
- the included CLI now uses a [timeouts map](https://github.com/onyx-and-iris/q3rcon/blob/cd15e8983726177d6edd985a8bf3d7f4e0d7f346/cmd/q3rcon/main.go#L109).
- even though this is only an example implementation it should still be basically usable.
# [0.3.0] - 2025-04-05
### Changed

View File

@@ -7,18 +7,35 @@ import (
"io"
"os"
"strings"
"github.com/onyx-and-iris/q3rcon"
"time"
log "github.com/sirupsen/logrus"
"github.com/onyx-and-iris/q3rcon"
)
func exitOnError(err error) {
_, _ = fmt.Fprintf(os.Stderr, "Error: %s\n", err)
os.Exit(1)
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
}
}
func main() {
// 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
@@ -30,9 +47,19 @@ func main() {
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.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.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")
@@ -44,53 +71,69 @@ func main() {
level, err := log.ParseLevel(loglevel)
if err != nil {
exitOnError(fmt.Errorf("invalid log level: %s", loglevel))
return nil, fmt.Errorf("invalid log level: %s", loglevel)
}
log.SetLevel(level)
if port < 1024 || port > 65535 {
exitOnError(fmt.Errorf("invalid port value, got: (%d) expected: in range 1024-65535", port))
return nil, fmt.Errorf("invalid port value, got: (%d) expected: in range 1024-65535", port)
}
if len(rconpass) < 8 {
exitOnError(fmt.Errorf("invalid rcon password, got: (%s) expected: at least 8 characters", rconpass))
return nil, fmt.Errorf(
"invalid rcon password, got: (%s) expected: at least 8 characters",
rconpass,
)
}
rcon, err := connectRcon(host, port, rconpass)
client, closer, err := connectRcon(host, port, rconpass)
if err != nil {
exitOnError(err)
}
defer rcon.Close()
if !interactive {
runCommands(flag.Args(), rcon)
return
return nil, fmt.Errorf("failed to connect to rcon: %w", err)
}
fmt.Printf("Enter 'Q' to exit.\n>> ")
err = interactiveMode(rcon, os.Stdin)
if err != nil {
exitOnError(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
}
func connectRcon(host string, port int, password string) (*q3rcon.Rcon, error) {
rcon, err := q3rcon.New(host, port, password)
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, err
return nil, nil, err
}
return rcon, nil
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(commands []string, rcon *q3rcon.Rcon) {
if len(commands) == 0 {
commands = append(commands, "status")
}
func runCommands(client *q3rcon.Rcon, commands []string) {
for _, cmd := range commands {
resp, err := rcon.Send(cmd)
resp, err := client.Send(cmd)
if err != nil {
log.Error(err)
continue
@@ -100,7 +143,7 @@ func runCommands(commands []string, rcon *q3rcon.Rcon) {
}
// interactiveMode continuously reads from input until a quit signal is given.
func interactiveMode(rcon *q3rcon.Rcon, input io.Reader) error {
func interactiveMode(client *q3rcon.Rcon, input io.Reader) error {
scanner := bufio.NewScanner(input)
for scanner.Scan() {
cmd := scanner.Text()
@@ -108,7 +151,7 @@ func interactiveMode(rcon *q3rcon.Rcon, input io.Reader) error {
return nil
}
resp, err := rcon.Send(cmd)
resp, err := client.Send(cmd)
if err != nil {
log.Error(err)
continue

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -140,6 +140,6 @@ func (r Rcon) listen(timeout time.Duration, respChan chan<- string, errChan chan
}
}
func (r Rcon) Close() {
r.conn.Close()
func (r Rcon) Close() error {
return r.conn.Close()
}