github.com/terhitormanen/cmd@v1.1.4/harness/harness.go (about)

     1  // Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
     2  // Revel Framework source code and usage is governed by a MIT style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package harness for a Revel Framework.
     6  //
     7  // It has a following responsibilities:
     8  // 1. Parse the user program, generating a main.go file that registers
     9  //    controller classes and starts the user's server.
    10  // 2. Build and run the user program.  Show compile errors.
    11  // 3. Monitor the user source and re-build / restart the program when necessary.
    12  //
    13  // Source files are generated in the app/tmp directory.
    14  package harness
    15  
    16  import (
    17  	"crypto/tls"
    18  	"encoding/json"
    19  	"errors"
    20  	"fmt"
    21  	"go/build"
    22  	"html/template"
    23  	"io"
    24  	"io/ioutil"
    25  	"net"
    26  	"net/http"
    27  	"net/http/httputil"
    28  	"net/url"
    29  	"os"
    30  	"os/signal"
    31  	"path/filepath"
    32  	"strings"
    33  	"sync"
    34  	"sync/atomic"
    35  	"time"
    36  
    37  	"github.com/terhitormanen/cmd/model"
    38  	"github.com/terhitormanen/cmd/utils"
    39  	"github.com/terhitormanen/cmd/watcher"
    40  )
    41  
    42  var (
    43  	doNotWatch = []string{"tmp", "views", "routes"}
    44  
    45  	lastRequestHadError int32
    46  	startupError        int32
    47  	startupErrorText    error
    48  )
    49  
    50  // Harness reverse proxies requests to the application server.
    51  // It builds / runs / rebuilds / restarts the server when code is changed.
    52  type Harness struct {
    53  	app        *App                   // The application
    54  	useProxy   bool                   // True if proxy is in use
    55  	serverHost string                 // The proxy server host
    56  	port       int                    // The proxy serber port
    57  	proxy      *httputil.ReverseProxy // The proxy
    58  	watcher    *watcher.Watcher       // The file watched
    59  	mutex      *sync.Mutex            // A mutex to prevent concurrent updates
    60  	paths      *model.RevelContainer  // The Revel container
    61  	config     *model.CommandConfig   // The configuration
    62  	runMode    string                 // The runmode the harness is running in
    63  	ranOnce    bool                   // True app compiled once
    64  }
    65  
    66  func (h *Harness) renderError(iw http.ResponseWriter, ir *http.Request, err error) {
    67  	// Render error here
    68  	// Grab the template from three places
    69  	// 1) Application/views/errors
    70  	// 2) revel_home/views/errors
    71  	// 3) views/errors
    72  	if err == nil {
    73  		utils.Logger.Panic("Caller passed in a nil error")
    74  	}
    75  
    76  	templateSet := template.New("__root__")
    77  	seekViewOnPath := func(view string) (path string) {
    78  		path = filepath.Join(h.paths.ViewsPath, "errors", view)
    79  		if !utils.Exists(path) {
    80  			path = filepath.Join(h.paths.RevelPath, "templates", "errors", view)
    81  		}
    82  
    83  		data, err := ioutil.ReadFile(path)
    84  		if err != nil {
    85  			utils.Logger.Error("Unable to read template file", path)
    86  		}
    87  		_, err = templateSet.New("errors/" + view).Parse(string(data))
    88  		if err != nil {
    89  			utils.Logger.Error("Unable to parse template file", path)
    90  		}
    91  		return
    92  	}
    93  
    94  	target := []string{seekViewOnPath("500.html"), seekViewOnPath("500-dev.html")}
    95  	if !utils.Exists(target[0]) {
    96  		fmt.Fprintf(iw, "Target template not found not found %s<br />\n", target[0])
    97  		fmt.Fprintf(iw, "An error occurred %s", err.Error())
    98  		return
    99  	}
   100  
   101  	var revelError *utils.SourceError
   102  
   103  	if !errors.As(err, &revelError) {
   104  		revelError = &utils.SourceError{
   105  			Title:       "Server Error",
   106  			Description: err.Error(),
   107  		}
   108  	}
   109  
   110  	if revelError == nil {
   111  		panic("no error provided")
   112  	}
   113  
   114  	viewArgs := map[string]interface{}{}
   115  	viewArgs["RunMode"] = h.paths.RunMode
   116  	viewArgs["DevMode"] = h.paths.DevMode
   117  	viewArgs["Error"] = revelError
   118  
   119  	// Render the template from the file
   120  	err = templateSet.ExecuteTemplate(iw, "errors/500.html", viewArgs)
   121  	if err != nil {
   122  		utils.Logger.Error("Failed to execute", "error", err)
   123  	}
   124  }
   125  
   126  // ServeHTTP handles all requests.
   127  // It checks for changes to app, rebuilds if necessary, and forwards the request.
   128  func (h *Harness) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   129  	// Don't rebuild the app for favicon requests.
   130  	if lastRequestHadError > 0 && r.URL.Path == "/favicon.ico" {
   131  		return
   132  	}
   133  
   134  	// Flush any change events and rebuild app if necessary.
   135  	// Render an error page if the rebuild / restart failed.
   136  	err := h.watcher.Notify()
   137  	if err != nil {
   138  		// In a thread safe manner update the flag so that a request for
   139  		// /favicon.ico does not trigger a rebuild
   140  		atomic.CompareAndSwapInt32(&lastRequestHadError, 0, 1)
   141  		h.renderError(w, r, err)
   142  		return
   143  	}
   144  
   145  	// In a thread safe manner update the flag so that a request for
   146  	// /favicon.ico is allowed
   147  	atomic.CompareAndSwapInt32(&lastRequestHadError, 1, 0)
   148  
   149  	// Reverse proxy the request.
   150  	// (Need special code for websockets, courtesy of bradfitz)
   151  	if strings.EqualFold(r.Header.Get("Upgrade"), "websocket") {
   152  		h.proxyWebsocket(w, r, h.serverHost)
   153  	} else {
   154  		h.proxy.ServeHTTP(w, r)
   155  	}
   156  }
   157  
   158  // NewHarness method returns a reverse proxy that forwards requests
   159  // to the given port.
   160  func NewHarness(c *model.CommandConfig, paths *model.RevelContainer, runMode string, noProxy bool) *Harness {
   161  	// Get a template loader to render errors.
   162  	// Prefer the app's views/errors directory, and fall back to the stock error pages.
   163  	// revel.MainTemplateLoader = revel.NewTemplateLoader(
   164  	//	[]string{filepath.Join(revel.RevelPath, "templates")})
   165  	// if err := revel.MainTemplateLoader.Refresh(); err != nil {
   166  	//	revel.RevelLog.Error("Template loader error", "error", err)
   167  	// }
   168  
   169  	addr := paths.HTTPAddr
   170  	port := paths.Config.IntDefault("harness.port", 0)
   171  	scheme := "http"
   172  
   173  	if paths.HTTPSsl {
   174  		scheme = "https"
   175  	}
   176  
   177  	// If the server is running on the wildcard address, use "localhost"
   178  	if addr == "" {
   179  		utils.Logger.Warn("No http.addr specified in the app.conf listening on localhost interface only. " +
   180  			"This will not allow external access to your application")
   181  		addr = "localhost"
   182  	}
   183  
   184  	if port == 0 {
   185  		port = getFreePort()
   186  	}
   187  
   188  	serverURL, _ := url.ParseRequestURI(fmt.Sprintf(scheme+"://%s:%d", addr, port))
   189  
   190  	serverHarness := &Harness{
   191  		port:       port,
   192  		serverHost: serverURL.String()[len(scheme+"://"):],
   193  		proxy:      httputil.NewSingleHostReverseProxy(serverURL),
   194  		mutex:      &sync.Mutex{},
   195  		paths:      paths,
   196  		useProxy:   !noProxy,
   197  		config:     c,
   198  		runMode:    runMode,
   199  	}
   200  
   201  	if paths.HTTPSsl {
   202  		serverHarness.proxy.Transport = &http.Transport{
   203  			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
   204  		}
   205  	}
   206  	return serverHarness
   207  }
   208  
   209  // Refresh method rebuilds the Revel application and run it on the given port.
   210  // called by the watcher.
   211  func (h *Harness) Refresh() (err *utils.SourceError) {
   212  	t := time.Now()
   213  	fmt.Println("Change detected, recompiling")
   214  	err = h.refresh()
   215  	if err != nil && !h.ranOnce && h.useProxy {
   216  		addr := fmt.Sprintf("%s:%d", h.paths.HTTPAddr, h.paths.HTTPPort)
   217  
   218  		fmt.Printf("\nError compiling code, to view error details see proxy running on http://%s\n\n", addr)
   219  	}
   220  
   221  	h.ranOnce = true
   222  	fmt.Printf("\nTime to recompile %s\n", time.Since(t).String())
   223  	return
   224  }
   225  
   226  func (h *Harness) refresh() (err *utils.SourceError) {
   227  	// Allow only one thread to rebuild the process
   228  	// If multiple requests to rebuild are queued only the last one is executed on
   229  	// So before a build is started we wait for a second to determine if
   230  	// more requests for a build are triggered.
   231  	// Once no more requests are triggered the build will be processed
   232  	h.mutex.Lock()
   233  	defer h.mutex.Unlock()
   234  
   235  	if h.app != nil {
   236  		h.app.Kill()
   237  	}
   238  
   239  	utils.Logger.Info("Rebuild Called")
   240  	var newErr error
   241  	h.app, newErr = Build(h.config, h.paths)
   242  	if newErr != nil {
   243  		utils.Logger.Error("Build detected an error", "error", newErr)
   244  
   245  		var castErr *utils.SourceError
   246  		if errors.As(newErr, &castErr) {
   247  			return castErr
   248  		}
   249  
   250  		err = &utils.SourceError{
   251  			Title:       "App failed to start up",
   252  			Description: newErr.Error(),
   253  		}
   254  
   255  		return
   256  	}
   257  
   258  	if h.useProxy {
   259  		h.app.Port = h.port
   260  		runMode := h.runMode
   261  
   262  		if !h.config.HistoricMode {
   263  			// Recalulate run mode based on the config
   264  			var paths []byte
   265  			if len(h.app.PackagePathMap) > 0 {
   266  				paths, _ = json.Marshal(h.app.PackagePathMap)
   267  			}
   268  
   269  			runMode = fmt.Sprintf(`{"mode":"%s", "specialUseFlag":%v,"packagePathMap":%s}`, h.app.Paths.RunMode, h.config.GetVerbose(), string(paths))
   270  		}
   271  
   272  		if err2 := h.app.Cmd(runMode).Start(h.config); err2 != nil {
   273  			utils.Logger.Error("Could not start application", "error", err2)
   274  
   275  			var serr *utils.SourceError
   276  			if errors.As(err2, &serr) {
   277  				return err
   278  			}
   279  
   280  			return &utils.SourceError{
   281  				Title:       "App failed to start up",
   282  				Description: err2.Error(),
   283  			}
   284  		}
   285  	} else {
   286  		h.app = nil
   287  	}
   288  
   289  	return
   290  }
   291  
   292  // WatchDir method returns false to file matches with doNotWatch
   293  // otheriwse true.
   294  func (h *Harness) WatchDir(info os.FileInfo) bool {
   295  	return !utils.ContainsString(doNotWatch, info.Name())
   296  }
   297  
   298  // WatchFile method returns true given filename HasSuffix of ".go"
   299  // otheriwse false - implements revel.DiscerningListener.
   300  func (h *Harness) WatchFile(filename string) bool {
   301  	return strings.HasSuffix(filename, ".go")
   302  }
   303  
   304  // Run the harness, which listens for requests and proxies them to the app
   305  // server, which it runs and rebuilds as necessary.
   306  func (h *Harness) Run() {
   307  	var paths []string
   308  	if h.paths.Config.BoolDefault("watch.gopath", false) {
   309  		gopaths := filepath.SplitList(build.Default.GOPATH)
   310  		paths = append(paths, gopaths...)
   311  	}
   312  	paths = append(paths, h.paths.CodePaths...)
   313  	h.watcher = watcher.NewWatcher(h.paths, false)
   314  	h.watcher.Listen(h, paths...)
   315  
   316  	go func() {
   317  		if err := h.Refresh(); err != nil {
   318  			utils.Logger.Error("Failed to refresh", "error", err)
   319  		}
   320  	}()
   321  
   322  	if h.useProxy {
   323  		go func() {
   324  			// Check the port to start on a random port
   325  			if h.paths.HTTPPort == 0 {
   326  				h.paths.HTTPPort = getFreePort()
   327  			}
   328  			addr := fmt.Sprintf("%s:%d", h.paths.HTTPAddr, h.paths.HTTPPort)
   329  			utils.Logger.Infof("Proxy server is listening on %s", addr)
   330  			var err error
   331  			if h.paths.HTTPSsl {
   332  				err = http.ListenAndServeTLS(
   333  					addr,
   334  					h.paths.HTTPSslCert,
   335  					h.paths.HTTPSslKey,
   336  					h)
   337  			} else {
   338  				err = http.ListenAndServe(addr, h)
   339  			}
   340  			if err != nil {
   341  				utils.Logger.Error("Failed to start reverse proxy:", "error", err)
   342  			}
   343  		}()
   344  	}
   345  
   346  	// Make a new channel to listen for the interrupt event
   347  	ch := make(chan os.Signal)
   348  	//nolint:staticcheck // os.Kill ineffective on Unix, useful on Windows?
   349  	signal.Notify(ch, os.Interrupt, os.Kill)
   350  	<-ch
   351  	// Kill the app and exit
   352  	if h.app != nil {
   353  		h.app.Kill()
   354  	}
   355  	os.Exit(1)
   356  }
   357  
   358  // Find an unused port.
   359  func getFreePort() (port int) {
   360  	conn, err := net.Listen("tcp", ":0")
   361  	if err != nil {
   362  		utils.Logger.Fatal("Unable to fetch a freee port address", "error", err)
   363  	}
   364  
   365  	port = conn.Addr().(*net.TCPAddr).Port
   366  	err = conn.Close()
   367  	if err != nil {
   368  		utils.Logger.Fatal("Unable to close port", "error", err)
   369  	}
   370  	return port
   371  }
   372  
   373  // proxyWebsocket copies data between websocket client and server until one side
   374  // closes the connection.  (ReverseProxy doesn't work with websocket requests.)
   375  func (h *Harness) proxyWebsocket(w http.ResponseWriter, r *http.Request, host string) {
   376  	var (
   377  		d   net.Conn
   378  		err error
   379  	)
   380  	if h.paths.HTTPSsl {
   381  		// since this proxy isn't used in production,
   382  		// it's OK to set InsecureSkipVerify to true
   383  		// no need to add another configuration option.
   384  		d, err = tls.Dial("tcp", host, &tls.Config{InsecureSkipVerify: true})
   385  	} else {
   386  		d, err = net.Dial("tcp", host)
   387  	}
   388  	if err != nil {
   389  		http.Error(w, "Error contacting backend server.", 500)
   390  		utils.Logger.Error("Error dialing websocket backend ", "host", host, "error", err)
   391  		return
   392  	}
   393  	hj, ok := w.(http.Hijacker)
   394  	if !ok {
   395  		http.Error(w, "Not a hijacker?", 500)
   396  		return
   397  	}
   398  	nc, _, err := hj.Hijack()
   399  	if err != nil {
   400  		utils.Logger.Error("Hijack error", "error", err)
   401  		return
   402  	}
   403  	defer func() {
   404  		if err = nc.Close(); err != nil {
   405  			utils.Logger.Error("Connection close error", "error", err)
   406  		}
   407  		if err = d.Close(); err != nil {
   408  			utils.Logger.Error("Dial close error", "error", err)
   409  		}
   410  	}()
   411  
   412  	err = r.Write(d)
   413  	if err != nil {
   414  		utils.Logger.Error("Error copying request to target", "error", err)
   415  		return
   416  	}
   417  
   418  	errc := make(chan error, 2)
   419  	cp := func(dst io.Writer, src io.Reader) {
   420  		_, err := io.Copy(dst, src)
   421  		errc <- err
   422  	}
   423  	go cp(d, nc)
   424  	go cp(nc, d)
   425  	<-errc
   426  }