Add class to fork and exec kati, based on the commandline option given.

Test: m product-config-test && java -jar out/host/linux-x86/testcases/product-config-test/product-config-test.jar
Change-Id: I4706e32ff7ac4424b6835b94fef40a2c838f8492
diff --git a/tools/product_config/src/com/android/build/config/Errors.java b/tools/product_config/src/com/android/build/config/Errors.java
index 63792c8..5424338 100644
--- a/tools/product_config/src/com/android/build/config/Errors.java
+++ b/tools/product_config/src/com/android/build/config/Errors.java
@@ -30,7 +30,7 @@
  * <b>Naming Convention:</b>
  * <ul>
  *  <li>ERROR_ for Categories with isLevelSettable false and Level.ERROR
- *  <li>WARNING_ for Categories with isLevelSettable false and default WARNING or HIDDEN
+ *  <li>WARNING_ for Categories with isLevelSettable true and default WARNING or HIDDEN
  *  <li>Don't have isLevelSettable true and not ERROR. (The constructor asserts this).
  * </ul>
  */
@@ -42,4 +42,7 @@
     public final Category WARNING_UNKNOWN_COMMAND_LINE_ERROR = new Category(2, true, Level.HIDDEN,
             "Passing unknown errors on the command line.  Hidden by default for\n"
             + "forward compatibility.");
+
+    public final Category ERROR_KATI = new Category(3, false, Level.ERROR,
+            "Error executing or reading from Kati.");
 }
