Browse Source

refactor ui to put views in separate packages. add subview of recent http requests with color-coded responses to terminal view

Alan Shreve 12 years ago
parent
commit
501953dc3c

+ 1 - 0
.gitignore

@@ -3,3 +3,4 @@ bin/
 pkg/
 pkg/
 src/code.google.com
 src/code.google.com
 src/github.com
 src/github.com
+src/ngrok/client/views/web/static/*.html.go

+ 2 - 2
Makefile

@@ -24,8 +24,8 @@ release-all: release-client release-server
 bindata:
 bindata:
 	echo $$GOPATH
 	echo $$GOPATH
 	go get github.com/inconshreveable/go-bindata
 	go get github.com/inconshreveable/go-bindata
-	./bin/go-bindata -b release -i templates/page.html -o src/ngrok/client/ui/static/page.html.go -m -p static -f PageHtml
-	./bin/go-bindata -b release -i templates/body.html -o src/ngrok/client/ui/static/body.html.go -m -p static -f BodyHtml
+	./bin/go-bindata -b release -i templates/page.html -o src/ngrok/client/views/web/static/page.html.go -m -p static -f PageHtml
+	./bin/go-bindata -b release -i templates/body.html -o src/ngrok/client/views/web/static/body.html.go -m -p static -f BodyHtml
 
 
 all: client server
 all: client server
 
 

+ 5 - 5
src/ngrok/client/main.go

@@ -6,6 +6,8 @@ import (
 	"io/ioutil"
 	"io/ioutil"
 	"net"
 	"net"
 	"ngrok/client/ui"
 	"ngrok/client/ui"
+	"ngrok/client/views/term"
+	"ngrok/client/views/web"
 	"ngrok/conn"
 	"ngrok/conn"
 	nlog "ngrok/log"
 	nlog "ngrok/log"
 	"ngrok/msg"
 	"ngrok/msg"
@@ -177,8 +179,8 @@ func Main() {
 
 
 	// init ui
 	// init ui
 	ctl := ui.NewController()
 	ctl := ui.NewController()
-	ui.NewTermView(ctl)
-	ui.NewWebView(ctl, s, opts.webport)
+	term.New(ctl, s)
+	web.NewWebView(ctl, s, opts.webport)
 
 
 	go control(s, ctl)
 	go control(s, ctl)
 
 
@@ -191,9 +193,7 @@ func Main() {
 			case cmd := <-ctl.Cmds:
 			case cmd := <-ctl.Cmds:
 				switch cmd.Code {
 				switch cmd.Code {
 				case ui.QUIT:
 				case ui.QUIT:
-					quitMessage = cmd.Payload.(string)
-					s.stopping = true
-					ctl.Update(s)
+					ctl.DoShutdown()
 					return
 					return
 				case ui.REPLAY:
 				case ui.REPLAY:
 					go func() {
 					go func() {

+ 1 - 3
src/ngrok/client/state.go

@@ -14,8 +14,7 @@ type State struct {
 	metrics   *ClientMetrics
 	metrics   *ClientMetrics
 
 
 	// just for UI purposes
 	// just for UI purposes
-	status   string
-	stopping bool
+	status string
 }
 }
 
 
 // implement client.ui.State
 // implement client.ui.State
@@ -25,7 +24,6 @@ func (s State) GetLocalAddr() string        { return s.opts.localaddr }
 func (s State) GetWebPort() int             { return s.opts.webport }
 func (s State) GetWebPort() int             { return s.opts.webport }
 func (s State) GetStatus() string           { return s.status }
 func (s State) GetStatus() string           { return s.status }
 func (s State) GetProtocol() proto.Protocol { return s.protocol }
 func (s State) GetProtocol() proto.Protocol { return s.protocol }
-func (s State) IsStopping() bool            { return s.stopping }
 
 
 func (s State) GetConnectionMetrics() (metrics.Meter, metrics.Timer) {
 func (s State) GetConnectionMetrics() (metrics.Meter, metrics.Timer) {
 	return s.metrics.connMeter, s.metrics.connTimer
 	return s.metrics.connMeter, s.metrics.connTimer

+ 11 - 0
src/ngrok/client/ui/command.go

@@ -0,0 +1,11 @@
+package ui
+
+type Command struct {
+	Code    int
+	Payload interface{}
+}
+
+const (
+	QUIT = iota
+	REPLAY
+)

+ 20 - 13
src/ngrok/client/ui/controller.go

@@ -8,16 +8,6 @@ import (
 	"sync"
 	"sync"
 )
 )
 
 
-type Command struct {
-	Code    int
-	Payload interface{}
-}
-
-const (
-	QUIT = iota
-	REPLAY
-)
-
 type Controller struct {
 type Controller struct {
 	// the model sends updates through this broadcast channel
 	// the model sends updates through this broadcast channel
 	Updates *util.Broadcast
 	Updates *util.Broadcast
@@ -27,13 +17,17 @@ type Controller struct {
 
 
 	// all threads may add themself to this to wait for clean shutdown
 	// all threads may add themself to this to wait for clean shutdown
 	Wait *sync.WaitGroup
 	Wait *sync.WaitGroup
+
+	// channel to signal shutdown
+	Shutdown chan int
 }
 }
 
 
 func NewController() *Controller {
 func NewController() *Controller {
 	ctl := &Controller{
 	ctl := &Controller{
-		Updates: util.NewBroadcast(),
-		Cmds:    make(chan Command),
-		Wait:    new(sync.WaitGroup),
+		Updates:  util.NewBroadcast(),
+		Cmds:     make(chan Command),
+		Wait:     new(sync.WaitGroup),
+		Shutdown: make(chan int),
 	}
 	}
 
 
 	return ctl
 	return ctl
@@ -42,3 +36,16 @@ func NewController() *Controller {
 func (ctl *Controller) Update(state State) {
 func (ctl *Controller) Update(state State) {
 	ctl.Updates.In() <- state
 	ctl.Updates.In() <- state
 }
 }
+
+func (ctl *Controller) DoShutdown() {
+	close(ctl.Shutdown)
+}
+
+func (ctl *Controller) IsShuttingDown() bool {
+	select {
+	case <-ctl.Shutdown:
+		return true
+	default:
+	}
+	return false
+}

+ 0 - 1
src/ngrok/client/ui/interface.go → src/ngrok/client/ui/state.go

@@ -12,7 +12,6 @@ type State interface {
 	GetStatus() string
 	GetStatus() string
 	GetProtocol() proto.Protocol
 	GetProtocol() proto.Protocol
 	GetWebPort() int
 	GetWebPort() int
-	IsStopping() bool
 	GetConnectionMetrics() (metrics.Meter, metrics.Timer)
 	GetConnectionMetrics() (metrics.Meter, metrics.Timer)
 	GetBytesInMetrics() (metrics.Counter, metrics.Histogram)
 	GetBytesInMetrics() (metrics.Counter, metrics.Histogram)
 	GetBytesOutMetrics() (metrics.Counter, metrics.Histogram)
 	GetBytesOutMetrics() (metrics.Counter, metrics.Histogram)

+ 0 - 153
src/ngrok/client/ui/terminal.go

@@ -1,153 +0,0 @@
-/* 
-   interactive terminal interface for local clients
-*/
-package ui
-
-import (
-	"fmt"
-	termbox "github.com/nsf/termbox-go"
-	"time"
-)
-
-const (
-	fgColor = termbox.ColorWhite
-	bgColor = termbox.ColorDefault
-)
-
-func clear() {
-	w, h := termbox.Size()
-
-	for i := 0; i < w; i++ {
-		for j := 0; j < h; j++ {
-			termbox.SetCell(i, j, ' ', fgColor, bgColor)
-		}
-	}
-}
-
-func printfAttr(x, y int, fg termbox.Attribute, arg0 string, args ...interface{}) {
-	s := fmt.Sprintf(arg0, args...)
-	for i, ch := range s {
-		termbox.SetCell(x+i, y, ch, fg, bgColor)
-	}
-}
-
-func printf(x, y int, arg0 string, args ...interface{}) {
-	printfAttr(x, y, fgColor, arg0, args...)
-}
-
-type TermView struct {
-	ctl            *Controller
-	statusColorMap map[string]termbox.Attribute
-	updates        chan interface{}
-}
-
-func NewTermView(ctl *Controller) *TermView {
-	t := &TermView{
-		ctl:     ctl,
-		updates: ctl.Updates.Reg(),
-		statusColorMap: map[string]termbox.Attribute{
-			"connecting":   termbox.ColorCyan,
-			"reconnecting": termbox.ColorRed,
-			"online":       termbox.ColorGreen,
-		},
-	}
-
-	go t.run()
-	return t
-}
-
-func (t *TermView) run() {
-	// XXX: clean this up? maybe Term should have its own waitgroup for
-	// both run and draw
-
-	// make sure we shut down cleanly
-	t.ctl.Wait.Add(1)
-	defer t.ctl.Wait.Done()
-
-	// init/close termbox library
-	termbox.Init()
-	defer termbox.Close()
-
-	go t.input()
-
-	t.draw()
-}
-
-func (t *TermView) draw() {
-	var state State
-	for {
-		select {
-		case newState := <-t.updates:
-			if newState != nil {
-				state = newState.(State)
-			}
-
-			if state == nil {
-				// log.Info("Got update to draw, but no state to draw with")
-				continue
-			}
-
-			// program is shutting down
-			if state.IsStopping() {
-				return
-			}
-
-			clear()
-
-			x, _ := termbox.Size()
-			quitMsg := "(Ctrl+C to quit)"
-			printf(x-len(quitMsg), 0, quitMsg)
-
-			printfAttr(0, 0, termbox.ColorBlue|termbox.AttrBold, "ngrok")
-
-			msec := float64(time.Millisecond)
-
-			printfAttr(0, 2, t.statusColorMap[state.GetStatus()], "%-30s%s", "Tunnel Status", state.GetStatus())
-			printf(0, 3, "%-30s%s", "Version", state.GetVersion())
-			printf(0, 4, "%-30s%s", "Protocol", state.GetProtocol().GetName())
-			printf(0, 5, "%-30s%s -> %s", "Forwarding", state.GetPublicUrl(), state.GetLocalAddr())
-			printf(0, 6, "%-30s%s", "Web Interface", "http://127.0.0.1:%d", state.GetWebPort())
-
-			connMeter, connTimer := state.GetConnectionMetrics()
-			printf(0, 7, "%-30s%d", "# Conn", connMeter.Count())
-			printf(0, 8, "%-30s%.2fms", "Avg Conn Time", connTimer.Mean()/msec)
-
-			/*
-				if state.GetProtocol() == "http" {
-					printf(0, 10, "HTTP Requests")
-					printf(0, 11, "-------------")
-					for i, http := range state.GetHistory() {
-						req := http.GetRequest()
-						resp := http.GetResponse()
-						printf(0, 12+i, "%s %v", req.Method, req.URL)
-						if resp != nil {
-							printf(30, 12+i, "%s", resp.Status)
-						}
-					}
-				}
-			*/
-
-			termbox.Flush()
-		}
-	}
-}
-
-func (t *TermView) input() {
-	for {
-		ev := termbox.PollEvent()
-		switch ev.Type {
-		case termbox.EventKey:
-			switch ev.Key {
-			case termbox.KeyCtrlC:
-				t.ctl.Cmds <- Command{QUIT, ""}
-				return
-			}
-
-		case termbox.EventResize:
-			t.updates <- nil
-
-		case termbox.EventError:
-			panic(ev.Err)
-		}
-	}
-}

