package com.aptana.rdt.internal.core.gems; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.parsers.FactoryConfigurationError; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParserFactory; import org.eclipse.core.net.proxy.IProxyData; import org.eclipse.core.net.proxy.IProxyService; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.SubMonitor; import org.eclipse.core.runtime.SubProgressMonitor; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.debug.core.DebugException; import org.eclipse.debug.core.DebugPlugin; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.ILaunchConfiguration; import org.eclipse.debug.core.ILaunchConfigurationType; import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; import org.eclipse.debug.core.ILaunchManager; import org.eclipse.debug.ui.IDebugUIConstants; import org.rubypeople.rdt.core.RubyCore; import org.rubypeople.rdt.core.util.Util; import org.rubypeople.rdt.launching.IRubyLaunchConfigurationConstants; import org.rubypeople.rdt.launching.IVMInstall; import org.rubypeople.rdt.launching.IVMInstallChangedListener; import org.rubypeople.rdt.launching.PropertyChangeEvent; import org.rubypeople.rdt.launching.RubyRuntime; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import com.aptana.rdt.AptanaRDTPlugin; import com.aptana.rdt.core.gems.AbstractGemManager; import com.aptana.rdt.core.gems.Gem; import com.aptana.rdt.core.gems.GemListener; import com.aptana.rdt.core.gems.GemRequirement; import com.aptana.rdt.core.gems.IGemManager; import com.aptana.rdt.core.gems.LogicalGem; import com.aptana.rdt.core.gems.Version; import com.aptana.rdt.core.preferences.IPreferenceConstants; // XXX If user tries to install a gem that someone has contributed a local copy of, try using the local copy! (Need to worry about dependencies then!) public class GemManager extends AbstractGemManager implements IGemManager, IVMInstallChangedListener { private static final String DETAIL_SWITCH = "-d"; private static final String SOURCE_SWITCH = "--source"; private static final String INCLUDE_DEPENDENCIES_SWITCH = "-y"; private static final String LOCAL_SWITCH = "-l"; private static final String VERSION_SWITCH = "-v"; private static final String REMOTE_SWITCH = "-r"; private static final String LIST_COMMAND = "list"; private static final String INSTALL_COMMAND = "install"; private static final String UNINSTALL_COMMAND = "uninstall"; private static final String UPDATE_COMMAND = "update"; private static final String CLEANUP_COMMAND = "cleanup"; private static final String EXECUTABLE = "ruby"; private static final String LOCAL_GEMS_CACHE_FILE = "local_gems.xml"; private static final String RUBYGEMS_UPDATE_GEM_NAME = "rubygems-update"; private static final String UPDATE_RUBYGEMS_COMMAND = "update_rubygems"; private static GemManager fgInstance; private Set<Gem> gems; private Set<String> urls; private List<IPath> fGemInstallPaths; private Map<String, Set<Gem>> fRemoteGems = new HashMap<String, Set<Gem>>(); protected boolean isInitialized; private Version fVersion; private static int seed = 0; // A number we append to the launch configs' name to ensure uniqueness (because the // method which is supposed to generate unique names in the LaunchManager doesn't // actually do it). protected GemManager() { super(); urls = new HashSet<String>(); gems = new HashSet<Gem>(); } public boolean isInitialized() { return isInitialized; } protected Set<Gem> loadLocalCache(File file) { FileReader fileReader = null; try { fileReader = new FileReader(file); XMLReader reader = SAXParserFactory.newInstance().newSAXParser().getXMLReader(); GemManagerContentHandler handler = new GemManagerContentHandler(); reader.setContentHandler(handler); reader.parse(new InputSource(fileReader)); return handler.getGems(); } catch (FileNotFoundException e) { // This is okay, will get thrown if no config exists yet } catch (SAXException e) { AptanaRDTPlugin.log(e); } catch (ParserConfigurationException e) { AptanaRDTPlugin.log(e); } catch (FactoryConfigurationError e) { AptanaRDTPlugin.log(e); } catch (IOException e) { AptanaRDTPlugin.log(e); } finally { try { if (fileReader != null) fileReader.close(); } catch (IOException e) { // ignore } } return new HashSet<Gem>(); } protected void storeGemCache(Set<Gem> gems, File file) { XMLWriter out = null; try { out = new XMLWriter(new FileOutputStream(file)); writeXML(gems, out); } catch (FileNotFoundException e) { AptanaRDTPlugin.log(e); } catch (IOException e) { AptanaRDTPlugin.log(e); } finally { if (out != null) out.close(); } } protected File getConfigFile(String fileName) { return AptanaRDTPlugin.getDefault().getStateLocation().append(fileName).toFile(); } /** * Writes each server configuration to file in XML format. * * @param gems * @param out * the writer to use */ private void writeXML(Set<Gem> gems, XMLWriter out) { out.startTag("gems", null); for (Gem gem : gems) { out.startTag("gem", null); out.printSimpleTag("name", gem.getName()); out.printSimpleTag("version", gem.getVersion()); out.printSimpleTag("description", gem.getDescription()); out.printSimpleTag("platform", gem.getPlatform()); out.endTag("gem"); } out.endTag("gems"); out.flush(); } protected Set<Gem> loadRemoteGems(String gemIndexUrl, IProgressMonitor monitor) { if (!isRubyGemsInstalled()) return new HashSet<Gem>(); IGemParser parser; String command = LIST_COMMAND + " " + DETAIL_SWITCH + " " + REMOTE_SWITCH + " " + SOURCE_SWITCH + " " + gemIndexUrl; String output = launchInBackgroundAndRead(command, getStateFile("remote_listing.txt")); if (output != null && output.contains("ERROR:")) { // If the text returned contains "ERROR:", then we should try the short listing command command = LIST_COMMAND + " " + REMOTE_SWITCH + " " + SOURCE_SWITCH + " " + gemIndexUrl; output = launchInBackgroundAndRead(command, getStateFile("remote_listing.txt")); parser = getGemParser(false); } else { parser = getGemParser(true); } try { return parser.parse(output); } catch (GemParseException e) { return Collections.emptySet(); } } protected IGemParser getGemParser() { return getGemParser(true); } protected IGemParser getGemParser(boolean detailed) { if (detailed) return new HybridGemParser(getVersion()); return new ShortListingGemParser(); } private Set<Gem> loadLocalGems(IProgressMonitor monitor) { if (!isRubyGemsInstalled()) return new HashSet<Gem>(); IGemParser parser = getGemParser(); String output = getLocalGemsListing(); try { return parser.parse(output); } catch (GemParseException e) { return Collections.emptySet(); } } private String launchInBackgroundAndRead(final ILaunchConfiguration config, final File file) { return RubyRuntime.launchInBackgroundAndRead(config, file); } private String launchInBackgroundAndRead(String command, File file) { return launchInBackgroundAndRead(createGemLaunchConfiguration(command, false), file); } public Version getVersion() { if (fVersion != null) return fVersion; int tries = 0; while (fVersion == null && tries < 3) { String version = launchInBackgroundAndRead("-v", getStateFile("version.txt")); try { if (version != null && version.trim().length() > 0) fVersion = new Version(version.trim()); } catch (RuntimeException e) { AptanaRDTPlugin.log(e); fVersion = null; } tries++; } return fVersion; } private String getLocalGemsListing() { String command = "query -d"; // If we're using RubyGems older than 0.9.3, we need to do a "gem list -l" to get the equivalent of query -d if (getVersion() != null && getVersion().isLessThanOrEqualTo("0.9.3")) { command = LIST_COMMAND + " " + LOCAL_SWITCH; } return launchInBackgroundAndRead(command, getGemListingFile()); } private File getGemListingFile() { return getStateFile("local_listing.txt"); } /* * (non-Javadoc) * @see com.aptana.rdt.internal.gems.IGemManager#update(com.aptana.rdt.internal.gems.Gem) */ public IStatus update(final Gem gem, IProgressMonitor monitor) { if (monitor == null) monitor = new NullProgressMonitor(); if (!isRubyGemsInstalled()) return new Status(IStatus.ERROR, AptanaRDTPlugin.PLUGIN_ID, -1, "RubyGems not installed", null); try { String command = UPDATE_COMMAND + " " + gem.getName(); command = addProxy(IGemManager.DEFAULT_GEM_HOST, command); ILaunchConfiguration config = createGemLaunchConfiguration(command, true); if (monitor.isCanceled()) return Status.CANCEL_STATUS; final ILaunch launch = config.launch(ILaunchManager.RUN_MODE, monitor); while (!launch.isTerminated()) { if (monitor.isCanceled()) { try { launch.terminate(); } catch (DebugException e) { // ignore } return Status.CANCEL_STATUS; } Thread.yield(); } refresh(monitor); for (GemListener listener : new ArrayList<GemListener>(listeners)) { listener.gemUpdated(gem); } return Status.OK_STATUS; } catch (CoreException e) { return e.getStatus(); } } private ILaunchConfigurationType getRubyApplicationConfigType() { return getLaunchManager().getLaunchConfigurationType(IRubyLaunchConfigurationConstants.ID_RUBY_APPLICATION); } private ILaunchManager getLaunchManager() { return DebugPlugin.getDefault().getLaunchManager(); } private ILaunchConfiguration createGemLaunchConfiguration(String arguments, boolean isSudo) { String gemPath = getGemScriptPath(); ILaunchConfiguration config = null; try { ILaunchConfigurationType configType = getRubyApplicationConfigType(); ILaunchConfigurationWorkingCopy wc = configType.newInstance(null, getUniqueName("gem")); wc.setAttribute(IRubyLaunchConfigurationConstants.ATTR_FILE_NAME, gemPath); wc.setAttribute(IRubyLaunchConfigurationConstants.ATTR_VM_INSTALL_NAME, RubyRuntime.getDefaultVMInstall() .getName()); wc.setAttribute(IRubyLaunchConfigurationConstants.ATTR_VM_INSTALL_TYPE, RubyRuntime.getDefaultVMInstall() .getVMInstallType().getId()); wc.setAttribute(IRubyLaunchConfigurationConstants.ATTR_PROGRAM_ARGUMENTS, arguments); wc.setAttribute(IRubyLaunchConfigurationConstants.ATTR_VM_ARGUMENTS, ""); wc.setAttribute(IRubyLaunchConfigurationConstants.ATTR_IS_SUDO, isSudo); if (isSudo) { wc.setAttribute(IRubyLaunchConfigurationConstants.ATTR_TERMINAL_COMMAND, "gem " + arguments); wc.setAttribute(IRubyLaunchConfigurationConstants.ATTR_USE_TERMINAL, "org.radrails.rails.shell"); // use // rails // shell // if // it's // available } Map<String, String> map = new HashMap<String, String>(); map.put(IRubyLaunchConfigurationConstants.ATTR_RUBY_COMMAND, EXECUTABLE); wc.setAttribute(IRubyLaunchConfigurationConstants.ATTR_VM_INSTALL_TYPE_SPECIFIC_ATTRS_MAP, map); wc.setAttribute(IDebugUIConstants.ATTR_PRIVATE, true); wc.setAttribute(IDebugUIConstants.ATTR_LAUNCH_IN_BACKGROUND, false); config = wc.doSave(); } catch (CoreException ce) { AptanaRDTPlugin.log(ce); } return config; } private synchronized String getUniqueName(String name) { return RubyRuntime.generateUniqueLaunchConfigurationNameFrom(name) + seed++; } public ILaunchConfiguration run(String args) throws CoreException { boolean useSudo = false; if ((args.contains("install ") || args.contains("update") || args.contains("uninstall ") || args .contains("cleanup")) && !args.contains("help ")) { useSudo = true; } return createGemLaunchConfiguration(args, useSudo); } private static String getGemScriptPath() { String path = Platform.getPreferencesService().getString(AptanaRDTPlugin.PLUGIN_ID, IPreferenceConstants.GEM_SCRIPT_PATH, "", null); if (path != null && path.trim().length() > 0) return path; // TODO Cache this result until the VM changes? IVMInstall vm = RubyRuntime.getDefaultVMInstall(); if (vm == null) return null; path = vm.getInstallLocation().getAbsolutePath() + File.separator + "bin" + File.separator + "gem"; // FIXME What if it picks up bad file like gemtopbm on cygwin?! Need to prioritize somehow! File gemScript = Util.findFileWithOptionalSuffix(path); if (gemScript == null) return null; return gemScript.getAbsolutePath(); } public boolean isRubyGemsInstalled() { String path = getGemScriptPath(); if (path == null) return false; File file = new File(path); return file.exists(); } /* * (non-Javadoc) * @see com.aptana.rdt.core.gems.IGemManager#installGem(com.aptana.rdt.core.gems.Gem, * org.eclipse.core.runtime.IProgressMonitor) */ public IStatus installGem(final Gem gem, IProgressMonitor monitor) { return installGem(gem, true, monitor); } public IStatus installGem(final Gem gem, boolean includeDependencies, IProgressMonitor monitor) { if (gem.isLocal()) { return doLocalInstallGem(gem, monitor); } return installGem(gem, DEFAULT_GEM_HOST, includeDependencies, monitor); } /* * (non-Javadoc) * @see com.aptana.rdt.internal.gems.IGemManager#removeGem(com.aptana.rdt.internal.gems.Gem) */ public IStatus removeGem(final Gem gem, IProgressMonitor monitor) { if (!isRubyGemsInstalled()) return new Status(IStatus.ERROR, AptanaRDTPlugin.PLUGIN_ID, -1, "RubyGems not installed", null); try { String command = UNINSTALL_COMMAND + " " + gem.getName(); if (gem.getVersion() != null && gem.getVersion().trim().length() > 0) { command += " " + VERSION_SWITCH + " " + gem.getVersion(); } ILaunchConfiguration config = createGemLaunchConfiguration(command, true); if (monitor.isCanceled()) return Status.CANCEL_STATUS; final ILaunch launch = config.launch(ILaunchManager.RUN_MODE, monitor); while (!launch.isTerminated()) { if (monitor.isCanceled()) { launch.terminate(); return Status.CANCEL_STATUS; } Thread.yield(); } refresh(monitor); // Need to wait until uninstall is finished for (GemListener listener : new ArrayList<GemListener>(listeners)) { listener.gemRemoved(gem); } return Status.OK_STATUS; } catch (CoreException e) { return e.getStatus(); } } /* * (non-Javadoc) * @see com.aptana.rdt.internal.gems.IGemManager#getGems() */ public Set<Gem> getGems() { return Collections.unmodifiableSortedSet(new TreeSet<Gem>(gems)); } public static GemManager getInstance() { if (fgInstance == null) fgInstance = new GemManager(); return fgInstance; } /* * (non-Javadoc) * @see com.aptana.rdt.core.gems.IGemManager#refresh(org.eclipse.core.runtime.IProgressMonitor) */ public IStatus refresh(IProgressMonitor monitor) { SubMonitor progress = SubMonitor.convert(monitor, 100); Set<Gem> newGems = loadLocalGems(progress.newChild(95)); gems = newGems; storeGemCache(gems, getConfigFile(LOCAL_GEMS_CACHE_FILE)); progress.worked(4); Job job = new Job("notifying Gem Listeners of refresh") { @Override protected IStatus run(IProgressMonitor monitor) { for (GemListener listener : new ArrayList<GemListener>(listeners)) { listener.gemsRefreshed(); } return Status.OK_STATUS; } }; job.setSystem(true); job.schedule(); progress.done(); return Status.OK_STATUS; } /* * (non-Javadoc) * @see com.aptana.rdt.internal.gems.IGemManager#getRemoteGems() */ public Set<Gem> getRemoteGems() { return getRemoteGems(DEFAULT_GEM_HOST, new NullProgressMonitor()); } public Set<Gem> getRemoteGems(String sourceURL, IProgressMonitor monitor) { Set<Gem> remoteGems = new HashSet<Gem>(); if (fRemoteGems.containsKey(sourceURL)) { // FIXME How long should we be caching this? remoteGems = fRemoteGems.get(sourceURL); } else { remoteGems = makeLogical(loadRemoteGems(sourceURL, monitor)); if (!remoteGems.isEmpty()) { addSourceURL(sourceURL); fRemoteGems.put(sourceURL, remoteGems); } } return Collections.unmodifiableSortedSet(new TreeSet<Gem>(remoteGems)); } protected void addSourceURL(String sourceURL) { if (urls.contains(sourceURL)) return; launchInBackgroundAndRead("sources -a " + sourceURL, getConfigFile("add_source.txt")); urls.add(sourceURL); } public Set<String> getSourceURLs() { return Collections.unmodifiableSet(new TreeSet<String>(urls)); } /* * (non-Javadoc) * @see com.aptana.rdt.internal.gems.IGemManager#gemInstalled(java.lang.String) */ public boolean gemInstalled(String gemName) { Set<Gem> gems = getGems(); for (Gem gem : gems) { if (gem.getName().equalsIgnoreCase(gemName)) return true; } return false; } public synchronized List<IPath> getGemInstallPaths() { if (fGemInstallPaths == null) { if (!isRubyGemsInstalled()) return null; ILaunchConfiguration config = createGemLaunchConfiguration("", false); if (config == null) return null; try { ILaunchConfigurationWorkingCopy wc = config.getWorkingCopy(); if (wc == null) return null; wc.setAttribute(IRubyLaunchConfigurationConstants.ATTR_VM_ARGUMENTS, "-r rubygems -e p(Gem.path)"); config = wc.doSave(); } catch (CoreException e) { AptanaRDTPlugin.log(e); } try { String output = launchInBackgroundAndRead(config, getGemInstallPathFile()); fGemInstallPaths = parseInstallPaths(output); } catch (IllegalArgumentException e) { fGemInstallPaths = null; return null; } } return fGemInstallPaths; } private List<IPath> parseInstallPaths(String output) { try { if (output == null || output.trim().length() == 0) throw new IllegalArgumentException("Got empty output for gem install paths"); output = output.trim(); if (!output.startsWith("[") || !output.endsWith("]")) throw new IllegalArgumentException("Expected an array for gem install paths, but was: " + output); // toss the array brackets output = new String(output.substring(1, output.length() - 1)); String[] paths = output.split(","); if (paths == null || paths.length < 1) return null; List<IPath> installPaths = new ArrayList<IPath>(); for (int i = 0; i < paths.length; i++) { String path = paths[i].trim(); // toss out the quotes path = new String(path.substring(1, path.length() - 1)); installPaths.add(new Path(path.trim())); } return installPaths; } catch (Exception e) { AptanaRDTPlugin.log(e); } return null; } private File getGemInstallPathFile() { return getStateFile("install_path.txt"); } private File getStateFile(String name) { String currentVMId = RubyRuntime.getDefaultVMInstall().getId(); File file = AptanaRDTPlugin.getDefault().getStateLocation().append("gems").append(currentVMId).append(name) .toFile(); try { file.getParentFile().mkdirs(); file.createNewFile(); } catch (IOException e) { // ignore } return file; } public IPath getGemPath(String gemName) { List<IPath> paths = getGemInstallPaths(); if (paths == null) return null; List<IPath> matches = new ArrayList<IPath>(); for (IPath path : paths) { path = path.append("gems"); File gemFolder = path.toFile(); File[] gems = gemFolder.listFiles(); if (gems == null) continue; for (int i = 0; i < gems.length; i++) { File gem = gems[i]; String name = gem.getName(); if (name.startsWith(gemName)) matches.add(new Path(gem.getAbsolutePath())); } } if (matches.isEmpty()) return null; if (matches.size() == 1) return matches.get(0).append("lib"); // otherwise, find latest version List<Version> versions = new ArrayList<Version>(); for (IPath match : matches) { String name = match.lastSegment(); String[] parts = name.split("-"); for (int i = parts.length - 1; i >= 0; i--) { String version = parts[i]; try { Version duh = new Version(version); versions.add(duh); break; } catch (IllegalArgumentException e) { // ignore, that part may not be version for gem } } } Collections.sort(versions); Version latest = versions.get(versions.size() - 1); for (IPath match : matches) { String name = match.lastSegment(); String[] parts = name.split("-"); String version = null; for (int i = parts.length - 1; i >= 0; i--) { version = parts[i]; try { Version duh = new Version(version); versions.add(duh); break; } catch (IllegalArgumentException e) { // ignore, that part may not be version for gem } } if (version != null && version.equals(latest.toString())) return match.append("lib"); } return null; } public IPath getGemPath(String gemName, String version) { return getGemPath(gemName + "-" + version); } public IStatus updateAll(IProgressMonitor monitor) { if (monitor == null) monitor = new NullProgressMonitor(); if (!isRubyGemsInstalled()) return new Status(IStatus.ERROR, AptanaRDTPlugin.PLUGIN_ID, "RubyGems not installed", null); // TODO create sub progress monitor? IStatus result = updateSystem(monitor); if (result != null && !result.isOK()) { return result; } try { ILaunchConfiguration config = createGemLaunchConfiguration(addProxy(IGemManager.DEFAULT_GEM_HOST, UPDATE_COMMAND + " " + INCLUDE_DEPENDENCIES_SWITCH), true); if (monitor.isCanceled()) return Status.CANCEL_STATUS; final ILaunch launch = config.launch(ILaunchManager.RUN_MODE, monitor); while (!launch.isTerminated()) { if (monitor.isCanceled()) { launch.terminate(); return Status.CANCEL_STATUS; } Thread.yield(); } refresh(monitor); return Status.OK_STATUS; } catch (CoreException e) { return e.getStatus(); } } public void initialize() { RubyRuntime.addVMInstallChangedListener(this); scheduleLoadingSources(); scheduleLoadingLocalGems(); } private void scheduleLoadingSources() { Job job = new Job("Loading Remote Gem Sources") { @Override protected IStatus run(IProgressMonitor monitor) { urls = loadSourceURLs(); // if (urls.size() < 2) { // addSourceURL(RAILS_GEM_HOST); // } return Status.OK_STATUS; } }; job.setPriority(Job.LONG); job.setSystem(true); job.schedule(); } protected Set<String> loadSourceURLs() { Set<String> sources = new HashSet<String>(); String output = launchInBackgroundAndRead("sources -l", getConfigFile("sources_list.txt")); if (output == null) return sources; String[] lines = output.split("\n"); if (lines == null) return sources; for (int i = 2; i < lines.length; i++) { sources.add(lines[i].trim()); } return sources; } private void scheduleLoadingLocalGems() { Job job = new Job(GemsMessages.GemManager_loading_local_gems) { @Override protected IStatus run(IProgressMonitor monitor) { try { gems = loadLocalCache(getConfigFile(LOCAL_GEMS_CACHE_FILE)); for (GemListener listener : new ArrayList<GemListener>(listeners)) { listener.gemsRefreshed(); } gems = loadLocalGems(monitor); int tries = 0; while (gems.isEmpty() && tries < 3) { // if we get back an empty list retry up to 3 times tries++; gems = loadLocalGems(monitor); } storeGemCache(gems, getConfigFile(LOCAL_GEMS_CACHE_FILE)); isInitialized = true; for (GemListener listener : new ArrayList<GemListener>(listeners)) { listener.managerInitialized(); } for (GemListener listener : new ArrayList<GemListener>(listeners)) { listener.gemsRefreshed(); } } catch (Exception e) { AptanaRDTPlugin.log(e); return Status.CANCEL_STATUS; } return Status.OK_STATUS; } }; job.setPriority(Job.LONG); job.setSystem(true); job.schedule(); } protected Set<Gem> makeLogical(Set<Gem> remoteGems) { SortedSet<Gem> sorted = new TreeSet<Gem>(remoteGems); SortedSet<Gem> logical = new TreeSet<Gem>(); String name = null; Collection<Gem> temp = new HashSet<Gem>(); for (Gem gem : sorted) { if (name != null && !gem.getName().equals(name)) { logical.add(LogicalGem.create(temp)); temp.clear(); } name = gem.getName(); temp.add(gem); } if (name != null && !temp.isEmpty()) { logical.add(LogicalGem.create(temp)); temp.clear(); } return Collections.unmodifiableSortedSet(logical); } public IStatus cleanup(IProgressMonitor monitor) { if (monitor == null) monitor = new NullProgressMonitor(); if (!isRubyGemsInstalled()) return new Status(IStatus.ERROR, AptanaRDTPlugin.PLUGIN_ID, "RubyGems not installed", null); try { String command = CLEANUP_COMMAND; ILaunchConfiguration config = createGemLaunchConfiguration(command, true); if (monitor.isCanceled()) return Status.CANCEL_STATUS; final ILaunch launch = config.launch(ILaunchManager.RUN_MODE, monitor); while (!launch.isTerminated()) { if (monitor.isCanceled()) { launch.terminate(); return Status.CANCEL_STATUS; } Thread.yield(); } refresh(monitor); return Status.OK_STATUS; } catch (CoreException e) { return e.getStatus(); } } /* * (non-Javadoc) * @see com.aptana.rdt.core.gems.IGemManager#installGem(com.aptana.rdt.core.gems.Gem, java.lang.String, * org.eclipse.core.runtime.IProgressMonitor) */ public IStatus installGem(Gem gem, String sourceURL, IProgressMonitor monitor) { return installGem(gem, sourceURL, true, new NullProgressMonitor()); } private IStatus doInstallGem(final Gem gem, String command, IProgressMonitor monitor) { SubMonitor progress = SubMonitor.convert(monitor, 100); if (!isRubyGemsInstalled()) return new Status(IStatus.ERROR, AptanaRDTPlugin.PLUGIN_ID, -1, "RubyGems not installed", null); try { ILaunchConfiguration config = createGemLaunchConfiguration(command, true); final ILaunch launch = config.launch(ILaunchManager.RUN_MODE, null); progress.worked(2); while (!launch.isTerminated()) { if (progress.isCanceled()) { try { launch.terminate(); } catch (DebugException e) { // ignore } return Status.CANCEL_STATUS; } Thread.yield(); } progress.worked(88); refresh(progress.newChild(7)); // Need to wait until install is finished for (GemListener listener : listeners) { listener.gemAdded(gem); } progress.worked(3); return Status.OK_STATUS; } catch (CoreException e) { return e.getStatus(); } finally { progress.done(); } } private IStatus doLocalInstallGem(final Gem gem, IProgressMonitor monitor) { SubMonitor progress = SubMonitor.convert(monitor, 100); progress.setTaskName("Installing " + gem.getName()); if (!isRubyGemsInstalled()) return new Status(IStatus.ERROR, AptanaRDTPlugin.PLUGIN_ID, -1, "RubyGems not installed", null); try { // force working directory to that containing the gem String command = INSTALL_COMMAND + " -l " + new File(gem.getAbsolutePath()).getName() + ""; ILaunchConfiguration config = createGemLaunchConfiguration(command, true); ILaunchConfigurationWorkingCopy wc = config.getWorkingCopy(); wc.setAttribute(IRubyLaunchConfigurationConstants.ATTR_WORKING_DIRECTORY, new File(gem.getAbsolutePath()) .getParent()); config = wc.doSave(); final ILaunch launch = config.launch(ILaunchManager.RUN_MODE, null); while (!launch.isTerminated()) { if (monitor.isCanceled()) { try { launch.terminate(); } catch (DebugException e) { // ignore } return Status.CANCEL_STATUS; } Thread.yield(); } progress.worked(90); refresh(progress.newChild(7)); // Need to wait until uninstall is finished for (GemListener listener : new ArrayList<GemListener>(listeners)) { listener.gemAdded(gem); } progress.worked(3); return Status.OK_STATUS; } catch (CoreException e) { return e.getStatus(); } finally { progress.done(); } } private IStatus installGem(final Gem gem, String sourceURL, boolean includeDependencies, IProgressMonitor monitor) { SubMonitor progress = SubMonitor.convert(monitor, 100); if (!gem.isInstallable()) return new Status(IStatus.ERROR, AptanaRDTPlugin.getPluginId(), "Gem is uninstallable: " + gem.getName()); if (gem.getName() == null || gem.getName().trim().length() == 0) return new Status(IStatus.ERROR, AptanaRDTPlugin.getPluginId(), "Can't install gem with empty name"); if (progress.isCanceled()) return Status.CANCEL_STATUS; String command = INSTALL_COMMAND + " " + gem.getName(); if (gem.getVersion() != null && gem.getVersion().trim().length() > 0) { command += " " + VERSION_SWITCH + " " + gem.getVersion(); } if (getVersion() == null || getVersion().isGreaterThanOrEqualTo("0.9.5")) // assume it's greater than 0.9.5 if // we can't determine version { if (!includeDependencies) { // dependencies included by default in 0.9.5+ command += " --ignore-dependencies"; } } else { if (includeDependencies) { // need to to add switch if on older than 0.9.5 command += " " + INCLUDE_DEPENDENCIES_SWITCH; } } if (sourceURL != null && !sourceURL.equals(DEFAULT_GEM_HOST)) { command += " " + SOURCE_SWITCH + " " + sourceURL; } command = addProxy(sourceURL, command); progress.worked(5); return doInstallGem(gem, command, progress.newChild(95)); } private String addProxy(String host, String command) { IProxyService service = getProxyService(); if (service == null || !service.isProxiesEnabled()) return command; IProxyData proxyData = service.getProxyDataForHost(host, IProxyData.HTTP_PROXY_TYPE); if (proxyData == null) return command; StringBuilder proxyLine = new StringBuilder(" -p http://"); if (proxyData.isRequiresAuthentication()) { proxyLine.append(proxyData.getUserId()); proxyLine.append(":"); proxyLine.append(proxyData.getPassword()); proxyLine.append("@"); } proxyLine.append(proxyData.getHost()); proxyLine.append(":"); proxyLine.append(proxyData.getPort()); return command + proxyLine; } private IProxyService getProxyService() { return AptanaRDTPlugin.getDefault().getProxyService(); } public Set<GemRequirement> getDependencies(Gem gem) { if (!isRubyGemsInstalled()) return Collections.emptySet(); String command = "dependency " + gem.getName() + " -v " + gem.getVersion(); File file = getStateFile("dependencies_" + gem.getName() + "_" + gem.getVersion() + ".txt"); String output = launchInBackgroundAndRead(command, file); Set<GemRequirement> requirements = parseDependencies(output); if (requirements.isEmpty() && gem.getName().equals("rails")) { AptanaRDTPlugin.log("Was unable to find out dependencies for rails gem!"); } return requirements; } private Set<GemRequirement> parseDependencies(String output) { if (output == null) return Collections.emptySet(); Set<GemRequirement> dependencies = new HashSet<GemRequirement>(); Pattern pat = Pattern.compile("\\s+(\\w+)\\s+\\((.+?)\\)"); String[] lines = output.split("[\\r|\\n]"); for (int i = 1; i < lines.length; i++) { // skip first line String line = lines[i]; Matcher matcher = pat.matcher(line); if (!matcher.find()) continue; String name = matcher.group(1); String version = matcher.group(2); dependencies.add(new GemRequirement(name, version)); } return dependencies; } public Gem findGem(GemRequirement dependency) { // There's probably a more efficient way to do this, but oh well. // FIXME Should grab latest version of gem that meets the requirements // FIXME Break logical gems up! for (Gem gem : gems) { if (gem instanceof LogicalGem) { LogicalGem logical = (LogicalGem) gem; Collection<Gem> logicalsGems = logical.getGems(); for (Gem gem2 : logicalsGems) { if (gem2.meetsRequirements(dependency)) return gem2; } } if (gem.meetsRequirements(dependency)) return gem; } return null; } public void defaultVMInstallChanged(IVMInstall previous, IVMInstall current) { fVersion = null; // invalidate the cached version fGemInstallPaths = null; // invalidate cached install path(s) Job job = new Job("Refreshing local gem listing") { @Override protected IStatus run(IProgressMonitor monitor) { return refresh(monitor); } }; job.schedule(); } public void vmAdded(IVMInstall newVm) { } public void vmChanged(PropertyChangeEvent event) { } public void vmRemoved(IVMInstall removedVm) { } public List<Version> getVersions(String gemName) { List<Version> versions = new ArrayList<Version>(); for (Gem gem : gems) { if (gem.getName().equals(gemName)) { versions.add(gem.getVersionObject()); } } return versions; } public String getName() { return "Local Gem Manager"; } /* * (non-Javadoc) * @see com.aptana.rdt.core.gems.IGemManager#updateSystem(org.eclipse.core.runtime.IProgressMonitor) */ public IStatus updateSystem(IProgressMonitor monitor) { if (monitor == null) monitor = new NullProgressMonitor(); monitor.beginTask("Updating RubyGems", 100); if (monitor.isCanceled()) return Status.CANCEL_STATUS; IStatus status = null; if (getVersion() != null && getVersion().isLessThan("1.3.4")) { // TODO Create a sub monitor for this portion? status = updateRubygems(monitor, "1.3.4"); if (status != null && !status.isOK()) return status; } return updateRubygems(monitor, Gem.ANY_VERSION); } private IStatus updateRubygems(IProgressMonitor monitor, String version) { IProgressMonitor subMonitor = new SubProgressMonitor(monitor, 33); IStatus status = installGem(new Gem(RUBYGEMS_UPDATE_GEM_NAME, version, null), subMonitor); subMonitor.done(); if (!status.isOK()) return status; try { // Run "update_rubygems" ILaunchConfiguration config = createGemLaunchConfiguration("", true); ILaunchConfigurationWorkingCopy wc = config.getWorkingCopy(); String fileName = getFileIfExists(UPDATE_RUBYGEMS_COMMAND); wc.setAttribute(IRubyLaunchConfigurationConstants.ATTR_FILE_NAME, fileName); config = wc.doSave(); ILaunch launch = config.launch(ILaunchManager.RUN_MODE, monitor); while (!launch.isTerminated()) { if (monitor.isCanceled()) { launch.terminate(); return Status.CANCEL_STATUS; } Thread.yield(); } monitor.done(); // Check exit status if (launch.getProcesses() != null && launch.getProcesses()[0] != null && launch.getProcesses()[0].getExitValue() != 0) return new Status(IStatus.ERROR, AptanaRDTPlugin.PLUGIN_ID, -1, "Updating rubygems failed", null); // success. Wipe the cached version fVersion = null; } catch (CoreException e) { return e.getStatus(); } return Status.OK_STATUS; } /** * Try to find bin script in interpreter bin path and then system PATH. * * @param command * @return */ private String getFileIfExists(String command) { IPath path = RubyRuntime.checkInterpreterBin(command); if (path != null && path.toFile().exists()) { return path.toOSString(); } path = RubyCore.checkSystemPath(command); if (path != null && path.toFile().exists()) return path.toOSString(); return null; } }