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