+ 5 - 0
src/ngrok/client/ui/view.go

@@ -0,0 +1,5 @@
+package ui
+
+type View interface {
+	Render()
+}

+ 46 - 0
src/ngrok/client/views/term/area.go

@@ -0,0 +1,46 @@
+// shared internal functions for handling output to the terminal
+package term
+
+import (
+	"fmt"
+	termbox "github.com/nsf/termbox-go"
+)
+
+const (
+	fgColor = termbox.ColorWhite
+	bgColor = termbox.ColorDefault
+)
+
+type area struct {
+	// top-left corner
+	x, y int
+
+	// size of the area
+	w, h int
+
+	// default colors
+	fgColor, bgColor termbox.Attribute
+}
+
+func NewArea(x, y, w, h int) *area {
+	return &area{x, y, w, h, fgColor, bgColor}
+}
+
+func (a *area) Clear() {
+	for i := 0; i < a.w; i++ {
+		for j := 0; j < a.h; j++ {
+			termbox.SetCell(a.x+i, a.y+j, ' ', a.fgColor, a.bgColor)
+		}
+	}
+}
+
+func (a *area) APrintf(fg termbox.Attribute, x, y int, arg0 string, args ...interface{}) {
+	s := fmt.Sprintf(arg0, args...)
+	for i, ch := range s {
+		termbox.SetCell(a.x+x+i, a.y+y, ch, fg, bgColor)
+	}
+}
+
+func (a *area) Printf(x, y int, arg0 string, args ...interface{}) {
+	a.APrintf(a.fgColor, x, y, arg0, args...)
+}

