/* * Copyright (c) 1995, 2013, Oracle and/or its affiliates. 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. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ /*IKVM*/ /* * Modified for IKVM by Jeroen Frijters */ package java.lang; import java.io.IOException; import java.io.File; import java.io.InputStream; import java.io.OutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileDescriptor; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.lang.ProcessBuilder.Redirect; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import cli.System.AsyncCallback; import cli.System.IAsyncResult; import cli.System.Diagnostics.ProcessStartInfo; import cli.System.EventArgs; import cli.System.EventHandler; import cli.System.IO.FileAccess; import cli.System.IO.FileShare; import cli.System.IO.FileMode; import cli.System.IO.FileOptions; import cli.System.IO.FileStream; import cli.System.IO.Stream; import cli.System.Security.AccessControl.FileSystemRights; /* This class is for the exclusive use of ProcessBuilder.start() to * create new processes. * * @author Martin Buchholz * @since 1.5 */ final class ProcessImpl extends Process { static class fdAccess { static Stream getHandle(FileDescriptor fd) { return fd.getStream(); } } /** * Open a file for writing. If {@code append} is {@code true} then the file * is opened for atomic append directly and a FileOutputStream constructed * with the resulting handle. This is because a FileOutputStream created * to append to a file does not open the file in a manner that guarantees * that writes by the child process will be atomic. */ private static FileOutputStream newFileOutputStream(File f, boolean append) throws IOException { if (append) { String path = f.getPath(); SecurityManager sm = System.getSecurityManager(); if (sm != null) sm.checkWrite(path); final FileDescriptor fd = openForAtomicAppend(path); return AccessController.doPrivileged( new PrivilegedAction() { public FileOutputStream run() { return new FileOutputStream(fd); } } ); } else { return new FileOutputStream(f); } } // System-dependent portion of ProcessBuilder.start() static Process start(String cmdarray[], java.util.Map environment, String dir, ProcessBuilder.Redirect[] redirects, boolean redirectErrorStream) throws IOException { FileInputStream f0 = null; FileOutputStream f1 = null; FileOutputStream f2 = null; try { Stream[] stdHandles; if (redirects == null) { stdHandles = new Stream[3]; } else { stdHandles = new Stream[3]; if (redirects[0] == Redirect.PIPE) stdHandles[0] = null; else if (redirects[0] == Redirect.INHERIT) stdHandles[0] = fdAccess.getHandle(FileDescriptor.in); else { f0 = new FileInputStream(redirects[0].file()); stdHandles[0] = fdAccess.getHandle(f0.getFD()); } if (redirects[1] == Redirect.PIPE) stdHandles[1] = null; else if (redirects[1] == Redirect.INHERIT) stdHandles[1] = fdAccess.getHandle(FileDescriptor.out); else { f1 = newFileOutputStream(redirects[1].file(), redirects[1].append()); stdHandles[1] = fdAccess.getHandle(f1.getFD()); } if (redirects[2] == Redirect.PIPE) stdHandles[2] = null; else if (redirects[2] == Redirect.INHERIT) stdHandles[2] = fdAccess.getHandle(FileDescriptor.err); else { f2 = newFileOutputStream(redirects[2].file(), redirects[2].append()); stdHandles[2] = fdAccess.getHandle(f2.getFD()); } } return new ProcessImpl(cmdarray, environment, dir, stdHandles, redirectErrorStream); } catch (Throwable t) { if (f0 != null) f0.close(); if (f1 != null) f1.close(); if (f2 != null) f2.close(); throw t; } finally { // HACK prevent the File[In|Out]putStream objects from closing the streams // (the System.IO.FileStream will eventually be closed explicitly or by its finalizer) if (f0 != null) cli.System.GC.SuppressFinalize(f0); if (f1 != null) cli.System.GC.SuppressFinalize(f1); if (f2 != null) cli.System.GC.SuppressFinalize(f2); } } private static class LazyPattern { // Escape-support version: // "(\")((?:\\\\\\1|.)+?)\\1|([^\\s\"]+)"; private static final Pattern PATTERN = Pattern.compile("[^\\s\"]+|\"[^\"]*\""); }; /* Parses the command string parameter into the executable name and * program arguments. * * The command string is broken into tokens. The token separator is a space * or quota character. The space inside quotation is not a token separator. * There are no escape sequences. */ private static String[] getTokensFromCommand(String command) { ArrayList matchList = new ArrayList<>(8); Matcher regexMatcher = LazyPattern.PATTERN.matcher(command); while (regexMatcher.find()) matchList.add(regexMatcher.group()); return matchList.toArray(new String[matchList.size()]); } private static final int VERIFICATION_CMD_BAT = 0; private static final int VERIFICATION_WIN32 = 1; private static final int VERIFICATION_LEGACY = 2; private static final char ESCAPE_VERIFICATION[][] = { // We guarantee the only command file execution for implicit [cmd.exe] run. // http://technet.microsoft.com/en-us/library/bb490954.aspx {' ', '\t', '<', '>', '&', '|', '^'}, {' ', '\t', '<', '>'}, {' ', '\t'} }; private static String createCommandLine(int verificationType, final String executablePath, final String cmd[]) { StringBuilder cmdbuf = new StringBuilder(80); cmdbuf.append(executablePath); for (int i = 1; i < cmd.length; ++i) { cmdbuf.append(' '); String s = cmd[i]; if (needsEscaping(verificationType, s)) { cmdbuf.append('"').append(s); // The code protects the [java.exe] and console command line // parser, that interprets the [\"] combination as an escape // sequence for the ["] char. // http://msdn.microsoft.com/en-us/library/17w5ykft.aspx // // If the argument is an FS path, doubling of the tail [\] // char is not a problem for non-console applications. // // The [\"] sequence is not an escape sequence for the [cmd.exe] // command line parser. The case of the [""] tail escape // sequence could not be realized due to the argument validation // procedure. if ((verificationType != VERIFICATION_CMD_BAT) && s.endsWith("\\")) { cmdbuf.append('\\'); } cmdbuf.append('"'); } else { cmdbuf.append(s); } } return cmdbuf.toString(); } private static boolean isQuoted(boolean noQuotesInside, String arg, String errorMessage) { int lastPos = arg.length() - 1; if (lastPos >=1 && arg.charAt(0) == '"' && arg.charAt(lastPos) == '"') { // The argument has already been quoted. if (noQuotesInside) { if (arg.indexOf('"', 1) != lastPos) { // There is ["] inside. throw new IllegalArgumentException(errorMessage); } } return true; } if (noQuotesInside) { if (arg.indexOf('"') >= 0) { // There is ["] inside. throw new IllegalArgumentException(errorMessage); } } return false; } private static boolean needsEscaping(int verificationType, String arg) { // Switch off MS heuristic for internal ["]. // Please, use the explicit [cmd.exe] call // if you need the internal ["]. // Example: "cmd.exe", "/C", "Extended_MS_Syntax" // For [.exe] or [.com] file the unpaired/internal ["] // in the argument is not a problem. boolean argIsQuoted = isQuoted( (verificationType == VERIFICATION_CMD_BAT), arg, "Argument has embedded quote, use the explicit CMD.EXE call."); if (!argIsQuoted) { char testEscape[] = ESCAPE_VERIFICATION[verificationType]; for (int i = 0; i < testEscape.length; ++i) { if (arg.indexOf(testEscape[i]) >= 0) { return true; } } } return false; } private static String getExecutablePath(String path) throws IOException { boolean pathIsQuoted = isQuoted(true, path, "Executable name has embedded quote, split the arguments"); // Win32 CreateProcess requires path to be normalized File fileToRun = new File(pathIsQuoted ? path.substring(1, path.length() - 1) : path); // From the [CreateProcess] function documentation: // // "If the file name does not contain an extension, .exe is appended. // Therefore, if the file name extension is .com, this parameter // must include the .com extension. If the file name ends in // a period (.) with no extension, or if the file name contains a path, // .exe is not appended." // // "If the file name !does not contain a directory path!, // the system searches for the executable file in the following // sequence:..." // // In practice ANY non-existent path is extended by [.exe] extension // in the [CreateProcess] funcion with the only exception: // the path ends by (.) return fileToRun.getPath(); } private boolean isShellFile(String executablePath) { String upPath = executablePath.toUpperCase(); return (upPath.endsWith(".CMD") || upPath.endsWith(".BAT")); } private String quoteString(String arg) { StringBuilder argbuf = new StringBuilder(arg.length() + 2); return argbuf.append('"').append(arg).append('"').toString(); } private cli.System.Diagnostics.Process handle; private OutputStream stdin_stream; private InputStream stdout_stream; private InputStream stderr_stream; private ProcessImpl(String cmd[], final java.util.Map envblock, final String path, final Stream[] stdHandles, final boolean redirectErrorStream) throws IOException { String cmdstr; SecurityManager security = System.getSecurityManager(); boolean allowAmbiguousCommands = false; if (security == null) { allowAmbiguousCommands = true; String value = System.getProperty("jdk.lang.Process.allowAmbiguousCommands"); if (value != null) allowAmbiguousCommands = !"false".equalsIgnoreCase(value); } if (allowAmbiguousCommands) { // Legacy mode. // Normalize path if possible. String executablePath = new File(cmd[0]).getPath(); // No worry about internal, unpaired ["], and redirection/piping. if (needsEscaping(VERIFICATION_LEGACY, executablePath) ) executablePath = quoteString(executablePath); cmdstr = createCommandLine( //legacy mode doesn't worry about extended verification VERIFICATION_LEGACY, executablePath, cmd); } else { String executablePath; try { executablePath = getExecutablePath(cmd[0]); } catch (IllegalArgumentException e) { // Workaround for the calls like // Runtime.getRuntime().exec("\"C:\\Program Files\\foo\" bar") // No chance to avoid CMD/BAT injection, except to do the work // right from the beginning. Otherwise we have too many corner // cases from // Runtime.getRuntime().exec(String[] cmd [, ...]) // calls with internal ["] and escape sequences. // Restore original command line. StringBuilder join = new StringBuilder(); // terminal space in command line is ok for (String s : cmd) join.append(s).append(' '); // Parse the command line again. cmd = getTokensFromCommand(join.toString()); executablePath = getExecutablePath(cmd[0]); // Check new executable name once more if (security != null) security.checkExec(executablePath); } // Quotation protects from interpretation of the [path] argument as // start of longer path with spaces. Quotation has no influence to // [.exe] extension heuristic. cmdstr = createCommandLine( // We need the extended verification procedure for CMD files. isShellFile(executablePath) ? VERIFICATION_CMD_BAT : VERIFICATION_WIN32, quoteString(executablePath), cmd); } handle = create(cmdstr, envblock, path, stdHandles, redirectErrorStream); java.security.AccessController.doPrivileged( new java.security.PrivilegedAction() { public Void run() { if (stdHandles[0] == null) stdin_stream = ProcessBuilder.NullOutputStream.INSTANCE; else { FileDescriptor stdin_fd = FileDescriptor.fromStream(stdHandles[0]); stdin_stream = new BufferedOutputStream( new FileOutputStream(stdin_fd)); } if (stdHandles[1] == null) stdout_stream = ProcessBuilder.NullInputStream.INSTANCE; else { FileDescriptor stdout_fd = FileDescriptor.fromStream(stdHandles[1]); stdout_stream = new BufferedInputStream( new FileInputStream(stdout_fd)); } if (stdHandles[2] == null) stderr_stream = ProcessBuilder.NullInputStream.INSTANCE; else { FileDescriptor stderr_fd = FileDescriptor.fromStream(stdHandles[2]); stderr_stream = new FileInputStream(stderr_fd); } return null; }}); } private static native String mapVfsExecutable(String path); public OutputStream getOutputStream() { return stdin_stream; } public InputStream getInputStream() { return stdout_stream; } public InputStream getErrorStream() { return stderr_stream; } public int exitValue() { if (!handle.get_HasExited()) throw new IllegalThreadStateException("process has not exited"); return handle.get_ExitCode(); } public int waitFor() throws InterruptedException { waitForInterruptibly(handle); if (Thread.interrupted()) throw new InterruptedException(); return exitValue(); } private static void waitForInterruptibly(cli.System.Diagnostics.Process handle) throws InterruptedException { // to be interruptable we have to use polling // (on .NET 2.0 WaitForExit is actually interruptible, but this isn't documented) Thread current = Thread.currentThread(); while (!current.isInterrupted() && !handle.WaitForExit(100)) ; } @Override public boolean waitFor(long timeout, TimeUnit unit) throws InterruptedException { if (handle.get_HasExited()) return true; if (timeout <= 0) return false; long msTimeout = unit.toMillis(timeout); waitForTimeoutInterruptibly(handle, msTimeout); if (Thread.interrupted()) throw new InterruptedException(); return handle.get_HasExited(); } private static void waitForTimeoutInterruptibly( cli.System.Diagnostics.Process handle, long timeout) { long now = System.currentTimeMillis(); long exp = now + timeout; if (exp < now) { // if we overflowed, just wait for a really long time exp = Long.MAX_VALUE; } Thread current = Thread.currentThread(); for (;;) { if (current.isInterrupted()) { return; } // wait for a maximum of 100 ms to be interruptible if (handle.WaitForExit((int)Math.min(100, exp - now))) { return; } now = System.currentTimeMillis(); if (now >= exp) { return; } } } public void destroy() { terminateProcess(handle); } @Override public Process destroyForcibly() { destroy(); return this; } private static void terminateProcess(cli.System.Diagnostics.Process handle) { try { if (false) throw new cli.System.ComponentModel.Win32Exception(); if (false) throw new cli.System.InvalidOperationException(); handle.Kill(); } catch (cli.System.ComponentModel.Win32Exception _) { } catch (cli.System.InvalidOperationException _) { } } @Override public boolean isAlive() { return isProcessAlive(handle); } private static boolean isProcessAlive(cli.System.Diagnostics.Process handle) { return !handle.get_HasExited(); } /** * Create a process using the win32 function CreateProcess. * The method is synchronized due to MS kb315939 problem. * All native handles should restore the inherit flag at the end of call. * * @param cmdstr the Windows command line * @param envblock NUL-separated, double-NUL-terminated list of * environment strings in VAR=VALUE form * @param dir the working directory of the process, or null if * inheriting the current directory from the parent process * @param stdHandles array of windows HANDLEs. Indexes 0, 1, and * 2 correspond to standard input, standard output and * standard error, respectively. On input, a value of -1 * means to create a pipe to connect child and parent * processes. On output, a value which is not -1 is the * parent pipe handle corresponding to the pipe which has * been created. An element of this array is -1 on input * if and only if it is not -1 on output. * @param redirectErrorStream redirectErrorStream attribute * @return the native subprocess HANDLE returned by CreateProcess */ private static cli.System.Diagnostics.Process create(String cmdstr, java.util.Map envblock, String dir, Stream[] stdHandles, boolean redirectErrorStream) throws IOException { int programEnd = parseCommandString(cmdstr); int argumentsStart = programEnd; if (cmdstr.length() > argumentsStart && cmdstr.charAt(argumentsStart) == ' ') { argumentsStart++; } String fileName = cmdstr.substring(0, programEnd); ProcessStartInfo si = new ProcessStartInfo(mapVfsExecutable(fileName), cmdstr.substring(argumentsStart)); si.set_UseShellExecute(false); si.set_RedirectStandardError(true); si.set_RedirectStandardOutput(true); si.set_RedirectStandardInput(true); si.set_CreateNoWindow(true); if (dir != null) { si.set_WorkingDirectory(dir); } if (envblock != null) { si.get_EnvironmentVariables().Clear(); for (String key : envblock.keySet()) { si.get_EnvironmentVariables().set_Item(key, envblock.get(key)); } } cli.System.Diagnostics.Process proc; try { if (false) throw new cli.System.ComponentModel.Win32Exception(); if (false) throw new cli.System.InvalidOperationException(); proc = cli.System.Diagnostics.Process.Start(si); } catch (cli.System.ComponentModel.Win32Exception x1) { throw new IOException(x1.getMessage()); } catch (cli.System.InvalidOperationException x2) { throw new IOException(x2.getMessage()); } // if any of the handles is redirected to/from a file, // we need to close the files as soon as the process exits if (stdHandles[0] instanceof FileStream || stdHandles[1] instanceof FileStream || stdHandles[2] instanceof FileStream) { final Stream s0 = stdHandles[0]; final Stream s1 = stdHandles[1]; final Stream s2 = stdHandles[2]; proc.set_EnableRaisingEvents(true); proc.add_Exited(new EventHandler(new EventHandler.Method() { public void Invoke(Object sender, EventArgs e) { if (s0 instanceof FileStream) s0.Close(); if (s1 instanceof FileStream) s1.Close(); if (s2 instanceof FileStream) s2.Close(); } })); } Stream stdin = proc.get_StandardInput().get_BaseStream(); Stream stdout = proc.get_StandardOutput().get_BaseStream(); Stream stderr = proc.get_StandardError().get_BaseStream(); if (stdHandles[0] != null) { connectPipe(stdHandles[0], stdin); stdHandles[0] = null; } else { stdHandles[0] = stdin; } Stream stdoutDrain = null; if (stdHandles[1] != null) { stdoutDrain = stdHandles[1]; connectPipe(stdout, stdoutDrain); stdHandles[1] = null; } else if (redirectErrorStream) { PipeStream pipe = new PipeStream(); connectPipe(stdout, pipe); connectPipe(stderr, pipe); stdHandles[1] = pipe; } else { stdHandles[1] = stdout; } if (redirectErrorStream) { if (stdoutDrain != null) { connectPipe(stderr, stdoutDrain); } stdHandles[2] = null; } else if (stdHandles[2] != null) { connectPipe(stderr, stdHandles[2]); stdHandles[2] = null; } else { stdHandles[2] = stderr; } return proc; } private static final class PipeStream extends Stream { private final byte[] buf = new byte[4096]; private int pos; private int users = 2; @Override public synchronized int Read(byte[] buffer, int offset, int count) { if (count == 0) { return 0; } while (pos == 0) { try { wait(); } catch (InterruptedException _) { } } if (pos == -1) { return 0; } count = Math.min(count, pos); System.arraycopy(buf, 0, buffer, offset, count); pos -= count; System.arraycopy(buf, count, buf, 0, pos); notifyAll(); return count; } @Override public synchronized void Write(byte[] buffer, int offset, int count) { while (buf.length - pos < count) { try { wait(); } catch (InterruptedException _) { } } System.arraycopy(buffer, offset, buf, pos, count); pos += count; notifyAll(); } @Override public synchronized void Close() { if (--users == 0) { pos = -1; notifyAll(); } } @Override public boolean get_CanRead() { return true; } @Override public boolean get_CanSeek() { return false; } @Override public boolean get_CanWrite() { return true; } @Override public void Flush() { } @Override public long get_Length() { ikvm.runtime.Util.throwException(new cli.System.NotSupportedException()); return 0; } @Override public long get_Position() { ikvm.runtime.Util.throwException(new cli.System.NotSupportedException()); return 0; } @Override public long Seek(long offset, cli.System.IO.SeekOrigin origin) { ikvm.runtime.Util.throwException(new cli.System.NotSupportedException()); return 0; } @Override public void SetLength(long value) { ikvm.runtime.Util.throwException(new cli.System.NotSupportedException()); } @Override public void set_Position(long position) { ikvm.runtime.Util.throwException(new cli.System.NotSupportedException()); } } private static native int parseCommandString(String cmdstr); /** * Opens a file for atomic append. The file is created if it doesn't * already exist. * * @param file the file to open or create * @return the native HANDLE */ private static FileDescriptor openForAtomicAppend(String path) throws IOException { try { if (false) throw new cli.System.ArgumentException(); if (false) throw new cli.System.IO.IOException(); if (false) throw new cli.System.Security.SecurityException(); if (false) throw new cli.System.UnauthorizedAccessException(); return FileDescriptor.fromStream(new FileStream(path, FileMode.wrap(FileMode.Append), FileSystemRights.wrap(FileSystemRights.AppendData), FileShare.wrap(FileShare.ReadWrite), 1, FileOptions.wrap(FileOptions.None))); } catch (cli.System.ArgumentException x) { throw new IOException(x.getMessage()); } catch (cli.System.IO.IOException x) { throw new IOException(x.getMessage()); } catch (cli.System.Security.SecurityException x) { throw new IOException(x.getMessage()); } catch (cli.System.UnauthorizedAccessException x) { throw new IOException(x.getMessage()); } } private static void connectPipe(final Stream in, final Stream out) { final byte[] buf = new byte[4096]; final AsyncCallback[] callback = new AsyncCallback[1]; callback[0] = new AsyncCallback(new AsyncCallback.Method() { public void Invoke(IAsyncResult ar) { try { int count = in.EndRead(ar); if (count > 0) { out.Write(buf, 0, count); out.Flush(); in.BeginRead(buf, 0, buf.length, callback[0], null); } else { out.Close(); } } catch (Throwable _) { } } }); try { in.BeginRead(buf, 0, buf.length, callback[0], null); } catch (Throwable _) { } } }