169 lines
3.8 KiB
Go
169 lines
3.8 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/gorilla/websocket"
|
|
|
|
// Termbox seems to be one of the best projects in any language for handling terminal input.
|
|
// It's cross-platform and the maintainer is disciplined about supporting the baseline of escape
|
|
// codes that work across the majority of terminals.
|
|
"github.com/nsf/termbox-go"
|
|
)
|
|
|
|
var (
|
|
logfile string
|
|
websocketAddresss = flag.String("addr", ":3334", "Web socket service address")
|
|
upgrader = websocket.Upgrader{
|
|
CheckOrigin: func(r *http.Request) bool { return true },
|
|
ReadBufferSize: 1024,
|
|
WriteBufferSize: 1024,
|
|
}
|
|
stdinChannel = make(chan string)
|
|
)
|
|
|
|
func setupLogging() {
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
logfile = fmt.Sprintf(filepath.Join(dir, "debug.log"))
|
|
if _, err := os.Stat(logfile); err == nil {
|
|
os.Truncate(logfile, 0)
|
|
}
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func log(msg string) {
|
|
f, oErr := os.OpenFile(logfile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
|
|
if oErr != nil {
|
|
panic(oErr)
|
|
}
|
|
defer f.Close()
|
|
|
|
msg = msg + "\n"
|
|
if _, wErr := f.WriteString(msg); wErr != nil {
|
|
panic(wErr)
|
|
}
|
|
}
|
|
|
|
func initialise() {
|
|
setupTermbox()
|
|
setupLogging()
|
|
}
|
|
|
|
func setupTermbox() {
|
|
err := termbox.Init()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
termbox.SetInputMode(termbox.InputAlt | termbox.InputMouse)
|
|
}
|
|
|
|
func sendTtySize() {
|
|
x, y := termbox.Size()
|
|
stdinChannel <- fmt.Sprintf("/tty_size,%d,%d", x, y)
|
|
}
|
|
|
|
func readStdin() {
|
|
var event string
|
|
defer termbox.Close()
|
|
for {
|
|
switch ev := termbox.PollEvent(); ev.Type {
|
|
case termbox.EventKey:
|
|
if ev.Key == termbox.KeyCtrlQ {
|
|
termbox.Close()
|
|
os.Exit(0)
|
|
}
|
|
event = fmt.Sprintf("EventKey: k: %d, c: %c, mod: %s", ev.Key, ev.Ch, ev.Mod)
|
|
stdinChannel <- event
|
|
case termbox.EventResize:
|
|
sendTtySize()
|
|
case termbox.EventMouse:
|
|
case termbox.EventError:
|
|
panic(ev.Err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func socketReader(ws *websocket.Conn) {
|
|
defer ws.Close()
|
|
for {
|
|
_, message, err := ws.ReadMessage()
|
|
parts := strings.Split(string(message), ",")
|
|
command := parts[0]
|
|
if command == "/frame" {
|
|
termbox.SetCursor(0, 0)
|
|
os.Stdout.Write([]byte(strings.Join(parts[1:], ",")))
|
|
termbox.Flush()
|
|
} else {
|
|
log("WEBEXT: " + string(message))
|
|
}
|
|
if err != nil {
|
|
if websocket.IsCloseError(err, websocket.CloseGoingAway) {
|
|
log("Socket reader detected that the browser closed the websocket")
|
|
triggerSocketWriterClose()
|
|
return
|
|
}
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// When the socket reader attempts to read from a closed websocket it quickly and
|
|
// simply closes its associated Go routine. However the socket writer won't
|
|
// automatically notice until it actually needs to send something. So we force that
|
|
// by sending this NOOP text.
|
|
// TODO: There's a potential race condition because new connections share the same
|
|
// Go channel. So we need to setup a new channel for every connection.
|
|
func triggerSocketWriterClose() {
|
|
stdinChannel <- "BROWSH CLIENT FORCING CLOSE OF WEBSOCKET WRITER"
|
|
}
|
|
|
|
func socketWriter(ws *websocket.Conn) {
|
|
var message string
|
|
defer ws.Close()
|
|
for {
|
|
message = <-stdinChannel
|
|
log(fmt.Sprintf("TTY sending: %s", message))
|
|
if err := ws.WriteMessage(websocket.TextMessage, []byte(message)); err != nil {
|
|
if err == websocket.ErrCloseSent {
|
|
log("Socket writer detected that the browser closed the websocket")
|
|
return
|
|
}
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func socketServer(w http.ResponseWriter, r *http.Request) {
|
|
log("Incoming web request from browser")
|
|
ws, err := upgrader.Upgrade(w, r, nil)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
go socketWriter(ws)
|
|
go socketReader(ws)
|
|
|
|
sendTtySize()
|
|
}
|
|
|
|
func main() {
|
|
initialise()
|
|
log("Starting Browsh CLI client")
|
|
go readStdin()
|
|
http.HandleFunc("/", socketServer)
|
|
if err := http.ListenAndServe(*websocketAddresss, nil); err != nil {
|
|
panic(err)
|
|
}
|
|
log("Exiting at end of main()")
|
|
}
|