diff --git a/go.mod b/go.mod index 53c767c..a130012 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,8 @@ module versestudios.com/go-telnet-asyncio-test go 1.16 -require github.com/xtaci/gaio v1.2.9 +require ( + github.com/creack/pty v1.1.11 // indirect + github.com/xtaci/gaio v1.2.9 + golang.org/x/sys v0.0.0-20210414055047-fe65e336abe0 // indirect +) diff --git a/go.sum b/go.sum index c7b5ef1..61c4902 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,6 @@ +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/xtaci/gaio v1.2.9 h1:EuVc7Q2JDzIY2mk5mjtq4K5BgTuO+kj5LXzCwjOK+mo= github.com/xtaci/gaio v1.2.9/go.mod h1:rJMerwiLCLnKa14YTM/sRggTPrnBZrlCg9U3DnV5VBE= +golang.org/x/sys v0.0.0-20210414055047-fe65e336abe0 h1:g9s1Ppvvun/fI+BptTMj909BBIcGrzQ32k9FNlcevOE= +golang.org/x/sys v0.0.0-20210414055047-fe65e336abe0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/main.go b/main.go index a1159e0..ec7bc30 100644 --- a/main.go +++ b/main.go @@ -1,17 +1,19 @@ package main import ( - "bufio" "context" "flag" + "io" "log" "net" + "os" "os/exec" "strconv" "strings" "sync" "time" + "github.com/creack/pty" "github.com/xtaci/gaio" "versestudios.com/go-telnet-asyncio-test/openconn" ) @@ -305,7 +307,9 @@ func exitHandler() []byte { func doorHandler(ctx context.Context, c net.Conn, w *gaio.Watcher, menuwg *sync.WaitGroup) error { defer menuwg.Done() + var wg sync.WaitGroup const bannerText = "\r\nCOLOSSAL CAVE\r\n\r\n" + err := w.Write(ctx, c, []byte(bannerText)) if err != nil { log.Printf("error writing to connection: %v\n", err) @@ -320,52 +324,154 @@ func doorHandler(ctx context.Context, c net.Conn, w *gaio.Watcher, menuwg *sync. cmd := exec.Command("/usr/games/adventure") - stdout, _ := cmd.StdoutPipe() - stdin, _ := cmd.StdinPipe() + 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. - cmd.Start() - - var wg sync.WaitGroup - wg.Add(1) + /* + stdout, _ := cmd.StdoutPipe() + stdin, _ := cmd.StdinPipe() + */ + //CmdOutChan := make(chan []byte) Terminator := make(chan bool) - go func(Terminator *chan bool, wg *sync.WaitGroup) { - log.Println("starting stdin reader goroutine") + + // cmd.Start() + wg.Add(1) + go func() { defer wg.Done() - Check: + waitingForInput := false + IOLoop: for { select { - case <-*Terminator: - break Check case result, ok := <-inChan: + var l int + log.Println("i") if ok { - if result.Operation == gaio.OpRead { + if result.Operation == gaio.OpRead && result.Size > 0 { // we asked for a read - send it to stdin - stdin.Write(result.Buffer[:result.Size]) + l, err = ptmx.Write(result.Buffer[:result.Size]) + if err != nil { + if err == io.EOF { + // terminal closed? wtfever + 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") + var l int + outBuf := make([]byte, 2048) + ptmx.SetReadDeadline(time.Now().Add(time.Millisecond)) + if l, err = ptmx.Read(outBuf); err != nil { + if err == io.EOF { + log.Println("EOF on cmd stdout - outta here") + break IOLoop + } else if err == os.ErrDeadlineExceeded { + // do nothing but be quiet about it ffs + } else if strings.TrimSpace(err.Error()) == "input/output error" { + // program exited methinks - 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 outBuf[l-1] == byte('>') { + // looks like a prompt to me, I hope! + // let's add a space just for fun + outBuf[0] = byte(' ') + w.Write(ctx, c, outBuf[0:1]) + // and now block until we get a read from the connection dammit + waitingForInput = true + } + } else { + // zero output? let's get some input? + // log.Println("0-read from cmd output - queueing watcher read") + w.Read(ctx, c, nil) + } + } + } + time.Sleep(time.Millisecond) + } + }() + + /* + // goroutine to read cmd stdout in a loop and write it to CmdOutChan. + // on io.EOF in the scanner, will send Terminator to kill the IOLoop + // that follows the goroutine + go func(Terminator chan bool, wg *sync.WaitGroup, stdout *io.ReadCloser) { + defer wg.Done() + log.Println("starting stdout reader goroutine") + scanner := bufio.NewScanner(*stdout) + scanner.Split(bufio.ScanLines) + + for scanner.Scan() { + m := scanner.Bytes() + m = append(m, '\n') + CmdOutChan <- m time.Sleep(time.Millisecond) } - } - log.Println("exiting stdin reader goroutine") - }(&Terminator, &wg) + log.Println("exiting stdout reader goroutine") + Terminator <- true + }(Terminator, &wg, &stdout) - log.Println("starting stdout reader goroutine") - scanner := bufio.NewScanner(stdout) - scanner.Split(bufio.ScanLines) + // goroutine to handle actual IO calls + wg.Add(1) + go func(Terminator chan bool, wg *sync.WaitGroup, stdout *io.ReadCloser) { + defer wg.Done() + log.Println("starting IOLoop") + IOLoop: + for { + select { + // if there's data coming out from cmd, write it to the watcher + case cmdOutput := <-CmdOutChan: + log.Printf("o") + err := w.Write(ctx, c, cmdOutput) + if err != nil { + log.Printf("err writing to watcher from cmd: %v\n", err) + } else { + log.Printf("send %d bytes from CmdOutChan from cmd stdout to watcher: %v\n", len(cmdOutput), string(cmdOutput)) + } - for scanner.Scan() { - m := scanner.Bytes() - m = append(m, '\n') - w.Write(ctx, c, m) - log.Printf("wrote %d bytes to watcher from door stdout: %v\n", len(m)+1, string(m)) - } - log.Println("exiting stdout reader goroutine") - Terminator <- true - - log.Println("waiting for wg") + // if there's data coming in from remote conn, send it to stdin for cmd + case result, ok := <-inChan: + log.Printf("i") + if ok { + if result.Operation == gaio.OpRead && result.Size > 0 { + // we asked for a read - send it to stdin + stdin.Write(result.Buffer[:result.Size]) + } + } else { + log.Println("yikes, problem getting a result from inChan in doorHandler!") + } + // if the Terminator shows up we're dead + case <-Terminator: + break IOLoop + } + time.Sleep(time.Millisecond) + } + log.Println("IOLoop exited") + }(Terminator, &wg, &stdout) + */ wg.Wait() log.Println("waiting for cmd") exitErr := cmd.Wait()