/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 1997-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]" * * The Original Software is NetBeans. The Initial Developer of the Original * Software is Sun Microsystems, Inc. Portions Copyright 1997-2008 Sun * Microsystems, Inc. All Rights Reserved. * * 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. */ package org.netbeans.api.ruby.platform; import java.awt.EventQueue; import java.beans.PropertyVetoException; import java.beans.VetoableChangeListener; import java.beans.VetoableChangeSupport; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.io.StringReader; import java.text.Collator; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; import org.netbeans.api.project.ProjectManager; import org.netbeans.api.ruby.platform.RubyPlatform.Info; import org.netbeans.modules.ruby.platform.execution.ExecutionUtils; import org.netbeans.modules.ruby.platform.RubyPreferences; import org.netbeans.modules.ruby.platform.Util; import org.netbeans.modules.ruby.spi.project.support.rake.PropertyUtils; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; import org.openide.modules.InstalledFileLocator; import org.openide.util.EditableProperties; import org.openide.util.Exceptions; import org.openide.util.Mutex; import org.openide.util.MutexException; import org.openide.util.RequestProcessor; import org.openide.util.Utilities; import org.openide.util.io.ReaderInputStream; /** * Represents one Ruby platform, i.e. installation of a Ruby interpreter. */ public final class RubyPlatformManager { public static final boolean PREINDEXING = Boolean.getBoolean("gsf.preindexing"); private static final String[] RUBY_EXECUTABLE_NAMES = { "ruby", "jruby", "rubinius" }; // NOI18N /** For unit tests. */ static Properties TEST_RUBY_PROPS; private static final String PLATFORM_PREFIX = "rubyplatform."; // NOI18N private static final String PLATFORM_INTEPRETER = ".interpreter"; // NOI18N private static final String PLATFORM_ID_DEFAULT = "default"; // NOI18N private static final Logger LOGGER = Logger.getLogger(RubyPlatformManager.class.getName()); /** * Compares alphabetically by the long descriptions. Fallbacks to * interpreter's paths comparison */ private static final Comparator<RubyPlatform> ALPHABETICAL_COMPARATOR = new Comparator<RubyPlatform>() { public int compare(RubyPlatform p1, RubyPlatform p2) { int result = Collator.getInstance().compare( p1.getInfo().getLongDescription(), p2.getInfo().getLongDescription()); if (result == 0) { result = p1.getInterpreter().compareTo(p2.getInterpreter()); } return result; } }; private static Set<RubyPlatform> platforms; /** * Change support for notifying of platform changes, using vetoable for * making it possible to prevent removing of a used platform. */ private static final VetoableChangeSupport VETOABLE_CHANGE_SUPPORT = new VetoableChangeSupport(RubyPlatformManager.class); private RubyPlatformManager() { // static methods only } /** * So far, for unit tests only. * <p> * Resets platforms cache. */ static void resetPlatforms() { platforms = null; firePlatformsChanged(); } /** * Get a set of all registered platforms. */ public static synchronized Set<RubyPlatform> getPlatforms() { return new HashSet<RubyPlatform>(getPlatformsInternal()); } /** * Get a set of all registered platforms, sorted alphabetically by long * description. Fallbacks to the interpreter's paths comparison. */ public static synchronized SortedSet<? extends RubyPlatform> getSortedPlatforms() { SortedSet<RubyPlatform> _platforms = new TreeSet<RubyPlatform>(ALPHABETICAL_COMPARATOR); _platforms.addAll(getPlatformsInternal()); return _platforms; } /** * Try to detect Ruby platforms available on the system. Might be slow. Do * not call from thread like EDT. */ public synchronized static void performPlatformDetection() { if (PREINDEXING) { return; } // Check the path to see if we find any other Ruby installations final Set<File> rubies = new LinkedHashSet<File>(); Collection<String> candidateDirs = new LinkedHashSet<String>(); candidateDirs.addAll(Util.dirsOnPath()); candidateDirs.addAll(Util.rvmRubies()); for (String dir : candidateDirs) { for (String ruby : RUBY_EXECUTABLE_NAMES) { File f = findPlatform(dir, ruby); if (f != null) { rubies.add(f); } } } RubyPlatform defaultPlatform = findDefaultPlatform(); if (defaultPlatform != null) { getPlatformsInternal().add(defaultPlatform); } for (File ruby : rubies) { try { if (getPlatformByFile(ruby) == null) { addPlatform(ruby); } } catch (IOException e) { // tell the user that something goes wrong LOGGER.log(Level.WARNING, e.getLocalizedMessage(), e); } } RubyPreferences.setFirstPlatformTouch(false); } private static RubyPlatform findDefaultPlatform() { String path = RubyInstallation.getInstance().getJRuby(); return path == null ? null : new RubyPlatform(PLATFORM_ID_DEFAULT, path, Info.forDefaultPlatform()); } private static void firePlatformsChanged() { try { VETOABLE_CHANGE_SUPPORT.fireVetoableChange("platforms", null, null); //NOI18N } catch (PropertyVetoException ex) { // do nothing, vetoing not implemented yet } } private static File findPlatform(final String dir, final String ruby) { File f = null; if (Utilities.isWindows()) { f = new File(dir, ruby + ".exe"); // NOI18N } else { f = new File(dir, ruby); // NOI18N // Don't include /usr/bin/ruby on the Mac - it's no good // Source: http://developer.apple.com/tools/rubyonrails.html // "The version of Ruby that shipped on Mac OS X Tiger prior to // v10.4.6 did not work well with Rails. If you're running // an earlier version of Tiger, you'll need to either upgrade // to 10.4.6 or upgrade your copy of Ruby to version 1.8.4 or // later using the open source distribution." if (ruby.equals("ruby") && Utilities.isMac() && "/usr/bin/ruby".equals(f.getPath())) { // NOI18N String version = System.getProperty("os.version"); // NOI18N if (version == null || version.startsWith("10.4")) { // Only a problem on Tiger // NOI18N return null; } } } if (f.isFile()) { return f; } return null; } private static synchronized Set<RubyPlatform> getPlatformsInternal() { if (platforms == null) { platforms = new HashSet<RubyPlatform>(); // Currently used by $NB_SRC/o.jruby/UPDATE.zsh preindexing hook. // Also see o.jruby/INDICES.txt. String hardcodedRuby = System.getProperty("ruby.interpreter"); if (hardcodedRuby != null) { Info info = new Info("User-specified Ruby", "0.1"); // NOI18N FileObject gems = FileUtil.toFileObject(new File(hardcodedRuby)).getParent().getParent().getFileObject("lib/ruby/gems/1.8"); // NOI18N if (gems != null) { Properties props = new Properties(); props.setProperty(Info.RUBY_KIND, "User-specified Ruby"); // NOI18N props.setProperty(Info.RUBY_VERSION, "0.1"); // NOI18N String gemHome = FileUtil.toFile(gems).getAbsolutePath(); props.setProperty(Info.GEM_HOME, gemHome); props.setProperty(Info.GEM_PATH, gemHome); props.setProperty(Info.GEM_VERSION, "1.0.1 (1.0.1)"); // NOI18N props.setProperty(Info.RUBY_LIB_DIR, new File(new File(hardcodedRuby).getParentFile().getParentFile(), "lib" + File.separator + "ruby" + File.separator + "1.8").getPath()); // NOI18N info = new Info(props); } platforms.add(new RubyPlatform(PLATFORM_ID_DEFAULT, hardcodedRuby, info)); return platforms; } Map<String, String> p = PropertyUtils.sequentialPropertyEvaluator(null, PropertyUtils.globalPropertyProvider()).getProperties(); if (p == null) { // #115909 p = Collections.emptyMap(); } boolean foundDefault = false; final List<String> skipped = new ArrayList<String>(); for (Map.Entry<String, String> entry : p.entrySet()) { String key = entry.getKey(); if (key.startsWith(PLATFORM_PREFIX) && key.endsWith(PLATFORM_INTEPRETER)) { String id = key.substring(PLATFORM_PREFIX.length(), key.length() - PLATFORM_INTEPRETER.length()); String idDot = id + '.'; Properties props = new Properties(); String libDir = p.get(PLATFORM_PREFIX + idDot + Info.RUBY_LIB_DIR); String kind = p.get(PLATFORM_PREFIX + idDot + Info.RUBY_KIND); String interpreterPath = entry.getValue(); if (kind == null) { // not supporting old 6.0 platform, skip skipped.add(interpreterPath); continue; } if (libDir != null) { // NOI18N props.put(Info.RUBY_LIB_DIR, libDir); } else { // Rubinius libDir is not detected by script if (!"Rubinius".equals(kind)) { // NOI18N LOGGER.warning("no libDir for platform: " + interpreterPath); // NOI18N skipped.add(interpreterPath); continue; } } props.put(Info.RUBY_KIND, kind); props.put(Info.RUBY_VERSION, p.get(PLATFORM_PREFIX + idDot + Info.RUBY_VERSION)); String jrubyVersion = p.get(PLATFORM_PREFIX + idDot + Info.JRUBY_VERSION); if (jrubyVersion != null) { props.put(Info.JRUBY_VERSION, jrubyVersion); } String patchLevel = p.get(PLATFORM_PREFIX + idDot + Info.RUBY_PATCHLEVEL); if (patchLevel != null){ props.put(Info.RUBY_PATCHLEVEL, patchLevel); } props.put(Info.RUBY_RELEASE_DATE, p.get(PLATFORM_PREFIX + idDot + Info.RUBY_RELEASE_DATE)); // props.put(Info.RUBY_EXECUTABLE, p.get(PLATFORM_PREFIX + idDot + Info.RUBY_EXECUTABLE)); props.put(Info.RUBY_PLATFORM, p.get(PLATFORM_PREFIX + idDot + Info.RUBY_PLATFORM)); String gemHome = p.get(PLATFORM_PREFIX + idDot + Info.GEM_HOME); if (gemHome != null) { props.put(Info.GEM_HOME, gemHome); props.put(Info.GEM_PATH, p.get(PLATFORM_PREFIX + idDot + Info.GEM_PATH)); props.put(Info.GEM_VERSION, p.get(PLATFORM_PREFIX + idDot + Info.GEM_VERSION)); } Info info = new Info(props); platforms.add(new RubyPlatform(id, interpreterPath, info)); foundDefault |= id.equals(PLATFORM_ID_DEFAULT); } } if (!foundDefault) { RubyPlatform defaultPlatform = findDefaultPlatform(); if (defaultPlatform != null) { platforms.add(defaultPlatform); } } RequestProcessor.getDefault().post(new Runnable() { public void run() { for (String interpreter : skipped) { try { addPlatform(new File(interpreter)); } catch (IOException ex) { Exceptions.printStackTrace(ex); } } } }); LOGGER.fine("RubyPlatform initial list: " + platforms); } return platforms; } /** Typically bundled JRuby. */ public static RubyPlatform getDefaultPlatform() { RubyPlatform defaultPlatform = RubyPlatformManager.getPlatformByID(PLATFORM_ID_DEFAULT); if (defaultPlatform == null) { LOGGER.fine("Default platform is not installed"); } return defaultPlatform; } /** * Find a platform by its ID. * @param id an ID (as in {@link #getID}) * @return the platform with that ID, or null */ public static synchronized RubyPlatform getPlatformByID(String id) { for (RubyPlatform p : getPlatformsInternal()) { if (p.getID().equals(id)) { return p; } } return null; } public static synchronized RubyPlatform getPlatformByFile(File interpreter) { for (RubyPlatform p : getPlatformsInternal()) { try { File current = new File(p.getInterpreter()).getCanonicalFile(); File toFind = interpreter.getCanonicalFile(); if (current.equals(toFind)) { return p; } } catch (IOException e) { LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e); } } return null; } static synchronized RubyPlatform getPlatformByPath(String path) { return getPlatformByFile(new File(path)); } /** * Adds platform to the current platform list. Checks whether such platform * is already present. * * @param interpreter interpreter to be added * @return <tt>null</tt>, if the given <tt>interpreter</tt> is not valid * Ruby interpreter. If the platform is already present, returns it. * Otherwise new platform instance is returned. * @throws java.io.IOException */ public static RubyPlatform addPlatform(final File interpreter) throws IOException { if (!interpreter.isFile()) { return null; } RubyPlatform plaf = getPlatformByFile(interpreter); if (plaf != null) { return plaf; } final Info info = computeInfo(interpreter); if (info == null) { return null; } if (info.getKind() == null) { // # see #128354 LOGGER.warning("Getting platform information for " + interpreter + " failed."); return null; } final String id = computeID(info.getKind()); try { ProjectManager.mutex().writeAccess(new Mutex.ExceptionAction<Void>() { public Void run() throws IOException { if (getPlatformByID(id) != null) { throw new IOException("ID " + id + " already taken"); // NOI18N } EditableProperties props = PropertyUtils.getGlobalProperties(); putPlatformProperties(id, interpreter, info, props); PropertyUtils.putGlobalProperties(props); return null; } }); } catch (MutexException e) { throw (IOException) e.getException(); } plaf = new RubyPlatform(id, interpreter.getAbsolutePath(), info); synchronized (RubyPlatform.class) { getPlatformsInternal().add(plaf); } firePlatformsChanged(); LOGGER.fine("RubyPlatform added: " + plaf); return plaf; } public static void removePlatform(final RubyPlatform plaf) throws IOException { try { ProjectManager.mutex().writeAccess(new Mutex.ExceptionAction<Void>() { public Void run() throws IOException { EditableProperties props = PropertyUtils.getGlobalProperties(); clearProperties(plaf, props); PropertyUtils.putGlobalProperties(props); return null; } }); } catch (MutexException e) { throw (IOException) e.getException(); } synchronized (RubyPlatform.class) { getPlatformsInternal().remove(plaf); } firePlatformsChanged(); LOGGER.fine("RubyPlatform removed: " + plaf); } public static void storePlatform(final RubyPlatform plaf) throws IOException { try { ProjectManager.mutex().writeAccess(new Mutex.ExceptionAction<Void>() { public Void run() throws IOException { EditableProperties props = PropertyUtils.getGlobalProperties(); clearProperties(plaf, props); putPlatformProperties(plaf.getID(), plaf.getInterpreterFile(), plaf.getInfo(), props); PropertyUtils.putGlobalProperties(props); return null; } }); } catch (MutexException e) { throw (IOException) e.getException(); } LOGGER.fine("RubyPlatform stored: " + plaf); } private static void clearProperties(RubyPlatform plaf, EditableProperties props) { String id = PLATFORM_PREFIX + plaf.getID(); props.remove(id + PLATFORM_INTEPRETER); String idDot = id + '.'; props.remove(PLATFORM_PREFIX + idDot + Info.RUBY_KIND); props.remove(PLATFORM_PREFIX + idDot + Info.RUBY_VERSION); props.remove(PLATFORM_PREFIX + idDot + Info.JRUBY_VERSION); props.remove(PLATFORM_PREFIX + idDot + Info.RUBY_PATCHLEVEL); props.remove(PLATFORM_PREFIX + idDot + Info.RUBY_RELEASE_DATE); // props.remove(PLATFORM_PREFIX + idDot + Info.RUBY_EXECUTABLE); props.remove(PLATFORM_PREFIX + idDot + Info.RUBY_PLATFORM); props.remove(PLATFORM_PREFIX + idDot + Info.RUBY_LIB_DIR); props.remove(PLATFORM_PREFIX + idDot + Info.GEM_HOME); props.remove(PLATFORM_PREFIX + idDot + Info.GEM_PATH); props.remove(PLATFORM_PREFIX + idDot + Info.GEM_VERSION); } private static void putPlatformProperties(final String id, final File interpreter, final Info info, final EditableProperties props) throws FileNotFoundException { String interpreterKey = PLATFORM_PREFIX + id + PLATFORM_INTEPRETER; props.setProperty(interpreterKey, interpreter.getAbsolutePath()); if (!interpreter.isFile()) { throw new FileNotFoundException(interpreter.getAbsolutePath()); } String idDot = id + '.'; props.setProperty(PLATFORM_PREFIX + idDot + Info.RUBY_KIND, info.getKind()); props.setProperty(PLATFORM_PREFIX + idDot + Info.RUBY_VERSION, info.getVersion()); if (info.getJVersion() != null) { props.setProperty(PLATFORM_PREFIX + idDot + Info.JRUBY_VERSION, info.getJVersion()); } if (info.getPatchlevel() != null) { props.setProperty(PLATFORM_PREFIX + idDot + Info.RUBY_PATCHLEVEL, info.getPatchlevel()); } props.setProperty(PLATFORM_PREFIX + idDot + Info.RUBY_RELEASE_DATE, info.getReleaseDate()); // props.setProperty(PLATFORM_PREFIX + idDot + Info.RUBY_EXECUTABLE, info.getExecutable()); props.setProperty(PLATFORM_PREFIX + idDot + Info.RUBY_PLATFORM, info.getPlatform()); if (!info.isRubinius()) { props.setProperty(PLATFORM_PREFIX + idDot + Info.RUBY_LIB_DIR, info.getLibDir()); } if (info.getGemHome() != null) { props.setProperty(PLATFORM_PREFIX + idDot + Info.GEM_HOME, info.getGemHome()); props.setProperty(PLATFORM_PREFIX + idDot + Info.GEM_PATH, info.getGemPath()); props.setProperty(PLATFORM_PREFIX + idDot + Info.GEM_VERSION, info.getGemVersion()); } } private static String computeID(final String kind) { String id = kind; for (int i = 0; getPlatformByID(id) != null; i++) { id = kind + '_' + i; } return id; } public static Iterator<RubyPlatform> platformIterator() { return getPlatformsInternal().iterator(); } /** * Might take longer time when detecting e.g. JRuby platform. So do not run * from within EDT and similar threads. * * @param interpreter representing Ruby platform * @return information about the platform or <tt>null</tt> if * <tt>interpreter</tt> is not recognized as platform */ static Info computeInfo(final File interpreter) { assert !EventQueue.isDispatchThread() : "computeInfo should not be run from EDT"; if (TEST_RUBY_PROPS != null && !RubyPlatformManager.getDefaultPlatform().getInterpreterFile().equals(interpreter)) { // tests return new Info(TEST_RUBY_PROPS); } Info info = null; try { File platformInfoScript = InstalledFileLocator.getDefault().locate( "platform_info.rb", "org.netbeans.modules.ruby.platform", false); // NOI18N if (platformInfoScript == null) { throw new IllegalStateException("Cannot locate platform_info.rb script"); // NOI18N } ProcessBuilder pb = new ProcessBuilder(interpreter.getAbsolutePath(), platformInfoScript.getAbsolutePath()); // NOI18N // be sure that JRUBY_HOME is not set during configuration // autodetection, otherwise interpreter under JRUBY_HOME would be // effectively used pb.environment().remove("JRUBY_HOME"); // NOI18N pb.environment().put("JAVA_HOME", ExecutionUtils.getJavaHome()); // NOI18N ExecutionUtils.logProcess(pb); final Process proc = pb.start(); // FIXME: set timeout Thread gatherer = new Thread(new Runnable() { public void run() { try { proc.waitFor(); } catch (InterruptedException e) { LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e); } } }, "Ruby Platform Gatherer"); // NOI18N gatherer.start(); try { gatherer.join(30000); // 30s timeout for platform_info.rb } catch (InterruptedException e) { LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e); return null; } int exitValue; try { exitValue = proc.exitValue(); } catch (IllegalThreadStateException e) { // process is still running LOGGER.warning("Detection of platform timeouted"); proc.destroy(); return null; } if (exitValue == 0) { Properties props = new Properties(); if (LOGGER.isLoggable(Level.FINER)) { String stdout = Util.readAsString(proc.getInputStream()); String stderr = Util.readAsString(proc.getErrorStream()); LOGGER.finer("stdout:\n" + stdout); LOGGER.finer("stderr:\n " + stderr); props.load(new ReaderInputStream(new StringReader(stdout))); } else { props.load(proc.getInputStream()); } info = new Info(props); } else { LOGGER.severe(interpreter.getAbsolutePath() + " does not seems to be a valid interpreter"); // TODO localize me BufferedReader errors = new BufferedReader(new InputStreamReader(proc.getErrorStream())); String line; while ((line = errors.readLine()) != null) { LOGGER.severe(line); } } } catch (IOException e) { LOGGER.log(Level.INFO, "Not a ruby platform: " + interpreter.getAbsolutePath()); // NOI18N } return info; } public static void addVetoableChangeListener(VetoableChangeListener listener) { VETOABLE_CHANGE_SUPPORT.addVetoableChangeListener(listener); } public static void removeVetoableChangeListener(VetoableChangeListener listener) { VETOABLE_CHANGE_SUPPORT.removeVetoableChangeListener(listener); } }