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 }