diff --git a/client.go b/client.go index 1e91e05..03ea14a 100644 --- a/client.go +++ b/client.go @@ -21,3 +21,14 @@ func NewAll(timeout time.Duration) *Client { Source: source.New(source.Options{Timeout: timeout}), } } + +func (c *Client) SetSourceDebug(debug bool) { + if c == nil { + return + } + timeout := 2 * time.Second + if c.Source != nil { + timeout = c.Source.Timeout() + } + c.Source = source.New(source.Options{Timeout: timeout, Debug: debug}) +} diff --git a/cmd/gamestatus/main.go b/cmd/gamestatus/main.go index cfdb478..8f080bf 100644 --- a/cmd/gamestatus/main.go +++ b/cmd/gamestatus/main.go @@ -29,6 +29,7 @@ func main() { } client := gamestatus.NewAll(cfg.timeout) + client.SetSourceDebug(cfg.debug) info, err := connect(client, cfg.edition, host, port, cfg.useQuery) if err != nil { @@ -54,6 +55,7 @@ type cliConfig struct { useQuery bool pretty bool motdRaw bool + debug bool } func parseCLI(args []string) (cliConfig, string, error) { @@ -70,6 +72,7 @@ func parseCLI(args []string) (cliConfig, string, error) { useQuery := fs.Bool("query", false, "use full query when supported") pretty := fs.Bool("pretty", false, "use compact human-friendly output") motdRaw := fs.Bool("motd-raw", false, "print raw MOTD without cleanup") + debug := fs.Bool("debug", false, "print protocol-specific diagnostics") if len(args) > 0 { switch strings.ToLower(strings.TrimSpace(args[0])) { @@ -96,6 +99,7 @@ func parseCLI(args []string) (cliConfig, string, error) { cfg.useQuery = *useQuery cfg.pretty = *pretty cfg.motdRaw = *motdRaw + cfg.debug = *debug if fs.NArg() != 1 { return cfg, "", nil @@ -126,6 +130,8 @@ func printUsage() { fmt.Fprintln(os.Stderr, " use compact human-friendly output") fmt.Fprintln(os.Stderr, " -motd-raw") fmt.Fprintln(os.Stderr, " print raw MOTD without cleanup") + fmt.Fprintln(os.Stderr, " -debug") + fmt.Fprintln(os.Stderr, " print protocol-specific diagnostics") } func connect(client *gamestatus.Client, edition, host string, port int, useQuery bool) (*gamestatus.ServerInfo, error) { diff --git a/source/source.go b/source/source.go index aa84637..75760b2 100644 --- a/source/source.go +++ b/source/source.go @@ -2,6 +2,8 @@ package source import ( "encoding/json" + "errors" + "fmt" "net" "strconv" "strings" @@ -14,6 +16,7 @@ import ( type Options struct { Timeout time.Duration + Debug bool } type Client struct { @@ -27,6 +30,13 @@ func New(opt Options) *Client { return &Client{opt: opt} } +func (c *Client) Timeout() time.Duration { + if c == nil { + return 0 + } + return c.opt.Timeout +} + func (c *Client) Connect(address string, port int) (*info.ServerInfo, error) { return c.query(address, port, false) } @@ -93,14 +103,14 @@ func (c *Client) query(address string, port int, full bool) (*info.ServerInfo, e addr := normalizeAddress(address, port) client, err := a2s.NewClient(addr, a2s.TimeoutOption(c.opt.Timeout)) if err != nil { - return nil, err + return nil, wrapQueryError(addr, "connect", err, c.opt.Debug) } defer client.Close() start := time.Now() infoResp, err := client.QueryInfo() if err != nil { - return nil, err + return nil, wrapQueryError(addr, "info", err, c.opt.Debug) } out := &info.ServerInfo{ @@ -133,10 +143,14 @@ func (c *Client) query(address string, port int, full bool) (*info.ServerInfo, e } } out.Players = names + } else if err != nil && c.opt.Debug { + return nil, wrapQueryError(addr, "players", err, true) } rulesResp, err := client.QueryRules() if err == nil && rulesResp != nil { out.RawQuery = rulesResp.Rules + } else if err != nil && c.opt.Debug { + return nil, wrapQueryError(addr, "rules", err, true) } } @@ -145,6 +159,25 @@ func (c *Client) query(address string, port int, full bool) (*info.ServerInfo, e return out, nil } +func wrapQueryError(addr, stage string, err error, debug bool) error { + if err == nil { + return nil + } + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + msg := fmt.Sprintf("no A2S response from %s", addr) + if debug && strings.TrimSpace(stage) != "" { + msg += fmt.Sprintf(" during %s", stage) + } + msg += " (server may have query disabled, blocked, or be using a different query port)" + return errors.New(msg) + } + if debug && strings.TrimSpace(stage) != "" { + return fmt.Errorf("source query %s failed for %s: %w", stage, addr, err) + } + return err +} + func normalizeAddress(host string, port int) string { if port == 0 { port = a2s.DefaultPort