diff --git a/cmd/servermon/main.go b/cmd/servermon/main.go new file mode 100644 index 00000000..f46e8330 --- /dev/null +++ b/cmd/servermon/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "context" + "flag" + "log/slog" + "os" + "os/signal" + "syscall" + "time" + + "github.com/peterbourgon/ff/v3" + "github.com/spacechunks/explorer/servermon" +) + +func main() { + var ( + logger = slog.New(slog.NewJSONHandler(os.Stdout, nil)) + ctx, cancel = context.WithCancel(context.Background()) + fs = flag.NewFlagSet("servermon", flag.ContinueOnError) + playerCountCheckInterval = fs.Duration("player-count-check-interval", 2*time.Minute, "in what interval the player count of the server will be checked") //nolint:lll + mgmtEndpoint = fs.String("mc-server-management-api-endpoint", "http://localhost:26656", "the endpoint at which the minecraft server management api is available") //nolint:lll + mgmtAPIToken = fs.String("mc-server-management-api-token", "", "token to use for the minecraft server management api") //nolint:lll + ) + + if err := ff.Parse(fs, os.Args[1:], + ff.WithEnvVarPrefix("SERVERMON"), + ff.WithConfigFileFlag("config"), + ff.WithConfigFileParser(ff.JSONParser), + ); err != nil { + logger.ErrorContext(ctx, "failed to parse config", "err", err) + os.Exit(1) + } + + var ( + cfg = servermon.Config{ + PlayerCountCheckInterval: *playerCountCheckInterval, + MCServerManagementAPIEndpoint: *mgmtEndpoint, + MCServerManagementAPIToken: *mgmtAPIToken, + } + mon = servermon.New(logger, cfg) + ) + + go func() { + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) + s := <-c + logger.Info("received shutdown signal", "signal", s.String()) + cancel() + }() + + if err := mon.Run(ctx); err != nil { + logger.ErrorContext(ctx, "error running servermon", "err", err) + os.Exit(1) + } +} diff --git a/servermon/monitor.go b/servermon/monitor.go new file mode 100644 index 00000000..2b42eb2d --- /dev/null +++ b/servermon/monitor.go @@ -0,0 +1,84 @@ +package servermon + +import ( + "context" + "fmt" + "log/slog" + "time" + + gorilla "github.com/gorilla/websocket" + "github.com/sourcegraph/jsonrpc2" + "github.com/sourcegraph/jsonrpc2/websocket" +) + +type Config struct { + PlayerCountCheckInterval time.Duration + MCServerManagementAPIEndpoint string + MCServerManagementAPIToken string +} + +type Monitor struct { + logger *slog.Logger + conf Config +} + +func New(logger *slog.Logger, cfg Config) Monitor { + return Monitor{ + logger: logger, + conf: cfg, + } +} + +type player struct { + ID string `json:"id"` + Name string `json:"name"` +} + +func (m Monitor) Run(ctx context.Context) error { + wsConn, _, err := gorilla.DefaultDialer.Dial(m.conf.MCServerManagementAPIEndpoint, map[string][]string{ + "Authorization": {"Bearer " + m.conf.MCServerManagementAPIToken}, + }) + if err != nil { + return fmt.Errorf("dial: %w", err) + } + + rpcConn := jsonrpc2.NewConn(ctx, websocket.NewObjectStream(wsConn), &m) + defer rpcConn.Close() + + listTicker := time.NewTicker(1 * time.Second) + defer listTicker.Stop() + + checkTicker := time.NewTicker(m.conf.PlayerCountCheckInterval) + defer checkTicker.Stop() + + joined := false + + for { + select { + case <-listTicker.C: + players := make([]player, 0) + + if err := rpcConn.Call(ctx, "minecraft:players", nil, &players); err != nil { + return fmt.Errorf("call players: %w", err) + } + + if len(players) > 0 { + joined = true + } + case <-checkTicker.C: + if joined { + m.logger.Info("JOINED") + joined = false + } else { + m.logger.Info("KILL") + } + + case <-ctx.Done(): + return nil + } + } +} + +// Handle is present, because jsonrpc2 crashes if we pass a nil handler to jsonrpc2.NewConn +// and receive a message afterward. +func (m Monitor) Handle(_ context.Context, _ *jsonrpc2.Conn, _ *jsonrpc2.Request) {}