From e10f08b8b954fdecb45fe0ba519e1eaf58c4ed02 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Wed, 30 May 2018 10:39:06 +0300 Subject: [PATCH] [platform] refactoring of exception handling/reporting subsystem - streamlines exception flow through the system - allows message and stacktrace editing by user (IDEA-188535) - deprecates methods/classes basically duplicating Logger.error(..) - annotates and comments methods of AbstractMessage --- .../intellij/diagnostic/AbstractMessage.java | 101 +++--- .../diagnostic/AttachmentFactory.java | 75 ++--- .../intellij/diagnostic/DialogAppender.java | 71 ++--- .../diagnostic/GroupedLogMessage.java | 69 ++-- .../src/com/intellij/diagnostic/ITNProxy.java | 158 ++++++--- .../com/intellij/diagnostic/ITNReporter.kt | 50 ++- .../intellij/diagnostic/IdeErrorsDialog.java | 300 ++++++++++++------ .../intellij/diagnostic/IdeMessagePanel.java | 51 +-- .../diagnostic/IdeaReportingEvent.java | 69 ++++ .../diagnostic/LogEventException.java | 29 +- .../com/intellij/diagnostic/LogMessage.java | 105 +++--- .../com/intellij/diagnostic/LogMessageEx.java | 150 ++------- .../com/intellij/diagnostic/MessagePool.java | 21 +- .../intellij/errorreport/bean/ErrorBean.java | 14 +- .../src/com/intellij/idea/IdeaLogger.java | 15 +- .../diagnostic/AttachmentFactoryTest.java | 53 ++-- .../testGuiFramework/impl/GuiTestUtilKt.kt | 18 +- .../openapi/diagnostic/Attachment.java | 126 +++----- .../RuntimeExceptionWithAttachments.java | 37 ++- 19 files changed, 733 insertions(+), 779 deletions(-) create mode 100644 platform/platform-impl/src/com/intellij/diagnostic/IdeaReportingEvent.java diff --git a/platform/platform-impl/src/com/intellij/diagnostic/AbstractMessage.java b/platform/platform-impl/src/com/intellij/diagnostic/AbstractMessage.java index ad8bc18224c4..ccb05acd1086 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/AbstractMessage.java +++ b/platform/platform-impl/src/com/intellij/diagnostic/AbstractMessage.java @@ -1,24 +1,11 @@ -/* - * Copyright 2000-2017 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. - */ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.diagnostic; import com.intellij.openapi.diagnostic.Attachment; import com.intellij.openapi.diagnostic.SubmittedReportInfo; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.Calendar; import java.util.Collections; @@ -26,45 +13,46 @@ import java.util.Date; import java.util.List; public abstract class AbstractMessage { - - private boolean myIsRead = false; - private boolean myIsSubmitting = false; + private final Date myDate = Calendar.getInstance().getTime(); + private boolean myIsRead; + private Runnable myOnReadCallback; + private boolean myIsSubmitting; private SubmittedReportInfo mySubmissionInfo; private String myAdditionalInfo; - private Runnable myOnReadCallback; private Integer myAssigneeId; - private final Date myDate; + public abstract @NotNull Throwable getThrowable(); + public abstract @NotNull String getThrowableText(); - public AbstractMessage() { - myDate = Calendar.getInstance().getTime(); + /** Returns a user message (see {@link LogMessage#createEvent}), if present. */ + public abstract @Nullable String getMessage(); + + /** Returns a (possibly empty) list of all attachments. */ + public @NotNull List getAllAttachments() { + return Collections.emptyList(); } - public abstract String getThrowableText(); - public abstract Throwable getThrowable(); - public abstract String getMessage(); + /** Returns a list of attachments marked by a user to be included into the error report. */ + public @NotNull List getIncludedAttachments() { + return ContainerUtil.filter(getAllAttachments(), Attachment::isIncluded); + } + + public @NotNull Date getDate() { + return myDate; + } public boolean isRead() { return myIsRead; } - public void setRead(boolean aReadFlag) { - myIsRead = aReadFlag; - if (myOnReadCallback != null && aReadFlag) { + public void setRead(boolean isRead) { + myIsRead = isRead; + if (isRead && myOnReadCallback != null) { myOnReadCallback.run(); myOnReadCallback = null; } } - public void setSubmitted(SubmittedReportInfo info) { - myIsSubmitting = false; - mySubmissionInfo = info; - } - - public SubmittedReportInfo getSubmissionInfo() { - return mySubmissionInfo; - } - public void setOnReadCallback(Runnable callback) { myOnReadCallback = callback; } @@ -77,10 +65,19 @@ public abstract class AbstractMessage { myIsSubmitting = isSubmitting; } + public SubmittedReportInfo getSubmissionInfo() { + return mySubmissionInfo; + } + + public void setSubmitted(SubmittedReportInfo info) { + myIsSubmitting = false; + mySubmissionInfo = info; + } + public boolean isSubmitted() { return mySubmissionInfo != null && - (mySubmissionInfo.getStatus() == SubmittedReportInfo.SubmissionStatus.NEW_ISSUE || - mySubmissionInfo.getStatus() == SubmittedReportInfo.SubmissionStatus.DUPLICATE); + (mySubmissionInfo.getStatus() == SubmittedReportInfo.SubmissionStatus.NEW_ISSUE || + mySubmissionInfo.getStatus() == SubmittedReportInfo.SubmissionStatus.DUPLICATE); } public String getAdditionalInfo() { @@ -91,34 +88,16 @@ public abstract class AbstractMessage { myAdditionalInfo = additionalInfo; } - public Date getDate() { - return myDate; - } - - public Integer getAssigneeId() { + public @Nullable Integer getAssigneeId() { return myAssigneeId; } - public void setAssigneeId(Integer assigneeId) { + public void setAssigneeId(@Nullable Integer assigneeId) { myAssigneeId = assigneeId; } - public List getAllAttachments() { - return Collections.emptyList(); - } - - /** - * @return list of attachments which are marked by user to be included into the error report - */ - public List getIncludedAttachments() { - return ContainerUtil.filter(getAllAttachments(), Attachment::isIncluded); - } - - /** - * @deprecated use {@link #getIncludedAttachments()} instead - */ - @NotNull + /** @deprecated use {@link #getIncludedAttachments()} instead (to be removed in IDEA 2020) */ public List getAttachments() { return getIncludedAttachments(); } -} +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/diagnostic/AttachmentFactory.java b/platform/platform-impl/src/com/intellij/diagnostic/AttachmentFactory.java index d19d59a991b1..cf47559e48dc 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/AttachmentFactory.java +++ b/platform/platform-impl/src/com/intellij/diagnostic/AttachmentFactory.java @@ -1,3 +1,4 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.diagnostic; import com.intellij.openapi.diagnostic.Attachment; @@ -7,10 +8,10 @@ import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.CharsetToolkit; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.PathUtilRt; import org.jetbrains.annotations.NotNull; import java.io.*; -import java.text.MessageFormat; /** * @author yole @@ -18,62 +19,54 @@ import java.text.MessageFormat; public class AttachmentFactory { private static final Logger LOG = Logger.getInstance(AttachmentFactory.class); - private static final Long BIG_FILE_THRESHOLD_BYTES = 50 * 1024L; + private static final long BIG_FILE_THRESHOLD_BYTES = 50 * 1024; - private static final String ERROR_MESSAGE_PATTERN = "[[[Can't get file contents: {0}]]]"; - private static final String BIG_FILE_MESSAGE_PATTERN = "[[[File is too big to display: {0}]]]"; + public static Attachment createContext(@NotNull Object start, Object... more) { + StringBuilder builder = new StringBuilder(String.valueOf(start)); + for (Object o : more) builder.append(",").append(o); + return new Attachment("current-context.txt", builder.length() > 0 ? builder.toString() : "(unknown)"); + } - public static Attachment createAttachment(Document document) { + public static Attachment createAttachment(@NotNull Document document) { VirtualFile file = FileDocumentManager.getInstance().getFile(document); return new Attachment(file != null ? file.getPath() : "unknown.txt", document.getText()); } public static Attachment createAttachment(@NotNull VirtualFile file) { - try { - boolean isBinary = file.getFileType().isBinary(); - boolean isBigFile = file.getLength() > BIG_FILE_THRESHOLD_BYTES; - - try (InputStream inputStream = file.getInputStream()) { - return createAttachment(file.getPresentableUrl(), inputStream, isBinary, isBigFile); - } - } catch (IOException e) { - return handleException(e, file.getName(), file.getPath()); + try (InputStream inputStream = file.getInputStream()) { + return createAttachment(file.getPresentableUrl(), inputStream, file.getLength(), file.getFileType().isBinary()); + } + catch (IOException e) { + return handleException(e, file.getPath()); } } public static Attachment createAttachment(@NotNull File file, boolean isBinary) { - try { - try (InputStream inputStream = new FileInputStream(file)) { - return createAttachment(file.getPath(), inputStream, isBinary, file.length() > BIG_FILE_THRESHOLD_BYTES); - } - } catch (IOException e) { - return handleException(e, file.getName(), file.getPath()); + try (InputStream inputStream = new FileInputStream(file)) { + return createAttachment(file.getPath(), inputStream, file.length(), isBinary); + } + catch (IOException e) { + return handleException(e, file.getPath()); } } - private static Attachment handleException(Throwable t, String name, String moniker) { - final String errorMessage = MessageFormat.format(ERROR_MESSAGE_PATTERN, t.getMessage()); - - LOG.warn("Unable to create Attachment from " + moniker + ": " + t.getMessage(), t); - return new Attachment(name, errorMessage); + private static Attachment handleException(Throwable t, String path) { + LOG.warn("failed to create an attachment from " + path, t); + return new Attachment(path, t); } - private static Attachment createAttachment(@NotNull String path, InputStream contentStream, boolean isBinary, boolean isBigFile) { - if (isBigFile) { - return new Attachment(path, contentStream, MessageFormat.format(BIG_FILE_MESSAGE_PATTERN, path)); - } else { - byte[] bytes = getBytes(contentStream); - final String displayText = isBinary ? "[File is binary]" : new String(bytes, CharsetToolkit.UTF8_CHARSET); + private static Attachment createAttachment(String path, InputStream content, long contentLength, boolean isBinary) throws IOException { + if (contentLength >= BIG_FILE_THRESHOLD_BYTES) { + File tempFile = FileUtil.createTempFile("ij-attachment-" + PathUtilRt.getFileName(path) + ".", isBinary ? ".bin" : ".txt", true); + try (OutputStream outputStream = new FileOutputStream(tempFile)) { + FileUtil.copy(content, contentLength, outputStream); + } + return new Attachment(path, tempFile, "[File is too big to display]"); + } + else { + byte[] bytes = FileUtil.loadBytes(content); + String displayText = isBinary ? "[File is binary]" : new String(bytes, CharsetToolkit.UTF8_CHARSET); return new Attachment(path, bytes, displayText); } } - - private static byte[] getBytes(InputStream inputStream) { - try { - return FileUtil.loadBytes(inputStream); - } - catch (IOException e) { - return MessageFormat.format(ERROR_MESSAGE_PATTERN, e.getMessage()).getBytes(CharsetToolkit.UTF8_CHARSET); - } - } -} +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/diagnostic/DialogAppender.java b/platform/platform-impl/src/com/intellij/diagnostic/DialogAppender.java index 060246aa93bc..5c6889874a71 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/DialogAppender.java +++ b/platform/platform-impl/src/com/intellij/diagnostic/DialogAppender.java @@ -1,18 +1,4 @@ -/* - * Copyright 2000-2015 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. - */ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.diagnostic; import com.intellij.idea.IdeaApplication; @@ -22,6 +8,7 @@ import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.ErrorLogger; import com.intellij.openapi.diagnostic.ExceptionWithAttachments; import com.intellij.openapi.diagnostic.IdeaLoggingEvent; +import com.intellij.openapi.diagnostic.RuntimeExceptionWithAttachments; import com.intellij.util.ExceptionUtil; import org.apache.log4j.AppenderSkeleton; import org.apache.log4j.Level; @@ -37,14 +24,14 @@ import java.util.concurrent.atomic.AtomicInteger; * @author Mike */ public class DialogAppender extends AppenderSkeleton { - private static final ErrorLogger DEFAULT_LOGGER = new DefaultIdeaErrorLogger(); + private static final ErrorLogger[] LOGGERS = {new DefaultIdeaErrorLogger()}; private static final int MAX_ASYNC_LOGGING_EVENTS = 5; - private volatile Runnable myDialogRunnable; private final AtomicInteger myPendingAppendCounts = new AtomicInteger(); + private volatile Runnable myDialogRunnable; @Override - protected synchronized void append(@NotNull final LoggingEvent event) { + protected synchronized void append(@NotNull LoggingEvent event) { if (!event.getLevel().isGreaterOrEqual(Level.ERROR) || Main.isCommandLine() || !IdeaApplication.isLoaded()) { @@ -60,7 +47,7 @@ public class DialogAppender extends AppenderSkeleton { //noinspection SSBasedInspection SwingUtilities.invokeLater(() -> { try { - appendToLoggers(event, new ErrorLogger[]{ DEFAULT_LOGGER }); + appendToLoggers(event, LOGGERS); } finally { myPendingAppendCounts.decrementAndGet(); @@ -74,23 +61,23 @@ public class DialogAppender extends AppenderSkeleton { return; } - final IdeaLoggingEvent ideaEvent; - final Object message = event.getMessage(); - if (message instanceof IdeaLoggingEvent) { - ideaEvent = (IdeaLoggingEvent)message; + IdeaLoggingEvent ideaEvent; + Object messageObject = event.getMessage(); + if (messageObject instanceof IdeaLoggingEvent) { + ideaEvent = (IdeaLoggingEvent)messageObject; } else { ThrowableInformation info = event.getThrowableInformation(); - if (info == null) { - return; - } - ideaEvent = extractLoggingEvent(message, info.getThrowable()); + if (info == null || info.getThrowable() == null) return; + ideaEvent = extractLoggingEvent(messageObject, info.getThrowable()); } + for (int i = errorLoggers.length - 1; i >= 0; i--) { - final ErrorLogger logger = errorLoggers[i]; + ErrorLogger logger = errorLoggers[i]; if (!logger.canHandle(ideaEvent)) { continue; } + //noinspection NonAtomicOperationOnVolatileField myDialogRunnable = () -> { try { logger.handle(ideaEvent); @@ -99,8 +86,7 @@ public class DialogAppender extends AppenderSkeleton { myDialogRunnable = null; } }; - - final Application app = ApplicationManager.getApplication(); + Application app = ApplicationManager.getApplication(); if (app == null) { new Thread(myDialogRunnable, "dialog appender logger").start(); } @@ -111,20 +97,27 @@ public class DialogAppender extends AppenderSkeleton { } } - private static IdeaLoggingEvent extractLoggingEvent(Object message, Throwable throwable) { - //noinspection ThrowableResultOfMethodCallIgnored + @SuppressWarnings("deprecation") + private static IdeaLoggingEvent extractLoggingEvent(Object messageObject, Throwable throwable) { Throwable rootCause = ExceptionUtil.getRootCause(throwable); if (rootCause instanceof LogEventException) { return ((LogEventException)rootCause).getLogMessage(); } - String strMessage = message == null ? " " : message.toString(); + String message = null; ExceptionWithAttachments withAttachments = ExceptionUtil.findCause(throwable, ExceptionWithAttachments.class); + if (withAttachments instanceof RuntimeExceptionWithAttachments) { + message = ((RuntimeExceptionWithAttachments)withAttachments).getUserMessage(); + } + if (message == null && messageObject != null) { + message = messageObject.toString(); + } if (withAttachments != null) { - return LogMessageEx.createEvent(strMessage, ExceptionUtil.getThrowableText(throwable), withAttachments.getAttachments()); + return LogMessage.createEvent(throwable, message, withAttachments.getAttachments()); + } + else { + return new IdeaLoggingEvent(message, throwable); } - - return new IdeaLoggingEvent(strMessage, throwable); } @TestOnly @@ -138,7 +131,5 @@ public class DialogAppender extends AppenderSkeleton { } @Override - public void close() { - } -} - + public void close() { } +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/diagnostic/GroupedLogMessage.java b/platform/platform-impl/src/com/intellij/diagnostic/GroupedLogMessage.java index 2f8c8b9d43a8..e4ccc43aeb04 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/GroupedLogMessage.java +++ b/platform/platform-impl/src/com/intellij/diagnostic/GroupedLogMessage.java @@ -1,30 +1,14 @@ -/* - * Copyright 2000-2009 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. - */ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.diagnostic; import com.intellij.openapi.diagnostic.Attachment; -import com.intellij.util.Function; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -import java.util.Collection; import java.util.List; public class GroupedLogMessage extends AbstractMessage { - private final List myMessages; public GroupedLogMessage(List messages) { @@ -35,41 +19,44 @@ public class GroupedLogMessage extends AbstractMessage { return myMessages; } - public String getThrowableText() { + @Override + public @NotNull Throwable getThrowable() { + return myMessages.get(0).getThrowable(); + } + + @Override + public @NotNull String getThrowableText() { StringBuilder result = new StringBuilder(); for (AbstractMessage each : myMessages) { - result.append(each.getThrowableText()).append("\n\n\n"); + if (result.length() > 0) result.append("\n\n\n"); + result.append(each.getThrowableText()); } return result.toString(); } @Override - public void setRead(boolean aReadFlag) { - for (AbstractMessage message : myMessages) { - message.setRead(aReadFlag); - } - super.setRead(aReadFlag); - } - - public Throwable getThrowable() { - return myMessages.get(0).getThrowable(); - } - - public String getMessage() { + public @Nullable String getMessage() { return myMessages.get(0).getMessage(); } @Override - public void setAssigneeId(Integer assigneeId) { + public @NotNull List getAllAttachments() { + return ContainerUtil.concat(getMessages(), message -> message.getAllAttachments()); + } + + @Override + public void setRead(boolean isRead) { + super.setRead(isRead); + for (AbstractMessage message : myMessages) { + message.setRead(isRead); + } + } + + @Override + public void setAssigneeId(@Nullable Integer assigneeId) { + super.setAssigneeId(assigneeId); for (AbstractMessage message : myMessages) { message.setAssigneeId(assigneeId); } - super.setAssigneeId(assigneeId); } - - @NotNull - @Override - public List getAllAttachments() { - return ContainerUtil.concat(getMessages(), message -> message.getAllAttachments()); - } -} +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/diagnostic/ITNProxy.java b/platform/platform-impl/src/com/intellij/diagnostic/ITNProxy.java index d24c89b65ba7..2afbb31e5334 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/ITNProxy.java +++ b/platform/platform-impl/src/com/intellij/diagnostic/ITNProxy.java @@ -1,26 +1,26 @@ // Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.diagnostic; -import com.intellij.errorreport.bean.ErrorBean; import com.intellij.errorreport.error.InternalEAPException; import com.intellij.errorreport.error.NoSuchEAPUserException; import com.intellij.errorreport.error.UpdateAvailableException; import com.intellij.idea.IdeaLogger; -import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ApplicationNamesInfo; import com.intellij.openapi.application.ex.ApplicationInfoEx; import com.intellij.openapi.diagnostic.Attachment; +import com.intellij.openapi.diagnostic.IdeaLoggingEvent; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.Task; import com.intellij.openapi.project.Project; import com.intellij.openapi.updateSettings.impl.UpdateSettings; +import com.intellij.openapi.util.AtomicNotNullLazyValue; import com.intellij.openapi.util.BuildNumber; +import com.intellij.openapi.util.NotNullLazyValue; import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.security.CompositeX509TrustManager; -import com.intellij.util.Consumer; import com.intellij.util.io.HttpRequests; import com.intellij.util.net.NetUtils; import com.intellij.util.net.ssl.CertificateUtil; @@ -44,9 +44,8 @@ import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.List; +import java.util.*; +import java.util.function.Consumer; import java.util.function.IntConsumer; /** @@ -60,6 +59,43 @@ class ITNProxy { private static final String NEW_THREAD_POST_URL = "https://ea-report.jetbrains.com/trackerRpc/idea/createScr"; private static final String NEW_THREAD_VIEW_URL = "https://ea.jetbrains.com/browser/ea_reports/"; + private static final NotNullLazyValue> TEMPLATE = AtomicNotNullLazyValue.createValue(() -> { + Map template = new LinkedHashMap<>(); + + template.put("protocol.version", "1"); + template.put("os.name", SystemInfo.OS_NAME); + template.put("java.version", SystemInfo.JAVA_VERSION); + template.put("java.vm.vendor", SystemInfo.JAVA_VENDOR); + + ApplicationInfoEx appInfo = ApplicationInfoEx.getInstanceEx(); + ApplicationNamesInfo namesInfo = ApplicationNamesInfo.getInstance(); + BuildNumber build = appInfo.getBuild(); + String buildNumberWithAllDetails = build.asString(); + if (StringUtil.startsWith(buildNumberWithAllDetails, build.getProductCode() + "-")) { + buildNumberWithAllDetails = buildNumberWithAllDetails.substring(build.getProductCode().length() + 1); + } + + template.put("app.name", namesInfo.getProductName()); + template.put("app.name.full", namesInfo.getFullProductName()); + template.put("app.name.version", appInfo.getVersionName()); + template.put("app.eap", Boolean.toString(appInfo.isEAP())); + template.put("app.internal", Boolean.toString(ApplicationManager.getApplication().isInternal())); + template.put("app.build", appInfo.getApiVersion()); + template.put("app.version.major", appInfo.getMajorVersion()); + template.put("app.version.minor", appInfo.getMinorVersion()); + template.put("app.build.date", format(appInfo.getBuildDate())); + template.put("app.build.date.release", format(appInfo.getMajorReleaseBuildDate())); + template.put("app.compilation.timestamp", IdeaLogger.getOurCompilationTimestamp()); + template.put("app.product.code", build.getProductCode()); + template.put("app.build.number", buildNumberWithAllDetails); + + return template; + }); + + private static @Nullable String format(@Nullable Calendar calendar) { + return calendar == null ? null : Long.toString(calendar.getTime().getTime()); + } + static @NotNull List fetchDevelopers(@NotNull ProgressIndicator indicator) throws IOException { return HttpRequests.request(DEVELOPERS_LIST_URL).connect(request -> { List developers = new ArrayList<>(); @@ -79,6 +115,24 @@ class ITNProxy { }); } + static class ErrorBean { + final IdeaLoggingEvent event; + final String comment; + final String pluginName; + final String pluginVersion; + final String lastActionId; + final int previousException; + + ErrorBean(IdeaLoggingEvent event, String comment, String pluginName, String pluginVersion, String lastActionId, int previousException) { + this.event = event; + this.comment = comment; + this.pluginName = pluginName; + this.pluginVersion = pluginVersion; + this.lastActionId = lastActionId; + this.previousException = previousException; + } + } + static void sendError(@Nullable Project project, @Nullable String login, @Nullable String password, @@ -101,7 +155,7 @@ class ITNProxy { onSuccess.accept(postNewThread(_login, _password, error)); } catch (Exception ex) { - onError.consume(ex); + onError.accept(ex); } } }.queue(); @@ -147,57 +201,57 @@ class ITNProxy { private static byte[] createRequest(String login, String password, ErrorBean error) throws UnsupportedEncodingException { StringBuilder builder = new StringBuilder(8192); - append(builder, "protocol.version", "1"); + for (Map.Entry entry : TEMPLATE.getValue().entrySet()) { + append(builder, entry.getKey(), entry.getValue()); + } append(builder, "user.login", login); append(builder, "user.password", password); - append(builder, "os.name", SystemInfo.OS_NAME); - append(builder, "java.version", SystemInfo.JAVA_VERSION); - append(builder, "java.vm.vendor", SystemInfo.JAVA_VENDOR); - - ApplicationInfoEx appInfo = ApplicationInfoEx.getInstanceEx(); - ApplicationNamesInfo namesInfo = ApplicationNamesInfo.getInstance(); - Application application = ApplicationManager.getApplication(); - append(builder, "app.name", namesInfo.getProductName()); - append(builder, "app.name.full", namesInfo.getFullProductName()); - append(builder, "app.name.version", appInfo.getVersionName()); - append(builder, "app.eap", Boolean.toString(appInfo.isEAP())); - append(builder, "app.internal", Boolean.toString(application.isInternal())); - append(builder, "app.build", appInfo.getApiVersion()); - append(builder, "app.version.major", appInfo.getMajorVersion()); - append(builder, "app.version.minor", appInfo.getMinorVersion()); - append(builder, "app.build.date", format(appInfo.getBuildDate())); - append(builder, "app.build.date.release", format(appInfo.getMajorReleaseBuildDate())); - append(builder, "app.compilation.timestamp", IdeaLogger.getOurCompilationTimestamp()); - - BuildNumber build = appInfo.getBuild(); - String buildNumberWithAllDetails = build.asString(); - append(builder, "app.product.code", build.getProductCode()); - if (StringUtil.startsWith(buildNumberWithAllDetails, build.getProductCode() + "-")) { - buildNumberWithAllDetails = buildNumberWithAllDetails.substring(build.getProductCode().length() + 1); - } - append(builder, "app.build.number", buildNumberWithAllDetails); - UpdateSettings updateSettings = UpdateSettings.getInstance(); append(builder, "update.channel.status", updateSettings.getSelectedChannelStatus().getCode()); append(builder, "update.ignored.builds", StringUtil.join(updateSettings.getIgnoredBuildNumbers(), ",")); - append(builder, "plugin.name", error.getPluginName()); - append(builder, "plugin.version", error.getPluginVersion()); + append(builder, "plugin.name", error.pluginName); + append(builder, "plugin.version", error.pluginVersion); + append(builder, "last.action", error.lastActionId); + if (error.previousException > 0) { + append(builder, "previous.exception", Integer.toString(error.previousException)); + } - append(builder, "last.action", error.getLastAction()); - append(builder, "previous.exception", error.getPreviousException() == null ? null : Integer.toString(error.getPreviousException())); + String message = error.event.getMessage(); + String stacktrace = error.event.getThrowableText(); + boolean redacted = false; + if (error.event instanceof IdeaReportingEvent) { + String originalMessage = ((IdeaReportingEvent)error.event).getOriginalMessage(); + String originalStacktrace = ((IdeaReportingEvent)error.event).getOriginalThrowableText(); + boolean messagesDiffer = !Objects.equals(message, originalMessage); + boolean tracesDiffer = !Objects.equals(stacktrace, originalStacktrace); + if (messagesDiffer || tracesDiffer) { + String summary = ""; + if (messagesDiffer) summary += "*** message was redacted (" + diff(originalMessage, message) + ")\n"; + if (tracesDiffer) summary += "*** stacktrace was redacted (" + diff(originalStacktrace, stacktrace) + ")\n"; + message = message != null ? summary + '\n' + message : summary.trim(); + redacted = true; + } + } + append(builder, "error.message", message); + append(builder, "error.stacktrace", stacktrace); + append(builder, "error.description", error.comment); + if (redacted) { + append(builder, "error.redacted", Boolean.toString(true)); + } - append(builder, "error.message", error.getMessage()); - append(builder, "error.stacktrace", error.getStackTrace()); - append(builder, "error.description", error.getDescription()); - - append(builder, "assignee.id", error.getAssigneeId() == null ? null : Integer.toString(error.getAssigneeId())); - - for (Attachment attachment : error.getAttachments()) { - append(builder, "attachment.name", attachment.getName()); - append(builder, "attachment.value", attachment.getEncodedBytes()); + Object eventData = error.event.getData(); + if (eventData instanceof AbstractMessage) { + AbstractMessage messageObj = (AbstractMessage)eventData; + for (Attachment attachment : messageObj.getIncludedAttachments()) { + append(builder, "attachment.name", attachment.getName()); + append(builder, "attachment.value", attachment.getEncodedBytes()); + } + if (messageObj.getAssigneeId() != null) { + append(builder, "assignee.id", Integer.toString(messageObj.getAssigneeId())); + } } return builder.toString().getBytes(StandardCharsets.UTF_8); @@ -209,8 +263,12 @@ class ITNProxy { builder.append(key).append('=').append(URLEncoder.encode(value, StandardCharsets.UTF_8.name())); } - private static @Nullable String format(@Nullable Calendar calendar) { - return calendar == null ? null : Long.toString(calendar.getTime().getTime()); + private static String diff(String original, String redacted) { + return "original:" + wc(original) + " submitted:" + wc(redacted); + } + + private static String wc(String s) { + return s == null ? "-" : StringUtil.splitByLines(s).length + "/" + s.split("[^\\w']+").length + "/" + s.length(); } private static HttpURLConnection post(URL url, byte[] bytes) throws IOException { diff --git a/platform/platform-impl/src/com/intellij/diagnostic/ITNReporter.kt b/platform/platform-impl/src/com/intellij/diagnostic/ITNReporter.kt index b749fd595036..44d6091d890e 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/ITNReporter.kt +++ b/platform/platform-impl/src/com/intellij/diagnostic/ITNReporter.kt @@ -4,12 +4,11 @@ package com.intellij.diagnostic import com.intellij.CommonBundle import com.intellij.credentialStore.hasOnlyUserName import com.intellij.credentialStore.isFulfilled -import com.intellij.errorreport.bean.ErrorBean +import com.intellij.diagnostic.ITNProxy.ErrorBean import com.intellij.errorreport.error.InternalEAPException import com.intellij.errorreport.error.NoSuchEAPUserException import com.intellij.errorreport.error.UpdateAvailableException import com.intellij.ide.DataManager -import com.intellij.ide.plugins.PluginManager import com.intellij.idea.IdeaLogger import com.intellij.notification.NotificationListener import com.intellij.notification.NotificationType @@ -28,7 +27,8 @@ import java.awt.Component import java.lang.Exception import javax.swing.Icon -private var previousExceptionThreadId = 0 +private const val INTERVAL = 10 * 60 * 1000L // an interval between exceptions to form a chain, ms +@Volatile private var previousReport: Pair? = null // (timestamp, threadID) of last reported exception open class ITNReporter : ErrorReportSubmitter() { override fun getReportActionText(): String = DiagnosticBundle.message("error.report.to.jetbrains.action") @@ -43,21 +43,18 @@ open class ITNReporter : ErrorReportSubmitter() { consumer: Consumer): Boolean { val event = events[0] - val errorBean = ErrorBean(event.throwable, IdeaLogger.ourLastActionId) - errorBean.message = event.message - errorBean.description = additionalInfo + val pluginInfo = IdeErrorsDialog.getPluginInfo(event) - setPluginInfo(event, errorBean) + val lastActionId = IdeaLogger.ourLastActionId - val data = event.data - if (data is AbstractMessage) { - errorBean.assigneeId = data.assigneeId - errorBean.attachments = data.includedAttachments + var previousReportId = -1 + val previousException = previousReport + val eventData = event.data + if (previousException != null && eventData is AbstractMessage && eventData.date.time - previousException.first in 0..INTERVAL) { + previousReportId = previousException.second } - if (previousExceptionThreadId != 0) { - errorBean.previousException = previousExceptionThreadId - } + val errorBean = ErrorBean(event, additionalInfo, pluginInfo?.first, pluginInfo?.second, lastActionId, previousReportId) val project = CommonDataKeys.PROJECT.getData(DataManager.getInstance().getDataContext(parentComponent)) @@ -70,17 +67,13 @@ open class ITNReporter : ErrorReportSubmitter() { open fun showErrorInRelease(event: IdeaLoggingEvent): Boolean = false } -fun setPluginInfo(event: IdeaLoggingEvent, errorBean: ErrorBean) { - val t = event.throwable - if (t != null) { - val pluginId = IdeErrorsDialog.findPluginId(t) - if (pluginId != null) { - val ideaPluginDescriptor = PluginManager.getPlugin(pluginId) - if (ideaPluginDescriptor != null && (!ideaPluginDescriptor.isBundled || ideaPluginDescriptor.allowBundledUpdate())) { - errorBean.pluginName = ideaPluginDescriptor.name - errorBean.pluginVersion = ideaPluginDescriptor.version - } - } +/** @deprecated use [IdeErrorsDialog.getPluginInfo] (to be removed in IDEA 2019) */ +@Suppress("unused", "DEPRECATION") +fun setPluginInfo(event: IdeaLoggingEvent, errorBean: com.intellij.errorreport.bean.ErrorBean) { + val pluginInfo = IdeErrorsDialog.getPluginInfo(event) + if (pluginInfo != null) { + errorBean.pluginName = pluginInfo.first + errorBean.pluginVersion = pluginInfo.second } } @@ -104,13 +97,14 @@ private fun submit(errorBean: ErrorBean, parentComponent: Component, callback: C } ITNProxy.sendError(project, credentials?.userName, credentials?.getPasswordAsString(), errorBean, - { threadId -> onSuccess(threadId, callback, project) }, + { threadId -> onSuccess(threadId, errorBean.event.data, callback, project) }, { e -> onError(e, errorBean, parentComponent, callback, project) }) return true } -private fun onSuccess(threadId: Int, callback: Consumer, project: Project?) { - previousExceptionThreadId = threadId +private fun onSuccess(threadId: Int, eventData: Any?, callback: Consumer, project: Project?) { + previousReport = if (eventData is AbstractMessage) eventData.date.time to threadId else null + val linkText = threadId.toString() val reportInfo = SubmittedReportInfo(ITNProxy.getBrowseUrl(threadId), linkText, SubmittedReportInfo.SubmissionStatus.NEW_ISSUE) callback.consume(reportInfo) diff --git a/platform/platform-impl/src/com/intellij/diagnostic/IdeErrorsDialog.java b/platform/platform-impl/src/com/intellij/diagnostic/IdeErrorsDialog.java index 210b815aca15..4af1335cb1d0 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/IdeErrorsDialog.java +++ b/platform/platform-impl/src/com/intellij/diagnostic/IdeErrorsDialog.java @@ -32,6 +32,7 @@ import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.Condition; +import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.wm.IdeFrame; @@ -39,6 +40,7 @@ import com.intellij.openapi.wm.WindowManager; import com.intellij.ui.*; import com.intellij.ui.components.JBLabel; import com.intellij.ui.components.JBScrollPane; +import com.intellij.util.ExceptionUtil; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.text.DateFormatUtil; import com.intellij.util.ui.JBUI; @@ -55,10 +57,13 @@ import java.awt.event.ActionEvent; import java.awt.event.ItemEvent; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.text.DateFormat; +import java.text.SimpleDateFormat; import java.util.*; import java.util.List; import java.util.zip.CRC32; +import static com.intellij.openapi.util.Pair.pair; import static java.awt.GridBagConstraints.*; public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListener, DataProvider { @@ -66,6 +71,9 @@ public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListene public static final DataKey CURRENT_TRACE_KEY = DataKey.create("current_stack_trace_key"); + private static final String STACKTRACE_ATTACHMENT = "stacktrace.txt"; + private static final String INDUCED_STACKTRACES_ATTACHMENT = "induced.txt"; + private static final DateFormat TIMESTAMP_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); private static final String ACCEPTED_NOTICES_KEY = "exception.accepted.notices"; private static final String ACCEPTED_NOTICES_SEPARATOR = ":"; private static List ourDevelopersList = Collections.emptyList(); @@ -74,8 +82,7 @@ public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListene private final Project myProject; private final boolean myInternalMode; private final Set myAcceptedNotices; - private final List myRawMessages = new ArrayList<>(); - private final List> myMergedMessages = new ArrayList<>(); + private final List myMessageClusters = new ArrayList<>(); // exceptions with the same stacktrace private int myIndex; private JLabel myCountLabel; @@ -144,11 +151,12 @@ public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListene } private void selectMessage(@Nullable LogMessage defaultMessage) { - for (int i = 0; i < myMergedMessages.size(); i++) { - AbstractMessage message = myMergedMessages.get(i).get(0); - if (defaultMessage != null && message == defaultMessage || defaultMessage == null && !message.isRead()) { - myIndex = i; - return; + for (int i = 0; i < myMessageClusters.size(); i++) { + for (AbstractMessage message : myMessageClusters.get(i).messages) { + if (defaultMessage != null && message == defaultMessage || defaultMessage == null && !message.isRead()) { + myIndex = i; + return; + } } } } @@ -210,12 +218,17 @@ public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListene int index = myAttachmentsList.getSelectedIndex(); if (index < 0) { myAttachmentArea.setText(""); + myAttachmentArea.setEditable(false); } else if (index == 0) { - myAttachmentArea.setText(getDetailsText(selectedMessage())); + MessageCluster cluster = selectedCluster(); + AbstractMessage message = cluster.first; + myAttachmentArea.setText(cluster.detailsText); + myAttachmentArea.setEditable(!(message.isSubmitted() || message.isSubmitting())); } else { myAttachmentArea.setText(selectedMessage().getAllAttachments().get(index - 1).getDisplayText()); + myAttachmentArea.setEditable(false); } myAttachmentArea.setCaretPosition(0); }); @@ -227,7 +240,16 @@ public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListene myAttachmentArea = new JTextArea(); myAttachmentArea.setMargin(JBUI.insets(2)); - myAttachmentArea.setEditable(false); + myAttachmentArea.getDocument().addDocumentListener(new DocumentAdapter() { + @Override + protected void textChanged(DocumentEvent e) { + if (myAttachmentsList.getSelectedIndex() == 0) { + String detailsText = myAttachmentArea.getText(); + selectedCluster().detailsText = detailsText; + setOKActionEnabled(isOKActionEnabled() && !StringUtil.isEmptyOrSpaces(detailsText)); + } + } + }); if (myInternalMode) { myAssigneeCombo = new ComboBox<>(); @@ -341,8 +363,8 @@ public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListene @Override public void doOKAction() { if (getOKAction().isEnabled()) { - boolean closeDialog = myMergedMessages.size() == 1; - boolean reportingStarted = reportMessage(selectedMessage(), closeDialog); + boolean closeDialog = myMessageClusters.size() == 1; + boolean reportingStarted = reportMessage(selectedCluster(), closeDialog); if (!closeDialog) { updateControls(); } @@ -358,32 +380,58 @@ public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListene super.dispose(); } + private MessageCluster selectedCluster() { + return myMessageClusters.get(myIndex); + } + private AbstractMessage selectedMessage() { - return myMergedMessages.get(myIndex).get(0); + return selectedCluster().first; } private void updateMessages() { - myRawMessages.clear(); - myRawMessages.addAll(myMessagePool.getFatalErrors(true, true)); - - Map> messageGroups = new LinkedHashMap<>(); - for (AbstractMessage message : myRawMessages) { + List rawMessages = myMessagePool.getFatalErrors(true, true); + Map clusters = new LinkedHashMap<>(); + for (AbstractMessage raw : rawMessages) { + AbstractMessage message = raw instanceof GroupedLogMessage ? pack((GroupedLogMessage)raw) : raw; CRC32 digest = new CRC32(); - digest.update(StringUtil.getThrowableText(message.getThrowable()).getBytes(StandardCharsets.UTF_8)); - messageGroups.computeIfAbsent(digest.getValue(), k -> new ArrayList<>()).add(message); + digest.update(ExceptionUtil.getThrowableText(message.getThrowable()).getBytes(StandardCharsets.UTF_8)); + clusters.computeIfAbsent(digest.getValue(), k -> new MessageCluster(message)).messages.add(message); } - myMergedMessages.clear(); - myMergedMessages.addAll(messageGroups.values()); + myMessageClusters.clear(); + myMessageClusters.addAll(clusters.values()); + } + + private static AbstractMessage pack(GroupedLogMessage message) { + AbstractMessage mainCause = message.getMessages().get(0); + + List attachments = new ArrayList<>(); + attachments.add(null); + StringBuilder stacktraces = new StringBuilder(); + for (AbstractMessage each : message.getMessages()) { + attachments.addAll(each.getAllAttachments()); + if (each != mainCause) { + if (stacktraces.length() > 0) stacktraces.append("\n\n\n"); + stacktraces.append(TIMESTAMP_FORMAT.format(each.getDate())).append('\n'); + if (!StringUtil.isEmptyOrSpaces(each.getMessage())) stacktraces.append(each.getMessage()).append('\n'); + stacktraces.append(each.getThrowableText()); + } + } + attachments.set(0, new Attachment(INDUCED_STACKTRACES_ATTACHMENT, stacktraces.toString())); + + return new LogMessage(mainCause.getThrowable(), mainCause.getMessage(), attachments); } private void updateControls() { - myMergedMessages.get(myIndex).forEach(m -> m.setRead(true)); - AbstractMessage message = selectedMessage(); - ErrorReportSubmitter submitter = getSubmitter(message.getThrowable()); + MessageCluster cluster = selectedCluster(); + AbstractMessage message = cluster.first; + ErrorReportSubmitter submitter = cluster.submitter; + + cluster.messages.forEach(m -> m.setRead(true)); + boolean unsent = !(message.isSubmitted() || message.isSubmitting()); boolean canReport = unsent && submitter != null; - updateLabels(message, submitter); + updateLabels(cluster); updateDetails(message, canReport); @@ -398,19 +446,22 @@ public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListene setOKButtonTooltip(submitter != null ? null : DiagnosticBundle.message("error.report.impossible.tooltip")); } - private void updateLabels(AbstractMessage message, @Nullable ErrorReportSubmitter submitter) { - myCountLabel.setText(DiagnosticBundle.message("error.list.message.index.count", myIndex + 1, myMergedMessages.size())); + private void updateLabels(MessageCluster cluster) { + AbstractMessage message = cluster.first; + + myCountLabel.setText(DiagnosticBundle.message("error.list.message.index.count", myIndex + 1, myMessageClusters.size())); Throwable t = message.getThrowable(); if (t instanceof MessagePool.TooManyErrorsException) { - myInfoLabel.setText(message.getMessage()); + myInfoLabel.setText(t.getMessage()); myDisableLink.setVisible(false); myForeignPluginWarningLabel.setVisible(false); + myNoticePanel.setVisible(false); return; } - PluginId pluginId = findPluginId(t); - IdeaPluginDescriptor plugin = PluginManager.getPlugin(pluginId); + PluginId pluginId = cluster.pluginId; + IdeaPluginDescriptor plugin = cluster.plugin; StringBuilder info = new StringBuilder(); String url = null; @@ -426,7 +477,7 @@ public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListene } String date = DateFormatUtil.formatPrettyDateTime(message.getDate()); - int count = myMergedMessages.get(myIndex).size(); + int count = cluster.messages.size(); info.append(' ').append(DiagnosticBundle.message("error.list.message.info", date, count)); if (message.isSubmitted()) { @@ -445,6 +496,7 @@ public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListene myDisableLink.setVisible(pluginId != null && !ApplicationInfoEx.getInstanceEx().isEssentialPlugin(pluginId.getIdString())); + ErrorReportSubmitter submitter = cluster.submitter; if (submitter == null && plugin != null && !PluginManagerMain.isDevelopedByJetBrains(plugin)) { myForeignPluginWarningLabel.setVisible(true); String vendor = plugin.getVendor(); @@ -491,7 +543,7 @@ public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListene myCommentArea.setEditable(canReport); myAttachmentsList.clear(); - myAttachmentsList.addItem("stacktrace.txt", true); + myAttachmentsList.addItem(STACKTRACE_ATTACHMENT, true); for (Attachment attachment : message.getAllAttachments()) { myAttachmentsList.addItem(attachment.getName(), myInternalMode || attachment.isIncluded()); } @@ -533,9 +585,10 @@ public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListene } } - private boolean reportMessage(AbstractMessage message, boolean dialogClosed) { - ErrorReportSubmitter submitter = getSubmitter(message.getThrowable()); + private boolean reportMessage(MessageCluster cluster, boolean dialogClosed) { + ErrorReportSubmitter submitter = cluster.submitter; if (submitter == null) return false; + AbstractMessage message = cluster.first; message.setSubmitting(true); @@ -547,13 +600,8 @@ public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListene } } - IdeaLoggingEvent[] events; - if (message instanceof GroupedLogMessage) { - events = ((GroupedLogMessage)message).getMessages().stream().map(IdeErrorsDialog::getEvent).toArray(IdeaLoggingEvent[]::new); - } - else { - events = new IdeaLoggingEvent[]{getEvent(message)}; - } + Pair pair = cluster.decouple(); + IdeaLoggingEvent[] events = {new IdeaReportingEvent(message, pair.first, pair.second, cluster.plugin)}; Container parentComponent = getRootPane(); if (dialogClosed) { @@ -564,64 +612,52 @@ public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListene return submitter.submit(events, message.getAdditionalInfo(), parentComponent, reportInfo -> { message.setSubmitting(false); message.setSubmitted(reportInfo); - ApplicationManager.getApplication().invokeLater(() -> updateOnSubmit()); + UIUtil.invokeLaterIfNeeded(() -> updateOnSubmit()); }); } - private static IdeaLoggingEvent getEvent(AbstractMessage message) { - if (message instanceof LogMessageEx) { - return ((LogMessageEx)message).toEvent(); - } - else { - return new IdeaLoggingEvent(message.getMessage(), message.getThrowable(), message); - } - } - private void disablePlugin() { - PluginId pluginId = findPluginId(selectedMessage().getThrowable()); - if (pluginId != null) { - IdeaPluginDescriptor plugin = PluginManager.getPlugin(pluginId); - if (plugin != null) { - Ref hasDependants = new Ref<>(false); - PluginManagerCore.checkDependants(plugin, PluginManager::getPlugin, dependantId -> { - if (PluginManagerCore.CORE_PLUGIN_ID.equals(dependantId.getIdString())) { - return true; - } - else { - hasDependants.set(true); - return false; - } - }); - boolean canRestart = ApplicationManager.getApplication().isRestartCapable(); - - String message = - "" + - DiagnosticBundle.message("error.dialog.disable.prompt", plugin.getName()) + "
" + - DiagnosticBundle.message(hasDependants.get() ? "error.dialog.disable.prompt.deps" : "error.dialog.disable.prompt.lone") + "

" + - DiagnosticBundle.message(canRestart ? "error.dialog.disable.plugin.can.restart" : "error.dialog.disable.plugin.no.restart") + - ""; - String title = DiagnosticBundle.message("error.dialog.disable.plugin.title"); - String disable = DiagnosticBundle.message("error.dialog.disable.plugin.action.disable"); - String cancel = IdeBundle.message("button.cancel"); - - boolean doDisable, doRestart; - if (canRestart) { - String restart = DiagnosticBundle.message("error.dialog.disable.plugin.action.disableAndRestart"); - int result = Messages.showYesNoCancelDialog(myProject, message, title, disable, restart, cancel, Messages.getQuestionIcon()); - doDisable = result == Messages.YES || result == Messages.NO; - doRestart = result == Messages.NO; + IdeaPluginDescriptor plugin = selectedCluster().plugin; + if (plugin != null) { + Ref hasDependants = new Ref<>(false); + PluginManagerCore.checkDependants(plugin, PluginManager::getPlugin, dependantId -> { + if (PluginManagerCore.CORE_PLUGIN_ID.equals(dependantId.getIdString())) { + return true; } else { - int result = Messages.showYesNoDialog(myProject, message, title, disable, cancel, Messages.getQuestionIcon()); - doDisable = result == Messages.YES; - doRestart = false; + hasDependants.set(true); + return false; } + }); + boolean canRestart = ApplicationManager.getApplication().isRestartCapable(); - if (doDisable) { - PluginManagerCore.disablePlugin(pluginId.getIdString()); - if (doRestart) { - ApplicationManager.getApplication().restart(); - } + String message = + "" + + DiagnosticBundle.message("error.dialog.disable.prompt", plugin.getName()) + "
" + + DiagnosticBundle.message(hasDependants.get() ? "error.dialog.disable.prompt.deps" : "error.dialog.disable.prompt.lone") + "

" + + DiagnosticBundle.message(canRestart ? "error.dialog.disable.plugin.can.restart" : "error.dialog.disable.plugin.no.restart") + + ""; + String title = DiagnosticBundle.message("error.dialog.disable.plugin.title"); + String disable = DiagnosticBundle.message("error.dialog.disable.plugin.action.disable"); + String cancel = IdeBundle.message("button.cancel"); + + boolean doDisable, doRestart; + if (canRestart) { + String restart = DiagnosticBundle.message("error.dialog.disable.plugin.action.disableAndRestart"); + int result = Messages.showYesNoCancelDialog(myProject, message, title, disable, restart, cancel, Messages.getQuestionIcon()); + doDisable = result == Messages.YES || result == Messages.NO; + doRestart = result == Messages.NO; + } + else { + int result = Messages.showYesNoDialog(myProject, message, title, disable, cancel, Messages.getQuestionIcon()); + doDisable = result == Messages.YES; + doRestart = false; + } + + if (doDisable) { + PluginManagerCore.disablePlugin(plugin.getPluginId().getIdString()); + if (doRestart) { + ApplicationManager.getApplication().restart(); } } } @@ -667,7 +703,7 @@ public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListene @Override public void update(AnActionEvent e) { - e.getPresentation().setEnabled(myIndex < myMergedMessages.size() - 1); + e.getPresentation().setEnabled(myIndex < myMessageClusters.size() - 1); } @Override @@ -762,20 +798,72 @@ public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListene @Override public Object getData(String dataId) { - return CURRENT_TRACE_KEY.is(dataId) ? getDetailsText(selectedMessage()) : null; + return CURRENT_TRACE_KEY.is(dataId) ? selectedMessage().getThrowableText() : null; } /* helpers */ - private static String getDetailsText(AbstractMessage message) { - Throwable t = message.getThrowable(); - return t instanceof MessagePool.TooManyErrorsException ? message.getMessage() : - t instanceof NullPointerException ? message.getThrowableText() : - message.getMessage() + "\n" + message.getThrowableText(); + private static class MessageCluster { + private final AbstractMessage first; + private final @Nullable PluginId pluginId; + private final @Nullable IdeaPluginDescriptor plugin; + private final @Nullable ErrorReportSubmitter submitter; + private String detailsText; + private final List messages = new ArrayList<>(); + + private MessageCluster(AbstractMessage message) { + first = message; + pluginId = findPluginId(message.getThrowable()); + plugin = PluginManager.getPlugin(pluginId); + submitter = getSubmitter(message.getThrowable(), pluginId, plugin); + detailsText = detailsText(); + } + + private String detailsText() { + AbstractMessage message = first; + if (message instanceof GroupedLogMessage) { + message = ((GroupedLogMessage)message).getMessages().get(0); + } + + Throwable t = message.getThrowable(); + if (t instanceof MessagePool.TooManyErrorsException) { + return t.getMessage(); + } + + String userMessage = message.getMessage(), stacktrace = message.getThrowableText(); + return StringUtil.isEmptyOrSpaces(userMessage) ? stacktrace : userMessage + "\n\n" + stacktrace; + } + + private Pair decouple() { + @SuppressWarnings("ThrowableNotThrown") String className = first.getThrowable().getClass().getName(); + int p = detailsText.indexOf(className); + if (p == 0) { + return pair(null, detailsText); + } + else if (p > 0 && detailsText.charAt(p - 1) == '\n') { + return pair(detailsText.substring(0, p).trim(), detailsText.substring(p)); + } + else { + return pair("*** exception class was changed or removed", detailsText); + } + } } - @Nullable - public static PluginId findPluginId(@NotNull Throwable t) { + public static @Nullable Pair getPluginInfo(@NotNull IdeaLoggingEvent event) { + IdeaPluginDescriptor plugin = null; + if (event instanceof IdeaReportingEvent) { + plugin = ((IdeaReportingEvent)event).getPlugin(); + } + else { + Throwable t = event.getThrowable(); + if (t != null) { + plugin = PluginManager.getPlugin(findPluginId(t)); + } + } + return plugin != null && (!plugin.isBundled() || plugin.allowBundledUpdate()) ? pair(plugin.getName(), plugin.getVersion()) : null; + } + + public static @Nullable PluginId findPluginId(@NotNull Throwable t) { if (t instanceof PluginException) { return ((PluginException)t).getPluginId(); } @@ -859,8 +947,13 @@ public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListene } } - @Nullable - static ErrorReportSubmitter getSubmitter(@NotNull Throwable t) { + static @Nullable ErrorReportSubmitter getSubmitter(@NotNull Throwable t) { + PluginId pluginId = findPluginId(t); + IdeaPluginDescriptor plugin = PluginManager.getPlugin(pluginId); + return getSubmitter(t, pluginId, plugin); + } + + private static ErrorReportSubmitter getSubmitter(Throwable t, PluginId pluginId, IdeaPluginDescriptor plugin) { if (t instanceof MessagePool.TooManyErrorsException || t instanceof AbstractMethodError) { return null; } @@ -873,9 +966,6 @@ public class IdeErrorsDialog extends DialogWrapper implements MessagePoolListene return null; } - PluginId pluginId = findPluginId(t); - IdeaPluginDescriptor plugin = PluginManager.getPlugin(pluginId); - if (plugin != null) { for (ErrorReportSubmitter reporter : reporters) { PluginDescriptor descriptor = reporter.getPluginDescriptor(); diff --git a/platform/platform-impl/src/com/intellij/diagnostic/IdeMessagePanel.java b/platform/platform-impl/src/com/intellij/diagnostic/IdeMessagePanel.java index 896c8a631a46..5e84c0a49d62 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/IdeMessagePanel.java +++ b/platform/platform-impl/src/com/intellij/diagnostic/IdeMessagePanel.java @@ -182,10 +182,7 @@ public class IdeMessagePanel extends JPanel implements MessagePoolListener, Icon else if (state == IdeErrorsIcon.State.UnreadErrors && !myNotificationPopupAlreadyShown) { Project project = myFrame == null ? null : myFrame.getProject(); if (project != null) { - ApplicationManager.getApplication().invokeLater(() -> { - String notificationText = getNotificationText(myMessagePool.getFatalErrors(false, false)); - showErrorNotification(notificationText, project); - }, project.getDisposed()); + ApplicationManager.getApplication().invokeLater(() -> showErrorNotification(project), project.getDisposed()); myNotificationPopupAlreadyShown = true; } } @@ -194,20 +191,15 @@ public class IdeMessagePanel extends JPanel implements MessagePoolListener, Icon private static final String ERROR_TITLE = DiagnosticBundle.message("error.new.notification.title"); private static final String ERROR_LINK = DiagnosticBundle.message("error.new.notification.link"); - private void showErrorNotification(@Nullable String notificationText, @NotNull Project project) { - String title = notificationText == null ? ERROR_TITLE : ""; - String content = notificationText == null ? "" : notificationText; - Notification notification = new Notification("", AllIcons.Ide.FatalError, title, null, content, NotificationType.ERROR, null); - - if (notificationText == null) { - notification.addAction(new NotificationAction(ERROR_LINK) { - @Override - public void actionPerformed(@NotNull AnActionEvent e, @NotNull Notification notification) { - notification.expire(); - doOpenErrorsDialog(null); - } - }); - } + private void showErrorNotification(@NotNull Project project) { + Notification notification = new Notification("", AllIcons.Ide.FatalError, ERROR_TITLE, null, null, NotificationType.ERROR, null); + notification.addAction(new NotificationAction(ERROR_LINK) { + @Override + public void actionPerformed(@NotNull AnActionEvent e, @NotNull Notification notification) { + notification.expire(); + doOpenErrorsDialog(null); + } + }); BalloonLayout layout = myFrame.getBalloonLayout(); assert layout != null; @@ -222,27 +214,4 @@ public class IdeMessagePanel extends JPanel implements MessagePoolListener, Icon Disposer.register(myBalloon, () -> myBalloon = null); layout.add(myBalloon); } - - private static String getNotificationText(List messages) { - String result = null; - for (AbstractMessage message : messages) { - String s; - if (message instanceof LogMessageEx) { - s = ((LogMessageEx)message).getNotificationText(); - } - else if (message instanceof GroupedLogMessage) { - s = getNotificationText(((GroupedLogMessage)message).getMessages()); - } - else { - return null; - } - if (result == null) { - result = s; - } - else if (!result.equals(s)) { - return null; // if texts are different, show default - } - } - return result; - } } \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/diagnostic/IdeaReportingEvent.java b/platform/platform-impl/src/com/intellij/diagnostic/IdeaReportingEvent.java new file mode 100644 index 000000000000..6c667b799290 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/diagnostic/IdeaReportingEvent.java @@ -0,0 +1,69 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.diagnostic; + +import com.intellij.ide.plugins.IdeaPluginDescriptor; +import com.intellij.openapi.diagnostic.IdeaLoggingEvent; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.PrintStream; +import java.io.PrintWriter; + +public class IdeaReportingEvent extends IdeaLoggingEvent { + private final String myStacktrace; + private final Throwable myThrowable; + private final IdeaPluginDescriptor myPlugin; + + public IdeaReportingEvent(@NotNull AbstractMessage messageObject, String message, @NotNull String stacktrace, IdeaPluginDescriptor plugin) { + super(message, null, messageObject); + myStacktrace = stacktrace; + myThrowable = new TextBasedThrowable(stacktrace); + myPlugin = plugin; + } + + public @Nullable String getOriginalMessage() { + return getData().getMessage(); + } + + public @NotNull String getOriginalThrowableText() { + return getData().getThrowableText(); + } + + public @Nullable IdeaPluginDescriptor getPlugin() { + return myPlugin; + } + + @Override + public Throwable getThrowable() { + return myThrowable; + } + + @Override + public String getThrowableText() { + return myStacktrace; + } + + @Override + @SuppressWarnings("ConstantConditions") + public @NotNull AbstractMessage getData() { + return (AbstractMessage)super.getData(); + } + + static class TextBasedThrowable extends Throwable { + private final String myStacktrace; + + TextBasedThrowable(String stacktrace) { + myStacktrace = stacktrace; + } + + @Override + public void printStackTrace(PrintWriter s) { + s.print(myStacktrace); + } + + @Override + public void printStackTrace(PrintStream s) { + s.print(myStacktrace); + } + } +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/diagnostic/LogEventException.java b/platform/platform-impl/src/com/intellij/diagnostic/LogEventException.java index 03a3e77363ac..299ee5f37c1d 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/LogEventException.java +++ b/platform/platform-impl/src/com/intellij/diagnostic/LogEventException.java @@ -1,35 +1,20 @@ -/* - * Copyright 2000-2013 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. - */ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.diagnostic; import com.intellij.openapi.diagnostic.Attachment; import com.intellij.openapi.diagnostic.ExceptionWithAttachments; import com.intellij.openapi.diagnostic.IdeaLoggingEvent; -import com.intellij.psi.impl.DebugUtil; +import com.intellij.openapi.diagnostic.RuntimeExceptionWithAttachments; import com.intellij.util.ArrayUtil; import org.jetbrains.annotations.NotNull; -/** - * @author peter - */ +/** @deprecated use {@link RuntimeExceptionWithAttachments#RuntimeExceptionWithAttachments(String, String, Attachment...)} (to be removed in IDEA 2020) */ +@SuppressWarnings({"DeprecatedIsStillUsed", "unused"}) public class LogEventException extends RuntimeException implements ExceptionWithAttachments { private final IdeaLoggingEvent myLogMessage; - public LogEventException(String userMessage, final String details, final Attachment... attachments) { - this(LogMessageEx.createEvent(userMessage, details + "\n\n" + DebugUtil.currentStackTrace(), attachments)); + public LogEventException(String userMessage, String details, Attachment... attachments) { + this(LogMessage.createEvent(new Throwable(details), userMessage, attachments)); } public LogEventException(IdeaLoggingEvent logMessage) { @@ -52,4 +37,4 @@ public class LogEventException extends RuntimeException implements ExceptionWith } return Attachment.EMPTY_ARRAY; } -} +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/diagnostic/LogMessage.java b/platform/platform-impl/src/com/intellij/diagnostic/LogMessage.java index 4cde24cf7f6a..54dd88553e87 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/LogMessage.java +++ b/platform/platform-impl/src/com/intellij/diagnostic/LogMessage.java @@ -1,18 +1,4 @@ -/* - * Copyright 2000-2009 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. - */ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.diagnostic; import com.intellij.idea.IdeaLogger; @@ -22,83 +8,70 @@ import com.intellij.openapi.util.objectTree.ThrowableInterner; import com.intellij.openapi.util.text.StringUtil; import com.intellij.util.containers.ContainerUtil; import org.apache.log4j.Layout; -import org.apache.log4j.spi.LoggingEvent; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; public class LogMessage extends AbstractMessage { private final Throwable myThrowable; - private final String myHeader; - private List myAttachments; + private final String myMessage; + private final List myAttachments; - LogMessage(@NotNull LoggingEvent event) { - Throwable throwable = event.getThrowableInformation() == null ? null : event.getThrowableInformation().getThrowable(); - myThrowable = throwable == null ? null : ThrowableInterner.intern(throwable); + LogMessage(Throwable throwable, String message, List attachments) { + myThrowable = ThrowableInterner.intern(throwable); - myHeader = - event.getMessage() == null || event.getMessage().toString().isEmpty() ? - getThrowable().toString() : event.getMessage().toString(); - } - - LogMessage(@NotNull IdeaLoggingEvent event) { - Throwable throwable = event.getThrowable(); - myThrowable = throwable == null ? null : ThrowableInterner.intern(throwable); - - String header = null; - - if (!StringUtil.isEmptyOrSpaces(event.getMessage())) { - header = event.getMessage(); - } - - if (myThrowable != null) { - String message = myThrowable.getMessage(); - if (StringUtil.isNotEmpty(message) && (header == null || !header.startsWith(message))) { - if (header != null) { - if (header.endsWith(":")) header += " "; - else if (!header.endsWith(": ")) header += ": "; - header += message; - } - else { - header = message; - } + String str = message; + if (str != null && throwable.getMessage() != null) { + str = StringUtil.trimStart(str, throwable.getMessage()); + if (str != message) { + str = StringUtil.trimStart(str, ": "); } } - - if (header == null) { - header = "No message"; + if ("null".equals(str)) { + str = null; } + myMessage = StringUtil.nullize(str, true); - myHeader = header; + myAttachments = new ArrayList<>(ContainerUtil.filter(attachments, attachment -> attachment != null)); } @Override - public Throwable getThrowable() { + public @NotNull Throwable getThrowable() { return myThrowable; } @Override - public String getMessage() { - return myHeader; + public @NotNull String getThrowableText() { + return StringUtil.join(IdeaLogger.getThrowableRenderer().doRender(myThrowable), Layout.LINE_SEP); } @Override - public String getThrowableText() { - return StringUtil.join(IdeaLogger.getThrowableRenderer().doRender(getThrowable()), Layout.LINE_SEP); + public @NotNull String getMessage() { + return myMessage != null ? myMessage : ""; } - @SuppressWarnings("WeakerAccess") - public void addAttachment(@NotNull Attachment attachment) { - if (myAttachments == null) { - myAttachments = ContainerUtil.createLockFreeCopyOnWriteList(); - } + @Override + public @NotNull List getAllAttachments() { + return Collections.unmodifiableList(myAttachments); + } + + /** @deprecated pass all attachments to {@link #createEvent(Throwable, String, Attachment...)} (to be removed in IDEA 2019) */ + public synchronized void addAttachment(@NotNull Attachment attachment) { myAttachments.add(attachment); } - @NotNull - @Override - public List getAllAttachments() { - return myAttachments != null ? myAttachments : Collections.emptyList(); + // factory methods + + /** + * @param userMessage user-friendly message description (short, single line if possible) + * @param attachments attachments that will be suggested to include to the report + */ + public static IdeaLoggingEvent createEvent(@NotNull Throwable throwable, @Nullable String userMessage, @NotNull Attachment... attachments) { + @SuppressWarnings("deprecation") AbstractMessage message = new LogMessageEx(throwable, userMessage, Arrays.asList(attachments), null); + return new IdeaLoggingEvent(userMessage, throwable, message); } -} +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/diagnostic/LogMessageEx.java b/platform/platform-impl/src/com/intellij/diagnostic/LogMessageEx.java index bbba8bb25243..ab8e9bd01a47 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/LogMessageEx.java +++ b/platform/platform-impl/src/com/intellij/diagnostic/LogMessageEx.java @@ -1,125 +1,57 @@ -/* - * Copyright 2000-2011 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. - */ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.diagnostic; +import com.intellij.diagnostic.IdeaReportingEvent.TextBasedThrowable; import com.intellij.openapi.diagnostic.Attachment; import com.intellij.openapi.diagnostic.IdeaLoggingEvent; import com.intellij.openapi.diagnostic.Logger; -import com.intellij.util.ExceptionUtil; -import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import java.io.PrintStream; -import java.io.PrintWriter; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.List; -/** - * @author ksafonov - */ +/** @deprecated use {@link LogMessage} (to be removed in IDEA 2020) */ +@SuppressWarnings({"DeprecatedIsStillUsed", "unused"}) public class LogMessageEx extends LogMessage { - private final IdeaLoggingEvent myEvent; - private final String myTitle; - private final String myNotificationText; - - /** - * @param title text to show in Event Log tool window entry (it comes before 'more') - * @param notificationText text to show in the error balloon that is popped up automatically - */ - public LogMessageEx(IdeaLoggingEvent aEvent, String title, String notificationText) { - super(aEvent); - myEvent = aEvent; - myTitle = title; - myNotificationText = notificationText; + /** @deprecated use {@link Logger#error(String, Throwable, Attachment...)} or {@link LogMessage} (to be removed in IDEA 2020) */ + public LogMessageEx(IdeaLoggingEvent event, String title, String notificationText) { + super(event.getThrowable(), event.getMessage(), attachments(event)); } - /** - * @return text to show in the error balloon that is popped up automatically - */ - public String getNotificationText() { - return myNotificationText; + // needed for compatibility - some reporters expect/check for instances of this class + LogMessageEx(Throwable throwable, String message, List attachments, String notificationText) { + super(throwable, message, attachments); } - /** - * @return text to show in Event Log tool window entry (it comes before 'more') - */ - public String getTitle() { - return myTitle; + private static List attachments(IdeaLoggingEvent event) { + Object data = event.getData(); + return data instanceof AbstractMessage ? ((AbstractMessage)data).getAllAttachments() : Collections.emptyList(); } - public IdeaLoggingEvent toEvent() { - return myEvent; + /** @deprecated use {@link Logger#error(String, Throwable, Attachment...)} (to be removed in IDEA 2020) */ + public static IdeaLoggingEvent createEvent(String message, String details, Attachment... attachments) { + return createEvent(new TextBasedThrowable(details), message, attachments); } - /** - * @param userMessage user-friendly message description (short, single line if possible) - * @param details technical details (exception stack trace etc.) - * @param attachments attachments that will be suggested to include to the report - */ - public static IdeaLoggingEvent createEvent(String userMessage, final String details, final Attachment... attachments) { - return createEvent(userMessage, details, userMessage, null, Arrays.asList(attachments)); + /** @deprecated use {@link Logger#error(String, Throwable, Attachment...)} (to be removed in IDEA 2020) */ + public static IdeaLoggingEvent createEvent(String message, String details, String title, String notificationText, Attachment attachment) { + return createEvent(new TextBasedThrowable(details), message, attachment); } - - /** - * @param userMessage user-friendly message description (short, single line if possible) - * @param details technical details (exception stack trace etc.) - * @param title text to show in Event Log tool window entry (it comes before 'more'), use {@code null} to reuse {@code userMessage} - * @param notificationText text to show in the error balloon that is popped up automatically. Default is {@code com.intellij.diagnostic.IdeMessagePanel#INTERNAL_ERROR_NOTICE} - * @param attachments attachments that will be suggested to include to the report - */ - public static IdeaLoggingEvent createEvent(final String userMessage, - final String details, - @Nullable final String title, - @Nullable final String notificationText, - final Collection attachments) { - final Throwable throwable = new Throwable() { - @Override - public void printStackTrace(PrintWriter s) { - s.print(details); - } - - @Override - public void printStackTrace(PrintStream s) { - s.print(details); - } - }; - - return new IdeaLoggingEvent(userMessage, throwable) { - @Override - public Object getData() { - final LogMessageEx logMessageEx = new LogMessageEx(this, title != null ? title : userMessage, notificationText); - for (Attachment attachment : attachments) { - logMessageEx.addAttachment(attachment); - } - return logMessageEx; - } - }; + /** @deprecated use {@link Logger#error(String, Throwable, Attachment...)} (to be removed in IDEA 2020) */ + public static IdeaLoggingEvent createEvent(String message, String details, String title, String notificationText, Collection attachments) { + Attachment[] array = attachments != null ? attachments.toArray(Attachment.EMPTY_ARRAY) : Attachment.EMPTY_ARRAY; + return createEvent(new TextBasedThrowable(details), message, array); } - public static void error(@NotNull Logger logger, @NonNls @NotNull String message, @NotNull String... attachmentText) { + /** @deprecated use {@link Logger#error(String, Throwable, Attachment...)} and {@link AttachmentFactory#createContext} (to be removed in IDEA 2020) */ + public static void error(@NotNull Logger logger, @NotNull String message, @NotNull String... attachmentText) { error(logger, message, new Throwable(), attachmentText); } - public static void error(@NotNull Logger logger, - @NotNull String message, - @NotNull Throwable cause, - @NotNull String... attachmentText) { + /** @deprecated use {@link Logger#error(String, Throwable, Attachment...)} and {@link AttachmentFactory#createContext} (to be removed in IDEA 2020) */ + public static void error(Logger logger, String message, Throwable cause, String... attachmentText) { StringBuilder detailsBuffer = new StringBuilder(); for (String detail : attachmentText) { detailsBuffer.append(detail).append(","); @@ -127,23 +59,11 @@ public class LogMessageEx extends LogMessage { if (attachmentText.length > 0 && detailsBuffer.length() > 0) { detailsBuffer.setLength(detailsBuffer.length() - 1); } - Attachment attachment = detailsBuffer.length() > 0 ? new Attachment("current-context.txt", detailsBuffer.toString()) : null; - logger.error(createEvent(message, ExceptionUtil.getThrowableText(cause), null, null, attachment)); + if (detailsBuffer.length() > 0) { + logger.error(message, cause, AttachmentFactory.createContext(detailsBuffer)); + } + else { + logger.error(message, cause); + } } - - /** - * @param userMessage user-friendly message description (short, single line if possible) - * @param details technical details (exception stack trace etc.) - * @param title text to show in Event Log tool window entry (it comes before 'more'), use {@code null} to reuse {@code userMessage} - * @param notificationText text to show in the error balloon that is popped up automatically. Default is {@code com.intellij.diagnostic.IdeMessagePanel#INTERNAL_ERROR_NOTICE} - * @param attachment attachment that will be suggested to include to the report - */ - public static IdeaLoggingEvent createEvent(String userMessage, - final String details, - @Nullable final String title, - @Nullable final String notificationText, - @Nullable Attachment attachment) { - return createEvent(userMessage, details, title, notificationText, - attachment != null ? Collections.singletonList(attachment) : Collections.emptyList()); - } -} +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/diagnostic/MessagePool.java b/platform/platform-impl/src/com/intellij/diagnostic/MessagePool.java index f45953c8a004..0243745afd36 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/MessagePool.java +++ b/platform/platform-impl/src/com/intellij/diagnostic/MessagePool.java @@ -4,12 +4,10 @@ package com.intellij.diagnostic; import com.intellij.openapi.diagnostic.IdeaLoggingEvent; import com.intellij.util.concurrency.AppExecutorUtil; import com.intellij.util.containers.ContainerUtil; -import org.apache.log4j.Level; -import org.apache.log4j.LogManager; -import org.apache.log4j.spi.LoggingEvent; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; @@ -35,14 +33,21 @@ public class MessagePool { private MessagePool() { } public void addIdeFatalMessage(@NotNull IdeaLoggingEvent event) { - Object data = event.getData(); - LogMessage message = data instanceof LogMessage ? (LogMessage)data : new LogMessage(event); if (myErrors.size() < MAX_POOL_SIZE) { - myGrouper.addToGroup(message); + Object data = event.getData(); + if (data instanceof GroupedLogMessage) { + myGrouper.addToGroup(new LogMessage(new Throwable(), "illegal reuse a grouped message", Collections.emptyList())); + } + else if (data instanceof AbstractMessage) { + myGrouper.addToGroup((AbstractMessage)data); + } + else { + myGrouper.addToGroup(new LogMessage(event.getThrowable(), event.getMessage(), Collections.emptyList())); + } } else if (myErrors.size() == MAX_POOL_SIZE) { - TooManyErrorsException t = new TooManyErrorsException(); - myGrouper.addToGroup(new LogMessage(new LoggingEvent(t.getMessage(), LogManager.getRootLogger(), Level.ERROR, null, t))); + TooManyErrorsException e = new TooManyErrorsException(); + myGrouper.addToGroup(new LogMessage(e, null, Collections.emptyList())); } } diff --git a/platform/platform-impl/src/com/intellij/errorreport/bean/ErrorBean.java b/platform/platform-impl/src/com/intellij/errorreport/bean/ErrorBean.java index 7252e49b8451..c861cbb2e44e 100644 --- a/platform/platform-impl/src/com/intellij/errorreport/bean/ErrorBean.java +++ b/platform/platform-impl/src/com/intellij/errorreport/bean/ErrorBean.java @@ -7,10 +7,8 @@ import com.intellij.util.ExceptionUtil; import java.util.Collections; import java.util.List; -/** - * @author stathik - * @since May 5, 2003 - */ +/** @deprecated trivial and not particularly helpful, could be replaced with {@link java.util.Map} (to be removed in IDEA 2020) */ +@SuppressWarnings({"unused", "DeprecatedIsStillUsed"}) public class ErrorBean { private final String stackTrace; private final String lastAction; @@ -70,14 +68,14 @@ public class ErrorBean { this.pluginVersion = pluginVersion; } - public void setAttachments(List attachments) { - this.attachments = attachments; - } - public List getAttachments() { return attachments; } + public void setAttachments(List attachments) { + this.attachments = attachments; + } + public Integer getAssigneeId() { return assigneeId; } diff --git a/platform/platform-impl/src/com/intellij/idea/IdeaLogger.java b/platform/platform-impl/src/com/intellij/idea/IdeaLogger.java index 687c9de39e2b..cd3b8e2e0eca 100644 --- a/platform/platform-impl/src/com/intellij/idea/IdeaLogger.java +++ b/platform/platform-impl/src/com/intellij/idea/IdeaLogger.java @@ -1,9 +1,7 @@ -/* - * Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. - */ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.idea; -import com.intellij.diagnostic.LogMessageEx; +import com.intellij.diagnostic.LogMessage; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ex.ApplicationInfoEx; import com.intellij.openapi.application.impl.ApplicationImpl; @@ -95,8 +93,7 @@ public class IdeaLogger extends Log4jBasedLogger { @Override public void error(String message, @Nullable Throwable t, @NotNull Attachment... attachments) { - String trace = ExceptionUtil.getThrowableText(t != null ? t : new Throwable()); - myLogger.error(LogMessageEx.createEvent(message, trace, attachments)); + myLogger.error(LogMessage.createEvent(t != null ? t : new Throwable(), message, attachments)); } @Override @@ -120,11 +117,7 @@ public class IdeaLogger extends Log4jBasedLogger { } private void logErrorHeader() { - String info = ourApplicationInfoProvider.getInfo(); - - if (info != null) { - myLogger.error(info); - } + myLogger.error(ourApplicationInfoProvider.getInfo()); if (ourCompilationTimestamp != null) { myLogger.error("Internal version. Compiled " + ourCompilationTimestamp); diff --git a/platform/platform-tests/testSrc/com/intellij/diagnostic/AttachmentFactoryTest.java b/platform/platform-tests/testSrc/com/intellij/diagnostic/AttachmentFactoryTest.java index 39d22b4ddec7..b9ecd8b8b567 100644 --- a/platform/platform-tests/testSrc/com/intellij/diagnostic/AttachmentFactoryTest.java +++ b/platform/platform-tests/testSrc/com/intellij/diagnostic/AttachmentFactoryTest.java @@ -1,42 +1,45 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.diagnostic; import com.intellij.openapi.diagnostic.Attachment; import com.intellij.openapi.util.io.FileUtil; -import org.junit.Assert; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.testFramework.rules.TempDirectory; +import org.junit.Rule; import org.junit.Test; -import java.io.*; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.assertThat; public class AttachmentFactoryTest { + @Rule public TempDirectory tempDir = new TempDirectory(); + @Test public void testBigFilesStoredOnDisk() throws IOException { - final File testFile = FileUtil.createTempFile("test", ".bin", true); - try { - FileUtil.writeToFile(testFile, new byte[150000]); - Attachment attachment = AttachmentFactory.createAttachment(testFile, true); + File testFile = tempDir.newFile("a big one.txt"); + String content = StringUtil.repeat("*", 100 * 1024); + byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8); + FileUtil.writeToFile(testFile, contentBytes); - try (InputStream contentStream = attachment.openContentStream()) { - Assert.assertTrue(contentStream instanceof FileInputStream); - } - } finally { - //noinspection ResultOfMethodCallIgnored - testFile.delete(); - } + Attachment attachment = AttachmentFactory.createAttachment(testFile, false); + assertThat(attachment.getDisplayText()).isNotEmpty().isNotEqualTo(content); + assertThat(attachment.getBytes()).isEqualTo(contentBytes); + assertThat(FileUtil.loadBytes(attachment.openContentStream())).isEqualTo(contentBytes); } @Test public void testSmallFilesStoredInMemory() throws IOException { - final File testFile = FileUtil.createTempFile("test", ".bin", true); - try { - FileUtil.writeToFile(testFile, new byte[1500]); - Attachment attachment = AttachmentFactory.createAttachment(testFile, true); + File testFile = tempDir.newFile("a little one.txt"); + String content = StringUtil.repeat("*", 1024); + byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8); + FileUtil.writeToFile(testFile, contentBytes); - try (InputStream contentStream = attachment.openContentStream()) { - Assert.assertTrue(contentStream instanceof ByteArrayInputStream); - } - } finally { - //noinspection ResultOfMethodCallIgnored - testFile.delete(); - } + Attachment attachment = AttachmentFactory.createAttachment(testFile, false); + assertThat(attachment.getDisplayText()).isEqualTo(content); + assertThat(attachment.getBytes()).isEqualTo(contentBytes); + assertThat(FileUtil.loadBytes(attachment.openContentStream())).isEqualTo(contentBytes); } -} +} \ No newline at end of file diff --git a/platform/testGuiFramework/src/com/intellij/testGuiFramework/impl/GuiTestUtilKt.kt b/platform/testGuiFramework/src/com/intellij/testGuiFramework/impl/GuiTestUtilKt.kt index 0534235f8503..4784d34b0023 100644 --- a/platform/testGuiFramework/src/com/intellij/testGuiFramework/impl/GuiTestUtilKt.kt +++ b/platform/testGuiFramework/src/com/intellij/testGuiFramework/impl/GuiTestUtilKt.kt @@ -1,18 +1,4 @@ -/* - * Copyright 2000-2017 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. - */ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.testGuiFramework.impl import com.intellij.diagnostic.MessagePool @@ -319,7 +305,7 @@ object GuiTestUtilKt { val freshErrorMessages = errorMessages.filter { it.date > afterDate } val errors = mutableListOf() for (errorMessage in freshErrorMessages) { - val messageBuilder = StringBuilder(errorMessage.message) + val messageBuilder = StringBuilder(errorMessage.message ?: "") val additionalInfo : String? = errorMessage.additionalInfo if (additionalInfo != null && additionalInfo.isNotEmpty()) messageBuilder.append(System.getProperty("line.separator")).append("Additional Info: ").append(additionalInfo) diff --git a/platform/util/src/com/intellij/openapi/diagnostic/Attachment.java b/platform/util/src/com/intellij/openapi/diagnostic/Attachment.java index 43de33e03372..fd016ebb9cd2 100644 --- a/platform/util/src/com/intellij/openapi/diagnostic/Attachment.java +++ b/platform/util/src/com/intellij/openapi/diagnostic/Attachment.java @@ -1,18 +1,4 @@ -/* - * Copyright 2000-2016 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. - */ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.openapi.diagnostic; import com.intellij.openapi.util.io.FileUtil; @@ -32,66 +18,36 @@ public class Attachment { public static final Attachment[] EMPTY_ARRAY = new Attachment[0]; private final String myPath; - @Nullable private final File myTemporaryFile; - @Nullable private final byte[] myBytes; - private boolean myIncluded; // opt-out for traces, opt-in otherwise private final String myDisplayText; - - public Attachment(@NotNull String path, @NotNull String content) { - this(path, content.getBytes(CharsetToolkit.UTF8_CHARSET), content); - } - - public Attachment(@NotNull String path, @NotNull byte[] bytes, @NotNull String displayText) { - myPath = path; - myDisplayText = displayText; - myBytes = bytes; - myTemporaryFile = null; - } - - public Attachment(@NotNull String path, @NotNull InputStream inputStream, @NotNull String displayText) { - myPath = path; - myDisplayText = displayText; - - myBytes = null; - - File temporaryFile; - try { - temporaryFile = FileUtil.createTempFile("intellij-attachment", ".bin", true); - temporaryFile.deleteOnExit(); - } catch (IOException e) { - LOG.error("Unable to create temp file for attachment: " + e.getMessage(), e); - temporaryFile = null; - } - - if (temporaryFile != null) { - try { - OutputStream outputStream = new FileOutputStream(temporaryFile); - try { - FileUtil.copy(inputStream, outputStream); - } finally { - outputStream.close(); - } - } catch (IOException e) { - LOG.error("Unable to write temp file for attachment at " + temporaryFile + ": " + e.getMessage(), e); - temporaryFile = null; - } - } - - myTemporaryFile = temporaryFile; - } - - public Attachment(@NotNull String path, @NotNull File existingTemporaryFile, @NotNull String displayText) { - myPath = path; - myDisplayText = displayText; - myTemporaryFile = existingTemporaryFile; - myBytes = null; - } + private final @Nullable byte[] myBytes; + private final @Nullable File myTemporaryFile; + private boolean myIncluded; // opt-out for traces, opt-in otherwise public Attachment(@NotNull String name, @NotNull Throwable throwable) { this(name + ".trace", ExceptionUtil.getThrowableText(throwable)); myIncluded = true; } + public Attachment(@NotNull String path, @NotNull String content) { + this(path, content, content.getBytes(CharsetToolkit.UTF8_CHARSET), null); + } + + public Attachment(@NotNull String path, @NotNull byte[] bytes, @NotNull String displayText) { + this(path, displayText, bytes, null); + } + + public Attachment(@NotNull String path, @NotNull File temporaryFile, @NotNull String displayText) { + this(path, displayText, null, temporaryFile); + } + + private Attachment(String path, String displayText, @Nullable byte[] bytes, @Nullable File temporaryFile) { + assert bytes != null || temporaryFile != null; + myPath = path; + myDisplayText = displayText; + myBytes = bytes; + myTemporaryFile = temporaryFile; + } + @NotNull public String getDisplayText() { return myDisplayText; @@ -118,16 +74,16 @@ public class Attachment { return myBytes; } - if (myTemporaryFile == null) { - return ArrayUtil.EMPTY_BYTE_ARRAY; + if (myTemporaryFile != null) { + try { + return FileUtil.loadFileBytes(myTemporaryFile); + } + catch (IOException e) { + LOG.error("Failed to read attachment content from temp. file " + myTemporaryFile, e); + } } - try { - return FileUtil.loadFileBytes(myTemporaryFile); - } catch (IOException e) { - LOG.error("Unable to read attachment content from temporary file " + myTemporaryFile + ": " + e.getMessage(), e); - return ArrayUtil.EMPTY_BYTE_ARRAY; - } + return ArrayUtil.EMPTY_BYTE_ARRAY; } @NotNull @@ -136,16 +92,16 @@ public class Attachment { return new ByteArrayInputStream(myBytes); } - if (myTemporaryFile == null) { - return new ByteArrayInputStream(ArrayUtil.EMPTY_BYTE_ARRAY); + if (myTemporaryFile != null) { + try { + return new FileInputStream(myTemporaryFile); + } + catch (FileNotFoundException e) { + LOG.error("Failed to read attachment content from temp. file " + myTemporaryFile, e); + } } - try { - return new FileInputStream(myTemporaryFile); - } catch (FileNotFoundException e) { - LOG.warn("Unable to read attachment content from temporary file " + myTemporaryFile + ": " + e.getMessage(), e); - return new ByteArrayInputStream(ArrayUtil.EMPTY_BYTE_ARRAY); - } + return new ByteArrayInputStream(ArrayUtil.EMPTY_BYTE_ARRAY); } public boolean isIncluded() { @@ -155,4 +111,4 @@ public class Attachment { public void setIncluded(boolean included) { myIncluded = included; } -} +} \ No newline at end of file diff --git a/platform/util/src/com/intellij/openapi/diagnostic/RuntimeExceptionWithAttachments.java b/platform/util/src/com/intellij/openapi/diagnostic/RuntimeExceptionWithAttachments.java index f60f52edb008..c7ef5a4830f3 100644 --- a/platform/util/src/com/intellij/openapi/diagnostic/RuntimeExceptionWithAttachments.java +++ b/platform/util/src/com/intellij/openapi/diagnostic/RuntimeExceptionWithAttachments.java @@ -1,42 +1,47 @@ -/* - * Copyright 2000-2017 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. - */ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.openapi.diagnostic; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** * @author peter */ @SuppressWarnings("ExceptionClassNameDoesntEndWithException") public class RuntimeExceptionWithAttachments extends RuntimeException implements ExceptionWithAttachments { + private final String myUserMessage; private final Attachment[] myAttachments; public RuntimeExceptionWithAttachments(String message, Attachment... attachments) { super(message); + myUserMessage = null; myAttachments = attachments; } public RuntimeExceptionWithAttachments(Throwable cause, Attachment... attachments) { super(cause); + myUserMessage = null; myAttachments = attachments; } + /** + * Corresponds to {@link Logger#error(String, Throwable, Attachment...)} + * ({@code LOG.error(userMessage, new RuntimeException(details), attachments)}). + */ + public RuntimeExceptionWithAttachments(String userMessage, String details, Attachment... attachments) { + super(details); + myUserMessage = userMessage; + myAttachments = attachments; + } + + @Nullable + public String getUserMessage() { + return myUserMessage; + } + @NotNull @Override public Attachment[] getAttachments() { return myAttachments; } -} +} \ No newline at end of file