[platform] extracting LogUploader from LogPacker

(cherry picked from commit 21f595f5b5aed2a8a41738de3181b82ad79027ed)

IJ-CR-159540

GitOrigin-RevId: cb419858a0385aa81c48af746605e573d3c33201
This commit is contained in:
Roman Shevchenko
2025-04-03 18:14:00 +02:00
committed by intellij-monorepo-bot
parent 721af91e43
commit fb8ee055e4
5 changed files with 126 additions and 112 deletions

View File

@@ -32,6 +32,7 @@
<orderEntry type="module" module-name="intellij.platform.core.ui" />
<orderEntry type="module" module-name="intellij.platform.ide.util.io.impl" />
<orderEntry type="module" module-name="intellij.platform.ide.util.netty" />
<orderEntry type="module" module-name="intellij.platform.util.progress" />
<orderEntry type="library" name="kotlinx-coroutines-core" level="project" />
<orderEntry type="library" name="netty-codec-compression" level="project" />
</component>

View File

@@ -6,11 +6,14 @@ import com.intellij.ide.IdeBundle
import com.intellij.ide.actions.COLLECT_LOGS_NOTIFICATION_GROUP
import com.intellij.ide.actions.ReportFeedbackService
import com.intellij.ide.logsUploader.LogPacker
import com.intellij.ide.logsUploader.LogUploader
import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.openapi.components.service
import com.intellij.openapi.progress.checkCanceled
import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.platform.util.progress.reportProgress
import com.intellij.util.io.jackson.obj
import com.intellij.util.ui.IoErrorText
import io.netty.channel.ChannelHandlerContext
@@ -56,18 +59,26 @@ private class UploadLogsService : RestService() {
service<ReportFeedbackService>().coroutineScope.launch {
try {
withBackgroundProgress(project, IdeBundle.message("collect.upload.logs.progress.title"), true) {
try {
val byteOut = BufferExposingByteArrayOutputStream()
val uploadedID = LogPacker.uploadLogs(project)
JsonFactory().createGenerator(byteOut).useDefaultPrettyPrinter().use { writer ->
writer.obj {
writer.writeStringField("Upload_id", uploadedID)
reportProgress { reporter ->
reporter.indeterminateStep {
@Suppress("IncorrectCancellationExceptionHandling")
try {
val file = LogPacker.packLogs(project)
checkCanceled()
val uploadedID = LogUploader.uploadFile(file)
LogUploader.notify(project, uploadedID)
val byteOut = BufferExposingByteArrayOutputStream()
JsonFactory().createGenerator(byteOut).useDefaultPrettyPrinter().use { writer ->
writer.obj {
writer.writeStringField("Upload_id", uploadedID)
}
}
send(byteOut, request, context)
}
catch (_: CancellationException) {
sendStatus(HttpResponseStatus.BAD_REQUEST, false, channel)
}
}
send(byteOut, request, context)
}
catch (_: CancellationException) {
sendStatus(HttpResponseStatus.BAD_REQUEST, false, channel)
}
}
}

View File

@@ -1,20 +1,30 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// 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.ide.actions
import com.intellij.ide.IdeBundle
import com.intellij.ide.logsUploader.LogPacker
import com.intellij.ide.logsUploader.LogUploader
import com.intellij.openapi.progress.checkCanceled
import com.intellij.openapi.project.Project
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.platform.util.progress.reportProgress
import kotlinx.coroutines.CoroutineScope
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
class DefaultReportFeedbackService(override val coroutineScope: CoroutineScope): ReportFeedbackService {
override suspend fun collectLogs(project: Project?): String? {
if (project == null) return null
return withBackgroundProgress(project, IdeBundle.message("reportProblemAction.progress.title.submitting"), true) {
LogPacker.getBrowseUrl(LogPacker.uploadLogs(project))
override suspend fun collectLogs(project: Project?): String? =
if (project == null) null
else withBackgroundProgress(project, IdeBundle.message("reportProblemAction.progress.title.submitting"), true) {
val id = reportProgress { reporter ->
reporter.indeterminateStep("") {
val file = LogPacker.packLogs(project)
checkCanceled()
val id = LogUploader.uploadFile(file)
LogUploader.notify(project, id)
id
}
}
LogUploader.getBrowseUrl(id)
}
}
}

View File

@@ -1,60 +1,37 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// 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.ide.logsUploader
import com.fasterxml.jackson.core.JsonFactory
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import com.intellij.diagnostic.MacOSDiagnosticReportDirectories
import com.intellij.diagnostic.PerformanceWatcher
import com.intellij.ide.IdeBundle
import com.intellij.ide.actions.COLLECT_LOGS_NOTIFICATION_GROUP
import com.intellij.ide.troubleshooting.CompositeGeneralTroubleInfoCollector
import com.intellij.ide.troubleshooting.collectDimensionServiceDiagnosticsData
import com.intellij.idea.LoggerFactory
import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.openapi.application.ApplicationNamesInfo
import com.intellij.openapi.application.PathManager
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.checkCanceled
import com.intellij.openapi.progress.coroutineToIndicator
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.SystemInfoRt
import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream
import com.intellij.platform.util.progress.reportProgress
import com.intellij.troubleshooting.TroubleInfoCollector
import com.intellij.util.SystemProperties
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.util.io.Compressor
import com.intellij.util.io.HttpRequests
import com.intellij.util.io.jackson.obj
import com.intellij.util.net.NetUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
import java.io.IOException
import java.net.HttpURLConnection
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import java.text.SimpleDateFormat
import java.util.*
import kotlin.io.path.*
import kotlin.io.path.exists
import kotlin.io.path.forEachDirectoryEntry
import kotlin.io.path.isDirectory
import kotlin.io.path.name
@ApiStatus.Internal
object LogPacker {
private const val UPLOADS_SERVICE_URL = "https://uploads.jetbrains.com"
private val gson: Gson by lazy {
GsonBuilder()
.setPrettyPrinting()
.disableHtmlEscaping()
.create()
}
@JvmStatic
@RequiresBackgroundThread
@Throws(IOException::class)
@@ -135,73 +112,6 @@ object LogPacker {
archive
}
@RequiresBackgroundThread
@Throws(IOException::class)
suspend fun uploadLogs(project: Project?): String = reportProgress { reporter ->
reporter.indeterminateStep("") {
val file = packLogs(project)
checkCanceled()
val folderName = uploadFile(file)
val message = IdeBundle.message("collect.logs.notification.sent.success", UPLOADS_SERVICE_URL, folderName)
Notification(COLLECT_LOGS_NOTIFICATION_GROUP, message, NotificationType.INFORMATION).notify(project)
folderName
}
}
suspend fun uploadFile(file: Path): String {
val responseJson = requestSign(file.name)
val uploadUrl = responseJson["url"] as String
val folderName = responseJson["folderName"] as String
val headers = responseJson["headers"] as Map<*, *>
checkCanceled()
coroutineToIndicator {
upload(file, uploadUrl, headers)
}
return folderName
}
private fun requestSign(fileName: String): Map<String, Any> {
return HttpRequests.post("$UPLOADS_SERVICE_URL/sign", HttpRequests.JSON_CONTENT_TYPE)
.accept(HttpRequests.JSON_CONTENT_TYPE)
.connect { request ->
val out = BufferExposingByteArrayOutputStream()
JsonFactory().createGenerator(out).useDefaultPrettyPrinter().use { writer ->
writer.obj {
writer.writeStringField("filename", fileName)
writer.writeStringField("method", "put")
writer.writeStringField("contentType", "application/octet-stream")
}
}
request.write(out.toByteArray())
gson.fromJson(request.reader, object : TypeToken<Map<String, Any?>?>() {}.type)
}
}
private fun upload(file: Path, uploadUrl: String, headers: Map<*, *>) {
val indicator = ProgressManager.getGlobalProgressIndicator()
HttpRequests.put(uploadUrl, "application/octet-stream")
.productNameAsUserAgent()
.tuner { urlConnection ->
headers.forEach {
urlConnection.addRequestProperty(it.key as String, it.value as String)
}
}
.connect {
val http = it.connection as HttpURLConnection
val length = file.fileSize()
http.setFixedLengthStreamingMode(length)
http.outputStream.use { outputStream ->
file.inputStream().buffered(64 * 1024).use { inputStream ->
NetUtils.copyStreamContent(indicator, inputStream, outputStream, length)
}
}
}
}
fun getBrowseUrl(folderName: String): String = "$UPLOADS_SERVICE_URL/browse#$folderName"
private fun doesMacOSDiagnosticReportBelongToThisApp(path: Path): Boolean {
val name = path.name
if (name.contains(ApplicationNamesInfo.getInstance().scriptName, ignoreCase = true)) return true

View File

@@ -0,0 +1,82 @@
// 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.ide.logsUploader;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.intellij.ide.IdeBundle;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.progress.ProgressIndicatorProvider;
import com.intellij.openapi.project.Project;
import com.intellij.util.io.HttpRequests;
import com.intellij.util.net.NetUtils;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import static com.intellij.ide.actions.CollectZippedLogsActionKt.COLLECT_LOGS_NOTIFICATION_GROUP;
@ApiStatus.Internal
public final class LogUploader {
private static final String SERVICE_URL = "https://uploads.jetbrains.com";
private static final String BYTES_CONTENT_TYPE = "application/octet-stream";
private static final String JSON_CONTENT_TYPE = "application/json";
public static @NotNull String uploadFile(@NotNull Path file) throws IOException {
return uploadFile(file, file.getFileName().toString());
}
public static @NotNull String uploadFile(@NotNull Path file, @NotNull String fileName) throws IOException {
var mapper = new ObjectMapper();
var requestObj = mapper.writeValueAsString(Map.of(
"filename", fileName,
"method", "put",
"contentType", BYTES_CONTENT_TYPE
));
var responseObj = HttpRequests.post(SERVICE_URL + "/sign", JSON_CONTENT_TYPE + "; charset=utf-8")
.accept(JSON_CONTENT_TYPE)
.connect(request -> {
request.write(requestObj);
return mapper.readValue(request.getReader(), Map.class);
});
var uploadUrl = responseObj.get("url").toString();
@SuppressWarnings("unchecked")
var headers = (Map<String, String>)responseObj.get("headers");
var id = responseObj.get("folderName").toString();
@SuppressWarnings("UsagesOfObsoleteApi")
var indicator = ProgressIndicatorProvider.getGlobalProgressIndicator();
HttpRequests.put(uploadUrl, "application/octet-stream")
.productNameAsUserAgent()
.tuner(urlConnection -> headers.forEach((k, v) -> urlConnection.addRequestProperty(k, v)))
.connect(it -> {
var http = ((HttpURLConnection)it.getConnection());
var length = Files.size(file);
http.setFixedLengthStreamingMode(length);
try (var outputStream = http.getOutputStream(); var inputStream = new BufferedInputStream(http.getInputStream(), 64 * 1024)) {
NetUtils.copyStreamContent(indicator, inputStream, outputStream, length);
}
return null;
});
return id;
}
public static void notify(@Nullable Project project, @NotNull String id) {
var message = IdeBundle.message("collect.logs.notification.sent.success", SERVICE_URL, id);
new Notification(COLLECT_LOGS_NOTIFICATION_GROUP, message, NotificationType.INFORMATION)
.notify(project);
}
public static @NotNull String getBrowseUrl(@NotNull String id) {
return SERVICE_URL + "/browse#" + id;
}
}