Home | History | Annotate | Download | only in lang
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package libcore.java.lang;
     18 
     19 import android.system.ErrnoException;
     20 import android.system.Os;
     21 import java.io.ByteArrayOutputStream;
     22 import java.io.File;
     23 import java.io.FileDescriptor;
     24 import java.io.FileWriter;
     25 import java.io.IOException;
     26 import java.io.InputStream;
     27 import java.io.OutputStream;
     28 import java.io.Writer;
     29 import java.lang.ProcessBuilder.Redirect;
     30 import java.lang.ProcessBuilder.Redirect.Type;
     31 import java.nio.charset.Charset;
     32 import java.util.Arrays;
     33 import java.util.Collections;
     34 import java.util.HashMap;
     35 import java.util.List;
     36 import java.util.Map;
     37 import java.util.concurrent.Future;
     38 import java.util.concurrent.FutureTask;
     39 import java.util.regex.Matcher;
     40 import java.util.regex.Pattern;
     41 import junit.framework.TestCase;
     42 import libcore.io.IoUtils;
     43 
     44 import static java.lang.ProcessBuilder.Redirect.INHERIT;
     45 import static java.lang.ProcessBuilder.Redirect.PIPE;
     46 
     47 public class ProcessBuilderTest extends TestCase {
     48     private static final String TAG = ProcessBuilderTest.class.getSimpleName();
     49 
     50     /**
     51      * Returns the path to a command that is in /system/bin/ on Android but
     52      * /bin/ elsewhere.
     53      *
     54      * @param desktopPath the command path outside Android; must start with /bin/.
     55      */
     56     private static String commandPath(String desktopPath) {
     57         if (!desktopPath.startsWith("/bin/")) {
     58             throw new IllegalArgumentException(desktopPath);
     59         }
     60         String devicePath = System.getenv("ANDROID_ROOT") + desktopPath;
     61         return new File(devicePath).exists() ? devicePath : desktopPath;
     62     }
     63 
     64     private static String shell() {
     65         return commandPath("/bin/sh");
     66     }
     67 
     68     private static void assertRedirectErrorStream(boolean doRedirect,
     69             String expectedOut, String expectedErr) throws Exception {
     70         ProcessBuilder pb = new ProcessBuilder(shell(), "-c", "echo out; echo err 1>&2");
     71         pb.redirectErrorStream(doRedirect);
     72         checkProcessExecution(pb, ResultCodes.ZERO,
     73                 "" /* processInput */, expectedOut, expectedErr);
     74     }
     75 
     76     public void test_redirectErrorStream_true() throws Exception {
     77         assertRedirectErrorStream(true, "out\nerr\n", "");
     78     }
     79 
     80     public void test_redirectErrorStream_false() throws Exception {
     81         assertRedirectErrorStream(false, "out\n", "err\n");
     82     }
     83 
     84     public void testRedirectErrorStream_outputAndErrorAreMerged() throws Exception {
     85         Process process = new ProcessBuilder(shell())
     86                 .redirectErrorStream(true)
     87                 .start();
     88         try {
     89             long pid = getChildProcessPid(process);
     90             String path = "/proc/" + pid + "/fd/";
     91             assertEquals("stdout and stderr should point to the same socket",
     92                     Os.stat(path + "1").st_ino, Os.stat(path + "2").st_ino);
     93         } finally {
     94             process.destroy();
     95         }
     96     }
     97 
     98     /**
     99      * Tests that a child process can INHERIT this parent process's
    100      * stdin / stdout / stderr file descriptors.
    101      */
    102     public void testRedirectInherit() throws Exception {
    103         // We can't run shell() here because that exits when run with INHERITed
    104         // file descriptors from this process; "sleep" is less picky.
    105         Process process = new ProcessBuilder()
    106                 .command(commandPath("/bin/sleep"), "5") // in seconds
    107                 .redirectInput(Redirect.INHERIT)
    108                 .redirectOutput(Redirect.INHERIT)
    109                 .redirectError(Redirect.INHERIT)
    110                 .start();
    111         try {
    112             List<Long> parentInodes = Arrays.asList(
    113                     Os.fstat(FileDescriptor.in).st_ino,
    114                     Os.fstat(FileDescriptor.out).st_ino,
    115                     Os.fstat(FileDescriptor.err).st_ino);
    116             long childPid = getChildProcessPid(process);
    117             // Get the inode numbers of the ends of the symlink chains
    118             List<Long> childInodes = Arrays.asList(
    119                     Os.stat("/proc/" + childPid + "/fd/0").st_ino,
    120                     Os.stat("/proc/" + childPid + "/fd/1").st_ino,
    121                     Os.stat("/proc/" + childPid + "/fd/2").st_ino);
    122 
    123             assertEquals(parentInodes, childInodes);
    124         } catch (ErrnoException e) {
    125             // Either (a) Os.fstat on our PID, or (b) Os.stat on our child's PID, failed.
    126             throw new AssertionError("stat failed; child process: " + process, e);
    127         } finally {
    128             process.destroy();
    129         }
    130     }
    131 
    132     public void testRedirectFile_input() throws Exception {
    133         String inputFileContents = "process input for testing\n" + TAG;
    134         File file = File.createTempFile(TAG, "in");
    135         try (Writer writer = new FileWriter(file)) {
    136             writer.write(inputFileContents);
    137         }
    138         ProcessBuilder pb = new ProcessBuilder(shell(), "-c", "cat").redirectInput(file);
    139         checkProcessExecution(pb, ResultCodes.ZERO, /* processInput */ "",
    140                 /* expectedOutput */ inputFileContents, /* expectedError */ "");
    141         assertTrue(file.delete());
    142     }
    143 
    144     public void testRedirectFile_output() throws Exception {
    145         File file = File.createTempFile(TAG, "out");
    146         String processInput = TAG + "\narbitrary string for testing!";
    147         ProcessBuilder pb = new ProcessBuilder(shell(), "-c", "cat").redirectOutput(file);
    148         checkProcessExecution(pb, ResultCodes.ZERO, processInput,
    149                 /* expectedOutput */ "", /* expectedError */ "");
    150 
    151         String fileContents = new String(IoUtils.readFileAsByteArray(
    152                 file.getAbsolutePath()));
    153         assertEquals(processInput, fileContents);
    154         assertTrue(file.delete());
    155     }
    156 
    157     public void testRedirectFile_error() throws Exception {
    158         File file = File.createTempFile(TAG, "err");
    159         String processInput = "";
    160         String missingFilePath = "/test-missing-file-" + TAG;
    161         ProcessBuilder pb = new ProcessBuilder("ls", missingFilePath).redirectError(file);
    162         checkProcessExecution(pb, ResultCodes.NONZERO, processInput,
    163                 /* expectedOutput */ "", /* expectedError */ "");
    164 
    165         String fileContents = new String(IoUtils.readFileAsByteArray(file.getAbsolutePath()));
    166         assertTrue(file.delete());
    167         // We assume that the path of the missing file occurs in the ls stderr.
    168         assertTrue("Unexpected output: " + fileContents,
    169                 fileContents.contains(missingFilePath) && !fileContents.equals(missingFilePath));
    170     }
    171 
    172     public void testRedirectPipe_inputAndOutput() throws Exception {
    173         //checkProcessExecution(pb, expectedResultCode, processInput, expectedOutput, expectedError)
    174 
    175         String testString = "process input and output for testing\n" + TAG;
    176         {
    177             ProcessBuilder pb = new ProcessBuilder(shell(), "-c", "cat")
    178                     .redirectInput(PIPE)
    179                     .redirectOutput(PIPE);
    180             checkProcessExecution(pb, ResultCodes.ZERO, testString, testString, "");
    181         }
    182 
    183         // Check again without specifying PIPE explicitly, since that is the default
    184         {
    185         ProcessBuilder pb = new ProcessBuilder(shell(), "-c", "cat");
    186         checkProcessExecution(pb, ResultCodes.ZERO, testString, testString, "");
    187         }
    188 
    189         // Because the above test is symmetric regarding input vs. output, test
    190         // another case where input and output are different.
    191         {
    192             ProcessBuilder pb = new ProcessBuilder("echo", testString);
    193             checkProcessExecution(pb, ResultCodes.ZERO, "", testString + "\n", "");
    194         }
    195     }
    196 
    197     public void testRedirectPipe_error() throws Exception {
    198         String missingFilePath = "/test-missing-file-" + TAG;
    199 
    200         // Can't use checkProcessExecution() because we don't want to rely on an exact error content
    201         Process process = new ProcessBuilder("ls", missingFilePath)
    202                 .redirectError(Redirect.PIPE).start();
    203         process.getOutputStream().close(); // no process input
    204         int resultCode = process.waitFor();
    205         ResultCodes.NONZERO.assertMatches(resultCode);
    206         assertEquals("", readAsString(process.getInputStream())); // no process output
    207         String errorString = readAsString(process.getErrorStream());
    208         // We assume that the path of the missing file occurs in the ls stderr.
    209         assertTrue("Unexpected output: " + errorString,
    210                 errorString.contains(missingFilePath) && !errorString.equals(missingFilePath));
    211     }
    212 
    213     public void testRedirect_nullStreams() throws IOException {
    214         Process process = new ProcessBuilder()
    215                 .command(shell())
    216                 .inheritIO()
    217                 .start();
    218         try {
    219             assertNullInputStream(process.getInputStream());
    220             assertNullOutputStream(process.getOutputStream());
    221             assertNullInputStream(process.getErrorStream());
    222         } finally {
    223             process.destroy();
    224         }
    225     }
    226 
    227     public void testRedirectErrorStream_nullStream() throws IOException {
    228         Process process = new ProcessBuilder()
    229                 .command(shell())
    230                 .redirectErrorStream(true)
    231                 .start();
    232         try {
    233             assertNullInputStream(process.getErrorStream());
    234         } finally {
    235             process.destroy();
    236         }
    237     }
    238 
    239     public void testEnvironment() throws Exception {
    240         ProcessBuilder pb = new ProcessBuilder(shell(), "-c", "echo $A");
    241         pb.environment().put("A", "android");
    242         checkProcessExecution(pb, ResultCodes.ZERO, "", "android\n", "");
    243     }
    244 
    245     public void testDestroyClosesEverything() throws IOException {
    246         Process process = new ProcessBuilder(shell(), "-c", "echo out; echo err 1>&2").start();
    247         InputStream in = process.getInputStream();
    248         InputStream err = process.getErrorStream();
    249         OutputStream out = process.getOutputStream();
    250         process.destroy();
    251 
    252         try {
    253             in.read();
    254             fail();
    255         } catch (IOException expected) {
    256         }
    257         try {
    258             err.read();
    259             fail();
    260         } catch (IOException expected) {
    261         }
    262         try {
    263             /*
    264              * We test write+flush because the RI returns a wrapped stream, but
    265              * only bothers to close the underlying stream.
    266              */
    267             out.write(1);
    268             out.flush();
    269             fail();
    270         } catch (IOException expected) {
    271         }
    272     }
    273 
    274     public void testDestroyDoesNotLeak() throws IOException {
    275         Process process = new ProcessBuilder(shell(), "-c", "echo out; echo err 1>&2").start();
    276         process.destroy();
    277     }
    278 
    279     public void testEnvironmentMapForbidsNulls() throws Exception {
    280         ProcessBuilder pb = new ProcessBuilder(shell(), "-c", "echo $A");
    281         Map<String, String> environment = pb.environment();
    282         Map<String, String> before = new HashMap<String, String>(environment);
    283         try {
    284             environment.put("A", null);
    285             fail();
    286         } catch (NullPointerException expected) {
    287         }
    288         try {
    289             environment.put(null, "android");
    290             fail();
    291         } catch (NullPointerException expected) {
    292         }
    293         try {
    294             environment.containsKey(null);
    295             fail("Attempting to check the presence of a null key should throw");
    296         } catch (NullPointerException expected) {
    297         }
    298         try {
    299             environment.containsValue(null);
    300             fail("Attempting to check the presence of a null value should throw");
    301         } catch (NullPointerException expected) {
    302         }
    303         assertEquals(before, environment);
    304     }
    305 
    306     /**
    307      * Tests attempting to query the presence of a non-String key or value
    308      * in the environment map. Since that is a {@code Map<String, String>},
    309      * it's hard to imagine this ever breaking, but it's good to have a test
    310      * since it's called out in the documentation.
    311      */
    312     public void testEnvironmentMapForbidsNonStringKeysAndValues() {
    313         ProcessBuilder pb = new ProcessBuilder("echo", "Hello, world!");
    314         Map<String, String> environment = pb.environment();
    315         Integer nonString = Integer.valueOf(23);
    316         try {
    317             environment.containsKey(nonString);
    318             fail("Attempting to query the presence of a non-String key should throw");
    319         } catch (ClassCastException expected) {
    320         }
    321         try {
    322             environment.get(nonString);
    323             fail("Attempting to query the presence of a non-String key should throw");
    324         } catch (ClassCastException expected) {
    325         }
    326         try {
    327             environment.containsValue(nonString);
    328             fail("Attempting to query the presence of a non-String value should throw");
    329         } catch (ClassCastException expected) {
    330         }
    331     }
    332 
    333     /**
    334      * Checks that INHERIT and PIPE tend to have different hashCodes
    335      * in any particular instance of the runtime.
    336      * We test this by asserting that they use the identity hashCode,
    337      * which is a sufficient but not necessary condition for this.
    338      * If the implementation changes to a different sufficient condition
    339      * in future, this test should be updated accordingly.
    340      */
    341     public void testRedirect_inheritAndPipeTendToHaveDifferentHashCode() {
    342         assertIdentityHashCode(INHERIT);
    343         assertIdentityHashCode(PIPE);
    344     }
    345 
    346     public void testRedirect_hashCodeDependsOnFile() {
    347         File file = new File("/tmp/file");
    348         File otherFile = new File("/tmp/some_other_file") {
    349             @Override public int hashCode() { return 1 + file.hashCode(); }
    350         };
    351         Redirect a = Redirect.from(file);
    352         Redirect b = Redirect.from(otherFile);
    353         assertFalse("Unexpectedly equal hashCode: " + a + " vs. " + b,
    354                 a.hashCode() == b.hashCode());
    355     }
    356 
    357     /**
    358      * Tests that {@link Redirect}'s equals() and hashCode() is sane.
    359      */
    360     public void testRedirect_equals() {
    361         File fileA = new File("/tmp/fileA");
    362         File fileB = new File("/tmp/fileB");
    363         File fileB2 = new File("/tmp/fileB");
    364         // check that test is set up correctly
    365         assertFalse(fileA.equals(fileB));
    366         assertEquals(fileB, fileB2);
    367 
    368         assertSymmetricEquals(Redirect.appendTo(fileB), Redirect.appendTo(fileB2));
    369         assertSymmetricEquals(Redirect.from(fileB), Redirect.from(fileB2));
    370         assertSymmetricEquals(Redirect.to(fileB), Redirect.to(fileB2));
    371 
    372         Redirect[] redirects = new Redirect[] {
    373                 INHERIT,
    374                 PIPE,
    375                 Redirect.appendTo(fileA),
    376                 Redirect.from(fileA),
    377                 Redirect.to(fileA),
    378                 Redirect.appendTo(fileB),
    379                 Redirect.from(fileB),
    380                 Redirect.to(fileB),
    381         };
    382         for (Redirect a : redirects) {
    383             for (Redirect b : redirects) {
    384                 if (a != b) {
    385                     assertFalse("Unexpectedly equal: " + a + " vs. " + b, a.equals(b));
    386                     assertFalse("Unexpected asymmetric equality: " + a + " vs. " + b, b.equals(a));
    387                 }
    388             }
    389         }
    390     }
    391 
    392     /**
    393      * Tests the {@link Redirect#type() type} and {@link Redirect#file() file} of
    394      * various Redirects. These guarantees are made in the respective javadocs,
    395      * so we're testing them together here.
    396      */
    397     public void testRedirect_fileAndType() {
    398         File file = new File("/tmp/fake-file-for/java.lang.ProcessBuilderTest");
    399         assertRedirectFileAndType(null, Type.INHERIT, INHERIT);
    400         assertRedirectFileAndType(null, Type.PIPE, PIPE);
    401         assertRedirectFileAndType(file, Type.APPEND, Redirect.appendTo(file));
    402         assertRedirectFileAndType(file, Type.READ, Redirect.from(file));
    403         assertRedirectFileAndType(file, Type.WRITE, Redirect.to(file));
    404     }
    405 
    406     private static void assertRedirectFileAndType(File expectedFile, Type expectedType,
    407             Redirect redirect) {
    408         assertEquals(redirect.toString(), expectedFile, redirect.file());
    409         assertEquals(redirect.toString(), expectedType, redirect.type());
    410     }
    411 
    412     public void testRedirect_defaultsToPipe() {
    413         assertRedirects(PIPE, PIPE, PIPE, new ProcessBuilder());
    414     }
    415 
    416     public void testRedirect_setAndGet() {
    417         File file = new File("/tmp/fake-file-for/java.lang.ProcessBuilderTest");
    418         assertRedirects(Redirect.from(file), PIPE, PIPE, new ProcessBuilder().redirectInput(file));
    419         assertRedirects(PIPE, Redirect.to(file), PIPE, new ProcessBuilder().redirectOutput(file));
    420         assertRedirects(PIPE, PIPE, Redirect.to(file), new ProcessBuilder().redirectError(file));
    421         assertRedirects(Redirect.from(file), INHERIT, Redirect.to(file),
    422                 new ProcessBuilder()
    423                         .redirectInput(PIPE)
    424                         .redirectOutput(INHERIT)
    425                         .redirectError(file)
    426                         .redirectInput(file));
    427 
    428         assertRedirects(Redirect.INHERIT, Redirect.INHERIT, Redirect.INHERIT,
    429                 new ProcessBuilder().inheritIO());
    430     }
    431 
    432     public void testCommand_setAndGet() {
    433         List<String> expected = Collections.unmodifiableList(
    434                 Arrays.asList("echo", "fake", "command", "for", TAG));
    435         assertEquals(expected, new ProcessBuilder().command(expected).command());
    436         assertEquals(expected, new ProcessBuilder().command("echo", "fake", "command", "for", TAG)
    437                 .command());
    438     }
    439 
    440     public void testDirectory_setAndGet() {
    441         File directory = new File("/tmp/fake/directory/for/" + TAG);
    442         assertEquals(directory, new ProcessBuilder().directory(directory).directory());
    443         assertNull(new ProcessBuilder().directory());
    444         assertNull(new ProcessBuilder()
    445                 .directory(directory)
    446                 .directory(null)
    447                 .directory());
    448     }
    449 
    450     /**
    451      * One or more result codes returned by {@link Process#waitFor()}.
    452      */
    453     enum ResultCodes {
    454         ZERO { @Override void assertMatches(int actualResultCode) {
    455             assertEquals(0, actualResultCode);
    456         } },
    457         NONZERO { @Override void assertMatches(int actualResultCode) {
    458             assertTrue("Expected resultCode != 0, got 0", actualResultCode != 0);
    459         } };
    460 
    461         /** asserts that the given code falls within this ResultCodes */
    462         abstract void assertMatches(int actualResultCode);
    463     }
    464 
    465     /**
    466      * Starts the specified process, writes the specified input to it and waits for the process
    467      * to finish; then, then checks that the result code and output / error are expected.
    468      *
    469      * <p>This method assumes that the process consumes and produces character data encoded with
    470      * the platform default charset.
    471      */
    472     private static void checkProcessExecution(ProcessBuilder pb,
    473             ResultCodes expectedResultCode, String processInput,
    474             String expectedOutput, String expectedError) throws Exception {
    475         Process process = pb.start();
    476         Future<String> processOutput = asyncRead(process.getInputStream());
    477         Future<String> processError = asyncRead(process.getErrorStream());
    478         try (OutputStream outputStream = process.getOutputStream()) {
    479             outputStream.write(processInput.getBytes(Charset.defaultCharset()));
    480         }
    481         int actualResultCode = process.waitFor();
    482         expectedResultCode.assertMatches(actualResultCode);
    483         assertEquals(expectedOutput, processOutput.get());
    484         assertEquals(expectedError, processError.get());
    485     }
    486 
    487     /**
    488      * Asserts that inputStream is a <a href="ProcessBuilder#redirect-input">null input stream</a>.
    489      */
    490     private static void assertNullInputStream(InputStream inputStream) throws IOException {
    491         assertEquals(-1, inputStream.read());
    492         assertEquals(0, inputStream.available());
    493         inputStream.close(); // should do nothing
    494     }
    495 
    496     /**
    497      * Asserts that outputStream is a <a href="ProcessBuilder#redirect-output">null output
    498      * stream</a>.
    499      */
    500     private static void assertNullOutputStream(OutputStream outputStream) throws IOException {
    501         try {
    502             outputStream.write(42);
    503             fail("NullOutputStream.write(int) must throw IOException: " + outputStream);
    504         } catch (IOException expected) {
    505             // expected
    506         }
    507         outputStream.close(); // should do nothing
    508     }
    509 
    510     private static void assertRedirects(Redirect in, Redirect out, Redirect err, ProcessBuilder pb) {
    511         List<Redirect> expected = Arrays.asList(in, out, err);
    512         List<Redirect> actual = Arrays.asList(
    513                 pb.redirectInput(), pb.redirectOutput(), pb.redirectError());
    514         assertEquals(expected, actual);
    515     }
    516 
    517     private static void assertIdentityHashCode(Redirect redirect) {
    518         assertEquals(System.identityHashCode(redirect), redirect.hashCode());
    519     }
    520 
    521     private static void assertSymmetricEquals(Redirect a, Redirect b) {
    522         assertEquals(a, b);
    523         assertEquals(b, a);
    524         assertEquals(a.hashCode(), b.hashCode());
    525     }
    526 
    527     private static long getChildProcessPid(Process process) {
    528         // Hack: UNIXProcess.pid is private; parse toString() instead of reflection
    529         Matcher matcher = Pattern.compile("pid=(\\d+)").matcher(process.toString());
    530         assertTrue("Can't find PID in: " + process, matcher.find());
    531         long result = Integer.parseInt(matcher.group(1));
    532         return result;
    533     }
    534 
    535     static String readAsString(InputStream inputStream) throws IOException {
    536         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    537         byte[] data = new byte[1024];
    538         int numRead;
    539         while ((numRead = inputStream.read(data)) >= 0) {
    540             outputStream.write(data, 0, numRead);
    541         }
    542         return new String(outputStream.toByteArray(), Charset.defaultCharset());
    543     }
    544 
    545     /**
    546      * Reads the entire specified {@code inputStream} asynchronously.
    547      */
    548     static FutureTask<String> asyncRead(final InputStream inputStream) {
    549         final FutureTask<String> result = new FutureTask<>(() -> readAsString(inputStream));
    550         new Thread("read asynchronously from " + inputStream) {
    551             @Override
    552             public void run() {
    553                 result.run();
    554             }
    555         }.start();
    556         return result;
    557     }
    558 
    559 }
    560