A SimpleX Chat bot that answers arithmetic (closes #1)
check / check (push) Successful in 54s
check / check (push) Successful in 54s
Remove the template's HTTP service, database and fx wiring. Add exact arithmetic on go/parser and go/constant, a client that runs simplex-chat as a child process and drives its WebSocket API, and the bot, which keeps an auto-accepting address and replies to each message. The image adds the checksum-pinned simplex-chat v7.0.2 on Ubuntu 22.04. Model: opus-5-5
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
package simplex
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Binary is the chat client's executable, looked up on PATH.
|
||||
const Binary = "simplex-chat"
|
||||
|
||||
// stopGrace is how long the chat client gets to exit after SIGTERM
|
||||
// before it is killed.
|
||||
const stopGrace = 10 * time.Second
|
||||
|
||||
// maxLine bounds how much of one unterminated output line is held
|
||||
// before it is logged anyway.
|
||||
const maxLine = 64 << 10
|
||||
|
||||
// CLI is a running chat client process.
|
||||
type CLI struct {
|
||||
done chan struct{}
|
||||
err error // how the process ended; valid once done is closed
|
||||
}
|
||||
|
||||
// StartCLI launches the chat client with its database at dbPrefix,
|
||||
// serving its API on localhost at port. On the first start, with no
|
||||
// database yet, the client creates a bot profile named displayName;
|
||||
// every later start uses the profile already in the database.
|
||||
//
|
||||
// Cancelling ctx stops the client: SIGTERM, then SIGKILL if it has not
|
||||
// exited after stopGrace. Its output is logged line by line, so the
|
||||
// process emits one log format.
|
||||
func StartCLI(
|
||||
ctx context.Context, log *slog.Logger, dbPrefix, displayName string, port int,
|
||||
) (*CLI, error) {
|
||||
// No shell is involved: each argument reaches the client as one
|
||||
// argv entry, whatever it contains.
|
||||
//nolint:gosec // G204: the arguments are this program's own settings.
|
||||
cmd := exec.CommandContext(ctx, Binary,
|
||||
"--database", dbPrefix,
|
||||
"--chat-server-port", strconv.Itoa(port),
|
||||
"--create-bot-display-name", displayName,
|
||||
// Confirms the database migrations a newer client brings,
|
||||
// which it would otherwise wait to have confirmed on a
|
||||
// terminal that nobody is at.
|
||||
"--yes-migrate",
|
||||
)
|
||||
|
||||
cmd.Cancel = func() error {
|
||||
return cmd.Process.Signal(syscall.SIGTERM)
|
||||
}
|
||||
cmd.WaitDelay = stopGrace
|
||||
|
||||
out := &lineLogger{log: log}
|
||||
cmd.Stdout = out
|
||||
cmd.Stderr = out
|
||||
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("starting %s: %w", Binary, err)
|
||||
}
|
||||
|
||||
cli := &CLI{done: make(chan struct{})}
|
||||
|
||||
go func() {
|
||||
defer close(cli.done)
|
||||
|
||||
cli.err = cmd.Wait()
|
||||
|
||||
out.flush()
|
||||
}()
|
||||
|
||||
return cli, nil
|
||||
}
|
||||
|
||||
// Done is closed when the process has exited; Err then says how.
|
||||
func (c *CLI) Done() <-chan struct{} {
|
||||
return c.done
|
||||
}
|
||||
|
||||
// Err returns how the process ended. Call it only after Done is closed.
|
||||
func (c *CLI) Err() error {
|
||||
return c.err
|
||||
}
|
||||
|
||||
// lineLogger is the chat client's stdout and stderr.
|
||||
type lineLogger struct {
|
||||
log *slog.Logger
|
||||
|
||||
// mu covers the final flush after Wait, which can overlap a last
|
||||
// Write when Wait gave up on the output after stopGrace.
|
||||
mu sync.Mutex
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func (l *lineLogger) Write(p []byte) (int, error) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
l.buf = append(l.buf, p...)
|
||||
|
||||
for {
|
||||
line, rest, found := bytes.Cut(l.buf, []byte{'\n'})
|
||||
if !found {
|
||||
break
|
||||
}
|
||||
|
||||
l.emit(line)
|
||||
l.buf = rest
|
||||
}
|
||||
|
||||
if len(l.buf) > maxLine {
|
||||
l.flushLocked()
|
||||
}
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// flush logs what is left of an unterminated last line.
|
||||
func (l *lineLogger) flush() {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
l.flushLocked()
|
||||
}
|
||||
|
||||
func (l *lineLogger) flushLocked() {
|
||||
if len(l.buf) > 0 {
|
||||
l.emit(l.buf)
|
||||
}
|
||||
|
||||
l.buf = nil
|
||||
}
|
||||
|
||||
func (l *lineLogger) emit(line []byte) {
|
||||
line = bytes.TrimSpace(line)
|
||||
if len(line) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
l.log.Info("simplex-chat", "output", string(line))
|
||||
}
|
||||
Reference in New Issue
Block a user