/*
* 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]"
*
* Contributor(s):
*
* 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.modules.ruby.platform.gems;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dialog;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JButton;
import org.netbeans.api.ruby.platform.RubyPlatform;
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.platform.execution.RubyExecutionDescriptor;
import org.netbeans.modules.ruby.platform.execution.Sudo;
import org.openide.DialogDescriptor;
import org.openide.DialogDisplayer;
import org.openide.util.HelpCtx;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
final class GemRunner {
private static final Logger LOGGER = Logger.getLogger(GemRunner.class.getName());
private final RubyPlatform platform;
private List<String> output;
private File pwd;
GemRunner(final RubyPlatform platform) {
this.platform = platform;
assert platform.hasRubyGemsInstalled() : "GemRunner cannot be used fro platform without RubyGems: " + platform;
}
/**
* Compound options based on passed <em>custom</em> ones, defaults, and
* those stored in preferences.
*/
private String[] getOptions(final String... custom) {
List<String> options = new ArrayList<String>();
options.addAll(Arrays.asList(custom));
if (RubyPreferences.shallFetchGemDescriptions()) {
options.add("--details"); // NOI18N
}
if (!platform.getGemManager().hasAncientRubyGemsVersion() &&
RubyPreferences.shallFetchAllVersions()) {
options.add("--all"); // NOI18N
}
return options.toArray(new String[options.size()]);
}
boolean fetchBoth() {
return runGemTool("list", false, getOptions("--both")); // NOI18N
}
boolean fetchRemote() {
return runGemTool("list", false, getOptions("--remote")); // NOI18N
}
boolean fetchLocal() {
return runGemTool("list", false, getOptions("--local")); // NOI18N
}
boolean install(final List<String> gemNames, boolean rdoc, boolean ri, boolean includeDeps,
String version) {
return installRemote(gemNames, rdoc, ri, includeDeps, version, null, null);
}
boolean installLocal(File gem, boolean rdoc, boolean ri) {
return installLocal(gem, rdoc, ri, null, null);
}
boolean installAsynchronously(List<String> gemNames, boolean rdoc, boolean ri,
boolean includeDeps, String version, Runnable asyncCompletionTask, Component parent) {
return installRemote(gemNames, rdoc, ri, includeDeps, version, asyncCompletionTask, parent);
}
boolean installLocalAsynchronously(File gem, boolean rdoc, boolean ri,
Runnable asyncCompletionTask, Component parent) {
return installLocal(gem, rdoc, ri, asyncCompletionTask, parent);
}
boolean update(final List<String> gemNames, boolean rdoc, boolean ri, boolean includeDependencies) {
return update(gemNames, rdoc, ri, includeDependencies, null, null);
}
boolean updateAsynchronously(List<String> gemNames, boolean rdoc, boolean ri, boolean includeDependencies,
Runnable asyncCompletionTask, Component parent) {
return update(gemNames, rdoc, ri, includeDependencies, asyncCompletionTask, parent);
}
boolean uninstall(final List<GemInstallInfo> gems) {
return uninstall(gems, null, null);
}
boolean uninstallAsynchronously(List<GemInstallInfo> gems, Runnable asyncCompletionTask, Component parent) {
return uninstall(gems, asyncCompletionTask, parent);
}
private boolean install(final List<String> gems, boolean rdoc, boolean ri, boolean includeDeps,
String version, Runnable asyncCompletionTask, Component parent) {
List<String> argList = new ArrayList<String>();
for (String gem : gems) {
argList.add(gem);
}
if (!rdoc) {
argList.add("--no-rdoc"); // NOI18N
}
if (!ri) {
argList.add("--no-ri"); // NOI18N
}
if (includeDeps) {
includeDeps(argList);
} else {
argList.add("--ignore-dependencies"); // NOI18N
}
if ((version != null) && (version.length() > 0)) {
argList.add("--version"); // NOI18N
argList.add(version);
}
String[] args = argList.toArray(new String[argList.size()]);
String gemCmd = "install"; // NOI18N
if (asyncCompletionTask != null) {
String title = NbBundle.getMessage(GemRunner.class, "Installation");
String success = NbBundle.getMessage(GemRunner.class, "InstallationOk");
String failure = NbBundle.getMessage(GemRunner.class, "InstallationFailed");
asynchGemRunner(parent, title, success, failure, asyncCompletionTask, gemCmd, args);
return false;
} else {
return runGemTool(gemCmd, args);
}
}
private boolean installRemote(final List<String> gemNames, boolean rdoc, boolean ri, boolean includeDeps,
String version, Runnable asyncCompletionTask, Component parent) {
return install(gemNames, rdoc, ri, includeDeps, version, asyncCompletionTask, parent);
}
private boolean installLocal(final File gem, boolean rdoc,
boolean ri, Runnable asyncCompletionTask, Component parent) {
// XXX make 'includeDeps' customizable
this.pwd = gem.getParentFile();
return install(Collections.singletonList(gem.getName()), rdoc, ri, false, null, asyncCompletionTask, parent);
}
private boolean update(final List<String> gemNames, boolean rdoc, boolean ri,
boolean includeDependencies, Runnable asyncCompletionTask, Component parent) {
List<String> argList = new ArrayList<String>();
if (gemNames != null) {
for (String gemName : gemNames) {
argList.add(gemName);
}
}
if (RubyPreferences.isGemVerboseOutput()) {
argList.add("--verbose"); // NOI18N
}
if (!rdoc) {
argList.add("--no-rdoc"); // NOI18N
}
if (!ri) {
argList.add("--no-ri"); // NOI18N
}
if (includeDependencies) {
argList.add("--include-dependencies"); //NOI18N
}
includeDeps(argList);
String[] args = argList.toArray(new String[argList.size()]);
String gemCmd = "update"; // NOI18N
if (asyncCompletionTask != null) {
String title = NbBundle.getMessage(GemRunner.class, "Update");
String success = NbBundle.getMessage(GemRunner.class, "UpdateOk");
String failure = NbBundle.getMessage(GemRunner.class, "UpdateFailed");
asynchGemRunner(parent, title, success, failure, asyncCompletionTask, gemCmd, args);
return false;
} else {
return runGemTool(gemCmd, args);
}
}
private static String[] uninstallArgsFor(GemInstallInfo info) {
List<String> argList = new ArrayList<String>();
argList.add(info.getName());
if (info.getVersion() == null) {
argList.add("--all"); // NOI18N
} else {
argList.add("-v " + info.getVersion()); // NOI18N
}
argList.add("--executables"); // NOI18N
if (info.isIgnoreDependencies()) {
argList.add("--ignore-dependencies"); // NOI18N
}
return argList.toArray(new String[argList.size()]);
}
private boolean uninstall(final List<GemInstallInfo> gems, Runnable asyncCompletionTask, Component parent) {
final String gemCmd = "uninstall"; // NOI18N
if (asyncCompletionTask != null) {
String title = NbBundle.getMessage(GemRunner.class, "Uninstallation");
String success = NbBundle.getMessage(GemRunner.class, "UninstallationOk");
String failure = NbBundle.getMessage(GemRunner.class, "UninstallationFailed");
for (GemInstallInfo gem : gems) {
asynchGemRunner(parent, title, success, failure, asyncCompletionTask, gemCmd, uninstallArgsFor(gem));
}
return false;
} else {
boolean ok = true;
for (GemInstallInfo gem : gems) {
ok = runGemTool(gemCmd, uninstallArgsFor(gem));
}
return ok;
}
}
List<String> getOutput() {
return output;
}
private void includeDeps(List<String> argList) {
// -y and --include-dependencies is deprecated since 0.9.5 (and automatic)
if (Util.compareVersions(platform.getInfo().getGemVersion(), "0.9.5") < 0) { // NOI18N
argList.add("--include-dependencies"); // NOI18N
}
}
private boolean runGemTool(String gemCommand, boolean needsWrite, String... commandArgs) {
return runGemTool(gemCommand, needsWrite, null, null, commandArgs);
}
private boolean runGemTool(String gemCommand, String... commandArgs) {
return runGemTool(gemCommand, true, commandArgs);
}
/**
* Runs <em>gem</em> tools.
*
* @param gemCommand like, <em>list</em>, <em>update</em>, <em>install</em>, ...
* @param needsWrite whether the gem tool will need a write access to the
* reprository (migth trigger sudo)
* @param progressPanel {@link GemProgressPanel} isntance
* @param processHolder will put a {@link Process} instance into the first
* element
* @param commandArgs argument to the <tt>gemCommand</tt>
* @return whether underlaying <em>gem</em> tool succeeded
*/
private boolean runGemTool(String gemCommand, boolean needsWrite,
GemProgressPanel progressPanel, Process[] processHolder,
String... commandArgs) {
// Install the given gem
List<String> argList = new ArrayList<String>();
File cmd = new File(platform.getInterpreter());
if (!cmd.getName().startsWith("jruby") || ExecutionUtils.launchJRubyScript()) { // NOI18N
argList.add(cmd.getPath());
}
argList.addAll(ExecutionUtils.getRubyArgs(platform));
GemManager gemManager = platform.getGemManager();
assert gemManager != null : "gemManager cannot be null";
// be sure gem tool finds RubyGems libraries. Seems that sudo tools have
// problem with inheriting the process environment (RUBYLIB)
// I've not found any switches to force the RUBYLIB to be inherited.
File gemToolDir = new File(platform.getGemTool()).getParentFile().getParentFile();
// always use forward slashes in the load path, even on Win
argList.add("-I" + gemToolDir + "/" + "lib"); // NOI18N
argList.add(platform.getGemTool());
argList.add(gemCommand);
for (String arg : commandArgs) {
argList.add(arg);
}
if (needsWrite && !gemManager.isGemHomeWritable()) {
String message = NbBundle.getMessage(GemRunner.class, "GemRunner.message.for.sudo");
Sudo sudo = new Sudo(argList, message);
argList = sudo.createCommand();
}
String[] args = argList.toArray(new String[argList.size()]);
ProcessBuilder pb = new ProcessBuilder(args);
GemManager.adjustEnvironment(platform, pb.environment());
pb.directory(pwd == null ? cmd.getParentFile() : pwd);
pb.redirectErrorStream(true);
// TODO: Following unfortunately does not work -- gems blows up. Looks
// like a RubyGems bug.
// ERROR: While executing gem ... (NoMethodError)
// undefined method `[]=' for #<Gem::ConfigFile:0xb6c763 @hash={} ,@args=["--remote", "-p", "http://foo.bar:8080"] ,@config_file_name=nil ,@verbose=true>
//argList.add("--http-proxy"); // NOI18N
//argList.add(proxy);
// (If you uncomment the above, move it up above the args = argList.toArray line)
Util.adjustProxy(pb);
// PATH additions for JRuby etc.
RubyExecutionDescriptor descriptor = new RubyExecutionDescriptor(platform, "gem", pb.directory()).cmd(cmd);
ExecutionUtils.setupProcessEnvironment(pb.environment(), descriptor.getCmd().getParent(), descriptor.getAppendJdkToPath());
if (output == null) {
output = new ArrayList<String>(40);
}
int exitCode = -1;
try {
ExecutionUtils.logProcess(pb);
pb.redirectErrorStream(true);
Process process = pb.start();
if (processHolder != null) {
processHolder[0] = process;
}
InputStream is = process.getInputStream();
if (progressPanel != null) {
progressPanel.setProcessInput(process.getOutputStream());
}
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String line;
try {
while (true) {
line = br.readLine();
if (line == null) {
break;
}
if (progressPanel != null) {
// Add "\n" ?
progressPanel.appendOutput(line);
}
output.add(line);
}
} catch (IOException ioe) {
// When we cancel we call Process.destroy which may quite possibly
// raise an IO Exception in this thread reading text out of the
// process. Silently ignore that.
String message = "*** Gem Process Killed ***\n"; // NOI18N
output.add(message);
if (progressPanel != null) {
progressPanel.appendOutput(message);
}
}
exitCode = process.waitFor();
if (exitCode != 0) {
try {
// This might not be necessary now that I'm
// calling ProcessBuilder.redirectErrorStream(true)
// but better safe than sorry
is = process.getErrorStream();
isr = new InputStreamReader(is);
br = new BufferedReader(isr);
while ((line = br.readLine()) != null) {
if (progressPanel != null) {
// Add "\n" ?
progressPanel.appendOutput(line);
}
output.add(line);
}
} catch (IOException ioe) {
// When we cancel we call Process.destroy which may quite possibly
// raise an IO Exception in this thread reading text out of the
// process. Silently ignore that.
String message = "*** Gem Process Killed ***\n"; // NOI18N
output.add(message);
if (progressPanel != null) {
progressPanel.appendOutput(message);
}
}
}
} catch (IOException e) {
LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
} catch (InterruptedException e) {
LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
}
LOGGER.finer("Process finished with exit code: " + exitCode);
boolean succeeded = exitCode == 0;
return succeeded;
}
/** Non-blocking gem executor which also provides progress UI etc. */
private void asynchGemRunner(final Component parent, final String description,
final String successMessage, final String failureMessage,
final Runnable successCompletionTask, final String gemCommand,
final String... commandArgs) {
if (!EventQueue.isDispatchThread()) {
throw new AssertionError("#asynchGemRunner must be called from EDT");
}
final Cursor originalCursor;
if (parent != null) {
originalCursor = parent.getCursor();
Cursor busy = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR);
parent.setCursor(busy);
} else {
originalCursor = null;
}
final JButton closeButton = new JButton(NbBundle.getMessage(GemRunner.class, "CTL_Close"));
final JButton cancelButton =
new JButton(NbBundle.getMessage(GemRunner.class, "CTL_Cancel"));
closeButton.getAccessibleContext()
.setAccessibleDescription(NbBundle.getMessage(GemRunner.class, "AD_Close"));
Object[] options = new Object[] { closeButton, cancelButton };
closeButton.setEnabled(false);
final GemProgressPanel progress =
new GemProgressPanel(NbBundle.getMessage(GemRunner.class, "GemPleaseWait"));
progress.getAccessibleContext().setAccessibleDescription(
NbBundle.getMessage(GemRunner.class, "GemProgressPanel.AccessibleContext.accessibleDescription"));
DialogDescriptor descriptor =
new DialogDescriptor(progress, description, true, options, closeButton,
DialogDescriptor.DEFAULT_ALIGN, new HelpCtx(GemRunner.class), null); // NOI18N
descriptor.setModal(true);
final Process[] processHolder = new Process[1];
final Dialog dlg = DialogDisplayer.getDefault().createDialog(descriptor);
closeButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent ev) {
dlg.setVisible(false);
dlg.dispose();
resetCursor(parent, originalCursor);
}
});
Runnable runner = new Runnable() {
public void run() {
try {
final boolean succeeded =
runGemTool(gemCommand, true, progress, processHolder, commandArgs);
EventQueue.invokeLater(new Runnable() {
public void run() {
closeButton.setEnabled(true);
cancelButton.setEnabled(false);
progress.done(succeeded ? successMessage : failureMessage);
}
});
if (succeeded && (successCompletionTask != null)) {
successCompletionTask.run();
}
} finally {
resetCursor(parent, originalCursor);
}
}
};
RequestProcessor.getDefault().post(runner, 50);
dlg.setVisible(true);
if ((descriptor.getValue() == DialogDescriptor.CANCEL_OPTION) ||
(descriptor.getValue() == cancelButton)) {
resetCursor(parent, originalCursor);
cancelButton.setEnabled(false);
Process process = processHolder[0];
if (process != null) {
process.destroy();
dlg.setVisible(false);
dlg.dispose();
}
}
}
private static void resetCursor(final Component parent, final Cursor originalCursor) {
if (parent != null) {
if (!EventQueue.isDispatchThread()) {
EventQueue.invokeLater(new Runnable() {
public void run() {
resetCursor(parent, originalCursor);
}
});
return;
}
parent.setCursor(originalCursor);
}
}
}