1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 package vcweb
31
32 import (
33 "bufio"
34 "cmd/internal/script"
35 "context"
36 "crypto/sha256"
37 "errors"
38 "fmt"
39 "io"
40 "io/fs"
41 "log"
42 "net/http"
43 "os"
44 "os/exec"
45 "path"
46 "path/filepath"
47 "runtime/debug"
48 "strings"
49 "sync"
50 "text/tabwriter"
51 "time"
52 )
53
54
55 type Server struct {
56 env []string
57 logger *log.Logger
58
59 scriptDir string
60 workDir string
61 homeDir string
62 engine *script.Engine
63
64 scriptCache sync.Map
65
66 vcsHandlers map[string]vcsHandler
67 }
68
69
70 type vcsHandler interface {
71 Available() bool
72 Handler(dir string, env []string, logger *log.Logger) (http.Handler, error)
73 }
74
75
76 type scriptResult struct {
77 mu sync.RWMutex
78
79 hash [sha256.Size]byte
80 hashTime time.Time
81
82 handler http.Handler
83 err error
84 }
85
86
87
88
89
90
91
92 func NewServer(scriptDir, workDir string, logger *log.Logger) (*Server, error) {
93 if scriptDir == "" {
94 panic("vcweb.NewServer: scriptDir is required")
95 }
96 var err error
97 scriptDir, err = filepath.Abs(scriptDir)
98 if err != nil {
99 return nil, err
100 }
101
102 if workDir == "" {
103 workDir, err = os.MkdirTemp("", "vcweb-*")
104 if err != nil {
105 return nil, err
106 }
107 logger.Printf("vcweb work directory: %s", workDir)
108 } else {
109 workDir, err = filepath.Abs(workDir)
110 if err != nil {
111 return nil, err
112 }
113 }
114
115 homeDir := filepath.Join(workDir, "home")
116 if err := os.MkdirAll(homeDir, 0755); err != nil {
117 return nil, err
118 }
119
120 env := scriptEnviron(homeDir)
121
122 s := &Server{
123 env: env,
124 logger: logger,
125 scriptDir: scriptDir,
126 workDir: workDir,
127 homeDir: homeDir,
128 engine: newScriptEngine(),
129 vcsHandlers: map[string]vcsHandler{
130 "auth": new(authHandler),
131 "dir": new(dirHandler),
132 "bzr": new(bzrHandler),
133 "fossil": new(fossilHandler),
134 "git": new(gitHandler),
135 "hg": new(hgHandler),
136 "insecure": new(insecureHandler),
137 "svn": &svnHandler{svnRoot: workDir, logger: logger},
138 },
139 }
140
141 if err := os.WriteFile(filepath.Join(s.homeDir, ".gitconfig"), []byte(gitConfig), 0644); err != nil {
142 return nil, err
143 }
144 gitConfigDir := filepath.Join(s.homeDir, ".config", "git")
145 if err := os.MkdirAll(gitConfigDir, 0755); err != nil {
146 return nil, err
147 }
148 if err := os.WriteFile(filepath.Join(gitConfigDir, "ignore"), []byte(""), 0644); err != nil {
149 return nil, err
150 }
151
152 if err := os.WriteFile(filepath.Join(s.homeDir, ".hgrc"), []byte(hgrc), 0644); err != nil {
153 return nil, err
154 }
155
156 return s, nil
157 }
158
159 func (s *Server) Close() error {
160 var firstErr error
161 for _, h := range s.vcsHandlers {
162 if c, ok := h.(io.Closer); ok {
163 if closeErr := c.Close(); firstErr == nil {
164 firstErr = closeErr
165 }
166 }
167 }
168 return firstErr
169 }
170
171
172
173 var gitConfig = `
174 [user]
175 name = Go Gopher
176 email = gopher@golang.org
177 [init]
178 defaultBranch = main
179 [core]
180 eol = lf
181 [gui]
182 encoding = utf-8
183 `[1:]
184
185
186
187 var hgrc = `
188 [ui]
189 username=Go Gopher <gopher@golang.org>
190 [phases]
191 new-commit=public
192 [extensions]
193 convert=
194 `[1:]
195
196
197 func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
198 s.logger.Printf("serving %s", req.URL)
199
200 defer func() {
201 if v := recover(); v != nil {
202 debug.PrintStack()
203 s.logger.Fatal(v)
204 }
205 }()
206
207 urlPath := req.URL.Path
208 if !strings.HasPrefix(urlPath, "/") {
209 urlPath = "/" + urlPath
210 }
211 clean := path.Clean(urlPath)[1:]
212 if clean == "" {
213 s.overview(w, req)
214 return
215 }
216 if clean == "help" {
217 s.help(w, req)
218 return
219 }
220
221
222
223
224
225
226 scriptPath := "."
227 for _, part := range strings.Split(clean, "/") {
228 scriptPath = filepath.Join(scriptPath, part)
229 dir := filepath.Join(s.scriptDir, scriptPath)
230 if _, err := os.Stat(dir); err != nil {
231 if !os.IsNotExist(err) {
232 http.Error(w, err.Error(), http.StatusInternalServerError)
233 return
234 }
235
236
237 break
238 }
239 }
240 scriptPath += ".txt"
241
242 err := s.HandleScript(scriptPath, s.logger, func(handler http.Handler) {
243 handler.ServeHTTP(w, req)
244 })
245 if err != nil {
246 s.logger.Print(err)
247 if notFound := (ScriptNotFoundError{}); errors.As(err, ¬Found) {
248 http.NotFound(w, req)
249 } else if notInstalled := (ServerNotInstalledError{}); errors.As(err, ¬Installed) || errors.Is(err, exec.ErrNotFound) {
250 http.Error(w, err.Error(), http.StatusNotImplemented)
251 } else {
252 http.Error(w, err.Error(), http.StatusInternalServerError)
253 }
254 }
255 }
256
257
258
259 type ScriptNotFoundError struct{ err error }
260
261 func (e ScriptNotFoundError) Error() string { return e.err.Error() }
262 func (e ScriptNotFoundError) Unwrap() error { return e.err }
263
264
265
266 type ServerNotInstalledError struct{ name string }
267
268 func (v ServerNotInstalledError) Error() string {
269 return fmt.Sprintf("server for %#q VCS is not installed", v.name)
270 }
271
272
273
274
275
276
277
278
279
280 func (s *Server) HandleScript(scriptRelPath string, logger *log.Logger, f func(http.Handler)) error {
281 ri, ok := s.scriptCache.Load(scriptRelPath)
282 if !ok {
283 ri, _ = s.scriptCache.LoadOrStore(scriptRelPath, new(scriptResult))
284 }
285 r := ri.(*scriptResult)
286
287 relDir := strings.TrimSuffix(scriptRelPath, filepath.Ext(scriptRelPath))
288 workDir := filepath.Join(s.workDir, relDir)
289 prefix := path.Join("/", filepath.ToSlash(relDir))
290
291 r.mu.RLock()
292 defer r.mu.RUnlock()
293 for {
294
295
296
297
298
299
300 content, err := os.ReadFile(filepath.Join(s.scriptDir, scriptRelPath))
301 if err != nil {
302 if !os.IsNotExist(err) {
303 return err
304 }
305 return ScriptNotFoundError{err}
306 }
307
308 hash := sha256.Sum256(content)
309 if prevHash := r.hash; prevHash != hash {
310
311 func() {
312 r.mu.RUnlock()
313 r.mu.Lock()
314 defer func() {
315 r.mu.Unlock()
316 r.mu.RLock()
317 }()
318 if r.hash != prevHash {
319
320
321
322 return
323 }
324
325 r.hash = hash
326 r.hashTime = time.Now()
327 r.handler, r.err = nil, nil
328
329 if err := os.RemoveAll(workDir); err != nil {
330 r.err = err
331 return
332 }
333
334
335
336
337 scriptHandler, err := s.loadScript(context.Background(), logger, scriptRelPath, content, workDir)
338 if err != nil {
339 r.err = err
340 return
341 }
342 r.handler = http.StripPrefix(prefix, scriptHandler)
343 }()
344 }
345
346 if r.hash != hash {
347 continue
348 }
349
350 if r.err != nil {
351 return r.err
352 }
353 f(r.handler)
354 return nil
355 }
356 }
357
358
359
360 func (s *Server) overview(w http.ResponseWriter, r *http.Request) {
361 fmt.Fprintf(w, "<html>\n")
362 fmt.Fprintf(w, "<title>vcweb</title>\n<pre>\n")
363 fmt.Fprintf(w, "<b>vcweb</b>\n\n")
364 fmt.Fprintf(w, "This server serves various version control repos for testing the go command.\n\n")
365 fmt.Fprintf(w, "For an overview of the script language, see <a href=\"/help\">/help</a>.\n\n")
366
367 fmt.Fprintf(w, "<b>cache</b>\n")
368
369 tw := tabwriter.NewWriter(w, 1, 8, 1, '\t', 0)
370 err := filepath.WalkDir(s.scriptDir, func(path string, d fs.DirEntry, err error) error {
371 if err != nil {
372 return err
373 }
374 if filepath.Ext(path) != ".txt" {
375 return nil
376 }
377
378 rel, err := filepath.Rel(s.scriptDir, path)
379 if err != nil {
380 return err
381 }
382 hashTime := "(not loaded)"
383 status := ""
384 if ri, ok := s.scriptCache.Load(rel); ok {
385 r := ri.(*scriptResult)
386 r.mu.RLock()
387 defer r.mu.RUnlock()
388
389 if !r.hashTime.IsZero() {
390 hashTime = r.hashTime.Format(time.RFC3339)
391 }
392 if r.err == nil {
393 status = "ok"
394 } else {
395 status = r.err.Error()
396 }
397 }
398 fmt.Fprintf(tw, "%s\t%s\t%s\n", rel, hashTime, status)
399 return nil
400 })
401 tw.Flush()
402
403 if err != nil {
404 fmt.Fprintln(w, err)
405 }
406 }
407
408
409 func (s *Server) help(w http.ResponseWriter, req *http.Request) {
410 st, err := s.newState(req.Context(), s.workDir)
411 if err != nil {
412 http.Error(w, err.Error(), http.StatusInternalServerError)
413 return
414 }
415
416 scriptLog := new(strings.Builder)
417 err = s.engine.Execute(st, "help", bufio.NewReader(strings.NewReader("help")), scriptLog)
418 if err != nil {
419 http.Error(w, err.Error(), http.StatusInternalServerError)
420 return
421 }
422
423 w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
424 io.WriteString(w, scriptLog.String())
425 }
426
View as plain text