1
2
3
4
5 package vcweb
6
7 import (
8 "bufio"
9 "bytes"
10 "cmd/internal/script"
11 "context"
12 "errors"
13 "fmt"
14 "internal/txtar"
15 "io"
16 "log"
17 "net/http"
18 "os"
19 "os/exec"
20 "path/filepath"
21 "runtime"
22 "strconv"
23 "strings"
24 "time"
25
26 "golang.org/x/mod/module"
27 "golang.org/x/mod/zip"
28 )
29
30
31
32 func newScriptEngine() *script.Engine {
33 conds := script.DefaultConds()
34
35 add := func(name string, cond script.Cond) {
36 if _, ok := conds[name]; ok {
37 panic(fmt.Sprintf("condition %q is already registered", name))
38 }
39 conds[name] = cond
40 }
41 lazyBool := func(summary string, f func() bool) script.Cond {
42 return script.OnceCondition(summary, func() (bool, error) { return f(), nil })
43 }
44 add("bzr", lazyBool("the 'bzr' executable exists and provides the standard CLI", hasWorkingBzr))
45
46 interrupt := func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) }
47 gracePeriod := 30 * time.Second
48
49 cmds := script.DefaultCmds()
50 cmds["at"] = scriptAt()
51 cmds["bzr"] = script.Program("bzr", interrupt, gracePeriod)
52 cmds["fossil"] = script.Program("fossil", interrupt, gracePeriod)
53 cmds["git"] = script.Program("git", interrupt, gracePeriod)
54 cmds["hg"] = script.Program("hg", interrupt, gracePeriod)
55 cmds["handle"] = scriptHandle()
56 cmds["modzip"] = scriptModzip()
57 cmds["skip"] = scriptSkip()
58 cmds["svnadmin"] = script.Program("svnadmin", interrupt, gracePeriod)
59 cmds["svn"] = script.Program("svn", interrupt, gracePeriod)
60 cmds["unquote"] = scriptUnquote()
61
62 return &script.Engine{
63 Cmds: cmds,
64 Conds: conds,
65 }
66 }
67
68
69
70
71
72
73
74 func (s *Server) loadScript(ctx context.Context, logger *log.Logger, scriptPath string, scriptContent []byte, workDir string) (http.Handler, error) {
75 ar := txtar.Parse(scriptContent)
76
77 if err := os.MkdirAll(workDir, 0755); err != nil {
78 return nil, err
79 }
80
81 st, err := s.newState(ctx, workDir)
82 if err != nil {
83 return nil, err
84 }
85 if err := st.ExtractFiles(ar); err != nil {
86 return nil, err
87 }
88
89 scriptName := filepath.Base(scriptPath)
90 scriptLog := new(strings.Builder)
91 err = s.engine.Execute(st, scriptName, bufio.NewReader(bytes.NewReader(ar.Comment)), scriptLog)
92 closeErr := st.CloseAndWait(scriptLog)
93 logger.Printf("%s:", scriptName)
94 io.WriteString(logger.Writer(), scriptLog.String())
95 io.WriteString(logger.Writer(), "\n")
96 if err != nil {
97 return nil, err
98 }
99 if closeErr != nil {
100 return nil, err
101 }
102
103 sc, err := getScriptCtx(st)
104 if err != nil {
105 return nil, err
106 }
107 if sc.handler == nil {
108 return nil, errors.New("script completed without setting handler")
109 }
110 return sc.handler, nil
111 }
112
113
114 func (s *Server) newState(ctx context.Context, workDir string) (*script.State, error) {
115 ctx = &scriptCtx{
116 Context: ctx,
117 server: s,
118 }
119
120 st, err := script.NewState(ctx, workDir, s.env)
121 if err != nil {
122 return nil, err
123 }
124 return st, nil
125 }
126
127
128
129 func scriptEnviron(homeDir string) []string {
130 env := []string{
131 "USER=gopher",
132 homeEnvName() + "=" + homeDir,
133 "GIT_CONFIG_NOSYSTEM=1",
134 "HGRCPATH=" + filepath.Join(homeDir, ".hgrc"),
135 "HGENCODING=utf-8",
136 }
137
138 for _, k := range []string{
139 pathEnvName(),
140 tempEnvName(),
141 "SYSTEMROOT",
142 "WINDIR",
143 "ComSpec",
144 "DYLD_LIBRARY_PATH",
145 "LD_LIBRARY_PATH",
146 "LIBRARY_PATH",
147 "PYTHONPATH",
148 } {
149 if v, ok := os.LookupEnv(k); ok {
150 env = append(env, k+"="+v)
151 }
152 }
153
154 if os.Getenv("GO_BUILDER_NAME") != "" || os.Getenv("GIT_TRACE_CURL") == "1" {
155
156
157 env = append(env,
158 "GIT_TRACE_CURL=1",
159 "GIT_TRACE_CURL_NO_DATA=1",
160 "GIT_REDACT_COOKIES=o,SSO,GSSO_Uberproxy")
161 }
162
163 return env
164 }
165
166
167
168 func homeEnvName() string {
169 switch runtime.GOOS {
170 case "windows":
171 return "USERPROFILE"
172 case "plan9":
173 return "home"
174 default:
175 return "HOME"
176 }
177 }
178
179
180
181 func tempEnvName() string {
182 switch runtime.GOOS {
183 case "windows":
184 return "TMP"
185 case "plan9":
186 return "TMPDIR"
187 default:
188 return "TMPDIR"
189 }
190 }
191
192
193
194 func pathEnvName() string {
195 switch runtime.GOOS {
196 case "plan9":
197 return "path"
198 default:
199 return "PATH"
200 }
201 }
202
203
204
205 type scriptCtx struct {
206 context.Context
207 server *Server
208 commitTime time.Time
209 handlerName string
210 handler http.Handler
211 }
212
213
214 type scriptCtxKey struct{}
215
216 func (sc *scriptCtx) Value(key any) any {
217 if key == (scriptCtxKey{}) {
218 return sc
219 }
220 return sc.Context.Value(key)
221 }
222
223 func getScriptCtx(st *script.State) (*scriptCtx, error) {
224 sc, ok := st.Context().Value(scriptCtxKey{}).(*scriptCtx)
225 if !ok {
226 return nil, errors.New("scriptCtx not found in State.Context")
227 }
228 return sc, nil
229 }
230
231 func scriptAt() script.Cmd {
232 return script.Command(
233 script.CmdUsage{
234 Summary: "set the current commit time for all version control systems",
235 Args: "time",
236 Detail: []string{
237 "The argument must be an absolute timestamp in RFC3339 format.",
238 },
239 },
240 func(st *script.State, args ...string) (script.WaitFunc, error) {
241 if len(args) != 1 {
242 return nil, script.ErrUsage
243 }
244
245 sc, err := getScriptCtx(st)
246 if err != nil {
247 return nil, err
248 }
249
250 sc.commitTime, err = time.ParseInLocation(time.RFC3339, args[0], time.UTC)
251 if err == nil {
252 st.Setenv("GIT_COMMITTER_DATE", args[0])
253 st.Setenv("GIT_AUTHOR_DATE", args[0])
254 }
255 return nil, err
256 })
257 }
258
259 func scriptHandle() script.Cmd {
260 return script.Command(
261 script.CmdUsage{
262 Summary: "set the HTTP handler that will serve the script's output",
263 Args: "handler [dir]",
264 Detail: []string{
265 "The handler will be passed the script's current working directory and environment as arguments.",
266 "Valid handlers include 'dir' (for general http.Dir serving), 'bzr', 'fossil', 'git', and 'hg'",
267 },
268 },
269 func(st *script.State, args ...string) (script.WaitFunc, error) {
270 if len(args) == 0 || len(args) > 2 {
271 return nil, script.ErrUsage
272 }
273
274 sc, err := getScriptCtx(st)
275 if err != nil {
276 return nil, err
277 }
278
279 if sc.handler != nil {
280 return nil, fmt.Errorf("server handler already set to %s", sc.handlerName)
281 }
282
283 name := args[0]
284 h, ok := sc.server.vcsHandlers[name]
285 if !ok {
286 return nil, fmt.Errorf("unrecognized VCS %q", name)
287 }
288 sc.handlerName = name
289 if !h.Available() {
290 return nil, ServerNotInstalledError{name}
291 }
292
293 dir := st.Getwd()
294 if len(args) >= 2 {
295 dir = st.Path(args[1])
296 }
297 sc.handler, err = h.Handler(dir, st.Environ(), sc.server.logger)
298 return nil, err
299 })
300 }
301
302 func scriptModzip() script.Cmd {
303 return script.Command(
304 script.CmdUsage{
305 Summary: "create a Go module zip file from a directory",
306 Args: "zipfile path@version dir",
307 },
308 func(st *script.State, args ...string) (wait script.WaitFunc, err error) {
309 if len(args) != 3 {
310 return nil, script.ErrUsage
311 }
312 zipPath := st.Path(args[0])
313 mPath, version, ok := strings.Cut(args[1], "@")
314 if !ok {
315 return nil, script.ErrUsage
316 }
317 dir := st.Path(args[2])
318
319 if err := os.MkdirAll(filepath.Dir(zipPath), 0755); err != nil {
320 return nil, err
321 }
322 f, err := os.Create(zipPath)
323 if err != nil {
324 return nil, err
325 }
326 defer func() {
327 if closeErr := f.Close(); err == nil {
328 err = closeErr
329 }
330 }()
331
332 return nil, zip.CreateFromDir(f, module.Version{Path: mPath, Version: version}, dir)
333 })
334 }
335
336 func scriptSkip() script.Cmd {
337 return script.Command(
338 script.CmdUsage{
339 Summary: "skip the current test",
340 Args: "[msg]",
341 },
342 func(_ *script.State, args ...string) (script.WaitFunc, error) {
343 if len(args) > 1 {
344 return nil, script.ErrUsage
345 }
346 if len(args) == 0 {
347 return nil, SkipError{""}
348 }
349 return nil, SkipError{args[0]}
350 })
351 }
352
353 type SkipError struct {
354 Msg string
355 }
356
357 func (s SkipError) Error() string {
358 if s.Msg == "" {
359 return "skip"
360 }
361 return s.Msg
362 }
363
364 func scriptUnquote() script.Cmd {
365 return script.Command(
366 script.CmdUsage{
367 Summary: "unquote the argument as a Go string",
368 Args: "string",
369 },
370 func(st *script.State, args ...string) (script.WaitFunc, error) {
371 if len(args) != 1 {
372 return nil, script.ErrUsage
373 }
374
375 s, err := strconv.Unquote(`"` + args[0] + `"`)
376 if err != nil {
377 return nil, err
378 }
379
380 wait := func(*script.State) (stdout, stderr string, err error) {
381 return s, "", nil
382 }
383 return wait, nil
384 })
385 }
386
387 func hasWorkingBzr() bool {
388 bzr, err := exec.LookPath("bzr")
389 if err != nil {
390 return false
391 }
392
393
394 err = exec.Command(bzr, "help").Run()
395 return err == nil
396 }
397
View as plain text