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