+ 80 - 0
src/ngrok/client/views/term/http.go

@@ -0,0 +1,80 @@
+package term
+
+import (
+	termbox "github.com/nsf/termbox-go"
+	"ngrok/log"
+	"ngrok/proto"
+	"ngrok/util"
+)
+
+const (
+	size = 10
+)
+
+type HttpView struct {
+	httpProto    *proto.Http
+	HttpRequests *util.Ring
+	shutdown     chan int
+	*area
+	log.Logger
+}
+
+func colorFor(status string) termbox.Attribute {
+	switch status[0] {
+	case '3':
+		return termbox.ColorCyan
+	case '4':
+		return termbox.ColorYellow
+	case '5':
+		return termbox.ColorRed
+	default:
+	}
+	return termbox.ColorWhite
+}
+
+func NewHttp(proto *proto.Http, shutdown chan int, x, y int) *HttpView {
+	v := &HttpView{
+		httpProto:    proto,
+		HttpRequests: util.NewRing(size),
+		area:         NewArea(x, y, 70, size+5),
+		shutdown:     shutdown,
+		Logger:       log.NewPrefixLogger(),
+	}
+	v.AddLogPrefix("view")
+	v.AddLogPrefix("term")
+	v.AddLogPrefix("http")
+	go v.Run()
+	return v
+}
+
+func (v *HttpView) Run() {
+	updates := v.httpProto.Txns.Reg()
+
+	for {
+		select {
+		case <-v.shutdown:
+			return
+
+		case txn := <-updates:
+			v.Debug("Got HTTP update")
+			if txn.(*proto.HttpTxn).Resp == nil {
+				v.HttpRequests.Add(txn)
+			}
+			v.Render()
+		}
+	}
+}
+
+func (v *HttpView) Render() {
+	v.Clear()
+	v.Printf(0, 0, "HTTP Requests")
+	v.Printf(0, 1, "-------------")
+	for i, obj := range v.HttpRequests.Slice() {
+		txn := obj.(*proto.HttpTxn)
+		v.Printf(0, 3+i, "%s %v", txn.Req.Method, txn.Req.URL.Path)
+		if txn.Resp != nil {
+			v.APrintf(colorFor(txn.Resp.Status), 30, 3+i, "%s", txn.Resp.Status)
+		}
+	}
+	termbox.Flush()
+}

