mirror of https://github.com/perkeep/perkeep.git
Merge "server: restart camlistored from the /status handler"
This commit is contained in:
commit
63a0e5d9fe
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue