/*******************************************************************************
* Copyright (c) 2014 Mentor Graphics and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Mentor Graphics - initial API and implementation
*******************************************************************************/
package com.codesourcery.installer.actions;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.equinox.p2.core.IProvisioningAgent;
import org.eclipse.osgi.util.NLS;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import com.codesourcery.installer.IInstallMode;
import com.codesourcery.installer.IInstallProduct;
import com.codesourcery.installer.Installer;
import com.codesourcery.internal.installer.FileUtils;
import com.codesourcery.internal.installer.InstallMessages;
import com.codesourcery.internal.installer.InstallUtils;
/**
* An action that can be used to replace, prepend, or append values to system
* environment variables.
*/
public class EnvironmentAction extends AbstractInstallAction {
/** Enivornment variable operations */
public enum EnvironmentOperation {
REPLACE, // Replace environment variable
PREPEND, // Prepend to environment variable
APPEND // Append to environment variable
}
/** Action identifier */
public static final String ID = "com.codesourcery.installer.EnvironmentAction";
/** Variables element */
private static final String ELEMENT_VARIABLES = "variables";
/** Variable element */
private static final String ELEMENT_VARIABLE = "variable";
/** Variable name attribute */
private static final String ATTRIBUTE_NAME = "name";
/** Variable value attribute */
private static final String ATTRIBUTE_VALUE = "value";
/** Variable operation attribute */
private static final String ATTRIBUTE_OPERATION = "operation";
/** Variable delimiter attribute */
private static final String ATTRIBUTE_DELIMITER = "delimiter";
/** Windows user environment registry key */
private static final String REG_USER_ENVIRONMENT = "HKEY_CURRENT_USER\\Environment";
/** Profile file names. */
private static final String PROFILE_FILENAMES[] = {
".bash_profile",
".bash_login",
".profile"
};
/** Index of PROFILE_FILENAMES to use when creating a new profile. */
private static final int PROFILE_DEFAULT_INDEX = 2;
/** Environment variables */
private ArrayList<EnvironmentVariable> environmentVariables = new ArrayList<EnvironmentVariable>();
/** Needs reset or reLogin */
private boolean requiresResetOrRelogin = false;
/** Default system environment updation message timeout */
private static final int UPDATE_ENVIRONMENT_MESSAGE_TIMEOUT = 1500;
/**
* Constructor
*/
public EnvironmentAction() {
super(ID);
}
/**
* Reads a Windows environment variable.
* Only supported on the Windows platform.
*
* @param variableName Variable name
* @return Variable value
* @throws UnsupportedOperationException if not supported
* @throws CoreException on failure to read the variable value
*/
public static String readWindowsEnvironmentVariable(String variableName) throws UnsupportedOperationException, CoreException {
return Installer.getDefault().getInstallPlatform().getWindowsRegistryValue(REG_USER_ENVIRONMENT, variableName);
}
/**
* Checks if an environment variable currently exists and/or contains
* a value.
*
* @param variableName Variable name
* @param value Value to check for or <code>null</code> to just check if
* the variable exists.
* @param separator Separator or <code>null</code>
* @return <code>true</code> if variable exists and/or contains the specified value
* @throws CoreException on failure to read the environment
*/
public static boolean checkEnvironmentVariable(String variableName, String value, String separator) throws CoreException {
boolean exists = false;
try {
String currentValue = null;
// Read current environment variable
if (Installer.isWindows()) {
try {
currentValue = readWindowsEnvironmentVariable(variableName);
} catch (UnsupportedOperationException e) {
Installer.log(e.getMessage());
}
}
else if (Installer.isLinux()) {
currentValue = System.getenv(variableName);
}
// Environment variable exists
if (currentValue != null) {
// Check if variable contains value
if (value != null) {
String[] parts;
if (separator != null) {
parts = InstallUtils.getArrayFromString(value, separator);
}
else {
parts = new String[] { value };
}
boolean allFound = true;
for (String part : parts) {
if (!currentValue.contains(part)) {
allFound = false;
break;
}
}
exists = allFound;
}
else {
exists = true;
}
}
}
catch (Exception e) {
Installer.log(e);
}
return exists;
}
/**
* Adds an environment variable to set.
* The variable will be replaced.
*
* @param name Variable name
* @param value Variable value
*/
public void addVariable(String name, String value) {
EnvironmentVariable variable = new EnvironmentVariable(name, EnvironmentOperation.REPLACE, null);
variable.addValue(value);
environmentVariables.add(variable);
}
/**
* Adds an environment variable to set.
*
* @param name Variable name
* @param values Variable values
* @param operation Operation to perform (replace, prepend, or append)
* @param delimiter Delimiter to separate variables
*/
public void addVariable(String name, String[] values, EnvironmentOperation operation, String delimiter) {
EnvironmentVariable variable = new EnvironmentVariable(name, operation, delimiter);
for (String value : values) {
variable.addValue(value);
}
environmentVariables.add(variable);
}
@Override
public void run(IProvisioningAgent agent, IInstallProduct product,
IInstallMode mode, IProgressMonitor monitor) throws CoreException {
try {
if (mode.isInstall()) {
monitor.beginTask(InstallMessages.EnvironmentAction_SettingEnvironmentVariables, 1);
monitor.setTaskName(InstallMessages.EnvironmentAction_SettingEnvironmentVariables);
}
else {
monitor.beginTask(InstallMessages.EnvironmentAction_RemovingEnvironmentVariables, 1);
monitor.setTaskName(InstallMessages.EnvironmentAction_RemovingEnvironmentVariables);
}
// Windows
if (Installer.isWindows()) {
runWindows(mode, monitor);
}
// Linux
else {
runLinux(product, mode, monitor);
}
monitor.worked(1);
}
finally {
monitor.done();
}
}
/**
* Appends values to a buffer separating with a delimiter.
*
* @param values Buffer
* @param newValue Value to append
* @param delimiter Delimiter to separate values
*/
private void appendValues(StringBuffer values, String newValue, String delimiter) {
if ((values.length() > 0) && (delimiter != null))
values.append(delimiter);
values.append(newValue);
}
/**
* Handles Windows environment variables.
*
* @param mode Install mode
* @param monitor Progress monitor
* @throws CoreException on failure
*/
private void runWindows(IInstallMode mode, IProgressMonitor monitor)
throws CoreException {
for (EnvironmentVariable environmentVariable : environmentVariables) {
String existingValue = null;
// Get existing value
if ((environmentVariable.getOperation() != EnvironmentOperation.REPLACE)) {
try {
existingValue = readWindowsEnvironmentVariable(environmentVariable.getName());
}
catch (CoreException e) {
// Ignore
}
}
StringBuffer valuesBuffer = new StringBuffer();
// Install - prefix, append, or replace exiting value
if (mode.isInstall()) {
// Prepend or replace variable value
if ((environmentVariable.getOperation() == EnvironmentOperation.PREPEND) || (environmentVariable.getOperation() == EnvironmentOperation.REPLACE)) {
appendValues(valuesBuffer, environmentVariable.getValue(), environmentVariable.getDelimiter());
}
// Add existing variable value
if (environmentVariable.getOperation() != EnvironmentOperation.REPLACE) {
if (existingValue != null) {
if (valuesBuffer.length() > 0) {
valuesBuffer.append(environmentVariable.getDelimiter());
}
valuesBuffer.append(existingValue);
}
}
// Append variable value
if (environmentVariable.getOperation() == EnvironmentOperation.APPEND) {
appendValues(valuesBuffer, environmentVariable.getValue(), environmentVariable.getDelimiter());
}
// Set new variable value
Installer.getDefault().getInstallPlatform().setWindowsRegistryValue(REG_USER_ENVIRONMENT, environmentVariable.getName(), valuesBuffer.toString());
// Update Windows system environment
String result = Installer.getDefault().getInstallPlatform().updateWindowsSystemEnvironment(UPDATE_ENVIRONMENT_MESSAGE_TIMEOUT);
// Set reboot if broadcasting environment change step failed.
if (result != null && !result.isEmpty())
setNeedsResetOrRelogin(true);
}
// Uninstall - remove variable value (prefix,append) or remove variable (replace)
else {
// Remove values
if (existingValue != null) {
String[] parts = InstallUtils.getArrayFromString(existingValue, environmentVariable.getDelimiter());
ArrayList<String> existingValues = new ArrayList<String>(Arrays.asList(parts));
String[] removeValues = InstallUtils.getArrayFromString(environmentVariable.getValue(), environmentVariable.getDelimiter());
Iterator<String> iter = existingValues.iterator();
while (iter.hasNext()) {
String value = iter.next();
for (String removeValue : removeValues) {
if (removeValue.equals(value)) {
iter.remove();
break;
}
}
}
for (String value : existingValues) {
appendValues(valuesBuffer, value, environmentVariable.getDelimiter());
}
// Set new variable value
Installer.getDefault().getInstallPlatform().setWindowsRegistryValue(REG_USER_ENVIRONMENT, environmentVariable.getName(), valuesBuffer.toString());
// Update Windows system environment
Installer.getDefault().getInstallPlatform().updateWindowsSystemEnvironment(UPDATE_ENVIRONMENT_MESSAGE_TIMEOUT);
}
// Remove variable
else {
Installer.getDefault().getInstallPlatform().deleteWindowsRegistryValue(REG_USER_ENVIRONMENT, environmentVariable.getName());
}
}
}
}
/**
* Handles Linux path environment.
*
* @param product Product
* @param mode Mode
* @param monitor Progress monitor
* @throws CoreException on failure
*/
private void runLinux(IInstallProduct product, IInstallMode mode, IProgressMonitor monitor)
throws CoreException {
// Path to .profile
String homeDir = System.getProperty("user.home");
if (homeDir == null)
Installer.fail("Failed to get user home directory.");
IPath homePath = new Path(homeDir);
// Check for profile
String profileFilename = null;
File profileFile = null;
for (String name : PROFILE_FILENAMES) {
IPath profilePath = homePath.append(name);
profileFile = profilePath.toFile();
if (profileFile.exists()) {
profileFilename = name;
break;
}
else {
Installer.log(name + " not found, skipping.");
}
}
if (profileFilename == null) {
// Create a new profile.
profileFilename = PROFILE_FILENAMES[PROFILE_DEFAULT_INDEX];
IPath newProfilePath = homePath.append(profileFilename);
try {
profileFile = newProfilePath.toFile();
profileFile.createNewFile();
} catch (IOException e) {
Installer.log("Could not create profile " + newProfilePath);
return;
}
}
// Do not modify read-only profile
if (!profileFile.canWrite()) {
Installer.log("Profile was not modified because it is read-only.");
return;
}
// File date suffix
SimpleDateFormat fileDateFormat = new SimpleDateFormat("yyyyDDDHHmmss");
String fileDateDesc = fileDateFormat.format(new Date());
// Backup file path
String backupName = profileFilename + fileDateDesc;
IPath backupPath = homePath.append(backupName);
File backupFile = backupPath.toFile();
String line;
// Install
if (mode.isInstall()) {
// Date description
SimpleDateFormat dateFormat = new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy");
String dateDesc = dateFormat.format(new Date());
// Make backup of .profile
try {
FileUtils.copyFile(profileFile.toPath(), backupFile.toPath(), true);
} catch (IOException e) {
Installer.fail(InstallMessages.Error_FailedToBackupProfile, e);
}
// Write path extensions
BufferedWriter writer = null;
try {
writer = new BufferedWriter(new FileWriter(profileFile, true));
writer.newLine();
// Write product block start
writer.append(getProfileMarker(product, true));
writer.newLine();
writer.append("# Do NOT modify these lines; they are used to uninstall.");
writer.newLine();
line = MessageFormat.format("# New environment added by {0} on {1}.", new Object[] {
product.getName(),
dateDesc
});
writer.append(line);
writer.newLine();
line = MessageFormat.format("# The unmodified version of this file is saved in {0}.", backupPath.toOSString());
writer.append(line);
writer.newLine();
monitor.setTaskName("Setting environment variables...");
for (EnvironmentVariable environmentVariable : environmentVariables) {
monitor.setTaskName(NLS.bind(InstallMessages.SettingEnvironment, environmentVariable.getName()));
StringBuilder buffer = new StringBuilder();
buffer.append(environmentVariable.getName());
buffer.append('=');
// Append variable
if (environmentVariable.getOperation() == EnvironmentOperation.APPEND) {
buffer.append("${");
buffer.append(environmentVariable.getName());
buffer.append("}");
buffer.append(environmentVariable.getDelimiter());
}
boolean path = "PATH".equals(environmentVariable.getName());
if (path)
buffer.append('\"');
buffer.append(environmentVariable.getValue());
if (path)
buffer.append('\"');
if (environmentVariable.getOperation() == EnvironmentOperation.PREPEND) {
buffer.append(environmentVariable.getDelimiter());
buffer.append("${");
buffer.append(environmentVariable.getName());
buffer.append("}");
}
writer.append(buffer.toString());
writer.newLine();
writer.append("export ");
writer.append(environmentVariable.getName());
writer.newLine();
}
// Write product block end
writer.append(getProfileMarker(product, false));
writer.newLine();
} catch (IOException e) {
Installer.fail(InstallMessages.Error_FailedToUpdateProfile, e);
}
finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
// Ignore
}
}
}
setNeedsResetOrRelogin(true);
}
// Uninstall
else {
BufferedReader reader = null;
BufferedWriter writer = null;
boolean inProductBlock = false;
try {
reader = new BufferedReader(new FileReader(profileFile));
writer = new BufferedWriter(new FileWriter(backupFile));
while ((line = reader.readLine()) != null) {
// Start of product path block
if (line.startsWith(getProfileMarker(product, true))) {
inProductBlock = true;
}
// End of product path block
else if (line.startsWith(getProfileMarker(product, false))) {
inProductBlock = false;
}
// If not in product path block, copy lines
else if (!inProductBlock) {
writer.write(line);
writer.newLine();
}
}
}
catch (IOException e) {
Installer.fail(InstallMessages.Error_FailedToUpdateProfile, e);
}
finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
// Ignore
}
}
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
// Ignore
}
}
}
// Copy new profile
try {
FileUtils.copyFile(backupFile.toPath(), profileFile.toPath(), true);
} catch (IOException e) {
Installer.fail(InstallMessages.Error_FailedToUpdateProfile, e);
}
backupFile.delete();
}
}
/**
* Returns the marker for beginning or ending of profile path block.
*
* @param product Product
* @param start <code>true</code> for start of block marker,
* <code>false</code> for end of block marker.
* @return Marker
*/
private String getProfileMarker(IInstallProduct product, boolean start) {
if (start)
return MessageFormat.format("# Product Begin: {0}", new Object[] { product.getName() });
else
return MessageFormat.format("# Product End: {0}.", new Object[] { product.getName() });
}
@Override
public void save(Document document, Element node) throws CoreException {
Element element = document.createElement(ELEMENT_VARIABLES);
node.appendChild(element);
for (EnvironmentVariable environmentVariable : environmentVariables) {
Element variableElement = document.createElement(ELEMENT_VARIABLE);
element.appendChild(variableElement);
variableElement.setAttribute(ATTRIBUTE_NAME, environmentVariable.getName());
variableElement.setAttribute(ATTRIBUTE_VALUE, environmentVariable.getValue());
variableElement.setAttribute(ATTRIBUTE_OPERATION, environmentVariable.getOperation().toString());
variableElement.setAttribute(ATTRIBUTE_DELIMITER, environmentVariable.getDelimiter());
}
}
@Override
public void load(Element element) throws CoreException {
environmentVariables.clear();
NodeList variablesNodes = element.getElementsByTagName(ELEMENT_VARIABLES);
for (int variablesIndex = 0; variablesIndex < variablesNodes.getLength(); variablesIndex++) {
Node variablesNode = variablesNodes.item(variablesIndex);
if (variablesNode.getNodeType() == Node.ELEMENT_NODE) {
Element variablesElement = (Element)variablesNode;
NodeList variableNodes = variablesElement.getElementsByTagName(ELEMENT_VARIABLE);
for (int variableIndex = 0; variableIndex < variableNodes.getLength(); variableIndex++) {
Node variableNode = variableNodes.item(variableIndex);
if (variableNode.getNodeType() == Node.ELEMENT_NODE) {
Element pathElement = (Element)variableNode;
String name = pathElement.getAttribute(ATTRIBUTE_NAME);
String value = pathElement.getAttribute(ATTRIBUTE_VALUE);
EnvironmentOperation op = EnvironmentOperation.valueOf(pathElement.getAttribute(ATTRIBUTE_OPERATION));
String delimiter = pathElement.getAttribute(ATTRIBUTE_DELIMITER);
EnvironmentVariable variable = new EnvironmentVariable(name, op, delimiter);
variable.addValue(value);
environmentVariables.add(variable);
}
}
}
}
}
/**
* Sets whether restart or re-login required for action
*
* @param value <code>true</code> if restart or re-login is
* required
*/
public void setNeedsResetOrRelogin(boolean value) {
this.requiresResetOrRelogin = value;
}
@Override
public boolean needsRestartOrRelogin() {
return this.requiresResetOrRelogin;
}
/**
* Environment variable
*/
private class EnvironmentVariable {
/** Variable name */
private String name;
/** Variable value */
private String value = "";
/** Operation to perform */
private EnvironmentOperation operation;
/** Delimiter to separate variable values */
private String delimiter;
/**
* Constructor
*
* @param name Variable name
* @param operation Operation to perform
* @param delimiter Delimiter to separate values
*/
public EnvironmentVariable(String name, EnvironmentOperation operation, String delimiter) {
this.name = name;
this.operation = operation;
this.delimiter = delimiter;
}
/**
* Returns the variable name.
*
* @return Name
*/
public String getName() {
return name;
}
/**
* Adds a value to the variable. Multiple values will be separated
* with the delimiter.
*
* @param newValue Value to add
*/
public void addValue(String newValue) {
if (!value.isEmpty() && (getDelimiter() != null))
value += getDelimiter();
value += newValue;
}
/**
* Returns the variable value.
*
* @return Value
*/
public String getValue() {
return value;
}
/**
* Returns the operation that will be performed.
*
* @return Operation
*/
public EnvironmentOperation getOperation() {
return operation;
}
/**
* Returns the delimiter to separate variable values.
*
* @return Delimiter
*/
public String getDelimiter() {
return delimiter;
}
}
}