412 lines
12 KiB
Go
412 lines
12 KiB
Go
// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
|
|
// Revel Framework source code and usage is governed by a MIT style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// Package harness for a Revel Framework.
|
|
//
|
|
// It has a following responsibilities:
|
|
// 1. Parse the user program, generating a main.go file that registers
|
|
// controller classes and starts the user's server.
|
|
// 2. Build and run the user program. Show compile errors.
|
|
// 3. Monitor the user source and re-build / restart the program when necessary.
|
|
//
|
|
// Source files are generated in the app/tmp directory.
|
|
package harness
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"fmt"
|
|
"time"
|
|
"go/build"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync/atomic"
|
|
|
|
"github.com/revel/cmd/model"
|
|
"github.com/revel/cmd/utils"
|
|
"github.com/revel/cmd/watcher"
|
|
"html/template"
|
|
"io/ioutil"
|
|
"sync"
|
|
"encoding/json"
|
|
)
|
|
|
|
var (
|
|
doNotWatch = []string{"tmp", "views", "routes"}
|
|
|
|
lastRequestHadError int32
|
|
)
|
|
|
|
// Harness reverse proxies requests to the application server.
|
|
// It builds / runs / rebuilds / restarts the server when code is changed.
|
|
type Harness struct {
|
|
app *App // The application
|
|
useProxy bool // True if proxy is in use
|
|
serverHost string // The proxy server host
|
|
port int // The proxy serber port
|
|
proxy *httputil.ReverseProxy // The proxy
|
|
watcher *watcher.Watcher // The file watched
|
|
mutex *sync.Mutex // A mutex to prevent concurrent updates
|
|
paths *model.RevelContainer // The Revel container
|
|
config *model.CommandConfig // The configuration
|
|
runMode string // The runmode the harness is running in
|
|
isError bool // True if harness is in error state
|
|
ranOnce bool // True app compiled once
|
|
}
|
|
|
|
func (h *Harness) renderError(iw http.ResponseWriter, ir *http.Request, err error) {
|
|
// Render error here
|
|
// Grab the template from three places
|
|
// 1) Application/views/errors
|
|
// 2) revel_home/views/errors
|
|
// 3) views/errors
|
|
if err == nil {
|
|
utils.Logger.Panic("Caller passed in a nil error")
|
|
}
|
|
templateSet := template.New("__root__")
|
|
seekViewOnPath := func(view string) (path string) {
|
|
path = filepath.Join(h.paths.ViewsPath, "errors", view)
|
|
if !utils.Exists(path) {
|
|
path = filepath.Join(h.paths.RevelPath, "templates", "errors", view)
|
|
}
|
|
|
|
data, err := ioutil.ReadFile(path)
|
|
if err != nil {
|
|
utils.Logger.Error("Unable to read template file", path)
|
|
}
|
|
_, err = templateSet.New("errors/" + view).Parse(string(data))
|
|
if err != nil {
|
|
utils.Logger.Error("Unable to parse template file", path)
|
|
}
|
|
return
|
|
}
|
|
target := []string{seekViewOnPath("500.html"), seekViewOnPath("500-dev.html")}
|
|
if !utils.Exists(target[0]) {
|
|
fmt.Fprintf(iw, "Target template not found not found %s<br />\n", target[0])
|
|
fmt.Fprintf(iw, "An error ocurred %s", err.Error())
|
|
return
|
|
}
|
|
var revelError *utils.SourceError
|
|
switch e := err.(type) {
|
|
case *utils.SourceError:
|
|
revelError = e
|
|
case error:
|
|
revelError = &utils.SourceError{
|
|
Title: "Server Error",
|
|
Description: e.Error(),
|
|
}
|
|
}
|
|
|
|
if revelError == nil {
|
|
panic("no error provided")
|
|
}
|
|
viewArgs := map[string]interface{}{}
|
|
viewArgs["RunMode"] = h.paths.RunMode
|
|
viewArgs["DevMode"] = h.paths.DevMode
|
|
viewArgs["Error"] = revelError
|
|
|
|
// Render the template from the file
|
|
err = templateSet.ExecuteTemplate(iw, "errors/500.html", viewArgs)
|
|
if err != nil {
|
|
utils.Logger.Error("Failed to execute", "error", err)
|
|
}
|
|
}
|
|
|
|
// ServeHTTP handles all requests.
|
|
// It checks for changes to app, rebuilds if necessary, and forwards the request.
|
|
func (h *Harness) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
// Don't rebuild the app for favicon requests.
|
|
if lastRequestHadError > 0 && r.URL.Path == "/favicon.ico" {
|
|
return
|
|
}
|
|
|
|
// Flush any change events and rebuild app if necessary.
|
|
// Render an error page if the rebuild / restart failed.
|
|
err := h.watcher.Notify()
|
|
if err != nil {
|
|
// In a thread safe manner update the flag so that a request for
|
|
// /favicon.ico does not trigger a rebuild
|
|
atomic.CompareAndSwapInt32(&lastRequestHadError, 0, 1)
|
|
h.renderError(w, r, err)
|
|
return
|
|
}
|
|
|
|
// In a thread safe manner update the flag so that a request for
|
|
// /favicon.ico is allowed
|
|
atomic.CompareAndSwapInt32(&lastRequestHadError, 1, 0)
|
|
|
|
// Reverse proxy the request.
|
|
// (Need special code for websockets, courtesy of bradfitz)
|
|
if strings.EqualFold(r.Header.Get("Upgrade"), "websocket") {
|
|
h.proxyWebsocket(w, r, h.serverHost)
|
|
} else {
|
|
h.proxy.ServeHTTP(w, r)
|
|
}
|
|
}
|
|
|
|
// NewHarness method returns a reverse proxy that forwards requests
|
|
// to the given port.
|
|
func NewHarness(c *model.CommandConfig, paths *model.RevelContainer, runMode string, noProxy bool) *Harness {
|
|
// Get a template loader to render errors.
|
|
// Prefer the app's views/errors directory, and fall back to the stock error pages.
|
|
//revel.MainTemplateLoader = revel.NewTemplateLoader(
|
|
// []string{filepath.Join(revel.RevelPath, "templates")})
|
|
//if err := revel.MainTemplateLoader.Refresh(); err != nil {
|
|
// revel.RevelLog.Error("Template loader error", "error", err)
|
|
//}
|
|
|
|
addr := paths.HTTPAddr
|
|
port := paths.Config.IntDefault("harness.port", 0)
|
|
scheme := "http"
|
|
|
|
if paths.HTTPSsl {
|
|
scheme = "https"
|
|
}
|
|
|
|
// If the server is running on the wildcard address, use "localhost"
|
|
if addr == "" {
|
|
utils.Logger.Warn("No http.addr specified in the app.conf listening on localhost interface only. " +
|
|
"This will not allow external access to your application")
|
|
addr = "localhost"
|
|
}
|
|
|
|
if port == 0 {
|
|
port = getFreePort()
|
|
}
|
|
|
|
serverURL, _ := url.ParseRequestURI(fmt.Sprintf(scheme+"://%s:%d", addr, port))
|
|
|
|
serverHarness := &Harness{
|
|
port: port,
|
|
serverHost: serverURL.String()[len(scheme+"://"):],
|
|
proxy: httputil.NewSingleHostReverseProxy(serverURL),
|
|
mutex: &sync.Mutex{},
|
|
paths: paths,
|
|
useProxy: !noProxy,
|
|
config: c,
|
|
runMode: runMode,
|
|
}
|
|
|
|
if paths.HTTPSsl {
|
|
serverHarness.proxy.Transport = &http.Transport{
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
}
|
|
}
|
|
return serverHarness
|
|
}
|
|
|
|
// Refresh method rebuilds the Revel application and run it on the given port.
|
|
// called by the watcher
|
|
func (h *Harness) Refresh() (err *utils.SourceError) {
|
|
t := time.Now();
|
|
fmt.Println("Changed detected, recompiling")
|
|
err = h.refresh()
|
|
if err!=nil && !h.ranOnce && h.useProxy {
|
|
addr := fmt.Sprintf("%s:%d", h.paths.HTTPAddr, h.paths.HTTPPort)
|
|
|
|
fmt.Printf("\nError compiling code, to view error details see proxy running on http://%s\n\n",addr)
|
|
}
|
|
|
|
h.ranOnce = true
|
|
fmt.Printf("\nTime to recompile %s\n",time.Now().Sub(t).String())
|
|
return
|
|
}
|
|
|
|
func (h *Harness) refresh() (err *utils.SourceError) {
|
|
// Allow only one thread to rebuild the process
|
|
// If multiple requests to rebuild are queued only the last one is executed on
|
|
// So before a build is started we wait for a second to determine if
|
|
// more requests for a build are triggered.
|
|
// Once no more requests are triggered the build will be processed
|
|
h.mutex.Lock()
|
|
defer h.mutex.Unlock()
|
|
|
|
if h.app != nil {
|
|
h.app.Kill()
|
|
}
|
|
|
|
utils.Logger.Info("Rebuild Called")
|
|
var newErr error
|
|
h.app, newErr = Build(h.config, h.paths)
|
|
if newErr != nil {
|
|
utils.Logger.Error("Build detected an error", "error", newErr)
|
|
if castErr, ok := newErr.(*utils.SourceError); ok {
|
|
return castErr
|
|
}
|
|
err = &utils.SourceError{
|
|
Title: "App failed to start up",
|
|
Description: err.Error(),
|
|
}
|
|
return
|
|
}
|
|
|
|
if h.useProxy {
|
|
h.app.Port = h.port
|
|
runMode := h.runMode
|
|
if !h.config.HistoricMode {
|
|
// Recalulate run mode based on the config
|
|
var paths []byte
|
|
if len(h.app.PackagePathMap)>0 {
|
|
paths, _ = json.Marshal(h.app.PackagePathMap)
|
|
}
|
|
runMode = fmt.Sprintf(`{"mode":"%s", "specialUseFlag":%v,"packagePathMap":%s}`, h.app.Paths.RunMode, h.config.Verbose, string(paths))
|
|
|
|
}
|
|
if err2 := h.app.Cmd(runMode).Start(h.config); err2 != nil {
|
|
utils.Logger.Error("Could not start application", "error", err2)
|
|
if err,k :=err2.(*utils.SourceError);k {
|
|
return err
|
|
}
|
|
return &utils.SourceError{
|
|
Title: "App failed to start up",
|
|
Description: err2.Error(),
|
|
}
|
|
}
|
|
} else {
|
|
h.app = nil
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// WatchDir method returns false to file matches with doNotWatch
|
|
// otheriwse true
|
|
func (h *Harness) WatchDir(info os.FileInfo) bool {
|
|
return !utils.ContainsString(doNotWatch, info.Name())
|
|
}
|
|
|
|
// WatchFile method returns true given filename HasSuffix of ".go"
|
|
// otheriwse false - implements revel.DiscerningListener
|
|
func (h *Harness) WatchFile(filename string) bool {
|
|
return strings.HasSuffix(filename, ".go")
|
|
}
|
|
|
|
// Run the harness, which listens for requests and proxies them to the app
|
|
// server, which it runs and rebuilds as necessary.
|
|
func (h *Harness) Run() {
|
|
var paths []string
|
|
if h.paths.Config.BoolDefault("watch.gopath", false) {
|
|
gopaths := filepath.SplitList(build.Default.GOPATH)
|
|
paths = append(paths, gopaths...)
|
|
}
|
|
paths = append(paths, h.paths.CodePaths...)
|
|
h.watcher = watcher.NewWatcher(h.paths, false)
|
|
h.watcher.Listen(h, paths...)
|
|
go h.Refresh()
|
|
// h.watcher.Notify()
|
|
|
|
if h.useProxy {
|
|
go func() {
|
|
// Check the port to start on a random port
|
|
if h.paths.HTTPPort == 0 {
|
|
h.paths.HTTPPort = getFreePort()
|
|
}
|
|
addr := fmt.Sprintf("%s:%d", h.paths.HTTPAddr, h.paths.HTTPPort)
|
|
utils.Logger.Infof("Proxy server is listening on %s", addr)
|
|
|
|
|
|
var err error
|
|
if h.paths.HTTPSsl {
|
|
err = http.ListenAndServeTLS(
|
|
addr,
|
|
h.paths.HTTPSslCert,
|
|
h.paths.HTTPSslKey,
|
|
h)
|
|
} else {
|
|
err = http.ListenAndServe(addr, h)
|
|
}
|
|
if err != nil {
|
|
utils.Logger.Error("Failed to start reverse proxy:", "error", err)
|
|
}
|
|
}()
|
|
|
|
}
|
|
|
|
// Make a new channel to listen for the interrupt event
|
|
ch := make(chan os.Signal)
|
|
signal.Notify(ch, os.Interrupt, os.Kill)
|
|
<-ch
|
|
// Kill the app and exit
|
|
if h.app != nil {
|
|
h.app.Kill()
|
|
}
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Find an unused port
|
|
func getFreePort() (port int) {
|
|
conn, err := net.Listen("tcp", ":0")
|
|
if err != nil {
|
|
utils.Logger.Fatal("Unable to fetch a freee port address", "error", err)
|
|
}
|
|
|
|
port = conn.Addr().(*net.TCPAddr).Port
|
|
err = conn.Close()
|
|
if err != nil {
|
|
utils.Logger.Fatal("Unable to close port", "error", err)
|
|
}
|
|
return port
|
|
}
|
|
|
|
// proxyWebsocket copies data between websocket client and server until one side
|
|
// closes the connection. (ReverseProxy doesn't work with websocket requests.)
|
|
func (h *Harness) proxyWebsocket(w http.ResponseWriter, r *http.Request, host string) {
|
|
var (
|
|
d net.Conn
|
|
err error
|
|
)
|
|
if h.paths.HTTPSsl {
|
|
// since this proxy isn't used in production,
|
|
// it's OK to set InsecureSkipVerify to true
|
|
// no need to add another configuration option.
|
|
d, err = tls.Dial("tcp", host, &tls.Config{InsecureSkipVerify: true})
|
|
} else {
|
|
d, err = net.Dial("tcp", host)
|
|
}
|
|
if err != nil {
|
|
http.Error(w, "Error contacting backend server.", 500)
|
|
utils.Logger.Error("Error dialing websocket backend ", "host", host, "error", err)
|
|
return
|
|
}
|
|
hj, ok := w.(http.Hijacker)
|
|
if !ok {
|
|
http.Error(w, "Not a hijacker?", 500)
|
|
return
|
|
}
|
|
nc, _, err := hj.Hijack()
|
|
if err != nil {
|
|
utils.Logger.Error("Hijack error", "error", err)
|
|
return
|
|
}
|
|
defer func() {
|
|
if err = nc.Close(); err != nil {
|
|
utils.Logger.Error("Connection close error", "error", err)
|
|
}
|
|
if err = d.Close(); err != nil {
|
|
utils.Logger.Error("Dial close error", "error", err)
|
|
}
|
|
}()
|
|
|
|
err = r.Write(d)
|
|
if err != nil {
|
|
utils.Logger.Error("Error copying request to target", "error", err)
|
|
return
|
|
}
|
|
|
|
errc := make(chan error, 2)
|
|
cp := func(dst io.Writer, src io.Reader) {
|
|
_, err := io.Copy(dst, src)
|
|
errc <- err
|
|
}
|
|
go cp(d, nc)
|
|
go cp(nc, d)
|
|
<-errc
|
|
}
|