diff --git a/src/solaris/native/java/lang/UNIXProcess_md.c b/src/solaris/native/java/lang/UNIXProcess_md.c index 9cfe22a1f1629c1ae37dd7d6e98a2b85c9beda71..77de830785bffded195656c65fbf361c12ef1f0f 100644 --- a/src/solaris/native/java/lang/UNIXProcess_md.c +++ b/src/solaris/native/java/lang/UNIXProcess_md.c @@ -49,6 +49,18 @@ #include #include +#ifndef USE_CLONE +#ifdef __linux__ +#define USE_CLONE 1 +#else +#define USE_CLONE 0 +#endif +#endif + +#if USE_CLONE +#include +#endif + #ifndef STDIN_FILENO #define STDIN_FILENO 0 #endif @@ -376,70 +388,61 @@ debugPrint(char *format, ...) } #endif /* DEBUG_PROCESS */ -/* Version of execvpe when child's PATH differs from parent's */ -static int -execvp_usingParentPath(const char *file, const char *const argv[]) +/** + * Exec FILE as a traditional Bourne shell script (i.e. one without #!). + * If we could do it over again, we would probably not support such an ancient + * misfeature, but compatibility wins over sanity. The original support for + * this was imported accidentally from execvp(). + */ +static void +execve_as_traditional_shell_script(const char *file, + const char *argv[], + const char *const envp[]) { - char expanded_file[PATH_MAX]; - int filelen = strlen(file); - int sticky_errno = 0; - const char * const * dirs; - /* Search parent's PATH */ - for (dirs = parentPathv; *dirs; dirs++) { - const char * dir = *dirs; - int dirlen = strlen(dir); - if (filelen + dirlen + 1 >= PATH_MAX) { - /* Resist the urge to remove this limit; - * calling malloc after fork is unsafe. */ - errno = ENAMETOOLONG; - continue; - } - strcpy(expanded_file, dir); - strcpy(expanded_file + dirlen, file); - execvp(expanded_file, (char **) argv); - /* There are 3 responses to various classes of errno: - * return immediately, continue (especially for ENOENT), - * or continue with "sticky" errno. - * - * From exec(3): - * - * If permission is denied for a file (the attempted - * execve returned EACCES), these functions will continue - * searching the rest of the search path. If no other - * file is found, however, they will return with the - * global variable errno set to EACCES. - */ - switch (errno) { - case EACCES: - sticky_errno = errno; - /* FALLTHRU */ - case ENOENT: - case ENOTDIR: -#ifdef ELOOP - case ELOOP: -#endif -#ifdef ESTALE - case ESTALE: -#endif -#ifdef ENODEV - case ENODEV: -#endif -#ifdef ETIMEDOUT - case ETIMEDOUT: + /* Use the extra word of space provided for us in argv by caller. */ + const char *argv0 = argv[0]; + const char *const *end = argv; + while (*end != NULL) + ++end; + memmove(argv+2, argv+1, (end-argv) * sizeof (*end)); + argv[0] = "/bin/sh"; + argv[1] = file; + execve(argv[0], (char **) argv, (char **) envp); + /* Can't even exec /bin/sh? Big trouble, but let's soldier on... */ + memmove(argv+1, argv+2, (end-argv) * sizeof (*end)); + argv[0] = argv0; +} + +/** + * Like execve(2), except that in case of ENOEXEC, FILE is assumed to + * be a shell script and the system default shell is invoked to run it. + */ +static void +execve_with_shell_fallback(const char *file, + const char *argv[], + const char *const envp[]) +{ +#if USE_CLONE + execve(file, (char **) argv, (char **) envp); + if (errno == ENOEXEC) + execve_as_traditional_shell_script(file, argv, envp); +#else + /* Our address space is unshared, so can mutate environ. */ + extern char **environ; + environ = (char **) envp; + execvp(file, (char **) argv); #endif - break; /* Try other directories in PATH */ - default: - return -1; - } - } - if (sticky_errno != 0) - errno = sticky_errno; - return -1; } -/* execvpe should have been included in the Unix standards. */ -static int -execvpe(const char *file, const char *const argv[], const char *const envp[]) +/** + * execvpe should have been included in the Unix standards. + * execvpe is identical to execvp, except that the child environment is + * specified via the 3rd argument instead of being inherited from environ. + */ +static void +execvpe(const char *file, + const char *argv[], + const char *const envp[]) { /* This is one of the rare times it's more portable to declare an * external symbol explicitly, rather than via a system header. @@ -454,28 +457,73 @@ execvpe(const char *file, const char *const argv[], const char *const envp[]) */ extern char **environ; - if (envp != NULL) - environ = (char **) envp; - - if (/* Parent and child environment the same? Use child PATH. */ - (envp == NULL) - - /* http://www.opengroup.org/onlinepubs/009695399/functions/exec.html - * "If the file argument contains a slash character, it is used as - * the pathname for this file. Otherwise, the path prefix for this - * file is obtained by a search of the directories passed in the - * PATH environment variable" */ - || (strchr(file, '/') != NULL) - - /* Parent and child PATH the same? Use child PATH. */ - || (strcmp(parentPath, effectivePath()) == 0) + if (envp == NULL || (char **) envp == environ) { + execvp(file, (char **) argv); + return; + } - /* We want ENOENT, not EACCES, for zero-length program names. */ - || (*file == '\0')) + if (*file == '\0') { + errno = ENOENT; + return; + } - return execvp(file, (char **) argv); - else - return execvp_usingParentPath(file, argv); + if (strchr(file, '/') != NULL) { + execve_with_shell_fallback(file, argv, envp); + } else { + /* We must search PATH (parent's, not child's) */ + char expanded_file[PATH_MAX]; + int filelen = strlen(file); + int sticky_errno = 0; + const char * const * dirs; + for (dirs = parentPathv; *dirs; dirs++) { + const char * dir = *dirs; + int dirlen = strlen(dir); + if (filelen + dirlen + 1 >= PATH_MAX) { + errno = ENAMETOOLONG; + continue; + } + memcpy(expanded_file, dir, dirlen); + memcpy(expanded_file + dirlen, file, filelen); + expanded_file[dirlen + filelen] = '\0'; + execve_with_shell_fallback(expanded_file, argv, envp); + /* There are 3 responses to various classes of errno: + * return immediately, continue (especially for ENOENT), + * or continue with "sticky" errno. + * + * From exec(3): + * + * If permission is denied for a file (the attempted + * execve returned EACCES), these functions will continue + * searching the rest of the search path. If no other + * file is found, however, they will return with the + * global variable errno set to EACCES. + */ + switch (errno) { + case EACCES: + sticky_errno = errno; + /* FALLTHRU */ + case ENOENT: + case ENOTDIR: +#ifdef ELOOP + case ELOOP: +#endif +#ifdef ESTALE + case ESTALE: +#endif +#ifdef ENODEV + case ENODEV: +#endif +#ifdef ETIMEDOUT + case ETIMEDOUT: +#endif + break; /* Try other directories in PATH */ + default: + return; + } + } + if (sticky_errno != 0) + errno = sticky_errno; + } } static void @@ -516,10 +564,95 @@ readFully(int fd, void *buf, size_t nbyte) } } -#ifndef __solaris__ -#undef fork1 -#define fork1() fork() -#endif +typedef struct _ChildStuff +{ + int in[2]; + int out[2]; + int err[2]; + int fail[2]; + int fds[3]; + const char **argv; + const char **envv; + const char *pdir; + jboolean redirectErrorStream; +} ChildStuff; + +static void +copyPipe(int from[2], int to[2]) +{ + to[0] = from[0]; + to[1] = from[1]; +} + +/** + * Child process after a successful fork() or clone(). + * This function must not return, and must be prepared for either all + * of its address space to be shared with its parent, or to be a copy. + * It must not modify global variables such as "environ". + */ +static int +childProcess(void *arg) +{ + const ChildStuff* p = (const ChildStuff*) arg; + + /* Close the parent sides of the pipes. + Closing pipe fds here is redundant, since closeDescriptors() + would do it anyways, but a little paranoia is a good thing. */ + closeSafely(p->in[1]); + closeSafely(p->out[0]); + closeSafely(p->err[0]); + closeSafely(p->fail[0]); + + /* Give the child sides of the pipes the right fileno's. */ + /* Note: it is possible for in[0] == 0 */ + moveDescriptor(p->in[0] != -1 ? p->in[0] : p->fds[0], STDIN_FILENO); + moveDescriptor(p->out[1]!= -1 ? p->out[1] : p->fds[1], STDOUT_FILENO); + + if (p->redirectErrorStream) { + closeSafely(p->err[1]); + dup2(STDOUT_FILENO, STDERR_FILENO); + } else { + moveDescriptor(p->err[1] != -1 ? p->err[1] : p->fds[2], STDERR_FILENO); + } + + moveDescriptor(p->fail[1], FAIL_FILENO); + + /* close everything */ + if (closeDescriptors() == 0) { /* failed, close the old way */ + int max_fd = (int)sysconf(_SC_OPEN_MAX); + int i; + for (i = FAIL_FILENO + 1; i < max_fd; i++) + close(i); + } + + /* change to the new working directory */ + if (p->pdir != NULL && chdir(p->pdir) < 0) + goto WhyCantJohnnyExec; + + if (fcntl(FAIL_FILENO, F_SETFD, FD_CLOEXEC) == -1) + goto WhyCantJohnnyExec; + + execvpe(p->argv[0], p->argv, p->envv); + + WhyCantJohnnyExec: + /* We used to go to an awful lot of trouble to predict whether the + * child would fail, but there is no reliable way to predict the + * success of an operation without *trying* it, and there's no way + * to try a chdir or exec in the parent. Instead, all we need is a + * way to communicate any failure back to the parent. Easy; we just + * send the errno back to the parent over a pipe in case of failure. + * The tricky thing is, how do we communicate the *success* of exec? + * We use FD_CLOEXEC together with the fact that a read() on a pipe + * yields EOF when the write ends (we have two of them!) are closed. + */ + { + int errnum = errno; + write(FAIL_FILENO, &errnum, sizeof(errnum)); + } + close(FAIL_FILENO); + _exit(-1); + return 0; /* Suppress warning "no return value from function" */ +} JNIEXPORT jint JNICALL Java_java_lang_UNIXProcess_forkAndExec(JNIEnv *env, @@ -533,34 +666,43 @@ Java_java_lang_UNIXProcess_forkAndExec(JNIEnv *env, { int errnum; int resultPid = -1; +#if USE_CLONE + void *clone_stack = NULL; +#endif int in[2], out[2], err[2], fail[2]; - const char **argv = NULL; - const char **envv = NULL; - const char *pprog = getBytes(env, prog); - const char *pargBlock = getBytes(env, argBlock); - const char *penvBlock = getBytes(env, envBlock); - const char *pdir = getBytes(env, dir); jint *fds = NULL; + const char *pprog = NULL; + const char *pargBlock = NULL; + const char *penvBlock = NULL; + ChildStuff *c; in[0] = in[1] = out[0] = out[1] = err[0] = err[1] = fail[0] = fail[1] = -1; - assert(prog != NULL && argBlock != NULL); - if (pprog == NULL) goto Catch; - if (pargBlock == NULL) goto Catch; - if (envBlock != NULL && penvBlock == NULL) goto Catch; - if (dir != NULL && pdir == NULL) goto Catch; + if ((c = NEW(ChildStuff, 1)) == NULL) return -1; + c->argv = NULL; + c->envv = NULL; + c->pdir = NULL; - /* Convert pprog + pargBlock into a char ** argv */ - if ((argv = NEW(const char *, argc + 2)) == NULL) - goto Catch; - argv[0] = pprog; - initVectorFromBlock(argv+1, pargBlock, argc); + /* Convert prog + argBlock into a char ** argv. + * Add one word room for expansion of argv for use by + * execve_as_traditional_shell_script. + */ + assert(prog != NULL && argBlock != NULL); + if ((pprog = getBytes(env, prog)) == NULL) goto Catch; + if ((pargBlock = getBytes(env, argBlock)) == NULL) goto Catch; + if ((c->argv = NEW(const char *, argc + 3)) == NULL) goto Catch; + c->argv[0] = pprog; + initVectorFromBlock(c->argv+1, pargBlock, argc); if (envBlock != NULL) { - /* Convert penvBlock into a char ** envv */ - if ((envv = NEW(const char *, envc + 1)) == NULL) - goto Catch; - initVectorFromBlock(envv, penvBlock, envc); + /* Convert envBlock into a char ** envv */ + if ((penvBlock = getBytes(env, envBlock)) == NULL) goto Catch; + if ((c->envv = NEW(const char *, envc + 1)) == NULL) goto Catch; + initVectorFromBlock(c->envv, penvBlock, envc); + } + + if (dir != NULL) { + if ((c->pdir = getBytes(env, dir)) == NULL) goto Catch; } assert(std_fds != NULL); @@ -574,72 +716,45 @@ Java_java_lang_UNIXProcess_forkAndExec(JNIEnv *env, throwIOException(env, errno, "Bad file descriptor"); goto Catch; } + c->fds[0] = fds[0]; + c->fds[1] = fds[1]; + c->fds[2] = fds[2]; + + copyPipe(in, c->in); + copyPipe(out, c->out); + copyPipe(err, c->err); + copyPipe(fail, c->fail); + + c->redirectErrorStream = redirectErrorStream; + + { +#if USE_CLONE + /* See clone(2). + * Instead of worrying about which direction the stack grows, just + * allocate twice as much and start the stack in the middle. */ + const int stack_size = 64 * 1024; + if ((clone_stack = NEW(char, 2 * stack_size)) == NULL) goto Catch; + resultPid = clone(childProcess, clone_stack + stack_size, + /* CLONE_VFORK | // works, but unnecessary */ + CLONE_VM | SIGCHLD, c); +#else + /* From fork(2): In Solaris 10, a call to fork() is identical + * to a call to fork1(); only the calling thread is replicated + * in the child process. This is the POSIX-specified behavior + * for fork(). */ + resultPid = fork(); + if (resultPid == 0) { + childProcess(c); + assert(0); /* childProcess must not return */ + } +#endif + } - resultPid = fork1(); if (resultPid < 0) { throwIOException(env, errno, "Fork failed"); goto Catch; } - if (resultPid == 0) { - /* Child process */ - - /* Close the parent sides of the pipes. - Closing pipe fds here is redundant, since closeDescriptors() - would do it anyways, but a little paranoia is a good thing. */ - closeSafely(in[1]); - closeSafely(out[0]); - closeSafely(err[0]); - closeSafely(fail[0]); - - /* Give the child sides of the pipes the right fileno's. */ - /* Note: it is possible for in[0] == 0 */ - moveDescriptor(in[0] != -1 ? in[0] : fds[0], STDIN_FILENO); - moveDescriptor(out[1]!= -1 ? out[1] : fds[1], STDOUT_FILENO); - - if (redirectErrorStream) { - closeSafely(err[1]); - dup2(STDOUT_FILENO, STDERR_FILENO); - } else { - moveDescriptor(err[1] != -1 ? err[1] : fds[2], STDERR_FILENO); - } - - moveDescriptor(fail[1], FAIL_FILENO); - - /* close everything */ - if (closeDescriptors() == 0) { /* failed, close the old way */ - int max_fd = (int)sysconf(_SC_OPEN_MAX); - int i; - for (i = FAIL_FILENO + 1; i < max_fd; i++) - close(i); - } - - /* change to the new working directory */ - if (pdir != NULL && chdir(pdir) < 0) - goto WhyCantJohnnyExec; - - if (fcntl(FAIL_FILENO, F_SETFD, FD_CLOEXEC) == -1) - goto WhyCantJohnnyExec; - - execvpe(argv[0], argv, envv); - - WhyCantJohnnyExec: - /* We used to go to an awful lot of trouble to predict whether the - * child would fail, but there is no reliable way to predict the - * success of an operation without *trying* it, and there's no way - * to try a chdir or exec in the parent. Instead, all we need is a - * way to communicate any failure back to the parent. Easy; we just - * send the errno back to the parent over a pipe in case of failure. - * The tricky thing is, how do we communicate the *success* of exec? - * We use FD_CLOEXEC together with the fact that a read() on a pipe - * yields EOF when the write ends (we have two of them!) are closed. - */ - errnum = errno; - write(FAIL_FILENO, &errnum, sizeof(errnum)); - close(FAIL_FILENO); - _exit(-1); - } - /* parent process */ close(fail[1]); fail[1] = -1; /* See: WhyCantJohnnyExec */ @@ -660,6 +775,10 @@ Java_java_lang_UNIXProcess_forkAndExec(JNIEnv *env, fds[2] = (err[0] != -1) ? err[0] : -1; Finally: +#if USE_CLONE + free(clone_stack); +#endif + /* Always clean up the child's side of the pipes */ closeSafely(in [0]); closeSafely(out[1]); @@ -669,13 +788,14 @@ Java_java_lang_UNIXProcess_forkAndExec(JNIEnv *env, closeSafely(fail[0]); closeSafely(fail[1]); - free(argv); - free(envv); - releaseBytes(env, prog, pprog); releaseBytes(env, argBlock, pargBlock); releaseBytes(env, envBlock, penvBlock); - releaseBytes(env, dir, pdir); + releaseBytes(env, dir, c->pdir); + + free(c->argv); + free(c->envv); + free(c); if (fds != NULL) (*env)->ReleaseIntArrayElements(env, std_fds, fds, 0); diff --git a/test/java/lang/ProcessBuilder/Basic.java b/test/java/lang/ProcessBuilder/Basic.java index d5ac05f45992318e82d2ce792d8a7c6ceefc4d12..745fab468b9d4376a2b5362422900c8a025e089a 100644 --- a/test/java/lang/ProcessBuilder/Basic.java +++ b/test/java/lang/ProcessBuilder/Basic.java @@ -257,6 +257,18 @@ public class Basic { s.write(bytes); // Might hang! } + static void checkPermissionDenied(ProcessBuilder pb) { + try { + pb.start(); + fail("Expected IOException not thrown"); + } catch (IOException e) { + String m = e.getMessage(); + if (EnglishUnix.is() && + ! matches(m, "Permission denied")) + unexpected(e); + } catch (Throwable t) { unexpected(t); } + } + public static class JavaChild { public static void main(String args[]) throws Throwable { String action = args[0]; @@ -317,12 +329,10 @@ public class Basic { for (final ProcessBuilder pb : new ProcessBuilder[] {pb1, pb2}) { pb.command("true"); - r = run(pb.start()); - equal(r.exitValue(), True.exitValue()); + equal(run(pb).exitValue(), True.exitValue()); pb.command("false"); - r = run(pb.start()); - equal(r.exitValue(), False.exitValue()); + equal(run(pb).exitValue(), False.exitValue()); } if (failed != 0) throw new Error("null PATH"); @@ -367,31 +377,82 @@ public class Basic { // Can't execute a directory -- permission denied // Report EACCES errno new File("dir1/prog").mkdirs(); - try { - pb.start(); - fail("Expected IOException not thrown"); - } catch (IOException e) { - String m = e.getMessage(); - if (EnglishUnix.is() && - ! matches(m, "Permission denied")) - unexpected(e); - } catch (Throwable t) { unexpected(t); } + checkPermissionDenied(pb); // continue searching if EACCES copy("/bin/true", "dir2/prog"); - equal(run(pb.start()).exitValue(), True.exitValue()); + equal(run(pb).exitValue(), True.exitValue()); new File("dir1/prog").delete(); new File("dir2/prog").delete(); new File("dir2/prog").mkdirs(); copy("/bin/true", "dir1/prog"); - equal(run(pb.start()).exitValue(), True.exitValue()); + equal(run(pb).exitValue(), True.exitValue()); - // Check empty PATH component means current directory + // Check empty PATH component means current directory. + // + // While we're here, let's test different kinds of + // Unix executables, and PATH vs explicit searching. new File("dir1/prog").delete(); new File("dir2/prog").delete(); - copy("/bin/true", "./prog"); - equal(run(pb.start()).exitValue(), True.exitValue()); + for (String[] command : + new String[][] { + new String[] {"./prog"}, + cmd}) { + pb.command(command); + File prog = new File("./prog"); + // "Normal" binaries + copy("/bin/true", "./prog"); + equal(run(pb).exitValue(), + True.exitValue()); + copy("/bin/false", "./prog"); + equal(run(pb).exitValue(), + False.exitValue()); + prog.delete(); + // Interpreter scripts with #! + setFileContents(prog, "#!/bin/true\n"); + prog.setExecutable(true); + equal(run(pb).exitValue(), + True.exitValue()); + prog.delete(); + setFileContents(prog, "#!/bin/false\n"); + prog.setExecutable(true); + equal(run(pb).exitValue(), + False.exitValue()); + // Traditional shell scripts without #! + setFileContents(prog, "exec /bin/true\n"); + prog.setExecutable(true); + equal(run(pb).exitValue(), + True.exitValue()); + prog.delete(); + setFileContents(prog, "exec /bin/false\n"); + prog.setExecutable(true); + equal(run(pb).exitValue(), + False.exitValue()); + prog.delete(); + } + + // Test Unix interpreter scripts + File dir1Prog = new File("dir1/prog"); + dir1Prog.delete(); + pb.command(new String[] {"prog", "world"}); + setFileContents(dir1Prog, "#!/bin/echo hello\n"); + checkPermissionDenied(pb); + dir1Prog.setExecutable(true); + equal(run(pb).out(), "hello dir1/prog world\n"); + equal(run(pb).exitValue(), True.exitValue()); + dir1Prog.delete(); + pb.command(cmd); + + // Test traditional shell scripts without #! + setFileContents(dir1Prog, "/bin/echo \"$@\"\n"); + pb.command(new String[] {"prog", "hello", "world"}); + checkPermissionDenied(pb); + dir1Prog.setExecutable(true); + equal(run(pb).out(), "hello world\n"); + equal(run(pb).exitValue(), True.exitValue()); + dir1Prog.delete(); + pb.command(cmd); // If prog found on both parent and child's PATH, // parent's is used. @@ -402,10 +463,10 @@ public class Basic { copy("/bin/true", "dir1/prog"); copy("/bin/false", "dir3/prog"); pb.environment().put("PATH","dir3"); - equal(run(pb.start()).exitValue(), True.exitValue()); + equal(run(pb).exitValue(), True.exitValue()); copy("/bin/true", "dir3/prog"); copy("/bin/false", "dir1/prog"); - equal(run(pb.start()).exitValue(), False.exitValue()); + equal(run(pb).exitValue(), False.exitValue()); } finally { // cleanup @@ -1503,21 +1564,19 @@ public class Basic { childArgs.add("OutErr"); ProcessBuilder pb = new ProcessBuilder(childArgs); { - ProcessResults r = run(pb.start()); + ProcessResults r = run(pb); equal(r.out(), "outout"); equal(r.err(), "errerr"); } { pb.redirectErrorStream(true); - ProcessResults r = run(pb.start()); + ProcessResults r = run(pb); equal(r.out(), "outerrouterr"); equal(r.err(), ""); } } catch (Throwable t) { unexpected(t); } - if (! Windows.is() && - new File("/bin/true").exists() && - new File("/bin/false").exists()) { + if (Unix.is()) { //---------------------------------------------------------------- // We can find true and false when PATH is null //---------------------------------------------------------------- @@ -1526,7 +1585,7 @@ public class Basic { childArgs.add("null PATH"); ProcessBuilder pb = new ProcessBuilder(childArgs); pb.environment().remove("PATH"); - ProcessResults r = run(pb.start()); + ProcessResults r = run(pb); equal(r.out(), ""); equal(r.err(), ""); equal(r.exitValue(), 0); @@ -1540,7 +1599,7 @@ public class Basic { childArgs.add("PATH search algorithm"); ProcessBuilder pb = new ProcessBuilder(childArgs); pb.environment().put("PATH", "dir1:dir2:"); - ProcessResults r = run(pb.start()); + ProcessResults r = run(pb); equal(r.out(), ""); equal(r.err(), ""); equal(r.exitValue(), True.exitValue()); diff --git a/test/java/lang/ProcessBuilder/BigFork.java b/test/java/lang/ProcessBuilder/BigFork.java new file mode 100644 index 0000000000000000000000000000000000000000..9465bbb5ce87c01cbafe3a74ae58f40d8d45ee55 --- /dev/null +++ b/test/java/lang/ProcessBuilder/BigFork.java @@ -0,0 +1,103 @@ +/* + * Copyright 2009 Google Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ + +import java.util.*; +import java.io.*; + +/** + * A manual test that demonstrates the ability to start a subprocess + * on Linux without getting ENOMEM. Run this test like: + * + * java -Xmx7000m BigFork + * + * providing a -Xmx flag suitable for your operating environment. + * Here's the bad old behavior: + * + * ==> java -Xmx7000m -esa -ea BigFork + * ------- + * CommitLimit: 6214700 kB + * Committed_AS: 2484452 kB + * ------- + * size=4.6GB + * ------- + * CommitLimit: 6214700 kB + * Committed_AS: 7219680 kB + * ------- + * Exception in thread "main" java.io.IOException: Cannot run program "/bin/true": java.io.IOException: error=12, Cannot allocate memory + * at java.lang.ProcessBuilder.start(ProcessBuilder.java:1018) + * at BigFork.main(BigFork.java:79) + * Caused by: java.io.IOException: java.io.IOException: error=12, Cannot allocate memory + * at java.lang.UNIXProcess.(UNIXProcess.java:190) + * at java.lang.ProcessImpl.start(ProcessImpl.java:128) + * at java.lang.ProcessBuilder.start(ProcessBuilder.java:1010) + * ... 1 more + */ +public class BigFork { + static final Random rnd = new Random(); + static void touchPages(byte[] chunk) { + final int pageSize = 4096; + for (int i = 0; i < chunk.length; i+= pageSize) { + chunk[i] = (byte) rnd.nextInt(); + } + } + + static void showCommittedMemory() throws IOException { + BufferedReader r = + new BufferedReader( + new InputStreamReader( + new FileInputStream("/proc/meminfo"))); + System.out.println("-------"); + String line; + while ((line = r.readLine()) != null) { + if (line.startsWith("Commit")) { + System.out.printf("%s%n", line); + } + } + System.out.println("-------"); + } + + public static void main(String[] args) throws Throwable { + showCommittedMemory(); + + final int chunkSize = 1024 * 1024 * 100; + List chunks = new ArrayList(100); + try { + for (;;) { + byte[] chunk = new byte[chunkSize]; + touchPages(chunk); + chunks.add(chunk); + } + } catch (OutOfMemoryError e) { + chunks.set(0, null); // Free up one chunk + System.gc(); + int size = chunks.size(); + System.out.printf("size=%.2gGB%n", (double)size/10); + + showCommittedMemory(); + + // Can we fork/exec in our current bloated state? + Process p = new ProcessBuilder("/bin/true").start(); + p.waitFor(); + } + } +}