diff --git a/platform/platform-impl/src/com/intellij/diagnostic/ITNProxy.kt b/platform/platform-impl/src/com/intellij/diagnostic/ITNProxy.kt index adf74ad302ed..b6faf8cedda8 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/ITNProxy.kt +++ b/platform/platform-impl/src/com/intellij/diagnostic/ITNProxy.kt @@ -1,12 +1,16 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.diagnostic +import com.fasterxml.jackson.core.JsonFactory +import com.fasterxml.jackson.core.JsonToken import com.intellij.errorreport.error.InternalEAPException import com.intellij.errorreport.error.UpdateAvailableException import com.intellij.ide.plugins.PluginManagerCore +import com.intellij.idea.AppMode import com.intellij.internal.statistic.DeviceIdManager import com.intellij.internal.statistic.utils.getPluginInfoById import com.intellij.openapi.application.ApplicationNamesInfo +import com.intellij.openapi.application.PathManager import com.intellij.openapi.application.ex.ApplicationInfoEx import com.intellij.openapi.components.Service import com.intellij.openapi.diagnostic.IdeaLoggingEvent @@ -28,6 +32,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.ensureActive +import org.jetbrains.annotations.ApiStatus import java.io.OutputStreamWriter import java.net.HttpURLConnection import java.net.URI @@ -38,6 +43,8 @@ import java.net.http.HttpResponse import java.nio.charset.StandardCharsets import java.time.Duration import java.util.zip.GZIPOutputStream +import kotlin.io.path.Path +import kotlin.io.path.bufferedReader @Service internal class ITNProxyCoroutineScopeHolder(coroutineScope: CoroutineScope) { @@ -49,48 +56,101 @@ internal class ITNProxyCoroutineScopeHolder(coroutineScope: CoroutineScope) { internal val coroutineScope: CoroutineScope = coroutineScope.childScope("ITNProxy call", dispatcher) } -internal object ITNProxy { - private const val DEFAULT_USER = "idea_anonymous" - private const val DEFAULT_PASS = "guest" +@ApiStatus.Internal +object ITNProxy { + private const val DEFAULT_ENDPOINT = "https://ea-report.jetbrains.com/trackerRpc/idea/createScr" private const val DIOGEN_VIEW_URL = "https://diogen.labs.jb.gg/report/" - internal val DEVICE_ID: String = DeviceIdManager.getOrGenerateId(object : DeviceIdManager.DeviceIdToken {}, "EA") + internal val DEVICE_ID: String by lazy { + DeviceIdManager.getOrGenerateId(object : DeviceIdManager.DeviceIdToken {}, "EA") + } - private val LOG = logger() - - private val TEMPLATE: Map by lazy { + private val TEMPLATE_SAFE: Map by lazy { val template = LinkedHashMap() template["protocol.version"] = "1.1" + template["user.login"] = "idea_anonymous" + template["user.password"] = "guest" template["os.cpu.arch"] = if (CpuArch.isEmulated()) "${CpuArch.CURRENT}(emulated)" else "${CpuArch.CURRENT}" template["os.name"] = OS.CURRENT.name template["os.version"] = OS.CURRENT.version() - template["host.id"] = DEVICE_ID template["java.version"] = SystemInfo.JAVA_RUNTIME_VERSION template["java.vm.vendor"] = SystemInfo.JAVA_VENDOR + template + } + + private val TEMPLATE_APP: Map by lazy { val appInfo = ApplicationInfoEx.getInstanceEx() val namesInfo = ApplicationNamesInfo.getInstance() val build = appInfo.build - var buildNumberWithAllDetails = build.asString() - if (buildNumberWithAllDetails.startsWith(build.productCode + '-')) { - buildNumberWithAllDetails = buildNumberWithAllDetails.substring(build.productCode.length + 1) - } + + val template = LinkedHashMap() + template["host.id"] = DEVICE_ID template["app.name"] = namesInfo.productName template["app.name.full"] = namesInfo.fullProductName template["app.name.version"] = appInfo.versionName - template["app.eap"] = java.lang.Boolean.toString(appInfo.isEAP) + template["app.eap"] = appInfo.isEAP.toString() template["app.build"] = appInfo.apiVersion template["app.version.major"] = appInfo.majorVersion template["app.version.minor"] = appInfo.minorVersion - template["app.build.date"] = (appInfo.buildTime.toInstant().toEpochMilli()).toString() + template["app.build.date"] = appInfo.buildTime.toInstant().toEpochMilli().toString() template["app.build.date.release"] = appInfo.majorReleaseBuildDate.time.time.toString() template["app.product.code"] = build.productCode - template["app.build.number"] = buildNumberWithAllDetails + template["app.build.number"] = build.asStringWithoutProductCode() IdeProductInfo.getInstance().currentProductInfo.customProperties .find { it.key == CustomPropertyNames.GIT_REVISION } ?.let { template["app.source.revision"] = it.value } template } + private fun appendEarlyAppData(builder: StringBuilder) { + @Suppress("TestOnlyProblems") + val homeDir = System.getProperty("idea.home.path")?.let { Path(it) } + ?: PathManager.getHomeDirFor(ITNProxy::class.java) + ?: throw RuntimeException("Cannot detect the IDE home directory") + val appDataFile = homeDir.resolve(when { + AppMode.isRunningFromDevBuild() -> "bin/product-info.json" + OS.CURRENT == OS.macOS -> "Resources/product-info.json" + else -> "product-info.json" + }) + + try { + var appName = null as String? + var version = null as String? + var buildNumber = null as String? + var productCode = null as String? + JsonFactory().createParser(appDataFile.bufferedReader()).use { parser -> + if (parser.nextToken() == JsonToken.START_OBJECT) { + while (true) { + if (parser.nextToken() != JsonToken.FIELD_NAME) break + val name = parser.currentName() + if (parser.nextToken() != JsonToken.VALUE_STRING) break + val value = parser.text + when (name) { + "name" -> appName = value + "version" -> version = value + "buildNumber" -> buildNumber = value + "productCode" -> productCode = value + } + } + } + } + if (appName == null || version == null || buildNumber == null || productCode == null) throw RuntimeException("Malformed app data file") + + val shortName = appName.splitToSequence(' ').last() + val versionParts = version.split('.') + append(builder, "app.name", shortName) + append(builder, "app.name.full", appName) + append(builder, "app.build", "${productCode}-${buildNumber}") + append(builder, "app.version.major", versionParts[0]) + append(builder, "app.version.minor", versionParts.getOrNull(1) ?: "0") + append(builder, "app.product.code", productCode) + append(builder, "app.build.number", buildNumber) + } + catch (e: Exception) { + throw RuntimeException("Cannot read application data", e) + } + } + @JvmRecord internal data class ErrorBean( val event: IdeaLoggingEvent, @@ -102,7 +162,7 @@ internal object ITNProxy { val isAutoReportedByPlatform: Boolean, ) - fun getBrowseUrl(threadId: Long): String? = when { + internal fun getBrowseUrl(threadId: Long): String? = when { isInternalUser() -> DIOGEN_VIEW_URL + threadId else -> null } @@ -121,31 +181,38 @@ internal object ITNProxy { } @Throws(Exception::class) - suspend fun sendError(error: ErrorBean, newThreadPostUrl: String): Long { + internal suspend fun sendError(error: ErrorBean, newThreadPostUrl: String?): Long { val context = currentCoroutineContext() + val request = createRequest(error.event, error) + val response = post(newThreadPostUrl ?: DEFAULT_ENDPOINT, request) + context.ensureActive() + val reportId = handleResponse(response) + logger().info("report ID: ${reportId}") + return reportId + } - val response = post(newThreadPostUrl, createRequest(error)) + @JvmStatic + @Throws(Exception::class) + fun sendError(event: IdeaLoggingEvent): Long { + val request = createRequest(event, errorBean = null) + val response = post(DEFAULT_ENDPOINT, request) + return handleResponse(response) + } + + private fun handleResponse(response: HttpResponse): Long { val responseCode = response.statusCode() if (responseCode != HttpURLConnection.HTTP_OK) { throw InternalEAPException(DiagnosticBundle.message("error.http.result.code", responseCode)) } - context.ensureActive() - val responseText = response.body() - if (responseText == "unauthorized") { - throw InternalEAPException("Authorization failed") - } - if (responseText.startsWith("update ")) { - throw UpdateAvailableException(responseText.substring(7)) - } - if (responseText.startsWith("message ")) { - throw InternalEAPException(responseText.substring(8)) + when { + responseText == "unauthorized" -> throw InternalEAPException("Authorization failed") + responseText.startsWith("update ") -> throw UpdateAvailableException(responseText.substring(7)) + responseText.startsWith("message ") -> throw InternalEAPException(responseText.substring(8)) } try { - val reportId = responseText.trim() - LOG.info("report ID: ${reportId}") - return reportId.toLong() + return responseText.trim().toLong() } catch (_: NumberFormatException) { throw InternalEAPException(DiagnosticBundle.message("error.itn.returns.wrong.data")) @@ -153,72 +220,76 @@ internal object ITNProxy { } @JvmStatic - val appInfoString: String - get() { - val builder = StringBuilder() - appendAppInfo(builder) - return builder.toString() - } + internal val appInfoString: String + get() = StringBuilder().apply { appendAppInfo(this) }.toString() private fun appendAppInfo(builder: StringBuilder) { - for ((key, value) in TEMPLATE) { - append(builder, key, value) - } + TEMPLATE_SAFE.forEach { (key, value) -> append(builder, key, value) } + TEMPLATE_APP.forEach { (key, value) -> append(builder, key, value) } } - private fun createRequest(error: ErrorBean): StringBuilder { + private fun createRequest(event: IdeaLoggingEvent, errorBean: ErrorBean?): StringBuilder { val builder = StringBuilder(8192) - val eventData = error.event.data - val appInfo = if (eventData is AbstractMessage) eventData.appInfo else null - if (appInfo != null) { - builder.append(appInfo) + + if (errorBean != null) { + val appInfo = (event.data as? AbstractMessage)?.appInfo + if (appInfo != null) { + builder.append(appInfo) + } + else { + appendAppInfo(builder) + append(builder, "report.startup.error", "true") + } } else { - appendAppInfo(builder) + TEMPLATE_SAFE.forEach { (key, value) -> append(builder, key, value) } + appendEarlyAppData(builder) } - append(builder, "user.login", DEFAULT_USER) - append(builder, "user.password", DEFAULT_PASS) - JBAccountInfoService.getInstance()?.userData?.email?.takeIf { it.endsWith("@jetbrains.com", ignoreCase = true) }?.let { - append(builder, "user.email", it) - } - - val updateSettings = UpdateSettings.getInstance() - append(builder, "update.channel.status", updateSettings.selectedChannelStatus.code) - append(builder, "update.ignored.builds", java.lang.String.join(",", updateSettings.ignoredBuildNumbers)) - append(builder, "plugin.id", error.pluginId) - append(builder, "plugin.name", error.pluginName) - append(builder, "plugin.version", error.pluginVersion) - append(builder, "last.action", error.lastActionId) - - val nonBundledPlugins = PluginManagerCore.loadedPlugins - .filter { !it.isBundled } - .map { it.pluginId } - .filter { getPluginInfoById(it).isSafeToReport() } - - if (nonBundledPlugins.isNotEmpty()) { - append(builder, "plugins.nonbundled", nonBundledPlugins.joinToString(",") { it.idString }) - } - - append(builder, "error.message", error.event.message?.trim { it <= ' ' } ?: "") - append(builder, "error.stacktrace", error.event.throwableText) - (error.event.throwable as? UnhandledException)?.let { + append(builder, "error.message", event.message?.trim { it <= ' ' } ?: "") + append(builder, "error.stacktrace", event.throwableText) + (event.throwable as? UnhandledException)?.let { append(builder, "error.unhandled.interactive", it.isInteractive.toString()) } - - append(builder, "error.description", error.comment) - if (error.event.throwable is RecoveredThrowable) { + if (event.throwable is RecoveredThrowable) { append(builder, "error.redacted", "true") } - for (attachment in error.event.attachments) { + for (attachment in event.attachments) { append(builder, "attachment.name", attachment.name) append(builder, "attachment.value", attachment.encodedBytes) } - if (error.isAutoReportedByPlatform) { - append(builder, "report.automatic", "true") + // optional fields; added only when the app is loaded + if (errorBean != null) { + JBAccountInfoService.getInstance()?.userData?.email?.takeIf { it.endsWith("@jetbrains.com", ignoreCase = true) }?.let { + append(builder, "user.email", it) + } + + val updateSettings = UpdateSettings.getInstance() + append(builder, "update.channel.status", updateSettings.selectedChannelStatus.code) + append(builder, "update.ignored.builds", updateSettings.ignoredBuildNumbers.joinToString(",")) + + append(builder, "plugin.id", errorBean.pluginId) + append(builder, "plugin.name", errorBean.pluginName) + append(builder, "plugin.version", errorBean.pluginVersion) + append(builder, "last.action", errorBean.lastActionId) + + append(builder, "error.description", errorBean.comment) + + PluginManagerCore.loadedPlugins + .filter { !it.isBundled } + .map { it.pluginId } + .filter { getPluginInfoById(it).isSafeToReport() } + .takeIf { it.isNotEmpty() } + ?.joinToString(",") { it.idString } + ?.let { append(builder, "plugins.nonbundled", it) } + + if (errorBean.isAutoReportedByPlatform) { + append(builder, "report.automatic", "true") + } } + return builder } diff --git a/platform/platform-impl/src/com/intellij/diagnostic/ITNReporter.kt b/platform/platform-impl/src/com/intellij/diagnostic/ITNReporter.kt index d90da6948cfc..6c7ece3428ce 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/ITNReporter.kt +++ b/platform/platform-impl/src/com/intellij/diagnostic/ITNReporter.kt @@ -40,9 +40,9 @@ internal val NOTIFY_SUCCESS_EACH_REPORT = AtomicBoolean(true) // dirty hack, rep * Third-party plugins need to provide their own implementations of [ErrorReportSubmitter]. */ @InternalIgnoreDependencyViolation -open class ITNReporter internal constructor(private val postUrl: String) : ErrorReportSubmitter() { +open class ITNReporter internal constructor(private val postUrl: String?) : ErrorReportSubmitter() { @ApiStatus.Internal - constructor() : this("https://ea-report.jetbrains.com/trackerRpc/idea/createScr") + constructor() : this(postUrl = null) override fun getReportActionText(): String = DiagnosticBundle.message("error.report.to.jetbrains.action") diff --git a/platform/platform-impl/src/com/intellij/platform/ide/bootstrap/StartupErrorReporter.java b/platform/platform-impl/src/com/intellij/platform/ide/bootstrap/StartupErrorReporter.java index e70372b9b20d..1f11b8277455 100644 --- a/platform/platform-impl/src/com/intellij/platform/ide/bootstrap/StartupErrorReporter.java +++ b/platform/platform-impl/src/com/intellij/platform/ide/bootstrap/StartupErrorReporter.java @@ -1,10 +1,10 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.platform.ide.bootstrap; +import com.intellij.diagnostic.ITNProxy; import com.intellij.diagnostic.ImplementationConflictException; import com.intellij.diagnostic.LoadingState; import com.intellij.diagnostic.PluginException; -import com.intellij.ide.BootstrapBundle; import com.intellij.ide.logsUploader.LogUploader; import com.intellij.ide.plugins.EssentialPluginMissingException; import com.intellij.ide.plugins.PluginConflictReporter; @@ -22,17 +22,17 @@ import com.intellij.openapi.application.impl.ApplicationInfoImpl; import com.intellij.openapi.application.impl.ExceptionsKt; import com.intellij.openapi.diagnostic.ControlFlowException; import com.intellij.openapi.diagnostic.ExceptionWithAttachments; +import com.intellij.openapi.diagnostic.IdeaLoggingEvent; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.util.NlsSafe; import com.intellij.openapi.util.io.NioFiles; import com.intellij.util.io.Compressor; -import com.intellij.util.io.URLUtil; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NonNls; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NullMarked; import javax.swing.BorderFactory; import javax.swing.ImageIcon; @@ -44,7 +44,6 @@ import javax.swing.JProgressBar; import javax.swing.JScrollPane; import javax.swing.JTextPane; import javax.swing.ScrollPaneConstants; -import javax.swing.SwingWorker; import javax.swing.UIManager; import java.awt.AWTError; import java.awt.BorderLayout; @@ -63,16 +62,16 @@ import java.nio.file.Files; import java.nio.file.Path; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -import java.util.concurrent.ExecutionException; +import static com.intellij.ide.BootstrapBundle.message; import static java.util.Objects.requireNonNullElse; import static org.jetbrains.annotations.Nls.Capitalization.Sentence; import static org.jetbrains.annotations.Nls.Capitalization.Title; @ApiStatus.Internal +@NullMarked public final class StartupErrorReporter { private static final String SUPPORT_URL_PROPERTY = "ij.startup.error.support.url"; - private static final String REPORT_URL_PROPERTY = "ij.startup.error.report.url"; private static boolean hasGraphics = !ApplicationManagerEx.isInIntegrationTest(); @@ -81,7 +80,8 @@ public final class StartupErrorReporter { "Plugin Installation Problem", "The IDE failed to install or update some plugins.\n" + "Please try again, and if the problem persists, report it to the support.\n\n" + - "The cause: " + t.toString()); + "The cause: " + t + ); } /** Note: warnings should be hardcoded because it's too early to try loading localization plugins. */ @@ -101,23 +101,23 @@ public final class StartupErrorReporter { } } - public static void showError(@NotNull @Nls(capitalization = Title) String title, @NotNull Throwable t) { + public static void showError(@Nls(capitalization = Title) String title, Throwable t) { var message = new StringWriter(); var awtError = findCause(t, AWTError.class); if (awtError != null) { - message.append(BootstrapBundle.message("bootstrap.error.prefix.graphics")); + message.append(message("bootstrap.error.prefix.graphics")); hasGraphics = false; t = awtError; } else { - message.append(BootstrapBundle.message("bootstrap.error.prefix.other")); + message.append(message("bootstrap.error.prefix.other")); } message.append("\n\n"); t.printStackTrace(new PrintWriter(message)); - message.append("\n-----\n").append(BootstrapBundle.message("bootstrap.error.appendix.jre", jreDetails())); + message.append("\n-----\n").append(message("bootstrap.error.appendix.jre", jreDetails())); showError(title, message.toString(), t); //NON-NLS } @@ -158,10 +158,10 @@ public final class StartupErrorReporter { try { var messageObj = prepareMessage(message); - var close = BootstrapBundle.message("bootstrap.error.option.close"); + var close = message("bootstrap.error.option.close"); var iconUrl = StartupErrorReporter.class.getResource("/images/questionSign.png"); var learnMore = iconUrl != null ? new JLabel(new ImageIcon(iconUrl)) : new JLabel("?"); - learnMore.setToolTipText(BootstrapBundle.message("bootstrap.error.option.support")); + learnMore.setToolTipText(message("bootstrap.error.option.support")); learnMore.setCursor(new Cursor(Cursor.HAND_CURSOR)); learnMore.addMouseListener(new MouseAdapter() { @Override @@ -170,13 +170,13 @@ public final class StartupErrorReporter { } }); if (error != null) { - var options = new Object[]{close, BootstrapBundle.message("bootstrap.error.option.reset"), BootstrapBundle.message("bootstrap.error.option.report"), learnMore}; + var options = new Object[]{close, message("bootstrap.error.option.reset"), message("bootstrap.error.option.report"), learnMore}; var choice = JOptionPane.showOptionDialog( JOptionPane.getRootFrame(), messageObj, title, JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE, null, options, options[0] ); switch (choice) { case 1 -> cleanStart(); - case 2 -> reportProblem(title, message, error); + case 2 -> reportProblem(error); } } else { @@ -188,7 +188,7 @@ public final class StartupErrorReporter { } catch (Throwable t) { System.err.println("\n-----"); - System.err.println(BootstrapBundle.message("bootstrap.error.appendix.graphics")); + System.err.println(message("bootstrap.error.appendix.graphics")); t.printStackTrace(System.err); } } @@ -203,78 +203,45 @@ public final class StartupErrorReporter { } } - private static void reportProblem(String title, String description, @Nullable Throwable error) { - if (error != null) { - title += " (" + error.getClass().getSimpleName() + ": " + shorten(error.getMessage()) + ')'; - } - - var uploadId = (String)null; - if (error instanceof ExceptionWithAttachments ewa) { - var message = prepareMessage(BootstrapBundle.message("bootstrap.error.message.confirm")); - var ok = JOptionPane.showConfirmDialog(JOptionPane.getRootFrame(), message, BootstrapBundle.message("bootstrap.error.option.report"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.INFORMATION_MESSAGE); + private static void reportProblem(Throwable error) { + if (error instanceof ExceptionWithAttachments) { + var message = prepareMessage(message("bootstrap.error.message.confirm")); + var ok = JOptionPane.showConfirmDialog(JOptionPane.getRootFrame(), message, message("bootstrap.error.option.report"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.INFORMATION_MESSAGE); if (ok != JOptionPane.OK_OPTION) return; - - try { - uploadId = uploadLogs(ewa); - } - catch (Throwable t) { - var buf = new StringWriter(); - t.printStackTrace(new PrintWriter(buf)); - message = prepareMessage(BootstrapBundle.message("bootstrap.error.message.no.logs", buf)); - JOptionPane.showMessageDialog(JOptionPane.getRootFrame(), message, BootstrapBundle.message("bootstrap.error.title.no.logs"), JOptionPane.ERROR_MESSAGE); - return; - } - } - if (uploadId != null) { - description += "\n\n-----\n[Upload ID: " + uploadId + ']'; } - try { - var url = System.getProperty(REPORT_URL_PROPERTY, "https://youtrack.jetbrains.com/newissue?project=IJPL&clearDraft=true&summary=$TITLE$&description=$DESCR$&c=$SUBSYSTEM$") - .replace("$TITLE$", URLUtil.encodeURIComponent(title)) - .replace("$DESCR$", URLUtil.encodeURIComponent(description)) - .replace("$SUBSYSTEM$", URLUtil.encodeURIComponent("Subsystem: IDE. Startup")); - Desktop.getDesktop().browse(new URI(url)); - } - catch (Throwable t) { - showBrowserError(t); - } - } - - private static String shorten(String message) { - if (message.length() <= 200) return message; - int p = message.indexOf('\n', 200); - if (p < 0 || p >= 250) p = message.indexOf(". ", 200); - if (p < 0 || p >= 250) p = message.indexOf(' ', 200); - if (p < 0 || p >= 250) p = 200; - message = message.substring(0, p); - return message + (message.endsWith(".") ? ".." : "..."); - } - - private static @Nullable String uploadLogs(ExceptionWithAttachments error) throws ExecutionException, InterruptedException { var progressBar = new JProgressBar(); progressBar.setIndeterminate(true); - var label = new JLabel(BootstrapBundle.message("bootstrap.error.message.logs")); + var label = new JLabel(message("bootstrap.error.message.submitting")); var panel = new JPanel(new BorderLayout(5, 5)); panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); panel.add(label, BorderLayout.NORTH); panel.add(progressBar, BorderLayout.CENTER); - var progressDialog = new JDialog(JOptionPane.getRootFrame(), BootstrapBundle.message("bootstrap.error.title.logs"), true); + var progressDialog = new JDialog(JOptionPane.getRootFrame(), message("bootstrap.error.title.submitting"), true); progressDialog.add(panel); progressDialog.setSize(300, 100); progressDialog.setLocationRelativeTo(null); - @SuppressWarnings("SSBasedInspection") - var worker = new SwingWorker() { + @SuppressWarnings({"SSBasedInspection", "UnnecessaryFullyQualifiedName"}) + var worker = new javax.swing.SwingWorker() { @Override protected String doInBackground() throws Exception { - var logs = collectLogs(error); - try { - return LogUploader.uploadFile(logs); - } - finally { - NioFiles.deleteQuietly(logs); + var comment = "Startup error"; + + if (error instanceof ExceptionWithAttachments ewa) { + var logs = collectLogs(ewa); + try { + var uploadId = LogUploader.uploadFile(logs); + comment += "\n\nLogs upload ID: " + uploadId; + } + finally { + NioFiles.deleteQuietly(logs); + } } + + var id = ITNProxy.sendError(new IdeaLoggingEvent(comment, error)); + + return String.valueOf(id); } @Override @@ -287,7 +254,17 @@ public final class StartupErrorReporter { worker.execute(); progressDialog.setVisible(true); - return worker.get(); + try { + var reportId = worker.get(); + var message = message("bootstrap.error.message.submitted", reportId); + JOptionPane.showMessageDialog(JOptionPane.getRootFrame(), message, message("bootstrap.error.title.submitted"), JOptionPane.INFORMATION_MESSAGE); + } + catch (Throwable t) { + var buf = new StringWriter(); + t.printStackTrace(new PrintWriter(buf)); + var message = prepareMessage(message("bootstrap.error.message.no.report", buf)); + JOptionPane.showMessageDialog(JOptionPane.getRootFrame(), message, message("bootstrap.error.title.no.report"), JOptionPane.ERROR_MESSAGE); + } } private static Path collectLogs(ExceptionWithAttachments error) throws IOException { @@ -320,30 +297,30 @@ public final class StartupErrorReporter { try { var backupPath = ConfigBackup.Companion.getNextBackupPath(PathManager.getConfigDir()); CustomConfigMigrationOption.StartWithCleanConfig.INSTANCE.writeConfigMarkerFile(); - var message = BootstrapBundle.message("bootstrap.error.message.reset", backupPath); - JOptionPane.showMessageDialog(JOptionPane.getRootFrame(), message, BootstrapBundle.message("bootstrap.error.title.reset"), JOptionPane.INFORMATION_MESSAGE); + var message = message("bootstrap.error.message.reset", backupPath); + JOptionPane.showMessageDialog(JOptionPane.getRootFrame(), message, message("bootstrap.error.title.reset"), JOptionPane.INFORMATION_MESSAGE); } catch (Throwable t) { - var message = BootstrapBundle.message("bootstrap.error.message.reset.failed", t); - JOptionPane.showMessageDialog(JOptionPane.getRootFrame(), message, BootstrapBundle.message("bootstrap.error.title.reset"), JOptionPane.ERROR_MESSAGE); + var message = message("bootstrap.error.message.reset.failed", t); + JOptionPane.showMessageDialog(JOptionPane.getRootFrame(), message, message("bootstrap.error.title.reset"), JOptionPane.ERROR_MESSAGE); } } private static void showBrowserError(Throwable t) { - var message = prepareMessage(BootstrapBundle.message("bootstrap.error.message.browser", t)); - JOptionPane.showMessageDialog(JOptionPane.getRootFrame(), message, BootstrapBundle.message("bootstrap.error.title.browser"), JOptionPane.ERROR_MESSAGE); + var message = prepareMessage(message("bootstrap.error.message.browser", t)); + JOptionPane.showMessageDialog(JOptionPane.getRootFrame(), message, message("bootstrap.error.title.browser"), JOptionPane.ERROR_MESSAGE); } @SuppressWarnings({"UndesirableClassUsage", "HardCodedStringLiteral"}) private static JScrollPane prepareMessage(String message) { var textPane = new JTextPane(); textPane.setEditable(false); - textPane.setText(message.replaceAll("\t", " ")); + textPane.setText(message.replace("\t", " ")); textPane.setBackground(UIManager.getColor("Panel.background")); textPane.setCaretPosition(0); var scrollPane = new JScrollPane(textPane, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); - scrollPane.setBorder(null); + scrollPane.setBorder(BorderFactory.createEmptyBorder()); var maxHeight = Toolkit.getDefaultToolkit().getScreenSize().height / 2; var maxWidth = Toolkit.getDefaultToolkit().getScreenSize().width / 2; @@ -354,7 +331,7 @@ public final class StartupErrorReporter { return scrollPane; } - public static void processException(@NotNull Throwable t) { + public static void processException(Throwable t) { if (LoadingState.COMPONENTS_LOADED.isOccurred() && !(t instanceof StartupAbortedException)) { if (!(t instanceof ControlFlowException)) { PluginManagerCore.getLogger().error(t); @@ -367,8 +344,9 @@ public final class StartupErrorReporter { if (essentialPluginMissingException != null) { var pluginIds = essentialPluginMissingException.pluginIds; showError( - BootstrapBundle.message("bootstrap.error.title.corrupted"), - BootstrapBundle.message("bootstrap.error.essential.plugins", pluginIds.size(), " " + String.join("\n ", pluginIds) + "\n\n")); + message("bootstrap.error.title.corrupted"), + message("bootstrap.error.essential.plugins", pluginIds.size(), " " + String.join("\n ", pluginIds) + "\n\n") + ); System.exit(AppExitCodes.INSTALLATION_CORRUPTED); } @@ -398,20 +376,20 @@ public final class StartupErrorReporter { PluginManagerCore.disablePlugin(pluginId); var message = new StringWriter(); - message.append(BootstrapBundle.message("bootstrap.error.message.plugin.failed", pluginId.getIdString())); + message.append(message("bootstrap.error.message.plugin.failed", pluginId.getIdString())); message.append("\n\n"); requireNonNullElse(pluginException.getCause(), pluginException).printStackTrace(new PrintWriter(message)); - showError(BootstrapBundle.message("bootstrap.error.title.plugin.init"), message.toString()); //NON-NLS + showError(message("bootstrap.error.title.plugin.init"), message.toString()); //NON-NLS System.exit(AppExitCodes.PLUGIN_ERROR); } else { - showError(BootstrapBundle.message("bootstrap.error.title.start.failed"), t); + showError(message("bootstrap.error.title.start.failed"), t); System.exit(AppExitCodes.STARTUP_EXCEPTION); } } - private static T findCause(Throwable t, Class clazz) { + private static @Nullable T findCause(Throwable t, Class clazz) { while (t != null) { if (clazz.isInstance(t)) { return clazz.cast(t); diff --git a/platform/service-container/resources/messages/BootstrapBundle.properties b/platform/service-container/resources/messages/BootstrapBundle.properties index e93b6e8d8889..136ec4861215 100644 --- a/platform/service-container/resources/messages/BootstrapBundle.properties +++ b/platform/service-container/resources/messages/BootstrapBundle.properties @@ -13,13 +13,15 @@ bootstrap.error.option.support=Learn More bootstrap.error.option.report=Report Problem bootstrap.error.option.reset=Reset Settings\\&Plugins -bootstrap.error.title.logs=Collecting Logs -bootstrap.error.message.logs=Collecting logs, please wait… -bootstrap.error.title.no.logs=Cannot Collect Logs -bootstrap.error.message.no.logs=Cannot collect logs.\n\nThe cause: {0} bootstrap.error.message.confirm=\ The IDE will upload logs, which may contain sensitive data, to uploads.jetbrains.com.\n\ Uploaded logs are only accessible to JetBrains and deleted automatically after 60 days. +bootstrap.error.title.submitting=Submitting Error Report +bootstrap.error.message.submitting=Submitting the error report, please wait\u2026 +bootstrap.error.title.no.report=Cannot Submit Report +bootstrap.error.message.no.report=Cannot submit the error report.\n\nThe cause: {0} +bootstrap.error.title.submitted=Report Submitted +bootstrap.error.message.submitted=Report ID: {0}\nYou can use the number when contacting support for checking the status. bootstrap.error.title.browser=Cannot Open Browser bootstrap.error.message.browser=Cannot launch the default browser.\n\nThe cause: {0}