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
150 lines
3.2 KiB
Go
150 lines
3.2 KiB
Go
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))
|
|
}
|