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