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 mu sync.Mutex
30 wg sync.WaitGroup
31 ctx context.Context
32 cancel func()
33 cmds []*exec.Cmd
34 url map[string]*url.URL
35 }
36
37 func (h *hgHandler) Available() bool {
38 h.once.Do(func() {
39 h.hgPath, h.hgPathErr = exec.LookPath("hg")
40 })
41 return h.hgPathErr == nil
42 }
43
44 func (h *hgHandler) Close() error {
45 h.mu.Lock()
46 defer h.mu.Unlock()
47
48 if h.cancel == nil {
49 return nil
50 }
51
52 h.cancel()
53 for _, cmd := range h.cmds {
54 h.wg.Add(1)
55 go func() {
56 cmd.Wait()
57 h.wg.Done()
58 }()
59 }
60 h.wg.Wait()
61 h.url = nil
62 h.cmds = nil
63 h.ctx = nil
64 h.cancel = nil
65 return nil
66 }
67
68 func (h *hgHandler) Handler(dir string, env []string, logger *log.Logger) (http.Handler, error) {
69 if !h.Available() {
70 return nil, ServerNotInstalledError{name: "hg"}
71 }
72
73 handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
74
75
76
77
78
79
80
81
82
83
84 h.mu.Lock()
85
86 if h.ctx == nil {
87 h.ctx, h.cancel = context.WithCancel(context.Background())
88 }
89
90
91
92
93 u := h.url[dir]
94 if u != nil {
95 h.mu.Unlock()
96 logger.Printf("proxying hg request to %s", u)
97 httputil.NewSingleHostReverseProxy(u).ServeHTTP(w, req)
98 return
99 }
100
101 logger.Printf("starting hg serve for %s", dir)
102 cmd := exec.CommandContext(h.ctx, h.hgPath, "serve", "--port", "0", "--address", "localhost", "--accesslog", os.DevNull, "--name", "vcweb", "--print-url")
103 cmd.Dir = dir
104 cmd.Env = append(slices.Clip(env), "PWD="+dir)
105
106 cmd.Cancel = func() error {
107 err := cmd.Process.Signal(os.Interrupt)
108 if err != nil && !errors.Is(err, os.ErrProcessDone) {
109 err = cmd.Process.Kill()
110 }
111 return err
112 }
113
114
115
116 cmd.WaitDelay = 10 * time.Second
117
118 stderr := new(strings.Builder)
119 cmd.Stderr = stderr
120
121 stdout, err := cmd.StdoutPipe()
122 if err != nil {
123 h.mu.Unlock()
124 http.Error(w, err.Error(), http.StatusInternalServerError)
125 return
126 }
127
128 if err := cmd.Start(); err != nil {
129 h.mu.Unlock()
130 http.Error(w, err.Error(), http.StatusInternalServerError)
131 return
132 }
133
134 r := bufio.NewReader(stdout)
135 line, err := r.ReadString('\n')
136 if err != nil {
137 h.mu.Unlock()
138 http.Error(w, err.Error(), http.StatusInternalServerError)
139 return
140 }
141
142
143
144
145 h.wg.Add(1)
146 go func() {
147 io.Copy(io.Discard, r)
148 h.wg.Done()
149 }()
150
151
152
153
154
155 line = strings.ReplaceAll(line, "//1.0.0.127.in-addr.arpa", "//127.0.0.1")
156 line = strings.ReplaceAll(line, "//1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa", "//[::1]")
157
158 u, err = url.Parse(strings.TrimSpace(line))
159 if err != nil {
160 h.mu.Unlock()
161 logger.Printf("%v: %v", cmd, err)
162 http.Error(w, err.Error(), http.StatusBadGateway)
163 return
164 }
165
166 if h.url == nil {
167 h.url = make(map[string]*url.URL)
168 }
169 h.url[dir] = u
170 h.cmds = append(h.cmds, cmd)
171 h.mu.Unlock()
172
173 logger.Printf("proxying hg request to %s", u)
174 httputil.NewSingleHostReverseProxy(u).ServeHTTP(w, req)
175 })
176
177 return handler, nil
178 }
179
View as plain text