com.aptana.portal.ui.dispatch.configurationProcessors.RubyInstallProcessor.java Source code

Java tutorial

Introduction

Here is the source code for com.aptana.portal.ui.dispatch.configurationProcessors.RubyInstallProcessor.java

Source

/**
 * Aptana Studio
 * Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved.
 * Licensed under the terms of the GNU Public License (GPL) v3 (with exceptions).
 * Please see the license.html included with this distribution for details.
 * Any modifications to this file must keep this entire header intact.
 */
package com.aptana.portal.ui.dispatch.configurationProcessors;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Properties;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.window.Window;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.progress.UIJob;

import com.aptana.configurations.processor.ConfigurationStatus;
import com.aptana.core.logging.IdeLog;
import com.aptana.ide.core.io.LockUtils;
import com.aptana.portal.ui.PortalUIPlugin;
import com.aptana.portal.ui.dispatch.configurationProcessors.installer.InstallerOptionsDialog;

/**
 * A Ruby install processor.<br>
 * This class is in charge of downloading and installing Ruby and DevKit for Windows operating systems.<br>
 * Note: In case we decide to support something similar for MacOSX and Linux, this processor would probably need
 * delegators set up.
 * 
 * @author Shalom Gibly <sgibly@aptana.com>
 */
public class RubyInstallProcessor extends InstallerConfigurationProcessor {
    private static final String RUBY = "Ruby"; //$NON-NLS-1$
    private static final String DEVKIT = "DevKit"; //$NON-NLS-1$
    protected static final String RUBY_DEFAULT_INSTALL_DIR = "C:\\Ruby"; //$NON-NLS-1$
    protected static final String DEVKIT_FSTAB_PATH = "\\devkit\\msys\\1.0.11\\etc\\fstab"; //$NON-NLS-1$
    protected static final String DEVKIT_FSTAB_LOCATION_PREFIX = "C:/Ruby/"; //$NON-NLS-1$
    // The process return code for a Ruby installer cancel.
    private static final int RUBY_INSTALLER_PROCESS_CANCEL = 5;
    private static boolean installationInProgress;

    private String installDir;

    /**
     * Install Ruby on the machine.<br>
     * The configuration will grab the installer and the DevKit from the given attributes.<br>
     * We expect an array of attributes with the same structure described at {@link #loadAttributes(Object)}.
     * 
     * @param attributes
     *            An array of strings holding an optional attributes map and an array of rubyinstaller and the devkit
     *            URLs.
     * @see com.aptana.configurations.processor.AbstractConfigurationProcessor#configure(org.eclipse.core.runtime.IProgressMonitor,
     *      java.lang.Object)
     * @see #loadAttributes(Object)
     */
    @Override
    public ConfigurationStatus configure(IProgressMonitor progressMonitor, Object attributes) {
        // Get a Class lock to avoid multiple installations at the same time even with multiple instances of this
        // RubyInstallProcessor
        synchronized (this.getClass()) {
            if (installationInProgress) {
                return configurationStatus;
            }
            installationInProgress = true;
        }
        if (!Platform.OS_WIN32.equals(Platform.getOS())) {
            String err = "The Ruby installer processor is designed to work on Windows."; //$NON-NLS-1$
            IdeLog.logError(PortalUIPlugin.getDefault(), new Exception(err));
            applyErrorAttributes(err);
            installationInProgress = false;
            return configurationStatus;
        }
        try {
            configurationStatus.removeAttribute(CONFIG_ATTR);
            clearErrorAttributes();

            // Load the installer's attributes
            IStatus loadingStatus = loadAttributes(attributes);
            if (!loadingStatus.isOK()) {
                String message = loadingStatus.getMessage();
                applyErrorAttributes(message);
                IdeLog.logError(PortalUIPlugin.getDefault(), new Exception(message));
                return configurationStatus;
            }

            // Check that we got the expected two install URLs
            // TODO - Once we place DevKit back again, this should hold a value of 2 URLs.
            if (urls.length != 1) {
                // structure error
                String err = NLS.bind(Messages.InstallProcessor_wrongNumberOfInstallLinks,
                        new Object[] { RUBY, 1, urls.length });
                applyErrorAttributes(err);
                IdeLog.logError(PortalUIPlugin.getDefault(), new Exception(err));
                return configurationStatus;
            }
            // Try to get the default install directory from the optional attributes
            installDir = attributesMap.get(INSTALL_DIR_ATTRIBUTE);
            if (installDir == null) {
                installDir = RUBY_DEFAULT_INSTALL_DIR;
            }
            // Start the installation...
            configurationStatus.setStatus(ConfigurationStatus.PROCESSING);
            IStatus status = download(urls, progressMonitor);
            if (status.isOK()) {
                status = install(progressMonitor);
            }
            switch (status.getSeverity()) {
            case IStatus.OK:
            case IStatus.INFO:
            case IStatus.WARNING:
                displayMessageInUIThread(MessageDialog.INFORMATION,
                        NLS.bind(Messages.InstallProcessor_installerTitle, RUBY),
                        NLS.bind(Messages.InstallProcessor_installationSuccessful, RUBY));
                configurationStatus.setStatus(ConfigurationStatus.OK);
                break;
            case IStatus.ERROR:
                configurationStatus.setStatus(ConfigurationStatus.ERROR);
                break;
            case IStatus.CANCEL:
                configurationStatus.setStatus(ConfigurationStatus.INCOMPLETE);
                break;
            default:
                configurationStatus.setStatus(ConfigurationStatus.UNKNOWN);
            }
            return configurationStatus;
        } finally {
            synchronized (this.getClass()) {
                installationInProgress = false;
            }
        }
    }

