// Copyright 2019 The Chromium OS Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package main import ( "bytes" "fmt" "io" "io/ioutil" "os" "path/filepath" "reflect" "regexp" "sort" "strings" "text/template" ) const compareToOldWrapperFilePattern = "old_wrapper_compare" // Note: We can't rely on os.TempDir as that just returns the value of $TMPDIR, // which some packages set incorrectly. // E.g. dev-python/pygobject sets it to "`pwd`/pygobject-2.18.0". const tempDir = "/tmp" func compareToOldWrapper(env env, cfg *config, inputCmd *command, stdinBuffer []byte, newCmdResults []*commandResult, newExitCode int) error { pythonStringEscaper := strings.NewReplacer("\n", "\\n", "'", "\\'", "\\", "\\\\") oldWrapperCfg, err := newOldWrapperConfig(env, cfg, inputCmd) if err != nil { return err } oldWrapperCfg.MockCmds = cfg.mockOldWrapperCmds newCmds := []*command{} for _, cmdResult := range newCmdResults { oldWrapperCfg.CmdResults = append(oldWrapperCfg.CmdResults, oldWrapperCmdResult{ Stdout: pythonStringEscaper.Replace(cmdResult.Stdout), Stderr: pythonStringEscaper.Replace(cmdResult.Stderr), Exitcode: cmdResult.ExitCode, }) newCmds = append(newCmds, cmdResult.Cmd) } stderrBuffer := bytes.Buffer{} oldExitCode := 0 if strings.HasPrefix(oldWrapperCfg.OldWrapperContent, "#!/bin/sh") { oldExitCode, err = callOldShellWrapper(env, oldWrapperCfg, inputCmd, stdinBuffer, compareToOldWrapperFilePattern, &bytes.Buffer{}, &stderrBuffer) } else { oldExitCode, err = callOldPythonWrapper(env, oldWrapperCfg, inputCmd, stdinBuffer, compareToOldWrapperFilePattern, &bytes.Buffer{}, &stderrBuffer) } if err != nil { return err } differences := []string{} if oldExitCode != newExitCode { differences = append(differences, fmt.Sprintf("exit codes differ: old %d, new %d", oldExitCode, newExitCode)) } oldCmds, stderr := parseOldWrapperCommands(stderrBuffer.String()) if cmdDifferences := diffCommands(oldCmds, newCmds); cmdDifferences != "" { differences = append(differences, cmdDifferences) } if len(differences) > 0 { printCmd(env, inputCmd) return newErrorwithSourceLocf("wrappers differ:\n%s\nOld stderr:%s", strings.Join(differences, "\n"), stderr, ) } return nil } func parseOldWrapperCommands(stderr string) (cmds []*command, remainingStderr string) { allStderrLines := strings.Split(stderr, "\n") remainingStderrLines := []string{} commandPrefix := "command " argPrefix := "arg " envUpdatePrefix := "envupdate " currentCmd := (*command)(nil) for _, line := range allStderrLines { if strings.HasPrefix(line, commandPrefix) { currentCmd = &command{ Path: line[len(commandPrefix):], } cmds = append(cmds, currentCmd) } else if strings.HasPrefix(line, argPrefix) { currentCmd.Args = append(currentCmd.Args, line[len(argPrefix):]) } else if strings.HasPrefix(line, envUpdatePrefix) { currentCmd.EnvUpdates = append(currentCmd.EnvUpdates, line[len(envUpdatePrefix):]) } else { remainingStderrLines = append(remainingStderrLines, line) } } remainingStderr = strings.TrimSpace(strings.Join(remainingStderrLines, "\n")) return cmds, remainingStderr } func diffCommands(oldCmds []*command, newCmds []*command) string { maxLen := len(newCmds) if maxLen < len(oldCmds) { maxLen = len(oldCmds) } hasDifferences := false var cmdDifferences []string for i := 0; i < maxLen; i++ { var differences []string if i >= len(newCmds) { differences = append(differences, "missing command") } else if i >= len(oldCmds) { differences = append(differences, "extra command") } else { newCmd := newCmds[i] oldCmd := oldCmds[i] if newCmd.Path != oldCmd.Path { differences = append(differences, "path") } if !reflect.DeepEqual(newCmd.Args, oldCmd.Args) { differences = append(differences, "args") } // Sort the environment as we don't care in which order // it was modified. copyAndSort := func(data []string) []string { result := make([]string, len(data)) copy(result, data) sort.Strings(result) return result } newEnvUpdates := copyAndSort(newCmd.EnvUpdates) oldEnvUpdates := copyAndSort(oldCmd.EnvUpdates) if !reflect.DeepEqual(newEnvUpdates, oldEnvUpdates) { differences = append(differences, "env updates") } } if len(differences) > 0 { hasDifferences = true } else { differences = []string{"none"} } cmdDifferences = append(cmdDifferences, fmt.Sprintf("Index %d: %s", i, strings.Join(differences, ","))) } if hasDifferences { return fmt.Sprintf("commands differ:\n%s\nOld:%#v\nNew:%#v", strings.Join(cmdDifferences, "\n"), dumpCommands(oldCmds), dumpCommands(newCmds)) } return "" } func dumpCommands(cmds []*command) string { lines := []string{} for _, cmd := range cmds { lines = append(lines, fmt.Sprintf("%#v", cmd)) } return strings.Join(lines, "\n") } // Note: field names are upper case so they can be used in // a template via reflection. type oldWrapperConfig struct { WrapperPath string CmdPath string OldWrapperContent string MockCmds bool CmdResults []oldWrapperCmdResult } type oldWrapperCmdResult struct { Stdout string Stderr string Exitcode int } func newOldWrapperConfig(env env, cfg *config, inputCmd *command) (*oldWrapperConfig, error) { absWrapperPath, err := getAbsWrapperPath(env, inputCmd) if err != nil { return nil, err } absOldWrapperPath := cfg.oldWrapperPath if !filepath.IsAbs(absOldWrapperPath) { absOldWrapperPath = filepath.Join(filepath.Dir(absWrapperPath), cfg.oldWrapperPath) } oldWrapperContentBytes, err := ioutil.ReadFile(absOldWrapperPath) if err != nil { return nil, wrapErrorwithSourceLocf(err, "failed to read old wrapper") } oldWrapperContent := string(oldWrapperContentBytes) return &oldWrapperConfig{ WrapperPath: absWrapperPath, CmdPath: inputCmd.Path, OldWrapperContent: oldWrapperContent, }, nil } func callOldShellWrapper(env env, cfg *oldWrapperConfig, inputCmd *command, stdinBuffer []byte, filepattern string, stdout io.Writer, stderr io.Writer) (exitCode int, err error) { oldWrapperContent := cfg.OldWrapperContent oldWrapperContent = regexp.MustCompile(`(?m)^exec\b`).ReplaceAllString(oldWrapperContent, "exec_mock") oldWrapperContent = regexp.MustCompile(`\$EXEC`).ReplaceAllString(oldWrapperContent, "exec_mock") // TODO: Use strings.ReplaceAll once cros sdk uses golang >= 1.12 oldWrapperContent = strings.Replace(oldWrapperContent, "$0", cfg.CmdPath, -1) cfg.OldWrapperContent = oldWrapperContent mockFile, err := ioutil.TempFile(tempDir, filepattern) if err != nil { return 0, wrapErrorwithSourceLocf(err, "failed to create tempfile") } defer os.Remove(mockFile.Name()) const mockTemplate = ` EXEC=exec function exec_mock { echo command "$1" 1>&2 for arg in "${@:2}"; do echo arg "$arg" 1>&2 done {{if .MockCmds}} echo '{{(index .CmdResults 0).Stdout}}' echo '{{(index .CmdResults 0).Stderr}}' 1>&2 exit {{(index .CmdResults 0).Exitcode}} {{else}} $EXEC "$@" {{end}} } {{.OldWrapperContent}} ` tmpl, err := template.New("mock").Parse(mockTemplate) if err != nil { return 0, wrapErrorwithSourceLocf(err, "failed to parse old wrapper template") } if err := tmpl.Execute(mockFile, cfg); err != nil { return 0, wrapErrorwithSourceLocf(err, "failed execute old wrapper template") } if err := mockFile.Close(); err != nil { return 0, wrapErrorwithSourceLocf(err, "failed to close temp file") } // Note: Using a self executable wrapper does not work due to a race condition // on unix systems. See https://github.com/golang/go/issues/22315 oldWrapperCmd := &command{ Path: "/bin/sh", Args: append([]string{mockFile.Name()}, inputCmd.Args...), EnvUpdates: inputCmd.EnvUpdates, } return wrapSubprocessErrorWithSourceLoc(oldWrapperCmd, env.run(oldWrapperCmd, bytes.NewReader(stdinBuffer), stdout, stderr)) } func callOldPythonWrapper(env env, cfg *oldWrapperConfig, inputCmd *command, stdinBuffer []byte, filepattern string, stdout io.Writer, stderr io.Writer) (exitCode int, err error) { oldWrapperContent := cfg.OldWrapperContent // TODO: Use strings.ReplaceAll once cros sdk uses golang >= 1.12 oldWrapperContent = strings.Replace(oldWrapperContent, "from __future__ import print_function", "", -1) // Replace sets with lists to make our comparisons deterministic oldWrapperContent = strings.Replace(oldWrapperContent, "set(", "ListSet(", -1) oldWrapperContent = strings.Replace(oldWrapperContent, "if __name__ == '__main__':", "def runMain():", -1) oldWrapperContent = strings.Replace(oldWrapperContent, "__file__", "'"+cfg.WrapperPath+"'", -1) cfg.OldWrapperContent = oldWrapperContent mockFile, err := ioutil.TempFile(tempDir, filepattern) if err != nil { return 0, wrapErrorwithSourceLocf(err, "failed to create tempfile") } defer os.Remove(mockFile.Name()) const mockTemplate = ` # -*- coding: utf-8 -*- from __future__ import print_function class ListSet: def __init__(self, values): self.values = list(values) def __contains__(self, key): return self.values.__contains__(key) def __iter__(self): return self.values.__iter__() def __nonzero__(self): return len(self.values) > 0 def add(self, value): if value not in self.values: self.values.append(value) def discard(self, value): if value in self.values: self.values.remove(value) def intersection(self, values): return ListSet([value for value in self.values if value in values]) {{.OldWrapperContent}} import subprocess init_env = os.environ.copy() {{if .MockCmds}} mockResults = [{{range .CmdResults}} { 'stdout': '{{.Stdout}}', 'stderr': '{{.Stderr}}', 'exitcode': {{.Exitcode}}, },{{end}}] {{end}} def serialize_cmd(args): current_env = os.environ envupdates = [k + "=" + current_env.get(k, '') for k in set(list(current_env.keys()) + list(init_env.keys())) if current_env.get(k, '') != init_env.get(k, '')] print('command %s' % args[0], file=sys.stderr) for arg in args[1:]: print('arg %s' % arg, file=sys.stderr) for update in envupdates: print('envupdate %s' % update, file=sys.stderr) def check_output_mock(args): serialize_cmd(args) {{if .MockCmds}} result = mockResults.pop(0) print(result['stderr'], file=sys.stderr) if result['exitcode']: raise subprocess.CalledProcessError(result['exitcode']) return result['stdout'] {{else}} return old_check_output(args) {{end}} old_check_output = subprocess.check_output subprocess.check_output = check_output_mock def popen_mock(args, stdout=None, stderr=None): serialize_cmd(args) {{if .MockCmds}} result = mockResults.pop(0) if stdout is None: print(result['stdout'], file=sys.stdout) if stderr is None: print(result['stderr'], file=sys.stderr) class MockResult: def __init__(self, returncode): self.returncode = returncode def wait(self): return self.returncode def communicate(self): return (result['stdout'], result['stderr']) return MockResult(result['exitcode']) {{else}} return old_popen(args) {{end}} old_popen = subprocess.Popen subprocess.Popen = popen_mock def execv_mock(binary, args): serialize_cmd([binary] + args[1:]) {{if .MockCmds}} result = mockResults.pop(0) print(result['stdout'], file=sys.stdout) print(result['stderr'], file=sys.stderr) sys.exit(result['exitcode']) {{else}} old_execv(binary, args) {{end}} old_execv = os.execv os.execv = execv_mock sys.argv[0] = '{{.CmdPath}}' runMain() ` tmpl, err := template.New("mock").Parse(mockTemplate) if err != nil { return 0, wrapErrorwithSourceLocf(err, "failed to parse old wrapper template") } if err := tmpl.Execute(mockFile, cfg); err != nil { return 0, wrapErrorwithSourceLocf(err, "failed execute old wrapper template") } if err := mockFile.Close(); err != nil { return 0, wrapErrorwithSourceLocf(err, "failed to close temp file") } // Note: Using a self executable wrapper does not work due to a race condition // on unix systems. See https://github.com/golang/go/issues/22315 oldWrapperCmd := &command{ Path: "/usr/bin/python2", Args: append([]string{"-S", mockFile.Name()}, inputCmd.Args...), EnvUpdates: inputCmd.EnvUpdates, } return wrapSubprocessErrorWithSourceLoc(oldWrapperCmd, env.run(oldWrapperCmd, bytes.NewReader(stdinBuffer), stdout, stderr)) }