Merge "server: restart camlistored from the /status handler"

This commit is contained in:
Brad Fitzpatrick 2014-12-24 20:10:58 +00:00 committed by Gerrit Code Review
commit 63a0e5d9fe
3 changed files with 137 additions and 0 deletions

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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("<html><head><title>Status</title></head>")
f("<body><h2>Status</h2>")
f("<form method='post' action='restart' onsubmit='return confirm(\"Really restart now?\")'><button>restart server</button></form>")
f("<p>As JSON: <a href='status.json'>status.json</a>; and the <a href='%s?camli.mode=config'>discovery JSON</a>.</p>", st.rootPrefix)
f("<p>Not yet pretty HTML UI:</p>")
js, err := json.MarshalIndent(st, "", " ")
@ -224,3 +231,34 @@ func (sh *StatusHandler) serveStatusHTML(rw http.ResponseWriter, req *http.Reque
})
f("<pre>%s</pre>", 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()
}