    /**
     * Returns the application name.
     * 
     * @return "Ruby"
     */
    protected String getApplicationName() {
        return RUBY;
    }

    /**
     * Install Ruby and DevKit.
     * 
     * @param progressMonitor
     * @return
     */
    protected IStatus install(IProgressMonitor progressMonitor) {
        if (downloadedPaths == null || downloadedPaths[0] == null) {
            String failureMessge = Messages.InstallProcessor_couldNotLocateInstaller;
            if (downloadedPaths != null && downloadedPaths[0] != null) {
                failureMessge = NLS.bind(Messages.InstallProcessor_couldNotLocatePackage, DEVKIT);
            }
            String err = NLS.bind(Messages.InstallProcessor_failedToInstall, RUBY);
            displayMessageInUIThread(MessageDialog.ERROR, Messages.InstallProcessor_installationErrorTitle,
                    err + ' ' + failureMessge);
            return new Status(IStatus.ERROR, PortalUIPlugin.PLUGIN_ID, err + ' ' + failureMessge);
        }
        SubMonitor subMonitor = SubMonitor.convert(progressMonitor, Messages.InstallProcessor_installerProgressInfo,
                IProgressMonitor.UNKNOWN);
        try {
            subMonitor.beginTask(NLS.bind(Messages.InstallProcessor_installingTaskName, RUBY),
                    IProgressMonitor.UNKNOWN);
            final String[] installDir = new String[1];
            Job installRubyDialog = new UIJob("Ruby installer options") //$NON-NLS-1$
            {
                @Override
                public IStatus runInUIThread(IProgressMonitor monitor) {
                    RubyInstallerOptionsDialog dialog = new RubyInstallerOptionsDialog();
                    if (dialog.open() == Window.OK) {
                        installDir[0] = dialog.getInstallDir();
                        return Status.OK_STATUS;
                    } else {
                        return Status.CANCEL_STATUS;
                    }
                }
            };
            installRubyDialog.schedule();
            try {
                installRubyDialog.join();
            } catch (InterruptedException e) {
            }
            IStatus result = installRubyDialog.getResult();
            if (!result.isOK()) {
                return result;
            }

            IStatus status = installRuby(installDir[0]);
            if (!status.isOK()) {
                return status;
            }
            IdeLog.logInfo(PortalUIPlugin.getDefault(), "Successfully installed Ruby into " + installDir[0]); //$NON-NLS-1$
            // Ruby was installed successfully. Now we need to extract DevKit into the Ruby dir and change its
            // configurations to match the installation location.

            // TODO - We need to fix the DevKit installation. The DevKit team changed their installation way recently...

            // status = installDevKit(installDir[0]);
            // if (!status.isOK())
            // {
            // displayMessageInUIThread(MessageDialog.ERROR, Messages.InstallProcessor_installationErrorTitle, status
            // .getMessage());
            // return status;
            // }
            finalizeInstallation(installDir[0]);
            // PortalUIPlugin.logInfo(
            //               "Successfully installed DevKit into " + installDir[0] + ". Ruby installation completed.", null); //$NON-NLS-1$ //$NON-NLS-2$
            return Status.OK_STATUS;
        } catch (Exception e) {
            IdeLog.logError(PortalUIPlugin.getDefault(), "Error while installing Ruby", e); //$NON-NLS-1$
            return new Status(IStatus.ERROR, PortalUIPlugin.PLUGIN_ID,
                    NLS.bind(Messages.InstallProcessor_errorWhileInstalling, RUBY));
        } finally {
            subMonitor.done();
        }
    }

