Files
revel-cmd/harness/app.go
Julian Daube 7a91d0ca0b interrupt process on windows as well
On Windows, the killing of running services relies on the
60 second kill timeout, making it faster to restart the harness
to force rebuilding.

Since os.Interrupt works for me, remove the runtime os check in
harness/app.go completely.
2021-02-07 00:30:16 +01:00

206 lines
6.2 KiB
Go

// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
// Revel Framework source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
package harness
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"os/exec"
"sync"
"time"
"github.com/revel/cmd/model"
"github.com/revel/cmd/utils"
)
// App contains the configuration for running a Revel app. (Not for the app itself)
// Its only purpose is constructing the command to execute.
type App struct {
BinaryPath string // Path to the app executable
Port int // Port to pass as a command line argument.
cmd AppCmd // The last cmd returned.
PackagePathMap map[string]string // Package to directory path map
Paths *model.RevelContainer
}
// NewApp returns app instance with binary path in it.
func NewApp(binPath string, paths *model.RevelContainer, packagePathMap map[string]string) *App {
return &App{BinaryPath: binPath, Paths: paths, Port: paths.HTTPPort, PackagePathMap: packagePathMap}
}
// Cmd returns a command to run the app server using the current configuration.
func (a *App) Cmd(runMode string) AppCmd {
a.cmd = NewAppCmd(a.BinaryPath, a.Port, runMode, a.Paths)
return a.cmd
}
// Kill the last app command returned.
func (a *App) Kill() {
a.cmd.Kill()
}
// AppCmd manages the running of a Revel app server.
// It requires revel.Init to have been called previously.
type AppCmd struct {
*exec.Cmd
}
// NewAppCmd returns the AppCmd with parameters initialized for running app.
func NewAppCmd(binPath string, port int, runMode string, paths *model.RevelContainer) AppCmd {
cmd := exec.Command(binPath,
fmt.Sprintf("-port=%d", port),
fmt.Sprintf("-importPath=%s", paths.ImportPath),
fmt.Sprintf("-runMode=%s", runMode))
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
return AppCmd{cmd}
}
// Start the app server, and wait until it is ready to serve requests.
func (cmd AppCmd) Start(c *model.CommandConfig) error {
listeningWriter := &startupListeningWriter{os.Stdout, make(chan bool), c, &bytes.Buffer{}}
cmd.Stdout = listeningWriter
utils.CmdInit(cmd.Cmd, !c.Vendored, c.AppPath)
utils.Logger.Info("Exec app:", "path", cmd.Path, "args", cmd.Args, "dir", cmd.Dir, "env", cmd.Env)
if err := cmd.Cmd.Start(); err != nil {
utils.Logger.Fatal("Error running:", "error", err)
}
select {
case exitState := <-cmd.waitChan():
fmt.Println("Startup failure view previous messages, \n Proxy is listening :", c.Run.Port)
err := utils.NewError("", "Revel Run Error", "starting your application there was an exception. See terminal output, "+exitState, "")
// TODO pretiffy command line output
// err.MetaError = listeningWriter.getLastOutput()
return err
case <-time.After(60 * time.Second):
println("Revel proxy is listening, point your browser to :", c.Run.Port)
utils.Logger.Error("Killing revel server process did not respond after wait timeout.", "processid", cmd.Process.Pid)
cmd.Kill()
return errors.New("revel/harness: app timed out")
case <-listeningWriter.notifyReady:
println("Revel proxy is listening, point your browser to :", c.Run.Port)
return nil
}
}
// Run the app server inline. Never returns.
func (cmd AppCmd) Run(c *model.CommandConfig) {
utils.CmdInit(cmd.Cmd, !c.Vendored, c.AppPath)
utils.Logger.Info("Exec app:", "path", cmd.Path, "args", cmd.Args)
if err := cmd.Cmd.Run(); err != nil {
utils.Logger.Fatal("Error running:", "error", err)
}
}
// Kill terminates the app server if it's running.
func (cmd AppCmd) Kill() {
if cmd.Cmd != nil && (cmd.ProcessState == nil || !cmd.ProcessState.Exited()) {
// Windows appears to send the kill to all threads, shutting down the
// server before this can, this check will ensure the process is still running
if _, err := os.FindProcess(cmd.Process.Pid); err != nil {
// Server has already exited
utils.Logger.Info("Server not running revel server pid", "pid", cmd.Process.Pid)
return
}
// Wait for the shutdown channel
waitMutex := &sync.WaitGroup{}
waitMutex.Add(1)
ch := make(chan bool, 1)
go func() {
waitMutex.Done()
s, err := cmd.Process.Wait()
defer func() {
ch <- true
}()
if err != nil {
utils.Logger.Info("Wait failed for process ", "error", err)
}
if s != nil {
utils.Logger.Info("Revel App exited", "state", s.String())
}
}()
// Wait for the channel to begin waiting
waitMutex.Wait()
// Send an interrupt signal to allow for a graceful shutdown
utils.Logger.Info("Killing revel server pid", "pid", cmd.Process.Pid)
err := cmd.Process.Signal(os.Interrupt)
if err != nil {
utils.Logger.Info(
"Revel app already exited.",
"processid", cmd.Process.Pid, "error", err,
"killerror", cmd.Process.Kill())
return
}
// Use a timer to ensure that the process exits
utils.Logger.Info("Waiting to exit")
select {
case <-ch:
return
case <-time.After(60 * time.Second):
// Kill the process
utils.Logger.Error(
"Revel app failed to exit in 60 seconds - killing.",
"processid", cmd.Process.Pid,
"killerror", cmd.Process.Kill())
}
utils.Logger.Info("Done Waiting to exit")
}
}
// Return a channel that is notified when Wait() returns.
func (cmd AppCmd) waitChan() <-chan string {
ch := make(chan string, 1)
go func() {
_ = cmd.Wait()
state := cmd.ProcessState
exitStatus := " unknown "
if state != nil {
exitStatus = state.String()
}
ch <- exitStatus
}()
return ch
}
// A io.Writer that copies to the destination, and listens for "Revel engine is listening on.."
// in the stream. (Which tells us when the revel server has finished starting up)
// This is super ghetto, but by far the simplest thing that should work.
type startupListeningWriter struct {
dest io.Writer
notifyReady chan bool
c *model.CommandConfig
buffer *bytes.Buffer
}
// Writes to this output stream.
func (w *startupListeningWriter) Write(p []byte) (int, error) {
if w.notifyReady != nil && bytes.Contains(p, []byte("Revel engine is listening on")) {
w.notifyReady <- true
w.notifyReady = nil
}
if w.c.HistoricMode {
if w.notifyReady != nil && bytes.Contains(p, []byte("Listening on")) {
w.notifyReady <- true
w.notifyReady = nil
}
}
if w.notifyReady != nil {
w.buffer.Write(p)
}
return w.dest.Write(p)
}