package api import ( "encoding/json" "fmt" "net/http" "os" "strconv" "time" "github.com/smartystreets/goconvey/web/server/contract" "github.com/smartystreets/goconvey/web/server/messaging" ) type HTTPServer struct { watcher chan messaging.WatcherCommand executor contract.Executor latest *contract.CompleteOutput currentRoot string longpoll chan chan string paused bool } func (self *HTTPServer) ReceiveUpdate(root string, update *contract.CompleteOutput) { self.currentRoot = root self.latest = update } func (self *HTTPServer) Watch(response http.ResponseWriter, request *http.Request) { if request.Method == "POST" { self.adjustRoot(response, request) } else if request.Method == "GET" { response.Write([]byte(self.currentRoot)) } } func (self *HTTPServer) adjustRoot(response http.ResponseWriter, request *http.Request) { newRoot := self.parseQueryString("root", response, request) if newRoot == "" { return } info, err := os.Stat(newRoot) // TODO: how to unit test? if !info.IsDir() || err != nil { http.Error(response, err.Error(), http.StatusNotFound) return } self.watcher <- messaging.WatcherCommand{ Instruction: messaging.WatcherAdjustRoot, Details: newRoot, } } func (self *HTTPServer) Ignore(response http.ResponseWriter, request *http.Request) { paths := self.parseQueryString("paths", response, request) if paths != "" { self.watcher <- messaging.WatcherCommand{ Instruction: messaging.WatcherIgnore, Details: paths, } } } func (self *HTTPServer) Reinstate(response http.ResponseWriter, request *http.Request) { paths := self.parseQueryString("paths", response, request) if paths != "" { self.watcher <- messaging.WatcherCommand{ Instruction: messaging.WatcherReinstate, Details: paths, } } } func (self *HTTPServer) parseQueryString(key string, response http.ResponseWriter, request *http.Request) string { value := request.URL.Query()[key] if len(value) == 0 { http.Error(response, fmt.Sprintf("No '%s' query string parameter included!", key), http.StatusBadRequest) return "" } path := value[0] if path == "" { http.Error(response, "You must provide a non-blank path.", http.StatusBadRequest) } return path } func (self *HTTPServer) Status(response http.ResponseWriter, request *http.Request) { status := self.executor.Status() response.Write([]byte(status)) } func (self *HTTPServer) LongPollStatus(response http.ResponseWriter, request *http.Request) { if self.executor.ClearStatusFlag() { response.Write([]byte(self.executor.Status())) return } timeout, err := strconv.Atoi(request.URL.Query().Get("timeout")) if err != nil || timeout > 180000 || timeout < 0 { timeout = 60000 // default timeout is 60 seconds } myReqChan := make(chan string) select { case self.longpoll <- myReqChan: // this case means the executor's status is changing case <-time.After(time.Duration(timeout) * time.Millisecond): // this case means the executor hasn't changed status return } out := <-myReqChan if out != "" { // TODO: Why is this check necessary? Sometimes it writes empty string... response.Write([]byte(out)) } } func (self *HTTPServer) Results(response http.ResponseWriter, request *http.Request) { response.Header().Set("Content-Type", "application/json") response.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") response.Header().Set("Pragma", "no-cache") response.Header().Set("Expires", "0") if self.latest != nil { self.latest.Paused = self.paused } stuff, _ := json.Marshal(self.latest) response.Write(stuff) } func (self *HTTPServer) Execute(response http.ResponseWriter, request *http.Request) { go self.execute() } func (self *HTTPServer) execute() { self.watcher <- messaging.WatcherCommand{Instruction: messaging.WatcherExecute} } func (self *HTTPServer) TogglePause(response http.ResponseWriter, request *http.Request) { instruction := messaging.WatcherPause if self.paused { instruction = messaging.WatcherResume } self.watcher <- messaging.WatcherCommand{Instruction: instruction} self.paused = !self.paused fmt.Fprint(response, self.paused) // we could write out whatever helps keep the UI honest... } func NewHTTPServer( root string, watcher chan messaging.WatcherCommand, executor contract.Executor, status chan chan string) *HTTPServer { self := new(HTTPServer) self.currentRoot = root self.watcher = watcher self.executor = executor self.longpoll = status return self }