moth

Monarch Of The Hill game server
git clone https://git.woozle.org/neale/moth.git

moth / cmd / mothd
Neale Pickett  ·  2023-04-11

providercommand.go

  1// Provides a Puzzle interface that runs a command for each request
  2package main
  3
  4import (
  5	"bytes"
  6	"context"
  7	"encoding/json"
  8	"fmt"
  9	"io"
 10	"log"
 11	"os"
 12	"os/exec"
 13	"sort"
 14	"strconv"
 15	"strings"
 16	"time"
 17
 18	"github.com/dirtbags/moth/v4/pkg/transpile"
 19)
 20
 21// ProviderCommand specifies a command to run for the puzzle API
 22type ProviderCommand struct {
 23	Path string
 24	Args []string
 25}
 26
 27// Inventory runs with "action=inventory", and parses the output into a category list.
 28func (pc ProviderCommand) Inventory() (inv []Category) {
 29	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
 30	defer cancel()
 31
 32	cmd := exec.CommandContext(ctx, pc.Path, pc.Args...)
 33	cmd.Env = os.Environ()
 34	cmd.Env = append(cmd.Env, "ACTION=inventory")
 35
 36	stdout, err := cmd.Output()
 37	if err != nil {
 38		log.Print(err)
 39		return
 40	}
 41
 42	for _, line := range strings.Split(string(stdout), "\n") {
 43		line = strings.TrimSpace(line)
 44		if line == "" {
 45			continue
 46		}
 47		parts := strings.Split(line, " ")
 48		if len(parts) < 2 {
 49			log.Println("Skipping misformatted line:", line)
 50			continue
 51		}
 52		name := parts[0]
 53		puzzles := make([]int, 0, 10)
 54		for _, pointsString := range parts[1:] {
 55			points, err := strconv.Atoi(pointsString)
 56			if err != nil {
 57				log.Println(err)
 58				continue
 59			}
 60			puzzles = append(puzzles, points)
 61		}
 62		sort.Ints(puzzles)
 63		inv = append(inv, Category{name, puzzles})
 64	}
 65	return
 66}
 67
 68// NullReadSeekCloser wraps a no-op Close method around an io.ReadSeeker.
 69type NullReadSeekCloser struct {
 70	io.ReadSeeker
 71}
 72
 73// Close does nothing.
 74func (f NullReadSeekCloser) Close() error {
 75	return nil
 76}
 77
 78// Open passes its arguments to the command with "action=open".
 79func (pc ProviderCommand) Open(cat string, points int, path string) (ReadSeekCloser, time.Time, error) {
 80	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
 81	defer cancel()
 82
 83	cmd := exec.CommandContext(ctx, pc.Path, pc.Args...)
 84	cmd.Env = os.Environ()
 85	cmd.Env = append(cmd.Env, "ACTION=open")
 86	cmd.Env = append(cmd.Env, "CAT="+cat)
 87	cmd.Env = append(cmd.Env, "POINTS="+strconv.Itoa(points))
 88	cmd.Env = append(cmd.Env, "FILENAME="+path)
 89
 90	stdoutBytes, err := cmd.Output()
 91	stdout := NullReadSeekCloser{bytes.NewReader(stdoutBytes)}
 92	now := time.Now()
 93	return stdout, now, err
 94}
 95
 96// CheckAnswer passes its arguments to the command with "action=answer".
 97// If the command exits successfully and sends "correct" to stdout,
 98// nil is returned.
 99func (pc ProviderCommand) CheckAnswer(cat string, points int, answer string) (bool, error) {
100	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
101	defer cancel()
102
103	cmd := exec.CommandContext(ctx, pc.Path, pc.Args...)
104	cmd.Env = os.Environ()
105	cmd.Env = append(cmd.Env, "ACTION=answer")
106	cmd.Env = append(cmd.Env, "CAT="+cat)
107	cmd.Env = append(cmd.Env, "POINTS="+strconv.Itoa(points))
108	cmd.Env = append(cmd.Env, "ANSWER="+answer)
109
110	stdout, err := cmd.Output()
111	if ee, ok := err.(*exec.ExitError); ok {
112		log.Printf("WARNING: %s: %s", pc.Path, string(ee.Stderr))
113		return false, err
114	} else if err != nil {
115		return false, err
116	}
117
118	ans := transpile.AnswerResponse{}
119	if err := json.Unmarshal(stdout, &ans); err != nil {
120		return false, err
121	}
122
123	return ans.Correct, nil
124}
125
126// Mothball just returns an error
127func (pc ProviderCommand) Mothball(cat string) (*bytes.Reader, error) {
128	return nil, fmt.Errorf("can't package a command-generated category")
129}
130
131// Maintain does nothing: a command puzzle ProviderCommand has no housekeeping
132func (pc ProviderCommand) Maintain(updateInterval time.Duration) {
133}