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