diff options
-rw-r--r-- | luni/src/main/native/java_lang_ProcessManager.cpp | 358 | ||||
-rw-r--r-- | luni/src/test/java/tests/api/java/lang/ProcessTest.java | 265 |
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(); + } } |