Adding input file redirects to RunUtils runCmd APIs.

Bug: 123529934
Change-Id: I71cf4b71043488a065bc6c7efbd19fcd0b961894
Merged-In: I71cf4b71043488a065bc6c7efbd19fcd0b961894
Fixes: 121326572
Test: RunUtilFuncTest
diff --git a/src/com/android/tradefed/util/IRunUtil.java b/src/com/android/tradefed/util/IRunUtil.java
index 45620aa..4113588 100644
--- a/src/com/android/tradefed/util/IRunUtil.java
+++ b/src/com/android/tradefed/util/IRunUtil.java
@@ -16,6 +16,7 @@
 
 package com.android.tradefed.util;
 
+import com.android.annotations.Nullable;
 import com.android.tradefed.command.CommandScheduler;
 
 import java.io.File;
@@ -170,9 +171,22 @@
     CommandResult runTimedCmdWithInput(long timeout, String input, List<String> command);
 
     /**
+     * Helper method to execute a system command that requires redirecting Stdin from a file, and
+     * aborting if it takes longer than a specified time.
+     *
+     * @param timeout maximum time to wait in ms
+     * @param inputRedirect the {@link File} to redirect as standard input using {@link
+     *     ProcessBuilder#redirectInput()}. If null, stdin won't be redirected.
+     * @param command the specified system command and optionally arguments to exec
+     * @return a {@link CommandResult} containing result from command run
+     */
+    CommandResult runTimedCmdWithInputRedirect(
+            long timeout, @Nullable File inputRedirect, String... command);
+
+    /**
      * Helper method to execute a system command asynchronously.
-     * <p/>
-     * Will return immediately after launching command.
+     *
+     * <p>Will return immediately after launching command.
      *
      * @param command the specified system command and optionally arguments to exec
      * @return the {@link Process} of the executed command
diff --git a/src/com/android/tradefed/util/RunUtil.java b/src/com/android/tradefed/util/RunUtil.java
index 20100a7..ce69df1 100644
--- a/src/com/android/tradefed/util/RunUtil.java
+++ b/src/com/android/tradefed/util/RunUtil.java
@@ -16,6 +16,7 @@
 
 package com.android.tradefed.util;
 
+import com.android.annotations.Nullable;
 import com.android.tradefed.command.CommandInterrupter;
 import com.android.tradefed.log.LogUtil.CLog;
 
@@ -59,9 +60,7 @@
 
     private final CommandInterrupter mInterrupter;
 
-    /**
-     * Create a new {@link RunUtil} object to use.
-     */
+    /** Create a new {@link RunUtil} object to use. */
     public RunUtil() {
         this(CommandInterrupter.INSTANCE);
     }