+ 140 - 0
src/ngrok/client/views/term/view.go

@@ -0,0 +1,140 @@
+/* 
+   interactive terminal interface for local clients
+*/
+package term
+
+import (
+	"fmt"
+	termbox "github.com/nsf/termbox-go"
+	"ngrok/client/ui"
+	"ngrok/log"
+	"ngrok/proto"
+	"time"
+)
+
+type TermView struct {
+	ctl      *ui.Controller
+	updates  chan interface{}
+	subviews []ui.View
+	state    ui.State
+	log.Logger
+	*area
+}
+
+func New(ctl *ui.Controller, state ui.State) *TermView {
+	// initialize terminal display
+	termbox.Init()
+
+	// make sure ngrok doesn't quit until we've cleaned up
+	ctl.Wait.Add(1)
+
+	w, _ := termbox.Size()
+
+	v := &TermView{
+		ctl:      ctl,
+		updates:  ctl.Updates.Reg(),
+		subviews: make([]ui.View, 0),
+		state:    state,
+		Logger:   log.NewPrefixLogger(),
+		area:     NewArea(0, 0, w, 10),
+	}
+
+	v.Logger.AddLogPrefix("view")
+	v.Logger.AddLogPrefix("term")
+
+	switch p := state.GetProtocol().(type) {
+	case *proto.Http:
+		v.subviews = append(v.subviews, NewHttp(p, ctl.Shutdown, 0, 10))
+	default:
+	}
+
+	v.Render()
+
+	go v.run()
+	go v.input()
+
+	return v
+}
+
+func colorForConn(status string) termbox.Attribute {
+	switch status {
+	case "connecting":
+		return termbox.ColorCyan
+	case "reconnecting":
+		return termbox.ColorRed
+	case "online":
+		return termbox.ColorGreen
+	}
+	return termbox.ColorWhite
+}
+
+func (v *TermView) Render() {
+	v.Clear()
+
+	// quit instructions
+	quitMsg := "(Ctrl+C to quit)"
+	v.Printf(v.x-len(quitMsg), 0, quitMsg)
+
+	v.APrintf(termbox.ColorBlue|termbox.AttrBold, 0, 0, "ngrok")
+
+	status := v.state.GetStatus()
+	v.APrintf(colorForConn(status), 0, 2, "%-30s%s", "Tunnel Status", status)
+
+	v.Printf(0, 3, "%-30s%s", "Version", v.state.GetVersion())
+	v.Printf(0, 4, "%-30s%s", "Protocol", v.state.GetProtocol().GetName())
+	v.Printf(0, 5, "%-30s%s -> %s", "Forwarding", v.state.GetPublicUrl(), v.state.GetLocalAddr())
+	webAddr := fmt.Sprintf("http://localhost:%d", v.state.GetWebPort())
+	v.Printf(0, 6, "%-30s%s", "Web Interface", webAddr)
+
+	connMeter, connTimer := v.state.GetConnectionMetrics()
+	v.Printf(0, 7, "%-30s%d", "# Conn", connMeter.Count())
+
+	msec := float64(time.Millisecond)
+	v.Printf(0, 8, "%-30s%.2fms", "Avg Conn Time", connTimer.Mean()/msec)
+
+	termbox.Flush()
+}
+
+func (v *TermView) run() {
+	defer v.ctl.Wait.Done()
+	defer termbox.Close()
+
+	for {
+		v.Debug("Waiting for update")
+		select {
+		case obj := <-v.updates:
+			v.state = obj.(ui.State)
+			v.Render()
+
+		case <-v.ctl.Shutdown:
+			return
+		}
+	}
+}
+
+func (v *TermView) input() {
+	for {
+		ev := termbox.PollEvent()
+		switch ev.Type {
+		case termbox.EventKey:
+			switch ev.Key {
+			case termbox.KeyCtrlC:
+				v.Info("Got quit command")
+				v.ctl.Cmds <- ui.Command{ui.QUIT, ""}
+			}
+
+		case termbox.EventResize:
+			v.Info("Resize event, redrawing")
+			v.Render()
+			for _, sv := range v.subviews {
+				sv.Render()
+			}
+
+		case termbox.EventError:
+			if v.ctl.IsShuttingDown() {
+				return
+			}
+			panic(ev.Err)
+		}
+	}
+}

