Source file src/cmd/go/internal/vcweb/script.go

     1  // Copyright 2022 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     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  // newScriptEngine returns a script engine augmented with commands for
    31  // reproducing version-control repositories by replaying commits.
    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 // arbitrary
    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  // loadScript interprets the given script content using the vcweb script engine.
    69  // loadScript always returns either a non-nil handler or a non-nil error.
    70  //
    71  // The script content must be a txtar archive with a comment containing a script
    72  // with exactly one "handle" command and zero or more VCS commands to prepare
    73  // the repository to be served.
    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  // newState returns a new script.State for executing scripts in workDir.
   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  // scriptEnviron returns a new environment that attempts to provide predictable
   128  // behavior for the supported version-control tools.
   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  	// Preserve additional environment variables that may be needed by VCS tools.
   138  	for _, k := range []string{
   139  		pathEnvName(),
   140  		tempEnvName(),
   141  		"SYSTEMROOT",        // must be preserved on Windows to find DLLs; golang.org/issue/25210
   142  		"WINDIR",            // must be preserved on Windows to be able to run PowerShell command; golang.org/issue/30711
   143  		"ComSpec",           // must be preserved on Windows to be able to run Batch files; golang.org/issue/56555
   144  		"DYLD_LIBRARY_PATH", // must be preserved on macOS systems to find shared libraries
   145  		"LD_LIBRARY_PATH",   // must be preserved on Unix systems to find shared libraries
   146  		"LIBRARY_PATH",      // allow override of non-standard static library paths
   147  		"PYTHONPATH",        // may be needed by hg to find imported modules
   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  		// To help diagnose https://go.dev/issue/52545,
   156  		// enable tracing for Git HTTPS requests.
   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  // homeEnvName returns the environment variable used by os.UserHomeDir
   167  // to locate the user's home directory.
   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  // tempEnvName returns the environment variable used by os.TempDir
   180  // to locate the default directory for temporary files.
   181  func tempEnvName() string {
   182  	switch runtime.GOOS {
   183  	case "windows":
   184  		return "TMP"
   185  	case "plan9":
   186  		return "TMPDIR" // actually plan 9 doesn't have one at all but this is fine
   187  	default:
   188  		return "TMPDIR"
   189  	}
   190  }
   191  
   192  // pathEnvName returns the environment variable used by exec.LookPath to
   193  // identify directories to search for executables.
   194  func pathEnvName() string {
   195  	switch runtime.GOOS {
   196  	case "plan9":
   197  		return "path"
   198  	default:
   199  		return "PATH"
   200  	}
   201  }
   202  
   203  // A scriptCtx is a context.Context that stores additional state for script
   204  // commands.
   205  type scriptCtx struct {
   206  	context.Context
   207  	server      *Server
   208  	commitTime  time.Time
   209  	handlerName string
   210  	handler     http.Handler
   211  }
   212  
   213  // scriptCtxKey is the key associating the *scriptCtx in a script's Context..
   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  	// Check that 'bzr help' exits with code 0.
   393  	// See go.dev/issue/71504 for an example where 'bzr' exists in PATH but doesn't work.
   394  	err = exec.Command(bzr, "help").Run()
   395  	return err == nil
   396  }
   397  

View as plain text