@@ -134,20 +133,16 @@
         mRedirectStderr = redirect;
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    /** {@inheritDoc} */
     @Override
     public CommandResult runTimedCmd(final long timeout, final String... command) {
         return runTimedCmd(timeout, null, null, command);
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    /** {@inheritDoc} */
     @Override
-    public CommandResult runTimedCmd(final long timeout, OutputStream stdout,
-            OutputStream stderr, final String... command) {
+    public CommandResult runTimedCmd(
+            final long timeout, OutputStream stdout, OutputStream stderr, final String... command) {
         RunnableResult osRunnable = createRunnableResult(stdout, stderr, command);
         CommandStatus status = runTimed(timeout, osRunnable, true);
         CommandResult result = osRunnable.getResult();
@@ -162,7 +157,12 @@
     @VisibleForTesting
     RunnableResult createRunnableResult(
             OutputStream stdout, OutputStream stderr, String... command) {
-        return new RunnableResult(null, createProcessBuilder(command), stdout, stderr);
+        return new RunnableResult(
+                /* input= */ null,
+                createProcessBuilder(command),
+                stdout,
+                stderr,
+                /* inputRedirect= */ null);
     }
 
     /** {@inheritDoc} */
@@ -214,21 +214,17 @@
         return processBuilder.command(commandList);
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    /** {@inheritDoc} */
     @Override
-    public CommandResult runTimedCmdWithInput(final long timeout, String input,
-            final String... command) {
+    public CommandResult runTimedCmdWithInput(
+            final long timeout, String input, final String... command) {
         return runTimedCmdWithInput(timeout, input, ArrayUtil.list(command));
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    /** {@inheritDoc} */
     @Override
-    public CommandResult runTimedCmdWithInput(final long timeout, String input,
-            final List<String> command) {
+    public CommandResult runTimedCmdWithInput(
+            final long timeout, String input, final List<String> command) {
         RunnableResult osRunnable = new RunnableResult(input, createProcessBuilder(command));
         CommandStatus status = runTimed(timeout, osRunnable, true);
         CommandResult result = osRunnable.getResult();
@@ -236,6 +232,23 @@
         return result;
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public CommandResult runTimedCmdWithInputRedirect(
+            final long timeout, @Nullable File inputRedirect, final String... command) {
+        RunnableResult osRunnable =
+                new RunnableResult(
+                        /* input= */ null,
+                        createProcessBuilder(command),
+                        /* stdout= */ null,
+                        /* stderr= */ null,
+                        inputRedirect);
+        CommandStatus status = runTimed(timeout, osRunnable, true);
+        CommandResult result = osRunnable.getResult();
+        result.setStatus(status);
+        return result;
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -305,12 +318,10 @@
         return process;
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    /** {@inheritDoc} */
     @Override
-    public CommandStatus runTimed(long timeout, IRunUtil.IRunnableResult runnable,
-            boolean logErrors) {
+    public CommandStatus runTimed(
+            long timeout, IRunUtil.IRunnableResult runnable, boolean logErrors) {
         mInterrupter.checkInterrupted();
         RunnableNotifier runThread = new RunnableNotifier(runnable, logErrors);
         if (logErrors) {
@@ -429,16 +440,14 @@
 
     /**
      * Retrieves the current system clock time.
-     * <p/>
-     * Exposed so it can be mocked for unit testing
+     *
+     * <p>Exposed so it can be mocked for unit testing
      */
     long getCurrentTime() {
         return System.currentTimeMillis();
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    /** {@inheritDoc} */
     @Override
     public void sleep(long time) {
         mInterrupter.checkInterrupted();
@@ -537,48 +546,50 @@
         private Process mProcess = null;
         private CountDownLatch mCountDown = null;
         private Thread mExecutionThread;
-        private OutputStream stdOut = null;
-        private OutputStream stdErr = null;
+        private OutputStream mStdOut = null;
+        private OutputStream mStdErr = null;
+        private final File mInputRedirect;
         private boolean mCreatedStdoutStream = false;
         private boolean mCreatedStderrStream = false;
         private final Object mLock = new Object();
         private boolean mCancelled = false;
 
         RunnableResult(final String input, final ProcessBuilder processBuilder) {
-            this(input, processBuilder, null, null);
+            this(input, processBuilder, null, null, null);
         }
 
         /**
          * Alternative constructor that allows redirecting the output to any Outputstream. Stdout
          * and stderr can be independently redirected to different Outputstream implementations. If
          * streams are null, default behavior of using a buffer will be used.
+         *
+         * <p>Additionally, Stdin can be redirected from a File.
          */
         RunnableResult(
                 final String input,
                 final ProcessBuilder processBuilder,
                 OutputStream stdoutStream,
-                OutputStream stderrStream) {
+                OutputStream stderrStream,
+                File inputRedirect) {
             mProcessBuilder = processBuilder;
             mInput = input;
+
+            mInputRedirect = inputRedirect;
+            if (mInputRedirect != null) {
+                // Set Stdin to mInputRedirect file.
+                mProcessBuilder.redirectInput(mInputRedirect);
+            }
+
             mCommandResult = new CommandResult();
             // Ensure the outputs are never null
             mCommandResult.setStdout("");
             mCommandResult.setStderr("");
             mCountDown = new CountDownLatch(1);
+
             // Redirect IO, so that the outputstream for the spawn process does not fill up
             // and cause deadlock.
-            if (stdoutStream != null) {
-                stdOut = stdoutStream;
-            } else {
-                stdOut = new ByteArrayOutputStream();
-                mCreatedStdoutStream = true;
-            }
-            if (stderrStream != null) {
-                stdErr = stderrStream;
-            } else {
-                stdErr = new ByteArrayOutputStream();
-                mCreatedStderrStream = true;
-            }
+            mStdOut = stdoutStream != null ? stdoutStream : new ByteArrayOutputStream();
+            mStdErr = stderrStream != null ? stderrStream : new ByteArrayOutputStream();
         }
 
         public CommandResult getResult() {
@@ -615,21 +626,21 @@
                 stdoutThread =
                         inheritIO(
                                 mProcess.getInputStream(),
-                                stdOut,
+                                mStdOut,
                                 String.format("inheritio-stdout-%s", mProcessBuilder.command()));
                 stderrThread =
                         inheritIO(
                                 mProcess.getErrorStream(),
-                                stdErr,
+                                mStdErr,
                                 String.format("inheritio-stderr-%s", mProcessBuilder.command()));
 
                 // Close the stdout/err streams if created by us. Streams provided by the caller
                 // should be closed by the caller.
                 if (mCreatedStdoutStream) {
-                    stdOut.close();
+                    mStdOut.close();
                 }
                 if (mCreatedStderrStream) {
-                    stdErr.close();
+                    mStdErr.close();
                 }
             }
             // Wait for process to complete.
@@ -650,17 +661,19 @@
                     mCommandResult.setExitCode(rc);
 
                     // Write out the streams to the result.
-                    if (stdOut instanceof ByteArrayOutputStream) {
-                        mCommandResult.setStdout(((ByteArrayOutputStream)stdOut).toString("UTF-8"));
+                    if (mStdOut instanceof ByteArrayOutputStream) {
+                        mCommandResult.setStdout(
+                                ((ByteArrayOutputStream) mStdOut).toString("UTF-8"));
                     } else {
                         mCommandResult.setStdout(
-                                "redirected to " + stdOut.getClass().getSimpleName());
+                                "redirected to " + mStdOut.getClass().getSimpleName());
                     }
-                    if (stdErr instanceof ByteArrayOutputStream) {
-                        mCommandResult.setStderr(((ByteArrayOutputStream)stdErr).toString("UTF-8"));
+                    if (mStdErr instanceof ByteArrayOutputStream) {
+                        mCommandResult.setStderr(
+                                ((ByteArrayOutputStream) mStdErr).toString("UTF-8"));
                     } else {
                         mCommandResult.setStderr(
-                                "redirected to " + stdErr.getClass().getSimpleName());
+                                "redirected to " + mStdErr.getClass().getSimpleName());
                     }
                 }
             } finally {
diff --git a/tests/src/com/android/tradefed/util/RunUtilFuncTest.java b/tests/src/com/android/tradefed/util/RunUtilFuncTest.java
index 030d576..9e5c198 100644
--- a/tests/src/com/android/tradefed/util/RunUtilFuncTest.java
+++ b/tests/src/com/android/tradefed/util/RunUtilFuncTest.java
@@ -22,6 +22,7 @@
 
 import java.io.BufferedWriter;
 import java.io.File;
+import java.io.FileOutputStream;
 import java.io.FileWriter;
 import java.io.IOException;
 import java.io.Writer;
@@ -209,4 +210,45 @@
                     t.getName().contains(RunUtil.INHERITIO_PREFIX));
         }
     }
+
+    /** Test running a command with redirecting input from a file. */
+    public void testRunTimedCmd_WithInputRedirect() throws IOException {
+        File inputRedirect = FileUtil.createTempFile("input_redirect", ".txt");
+
+        try {
+            FileUtil.writeToFile("TEST_INPUT", inputRedirect);
+
+            CommandResult result =
+                    RunUtil.getDefault()
+                            .runTimedCmdWithInputRedirect(SHORT_TIMEOUT_MS, inputRedirect, "cat");
+            assertTrue(CommandStatus.SUCCESS.equals(result.getStatus()));
+            assertEquals("TEST_INPUT", result.getStdout());
+
+        } finally {
+            FileUtil.deleteFile(inputRedirect);
+        }
+    }
+
+    /** Test running a command with redirecting output to a file. */
+    public void testRunTimedCmd_WithOutputRedirect() throws IOException {
+        File outputRedirect = FileUtil.createTempFile("output_redirect", ".txt");
+        FileOutputStream outputStream = new FileOutputStream(outputRedirect);
+        try {
+
+            CommandResult result =
+                    RunUtil.getDefault()
+                            .runTimedCmd(
+                                    SHORT_TIMEOUT_MS,
+                                    outputStream,
+                                    /* stderr= */ null,
+                                    "echo",
+                                    "TEST_OUTPUT");
+            assertTrue(CommandStatus.SUCCESS.equals(result.getStatus()));
+            assertEquals("TEST_OUTPUT\n", FileUtil.readStringFromFile(outputRedirect));
+
+        } finally {
+            FileUtil.deleteFile(outputRedirect);
+            outputStream.close();
+        }
+    }
 }
diff --git a/tests/src/com/android/tradefed/util/RunUtilTest.java b/tests/src/com/android/tradefed/util/RunUtilTest.java
index 9e74e66..f72ded8 100644
--- a/tests/src/com/android/tradefed/util/RunUtilTest.java
+++ b/tests/src/com/android/tradefed/util/RunUtilTest.java
@@ -107,12 +107,10 @@
                 mRunUtil.runTimed(SHORT_TIMEOUT_MS, mockRunnable, true));
     }
 
-    /**
-     * Test failure case for {@link RunUtil#runTimed(long, IRunnableResult, boolean)}.
-     */
+    /** Test failure case for {@link RunUtil#runTimed(long, IRunnableResult, boolean)}. */
     public void testRunTimed_failed() throws Exception {
-        IRunUtil.IRunnableResult mockRunnable = EasyMock.createStrictMock(
-                IRunUtil.IRunnableResult.class);
+        IRunUtil.IRunnableResult mockRunnable =
+                EasyMock.createStrictMock(IRunUtil.IRunnableResult.class);
         EasyMock.expect(mockRunnable.run()).andReturn(Boolean.FALSE);
         mockRunnable.cancel(); // always ensure execution is cancelled
         EasyMock.replay(mockRunnable);
@@ -120,12 +118,10 @@
                 mRunUtil.runTimed(SHORT_TIMEOUT_MS, mockRunnable, true));
     }
 
-    /**
-     * Test exception case for {@link RunUtil#runTimed(long, IRunnableResult, boolean)}.
-     */
+    /** Test exception case for {@link RunUtil#runTimed(long, IRunnableResult, boolean)}. */
     public void testRunTimed_exception() throws Exception {
-        IRunUtil.IRunnableResult mockRunnable = EasyMock.createStrictMock(
-                IRunUtil.IRunnableResult.class);
+        IRunUtil.IRunnableResult mockRunnable =
+                EasyMock.createStrictMock(IRunUtil.IRunnableResult.class);
         EasyMock.expect(mockRunnable.run()).andThrow(new RuntimeException());
         mockRunnable.cancel(); // cancel due to exception
         mockRunnable.cancel(); // always ensure execution is cancelled
@@ -325,10 +321,10 @@
         CommandResult result =
                 spyUtil.runTimedCmd(LONG_TIMEOUT_MS, stdoutStream, stderrStream, command);
         assertEquals(CommandStatus.SUCCESS, result.getStatus());
-        assertEquals(result.getStdout(),
-                "redirected to " + stdoutStream.getClass().getSimpleName());
-        assertEquals(result.getStderr(),
-                "redirected to " + stderrStream.getClass().getSimpleName());
+        assertEquals(
+                result.getStdout(), "redirected to " + stdoutStream.getClass().getSimpleName());
+        assertEquals(
+                result.getStderr(), "redirected to " + stderrStream.getClass().getSimpleName());
         assertTrue(stdout.exists());
         assertTrue(stderr.exists());
         try {
@@ -380,17 +376,13 @@
         CommandResult result =
                 spyUtil.runTimedCmd(SHORT_TIMEOUT_MS, stdoutStream, stderrStream, command);
         assertEquals(CommandStatus.SUCCESS, result.getStatus());
-        assertEquals(result.getStdout(),
-                "redirected to " + stdoutStream.getClass().getSimpleName());
-        assertEquals(result.getStderr(),
-                "redirected to " + stderrStream.getClass().getSimpleName());
+        assertEquals(
+                result.getStdout(), "redirected to " + stdoutStream.getClass().getSimpleName());
+        assertEquals(
+                result.getStderr(), "redirected to " + stderrStream.getClass().getSimpleName());
         assertTrue(stdout.exists());
         assertTrue(stderr.exists());
         try {
-// <<<<<<< HEAD
-//             assertEquals("TEST\n", FileUtil.readStringFromFile(stdout));
-//             assertEquals("", FileUtil.readStringFromFile(stderr));
-// =======
             assertEquals(CommandStatus.SUCCESS, result.getStatus());
             assertEquals(
                     result.getStdout(), "redirected to " + stdoutStream.getClass().getSimpleName());
@@ -400,7 +392,6 @@
             assertTrue(stderr.exists());
             assertEquals("TEST STDOUT\n", FileUtil.readStringFromFile(stdout));
             assertEquals("TEST STDERR\n", FileUtil.readStringFromFile(stderr));
-// >>>>>>> 7f6b1bf55... HostGTest: Merge stdout and stderr output and send to result parser
         } catch (IOException e) {
             fail(e.getMessage());
         } finally {
@@ -444,9 +435,7 @@
         assertTrue(success);
     }
 
-    /**
-     * Test whether a {@link RunUtil#setInterruptibleInFuture} has not change the state yet.
-     */
+    /** Test whether a {@link RunUtil#setInterruptibleInFuture} has not change the state yet. */
     public void testSetInterruptibleInFuture_beforeTimeout() {
         mRunUtil.allowInterrupt(false);
         assertFalse(mRunUtil.isInterruptAllowed());
@@ -459,9 +448,7 @@
         assertTrue(mRunUtil.isInterruptAllowed());
     }
 
-    /**
-     * Test {@link RunUtil#setEnvVariablePriority(EnvPriority)} properly prioritize unset.
-     */
+    /** Test {@link RunUtil#setEnvVariablePriority(EnvPriority)} properly prioritize unset. */
     public void testUnsetPriority() {
         final String ENV_NAME = "TF_GLO";
         RunUtil testRunUtil = new RunUtil();