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  }