diff --git a/tools/product_config/src/com/android/build/config/KatiCommand.java b/tools/product_config/src/com/android/build/config/KatiCommand.java
new file mode 100644
index 0000000..f3c71d2
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/KatiCommand.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.config;
+
+import java.util.Arrays;
+import java.util.List;
+
+public interface KatiCommand {
+    public static class KatiException extends Exception {
+        private String mStderr;
+
+        public KatiException(List<String> cmd, String stderr) {
+            super("Error running kati: " + Arrays.toString(cmd.toArray()));
+            mStderr = stderr;
+        }
+
+        public String getStderr() {
+            return mStderr;
+        }
+    }
+
+    /**
+     * Run kati directly. Returns stdout data.
+     *
+     * @throws KatiException if there is an error. KatiException will contain
+     * the stderr from the kati invocation.
+     */
+    public String run(String[] args) throws KatiException;
+}
diff --git a/tools/product_config/src/com/android/build/config/KatiCommandImpl.java b/tools/product_config/src/com/android/build/config/KatiCommandImpl.java
new file mode 100644
index 0000000..53480d4
--- /dev/null
+++ b/tools/product_config/src/com/android/build/config/KatiCommandImpl.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.build.config;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.nio.charset.StandardCharsets;
+
+public class KatiCommandImpl implements KatiCommand {
+    final Errors mErrors;
+    final Options mOptions;
+
+    /**
+     * Runnable that consumes all of an InputStream until EOF, writes the contents
+     * into a StringBuilder, and then closes the stream.
+     */
+    class OutputReader implements Runnable {
+        private final InputStream mStream;
+        private final StringBuilder mOutput;
+
+        OutputReader(InputStream stream, StringBuilder output) {
+            mStream = stream;
+            mOutput = output;
+        }
+
+        @Override
+        public void run() {
+            final char[] buf = new char[16*1024];
+            final InputStreamReader reader = new InputStreamReader(mStream, StandardCharsets.UTF_8);
+            try {
+                int amt;
+                while ((amt = reader.read(buf, 0, buf.length)) >= 0) {
+                    mOutput.append(buf, 0, amt);
+                }
+            } catch (IOException ex) {
+                mErrors.ERROR_KATI.add("Error reading from kati: " + ex.getMessage());
+            } finally {
+                try {
+                    reader.close();
+                } catch (IOException ex) {
+                    // Close doesn't throw
+                }
+            }
+        }
+    }
+
+    public KatiCommandImpl(Errors errors, Options options) {
+        mErrors = errors;
+        mOptions = options;
+    }
+
+    /**
+     * Run kati directly. Returns stdout data.
+     *
+     * @throws KatiException if there is an error. KatiException will contain
+     * the stderr from the kati invocation.
+     */
+    public String run(String[] args) throws KatiException {
+        final ArrayList<String> cmd = new ArrayList();
+        cmd.add(mOptions.getCKatiBin());
+        for (String arg: args) {
+            cmd.add(arg);
+        }
+
+        final ProcessBuilder builder = new ProcessBuilder(cmd);
+        builder.redirectOutput(ProcessBuilder.Redirect.PIPE);
+        builder.redirectError(ProcessBuilder.Redirect.PIPE);
+
+        Process process = null;
+
+        try {
+            process = builder.start();
+        } catch (IOException ex) {
+            throw new KatiException(cmd, "IOException running process: " + ex.getMessage());
+        }
+
+        final StringBuilder stdout = new StringBuilder();
+        final Thread stdoutThread = new Thread(new OutputReader(process.getInputStream(), stdout),
+                "kati_stdout_reader");
+        stdoutThread.start();
+
+        final StringBuilder stderr = new StringBuilder();
+        final Thread stderrThread = new Thread(new OutputReader(process.getErrorStream(), stderr),
+                "kati_stderr_reader");
+        stderrThread.start();
+
+        int returnCode = waitForProcess(process);
+        joinThread(stdoutThread);
+        joinThread(stderrThread);
+
+        if (returnCode != 0) {
+            throw new KatiException(cmd, stderr.toString());
+        }
+
+        return stdout.toString();
+    }
+
+    /**
+     * Wrap Process.waitFor() because it throws InterruptedException.
+     */
+    private static int waitForProcess(Process proc) {
+        while (true) {
+            try {
+                return proc.waitFor();
+            } catch (InterruptedException ex) {
+            }
+        }
+    }
+
+    /**
+     * Wrap Thread.join() because it throws InterruptedException.
+     */
+    private static void joinThread(Thread thread) {
+        while (true) {
+            try {
+                thread.join();
+                return;
+            } catch (InterruptedException ex) {
+            }
+        }
+    }
+}
+
diff --git a/tools/product_config/src/com/android/build/config/Main.java b/tools/product_config/src/com/android/build/config/Main.java
index b792193..e577dfe 100644
--- a/tools/product_config/src/com/android/build/config/Main.java
+++ b/tools/product_config/src/com/android/build/config/Main.java
@@ -47,7 +47,7 @@
         int exitCode = 0;
 
         try {
-            Options options = Options.parse(errors, args);
+            Options options = Options.parse(errors, args, System.getenv());
             if (errors.hadError()) {
                 Options.printHelp(System.err);
                 System.err.println();
@@ -62,7 +62,7 @@
                     Options.printHelp(System.out);
                     return;
             }
-        } catch (CommandException ex) {
+        } catch (CommandException | Errors.FatalException ex) {
             // These are user errors, so don't show a stack trace
             exitCode = 1;
         } catch (Throwable ex) {
diff --git a/tools/product_config/src/com/android/build/config/Options.java b/tools/product_config/src/com/android/build/config/Options.java
index 48146cb..4e60484 100644
--- a/tools/product_config/src/com/android/build/config/Options.java
+++ b/tools/product_config/src/com/android/build/config/Options.java
@@ -17,6 +17,7 @@
 package com.android.build.config;
 
 import java.io.PrintStream;
+import java.util.Map;
 import java.util.TreeMap;
 
 public class Options {
@@ -27,19 +28,50 @@
 
     private Action mAction = Action.DEFAULT;
 
+    private String mProduct;
+    private String mVariant;
+    private String mOutDir;
+    private String mCKatiBin;
+
     public Action getAction() {
         return mAction;
     }
 
+    public String getProduct() {
+        return mProduct;
+    }
+
+    public String getVariant() {
+        return mVariant;
+    }
+
+    public String getOutDir() {
+        return mOutDir != null ? mOutDir : "out";
+    }
+
+    public String getCKatiBin() {
+        return mCKatiBin;
+    }
+
     public static void printHelp(PrintStream out) {
         out.println("usage: product_config");
         out.println();
-        out.println("OPTIONS");
+        out.println("REQUIRED FLAGS");
+        out.println("  --ckati_bin CKATI        Kati binary to use.");
+        out.println();
+        out.println("OPTIONAL FLAGS");
         out.println("  --hide ERROR_ID          Suppress this error.");
         out.println("  --error ERROR_ID         Make this ERROR_ID a fatal error.");
         out.println("  --help -h                This message.");
         out.println("  --warning ERROR_ID       Make this ERROR_ID a warning.");
         out.println();
+        out.println("REQUIRED ENVIRONMENT");
+        out.println("  TARGET_PRODUCT           Product to build from lunch command.");
+        out.println("  TARGET_BUILD_VARIANT     Build variant from lunch command.");
+        out.println();
+        out.println("OPTIONAL ENVIRONMENT");
+        out.println("  OUT_DIR                  Build output directory. Defaults to \"out\".");
+        out.println();
         out.println("ERRORS");
         out.println("  The following are the errors that can be controlled on the");
         out.println("  commandline with the --hide --warning --error flags.");
@@ -63,20 +95,26 @@
 
         private Errors mErrors;
         private String[] mArgs;
+        private Map<String,String> mEnv;
         private Options mResult = new Options();
         private int mIndex;
+        private boolean mSkipRequiredArgValidation;
 
-        public Parser(Errors errors, String[] args) {
+        public Parser(Errors errors, String[] args, Map<String,String> env) {
             mErrors = errors;
             mArgs = args;
+            mEnv = env;
         }
 
         public Options parse() {
+            // Args
             try {
                 while (mIndex < mArgs.length) {
                     final String arg = mArgs[mIndex];
 
-                    if ("--hide".equals(arg)) {
+                    if ("--ckati_bin".equals(arg)) {
+                        mResult.mCKatiBin = requireNextStringArg(arg);
+                    } else if ("--hide".equals(arg)) {
                         handleErrorCode(arg, Errors.Level.HIDDEN);
                     } else if ("--error".equals(arg)) {
                         handleErrorCode(arg, Errors.Level.ERROR);
@@ -99,11 +137,45 @@
                 mErrors.ERROR_COMMAND_LINE.add(ex.getMessage());
             }
 
+            // Environment
+            mResult.mProduct = mEnv.get("TARGET_PRODUCT");
+            mResult.mVariant = mEnv.get("TARGET_BUILD_VARIANT");
+            mResult.mOutDir = mEnv.get("OUT_DIR");
+
+            validateArgs();
+
             return mResult;
         }
 
-        private void addWarning(Errors.Category category, String message) {
-            category.add(message);
+        /**
+         * For testing; don't generate errors about missing arguments
+         */
+        public void setSkipRequiredArgValidation() {
+            mSkipRequiredArgValidation = true;
+        }
+
+        private void validateArgs() {
+            if (!mSkipRequiredArgValidation) {
+                if (mResult.mCKatiBin == null || "".equals(mResult.mCKatiBin)) {
+                    addMissingArgError("--ckati_bin");
+                }
+                if (mResult.mProduct == null) {
+                    addMissingEnvError("TARGET_PRODUCT");
+                }
+                if (mResult.mVariant == null) {
+                    addMissingEnvError("TARGET_BUILD_VARIANT");
+                }
+            }
+        }
+
+        private void addMissingArgError(String argName) {
+            mErrors.ERROR_COMMAND_LINE.add("Required command line argument missing: "
+                    + argName);
+        }
+
+        private void addMissingEnvError(String envName) {
+            mErrors.ERROR_COMMAND_LINE.add("Required environment variable missing: "
+                    + envName);
         }
 
         private String getNextNonFlagArg() {
@@ -117,6 +189,14 @@
             return mArgs[mIndex];
         }
 
+        private String requireNextStringArg(String arg) throws ParseException {
+            final String val = getNextNonFlagArg();
+            if (val == null) {
+                throw new ParseException(arg + " requires a string argument.");
+            }
+            return val;
+        }
+
         private int requireNextNumberArg(String arg) throws ParseException {
             final String val = getNextNonFlagArg();
             if (val == null) {
@@ -151,7 +231,7 @@
      * <p>
      * Adds errors encountered to Errors object.
      */
-    public static Options parse(Errors errors, String[] args) {
-        return (new Parser(errors, args)).parse();
+    public static Options parse(Errors errors, String[] args, Map<String, String> env) {
+        return (new Parser(errors, args, env)).parse();
     }
 }
diff --git a/tools/product_config/test/com/android/build/config/OptionsTest.java b/tools/product_config/test/com/android/build/config/OptionsTest.java
index 2c36322..459efa5 100644
--- a/tools/product_config/test/com/android/build/config/OptionsTest.java
+++ b/tools/product_config/test/com/android/build/config/OptionsTest.java
@@ -19,12 +19,24 @@
 import org.junit.Assert;
 import org.junit.Test;
 
+import java.util.HashMap;
+
 public class OptionsTest {
+
+    private Options parse(Errors errors, String[] args) {
+        final HashMap<String, String> env = new HashMap();
+        env.put("TARGET_PRODUCT", "test_product");
+        env.put("TARGET_BUILD_VARIANT", "user");
+        final Options.Parser parser = new Options.Parser(errors, args, env);
+        parser.setSkipRequiredArgValidation();
+        return parser.parse();
+    }
+
     @Test
     public void testErrorMissingLast() {
         final Errors errors = new Errors();
 
-        final Options options = Options.parse(errors, new String[] {
+        final Options options = parse(errors, new String[] {
                     "--error"
                 });
 
@@ -37,7 +49,7 @@
     public void testErrorMissingNotLast() {
         final Errors errors = new Errors();
 
-        final Options options = Options.parse(errors, new String[] {
+        final Options options = parse(errors, new String[] {
                     "--error", "--warning", "2"
                 });
 
@@ -50,7 +62,7 @@
     public void testErrorNotNumeric() {
         final Errors errors = new Errors();
 
-        final Options options = Options.parse(errors, new String[] {
+        final Options options = parse(errors, new String[] {
                     "--error", "notgood"
                 });
 
@@ -63,7 +75,7 @@
     public void testErrorInvalidError() {
         final Errors errors = new Errors();
 
-        final Options options = Options.parse(errors, new String[] {
+        final Options options = parse(errors, new String[] {
                     "--error", "50000"
                 });
 
@@ -76,7 +88,7 @@
     public void testErrorOne() {
         final Errors errors = new Errors();
 
-        final Options options = Options.parse(errors, new String[] {
+        final Options options = parse(errors, new String[] {
                     "--error", "2"
                 });
 
@@ -89,7 +101,7 @@
     public void testWarningOne() {
         final Errors errors = new Errors();
 
-        final Options options = Options.parse(errors, new String[] {
+        final Options options = parse(errors, new String[] {
                     "--warning", "2"
                 });
 
@@ -102,7 +114,7 @@
     public void testHideOne() {
         final Errors errors = new Errors();
 
-        final Options options = Options.parse(errors, new String[] {
+        final Options options = parse(errors, new String[] {
                     "--hide", "2"
                 });
 
@@ -110,5 +122,16 @@
         Assert.assertEquals(Options.Action.DEFAULT, options.getAction());
         Assert.assertFalse(errors.hadWarningOrError());
     }
+
+    @Test
+    public void testEnv() {
+        final Errors errors = new Errors();
+
+        final Options options = parse(errors, new String[0]);
+
+        Assert.assertEquals("test_product", options.getProduct());
+        Assert.assertEquals("user", options.getVariant());
+        Assert.assertFalse(errors.hadWarningOrError());
+    }
 }