+ 6 - 5
src/ngrok/client/ui/webhttp.go → src/ngrok/client/views/web/http.go

@@ -1,5 +1,5 @@
 // interative web user interface
 // interative web user interface
-package ui
+package web
 
 
 import (
 import (
 	"bytes"
 	"bytes"
@@ -9,7 +9,8 @@ import (
 	"net/http"
 	"net/http"
 	"net/http/httputil"
 	"net/http/httputil"
 	"net/url"
 	"net/url"
-	"ngrok/client/ui/static"
+	"ngrok/client/ui"
+	"ngrok/client/views/web/static"
 	"ngrok/proto"
 	"ngrok/proto"
 	"ngrok/util"
 	"ngrok/util"
 	"strings"
 	"strings"
@@ -28,13 +29,13 @@ type WebHttpTxn struct {
 }
 }
 
 
 type WebHttpView struct {
 type WebHttpView struct {
-	ctl          *Controller
+	ctl          *ui.Controller
 	httpProto    *proto.Http
 	httpProto    *proto.Http
 	HttpRequests *util.Ring
 	HttpRequests *util.Ring
 	idToTxn      map[string]*WebHttpTxn
 	idToTxn      map[string]*WebHttpTxn
 }
 }
 
 
-func NewWebHttpView(ctl *Controller, proto *proto.Http) *WebHttpView {
+func NewWebHttpView(ctl *ui.Controller, proto *proto.Http) *WebHttpView {
 	w := &WebHttpView{
 	w := &WebHttpView{
 		ctl:          ctl,
 		ctl:          ctl,
 		httpProto:    proto,
 		httpProto:    proto,
@@ -80,7 +81,7 @@ func (h *WebHttpView) register() {
 			if err != nil {
 			if err != nil {
 				panic(err)
 				panic(err)
 			}
 			}
-			h.ctl.Cmds <- Command{REPLAY, bodyBytes}
+			h.ctl.Cmds <- ui.Command{ui.REPLAY, bodyBytes}
 			w.Write([]byte(http.StatusText(200)))
 			w.Write([]byte(http.StatusText(200)))
 		} else {
 		} else {
 			// XXX: 400
 			// XXX: 400

+ 0 - 0
src/ngrok/client/ui/static/debug.html.go → src/ngrok/client/views/web/static/debug.go


+ 3 - 2
src/ngrok/client/ui/web.go → src/ngrok/client/views/web/view.go

@@ -1,15 +1,16 @@
 // interative web user interface
 // interative web user interface
-package ui
+package web
 
 
 import (
 import (
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
+	"ngrok/client/ui"
 	"ngrok/proto"
 	"ngrok/proto"
 )
 )
 
 
 type WebView struct{}
 type WebView struct{}
 
 
-func NewWebView(ctl *Controller, state State, port int) *WebView {
+func NewWebView(ctl *ui.Controller, state ui.State, port int) *WebView {
 	w := &WebView{}
 	w := &WebView{}
 
 
 	switch p := state.GetProtocol().(type) {
 	switch p := state.GetProtocol().(type) {