/*
* Copyright 2016 Luke Imhoff
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.elixir_lang.jps.builder;
import com.intellij.execution.CommandLineUtil;
import com.intellij.ide.IdeBundle;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.UserDataHolder;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.EnvironmentUtil;
import com.intellij.util.PlatformUtils;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.text.CaseInsensitiveStringHashingStrategy;
import gnu.trove.THashMap;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.*;
/**
* OS-independent way of executing external processes with complex parameters.
* <p/>
* Main idea of the class is to accept parameters "as-is", just as they should look to an external process, and quote/escape them
* as required by the underlying platform.
*
* @see com.intellij.execution.process.OSProcessHandler
*/
public class GeneralCommandLine implements UserDataHolder {
private static final Logger LOG = Logger.getInstance("#org.elixir_lang.jps.builders.GeneralCommandLine");
private String myExePath = null;
private File myWorkDirectory = null;
private final Map<String, String> myEnvParams = new MyTHashMap();
private final ParametersList myProgramParams = new ParametersList();
private Map<Object, Object> myUserData = null;
public GeneralCommandLine() { }
@SuppressWarnings("unused")
public String getExePath() {
return myExePath;
}
public void setExePath(@NotNull @NonNls final String exePath) {
myExePath = exePath.trim();
}
public File getWorkDirectory() {
return myWorkDirectory;
}
@NotNull
public GeneralCommandLine withWorkDirectory(@Nullable final String path) {
return withWorkDirectory(path != null ? new File(path) : null);
}
@NotNull
public GeneralCommandLine withWorkDirectory(@Nullable final File workDirectory) {
myWorkDirectory = workDirectory;
return this;
}
/**
* Note: the map returned is forgiving to passing null values into putAll().
*/
@NotNull
public Map<String, String> getEnvironment() {
return myEnvParams;
}
/**
* @deprecated use {@link #getEnvironment()} (to remove in IDEA 14)
*/
@SuppressWarnings("unused")
public Map<String, String> getEnvParams() {
return getEnvironment();
}
/**
* @deprecated use {@link #getEnvironment()} (to remove in IDEA 14)
*/
@SuppressWarnings("unused")
public void setEnvParams(@Nullable Map<String, String> envParams) {
myEnvParams.clear();
if (envParams != null) {
myEnvParams.putAll(envParams);
}
}
/**
* @return unmodifiable map of the parent environment, that will be passed to the process if isPassParentEnvironment() == true
*/
@NotNull
public Map<String, String> getParentEnvironment() {
return PlatformUtils.isAppCode() ? System.getenv() // Temporarily fix for OC-8606
: EnvironmentUtil.getEnvironmentMap();
}
public void addParameters(final String... parameters) {
for (String parameter : parameters) {
addParameter(parameter);
}
}
public void addParameters(@NotNull final List<String> parameters) {
for (final String parameter : parameters) {
addParameter(parameter);
}
}
public void addParameter(@NotNull @NonNls final String parameter) {
myProgramParams.add(parameter);
}
@SuppressWarnings("unused")
public ParametersList getParametersList() {
return myProgramParams;
}
/**
* Returns string representation of this command line.<br/>
* Warning: resulting string is not OS-dependent - <b>do not</b> use it for executing this command line.
*
* @return single-string representation of this command line.
*/
public String getCommandLineString() {
return getCommandLineString(null);
}
/**
* Returns string representation of this command line.<br/>
* Warning: resulting string is not OS-dependent - <b>do not</b> use it for executing this command line.
*
* @param exeName use this executable name instead of given by {@link #setExePath(String)}
* @return single-string representation of this command line.
*/
public String getCommandLineString(@Nullable final String exeName) {
return ParametersList.join(getCommandLineList(exeName));
}
public List<String> getCommandLineList(@Nullable final String exeName) {
final List<String> commands = new ArrayList<String>();
if (exeName != null) {
commands.add(exeName);
}
else if (myExePath != null) {
commands.add(myExePath);
}
else {
commands.add("<null>");
}
commands.addAll(myProgramParams.getList());
return commands;
}
@SuppressWarnings("UnresolvedPropertyKey")
@NotNull
public Process createProcess() throws ExecutionException {
if (LOG.isDebugEnabled()) {
LOG.debug("Executing [" + getCommandLineString() + "]");
}
List<String> commands;
try {
checkWorkingDirectory();
if (StringUtil.isEmptyOrSpaces(myExePath)) {
throw new ExecutionException(IdeBundle.message("run.configuration.error.executable.not.specified"));
}
commands = CommandLineUtil.toCommandLine(myExePath, myProgramParams.getList());
}
catch (ExecutionException e) {
LOG.info(e);
throw e;
}
try {
return startProcess(commands);
}
catch (IOException e) {
LOG.info(e);
throw new ProcessNotCreatedException(e.getMessage(), e, this);
}
}
@NotNull
protected Process startProcess(@NotNull List<String> commands) throws IOException {
ProcessBuilder builder = new ProcessBuilder(commands);
setupEnvironment(builder.environment());
builder.directory(myWorkDirectory);
builder.redirectErrorStream(false);
return builder.start();
}
@SuppressWarnings("UnresolvedPropertyKey")
private void checkWorkingDirectory() throws ExecutionException {
if (myWorkDirectory == null) {
return;
}
if (!myWorkDirectory.exists()) {
throw new ExecutionException(
IdeBundle.message("run.configuration.error.working.directory.does.not.exist", myWorkDirectory.getAbsolutePath()));
}
if (!myWorkDirectory.isDirectory()) {
throw new ExecutionException(IdeBundle.message("run.configuration.error.working.directory.not.directory"));
}
}
protected void setupEnvironment(@NotNull Map<String, String> environment) {
environment.clear();
environment.putAll(getParentEnvironment());
if (!myEnvParams.isEmpty()) {
if (SystemInfo.isWindows) {
THashMap<String, String> envVars = new THashMap<String, String>(CaseInsensitiveStringHashingStrategy.INSTANCE);
envVars.putAll(environment);
envVars.putAll(myEnvParams);
environment.clear();
environment.putAll(envVars);
}
else {
environment.putAll(myEnvParams);
}
}
}
@Override
public String toString() {
return myExePath + " " + myProgramParams;
}
@Override
public <T> T getUserData(@NotNull final Key<T> key) {
if (myUserData != null) {
@SuppressWarnings({"UnnecessaryLocalVariable", "unchecked"}) final T t = (T)myUserData.get(key);
return t;
}
return null;
}
@Override
public <T> void putUserData(@NotNull final Key<T> key, @Nullable final T value) {
if (myUserData == null) {
myUserData = ContainerUtil.newHashMap();
}
myUserData.put(key, value);
}
private static class MyTHashMap extends THashMap<String, String> {
@Override
public String put(String key, String value) {
if (key == null || value == null) {
LOG.error(new Exception("Nulls are not allowed"));
return null;
}
if (key.isEmpty()) {
// Windows: passing an environment variable with empty name causes "CreateProcess error=87, The parameter is incorrect"
LOG.warn("Skipping environment variable with empty name, value: " + value);
return null;
}
return super.put(key, value);
}
@Override
public void putAll(Map<? extends String, ? extends String> map) {
if (map != null) {
super.putAll(map);
}
}
}
}