    /**
     * Run the Ruby installer and install Ruby into the given directory.
     * 
     * @param installDir
     * @return The status of this installation
     */
    protected IStatus installRuby(final String installDir) {
        Job job = new Job(NLS.bind(Messages.InstallProcessor_installerJobName,
                RUBY + ' ' + Messages.InstallProcessor_installerGroupTitle)) {
            @Override
            protected IStatus run(IProgressMonitor monitor) {
                try {
                    SubMonitor subMonitor = SubMonitor.convert(monitor, IProgressMonitor.UNKNOWN);
                    subMonitor.beginTask(NLS.bind(Messages.InstallProcessor_installingTaskName, RUBY),
                            IProgressMonitor.UNKNOWN);
                    IdeLog.logInfo(PortalUIPlugin.getDefault(), "Installing Ruby into " + installDir); //$NON-NLS-1$

                    // Try to get a file lock first, before running the process. This file was just downloaded, so there
                    // is a chance it's still being held by the OS or by the downloader.
                    IStatus fileLockStatus = LockUtils.waitForLockRelease(downloadedPaths[0], 10000L);
                    if (!fileLockStatus.isOK()) {
                        return new Status(IStatus.ERROR, PortalUIPlugin.PLUGIN_ID,
                                NLS.bind(Messages.InstallProcessor_failedToInstallSeeLog, RUBY));
                    }

                    ProcessBuilder processBuilder = new ProcessBuilder(downloadedPaths[0], "/silent", //$NON-NLS-1$
                            "/dir=\"" + installDir + "\"", "/tasks=\"modpath\""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                    Process process = processBuilder.start();
                    int res = process.waitFor();
                    if (res == RUBY_INSTALLER_PROCESS_CANCEL) {
                        IdeLog.logInfo(PortalUIPlugin.getDefault(), "Ruby installation cancelled"); //$NON-NLS-1$
                        return Status.CANCEL_STATUS;
                    }
                    if (res != 0) {
                        // We had an error while installing
                        IdeLog.logError(PortalUIPlugin.getDefault(),
                                "Failed to install Ruby. The ruby installer process returned a termination code of " //$NON-NLS-1$
                                        + res);
                        return new Status(IStatus.ERROR, PortalUIPlugin.PLUGIN_ID, res,
                                NLS.bind(Messages.InstallProcessor_installationErrorMessage, RUBY, RUBY), null);
                    } else if (!new File(installDir).exists()) {
                        // Just to be sure that we got everything in place
                        IdeLog.logError(PortalUIPlugin.getDefault(),
                                "Failed to install Ruby. The " + installDir + " directory was not created"); //$NON-NLS-1$ //$NON-NLS-2$
                        return new Status(IStatus.ERROR, PortalUIPlugin.PLUGIN_ID, res,
                                NLS.bind(Messages.InstallProcessor_installationError_installDirMissing, RUBY),
                                null);
                    }
                    return Status.OK_STATUS;
                } catch (Exception e) {
                    IdeLog.logError(PortalUIPlugin.getDefault(), e);
                    return new Status(IStatus.ERROR, PortalUIPlugin.PLUGIN_ID,
                            NLS.bind(Messages.InstallProcessor_failedToInstallSeeLog, RUBY), e);
                } finally {
                    monitor.done();
                }
            }
        };
        // Give it a little delay, just in case the downloader still holds on to the rubyinstaller file.
        job.schedule(1000);
        try {
            job.join();
        } catch (InterruptedException e) {
            IdeLog.logError(PortalUIPlugin.getDefault(), e.getMessage(), e);
            return Status.CANCEL_STATUS;
        }
        return job.getResult();
    }

    /**
     * Extract the downloaded DevKit into the install dir and configure it to work.<br>
     * At this stage, we assume that the install dir and the DevKit package have been verified and valid!
     * 
     * @param dir
     * @return The result status of the installation
     * @throws Exception
     */
    protected IStatus installDevKit(String dir) {
        final String installDir = dir + File.separatorChar + "DevKit"; //$NON-NLS-1$
        Job job = new Job(NLS.bind(Messages.InstallProcessor_installingTaskName, DEVKIT)) {
            @Override
            protected IStatus run(IProgressMonitor monitor) {
                try {
                    SubMonitor subMonitor = SubMonitor.convert(monitor, 1000);
                    subMonitor.beginTask(
                            NLS.bind(Messages.InstallProcessor_extractingPackageTaskName, DEVKIT, installDir), 900);
                    // We get a folder status first, before unzipping into the folder. This folder was just created,
                    // so there is a chance it's still being held by the OS or by the Ruby installer.
                    IStatus folderStatus = LockUtils.waitForFolderAccess(installDir, 10000);
                    if (!folderStatus.isOK()) {
                        PortalUIPlugin.getDefault().getLog().log(folderStatus);
                        return new Status(IStatus.ERROR, PortalUIPlugin.PLUGIN_ID,
                                NLS.bind(Messages.InstallProcessor_failedToInstallSeeLog, DEVKIT));
                    }
                    // DevKit arrives as a 7zip package, so we use a specific Windows decoder to extract it.
                    // This extraction process follows the instructions at:
                    // http://wiki.github.com/oneclick/rubyinstaller/development-kit
                    extractWin(downloadedPaths[1], installDir);
                    subMonitor.worked(900);

                    subMonitor.beginTask(NLS.bind(Messages.InstallProcessor_updatingTaskName, DEVKIT), 100);
                    // Update the /devkit/msys/1.0.11/etc/fstab with the Ruby installation path
                    File fstab = new File(installDir, DEVKIT_FSTAB_PATH);
                    StringBuilder builder = new StringBuilder();
                    // read the content of the original file and update the Ruby location to the selected one. Then,
                    // save the new content and override the file.
                    String pathReplacement = installDir.replaceAll("\\\\", "/"); //$NON-NLS-1$ //$NON-NLS-2$
                    if (!pathReplacement.endsWith("/")) //$NON-NLS-1$
                    {
                        pathReplacement = pathReplacement + '/';
                    }
                    String newLine = System.getProperty("line.separator", "\n"); //$NON-NLS-1$ //$NON-NLS-2$
                    BufferedReader reader = new BufferedReader(new FileReader(fstab));
                    String line = null;
                    while ((line = reader.readLine()) != null) {
                        line = line.replaceAll(DEVKIT_FSTAB_LOCATION_PREFIX, pathReplacement);
                        builder.append(line);
                        builder.append(newLine);
                    }
                    reader.close();
                    // Now save the modified content into the same file
                    FileWriter writer = new FileWriter(fstab);
                    writer.write(builder.toString());
                    writer.flush();
                    writer.close();
                    subMonitor.worked(100);
                } catch (Throwable t) {
                    IdeLog.logError(PortalUIPlugin.getDefault(), "Failed to install DevKit", t); //$NON-NLS-1$
                    return new Status(IStatus.ERROR, PortalUIPlugin.PLUGIN_ID,
                            NLS.bind(Messages.InstallProcessor_failedToInstallSeeLog, DEVKIT), t);
                } finally {
                    monitor.done();
                }
                return Status.OK_STATUS;
            }
        };
        job.schedule(500);
        try {
            job.join();
        } catch (InterruptedException e) {
            return Status.CANCEL_STATUS;
        }
        return job.getResult();
    }

    /**
     * Finalize the installation by placing a .aptana file in the installed directory, specifying some properties.
     * 
     * @param installDir
     */
    protected void finalizeInstallation(String installDir) {
        super.finalizeInstallation(installDir);
        File propertiesFile = new File(installDir, APTANA_PROPERTIES_FILE_NAME);
        Properties properties = new Properties();
        properties.put("ruby_install", urls[0]); //$NON-NLS-1$
        // TODO - Uncomment this once we have DevKit back
        // properties.put("devkit_install", urls[1]); //$NON-NLS-1$
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(propertiesFile);
            properties.store(fileOutputStream,
                    NLS.bind(Messages.InstallProcessor_aptanaInstallationComment, "Ruby & DevKit")); //$NON-NLS-1$
        } catch (IOException e) {
            IdeLog.logError(PortalUIPlugin.getDefault(), e);
        } finally {
            if (fileOutputStream != null) {
                try {
                    fileOutputStream.flush();
                    fileOutputStream.close();
                } catch (IOException e) {
                }
            }
        }
    }

    /**
     * Ruby installer dialog.<br>
     * This dialog only asks for the install directory.
     * 
     * @author Shalom Gibly <sgibly@aptana.com>
     */
    private class RubyInstallerOptionsDialog extends InstallerOptionsDialog {
        public RubyInstallerOptionsDialog() {
            super(Display.getDefault().getActiveShell(), RUBY);
            setTitleImage(PortalUIPlugin.getDefault().getImageRegistry().get(PortalUIPlugin.RUBY_IMAGE));
        }

        /**
         * Returns the installation dir selected in the text field.
         * 
         * @return the installation directory
         */
        public String getInstallDir() {
            return attributes.get(INSTALL_DIR_ATTR).toString();
        }

        @Override
        protected void setAttributes() {
            attributes.put(INSTALL_DIR_ATTR, installDir);
        }
    }
}