/* * Copyright 2013-2016 Sergey Ignatov, Alexander Zolotov, Florin Patan * * 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 com.goide.util; import com.goide.GoConstants; import com.goide.project.GoModuleSettings; import com.goide.runconfig.GoConsoleFilter; import com.goide.runconfig.GoRunUtil; import com.goide.sdk.GoSdkService; import com.goide.sdk.GoSdkUtil; import com.intellij.execution.ExecutionException; import com.intellij.execution.ExecutionHelper; import com.intellij.execution.ExecutionModes; import com.intellij.execution.RunContentExecutor; import com.intellij.execution.configurations.GeneralCommandLine; import com.intellij.execution.configurations.ParametersList; import com.intellij.execution.configurations.PtyCommandLine; import com.intellij.execution.process.*; import com.intellij.notification.NotificationType; import com.intellij.notification.Notifications; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.module.Module; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.progress.Task; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.CharsetToolkit; import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.util.Consumer; import com.intellij.util.EnvironmentUtil; import com.intellij.util.ObjectUtils; import com.intellij.util.ThreeState; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; import java.util.Collection; import java.util.Collections; import java.util.Map; public class GoExecutor { private static final Logger LOGGER = Logger.getInstance(GoExecutor.class); @NotNull private final Map<String, String> myExtraEnvironment = ContainerUtil.newHashMap(); @NotNull private final ParametersList myParameterList = new ParametersList(); @NotNull private final ProcessOutput myProcessOutput = new ProcessOutput(); @NotNull private final Project myProject; @Nullable private Boolean myVendoringEnabled; @Nullable private final Module myModule; @Nullable private String myGoRoot; @Nullable private String myGoPath; @Nullable private String myEnvPath; @Nullable private String myWorkDirectory; private boolean myShowOutputOnError; private boolean myShowNotificationsOnError; private boolean myShowNotificationsOnSuccess; private boolean myShowGoEnvVariables = true; private GeneralCommandLine.ParentEnvironmentType myParentEnvironmentType = GeneralCommandLine.ParentEnvironmentType.CONSOLE; private boolean myPtyDisabled; @Nullable private String myExePath; @Nullable private String myPresentableName; private OSProcessHandler myProcessHandler; private final Collection<ProcessListener> myProcessListeners = ContainerUtil.newArrayList(); private GoExecutor(@NotNull Project project, @Nullable Module module) { myProject = project; myModule = module; } public static GoExecutor in(@NotNull Project project, @Nullable Module module) { return module != null ? in(module) : in(project); } @NotNull private static GoExecutor in(@NotNull Project project) { return new GoExecutor(project, null) .withGoRoot(GoSdkService.getInstance(project).getSdkHomePath(null)) .withGoPath(GoSdkUtil.retrieveGoPath(project, null)) .withGoPath(GoSdkUtil.retrieveEnvironmentPathForGo(project, null)); } @NotNull public static GoExecutor in(@NotNull Module module) { Project project = module.getProject(); ThreeState vendoringEnabled = GoModuleSettings.getInstance(module).getVendoringEnabled(); return new GoExecutor(project, module) .withGoRoot(GoSdkService.getInstance(project).getSdkHomePath(module)) .withGoPath(GoSdkUtil.retrieveGoPath(project, module)) .withEnvPath(GoSdkUtil.retrieveEnvironmentPathForGo(project, module)) .withVendoring(vendoringEnabled != ThreeState.UNSURE ? vendoringEnabled.toBoolean() : null); } @NotNull public GoExecutor withPresentableName(@Nullable String presentableName) { myPresentableName = presentableName; return this; } @NotNull public GoExecutor withExePath(@Nullable String exePath) { myExePath = exePath; return this; } @NotNull public GoExecutor withWorkDirectory(@Nullable String workDirectory) { myWorkDirectory = workDirectory; return this; } @NotNull public GoExecutor withGoRoot(@Nullable String goRoot) { myGoRoot = goRoot; return this; } @NotNull public GoExecutor withGoPath(@Nullable String goPath) { myGoPath = goPath; return this; } @NotNull public GoExecutor withEnvPath(@Nullable String envPath) { myEnvPath = envPath; return this; } @NotNull public GoExecutor withVendoring(@Nullable Boolean enabled) { myVendoringEnabled = enabled; return this; } public GoExecutor withProcessListener(@NotNull ProcessListener listener) { myProcessListeners.add(listener); return this; } @NotNull public GoExecutor withExtraEnvironment(@NotNull Map<String, String> environment) { myExtraEnvironment.putAll(environment); return this; } @NotNull public GoExecutor withPassParentEnvironment(boolean passParentEnvironment) { myParentEnvironmentType = passParentEnvironment ? GeneralCommandLine.ParentEnvironmentType.CONSOLE : GeneralCommandLine.ParentEnvironmentType.NONE; return this; } @NotNull public GoExecutor withParameterString(@NotNull String parameterString) { myParameterList.addParametersString(parameterString); return this; } @NotNull public GoExecutor withParameters(@NotNull String... parameters) { myParameterList.addAll(parameters); return this; } public GoExecutor showGoEnvVariables(boolean show) { myShowGoEnvVariables = show; return this; } @NotNull public GoExecutor showOutputOnError() { myShowOutputOnError = true; return this; } @NotNull public GoExecutor disablePty() { myPtyDisabled = true; return this; } @NotNull public GoExecutor showNotifications(boolean onError, boolean onSuccess) { myShowNotificationsOnError = onError; myShowNotificationsOnSuccess = onSuccess; return this; } public boolean execute() { Logger.getInstance(getClass()).assertTrue(!ApplicationManager.getApplication().isDispatchThread(), "It's bad idea to run external tool on EDT"); Logger.getInstance(getClass()).assertTrue(myProcessHandler == null, "Process has already run with this executor instance"); Ref<Boolean> result = Ref.create(false); GeneralCommandLine commandLine = null; try { commandLine = createCommandLine(); GeneralCommandLine finalCommandLine = commandLine; myProcessHandler = new KillableColoredProcessHandler(finalCommandLine, true) { @Override public void startNotify() { if (myShowGoEnvVariables) { GoRunUtil.printGoEnvVariables(finalCommandLine, this); } super.startNotify(); } }; GoHistoryProcessListener historyProcessListener = new GoHistoryProcessListener(); myProcessHandler.addProcessListener(historyProcessListener); for (ProcessListener listener : myProcessListeners) { myProcessHandler.addProcessListener(listener); } CapturingProcessAdapter processAdapter = new CapturingProcessAdapter(myProcessOutput) { @Override public void processTerminated(@NotNull ProcessEvent event) { super.processTerminated(event); boolean success = event.getExitCode() == 0 && myProcessOutput.getStderr().isEmpty(); boolean nothingToShow = myProcessOutput.getStdout().isEmpty() && myProcessOutput.getStderr().isEmpty(); boolean cancelledByUser = (event.getExitCode() == -1 || event.getExitCode() == 2) && nothingToShow; result.set(success); if (success) { if (myShowNotificationsOnSuccess) { showNotification("Finished successfully", NotificationType.INFORMATION); } } else if (cancelledByUser) { if (myShowNotificationsOnError) { showNotification("Interrupted", NotificationType.WARNING); } } else if (myShowOutputOnError) { ApplicationManager.getApplication().invokeLater(() -> showOutput(myProcessHandler, historyProcessListener)); } } }; myProcessHandler.addProcessListener(processAdapter); myProcessHandler.startNotify(); ExecutionModes.SameThreadMode sameThreadMode = new ExecutionModes.SameThreadMode(getPresentableName()); ExecutionHelper.executeExternalProcess(myProject, myProcessHandler, sameThreadMode, commandLine); LOGGER.debug("Finished `" + getPresentableName() + "` with result: " + result.get()); return result.get(); } catch (ExecutionException e) { if (myShowOutputOnError) { ExecutionHelper.showErrors(myProject, Collections.singletonList(e), getPresentableName(), null); } if (myShowNotificationsOnError) { showNotification(StringUtil.notNullize(e.getMessage(), "Unknown error, see logs for details"), NotificationType.ERROR); } String commandLineInfo = commandLine != null ? commandLine.getCommandLineString() : "not constructed"; LOGGER.debug("Finished `" + getPresentableName() + "` with an exception. Commandline: " + commandLineInfo, e); return false; } } public void executeWithProgress(boolean modal) { //noinspection unchecked executeWithProgress(modal, Consumer.EMPTY_CONSUMER); } public void executeWithProgress(boolean modal, @NotNull Consumer<Boolean> consumer) { ProgressManager.getInstance().run(new Task.Backgroundable(myProject, getPresentableName(), true) { private boolean doNotStart; @Override public void onCancel() { doNotStart = true; ProcessHandler handler = getProcessHandler(); if (handler != null) { handler.destroyProcess(); } } @Override public boolean shouldStartInBackground() { return !modal; } @Override public boolean isConditionalModal() { return modal; } @Override public void run(@NotNull ProgressIndicator indicator) { if (doNotStart || myProject == null || myProject.isDisposed()) { return; } indicator.setIndeterminate(true); consumer.consume(execute()); } }); } @Nullable public ProcessHandler getProcessHandler() { return myProcessHandler; } private void showNotification(@NotNull String message, NotificationType type) { ApplicationManager.getApplication().invokeLater(() -> { String title = getPresentableName(); Notifications.Bus.notify(GoConstants.GO_EXECUTION_NOTIFICATION_GROUP.createNotification(title, message, type, null), myProject); }); } private void showOutput(@NotNull OSProcessHandler originalHandler, @NotNull GoHistoryProcessListener historyProcessListener) { if (myShowOutputOnError) { BaseOSProcessHandler outputHandler = new KillableColoredProcessHandler(originalHandler.getProcess(), null); RunContentExecutor runContentExecutor = new RunContentExecutor(myProject, outputHandler) .withTitle(getPresentableName()) .withActivateToolWindow(myShowOutputOnError) .withFilter(new GoConsoleFilter(myProject, myModule, myWorkDirectory != null ? VfsUtilCore.pathToUrl(myWorkDirectory) : null)); Disposer.register(myProject, runContentExecutor); runContentExecutor.run(); historyProcessListener.apply(outputHandler); } if (myShowNotificationsOnError) { showNotification("Failed to run", NotificationType.ERROR); } } @NotNull public GeneralCommandLine createCommandLine() throws ExecutionException { if (myGoRoot == null) { throw new ExecutionException("Sdk is not set or Sdk home path is empty for module"); } GeneralCommandLine commandLine = !myPtyDisabled && PtyCommandLine.isEnabled() ? new PtyCommandLine() : new GeneralCommandLine(); commandLine.setExePath(ObjectUtils.notNull(myExePath, GoSdkService.getGoExecutablePath(myGoRoot))); commandLine.getEnvironment().putAll(myExtraEnvironment); commandLine.getEnvironment().put(GoConstants.GO_ROOT, StringUtil.notNullize(myGoRoot)); commandLine.getEnvironment().put(GoConstants.GO_PATH, StringUtil.notNullize(myGoPath)); if (myVendoringEnabled != null) { commandLine.getEnvironment().put(GoConstants.GO_VENDORING_EXPERIMENT, myVendoringEnabled ? "1" : "0"); } Collection<String> paths = ContainerUtil.newArrayList(); ContainerUtil.addIfNotNull(paths, StringUtil.nullize(commandLine.getEnvironment().get(GoConstants.PATH), true)); ContainerUtil.addIfNotNull(paths, StringUtil.nullize(EnvironmentUtil.getValue(GoConstants.PATH), true)); ContainerUtil.addIfNotNull(paths, StringUtil.nullize(myEnvPath, true)); commandLine.getEnvironment().put(GoConstants.PATH, StringUtil.join(paths, File.pathSeparator)); commandLine.withWorkDirectory(myWorkDirectory); commandLine.addParameters(myParameterList.getList()); commandLine.withParentEnvironmentType(myParentEnvironmentType); commandLine.withCharset(CharsetToolkit.UTF8_CHARSET); return commandLine; } @NotNull private String getPresentableName() { return ObjectUtils.notNull(myPresentableName, "go"); } }