compiler_wrapper: report parent processes' info in warnings tarballs

The bug I'm chasing down right now depends on environment variables, and
could be due to a `make` invocation in one of N places. Having this
context available to me is useful.

It seriously pollutes the JSON we dump, but use of `jq` that I've put in
the doc about this makes that a non-issue.

BUG=None
TEST=Ran on stress-ng. parent process info was dumped.

Change-Id: Ib4d3b6c9887b4cf2a38472ef299c81af27e9a617
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/toolchain-utils/+/2481829
Reviewed-by: Manoj Gupta <manojgupta@chromium.org>
Tested-by: George Burgess <gbiv@chromium.org>
diff --git a/compiler_wrapper/disable_werror_flag.go b/compiler_wrapper/disable_werror_flag.go
index 1180127..8f20b6f 100644
--- a/compiler_wrapper/disable_werror_flag.go
+++ b/compiler_wrapper/disable_werror_flag.go
@@ -7,9 +7,12 @@
 import (
 	"bytes"
 	"encoding/json"
+	"fmt"
 	"io"
 	"io/ioutil"
 	"os"
+	"path"
+	"strconv"
 	"strings"
 	"syscall"
 )
@@ -119,10 +122,14 @@
 	}
 	outputToLog := strings.Join(lines, "\n")
 
+	// Ignore the error here; we can't do anything about it. The result is always valid (though
+	// perhaps incomplete) even if this returns an error.
+	parentProcesses, _ := collectAllParentProcesses()
 	jsonData := warningsJSONData{
-		Cwd:     env.getwd(),
-		Command: append([]string{originalCmd.Path}, originalCmd.Args...),
-		Stdout:  outputToLog,
+		Cwd:             env.getwd(),
+		Command:         append([]string{originalCmd.Path}, originalCmd.Args...),
+		Stdout:          outputToLog,
+		ParentProcesses: parentProcesses,
 	}
 
 	// Write warning report to stdout for Android.  On Android,
@@ -186,10 +193,87 @@
 	return retryExitCode, nil
 }
 
-// Struct used to write JSON. Fileds have to be uppercase for the json
-// encoder to read them.
+func parseParentPidFromPidStat(pidStatContents string) (parentPid int, ok bool) {
+	// The parent's pid is the fourth field of /proc/[pid]/stat. Sadly, the second field can
+	// have spaces in it. It ends at the last ')' in the contents of /proc/[pid]/stat.
+	lastParen := strings.LastIndex(pidStatContents, ")")
+	if lastParen == -1 {
+		return 0, false
+	}
+
+	thirdFieldAndBeyond := strings.TrimSpace(pidStatContents[lastParen+1:])
+	fields := strings.Fields(thirdFieldAndBeyond)
+	if len(fields) < 2 {
+		return 0, false
+	}
+
+	fourthField := fields[1]
+	parentPid, err := strconv.Atoi(fourthField)
+	if err != nil {
+		return 0, false
+	}
+	return parentPid, true
+}
+
+func collectProcessData(pid int) (args, env []string, parentPid int, err error) {
+	procDir := fmt.Sprintf("/proc/%d", pid)
+
+	readFile := func(fileName string) (string, error) {
+		s, err := ioutil.ReadFile(path.Join(procDir, fileName))
+		if err != nil {
+			return "", fmt.Errorf("reading %s: %v", fileName, err)
+		}
+		return string(s), nil
+	}
+
+	statStr, err := readFile("stat")
+	if err != nil {
+		return nil, nil, 0, err
+	}
+
+	parentPid, ok := parseParentPidFromPidStat(statStr)
+	if !ok {
+		return nil, nil, 0, fmt.Errorf("no parseable parent PID found in %q", statStr)
+	}
+
+	argsStr, err := readFile("cmdline")
+	if err != nil {
+		return nil, nil, 0, err
+	}
+	args = strings.Split(argsStr, "\x00")
+
+	envStr, err := readFile("environ")
+	if err != nil {
+		return nil, nil, 0, err
+	}
+	env = strings.Split(envStr, "\x00")
+	return args, env, parentPid, nil
+}
+
+// The returned []processData is valid even if this returns an error. The error is just the first we
+// encountered when trying to collect parent process data.
+func collectAllParentProcesses() ([]processData, error) {
+	results := []processData{}
+	for parent := os.Getppid(); parent != 1; {
+		args, env, p, err := collectProcessData(parent)
+		if err != nil {
+			return results, fmt.Errorf("inspecting parent %d: %v", parent, err)
+		}
+		results = append(results, processData{Args: args, Env: env})
+		parent = p
+	}
+	return results, nil
+}
+
+type processData struct {
+	Args []string `json:"invocation"`
+	Env  []string `json:"env"`
+}
+
+// Struct used to write JSON. Fields have to be uppercase for the json encoder to read them.
 type warningsJSONData struct {
-	Cwd     string   `json:"cwd"`
-	Command []string `json:"command"`
-	Stdout  string   `json:"stdout"`
+	Cwd             string        `json:"cwd"`
+	Command         []string      `json:"command"`
+	Stdout          string        `json:"stdout"`
+	ParentProcesses []processData `json:"parent_process_data"`
 }
diff --git a/compiler_wrapper/disable_werror_flag_test.go b/compiler_wrapper/disable_werror_flag_test.go
index ecc1090..0c43dbe 100644
--- a/compiler_wrapper/disable_werror_flag_test.go
+++ b/compiler_wrapper/disable_werror_flag_test.go
@@ -500,3 +500,68 @@
 		}
 	})
 }
+
+func TestProcPidStatParsingWorksAsIntended(t *testing.T) {
+	t.Parallel()
+
+	type expected struct {
+		parent int
+		ok     bool
+	}
+
+	testCases := []struct {
+		input    string
+		expected expected
+	}{
+		{
+			input: "2556041 (cat) R 2519408 2556041 2519408 34818 2556041 4194304",
+			expected: expected{
+				parent: 2519408,
+				ok:     true,
+			},
+		},
+		{
+			input: "2556041 (c a t) R 2519408 2556041 2519408 34818 2556041 4194304",
+			expected: expected{
+				parent: 2519408,
+				ok:     true,
+			},
+		},
+		{
+			input: "",
+			expected: expected{
+				ok: false,
+			},
+		},
+		{
+			input: "foo (bar)",
+			expected: expected{
+				ok: false,
+			},
+		},
+		{
+			input: "foo (bar) baz",
+			expected: expected{
+				ok: false,
+			},
+		},
+		{
+			input: "foo (bar) baz 1qux2",
+			expected: expected{
+				ok: false,
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		parent, ok := parseParentPidFromPidStat(tc.input)
+		if tc.expected.ok != ok {
+			t.Errorf("Got ok=%v when parsing %q; expected %v", ok, tc.input, tc.expected.ok)
+			continue
+		}
+
+		if tc.expected.parent != parent {
+			t.Errorf("Got parent=%v when parsing %q; expected %v", parent, tc.input, tc.expected.parent)
+		}
+	}
+}