/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2010 Oracle and/or its affiliates. All rights reserved.
*
* Oracle and Java are registered trademarks of Oracle and/or its affiliates.
* Other names may be trademarks of their respective owners.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common
* Development and Distribution License("CDDL") (collectively, the
* "License"). You may not use this file except in compliance with the
* License. You can obtain a copy of the License at
* http://www.netbeans.org/cddl-gplv2.html
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
* specific language governing permissions and limitations under the
* License. When distributing the software, include this License Header
* Notice in each file and include the License file at
* nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the GPL Version 2 section of the License file that
* accompanied this code. If applicable, add the following below the
* License Header, with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* If you wish your version of this file to be governed by only the CDDL
* or only the GPL Version 2, indicate your decision by adding
* "[Contributor] elects to include this software in this distribution
* under the [CDDL or GPL Version 2] license." If you do not indicate a
* single choice of license, a recipient has the option to distribute
* your version of this file under either the CDDL, the GPL Version 2 or
* to extend the choice of license to its licensees as provided above.
* However, if you add GPL Version 2 code and therefore, elected the GPL
* Version 2 license, then the option applies only if the new code is
* made subject to such option by the copyright holder.
*
* Contributor(s):
*
* Portions Copyrighted 2008 Sun Microsystems, Inc.
*/
package org.netbeans.modules.ruby.platform.execution;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.netbeans.api.queries.FileEncodingQuery;
import org.netbeans.api.ruby.platform.RubyPlatform;
import org.openide.filesystems.FileObject;
import org.openide.modules.InstalledFileLocator;
import org.openide.util.Exceptions;
import org.openide.util.Utilities;
/**
* Utility methods for external execution.
* <p>
* <i>Most of the methods here
* used to be in <code>RubyExecution</code>.</i>
*
* @author Erno Mononen
*/
public final class ExecutionUtils {
private static final Logger LOGGER = Logger.getLogger(ExecutionUtils.class.getName());
private static final Pattern[] LOCATION_RECOGNIZER_PATTERNS = new Pattern[]{
RubyLineConvertorFactory.RAILS_RECOGNIZER,
RubyLineConvertorFactory.RUBY_COMPILER_WIN_MY,
RubyLineConvertorFactory.JRUBY_COMPILER,
RubyLineConvertorFactory.RUBY_COMPILER,
RubyLineConvertorFactory.RUBY_COMPILER_WIN};
/** When not set (the default) do stdio syncing for native Ruby binaries */
private static final boolean SYNC_RUBY_STDIO = System.getProperty("ruby.no.sync-stdio") == null; // NOI18N
/** Set to suppress using the -Kkcode flag in case you're using a weird interpreter which doesn't support it */
private static final boolean SKIP_KCODE = Boolean.getBoolean("ruby.no.kcode"); // NOI18N
/** When not set (the default) bypass the JRuby launcher unix/ba-file scripts and launch VM directly */
private static final boolean LAUNCH_JRUBY_SCRIPT = System.getProperty("ruby.use.jruby.script") != null; // NOI18N
private ExecutionUtils() {
}
/** When not set (the default) bypass the JRuby launcher unix/ba-file scripts and launch VM directly */
public static boolean launchJRubyScript() {
return LAUNCH_JRUBY_SCRIPT;
}
/**
* Returns the basic Ruby interpreter command and associated flags (not
* application arguments)
*/
public static List<? extends String> getRubyArgs(final RubyPlatform platform) {
RubyExecutionDescriptor desc = new RubyExecutionDescriptor(platform);
return getRubyArgs(platform.getHome().getAbsolutePath(),
platform.getInterpreterFile().getName(), desc, null);
}
static List<? extends String> getRubyArgs(String rubyHome, String cmdName, RubyExecutionDescriptor descriptor, String charsetName) {
List<String> argvList = new ArrayList<String>();
// Decide whether I'm launching JRuby, and if so, take a shortcut and launch
// the VM directly. This is important because killing JRuby via the launcher script
// is not working right; now that JRuby on Unix exec's the VM that part is okay but
// on Windows there are still problems.
if (!launchJRubyScript() && cmdName.startsWith("jruby")) { // NOI18N
String javaHome = getJavaHome();
argvList.add(javaHome + File.separator + "bin" + File.separator + // NOI18N
"java"); // NOI18N
// XXX Do I need java.exe on Windows?
// Additional execution flags specified in the JRuby startup script:
argvList.add("-Xverify:none"); // NOI18N
argvList.add("-da"); // NOI18N
String javaMemory = "-Xmx512m"; // NOI18N
String javaStack = "-Xss1024k"; // NOI18N
// use the client mode by default
String jvmMode = "-client";
String[] jvmArgs = descriptor == null ? null : descriptor.getJVMArguments();
if (jvmArgs != null) {
for (String arg : jvmArgs) {
if (arg.contains("-Xmx")) { // NOI18N
javaMemory = null;
}
if (arg.contains("-Xss")) { // NOI18N
javaStack = null;
}
if ("-client".equals(arg) || "-server".equals(arg)) { //NOI18N
jvmMode = null;
}
argvList.add(arg);
}
}
if (jvmMode != null) {
argvList.add(1, jvmMode);
}
if (javaMemory != null) {
argvList.add(javaMemory);
}
if (javaStack != null) {
argvList.add(javaStack);
}
// Classpath
argvList.add("-classpath"); // NOI18N
File rubyHomeDir = null;
try {
rubyHomeDir = new File(rubyHome);
rubyHomeDir = rubyHomeDir.getCanonicalFile();
} catch (IOException ioe) {
Exceptions.printStackTrace(ioe);
}
if (!rubyHomeDir.isDirectory()) {
throw new IllegalArgumentException(rubyHomeDir.getAbsolutePath() + " does not exist."); // NOI18N
}
File jrubyLib = new File(rubyHomeDir, "lib"); // NOI18N
if (!jrubyLib.isDirectory()) {
throw new AssertionError('"' + jrubyLib.getAbsolutePath() + "\" exists (\"" + descriptor.getCmd() + "\" is not valid JRuby executable?)");
}
argvList.add(computeJRubyClassPath(
descriptor == null ? null : descriptor.getClassPath(), jrubyLib));
argvList.add("-Djruby.base=" + rubyHomeDir); // NOI18N
argvList.add("-Djruby.home=" + rubyHomeDir); // NOI18N
argvList.add("-Djruby.lib=" + jrubyLib); // NOI18N
// TODO - turn off verifier?
if (Utilities.isWindows()) {
argvList.add("-Djruby.shell=\"cmd.exe\""); // NOI18N
argvList.add("-Djruby.script=jruby.bat"); // NOI18N
} else {
argvList.add("-Djruby.shell=/bin/sh"); // NOI18N
argvList.add("-Djruby.script=jruby"); // NOI18N
}
// Main class
argvList.add("org.jruby.Main"); // NOI18N
// TODO: JRUBYOPTS
// Application arguments follow
}
if (!SKIP_KCODE && cmdName.startsWith("ruby")) { // NOI18N
String cs = charsetName;
if (cs == null) {
// Add project encoding flags
FileObject fo = descriptor.getFileObject();
if (fo != null) {
Charset charset = FileEncodingQuery.getEncoding(fo);
if (charset != null) {
cs = charset.name();
}
}
}
if (cs != null) {
if (cs.equals("UTF-8")) { // NOI18N
argvList.add("-Ku"); // NOI18N
//} else if (cs.equals("")) {
// What else???
}
}
}
// Is this a native Ruby process? If so, do sync-io workaround.
if (SYNC_RUBY_STDIO && cmdName.startsWith("ruby")) { // NOI18N
int dot = cmdName.indexOf('.');
if ((dot == -1) || (dot == 4) || (dot == 5)) { // 5: rubyw
InstalledFileLocator locator = InstalledFileLocator.getDefault();
File f =
locator.locate("modules/org-netbeans-modules-ruby-project.jar", // NOI18N
null, false); // NOI18N
if (f == null) {
throw new RuntimeException("Can't find cluster"); // NOI18N
}
f = new File(f.getParentFile().getParentFile().getAbsolutePath() + File.separator +
"sync-stdio.rb"); // NOI18N
try {
f = f.getCanonicalFile();
} catch (IOException ioe) {
Exceptions.printStackTrace(ioe);
}
argvList.add("-r" + f.getAbsolutePath()); // NOI18N
}
}
return argvList;
}
public static String getExtraClassPath(String extraCp) {
if (extraCp != null && File.pathSeparatorChar != ':') {
// Ugly hack - getClassPath has mixed together path separator chars
// (:) and filesystem separators, e.g. I might have C:\foo:D:\bar but
// obviously only the path separator after "foo" should be changed to ;
StringBuilder p = new StringBuilder();
int pathOffset = 0;
for (int i = 0; i < extraCp.length(); i++) {
char c = extraCp.charAt(i);
if (c == ':' && pathOffset != 1) {
p.append(File.pathSeparatorChar);
pathOffset = 0;
continue;
} else {
pathOffset++;
}
p.append(c);
}
return p.toString();
}
return extraCp;
}
/** Package-private for unit test. */
static String computeJRubyClassPath(String extraCp, final File jrubyLib) {
StringBuilder cp = new StringBuilder();
File[] libs = jrubyLib.listFiles();
for (File lib : libs) {
if (lib.getName().endsWith(".jar")) { // NOI18N
if (cp.length() > 0) {
cp.append(File.pathSeparatorChar);
}
cp.append(lib.getAbsolutePath());
}
}
// Add in user-specified jars passed via JRUBY_EXTRA_CLASSPATH
extraCp = getExtraClassPath(extraCp);
if (extraCp == null) {
extraCp = System.getenv("JRUBY_EXTRA_CLASSPATH"); // NOI18N
}
if (extraCp != null) {
if (cp.length() > 0) {
cp.append(File.pathSeparatorChar);
}
//if (File.pathSeparatorChar != ':' && extraCp.indexOf(File.pathSeparatorChar) == -1 &&
// extraCp.indexOf(':') != -1) {
// extraCp = extraCp.replace(':', File.pathSeparatorChar);
//}
cp.append(extraCp);
}
return Utilities.isWindows() ? "\"" + cp.toString() + "\"" : cp.toString(); // NOI18N
}
public static void setupProcessEnvironment(Map<String, String> env, final String pwd, boolean appendJdkToPath) {
String path = pwd;
if (!Utilities.isWindows()) {
path = path.replace(" ", "\\ "); // NOI18N
}
// Find PATH environment variable - on Windows it can be some other
// case and we should use whatever it has.
String pathName = "PATH"; // NOI18N
if (Utilities.isWindows()) {
pathName = "Path"; // NOI18N
for (String key : env.keySet()) {
if ("PATH".equals(key.toUpperCase())) { // NOI18N
pathName = key;
break;
}
}
}
String currentPath = env.get(pathName);
if (currentPath == null) {
currentPath = "";
}
currentPath = path + File.pathSeparator + currentPath;
if (appendJdkToPath) {
// jruby.java.home always points to jdk(?)
String jdkHome = System.getProperty("jruby.java.home"); // NOI18N
if (jdkHome == null) {
// #115377 - add jdk bin to path
jdkHome = System.getProperty("jdk.home"); // NOI18N
}
String jdkBin = jdkHome + File.separator + "bin"; // NOI18N
if (!Utilities.isWindows()) {
jdkBin = jdkBin.replace(" ", "\\ "); // NOI18N
}
currentPath = currentPath + File.pathSeparator + jdkBin;
}
env.put(pathName, currentPath); // NOI18N
}
public static String getJavaHome() {
String javaHome = System.getProperty("jruby.java.home"); // NOI18N
if (javaHome == null) {
javaHome = System.getProperty("java.home"); // NOI18N
}
return javaHome;
}
/** Just helper method for logging. */
public static void logProcess(final ProcessBuilder pb) {
if (LOGGER.isLoggable(Level.FINE)) {
File dir = pb.directory();
String basedir = dir == null ? "" : "(basedir: " + dir.getAbsolutePath() + ") ";
LOGGER.fine("Running: " + basedir + '"' + getProcessAsString(pb.command()) + '"');
LOGGER.fine("Environment: " + pb.environment());
}
}
/** Just helper method for logging. */
private static String getProcessAsString(List<? extends String> process) {
StringBuilder sb = new StringBuilder();
for (String arg : process) {
sb.append(arg).append(' ');
}
return sb.toString().trim();
}
/**
* Creates a new thread factory that prefixes the names of the threads it
* creates with the given <code>namePrefix</code>.
*
* @param namePrefix the prefix to set.
* @return
*/
public static ThreadFactory namedThreadFactory(String namePrefix) {
return new NamedThreadFactory(namePrefix);
}
private static class NamedThreadFactory implements ThreadFactory {
private final ThreadFactory delegate;
private final String namePrefix;
private final AtomicInteger threadNumber = new AtomicInteger(1);
public NamedThreadFactory(String namePrefix) {
this.namePrefix = namePrefix;
this.delegate = Executors.defaultThreadFactory();
}
@Override
public Thread newThread(Runnable r) {
Thread result = delegate.newThread(r);
result.setName(namePrefix + "-" + threadNumber.getAndIncrement());
return result;
}
}
// TODO: find a better place for this method (doesn't have anything to
// do with external execution)
public static FileLocation getLocation(String line) {
final int fileGroup = 1;
final int lineGroup = 2;
if (line.length() > 400) {
return null;
}
for (Pattern pattern : LOCATION_RECOGNIZER_PATTERNS) {
Matcher match = pattern.matcher(line);
if (match.matches()) {
String file = null;
int lineno = -1;
if (fileGroup != -1) {
file = match.group(fileGroup);
// Make some adjustments - easier to do here than in the regular expression
// (See 109721 and 109724 for example)
if (file.startsWith("\"")) { // NOI18N
file = file.substring(1);
}
if (file.startsWith("./")) { // NOI18N
file = file.substring(2);
}
if (!(RubyLineConvertorFactory.EXT_RE.matcher(file).matches() || new File(file).isFile())) {
return null;
}
}
if (lineGroup != -1) {
String linenoStr = match.group(lineGroup);
try {
lineno = Integer.parseInt(linenoStr);
} catch (NumberFormatException nfe) {
Exceptions.printStackTrace(nfe);
lineno = 0;
}
}
return new FileLocation(file, lineno);
}
}
return null;
}
// TODO: move somewhere else (doesn't have anything to
// do with external execution)
public static final class FileLocation {
public final String file;
public final int line;
public FileLocation(String file, int line) {
this.file = file;
this.line = line;
}
}
}