1
2
3
4
5 package vcweb
6
7 import (
8 "bufio"
9 "context"
10 "errors"
11 "io"
12 "log"
13 "net/http"
14 "net/http/httputil"
15 "net/url"
16 "os"
17 "os/exec"
18 "slices"
19 "strings"
20 "sync"
21 "time"
22 )
23
24 type hgHandler struct {
25 once sync.Once
26 hgPath string
27 hgPathErr error
28 }
29
30 func (h *hgHandler) Available() bool {
31 h.once.Do(func() {
32 h.hgPath, h.hgPathErr = exec.LookPath("hg")
33 })
34 return h.hgPathErr == nil
35 }
36
37 func (h *hgHandler) Handler(dir string, env []string, logger *log.Logger) (http.Handler, error) {
38 if !h.Available() {
39 return nil, ServerNotInstalledError{name: "hg"}
40 }
41
42 handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
43
44
45
46
47
48
49
50
51
52
53 ctx, cancel := context.WithCancel(req.Context())
54 defer cancel()
55
56 cmd := exec.CommandContext(ctx, h.hgPath, "serve", "--port", "0", "--address", "localhost", "--accesslog", os.DevNull, "--name", "vcweb", "--print-url")
57 cmd.Dir = dir
58 cmd.Env = append(slices.Clip(env), "PWD="+dir)
59
60 cmd.Cancel = func() error {
61 err := cmd.Process.Signal(os.Interrupt)
62 if err != nil && !errors.Is(err, os.ErrProcessDone) {
63 err = cmd.Process.Kill()
64 }
65 return err
66 }
67
68
69
70 cmd.WaitDelay = 10 * time.Second
71
72 stderr := new(strings.Builder)
73 cmd.Stderr = stderr
74
75 stdout, err := cmd.StdoutPipe()
76 if err != nil {
77 http.Error(w, err.Error(), http.StatusInternalServerError)
78 return
79 }
80
81 if err := cmd.Start(); err != nil {
82 http.Error(w, err.Error(), http.StatusInternalServerError)
83 return
84 }
85 var wg sync.WaitGroup
86 defer func() {
87 cancel()
88 err := cmd.Wait()
89 if out := strings.TrimSuffix(stderr.String(), "interrupted!\n"); out != "" {
90 logger.Printf("%v: %v\n%s", cmd, err, out)
91 } else {
92 logger.Printf("%v", cmd)
93 }
94 wg.Wait()
95 }()
96
97 r := bufio.NewReader(stdout)
98 line, err := r.ReadString('\n')
99 if err != nil {
100 return
101 }
102
103
104
105
106 wg.Add(1)
107 go func() {
108 io.Copy(io.Discard, r)
109 wg.Done()
110 }()
111
112 u, err := url.Parse(strings.TrimSpace(line))
113 if err != nil {
114 logger.Printf("%v: %v", cmd, err)
115 http.Error(w, err.Error(), http.StatusBadGateway)
116 return
117 }
118 logger.Printf("proxying hg request to %s", u)
119 httputil.NewSingleHostReverseProxy(u).ServeHTTP(w, req)
120 })
121
122 return handler, nil
123 }
124
View as plain text