go-bbs/main.go

419 lines
11 KiB
Go

package main
import (
"bytes"
"context"
"errors"
"flag"
"io"
"log"
"net"
"os"
"os/exec"
"strconv"
"strings"
"sync"
"text/template"
"time"
"github.com/creack/pty"
"github.com/xtaci/gaio"
"golang.org/x/term"
"versestudios.com/go-telnet-asyncio-test/openconn"
)
type InChanKey string
type OutChanKey string
func ioHandler(w *gaio.Watcher, WatcherControl *chan string) {
for {
select {
case msg := <-*WatcherControl:
switch msg {
case "":
log.Println("empty WatcherControl command")
case "stop":
log.Println("stopping ioHandler via WatcherControl stop command")
return
default:
log.Println("unknown WatcherControl command:", msg)
}
default:
// loop wait for any IO events
results, err := w.WaitIO()
if err != nil {
log.Println(err)
return
}
IOLoop:
for _, res := range results {
if res.Context != nil && nil != res.Context.(context.Context) {
if inChan, _, ok := openconn.FromContext(res.Context.(context.Context)); ok {
switch res.Operation {
case gaio.OpRead: // read completion event
inChan <- res
// queue next read
w.Read(res.Context, res.Conn, nil)
default: // anything else (meaning write completions)
continue IOLoop
}
} else {
log.Printf("error getting inChan and outChan from context: %v\n", res.Context)
}
} else {
log.Printf("nil context! res: %v\n", res)
}
}
}
time.Sleep(time.Millisecond)
}
}
func main() {
port := flag.Int("port", 3333, "Port to accept connections on.")
host := flag.String("host", "127.0.0.1", "Host or IP to bind to")
flag.Parse()
w, err := gaio.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer w.Close()
l, err := net.Listen("tcp", *host+":"+strconv.Itoa(*port))
if err != nil {
log.Panicln(err)
}
log.Println("Listening to connections at '"+*host+"' on port", strconv.Itoa(*port))
log.Println("(If you want to run this on a different address or port, the command line flags --host and --port are available to you.)")
defer l.Close()
for {
conn, err := l.Accept()
if err != nil {
log.Panicln(err)
}
log.Println("new client: ", conn.RemoteAddr())
// chan to terminate ioHandler when neccessary
WatcherControl := make(chan string)
// get new context with inChan and outChan for this client connection
ctx := openconn.NewContext(context.Background())
log.Printf("new context for client %v: %v\n", conn.RemoteAddr(), ctx)
// submit the first async write IO request
welcomeScreen, err := screenHandler("welcome")
if err != nil {
log.Printf("err loading initial welcome screen: %v\n", err)
}
err = w.Write(ctx, conn, welcomeScreen)
if err != nil {
log.Printf("err sending welcomeHandler: %v\n", err)
return
}
// now that a prompt is (or will be) displayed, go ahead and listen for input
err = w.Read(ctx, conn, nil)
if err != nil {
log.Printf("err queueing w.Read: %v\n", err)
return
}
time.Sleep(time.Millisecond)
// menu handler for this connection
go menuHandler(ctx, conn, w)
// io handler for this connection
go ioHandler(w, &WatcherControl)
log.Println("main thread looping")
}
}
func menuHandler(ctx context.Context, conn net.Conn, w *gaio.Watcher) {
var wg sync.WaitGroup
MenuControl := make(chan string)
var inChan, outChan chan gaio.OpResult
var ok bool
if inChan, outChan, ok = openconn.FromContext(ctx); !ok {
log.Println("Could not get inChan/outChan from context!", ok, ctx)
}
wg.Add(1)
go func(inChan, outChan chan gaio.OpResult, MenuControl chan string) {
defer wg.Done()
log.Println("starting menu loop")
paused := false
ControlLoop:
for {
select {
case msg := <-MenuControl:
log.Println("msg on MenuControl:", msg)
switch string(msg) {
case "stop":
log.Println("closing menu loop")
paused = true
break ControlLoop
case "pause":
log.Println("pausing menu loop via menucontrol")
paused = true
case "unpause":
log.Println("unpausing menu loop via menucontrol")
paused = false
default:
log.Println("menu received unknown command: ", msg)
}
default:
if !paused {
select {
case res := <-inChan:
if res.Error != nil {
log.Println("error on inChan: ", res.Error)
}
if res.Operation == gaio.OpRead && res.Size > 0 && res.Conn == conn {
log.Printf("received on inChan: conn: %v, buffer size: %v\n", res.Conn.RemoteAddr(), res.Size)
log.Println("menu receive: ", strings.TrimSpace(string(res.Buffer[:res.Size-2])))
switch string(strings.TrimSpace(string(res.Buffer[:res.Size-2]))) {
case "welcome":
welcomeStr, err := screenHandler("welcome")
if err != nil {
log.Printf("err loading welcome screen: %v\n", err)
}
if err := w.Write(ctx, conn, welcomeStr); err != nil {
log.Printf("error sending screenHandler from cmd `welcome`: %v", err)
}
case "help":
helpStr, err := screenHandler("help")
if err != nil {
log.Printf("err loading help screen: %v\n", err)
}
if err := w.Write(ctx, conn, helpStr); err != nil {
log.Printf("error sending screenHandler from cmd `help`: %v", err)
}
case "operator":
opStr, err := screenHandler("operator")
if err != nil {
log.Printf("err loading operator screen: %v\n", err)
}
var lineNum int
var line string
baudrate := 40 // 300 baud is ~ 30 characters per second; I'll be nice and bump it to 400
for lineNum, line = range strings.Split(string(opStr), "\n") {
// changed my mind - I actually like it at a nice steady 400 baud, tyvm
// if baudrate < 120 && i > 1 { // 1200 baud is still very readable in realtime without being too fast
// baudrate += 1
//}
if lineNum < 10 { // spit the banner text out quickly
if err := w.Write(ctx, conn, []byte(line+"\r\n")); err != nil {
log.Printf("error sending rune from cmd `operator`: %v", err)
}
} else {
ms := float64(1) / float64(baudrate)
for _, c := range line {
if err := w.Write(ctx, conn, []byte{byte(c)}); err != nil {
log.Printf("error sending rune from cmd `operator`: %v", err)
}
time.Sleep(time.Duration(ms*1000) * time.Millisecond)
}
if err := w.Write(ctx, conn, []byte("\r\n")); err != nil {
log.Printf("error sending rune from cmd `operator`: %v", err)
}
}
}
// add prompt at end
if err := w.Write(ctx, conn, []byte("\r\n> ")); err != nil {
log.Printf("error sending rune from cmd `operator`: %v", err)
}
case "adventure":
// start the door and wait
var wg sync.WaitGroup
log.Println("starting door handler...")
wg.Add(1)
go doorHandler(ctx, conn, w, &wg)
log.Println("menu handler waiting for wg to return from door")
wg.Wait()
log.Println("returning from door")
// welcome back and prompt
err := w.Write(ctx, conn, []byte("Welcome back from your adventure!\n\n> "))
if err != nil {
log.Printf("error writing to connection: %v", err)
}
case "exit":
exitStr, err := screenHandler("exit")
if err != nil {
log.Printf("err loading exit screen: %v\n", err)
}
if err := w.Write(ctx, conn, exitStr); err != nil {
log.Printf("error sending exitHandler from cmd `exit`: %v", err)
}
// wait a second before really closing the connection
time.Sleep(time.Second)
w.Free(conn)
default:
err := w.Write(ctx, conn, []byte("huh?\n\n> "))
if err != nil {
log.Printf("error writing to connection: %v", err)
}
}
} else {
// noop
log.Printf("received on inChan: conn: %v, buffer: %v\n", res.Conn.RemoteAddr(), string(res.Buffer))
}
default:
// noop - wait a lil bit
time.Sleep(time.Millisecond)
}
if conn == nil {
log.Println("conn is nil!")
}
}
}
time.Sleep(time.Millisecond)
}
log.Println("menu controlloop closed!")
}(inChan, outChan, MenuControl)
log.Println("menu waiting on waitgroup")
wg.Wait()
log.Println("terminating menucontrol for client")
}
func screenHandler(screenName string) (screen []byte, err error) {
var screenBuf *bytes.Buffer
if tmpl, err := template.ParseFiles("screens/" + screenName + ".ans"); err != nil {
log.Printf("err loading welcome template: %v\n", err)
} else {
screenBuf = new(bytes.Buffer)
tmpl.Execute(screenBuf, nil)
}
return screenBuf.Bytes(), err
}
func doorHandler(ctx context.Context, c net.Conn, w *gaio.Watcher, menuwg *sync.WaitGroup) error {
defer menuwg.Done()
var wg sync.WaitGroup
var inChan, _ chan gaio.OpResult
var ok bool
if inChan, _, ok = openconn.FromContext(ctx); !ok {
log.Println("Could not get inChan/outChan from context!", ok, ctx)
}
cmd := exec.Command("/usr/games/adventure")
ptmx, err := pty.Start(cmd)
if err != nil {
return err
}
// Make sure to close the pty at the end.
defer func() { _ = ptmx.Close() }() // Best effort.
// Can I do this?
if term.IsTerminal(int(ptmx.Fd())) {
log.Println("Making cmd terminal raw")
oldState, _ := term.MakeRaw(int(ptmx.Fd()))
defer term.Restore(int(ptmx.Fd()), oldState)
}
Terminator := make(chan bool)
wg.Add(1)
go func() {
defer wg.Done()
waitingForInput := false
IOLoop:
for {
select {
case result, ok := <-inChan:
var l int
log.Println("i")
if ok {
if result.Operation == gaio.OpRead && result.Size > 0 {
l, err = ptmx.Write(result.Buffer[:result.Size])
if err != nil {
if err == io.EOF {
log.Println("EOF on cmd.write!!!")
break IOLoop
}
}
if l > 0 {
log.Printf("wrote %d bytes to cmd stdin: %v\n", l, string(result.Buffer[:result.Size]))
waitingForInput = false
}
}
} else {
log.Println("yikes, problem getting a result from inChan in doorHandler!")
}
case <-Terminator:
log.Println("Terminator'd!!!")
break IOLoop
default:
if !waitingForInput {
log.Println("o")
ptmx.SetReadDeadline(time.Now().Add(1 * time.Second))
var l int
outBuf := make([]byte, 4096)
if l, err = ptmx.Read(outBuf); err != nil {
if err == io.EOF {
log.Println("EOF on cmd stdout - outta here")
break IOLoop
} else if errors.Is(err, os.ErrDeadlineExceeded) {
// output deadline exceeded - get some input instead
w.Read(ctx, c, nil)
waitingForInput = true
} else if errors.Is(err, os.ErrClosed) {
// program exited - get outta here
break IOLoop
} else {
log.Printf("err reading from cmd term: %v\n", err)
break IOLoop
}
}
if l > 0 {
w.Write(ctx, c, outBuf[:l])
log.Printf("wrote %d bytes to watcher: %v\n", l, string(outBuf[:l]))
if strings.Contains(string(outBuf[:l]), ">") {
// a prompt appeared!
waitingForInput = true
}
} else {
// zero output? let's get some input?
// waitingForInput = true
w.Read(ctx, c, nil)
}
}
}
time.Sleep(time.Millisecond)
}
}()
wg.Wait()
log.Println("waiting for cmd")
exitErr := cmd.Wait()
if exitErr != nil {
log.Printf("cmd exited with err: %v\n", exitErr)
}
log.Println("leaving door handler")
return nil
}