package com.haskforce.utils;
import scala.util.Either;
import scala.util.Left;
import scala.util.Right;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.execution.process.CapturingProcessHandler;
import com.intellij.execution.process.ProcessOutput;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.List;
/**
* Helper class to perform execution related tasks, including locating programs.
*/
public class ExecUtil {
// Messages go to the log available in Help -> Show log in finder.
private final static Logger LOG = Logger.getInstance(ExecUtil.class);
/**
* Tries to get the absolute path for a command in the PATH.
*/
@Nullable
public static String locateExecutable(@NotNull final String exePath) {
GeneralCommandLine cmdLine = new GeneralCommandLine(
SystemInfo.isWindows ? "where" : "which"
);
cmdLine.addParameter(exePath);
final ProcessOutput processOutput;
try {
processOutput = new CapturingProcessHandler(cmdLine).runProcess();
} catch (ExecutionException e) {
throw new RuntimeException(
"Failed to execute command: " + cmdLine.getCommandLineString(),
e
);
}
final String stdout = processOutput.getStdout();
final String[] lines = stdout.trim().split("\n");
if (lines.length == 0) return null;
return lines[0].trim();
}
/**
* Tries to get the absolute path for a command by defaulting to first
* trying to locate the command in path, and falling back to trying likely
* directories.
*/
@Nullable
public static String locateExecutableByGuessing(@NotNull final String command) {
String located = locateExecutable(command);
if (located != null && !located.isEmpty()) {
// Found it!
return located;
}
char sep = File.separatorChar;
String homeDir = System.getProperty("user.home");
List<String> paths = ContainerUtil.newArrayList();
// Executables installed by stack.
paths.add(homeDir + sep + ".local" + sep + "bin");
//noinspection StatementWithEmptyBody
if (SystemInfo.isWindows) {
// TODO: Add windows paths.
} else {
// Unix bin dirs.
paths.add(homeDir + sep + "Library" + sep + "Haskell" + sep + "bin");
paths.add(homeDir + sep + ".cabal" + sep + "bin");
paths.add(sep + "usr" + sep + "bin");
paths.add(sep + "usr" + sep + "local" + sep + "bin");
paths.add(homeDir + sep + "bin");
}
for (String path : paths) {
String cmd = path + sep + command;
//noinspection ObjectAllocationInLoop
if (new File(cmd).canExecute()) return cmd;
}
return null;
}
@NotNull
public static String guessWorkDir(@NotNull Project project, @NotNull VirtualFile file) {
final Module module = ModuleUtilCore.findModuleForFile(file, project);
if (module != null) return guessWorkDir(module);
return getProjectPath(project);
}
@NotNull
public static String guessWorkDir(@NotNull PsiFile file) {
return guessWorkDir(file.getProject(), file.getVirtualFile());
}
@NotNull
public static String guessWorkDir(@NotNull Module module) {
final VirtualFile moduleFile = module.getModuleFile();
final VirtualFile moduleDir = moduleFile == null ? null : moduleFile.getParent();
if (moduleDir != null) return moduleDir.getPath();
return getProjectPath(module.getProject());
}
@NotNull
private static String getProjectPath(@NotNull Project project) {
final String projectPath = project.getBasePath();
if (projectPath == null) {
// This shouldn't actually happen since the projectPath will only be null for
// the default project, not a project which has a corresponding module.
throw new RuntimeException(
"Unable to guess work directory - project '" + project + "' does not have a " +
"base directory"
);
}
return projectPath;
}
/**
* Executes commandLine, optionally piping input to stdin, and return stdout.
*/
@NotNull
public static Either<ExecError, String> readCommandLine(@NotNull GeneralCommandLine commandLine,
@Nullable String input) {
Process process;
CapturingProcessHandler processHandler;
ProcessOutput processOutput;
try {
processHandler = new CapturingProcessHandler(commandLine);
process = processHandler.getProcess();
} catch (ExecutionException e) {
return new ExecError(
"Failed to create process for command: " + commandLine.getCommandLineString(),
e
).toLeft();
}
if (input != null) {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
try {
writer.write(input);
writer.flush();
writer.close();
} catch (IOException e) {
return new ExecError(
"IO error when writing to command process: " + commandLine.getCommandLineString(),
e
).toLeft();
}
}
processOutput = processHandler.runProcess();
if (processOutput.getExitCode() != 0) {
return new ExecError(
"Nonzero exit status (" + processOutput.getExitCode() + ") " +
"from command: " + commandLine.getCommandLineString() + "\n" +
"Process stderr: " + processOutput.getStderr(),
null
).toLeft();
}
return EitherUtil.right(processOutput.getStdout());
}
@NotNull
public static Either<ExecError, String> readCommandLine(@NotNull GeneralCommandLine commandLine) {
return readCommandLine(commandLine, null);
}
@NotNull
public static Either<ExecError, String> readCommandLine(@Nullable String workingDirectory, @NotNull String command, @NotNull String[] params, @Nullable String input) {
GeneralCommandLine commandLine = new GeneralCommandLine(command);
if (workingDirectory != null) {
commandLine.setWorkDirectory(workingDirectory);
}
commandLine.addParameters(params);
return readCommandLine(commandLine, input);
}
@NotNull
public static Either<ExecError, String> readCommandLine(@Nullable String workingDirectory, @NotNull String command, @NotNull String... params) {
return readCommandLine(workingDirectory, command, params, null);
}
public static class ExecError {
private final @NotNull String message;
private final @Nullable Throwable cause;
public ExecError(@NotNull String message, @Nullable Exception cause) {
this.cause = cause;
this.message = message;
// Use .warn() instead of .error() since the latter causes unit test failures.
LOG.warn(message, cause);
}
public String getMessage() {
final StringBuilder result = new StringBuilder(message);
if (cause != null) result.append("\nCaused by: ").append(cause);
return result.toString();
}
@Nullable
public Throwable getCause() {
return cause;
}
public <A> Left<ExecError, A> toLeft() {
return new Left<ExecError, A>(this);
}
}
}