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
17package libcore.java.lang;
18
19import android.system.ErrnoException;
20import android.system.Os;
21import java.io.ByteArrayOutputStream;
22import java.io.File;
23import java.io.FileDescriptor;
24import java.io.FileWriter;
25import java.io.IOException;
26import java.io.InputStream;
27import java.io.OutputStream;
28import java.io.Writer;
29import java.lang.ProcessBuilder.Redirect;
30import java.lang.ProcessBuilder.Redirect.Type;
31import java.nio.charset.Charset;
32import java.util.Arrays;
33import java.util.Collections;
34import java.util.HashMap;
35import java.util.List;
36import java.util.Map;
37import java.util.concurrent.Future;
38import java.util.concurrent.FutureTask;
39import java.util.regex.Matcher;
40import java.util.regex.Pattern;
41import junit.framework.TestCase;
42import libcore.io.IoUtils;
43
44import static java.lang.ProcessBuilder.Redirect.INHERIT;
45import static java.lang.ProcessBuilder.Redirect.PIPE;
46
47public 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