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      */
commandPath(String desktopPath)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 
shell()64     private static String shell() {
65         return commandPath("/bin/sh");
66     }
67 
assertRedirectErrorStream(boolean doRedirect, String expectedOut, String expectedErr)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 
test_redirectErrorStream_true()76     public void test_redirectErrorStream_true() throws Exception {
77         assertRedirectErrorStream(true, "out\nerr\n", "");
78     }
79 
test_redirectErrorStream_false()80     public void test_redirectErrorStream_false() throws Exception {
81         assertRedirectErrorStream(false, "out\n", "err\n");
82     }
83 
testRedirectErrorStream_outputAndErrorAreMerged()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      */
testRedirectInherit()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 
testRedirectFile_input()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 
testRedirectFile_output()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 
testRedirectFile_error()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 
testRedirectPipe_inputAndOutput()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 
testRedirectPipe_error()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 
testRedirect_nullStreams()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 
testRedirectErrorStream_nullStream()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 
testEnvironment()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 
testDestroyClosesEverything()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 
testDestroyDoesNotLeak()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 
testEnvironmentMapForbidsNulls()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")
testEnvironmentMapForbidsNonStringKeysAndValues()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      */
testRedirect_inheritAndPipeTendToHaveDifferentHashCode()342     public void testRedirect_inheritAndPipeTendToHaveDifferentHashCode() {
343         assertIdentityHashCode(INHERIT);
344         assertIdentityHashCode(PIPE);
345     }
346 
testRedirect_hashCodeDependsOnFile()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 useful.
360      */
testRedirect_equals()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      */
testRedirect_fileAndType()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 
assertRedirectFileAndType(File expectedFile, Type expectedType, Redirect redirect)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 
testRedirect_defaultsToPipe()413     public void testRedirect_defaultsToPipe() {
414         assertRedirects(PIPE, PIPE, PIPE, new ProcessBuilder());
415     }
416 
testRedirect_setAndGet()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 
testCommand_setAndGet()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 
testDirectory_setAndGet()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 {
assertMatches(int actualResultCode)455         ZERO { @Override void assertMatches(int actualResultCode) {
456             assertEquals(0, actualResultCode);
457         } },
assertMatches(int actualResultCode)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 */
assertMatches(int actualResultCode)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      */
checkProcessExecution(ProcessBuilder pb, ResultCodes expectedResultCode, String processInput, String expectedOutput, String expectedError)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      */
assertNullInputStream(InputStream inputStream)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      */
assertNullOutputStream(OutputStream outputStream)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 
assertRedirects(Redirect in, Redirect out, Redirect err, ProcessBuilder pb)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 
assertIdentityHashCode(Redirect redirect)518     private static void assertIdentityHashCode(Redirect redirect) {
519         assertEquals(System.identityHashCode(redirect), redirect.hashCode());
520     }
521 
assertSymmetricEquals(Redirect a, Redirect b)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 
getChildProcessPid(Process process)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 
readAsString(InputStream inputStream)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      */
asyncRead(final InputStream inputStream)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