summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorElliott Hughes <enh@google.com>2013-05-07 14:47:39 -0700
committerElliott Hughes <enh@google.com>2013-05-07 14:47:39 -0700
commitfd6fca42f9be9cf158cd6dd2e4c426f20d0e426f (patch)
tree8c4173a07f0960e97c9979316cbd8decc63bc0cd
parentf7e1ba4804f7365e6ab47a34e019532b428b1546 (diff)
downloadlibcore-fd6fca42f9be9cf158cd6dd2e4c426f20d0e426f.zip
libcore-fd6fca42f9be9cf158cd6dd2e4c426f20d0e426f.tar.gz
libcore-fd6fca42f9be9cf158cd6dd2e4c426f20d0e426f.tar.bz2
Improve ProcessManager.
Previously we'd leave zombies around if chdir(2) or execvp(2) failed. We also used to unnecessarily make 32Ki close(2) calls for every child. This patch fixes both problems and makes the tests less meaningless. Bug: https://code.google.com/p/android/issues/detail?id=55017 Change-Id: I23ecc84aedd6650dbe6df19b4b0c2e6429578e50
-rw-r--r--luni/src/main/native/java_lang_ProcessManager.cpp358
-rw-r--r--luni/src/test/java/tests/api/java/lang/ProcessTest.java265
2 files changed, 283 insertions, 340 deletions
diff --git a/luni/src/main/native/java_lang_ProcessManager.cpp b/luni/src/main/native/java_lang_ProcessManager.cpp
index e3f0f45..783fbd1 100644
--- a/luni/src/main/native/java_lang_ProcessManager.cpp
+++ b/luni/src/main/native/java_lang_ProcessManager.cpp
@@ -16,223 +16,239 @@
#define LOG_TAG "ProcessManager"
-#include <sys/resource.h>
-#include <sys/types.h>
-#include <unistd.h>
+#include <dirent.h>
+#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
-#include <errno.h>
+#include <sys/resource.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include "cutils/log.h"
#include "jni.h"
-#include "JNIHelp.h"
#include "JniConstants.h"
+#include "JNIHelp.h"
#include "Portability.h"
#include "ScopedLocalRef.h"
-#include "cutils/log.h"
#include "toStringArray.h"
-/** Close all open fds > 2 (i.e. everything but stdin/out/err), != skipFd. */
-static void closeNonStandardFds(int skipFd1, int skipFd2) {
- // TODO: rather than close all these non-open files, we could look in /proc/self/fd.
- rlimit rlimit;
- getrlimit(RLIMIT_NOFILE, &rlimit);
- const int max_fd = rlimit.rlim_max;
- for (int fd = 3; fd < max_fd; ++fd) {
- if (fd != skipFd1 && fd != skipFd2) {
- close(fd);
- }
+static void CloseNonStandardFds(int status_pipe_fd) {
+ // On Cygwin, Linux, and Solaris, the best way to close iterates over "/proc/self/fd/".
+ const char* fd_path = "/proc/self/fd";
+#ifdef __APPLE__
+ // On Mac OS, there's "/dev/fd/" which Linux seems to link to "/proc/self/fd/",
+ // but which on Solaris appears to be something quite different.
+ fd_path = "/dev/fd";
+#endif
+
+ // Keep track of the system properties fd so we don't close it.
+ int properties_fd = -1;
+ char* properties_fd_string = getenv("ANDROID_PROPERTY_WORKSPACE");
+ if (properties_fd_string != NULL) {
+ properties_fd = atoi(properties_fd_string);
+ }
+
+ DIR* d = opendir(fd_path);
+ int dir_fd = dirfd(d);
+ dirent* e;
+ while ((e = readdir(d)) != NULL) {
+ char* end;
+ int fd = strtol(e->d_name, &end, 10);
+ if (!*end) {
+ if (fd > STDERR_FILENO && fd != dir_fd && fd != status_pipe_fd && fd != properties_fd) {
+ close(fd);
+ }
}
+ }
+ closedir(d);
}
-#define PIPE_COUNT (4) // number of pipes used to communicate with child proc
-
-/** Closes all pipes in the given array. */
-static void closePipes(int pipes[], int skipFd) {
- for (int i = 0; i < PIPE_COUNT * 2; i++) {
- int fd = pipes[i];
- if (fd == -1) {
- return;
- }
- if (fd != skipFd) {
- close(pipes[i]);
- }
+#define PIPE_COUNT 4 // Number of pipes used to communicate with child.
+
+static void ClosePipes(int pipes[], int skip_fd) {
+ for (int i = 0; i < PIPE_COUNT * 2; i++) {
+ int fd = pipes[i];
+ if (fd != -1 && fd != skip_fd) {
+ close(pipes[i]);
}
+ }
}
-/** Executes a command in a child process. */
-static pid_t executeProcess(JNIEnv* env, char** commands, char** environment,
- const char* workingDirectory, jobject inDescriptor,
- jobject outDescriptor, jobject errDescriptor,
- jboolean redirectErrorStream) {
-
- // Keep track of the system properties fd so we don't close it.
- int androidSystemPropertiesFd = -1;
- char* fdString = getenv("ANDROID_PROPERTY_WORKSPACE");
- if (fdString) {
- androidSystemPropertiesFd = atoi(fdString);
- }
+static void AbortChild(int status_pipe_fd) {
+ int error = errno;
+ TEMP_FAILURE_RETRY(write(status_pipe_fd, &error, sizeof(int)));
+ close(status_pipe_fd);
+ _exit(127);
+}
- // Create 4 pipes: stdin, stdout, stderr, and an exec() status pipe.
- int pipes[PIPE_COUNT * 2] = { -1, -1, -1, -1, -1, -1, -1, -1 };
- for (int i = 0; i < PIPE_COUNT; i++) {
- if (pipe(pipes + i * 2) == -1) {
- jniThrowIOException(env, errno);
- closePipes(pipes, -1);
- return -1;
- }
+/** Executes a command in a child process. */
+static pid_t ExecuteProcess(JNIEnv* env, char** commands, char** environment,
+ const char* workingDirectory, jobject inDescriptor,
+ jobject outDescriptor, jobject errDescriptor,
+ jboolean redirectErrorStream) {
+
+ // Create 4 pipes: stdin, stdout, stderr, and an exec() status pipe.
+ int pipes[PIPE_COUNT * 2] = { -1, -1, -1, -1, -1, -1, -1, -1 };
+ for (int i = 0; i < PIPE_COUNT; i++) {
+ if (pipe(pipes + i * 2) == -1) {
+ jniThrowIOException(env, errno);
+ ClosePipes(pipes, -1);
+ return -1;
}
- int stdinIn = pipes[0];
- int stdinOut = pipes[1];
- int stdoutIn = pipes[2];
- int stdoutOut = pipes[3];
- int stderrIn = pipes[4];
- int stderrOut = pipes[5];
- int statusIn = pipes[6];
- int statusOut = pipes[7];
-
- pid_t childPid = fork();
-
- // If fork() failed...
- if (childPid == -1) {
- jniThrowIOException(env, errno);
- closePipes(pipes, -1);
- return -1;
+ }
+ int stdinIn = pipes[0];
+ int stdinOut = pipes[1];
+ int stdoutIn = pipes[2];
+ int stdoutOut = pipes[3];
+ int stderrIn = pipes[4];
+ int stderrOut = pipes[5];
+ int statusIn = pipes[6];
+ int statusOut = pipes[7];
+
+ pid_t childPid = fork();
+
+ // If fork() failed...
+ if (childPid == -1) {
+ jniThrowIOException(env, errno);
+ ClosePipes(pipes, -1);
+ return -1;
+ }
+
+ // If this is the child process...
+ if (childPid == 0) {
+ // Note: We cannot malloc(3) or free(3) after this point!
+ // A thread in the parent that no longer exists in the child may have held the heap lock
+ // when we forked, so an attempt to malloc(3) or free(3) would result in deadlock.
+
+ // Replace stdin, out, and err with pipes.
+ dup2(stdinIn, 0);
+ dup2(stdoutOut, 1);
+ if (redirectErrorStream) {
+ dup2(stdoutOut, 2);
+ } else {
+ dup2(stderrOut, 2);
}
- // If this is the child process...
- if (childPid == 0) {
- /*
- * Note: We cannot malloc(3) or free(3) after this point!
- * A thread in the parent that no longer exists in the child may have held the heap lock
- * when we forked, so an attempt to malloc(3) or free(3) would result in deadlock.
- */
-
- // Replace stdin, out, and err with pipes.
- dup2(stdinIn, 0);
- dup2(stdoutOut, 1);
- if (redirectErrorStream) {
- dup2(stdoutOut, 2);
- } else {
- dup2(stderrOut, 2);
- }
-
- // Close all but statusOut. This saves some work in the next step.
- closePipes(pipes, statusOut);
-
- // Make statusOut automatically close if execvp() succeeds.
- fcntl(statusOut, F_SETFD, FD_CLOEXEC);
-
- // Close remaining unwanted open fds.
- closeNonStandardFds(statusOut, androidSystemPropertiesFd);
-
- // Switch to working directory.
- if (workingDirectory != NULL) {
- if (chdir(workingDirectory) == -1) {
- goto execFailed;
- }
- }
-
- // Set up environment.
- if (environment != NULL) {
- extern char** environ; // Standard, but not in any header file.
- environ = environment;
- }
-
- // Execute process. By convention, the first argument in the arg array
- // should be the command itself. In fact, I get segfaults when this
- // isn't the case.
- execvp(commands[0], commands);
-
- // If we got here, execvp() failed or the working dir was invalid.
-execFailed:
- int error = errno;
- write(statusOut, &error, sizeof(int));
- close(statusOut);
- exit(error);
- }
+ // Close all but statusOut. This saves some work in the next step.
+ ClosePipes(pipes, statusOut);
- // This is the parent process.
+ // Make statusOut automatically close if execvp() succeeds.
+ fcntl(statusOut, F_SETFD, FD_CLOEXEC);
- // Close child's pipe ends.
- close(stdinIn);
- close(stdoutOut);
- close(stderrOut);
- close(statusOut);
+ // Close remaining unwanted open fds.
+ CloseNonStandardFds(statusOut);
- // Check status pipe for an error code. If execvp() succeeds, the other
- // end of the pipe should automatically close, in which case, we'll read
- // nothing.
- int result;
- int count = read(statusIn, &result, sizeof(int));
- close(statusIn);
- if (count > 0) {
- jniThrowIOException(env, result);
+ // Switch to working directory.
+ if (workingDirectory != NULL) {
+ if (chdir(workingDirectory) == -1) {
+ AbortChild(statusOut);
+ }
+ }
- close(stdoutIn);
- close(stdinOut);
- close(stderrIn);
+ // Set up environment.
+ if (environment != NULL) {
+ extern char** environ; // Standard, but not in any header file.
+ environ = environment;
+ }
- return -1;
+ // Execute process. By convention, the first argument in the arg array
+ // should be the command itself.
+ execvp(commands[0], commands);
+ AbortChild(statusOut);
+ }
+
+ // This is the parent process.
+
+ // Close child's pipe ends.
+ close(stdinIn);
+ close(stdoutOut);
+ close(stderrOut);
+ close(statusOut);
+
+ // Check status pipe for an error code. If execvp(2) succeeds, the other
+ // end of the pipe should automatically close, in which case, we'll read
+ // nothing.
+ int child_errno;
+ ssize_t count = TEMP_FAILURE_RETRY(read(statusIn, &child_errno, sizeof(int)));
+ close(statusIn);
+ if (count > 0) {
+ // chdir(2) or execvp(2) in the child failed.
+ // TODO: track which so we can be more specific in the detail message.
+ jniThrowIOException(env, child_errno);
+
+ close(stdoutIn);
+ close(stdinOut);
+ close(stderrIn);
+
+ // Reap our zombie child right away.
+ int status;
+ int rc = TEMP_FAILURE_RETRY(waitpid(childPid, &status, 0));
+ if (rc == -1) {
+ ALOGW("waitpid on failed exec failed: %s", strerror(errno));
}
- // Fill in file descriptor wrappers.
- jniSetFileDescriptorOfFD(env, inDescriptor, stdoutIn);
- jniSetFileDescriptorOfFD(env, outDescriptor, stdinOut);
- jniSetFileDescriptorOfFD(env, errDescriptor, stderrIn);
+ return -1;
+ }
+
+ // Fill in file descriptor wrappers.
+ jniSetFileDescriptorOfFD(env, inDescriptor, stdoutIn);
+ jniSetFileDescriptorOfFD(env, outDescriptor, stdinOut);
+ jniSetFileDescriptorOfFD(env, errDescriptor, stderrIn);
- return childPid;
+ return childPid;
}
/**
- * Converts Java String[] to char** and delegates to executeProcess().
+ * Converts Java String[] to char** and delegates to ExecuteProcess().
*/
static pid_t ProcessManager_exec(JNIEnv* env, jclass, jobjectArray javaCommands,
- jobjectArray javaEnvironment, jstring javaWorkingDirectory,
- jobject inDescriptor, jobject outDescriptor, jobject errDescriptor,
- jboolean redirectErrorStream) {
+ jobjectArray javaEnvironment, jstring javaWorkingDirectory,
+ jobject inDescriptor, jobject outDescriptor, jobject errDescriptor,
+ jboolean redirectErrorStream) {
- // Copy commands into char*[].
- char** commands = convertStrings(env, javaCommands);
+ // Copy commands into char*[].
+ char** commands = convertStrings(env, javaCommands);
- // Extract working directory string.
- const char* workingDirectory = NULL;
- if (javaWorkingDirectory != NULL) {
- workingDirectory = env->GetStringUTFChars(javaWorkingDirectory, NULL);
- }
+ // Extract working directory string.
+ const char* workingDirectory = NULL;
+ if (javaWorkingDirectory != NULL) {
+ workingDirectory = env->GetStringUTFChars(javaWorkingDirectory, NULL);
+ }
- // Convert environment array.
- char** environment = convertStrings(env, javaEnvironment);
+ // Convert environment array.
+ char** environment = convertStrings(env, javaEnvironment);
- pid_t result = executeProcess(env, commands, environment, workingDirectory,
- inDescriptor, outDescriptor, errDescriptor, redirectErrorStream);
+ pid_t result = ExecuteProcess(env, commands, environment, workingDirectory,
+ inDescriptor, outDescriptor, errDescriptor, redirectErrorStream);
- // Temporarily clear exception so we can clean up.
- jthrowable exception = env->ExceptionOccurred();
- env->ExceptionClear();
+ // Temporarily clear exception so we can clean up.
+ jthrowable exception = env->ExceptionOccurred();
+ env->ExceptionClear();
- freeStrings(env, javaEnvironment, environment);
+ freeStrings(env, javaEnvironment, environment);
- // Clean up working directory string.
- if (javaWorkingDirectory != NULL) {
- env->ReleaseStringUTFChars(javaWorkingDirectory, workingDirectory);
- }
+ // Clean up working directory string.
+ if (javaWorkingDirectory != NULL) {
+ env->ReleaseStringUTFChars(javaWorkingDirectory, workingDirectory);
+ }
- freeStrings(env, javaCommands, commands);
+ freeStrings(env, javaCommands, commands);
- // Re-throw exception if present.
- if (exception != NULL) {
- if (env->Throw(exception) < 0) {
- ALOGE("Error rethrowing exception!");
- }
+ // Re-throw exception if present.
+ if (exception != NULL) {
+ if (env->Throw(exception) < 0) {
+ ALOGE("Error rethrowing exception!");
}
+ }
- return result;
+ return result;
}
-static JNINativeMethod methods[] = {
- NATIVE_METHOD(ProcessManager, exec, "([Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;Ljava/io/FileDescriptor;Ljava/io/FileDescriptor;Ljava/io/FileDescriptor;Z)I"),
+static JNINativeMethod gMethods[] = {
+ NATIVE_METHOD(ProcessManager, exec, "([Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;Ljava/io/FileDescriptor;Ljava/io/FileDescriptor;Ljava/io/FileDescriptor;Z)I"),
};
void register_java_lang_ProcessManager(JNIEnv* env) {
- jniRegisterNativeMethods(env, "java/lang/ProcessManager", methods, NELEM(methods));
+ jniRegisterNativeMethods(env, "java/lang/ProcessManager", gMethods, NELEM(gMethods));
}
diff --git a/luni/src/test/java/tests/api/java/lang/ProcessTest.java b/luni/src/test/java/tests/api/java/lang/ProcessTest.java
index f875dc0..b1a331a 100644
--- a/luni/src/test/java/tests/api/java/lang/ProcessTest.java
+++ b/luni/src/test/java/tests/api/java/lang/ProcessTest.java
@@ -17,192 +17,119 @@
package tests.api.java.lang;
-import dalvik.annotation.BrokenTest;
-
+import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.util.ArrayList;
public class ProcessTest extends junit.framework.TestCase {
- /**
- * java.lang.Process#getInputStream()
- */
- public void test_getInputStream() {
- try {
- // Test for:
- //Object[] execArgs = Support_Exec.execJava2(
- // new String[] { "tests.support.Support_AvailTest" }, null,
- // true);
- //Process proc = (Process) execArgs[0];
-
- String[] commands = { "sleep", "1"};
- Process proc = Runtime.getRuntime().exec(commands, null, null);
-
- OutputStream os = proc.getOutputStream();
-
- // first number indicates total stream length
- // second number indicates length of data after second space
- // this will allow us to verify length at start, middle, and end
- os.write("10 5 abcde".getBytes());
- os.close();
-
- InputStream is = proc.getInputStream();
- StringBuffer msg = new StringBuffer("");
- while (true) {
- int c = is.read();
- if (c == -1)
- break;
- msg.append((char) c);
- }
- is.close();
- proc.waitFor();
- //Support_Exec.checkStderr(execArgs);
- proc.destroy();
- assertEquals("true", msg.toString(), msg.toString());
- } catch (IOException e) {
- fail("IOException executing avail test: " + e);
- } catch (InterruptedException e) {
- fail("InterruptedException executing avail test: " + e);
- }
+ public void test_55017() throws Exception {
+ ArrayList<Process> children = new ArrayList<Process>();
+ for (int i = 0; i < 256; ++i) {
+ try {
+ children.add(Runtime.getRuntime().exec(new String[] { "/system/bin/does-not-exist" }, null, null));
+ System.gc();
+ } catch (IOException expected) {
+ }
}
-
- /**
- * java.lang.Process#getOutputStream()
- */
- public void test_getOutputStream() {
- try {
- String[] commands = { "sleep", "1"};
- Process proc = Runtime.getRuntime().exec(commands, null, null);
- OutputStream os = proc.getOutputStream();
- // send data, and check if it is echoed back correctly
- String str1 = "Some data for testing communication between processes\n";
- String str2 = "More data that serves the same purpose.\n";
- String str3 = "Here is some more data.\n";
- os.write(str1.getBytes());
- try {
- Thread.sleep(2000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- os.close();
- InputStream is = proc.getInputStream();
- StringBuffer msg = new StringBuffer("");
- while (true) {
- int c = is.read();
- if (c == -1)
- break;
- msg.append((char) c);
- }
- is.close();
- proc.waitFor();
- //Support_Exec.checkStderr(execArgs);
- proc.destroy();
- String org = str1;
- String recvd = msg.toString();
- // Doesn't pass on RI
- // assertTrue("Data returned did not match data sent. Received: '"
- // + recvd + "' sent: '" + org + "'", recvd.equals(org));
- } catch (IOException e) {
- fail("IOException executing avail test: " + e);
- } catch (InterruptedException e) {
- fail("InterruptedException executing avail test: " + e);
- }
+ assertEquals(0, children.size());
+
+ boolean onDevice = new File("/system/bin").exists();
+ String[] psCommand = onDevice ? new String[] { "ps" } : new String[] { "ps", "s" };
+ Process ps = Runtime.getRuntime().exec(psCommand, null, null);
+ int zombieCount = 0;
+ for (String line : readAndCloseStream(ps.getInputStream()).split("\n")) {
+ if (line.contains(" Z ") || line.contains(" Z+ ")) {
+ ++zombieCount;
+ }
}
-
- public void test_exitValue() {
- try {
- String[] commands = { "ls" };
- Process process = Runtime.getRuntime().exec(commands, null, null);
- try {
- Thread.sleep(5000);
- } catch(Exception e) {
-
- }
- assertTrue(process.exitValue() == 0);
-
- String[] commandsSleep = { "sleep", "3" };
- process = Runtime.getRuntime().exec(commandsSleep, null, null);
- process.destroy();
- try {
- Thread.sleep(5000);
- } catch (Exception e) {}
- assertTrue(process.exitValue() != 0);
-
- process = Runtime.getRuntime().exec(commandsSleep, null, null);
- try {
- process.exitValue();
- fail("IllegalThreadStateException was not thrown.");
- } catch(IllegalThreadStateException itse) {
- //expected
- }
- } catch (IOException e) {
- fail("IOException was thrown.");
- }
+ assertEquals(0, zombieCount);
+ }
+
+ public void test_getOutputStream() throws Exception {
+ String[] commands = { "cat", "-"};
+ Process p = Runtime.getRuntime().exec(commands, null, null);
+ OutputStream os = p.getOutputStream();
+ // send data, and check if it is echoed back correctly
+ String str1 = "Some data for testing communication between processes\n";
+ String str2 = "More data that serves the same purpose.\n";
+ String str3 = "Here is some more data.\n";
+ os.write(str1.getBytes());
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
}
+ os.write(str2.getBytes());
+ os.write(str3.getBytes());
+ os.close();
- public void test_Constructor() {
- ProcessClass pc = new ProcessClass();
- assertTrue(pc.exitValue() == 0);
- }
+ String received = readAndCloseStream(p.getInputStream());
+ assertEquals(str1 + str2 + str3, received);
- @BrokenTest("Sporadic timeouts in CTS, but not in CoreTestRunner")
- public void test_destroy() {
- String[] commands = { "ls"};
- try {
- Process process = Runtime.getRuntime().exec(commands, null, null);
- process.destroy();
- } catch (IOException e) {
- fail("IOException was thrown.");
- }
- }
+ String stderr = readAndCloseStream(p.getErrorStream());
+ assertEquals("", stderr);
- protected void setUp() {
- }
+ p.waitFor();
+ p.destroy();
+ }
- protected void tearDown() {
- }
+ public void test_getErrorStream() throws Exception {
+ String[] commands = { "cat", "--no-such-option"};
+ Process p = Runtime.getRuntime().exec(commands, null, null);
- protected void doneSuite() {
- }
+ p.getOutputStream().close();
- class ProcessClass extends Process {
+ String received = readAndCloseStream(p.getInputStream());
+ assertEquals("", received);
- ProcessClass() {
- super();
- }
+ String stderr = readAndCloseStream(p.getErrorStream());
+ assertTrue(stderr, stderr.contains("unrecognized option") || stderr.contains("unknown option"));
- @Override
- public void destroy() {
-
- }
-
- @Override
- public int exitValue() {
- // TODO Auto-generated method stub
- return 0;
- }
-
- @Override
- public InputStream getErrorStream() {
- return null;
- }
-
- @Override
- public InputStream getInputStream() {
- return null;
- }
-
- @Override
- public OutputStream getOutputStream() {
- return null;
- }
-
- @Override
- public int waitFor() throws InterruptedException {
- // TODO Auto-generated method stub
- return 0;
- }
+ p.waitFor();
+ p.destroy();
+ }
+ private String readAndCloseStream(InputStream is) throws IOException {
+ StringBuffer result = new StringBuffer();
+ while (true) {
+ int c = is.read();
+ if (c == -1) {
+ break;
+ }
+ result.append((char) c);
+ }
+ is.close();
+ return result.toString();
+ }
+
+ public void test_exitValue() throws Exception {
+ String[] commands = { "ls" };
+ Process process = Runtime.getRuntime().exec(commands, null, null);
+ process.waitFor();
+ assertEquals(0, process.exitValue());
+
+ String[] commandsSleep = { "sleep", "3000" };
+ process = Runtime.getRuntime().exec(commandsSleep, null, null);
+ process.destroy();
+ process.waitFor(); // destroy is asynchronous.
+ assertTrue(process.exitValue() != 0);
+
+ process = Runtime.getRuntime().exec(new String[] { "sleep", "3000" }, null, null);
+ try {
+ process.exitValue();
+ fail();
+ } catch(IllegalThreadStateException expected) {
}
+ }
+
+ public void test_destroy() throws Exception {
+ String[] commands = { "ls"};
+ Process process = Runtime.getRuntime().exec(commands, null, null);
+ process.destroy();
+ process.destroy();
+ process.destroy();
+ }
}