diff --git a/pkg/server/app/app.go b/pkg/server/app/app.go index 5b0fe53a9..215ece411 100644 --- a/pkg/server/app/app.go +++ b/pkg/server/app/app.go @@ -19,6 +19,7 @@ limitations under the License. package app import ( + "errors" "fmt" "log" "net" @@ -29,6 +30,7 @@ import ( "os/exec" "path/filepath" "strings" + "time" "camlistore.org/pkg/auth" camhttputil "camlistore.org/pkg/httputil" @@ -46,6 +48,8 @@ type Handler struct { proxy *httputil.ReverseProxy // For redirecting requests to the app. backendURL string // URL that we proxy to (i.e. base URL of the app). + + process *os.Process // The app's Pid. To send it signals on restart, etc. } func (a *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { @@ -217,6 +221,7 @@ func (a *Handler) Start() error { if err := cmd.Start(); err != nil { return fmt.Errorf("could not start app %v: %v", name, err) } + a.process = cmd.Process return nil } @@ -243,3 +248,27 @@ func (a *Handler) AppConfig() map[string]interface{} { func (a *Handler) BackendURL() string { return a.backendURL } + +var errProcessTookTooLong = errors.New("proccess took too long to quit") + +// Quit sends the app's process a SIGINT, and waits up to 5 seconds for it +// to exit, returning an error if it doesn't. +func (a *Handler) Quit() error { + err := a.process.Signal(os.Interrupt) + if err != nil { + return err + } + + c := make(chan error) + go func() { + _, err := a.process.Wait() + c <- err + }() + select { + case err = <-c: + case <-time.After(5 * time.Second): + // TODO Do we want to SIGKILL here or just leave the app alone? + err = errProcessTookTooLong + } + return err +} diff --git a/pkg/server/app/app_test.go b/pkg/server/app/app_test.go index ce7131f0b..019d0174f 100644 --- a/pkg/server/app/app_test.go +++ b/pkg/server/app/app_test.go @@ -17,6 +17,11 @@ limitations under the License. package app import ( + "bufio" + "fmt" + "io" + "os" + "os/exec" "regexp" "testing" ) @@ -148,3 +153,68 @@ func TestRandPortBackendURL(t *testing.T) { } } } + +// We just want a helper command that ignores SIGINT. +func ignoreInterrupt() (*os.Process, error) { + script := `trap "echo hello" SIGINT +echo READY +sleep 10000` + cmd := exec.Command("bash") + + w, err := cmd.StdinPipe() + if err != nil { + return nil, fmt.Errorf("couldn't get pipe for helper shell") + } + go io.WriteString(w, script) + + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, fmt.Errorf("couldn't get pipe for helper shell") + } + + err = cmd.Start() + if err != nil { + return nil, fmt.Errorf("couldn't start helper shell") + } + + r := bufio.NewReader(stdout) + l, err := r.ReadBytes('\n') + if err != nil { + return nil, fmt.Errorf("couldn't read from helper shell") + } + if string(l) != "READY\n" { + return nil, fmt.Errorf("unexpected output from helper shell script") + } + return cmd.Process, nil +} + +func TestQuit(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } + + cmd := exec.Command("sleep", "10000") + err := cmd.Start() + if err != nil { + t.Skip("couldn't run test helper command") + } + h := Handler{ + process: cmd.Process, + } + err = h.Quit() + if err != nil { + t.Errorf("got %v, wanted %v", err, nil) + } + + pid, err := ignoreInterrupt() + if err != nil { + t.Skip("couldn't run test helper command: %v", err) + } + h = Handler{ + process: pid, + } + err = h.Quit() + if err != errProcessTookTooLong { + t.Errorf("got %v, wanted %v", err, errProcessTookTooLong) + } +} diff --git a/pkg/server/status.go b/pkg/server/status.go index 34f294b46..c578bb854 100644 --- a/pkg/server/status.go +++ b/pkg/server/status.go @@ -33,7 +33,9 @@ import ( "camlistore.org/pkg/httputil" "camlistore.org/pkg/index" "camlistore.org/pkg/jsonconfig" + "camlistore.org/pkg/osutil" "camlistore.org/pkg/search" + "camlistore.org/pkg/server/app" "camlistore.org/pkg/types/camtypes" ) @@ -86,6 +88,10 @@ func (sh *StatusHandler) InitHandler(hl blobserver.FindHandlerByTyper) error { func (sh *StatusHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { suffix := httputil.PathSuffix(req) + if suffix == "restart" { + sh.serveRestart(rw, req) + return + } if !httputil.IsGet(req) { http.Error(rw, "Illegal status method.", http.StatusMethodNotAllowed) return @@ -208,6 +214,7 @@ func (sh *StatusHandler) serveStatusHTML(rw http.ResponseWriter, req *http.Reque } f("
As JSON: status.json; and the discovery JSON.
", st.rootPrefix) f("Not yet pretty HTML UI:
") js, err := json.MarshalIndent(st, "", " ") @@ -224,3 +231,34 @@ func (sh *StatusHandler) serveStatusHTML(rw http.ResponseWriter, req *http.Reque }) f("%s", jsh) } + +func (sh *StatusHandler) serveRestart(rw http.ResponseWriter, req *http.Request) { + if req.Method != "POST" { + http.Error(rw, "POST to restart", http.StatusMethodNotAllowed) + return + } + + _, handlers := sh.handlerFinder.AllHandlers() + for _, h := range handlers { + ah, ok := h.(*app.Handler) + if !ok { + continue + } + log.Printf("Sending SIGINT to %s", ah.ProgramName()) + err := ah.Quit() + if err != nil { + msg := fmt.Sprintf("Not restarting: couldn't interrupt app %s: %v", ah.ProgramName(), err) + log.Printf(msg) + http.Error(rw, msg, http.StatusInternalServerError) + return + } + } + + log.Println("Restarting camlistored") + rw.Header().Set("Connection", "close") + http.Redirect(rw, req, sh.prefix, http.StatusFound) + if f, ok := rw.(http.Flusher); ok { + f.Flush() + } + osutil.RestartProcess() +}