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             int 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             int 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     @SuppressWarnings("CollectionIncompatibleType")
    313     public void testEnvironmentMapForbidsNonStringKeysAndValues() {
    314         ProcessBuilder pb = new ProcessBuilder("echo", "Hello, world!");
    315         Map<String, String> environment = pb.environment();
    316         Integer nonString = Integer.valueOf(23);
    317         try {
    318             environment.containsKey(nonString);
    319             fail("Attempting to query the presence of a non-String key should throw");
    320         } catch (ClassCastException expected) {
    321         }
    322         try {
    323             environment.get(nonString);
    324             fail("Attempting to query the presence of a non-String key should throw");
    325         } catch (ClassCastException expected) {
    326         }
    327         try {
    328             environment.containsValue(nonString);
    329             fail("Attempting to query the presence of a non-String value should throw");
    330         } catch (ClassCastException expected) {
    331         }
    332     }
    333 
    334     /**
    335      * Checks that INHERIT and PIPE tend to have different hashCodes
    336      * in any particular instance of the runtime.
    337      * We test this by asserting that they use the identity hashCode,
    338      * which is a sufficient but not necessary condition for this.
    339      * If the implementation changes to a different sufficient condition
    340      * in future, this test should be updated accordingly.
    341      */
    342     public void testRedirect_inheritAndPipeTendToHaveDifferentHashCode() {
    343         assertIdentityHashCode(INHERIT);
    344         assertIdentityHashCode(PIPE);
    345     }
    346 
    347     public void testRedirect_hashCodeDependsOnFile() {
    348         File file = new File("/tmp/file");
    349         File otherFile = new File("/tmp/some_other_file") {
    350             @Override public int hashCode() { return 1 + file.hashCode(); }
    351         };
    352         Redirect a = Redirect.from(file);
    353         Redirect b = Redirect.from(otherFile);
    354         assertFalse("Unexpectedly equal hashCode: " + a + " vs. " + b,
    355                 a.hashCode() == b.hashCode());
    356     }
    357 
    358     /**
    359      * Tests that {@link Redirect}'s equals() and hashCode() is sane.
    360      */
    361     public void testRedirect_equals() {
    362         File fileA = new File("/tmp/fileA");
    363         File fileB = new File("/tmp/fileB");
    364         File fileB2 = new File("/tmp/fileB");
    365         // check that test is set up correctly
    366         assertFalse(fileA.equals(fileB));
    367         assertEquals(fileB, fileB2);
    368 
    369         assertSymmetricEquals(Redirect.appendTo(fileB), Redirect.appendTo(fileB2));
    370         assertSymmetricEquals(Redirect.from(fileB), Redirect.from(fileB2));
    371         assertSymmetricEquals(Redirect.to(fileB), Redirect.to(fileB2));
    372 
    373         Redirect[] redirects = new Redirect[] {
    374                 INHERIT,
    375                 PIPE,
    376                 Redirect.appendTo(fileA),
    377                 Redirect.from(fileA),
    378                 Redirect.to(fileA),
    379                 Redirect.appendTo(fileB),
    380                 Redirect.from(fileB),
    381                 Redirect.to(fileB),
    382         };
    383         for (Redirect a : redirects) {
    384             for (Redirect b : redirects) {
    385                 if (a != b) {
    386                     assertFalse("Unexpectedly equal: " + a + " vs. " + b, a.equals(b));
    387                     assertFalse("Unexpected asymmetric equality: " + a + " vs. " + b, b.equals(a));
    388                 }
    389             }
    390         }
    391     }
    392 
    393     /**
    394      * Tests the {@link Redirect#type() type} and {@link Redirect#file() file} of
    395      * various Redirects. These guarantees are made in the respective javadocs,
    396      * so we're testing them together here.
    397      */
    398     public void testRedirect_fileAndType() {
    399         File file = new File("/tmp/fake-file-for/java.lang.ProcessBuilderTest");
    400         assertRedirectFileAndType(null, Type.INHERIT, INHERIT);
    401         assertRedirectFileAndType(null, Type.PIPE, PIPE);
    402         assertRedirectFileAndType(file, Type.APPEND, Redirect.appendTo(file));
    403         assertRedirectFileAndType(file, Type.READ, Redirect.from(file));
    404         assertRedirectFileAndType(file, Type.WRITE, Redirect.to(file));
    405     }
    406 
    407     private static void assertRedirectFileAndType(File expectedFile, Type expectedType,
    408             Redirect redirect) {
    409         assertEquals(redirect.toString(), expectedFile, redirect.file());
    410         assertEquals(redirect.toString(), expectedType, redirect.type());
    411     }
    412 
    413     public void testRedirect_defaultsToPipe() {
    414         assertRedirects(PIPE, PIPE, PIPE, new ProcessBuilder());
    415     }
    416 
    417     public void testRedirect_setAndGet() {
    418         File file = new File("/tmp/fake-file-for/java.lang.ProcessBuilderTest");
    419         assertRedirects(Redirect.from(file), PIPE, PIPE, new ProcessBuilder().redirectInput(file));
    420         assertRedirects(PIPE, Redirect.to(file), PIPE, new ProcessBuilder().redirectOutput(file));
    421         assertRedirects(PIPE, PIPE, Redirect.to(file), new ProcessBuilder().redirectError(file));
    422         assertRedirects(Redirect.from(file), INHERIT, Redirect.to(file),
    423                 new ProcessBuilder()
    424                         .redirectInput(PIPE)
    425                         .redirectOutput(INHERIT)
    426                         .redirectError(file)
    427                         .redirectInput(file));
    428 
    429         assertRedirects(Redirect.INHERIT, Redirect.INHERIT, Redirect.INHERIT,
    430                 new ProcessBuilder().inheritIO());
    431     }
    432 
    433     public void testCommand_setAndGet() {
    434         List<String> expected = Collections.unmodifiableList(
    435                 Arrays.asList("echo", "fake", "command", "for", TAG));
    436         assertEquals(expected, new ProcessBuilder().command(expected).command());
    437         assertEquals(expected, new ProcessBuilder().command("echo", "fake", "command", "for", TAG)
    438                 .command());
    439     }
    440 
    441     public void testDirectory_setAndGet() {
    442         File directory = new File("/tmp/fake/directory/for/" + TAG);
    443         assertEquals(directory, new ProcessBuilder().directory(directory).directory());
    444         assertNull(new ProcessBuilder().directory());
    445         assertNull(new ProcessBuilder()
    446                 .directory(directory)
    447                 .directory(null)
    448                 .directory());
    449     }
    450 
    451     /**
    452      * One or more result codes returned by {@link Process#waitFor()}.
    453      */
    454     enum ResultCodes {
    455         ZERO { @Override void assertMatches(int actualResultCode) {
    456             assertEquals(0, actualResultCode);
    457         } },
    458         NONZERO { @Override void assertMatches(int actualResultCode) {
    459             assertTrue("Expected resultCode != 0, got 0", actualResultCode != 0);
    460         } };
    461 
    462         /** asserts that the given code falls within this ResultCodes */
    463         abstract void assertMatches(int actualResultCode);
    464     }
    465 
    466     /**
    467      * Starts the specified process, writes the specified input to it and waits for the process
    468      * to finish; then, then checks that the result code and output / error are expected.
    469      *
    470      * <p>This method assumes that the process consumes and produces character data encoded with
    471      * the platform default charset.
    472      */
    473     private static void checkProcessExecution(ProcessBuilder pb,
    474             ResultCodes expectedResultCode, String processInput,
    475             String expectedOutput, String expectedError) throws Exception {
    476         Process process = pb.start();
    477         Future<String> processOutput = asyncRead(process.getInputStream());
    478         Future<String> processError = asyncRead(process.getErrorStream());
    479         try (OutputStream outputStream = process.getOutputStream()) {
    480             outputStream.write(processInput.getBytes(Charset.defaultCharset()));
    481         }
    482         int actualResultCode = process.waitFor();
    483         expectedResultCode.assertMatches(actualResultCode);
    484         assertEquals(expectedOutput, processOutput.get());
    485         assertEquals(expectedError, processError.get());
    486     }
    487 
    488     /**
    489      * Asserts that inputStream is a <a href="ProcessBuilder#redirect-input">null input stream</a>.
    490      */
    491     private static void assertNullInputStream(InputStream inputStream) throws IOException {
    492         assertEquals(-1, inputStream.read());
    493         assertEquals(0, inputStream.available());
    494         inputStream.close(); // should do nothing
    495     }
    496 
    497     /**
    498      * Asserts that outputStream is a <a href="ProcessBuilder#redirect-output">null output
    499      * stream</a>.
    500      */
    501     private static void assertNullOutputStream(OutputStream outputStream) throws IOException {
    502         try {
    503             outputStream.write(42);
    504             fail("NullOutputStream.write(int) must throw IOException: " + outputStream);
    505         } catch (IOException expected) {
    506             // expected
    507         }
    508         outputStream.close(); // should do nothing
    509     }
    510 
    511     private static void assertRedirects(Redirect in, Redirect out, Redirect err, ProcessBuilder pb) {
    512         List<Redirect> expected = Arrays.asList(in, out, err);
    513         List<Redirect> actual = Arrays.asList(
    514                 pb.redirectInput(), pb.redirectOutput(), pb.redirectError());
    515         assertEquals(expected, actual);
    516     }
    517 
    518     private static void assertIdentityHashCode(Redirect redirect) {
    519         assertEquals(System.identityHashCode(redirect), redirect.hashCode());
    520     }
    521 
    522     private static void assertSymmetricEquals(Redirect a, Redirect b) {
    523         assertEquals(a, b);
    524         assertEquals(b, a);
    525         assertEquals(a.hashCode(), b.hashCode());
    526     }
    527 
    528     private static int getChildProcessPid(Process process) {
    529         // Hack: UNIXProcess.pid is private; parse toString() instead of reflection
    530         Matcher matcher = Pattern.compile("pid=(\\d+)").matcher(process.toString());
    531         assertTrue("Can't find PID in: " + process, matcher.find());
    532         int result = Integer.parseInt(matcher.group(1));
    533         return result;
    534     }
    535 
    536     static String readAsString(InputStream inputStream) throws IOException {
    537         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    538         byte[] data = new byte[1024];
    539         int numRead;
    540         while ((numRead = inputStream.read(data)) >= 0) {
    541             outputStream.write(data, 0, numRead);
    542         }
    543         return new String(outputStream.toByteArray(), Charset.defaultCharset());
    544     }
    545 
    546     /**
    547      * Reads the entire specified {@code inputStream} asynchronously.
    548      */
    549     static FutureTask<String> asyncRead(final InputStream inputStream) {
    550         final FutureTask<String> result = new FutureTask<>(() -> readAsString(inputStream));
    551         new Thread("read asynchronously from " + inputStream) {
    552             @Override
    553             public void run() {
    554                 result.run();
    555             }
    556         }.start();
    557         return result;
    558     }
    559 
    560 }
    561