Skip to content

Commit

Permalink
cmd/go/internal/vcweb: simplify hgHandler cancellation
Browse files Browse the repository at this point in the history
This uses the new Cancel and WaitDelay fields of os/exec.Cmd
(added in #50436) to interrupt or kill the 'hg serve' command
when its incoming http.Request is canceled.

This should keep the vcweb hg handler from getting stuck if 'hg serve'
hangs after the request either completes or is canceled.

Fixes #57597 (maybe).

Change-Id: I53cf58e8ab953fd48c0c37f596f99e885a036d9b
Reviewed-on: https://go-review.googlesource.com/c/go/+/460997
Reviewed-by: Ian Lance Taylor <iant@google.com>
Run-TryBot: Bryan Mills <bcmills@google.com>
Auto-Submit: Bryan Mills <bcmills@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
  • Loading branch information
Bryan C. Mills authored and gopherbot committed Jan 19, 2023
1 parent e08642c commit 24a9d7b
Showing 1 changed file with 39 additions and 38 deletions.
77 changes: 39 additions & 38 deletions src/cmd/go/internal/vcweb/hg.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package vcweb

import (
"bufio"
"context"
"errors"
"io"
"log"
Expand All @@ -16,6 +17,7 @@ import (
"os/exec"
"strings"
"sync"
"time"
)

type hgHandler struct {
Expand Down Expand Up @@ -47,10 +49,25 @@ func (h *hgHandler) Handler(dir string, env []string, logger *log.Logger) (http.
// if "hg" works at all then "hg serve" works too, and we'll execute that as
// a subprocess, using a reverse proxy to forward the request and response.

cmd := exec.Command(h.hgPath, "serve", "--port", "0", "--address", "localhost", "--accesslog", os.DevNull, "--name", "vcweb", "--print-url")
ctx, cancel := context.WithCancel(req.Context())
defer cancel()

cmd := exec.CommandContext(ctx, h.hgPath, "serve", "--port", "0", "--address", "localhost", "--accesslog", os.DevNull, "--name", "vcweb", "--print-url")
cmd.Dir = dir
cmd.Env = append(env[:len(env):len(env)], "PWD="+dir)

cmd.Cancel = func() error {
err := cmd.Process.Signal(os.Interrupt)
if err != nil && !errors.Is(err, os.ErrProcessDone) {
err = cmd.Process.Kill()
}
return err
}
// This WaitDelay is arbitrary. After 'hg serve' prints its URL, any further
// I/O is only for debugging. (The actual output goes through the HTTP URL,
// not the standard I/O streams.)
cmd.WaitDelay = 10 * time.Second

stderr := new(strings.Builder)
cmd.Stderr = stderr

Expand All @@ -59,62 +76,46 @@ func (h *hgHandler) Handler(dir string, env []string, logger *log.Logger) (http.
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
readDone := make(chan struct{})
defer func() {
stdout.Close()
<-readDone
}()

hgURL := make(chan *url.URL, 1)
hgURLError := make(chan error, 1)
go func() {
defer close(readDone)
r := bufio.NewReader(stdout)
for {
line, err := r.ReadString('\n')
if err != nil {
return
}
u, err := url.Parse(strings.TrimSpace(line))
if err == nil {
hgURL <- u
} else {
hgURLError <- err
}
break
}
io.Copy(io.Discard, r)
}()

if err := cmd.Start(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var wg sync.WaitGroup
defer func() {
if err := cmd.Process.Signal(os.Interrupt); err != nil && !errors.Is(err, os.ErrProcessDone) {
cmd.Process.Kill()
}
cancel()
err := cmd.Wait()
if out := strings.TrimSuffix(stderr.String(), "interrupted!\n"); out != "" {
logger.Printf("%v: %v\n%s", cmd, err, out)
} else {
logger.Printf("%v", cmd)
}
wg.Wait()
}()

select {
case <-req.Context().Done():
logger.Printf("%v: %v", req.Context().Err(), cmd)
http.Error(w, req.Context().Err().Error(), http.StatusBadGateway)
r := bufio.NewReader(stdout)
line, err := r.ReadString('\n')
if err != nil {
return
case err := <-hgURLError:
}
// We have read what should be the server URL. 'hg serve' shouldn't need to
// write anything else to stdout, but it's not a big deal if it does anyway.
// Keep the stdout pipe open so that 'hg serve' won't get a SIGPIPE, but
// actively discard its output so that it won't hang on a blocking write.
wg.Add(1)
go func() {
io.Copy(io.Discard, r)
wg.Done()
}()

u, err := url.Parse(strings.TrimSpace(line))
if err != nil {
logger.Printf("%v: %v", cmd, err)
http.Error(w, err.Error(), http.StatusBadGateway)
return
case url := <-hgURL:
logger.Printf("proxying hg request to %s", url)
httputil.NewSingleHostReverseProxy(url).ServeHTTP(w, req)
}
logger.Printf("proxying hg request to %s", u)
httputil.NewSingleHostReverseProxy(u).ServeHTTP(w, req)
})

return handler, nil
Expand Down

0 comments on commit 24a9d7b

Please sign in to comment.