buildman: Detect Kconfig loops

Hex and int Kconfig options are supposed to have defaults. This is so we
can configure U-Boot without having to enter particular values for the
items that don't have specific values in the board's defconfig file.

If this rule is not followed, then introducing a new Kconfig can produce
a loop like this:

   Break things (BREAK_ME) [] (NEW)
   Error in reading or end of file.

   Break things (BREAK_ME) [] (NEW)
   Error in reading or end of file.

The continues forever since buildman passes /dev/null to 'conf', and
the build system just tries again. Eventually there is so much output that
buildman runs out of memory.

We can detect this situation by looking for a symbol (like 'BREAK_ME')
which has no default (the '[]' above) and is marked as new. If this
appears multiple times in the output, we know something is wrong.

Add a filter function for the output which detects this situation. Allow
it to return True to terminate the process. Implement this termination in
cros_subprocess.

With this we get a nice message:

   buildman --board sandbox -T0
   Building current source for 1 boards (0 threads, 32 jobs per thread)
      sandbox:  w+   sandbox
   +.config:66:warning: symbol value '' invalid for BREAK_ME
   +
   +Error in reading or end of file.
   +make[3]: *** [scripts/kconfig/Makefile:75: syncconfig] Terminated
   +make[2]: *** [Makefile:569: syncconfig] Terminated
   +make: *** [Makefile:177: sub-make] Terminated
   +(** did you define an int/hex Kconfig with no default? **)

Signed-off-by: Simon Glass <sjg@chromium.org>
diff --git a/tools/patman/command.py b/tools/patman/command.py
index bf8ea6c..d54b1e0 100644
--- a/tools/patman/command.py
+++ b/tools/patman/command.py
@@ -49,7 +49,8 @@
 
 def RunPipe(pipe_list, infile=None, outfile=None,
             capture=False, capture_stderr=False, oneline=False,
-            raise_on_error=True, cwd=None, binary=False, **kwargs):
+            raise_on_error=True, cwd=None, binary=False,
+            output_func=None, **kwargs):
     """
     Perform a command pipeline, with optional input/output filenames.
 
@@ -63,6 +64,8 @@
         capture: True to capture output
         capture_stderr: True to capture stderr
         oneline: True to strip newline chars from output
+        output_func: Output function to call with each output fragment
+            (if it returns True the function terminates)
         kwargs: Additional keyword arguments to cros_subprocess.Popen()
     Returns:
         CommandResult object
@@ -105,7 +108,7 @@
 
     if capture:
         result.stdout, result.stderr, result.combined = (
-                last_pipe.CommunicateFilter(None))
+                last_pipe.CommunicateFilter(output_func))
         if result.stdout and oneline:
             result.output = result.stdout.rstrip(b'\r\n')
         result.return_code = last_pipe.wait()
diff --git a/tools/patman/cros_subprocess.py b/tools/patman/cros_subprocess.py
index fdd5138..88a4693 100644
--- a/tools/patman/cros_subprocess.py
+++ b/tools/patman/cros_subprocess.py
@@ -128,6 +128,9 @@
                         sys.stdout or sys.stderr.
                 data: a string containing the data
 
+            Returns:
+                True to terminate the process
+
         Note: The data read is buffered in memory, so do not use this
         method if the data size is large or unlimited.
 
@@ -175,6 +178,7 @@
             stderr = bytearray()
         combined = bytearray()
 
+        stop_now = False
         input_offset = 0
         while read_set or write_set:
             try:
@@ -212,7 +216,7 @@
                     stdout += data
                     combined += data
                     if output:
-                        output(sys.stdout, data)
+                        stop_now = output(sys.stdout, data)
             if self.stderr in rlist:
                 data = b''
                 # We will get an error on read if the pty is closed
@@ -227,7 +231,9 @@
                     stderr += data
                     combined += data
                     if output:
-                        output(sys.stderr, data)
+                        stop_now = output(sys.stderr, data)
+            if stop_now:
+                self.terminate()
 
         # All data exchanged.    Translate lists into strings.
         stdout = self.ConvertData(stdout)