github.com/terhitormanen/cmd@v1.1.4/harness/app.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 6 7 import ( 8 "bytes" 9 "fmt" 10 "io" 11 "os" 12 "os/exec" 13 "sync" 14 "sync/atomic" 15 "time" 16 17 "github.com/terhitormanen/cmd/model" 18 "github.com/terhitormanen/cmd/utils" 19 ) 20 21 // Error is used for constant errors. 22 type Error string 23 24 // Error implements the error interface. 25 func (e Error) Error() string { 26 return string(e) 27 } 28 29 const ErrTimedOut Error = "app timed out" 30 31 // App contains the configuration for running a Revel app. (Not for the app itself) 32 // Its only purpose is constructing the command to execute. 33 type App struct { 34 BinaryPath string // Path to the app executable 35 Port int // Port to pass as a command line argument. 36 cmd AppCmd // The last cmd returned. 37 PackagePathMap map[string]string // Package to directory path map 38 Paths *model.RevelContainer 39 } 40 41 // NewApp returns app instance with binary path in it. 42 func NewApp(binPath string, paths *model.RevelContainer, packagePathMap map[string]string) *App { 43 return &App{BinaryPath: binPath, Paths: paths, Port: paths.HTTPPort, PackagePathMap: packagePathMap} 44 } 45 46 // Cmd returns a command to run the app server using the current configuration. 47 func (a *App) Cmd(runMode string) AppCmd { 48 a.cmd = NewAppCmd(a.BinaryPath, a.Port, runMode, a.Paths) 49 return a.cmd 50 } 51 52 // Kill the last app command returned. 53 func (a *App) Kill() { 54 a.cmd.Kill() 55 } 56 57 // AppCmd manages the running of a Revel app server. 58 // It requires revel.Init to have been called previously. 59 type AppCmd struct { 60 *exec.Cmd 61 } 62 63 // NewAppCmd returns the AppCmd with parameters initialized for running app. 64 func NewAppCmd(binPath string, port int, runMode string, paths *model.RevelContainer) AppCmd { 65 cmd := exec.Command(binPath, 66 fmt.Sprintf("-port=%d", port), 67 fmt.Sprintf("-importPath=%s", paths.ImportPath), 68 fmt.Sprintf("-runMode=%s", runMode)) 69 cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr 70 return AppCmd{cmd} 71 } 72 73 // Start the app server, and wait until it is ready to serve requests. 74 func (cmd AppCmd) Start(c *model.CommandConfig) error { 75 listeningWriter := &startupListeningWriter{os.Stdout, make(chan bool), c, &bytes.Buffer{}} 76 cmd.Stdout = listeningWriter 77 cmd.Stderr = listeningWriter 78 utils.CmdInit(cmd.Cmd, !c.Vendored, c.AppPath) 79 utils.Logger.Info("Exec app:", "path", cmd.Path, "args", cmd.Args, "dir", cmd.Dir, "env", cmd.Env) 80 if err := cmd.Cmd.Start(); err != nil { 81 utils.Logger.Fatal("Error running:", "error", err) 82 } 83 84 select { 85 case exitState := <-cmd.waitChan(): 86 fmt.Println("Startup failure view previous messages, \n Proxy is listening :", c.Run.Port) 87 err := utils.NewError("", "Revel Run Error", "starting your application there was an exception. See terminal output, "+exitState, "") 88 atomic.SwapInt32(&startupError, 1) 89 // TODO pretiffy command line output 90 err.Stack = listeningWriter.buffer.String() 91 return err 92 93 case <-time.After(60 * time.Second): 94 println("Revel proxy is listening, point your browser to :", c.Run.Port) 95 utils.Logger.Error("Killing revel server process did not respond after wait timeout.", "processid", cmd.Process.Pid) 96 cmd.Kill() 97 98 return fmt.Errorf("revel/harness: %w", ErrTimedOut) 99 100 case <-listeningWriter.notifyReady: 101 println("Revel proxy is listening, point your browser to :", c.Run.Port) 102 return nil 103 } 104 } 105 106 // Run the app server inline. Never returns. 107 func (cmd AppCmd) Run(c *model.CommandConfig) { 108 utils.CmdInit(cmd.Cmd, !c.Vendored, c.AppPath) 109 utils.Logger.Info("Exec app:", "path", cmd.Path, "args", cmd.Args) 110 if err := cmd.Cmd.Run(); err != nil { 111 utils.Logger.Fatal("Error running:", "error", err) 112 } 113 } 114 115 // Kill terminates the app server if it's running. 116 func (cmd AppCmd) Kill() { 117 if cmd.Cmd != nil && (cmd.ProcessState == nil || !cmd.ProcessState.Exited()) { 118 // Windows appears to send the kill to all threads, shutting down the 119 // server before this can, this check will ensure the process is still running 120 if _, err := os.FindProcess(cmd.Process.Pid); err != nil { 121 // Server has already exited 122 utils.Logger.Info("Server not running revel server pid", "pid", cmd.Process.Pid) 123 return 124 } 125 126 // Wait for the shutdown channel 127 waitMutex := &sync.WaitGroup{} 128 waitMutex.Add(1) 129 ch := make(chan bool, 1) 130 go func() { 131 waitMutex.Done() 132 s, err := cmd.Process.Wait() 133 defer func() { 134 ch <- true 135 }() 136 if err != nil { 137 utils.Logger.Info("Wait failed for process ", "error", err) 138 } 139 if s != nil { 140 utils.Logger.Info("Revel App exited", "state", s.String()) 141 } 142 }() 143 // Wait for the channel to begin waiting 144 waitMutex.Wait() 145 146 // Send an interrupt signal to allow for a graceful shutdown 147 utils.Logger.Info("Killing revel server pid", "pid", cmd.Process.Pid) 148 149 err := cmd.Process.Signal(os.Interrupt) 150 151 if err != nil { 152 utils.Logger.Info( 153 "Revel app already exited.", 154 "processid", cmd.Process.Pid, "error", err, 155 "killerror", cmd.Process.Kill()) 156 return 157 } 158 159 // Use a timer to ensure that the process exits 160 utils.Logger.Info("Waiting to exit") 161 select { 162 case <-ch: 163 return 164 case <-time.After(60 * time.Second): 165 // Kill the process 166 utils.Logger.Error( 167 "Revel app failed to exit in 60 seconds - killing.", 168 "processid", cmd.Process.Pid, 169 "killerror", cmd.Process.Kill()) 170 } 171 172 utils.Logger.Info("Done Waiting to exit") 173 } 174 } 175 176 // Return a channel that is notified when Wait() returns. 177 func (cmd AppCmd) waitChan() <-chan string { 178 ch := make(chan string, 1) 179 go func() { 180 _ = cmd.Wait() 181 state := cmd.ProcessState 182 exitStatus := " unknown " 183 if state != nil { 184 exitStatus = state.String() 185 } 186 187 ch <- exitStatus 188 }() 189 return ch 190 } 191 192 // A io.Writer that copies to the destination, and listens for "Revel engine is listening on.." 193 // in the stream. (Which tells us when the revel server has finished starting up) 194 // This is super ghetto, but by far the simplest thing that should work. 195 type startupListeningWriter struct { 196 dest io.Writer 197 notifyReady chan bool 198 c *model.CommandConfig 199 buffer *bytes.Buffer 200 } 201 202 // Writes to this output stream. 203 func (w *startupListeningWriter) Write(p []byte) (int, error) { 204 if w.notifyReady != nil && bytes.Contains(p, []byte("Revel engine is listening on")) { 205 w.notifyReady <- true 206 w.notifyReady = nil 207 } 208 if w.c.HistoricMode { 209 if w.notifyReady != nil && bytes.Contains(p, []byte("Listening on")) { 210 w.notifyReady <- true 211 w.notifyReady = nil 212 } 213 } 214 if w.notifyReady != nil { 215 w.buffer.Write(p) 216 } 217 return w.dest.Write(p) 218 }