| /* |
| * Copyright (C) 2013 The Android Open Source Project |
| * |
| * 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.android.tools.idea.gradle.invoker; |
| |
| import com.android.builder.model.AndroidProject; |
| import com.android.ide.common.blame.Message; |
| import com.android.ide.common.blame.SourceFile; |
| import com.android.ide.common.blame.SourceFilePosition; |
| import com.android.ide.common.blame.parser.PatternAwareOutputParser; |
| import com.android.tools.idea.gradle.IdeaGradleProject; |
| import com.android.tools.idea.gradle.compiler.AndroidGradleBuildConfiguration; |
| import com.android.tools.idea.gradle.facet.AndroidGradleFacet; |
| import com.android.tools.idea.gradle.invoker.console.view.GradleConsoleToolWindowFactory; |
| import com.android.tools.idea.gradle.invoker.console.view.GradleConsoleView; |
| import com.android.tools.idea.gradle.invoker.messages.GradleBuildTreeViewPanel; |
| import com.android.tools.idea.gradle.output.parser.BuildOutputParser; |
| import com.android.tools.idea.gradle.service.notification.errors.AbstractSyncErrorHandler; |
| import com.android.tools.idea.gradle.util.AndroidGradleSettings; |
| import com.android.tools.idea.sdk.IdeSdks; |
| import com.android.tools.idea.sdk.SelectSdkDialog; |
| import com.google.common.base.Stopwatch; |
| import com.google.common.collect.Lists; |
| import com.intellij.compiler.CompilerManagerImpl; |
| import com.intellij.compiler.CompilerWorkspaceConfiguration; |
| import com.intellij.execution.ui.ConsoleViewContentType; |
| import com.intellij.ide.errorTreeView.NewErrorTreeViewPanel; |
| import com.intellij.notification.Notification; |
| import com.intellij.notification.NotificationGroup; |
| import com.intellij.notification.NotificationType; |
| import com.intellij.openapi.application.Application; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.compiler.CompilerBundle; |
| import com.intellij.openapi.compiler.CompilerManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.externalSystem.model.ExternalSystemException; |
| import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId; |
| import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationListener; |
| import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationListenerAdapter; |
| import com.intellij.openapi.externalSystem.service.notification.NotificationCategory; |
| import com.intellij.openapi.externalSystem.service.notification.NotificationData; |
| import com.intellij.openapi.externalSystem.service.notification.NotificationSource; |
| import com.intellij.openapi.module.Module; |
| import com.intellij.openapi.progress.EmptyProgressIndicator; |
| import com.intellij.openapi.progress.ProgressIndicator; |
| import com.intellij.openapi.progress.Task; |
| import com.intellij.openapi.progress.util.AbstractProgressIndicatorExBase; |
| import com.intellij.openapi.project.DumbModeAction; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.project.ProjectManager; |
| import com.intellij.openapi.project.ProjectManagerListener; |
| import com.intellij.openapi.ui.MessageType; |
| import com.intellij.openapi.ui.Messages; |
| import com.intellij.openapi.util.Disposer; |
| import com.intellij.openapi.util.Key; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.openapi.wm.ToolWindow; |
| import com.intellij.openapi.wm.ToolWindowId; |
| import com.intellij.openapi.wm.ToolWindowManager; |
| import com.intellij.openapi.wm.ex.ProgressIndicatorEx; |
| import com.intellij.pom.Navigatable; |
| import com.intellij.ui.AppIcon; |
| import com.intellij.ui.content.*; |
| import com.intellij.util.Consumer; |
| import com.intellij.util.Function; |
| import com.intellij.util.SystemProperties; |
| import com.intellij.util.ui.MessageCategory; |
| import org.gradle.tooling.*; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.jetbrains.jps.service.JpsServiceManager; |
| import org.jetbrains.plugins.gradle.service.project.GradleExecutionHelper; |
| import org.jetbrains.plugins.gradle.settings.GradleExecutionSettings; |
| |
| import javax.swing.*; |
| import javax.swing.event.HyperlinkEvent; |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.PrintStream; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.concurrent.Semaphore; |
| |
| import static com.android.tools.idea.gradle.util.GradleBuilds.CONFIGURE_ON_DEMAND_OPTION; |
| import static com.android.tools.idea.gradle.util.GradleBuilds.PARALLEL_BUILD_OPTION; |
| import static com.android.tools.idea.gradle.util.GradleUtil.*; |
| import static com.android.tools.idea.gradle.util.Projects.getBaseDirPath; |
| import static com.android.tools.idea.startup.AndroidStudioSpecificInitializer.isAndroidStudio; |
| import static com.google.common.base.Splitter.on; |
| import static com.google.common.base.Strings.nullToEmpty; |
| import static com.google.common.io.Closeables.close; |
| import static com.intellij.execution.ui.ConsoleViewContentType.ERROR_OUTPUT; |
| import static com.intellij.execution.ui.ConsoleViewContentType.NORMAL_OUTPUT; |
| import static com.intellij.openapi.application.ModalityState.NON_MODAL; |
| import static com.intellij.openapi.ui.MessageType.*; |
| import static com.intellij.openapi.util.text.StringUtil.*; |
| import static com.intellij.openapi.vfs.VfsUtil.findFileByIoFile; |
| import static com.intellij.ui.AppUIUtil.invokeLaterIfProjectAlive; |
| import static com.intellij.util.ArrayUtil.toStringArray; |
| import static com.intellij.util.ExceptionUtil.getRootCause; |
| import static com.intellij.util.ui.UIUtil.invokeLaterIfNeeded; |
| import static java.util.concurrent.TimeUnit.MILLISECONDS; |
| import static org.jetbrains.android.AndroidPlugin.*; |
| import static org.jetbrains.plugins.gradle.service.project.GradleExecutionHelper.prepare; |
| |
| /** |
| * Invokes Gradle tasks as a IDEA task in the background. |
| */ |
| class GradleTasksExecutor extends Task.Backgroundable { |
| private static final ExternalSystemTaskNotificationListener GRADLE_LISTENER = new ExternalSystemTaskNotificationListenerAdapter() { |
| }; |
| |
| private static final long ONE_MINUTE_MS = 60L /*sec*/ * 1000L /*millisec*/; |
| private static final Logger LOG = Logger.getInstance(GradleInvoker.class); |
| public static final NotificationGroup LOGGING_NOTIFICATION = NotificationGroup.logOnlyGroup("Gradle Build (Logging)"); |
| public static final NotificationGroup BALLOON_NOTIFICATION = NotificationGroup.balloonGroup("Gradle Build (Balloon)"); |
| |
| // Dummy objects used for mapping {@link AbstractSyncErrorHandler} to the 'build messages' environment. |
| private static final Notification DUMMY_NOTIFICATION = new Notification("dummy", "dummy", "dummy", NotificationType.ERROR); |
| private static final Object DUMMY_EVENT_SOURCE = new Object(); |
| |
| @NonNls private static final String CONTENT_NAME = "Gradle Build"; |
| @NonNls private static final String APP_ICON_ID = "compiler"; |
| |
| private static final Key<Key<?>> CONTENT_ID_KEY = Key.create("CONTENT_ID"); |
| private static final int BUFFER_SIZE = 2048; |
| |
| private static final String GRADLE_RUNNING_MSG_TITLE = "Gradle Running"; |
| |
| @NotNull private final Key<Key<?>> myContentId = Key.create("compile_content"); |
| |
| @NotNull private final Object myMessageViewLock = new Object(); |
| @NotNull private final Object myCompletionLock = new Object(); |
| private int myCompletionCounter; |
| |
| @NotNull private final GradleTaskExecutionContext myContext; |
| @Nullable private GradleBuildTreeViewPanel myErrorTreeView; |
| |
| @NotNull private final GradleExecutionHelper myHelper = new GradleExecutionHelper(); |
| |
| private volatile int myErrorCount; |
| private volatile int myWarningCount; |
| |
| @NotNull private volatile ProgressIndicator myIndicator = new EmptyProgressIndicator(); |
| |
| private volatile boolean myMessageViewIsPrepared; |
| private volatile boolean myMessagesAutoActivated; |
| |
| private CloseListener myCloseListener; |
| |
| GradleTasksExecutor(@NotNull GradleTaskExecutionContext context) { |
| super(context.getProject(), "Gradle Build Running", true); |
| myContext = context; |
| } |
| |
| @Override |
| public String getProcessId() { |
| return "GradleTaskInvocation"; |
| } |
| |
| @Override |
| @NotNull |
| public DumbModeAction getDumbModeAction() { |
| return DumbModeAction.WAIT; |
| } |
| |
| @Override |
| @Nullable |
| public NotificationInfo getNotificationInfo() { |
| return new NotificationInfo(myErrorCount > 0 ? "Gradle Invocation (errors)" : "Gradle Invocation (success)", |
| "Gradle Invocation Finished", myErrorCount + " Errors, " + myWarningCount + " Warnings", true); |
| } |
| |
| @Override |
| public void run(@NotNull ProgressIndicator indicator) { |
| if (isAndroidStudio()) { |
| // See https://code.google.com/p/android/issues/detail?id=169743 |
| clearStoredGradleJvmArgs(getNotNullProject()); |
| } |
| |
| myIndicator = indicator; |
| |
| ProjectManager projectManager = ProjectManager.getInstance(); |
| Project project = getNotNullProject(); |
| myCloseListener = new CloseListener(); |
| projectManager.addProjectManagerListener(project, myCloseListener); |
| |
| Semaphore semaphore = ((CompilerManagerImpl)CompilerManager.getInstance(project)).getCompilationSemaphore(); |
| boolean acquired = false; |
| try { |
| try { |
| while (!acquired) { |
| acquired = semaphore.tryAcquire(300, MILLISECONDS); |
| if (indicator.isCanceled()) { |
| // Give up obtaining the semaphore, let compile work begin in order to stop gracefully on cancel event. |
| break; |
| } |
| } |
| } |
| catch (InterruptedException e) { |
| Thread.currentThread().interrupt(); |
| } |
| |
| if (!isHeadless()) { |
| addIndicatorDelegate(); |
| } |
| invokeGradleTasks(); |
| } |
| finally { |
| try { |
| indicator.stop(); |
| projectManager.removeProjectManagerListener(project, myCloseListener); |
| } |
| finally { |
| if (acquired) { |
| semaphore.release(); |
| } |
| } |
| } |
| } |
| |
| private void addIndicatorDelegate() { |
| if (myIndicator instanceof ProgressIndicatorEx) { |
| ProgressIndicatorEx indicator = (ProgressIndicatorEx)myIndicator; |
| indicator.addStateDelegate(new ProgressIndicatorStateDelegate()); |
| } |
| } |
| |
| private void closeView() { |
| ApplicationManager.getApplication().invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| synchronized (myMessageViewLock) { |
| if (myErrorTreeView != null && !getNotNullProject().isDisposed()) { |
| addStatisticsMessage(CompilerBundle.message("statistics.error.count", myErrorCount)); |
| addStatisticsMessage(CompilerBundle.message("statistics.warnings.count", myWarningCount)); |
| |
| addMessage(new Message(Message.Kind.INFO, "See complete output in console", SourceFilePosition.UNKNOWN), new OpenGradleConsole()); |
| myErrorTreeView.selectFirstMessage(); |
| } |
| } |
| } |
| |
| private void addStatisticsMessage(@NotNull String text) { |
| addMessage(new Message(Message.Kind.STATISTICS, text, SourceFilePosition.UNKNOWN), null); |
| } |
| }, NON_MODAL); |
| } |
| |
| private void invokeGradleTasks() { |
| final Project project = getNotNullProject(); |
| final GradleExecutionSettings executionSettings = getGradleExecutionSettings(project); |
| |
| Function<ProjectConnection, Void> executeTasksFunction = new Function<ProjectConnection, Void>() { |
| @Override |
| public Void fun(ProjectConnection connection) { |
| final Stopwatch stopwatch = Stopwatch.createStarted(); |
| |
| GradleConsoleView consoleView = GradleConsoleView.getInstance(project); |
| consoleView.clear(); |
| |
| addMessage(new Message(Message.Kind.INFO, "Gradle tasks " + myContext.getGradleTasks(), SourceFilePosition.UNKNOWN), null); |
| |
| String executingTasksText = "Executing tasks: " + myContext.getGradleTasks(); |
| consoleView.print(executingTasksText + SystemProperties.getLineSeparator() + SystemProperties.getLineSeparator(), NORMAL_OUTPUT); |
| addToEventLog(executingTasksText, INFO); |
| |
| GradleOutputForwarder output = new GradleOutputForwarder(consoleView); |
| |
| BuildException buildError = null; |
| final ExternalSystemTaskId id = myContext.getTaskId(); |
| CancellationTokenSource cancellationTokenSource = GradleConnector.newCancellationTokenSource(); |
| try { |
| AndroidGradleBuildConfiguration buildConfiguration = AndroidGradleBuildConfiguration.getInstance(project); |
| List<String> commandLineArgs = Lists.newArrayList(buildConfiguration.getCommandLineOptions()); |
| |
| if (buildConfiguration.USE_CONFIGURATION_ON_DEMAND && !commandLineArgs.contains(CONFIGURE_ON_DEMAND_OPTION)) { |
| commandLineArgs.add(CONFIGURE_ON_DEMAND_OPTION); |
| } |
| |
| if (!commandLineArgs.contains(PARALLEL_BUILD_OPTION) && |
| CompilerWorkspaceConfiguration.getInstance(project).PARALLEL_COMPILATION) { |
| commandLineArgs.add(PARALLEL_BUILD_OPTION); |
| } |
| |
| commandLineArgs.add(AndroidGradleSettings.createProjectProperty(AndroidProject.PROPERTY_INVOKED_FROM_IDE, true)); |
| commandLineArgs.addAll(myContext.getCommandLineArgs()); |
| addLocalMavenRepoInitScriptCommandLineOption(commandLineArgs); |
| attemptToUseEmbeddedGradle(project); |
| |
| LOG.info("Build command line options: " + commandLineArgs); |
| |
| List<String> jvmArgs = Collections.emptyList(); |
| BuildLauncher launcher = connection.newBuild(); |
| prepare(launcher, id, executionSettings, GRADLE_LISTENER, jvmArgs, commandLineArgs, connection); |
| |
| File javaHome = IdeSdks.getJdkPath(); |
| if (javaHome != null) { |
| launcher.setJavaHome(javaHome); |
| } |
| |
| myContext.storeCancellationInfoFor(id, cancellationTokenSource); |
| launcher.forTasks(toStringArray(myContext.getGradleTasks())); |
| launcher.withCancellationToken(cancellationTokenSource.token()); |
| |
| GradleOutputForwarder.Listener outputListener = null; |
| if (myContext.getTaskNotificationListener() != null) { |
| outputListener = new GradleOutputForwarder.Listener() { |
| @Override |
| public void onOutput(@NotNull ConsoleViewContentType contentType, @NotNull byte[] data, int offset, int length) { |
| if (myContext.isActive(id)) { |
| myContext.getTaskNotificationListener().onTaskOutput(id, new String(data, offset, length), contentType != ERROR_OUTPUT); |
| } |
| } |
| }; |
| } |
| output.attachTo(launcher, outputListener); |
| launcher.run(); |
| } |
| catch (BuildException e) { |
| buildError = e; |
| } |
| catch (Throwable e) { |
| handleTaskExecutionError(e); |
| } |
| finally { |
| myContext.dropCancellationInfoFor(id); |
| String gradleOutput = output.toString(); |
| Application application = ApplicationManager.getApplication(); |
| if (isGuiTestingMode()) { |
| String testOutput = application.getUserData(GRADLE_BUILD_OUTPUT_IN_GUI_TEST_KEY); |
| if (isNotEmpty(testOutput)) { |
| gradleOutput = testOutput; |
| application.putUserData(GRADLE_BUILD_OUTPUT_IN_GUI_TEST_KEY, null); |
| } |
| } |
| List<Message> buildMessages = Lists.newArrayList(showMessages(gradleOutput)); |
| if (myErrorCount == 0 && buildError != null && !hasCause(buildError, BuildCancelledException.class)) { |
| // Gradle throws BuildCancelledException when we cancel task execution. We don't want to force showing 'Messages' tool |
| // window for that situation though. |
| showBuildException(buildError, output.getStdErr(), buildMessages); |
| } |
| output.close(); |
| |
| stopwatch.stop(); |
| |
| application.invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| notifyGradleInvocationCompleted(stopwatch.elapsed(MILLISECONDS)); |
| } |
| }); |
| |
| if (buildError == null || !hasCause(buildError, BuildCancelledException.class)) { |
| // Gradle throws BuildCancelledException when we cancel task execution. We don't want to force showing 'Messages' tool |
| // window for that situation though. |
| application.invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| showMessages(); |
| } |
| }); |
| } |
| |
| boolean buildSuccessful = buildError == null; |
| GradleInvocationResult result = new GradleInvocationResult(myContext.getGradleTasks(), buildMessages, buildSuccessful); |
| for (GradleInvoker.AfterGradleInvocationTask task : myContext.getGradleInvoker().getAfterInvocationTasks()) { |
| task.execute(result); |
| } |
| } |
| return null; |
| } |
| }; |
| |
| if (isGuiTestingMode()) { |
| // We use this task in GUI tests to simulate errors coming from Gradle project sync. |
| Application application = ApplicationManager.getApplication(); |
| Runnable task = application.getUserData(EXECUTE_BEFORE_PROJECT_BUILD_IN_GUI_TEST_KEY); |
| if (task != null) { |
| application.putUserData(EXECUTE_BEFORE_PROJECT_BUILD_IN_GUI_TEST_KEY, null); |
| task.run(); |
| } |
| } |
| |
| File projectDirPath = getBaseDirPath(project); |
| myHelper.execute(projectDirPath.getPath(), executionSettings, executeTasksFunction); |
| } |
| |
| private void handleTaskExecutionError(@NotNull Throwable e) { |
| if (myIndicator.isCanceled()) { |
| LOG.info("Failed to complete Gradle execution. Project may be closing or already closed.", e); |
| return; |
| } |
| //noinspection ThrowableResultOfMethodCallIgnored |
| Throwable rootCause = getRootCause(e); |
| final String error = nullToEmpty(rootCause.getMessage()); |
| if (error.contains("Build cancelled")) { |
| return; |
| } |
| Runnable showErrorTask = new Runnable() { |
| @Override |
| public void run() { |
| String msg = "Failed to complete Gradle execution."; |
| if (isEmpty(error)) { |
| // Unlikely that 'error' is null or empty, since now we catch the real exception. |
| msg += " Cause: unknown."; |
| } |
| else { |
| msg += "\n\nCause:\n" + error; |
| } |
| addMessage(new Message(Message.Kind.ERROR, msg, SourceFilePosition.UNKNOWN), null); |
| showMessages(); |
| |
| // This is temporary. Once we have support for hyperlinks in "Messages" window, we'll show the error message the with a |
| // hyperlink to set the JDK home. |
| // For now we show the "Select SDK" dialog, but only giving the option to set the JDK path. |
| if (isAndroidStudio() && error.startsWith("Supplied javaHome is not a valid folder")) { |
| File androidHome = IdeSdks.getAndroidSdkPath(); |
| String androidSdkPath = androidHome != null ? androidHome.getPath() : null; |
| SelectSdkDialog selectSdkDialog = new SelectSdkDialog(null, androidSdkPath); |
| selectSdkDialog.setModal(true); |
| if (selectSdkDialog.showAndGet()) { |
| final String jdkHome = selectSdkDialog.getJdkHome(); |
| invokeLaterIfNeeded(new Runnable() { |
| @Override |
| public void run() { |
| ApplicationManager.getApplication().runWriteAction(new Runnable() { |
| @Override |
| public void run() { |
| IdeSdks.setJdkPath(new File(jdkHome)); |
| } |
| }); |
| } |
| }); |
| } |
| } |
| } |
| }; |
| invokeLaterIfProjectAlive(getNotNullProject(), showErrorTask); |
| } |
| |
| @NotNull |
| private List<Message> showMessages(@NotNull String gradleOutput) { |
| Iterable<PatternAwareOutputParser> parsers = JpsServiceManager.getInstance().getExtensions(PatternAwareOutputParser.class); |
| List<Message> compilerMessages = new BuildOutputParser(parsers).parseGradleOutput(gradleOutput); |
| for (Message msg : compilerMessages) { |
| addMessage(msg, null); |
| } |
| return compilerMessages; |
| } |
| |
| /** |
| * Something went wrong while invoking Gradle but the output parsers did not create any build messages. We show the stack trace in the |
| * "Messages" view. |
| */ |
| private void showBuildException(@NotNull BuildException e, @NotNull String stdErr, @NotNull List<Message> buildMessages) { |
| // There are no error messages to present. Show some feedback indicating that something went wrong. |
| if (!stdErr.trim().isEmpty()) { |
| // Show the contents of stderr as a compiler error. |
| Message msg = new Message(Message.Kind.ERROR, stdErr, SourceFilePosition.UNKNOWN); |
| buildMessages.add(msg); |
| addMessage(msg, null); |
| } |
| else { |
| // Since we have nothing else to show, just print the stack trace of the caught exception. |
| ByteArrayOutputStream out = new ByteArrayOutputStream(BUFFER_SIZE); |
| try { |
| //noinspection IOResourceOpenedButNotSafelyClosed |
| e.printStackTrace(new PrintStream(out)); |
| String message = "Internal error:" + SystemProperties.getLineSeparator() + out.toString(); |
| Message msg = new Message(Message.Kind.ERROR, message, SourceFilePosition.UNKNOWN); |
| buildMessages.add(msg); |
| addMessage(msg, null); |
| } |
| finally { |
| try { |
| close(out, true /* swallowIOException */); |
| } catch (IOException ex) { |
| // Cannot happen |
| } |
| } |
| } |
| } |
| |
| private void addMessage(@NotNull final Message message, @Nullable final Navigatable navigatable) { |
| prepareMessageView(); |
| switch (message.getKind()) { |
| case WARNING: |
| myWarningCount++; |
| break; |
| case ERROR: |
| myErrorCount++; |
| default: |
| // do nothing. |
| } |
| Runnable addMessageTask = new Runnable() { |
| @Override |
| public void run() { |
| openMessageView(); |
| add(message, navigatable); |
| } |
| }; |
| invokeLaterIfNeeded(addMessageTask); |
| } |
| |
| private void prepareMessageView() { |
| if (!myIndicator.isRunning() || myMessageViewIsPrepared) { |
| return; |
| } |
| myMessageViewIsPrepared = true; |
| ApplicationManager.getApplication().invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| if (!getNotNullProject().isDisposed()) { |
| synchronized (myMessageViewLock) { |
| // Clear messages from the previous compilation |
| if (myErrorTreeView == null) { |
| // If message view != null, the contents has already been cleared. |
| removeUnpinnedBuildMessages(getNotNullProject(), null); |
| } |
| } |
| } |
| } |
| }); |
| } |
| |
| static void clearMessageView(@NotNull final Project project) { |
| ApplicationManager.getApplication().invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| if (!project.isDisposed()) { |
| removeUnpinnedBuildMessages(project, null); |
| } |
| } |
| }); |
| } |
| |
| private static void removeUnpinnedBuildMessages(@NotNull final Project project, @Nullable final Content toKeep) { |
| if (project.isInitialized()) { |
| MessageView messageView = MessageView.SERVICE.getInstance(project); |
| Content[] contents = messageView.getContentManager().getContents(); |
| for (Content content : contents) { |
| if (content.isPinned() || content == toKeep) { |
| continue; |
| } |
| if (content.getUserData(CONTENT_ID_KEY) != null) { // the content was added by me |
| messageView.getContentManager().removeContent(content, true); |
| } |
| } |
| } |
| } |
| |
| private void openMessageView() { |
| if (myIndicator.isCanceled()) { |
| return; |
| } |
| |
| Project project = getNotNullProject(); |
| JComponent component; |
| synchronized (myMessageViewLock) { |
| if (myErrorTreeView != null) { |
| return; |
| } |
| //noinspection ConstantConditions |
| myErrorTreeView = new GradleBuildTreeViewPanel(project); |
| myErrorTreeView.setProcessController(new NewErrorTreeViewPanel.ProcessController() { |
| @Override |
| public void stopProcess() { |
| stopBuild(); |
| } |
| |
| @Override |
| public boolean isProcessStopped() { |
| return !myIndicator.isRunning(); |
| } |
| }); |
| component = myErrorTreeView.getComponent(); |
| } |
| |
| Content content = ContentFactory.SERVICE.getInstance().createContent(component, CONTENT_NAME, true); |
| content.putUserData(CONTENT_ID_KEY, myContentId); |
| |
| MessageView messageView = getMessageView(); |
| ContentManager contentManager = messageView.getContentManager(); |
| contentManager.addContent(content); |
| |
| myCloseListener.setContent(contentManager, content); |
| |
| removeUnpinnedBuildMessages(getNotNullProject(), content); |
| contentManager.setSelectedContent(content); |
| } |
| |
| private void activateGradleConsole() { |
| ToolWindow window = getToolWindowManager().getToolWindow(GradleConsoleToolWindowFactory.ID); |
| if (window != null) { |
| window.activate(null, false); |
| } |
| } |
| |
| private void add(@NotNull Message message, @Nullable Navigatable navigatable) { |
| synchronized (myMessageViewLock) { |
| if (myErrorTreeView != null && !getNotNullProject().isDisposed()) { |
| Message.Kind messageKind = message.getKind(); |
| int type = translateMessageKind(messageKind); |
| LinkAwareMessageData messageData = prepareMessage(message); |
| if (navigatable == null) { |
| VirtualFile file = findFileFrom(message); |
| myErrorTreeView.addMessage(type, |
| messageData.textLines, |
| file, |
| message.getLineNumber() - 1, |
| message.getColumn() - 1, |
| messageData.hyperlinkListener); |
| } |
| else { |
| myErrorTreeView.addMessage(type, messageData.textLines, null, navigatable, null, null, messageData.hyperlinkListener); |
| } |
| |
| boolean autoActivate = !myMessagesAutoActivated && type == MessageCategory.ERROR; |
| if (autoActivate) { |
| myMessagesAutoActivated = true; |
| activateMessageView(); |
| } |
| } |
| } |
| } |
| |
| @NotNull |
| private LinkAwareMessageData prepareMessage(@NotNull Message message) { |
| final List<String> rawTextLines; |
| String text = message.getText(); |
| if (text.indexOf('\n') == -1) { |
| rawTextLines = Collections.singletonList(text); |
| } |
| else { |
| rawTextLines = Lists.newArrayList(on('\n').split(text)); |
| } |
| if (message.getKind() != Message.Kind.ERROR) { |
| //noinspection unchecked |
| return new LinkAwareMessageData(toStringArray(rawTextLines), null); |
| } |
| |
| // The general idea is to adapt existing gradle output enhancers (AbstractSyncErrorHandler) to the 'gradle build' process. |
| // Their are built in assumption that they enhance external system's NotificationData by custom html hyperlinks markup and |
| // corresponding listeners. So, what we do here is just providing fake NotificationData to the handlers and extract the |
| // data added by them (if any). |
| List<String> enhancedTextLines = null; // Text lines with added hyperlinks, i.e. hold text to actually show to end-user |
| List<String> linesBuffer = Lists.newArrayListWithExpectedSize(1); |
| linesBuffer.add(""); |
| final NotificationData dummyData = |
| new NotificationData("", message.getText(), NotificationCategory.ERROR, NotificationSource.PROJECT_SYNC); |
| String previousMessage = dummyData.getMessage(); |
| for (AbstractSyncErrorHandler handler : AbstractSyncErrorHandler.EP_NAME.getExtensions()) { |
| // We experienced that AbstractSyncErrorHandler often look to the first line only (because gradle output is sequential, line-by-line. |
| // That's why we roll through all message lines and offer every of them to the handler. |
| for (int i = 0; i < rawTextLines.size(); i++) { |
| String line = rawTextLines.get(i); |
| |
| // This logic comes from gradle itself. Corresponding 'clearing' code remains at BuildFailureParser.parse() |
| String prefixToStrip = "> "; |
| if (line.startsWith(prefixToStrip)) { |
| line = line.substring(prefixToStrip.length()); |
| } |
| |
| linesBuffer.set(0, line); |
| boolean handled = handler.handleError(linesBuffer, new ExternalSystemException(message.getText()), dummyData, getNotNullProject()); |
| if (handled) { |
| // Extract text added by the handler and store it at the 'enhancedTextLines' collection. |
| String currentMessage = dummyData.getMessage(); |
| if (currentMessage.length() > previousMessage.length()) { |
| int j = previousMessage.length(); |
| if (currentMessage.charAt(j) == '\n') { |
| j++; |
| } |
| String addedText = currentMessage.substring(j); |
| if (enhancedTextLines == null) { |
| enhancedTextLines = Lists.newArrayList(rawTextLines); |
| } |
| enhancedTextLines.add(addedText); |
| previousMessage = currentMessage; |
| } |
| } |
| } |
| } |
| final List<String> textLinesToUse; |
| final Consumer<String> hyperlinkListener; |
| if (enhancedTextLines == null) { |
| textLinesToUse = rawTextLines; |
| hyperlinkListener = null; |
| } |
| else { |
| textLinesToUse = enhancedTextLines; |
| // AbstractSyncErrorHandler add hyperlinks (which we derived earlier and stored in 'enhancedTextLines') and hyperlink listeners |
| // (facaded by the NotificationData.getListener()). So, what we do here is just delegating 'activate link' event |
| // to those hyperlink listeners added by AbstractSyncErrorHandler. |
| hyperlinkListener = new Consumer<String>() { |
| @Override |
| public void consume(String url) { |
| HyperlinkEvent event = new HyperlinkEvent(DUMMY_EVENT_SOURCE, HyperlinkEvent.EventType.ACTIVATED, null, url); |
| dummyData.getListener().hyperlinkUpdate(DUMMY_NOTIFICATION, event); |
| } |
| }; |
| } |
| return new LinkAwareMessageData(toStringArray(textLinesToUse), hyperlinkListener); |
| } |
| |
| @Nullable |
| private VirtualFile findFileFrom(@NotNull Message message) { |
| SourceFile source = message.getSourceFilePositions().get(0).getFile(); |
| if (source.getSourceFile() != null) { |
| return findFileByIoFile(source.getSourceFile(), true); |
| } |
| if (source.getDescription() != null) { |
| String gradlePath = source.getDescription(); |
| Module module = findModuleByGradlePath(getNotNullProject(), gradlePath); |
| if (module != null) { |
| AndroidGradleFacet facet = AndroidGradleFacet.getInstance(module); |
| // if we got here facet is not null; |
| assert facet != null; |
| IdeaGradleProject gradleProject = facet.getGradleProject(); |
| return gradleProject != null ? gradleProject.getBuildFile() : null; |
| } |
| } |
| return null; |
| } |
| |
| private static int translateMessageKind(@NotNull Message.Kind kind) { |
| switch (kind) { |
| case INFO: |
| return MessageCategory.INFORMATION; |
| case WARNING: |
| return MessageCategory.WARNING; |
| case ERROR: |
| return MessageCategory.ERROR; |
| case STATISTICS: |
| return MessageCategory.STATISTICS; |
| case SIMPLE: |
| return MessageCategory.SIMPLE; |
| default: |
| LOG.info("Unknown message kind: " + kind); |
| return 0; |
| } |
| } |
| |
| private void notifyGradleInvocationCompleted(long durationMillis) { |
| Project project = getNotNullProject(); |
| if (!project.isDisposed()) { |
| String statusMsg = createStatusMessage(durationMillis); |
| MessageType messageType = myErrorCount > 0 ? ERROR : myWarningCount > 0 ? WARNING : INFO; |
| if (durationMillis > ONE_MINUTE_MS) { |
| BALLOON_NOTIFICATION.createNotification(statusMsg, messageType).notify(project); |
| } |
| else { |
| addToEventLog(statusMsg, messageType); |
| } |
| } |
| } |
| |
| @NotNull |
| private String createStatusMessage(long durationMillis) { |
| String message = "Gradle build finished"; |
| if (myErrorCount > 0) { |
| if (myWarningCount > 0) { |
| message += String.format(" with %d error(s) and %d warning(s)", myErrorCount, myWarningCount); |
| } |
| else { |
| message += String.format(" with %d error(s)", myErrorCount); |
| } |
| } |
| else if (myWarningCount > 0) { |
| message += String.format(" with %d warnings(s)", myWarningCount); |
| } |
| message = message + " in " + formatDuration(durationMillis); |
| return message; |
| } |
| |
| private void addToEventLog(@NotNull String message, @NotNull MessageType type) { |
| LOGGING_NOTIFICATION.createNotification(message, type).notify(myProject); |
| } |
| |
| @NotNull |
| private ToolWindowManager getToolWindowManager() { |
| return ToolWindowManager.getInstance(getNotNullProject()); |
| } |
| |
| private void showMessages() { |
| synchronized (myMessageViewLock) { |
| if (myErrorTreeView != null && !getNotNullProject().isDisposed()) { |
| MessageView messageView = getMessageView(); |
| Content[] contents = messageView.getContentManager().getContents(); |
| for (Content content : contents) { |
| if (content.getUserData(CONTENT_ID_KEY) != null) { |
| messageView.getContentManager().setSelectedContent(content); |
| return; |
| } |
| } |
| } |
| } |
| } |
| |
| @NotNull |
| private MessageView getMessageView() { |
| return MessageView.SERVICE.getInstance(getNotNullProject()); |
| } |
| |
| @NotNull |
| private Project getNotNullProject() { |
| assert myProject != null; |
| return myProject; |
| } |
| |
| private void activateMessageView() { |
| synchronized (myMessageViewLock) { |
| if (myErrorTreeView != null) { |
| ToolWindow window = getToolWindowManager().getToolWindow(ToolWindowId.MESSAGES_WINDOW); |
| if (window != null) { |
| window.activate(null, false); |
| } |
| } |
| } |
| } |
| |
| private void cancel() { |
| if (!myIndicator.isCanceled()) { |
| stopBuild(); |
| myIndicator.cancel(); |
| } |
| } |
| |
| private void stopBuild() { |
| ExternalSystemTaskId taskId = myContext.getTaskId(); |
| if (myIndicator.isRunning()) { |
| myIndicator.setText("Stopping Gradle build..."); |
| } |
| GradleInvoker.getInstance(getNotNullProject()).cancelTask(taskId); |
| } |
| |
| /** |
| * Regular {@link #queue()} method might return immediately if current task is executed in a separate non-calling thread. |
| * <p/> |
| * However, sometimes we want to wait for the task completion, e.g. consider a use-case when we execute an IDE run configuration. |
| * It opens dedicated run/debug tool window and displays execution output there. However, it is shown as finished as soon as |
| * control flow returns. That's why we don't want to return control flow until the actual task completion. |
| * <p/> |
| * This method allows to achieve that target - it executes gradle tasks under the IDE 'progress management system' (shows progress |
| * bar at the bottom) in a separate thread and doesn't return control flow to the calling thread until all target tasks are actually |
| * executed. |
| */ |
| public void queueAndWaitForCompletion() { |
| final int counterBefore; |
| synchronized (myCompletionLock) { |
| counterBefore = myCompletionCounter; |
| } |
| invokeLaterIfNeeded(new Runnable() { |
| @Override |
| public void run() { |
| queue(); |
| } |
| }); |
| synchronized (myCompletionLock) { |
| while (true) { |
| if (myCompletionCounter > counterBefore) { |
| break; |
| } |
| try { |
| myCompletionLock.wait(); |
| } |
| catch (InterruptedException e) { |
| // Just stop waiting. |
| break; |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void onSuccess() { |
| super.onSuccess(); |
| onCompletion(); |
| } |
| |
| @Override |
| public void onCancel() { |
| super.onCancel(); |
| onCompletion(); |
| } |
| |
| private void onCompletion() { |
| synchronized (myCompletionLock) { |
| myCompletionCounter++; |
| myCompletionLock.notifyAll(); |
| } |
| } |
| |
| private class CloseListener extends ContentManagerAdapter implements ProjectManagerListener { |
| private ContentManager myContentManager; |
| @Nullable private Content myContent; |
| |
| private boolean myIsApplicationExitingOrProjectClosing; |
| private boolean myUserAcceptedCancel; |
| |
| @Override |
| public void projectOpened(Project project) { |
| } |
| |
| @Override |
| public boolean canCloseProject(Project project) { |
| if (!project.equals(myProject)) { |
| return true; |
| } |
| if (shouldPromptUser()) { |
| myUserAcceptedCancel = askUserToCancelGradleExecution(); |
| if (!myUserAcceptedCancel) { |
| return false; // veto closing |
| } |
| cancel(); |
| return true; |
| } |
| return !myIndicator.isRunning(); |
| } |
| |
| @Override |
| public void projectClosed(Project project) { |
| if (project.equals(myProject) && myContent != null) { |
| myContentManager.removeContent(myContent, true); |
| } |
| } |
| |
| @Override |
| public void projectClosing(Project project) { |
| if (project.equals(myProject)) { |
| myIsApplicationExitingOrProjectClosing = true; |
| } |
| } |
| |
| void setContent(@NotNull ContentManager contentManager, @Nullable Content content) { |
| myContent = content; |
| myContentManager = contentManager; |
| contentManager.addContentManagerListener(this); |
| } |
| |
| @Override |
| public void contentRemoved(ContentManagerEvent event) { |
| if (event.getContent() == myContent) { |
| synchronized (myMessageViewLock) { |
| Project project = getNotNullProject(); |
| if (myErrorTreeView != null && !project.isDisposed()) { |
| Disposer.dispose(myErrorTreeView); |
| myErrorTreeView = null; |
| if (myIndicator.isRunning()) { |
| cancel(); |
| } |
| AppIcon appIcon = AppIcon.getInstance(); |
| if (appIcon.hideProgress(project, APP_ICON_ID)) { |
| //noinspection ConstantConditions |
| appIcon.setErrorBadge(project, null); |
| } |
| } |
| } |
| myContentManager.removeContentManagerListener(this); |
| if (myContent != null) { |
| myContent.release(); |
| } |
| myContent = null; |
| } |
| } |
| |
| @Override |
| public void contentRemoveQuery(ContentManagerEvent event) { |
| if (event.getContent() == myContent && !myIndicator.isCanceled() && shouldPromptUser()) { |
| myUserAcceptedCancel = askUserToCancelGradleExecution(); |
| if (!myUserAcceptedCancel) { |
| event.consume(); // veto closing |
| } |
| } |
| } |
| |
| private boolean shouldPromptUser() { |
| return !myUserAcceptedCancel && !myIsApplicationExitingOrProjectClosing && myIndicator.isRunning(); |
| } |
| |
| private boolean askUserToCancelGradleExecution() { |
| String msg = "Gradle is running. Proceed with Project closing?"; |
| int result = Messages.showYesNoDialog(myProject, msg, GRADLE_RUNNING_MSG_TITLE, Messages.getQuestionIcon()); |
| return result == Messages.YES; |
| } |
| } |
| |
| private class ProgressIndicatorStateDelegate extends AbstractProgressIndicatorExBase { |
| @Override |
| public void cancel() { |
| super.cancel(); |
| closeView(); |
| stopAppIconProgress(); |
| } |
| |
| @Override |
| public void stop() { |
| super.stop(); |
| if (!isCanceled()) { |
| closeView(); |
| } |
| stopAppIconProgress(); |
| } |
| |
| private void stopAppIconProgress() { |
| invokeLaterIfNeeded(new Runnable() { |
| @Override |
| public void run() { |
| AppIcon appIcon = AppIcon.getInstance(); |
| Project project = getNotNullProject(); |
| if (appIcon.hideProgress(project, APP_ICON_ID)) { |
| if (myErrorCount > 0) { |
| appIcon.setErrorBadge(project, String.valueOf(myErrorCount)); |
| appIcon.requestAttention(project, true); |
| } |
| else { |
| appIcon.setOkBadge(project, true); |
| appIcon.requestAttention(project, false); |
| } |
| } |
| } |
| }); |
| } |
| |
| @Override |
| protected void onProgressChange() { |
| prepareMessageView(); |
| } |
| } |
| |
| private class OpenGradleConsole implements Navigatable { |
| @Override |
| public void navigate(boolean requestFocus) { |
| activateGradleConsole(); |
| } |
| |
| @Override |
| public boolean canNavigate() { |
| return true; |
| } |
| |
| @Override |
| public boolean canNavigateToSource() { |
| return false; |
| } |
| } |
| |
| /** |
| * 'Parameter object' pattern for preparing data to be stored at the 'build messages' output tree structure. |
| */ |
| private static class LinkAwareMessageData { |
| /** Target node text split by lines. */ |
| @NotNull final String[] textLines; |
| /** A listener to use for the target text's hyperlinks (if any). Is expected to receives link's href value as an argument. */ |
| @Nullable final Consumer<String> hyperlinkListener; |
| |
| LinkAwareMessageData(@NotNull String[] textLines, @Nullable Consumer<String> hyperlinkListener) { |
| this.textLines = textLines; |
| this.hyperlinkListener = hyperlinkListener; |
| } |
| } |
| } |