diff --git a/build/src/org/jetbrains/intellij/build/OpenIdeProperties.kt b/build/src/org/jetbrains/intellij/build/OpenIdeProperties.kt index 4b56bd45b02b..2657101ccc7d 100644 --- a/build/src/org/jetbrains/intellij/build/OpenIdeProperties.kt +++ b/build/src/org/jetbrains/intellij/build/OpenIdeProperties.kt @@ -91,7 +91,7 @@ open class OpenIdeProperties(val communityHomeDir: Path): IdeaCommunityPropertie override fun getOutputDirectoryName(appInfo: ApplicationInfoProperties) = "open-ide" - override fun getAdditionalPluginPaths(context: BuildContext): List { + override suspend fun getAdditionalPluginPaths(context: BuildContext): List { return OpenIdePluginBundler.getBundlePluginPaths(context) } } \ No newline at end of file diff --git a/community-resources/resources/idea/IdeaApplicationInfo.xml b/community-resources/resources/idea/IdeaApplicationInfo.xml index 14bcce138fa6..309e07162452 100644 --- a/community-resources/resources/idea/IdeaApplicationInfo.xml +++ b/community-resources/resources/idea/IdeaApplicationInfo.xml @@ -5,7 +5,7 @@ - + diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/CommunityLibraryLicenses.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/CommunityLibraryLicenses.kt index 5f0d68b216ab..c7ee3c7f5d8b 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/CommunityLibraryLicenses.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/CommunityLibraryLicenses.kt @@ -1406,6 +1406,9 @@ object CommunityLibraryLicenses { LibraryLicense("zstd-jni", libraryName = "zstd-jni", url = "https://github.com/luben/zstd-jni") .simplifiedBsd("https://github.com/luben/zstd-jni/blob/master/LICENSE"), + LibraryLicense("sentry-java", libraryName = "io.sentry", url = "https://sentry.io/") + .mit("https://github.com/getsentry/sentry-java/blob/main/LICENSE"), + jetbrainsLibrary("ai.grazie.emb"), jetbrainsLibrary("ai.grazie.nlp.detect"), jetbrainsLibrary("ai.grazie.nlp.encoder.bert.uncased"), diff --git a/platform/statistics/uploader/src/com/intellij/internal/statistic/eventLog/connection/ConfigurationClientFactory.kt b/platform/statistics/uploader/src/com/intellij/internal/statistic/eventLog/connection/ConfigurationClientFactory.kt index 4af27c8f863e..add5f68c36da 100644 --- a/platform/statistics/uploader/src/com/intellij/internal/statistic/eventLog/connection/ConfigurationClientFactory.kt +++ b/platform/statistics/uploader/src/com/intellij/internal/statistic/eventLog/connection/ConfigurationClientFactory.kt @@ -5,49 +5,87 @@ package com.intellij.internal.statistic.eventLog.connection import com.jetbrains.fus.reporting.configuration.ConfigurationClient -import com.jetbrains.fus.reporting.model.http.StatsConnectionSettings +import com.jetbrains.fus.reporting.configuration.RegionCode +import com.jetbrains.fus.reporting.model.http.HttpClientBuilder +import com.jetbrains.fus.reporting.model.http.HttpRequestBuilder +import com.jetbrains.fus.reporting.model.serialization.Serializer import java.util.concurrent.TimeUnit class ConfigurationClientFactory { companion object { /** - * The remote client periodically downloads configuration. - * The frequency can be set, it is 10 minutes by default. - * Use the test configuration url in test cases. - * */ + * The remote client periodically downloads configuration for the given recorder id, + * product code and product version. + * @param recorderId Recorder id + * @param productCode Product code + * @param productVersion Product version + * @param isTestConfiguration Set it to true to use the test configuration url, + * otherwise set it false to use the production one. + * @param httpClientBuilder FUS-client supports java http client com.jetbrains.fus.reporting.connection.JavaHttpClientBuilder, + * You can configure it or use another http client [com.jetbrains.fus.reporting.model.http.HttpClientBuilder]. + * @param httpRequestBuilder FUS-client supports java http request com.jetbrains.fus.reporting.connection.JavaHttpRequestBuilder, + * You can configure it or use another http request [com.jetbrains.fus.reporting.model.http.HttpRequestBuilder]. + * @param regionCode The region code for the downloading configuration, + * configuration url depends on the region code. + * Configuration template url for China region code is [com.jetbrains.fus.reporting.configuration.RegionCode.CN]. + * Configuration template url for all other regions is [com.jetbrains.fus.reporting.configuration.RegionCode.ALL]. + * @param cacheTimeoutMs The frequency of the downloading configuration, it is 10 minutes by default. + * @param serializer FUS-client supports jackson serialization com.jetbrains.fus.reporting.serialization.FusJacksonSerializer, + * kotlin serialization com.jetbrains.fus.reporting.serialization.FusKotlinSerializer. + * You can use own serializer implementation [com.jetbrains.fus.reporting.model.serialization.Serializer]. + */ @JvmStatic fun create( recorderId: String, productCode: String, productVersion: String, - isTestConfig: Boolean, - connectionSettings: StatsConnectionSettings, - cacheTimeoutMs: Long = TimeUnit.MINUTES.toMillis(10) + isTestConfiguration: Boolean, + httpClientBuilder: HttpClientBuilder, + httpRequestBuilder: HttpRequestBuilder, + regionCode: RegionCode = RegionCode.ALL, + cacheTimeoutMs: Long = TimeUnit.MINUTES.toMillis(10), + serializer: Serializer + ): ConfigurationClient { + return createConfigurationClient( + recorderId, + productCode, + productVersion, + isTestConfiguration, + httpClientBuilder, + httpRequestBuilder, + regionCode, + cacheTimeoutMs, + serializer + ) + } + + private fun createConfigurationClient( + recorderId: String, + productCode: String, + productVersion: String, + isTestConfiguration: Boolean, + httpClientBuilder: HttpClientBuilder, + httpRequestBuilder: HttpRequestBuilder, + regionCode: RegionCode, + cacheTimeoutMs: Long, + serializer: Serializer ): ConfigurationClient { val eventLogSettingsURLTemplate = "https://stats.openide.ru/storage/fus/config/v4/%s/%s.json" val configurationUrl: String = - if (isTestConfig) { + if (isTestConfiguration) { String.format(eventLogSettingsURLTemplate, "test/$recorderId", productCode) } else { String.format(eventLogSettingsURLTemplate, recorderId, productCode) } - - return RemoteConfigurationClient(configurationUrl, productCode, productVersion, connectionSettings, cacheTimeoutMs) + return RemoteConfigurationClient( + configurationUrl, + productCode, + productVersion, + httpClientBuilder.newClient(configurationUrl), + httpRequestBuilder.newGetRequest(configurationUrl), + serializer, + cacheTimeoutMs + ) } - - /** - * The test client periodically downloads configuration from the given test configuration url. - * The frequency can be set, it is 10 minutes by default. - * */ - @JvmStatic - fun createTest( - productCode: String, - productVersion: String, - connectionSettings: StatsConnectionSettings, - cacheTimeoutMs: Long = TimeUnit.MINUTES.toMillis(10), - configurationUrl: String - ): ConfigurationClient = - RemoteConfigurationClient(configurationUrl, productCode, productVersion, connectionSettings, cacheTimeoutMs) - } } \ No newline at end of file diff --git a/platform/statistics/uploader/src/com/intellij/internal/statistic/eventLog/connection/RemoteConfigurationClient.kt b/platform/statistics/uploader/src/com/intellij/internal/statistic/eventLog/connection/RemoteConfigurationClient.kt index a67df0889637..d66d1e1aa507 100644 --- a/platform/statistics/uploader/src/com/intellij/internal/statistic/eventLog/connection/RemoteConfigurationClient.kt +++ b/platform/statistics/uploader/src/com/intellij/internal/statistic/eventLog/connection/RemoteConfigurationClient.kt @@ -5,32 +5,27 @@ package com.intellij.internal.statistic.eventLog.connection import com.jetbrains.fus.reporting.configuration.ConfigurationClient - -import com.jetbrains.fus.reporting.connection.StatsHttpClient -import com.jetbrains.fus.reporting.connection.StatsHttpResponse -import com.jetbrains.fus.reporting.connection.StatsRequestBuilder -import com.jetbrains.fus.reporting.connection.StatsResponseProcessor import com.jetbrains.fus.reporting.model.config.v4.Configuration import com.jetbrains.fus.reporting.model.config.v4.ConfigurationVersion import com.jetbrains.fus.reporting.model.exceptions.StatsResponseException -import com.jetbrains.fus.reporting.model.http.StatsConnectionSettings -import com.jetbrains.fus.reporting.serialization.FusKotlinSerializer +import com.jetbrains.fus.reporting.model.http.HttpClient +import com.jetbrains.fus.reporting.model.http.HttpRequest +import com.jetbrains.fus.reporting.model.http.HttpResponse +import com.jetbrains.fus.reporting.model.serialization.SerializationException +import com.jetbrains.fus.reporting.model.serialization.Serializer +import java.io.IOException import java.io.InputStream import java.nio.charset.StandardCharsets -/** - * The remote client periodically downloads configuration from the given url. - * The download frequency can be set. - * */ internal class RemoteConfigurationClient internal constructor( override val configurationUrl: String, override val productCode: String, private val productVersion: String, - connectionSettings: StatsConnectionSettings, + private val httpClient: HttpClient, + private val httpRequest: HttpRequest, + private val serializer: Serializer, cacheTimeoutMs: Long ) : ConfigurationClient() { - private val httpClient = StatsHttpClient.newClient(connectionSettings, configurationUrl) - private val httpRequest = StatsRequestBuilder.newGetRequest(connectionSettings, configurationUrl) private val cachedConfigurationVersion: StatisticsCachingSupplier = StatisticsCachingSupplier(::loadConfiguration, cacheTimeoutMs) @@ -49,20 +44,20 @@ internal class RemoteConfigurationClient internal constructor( * @throws [SerializationException] if the given JSON string is not a valid JSON input for the type [Configuration]. */ private fun loadConfiguration(): ConfigurationVersion? { - val function = object : StatsResponseProcessor { - override fun onSucceed(response: StatsHttpResponse): ConfigurationVersion? { + val processor = object : ResponseProcessor { + override fun onSucceed(response: HttpResponse): ConfigurationVersion? { val responseContent: InputStream? = response.read() if (responseContent != null) { val content = responseContent.bufferedReader(StandardCharsets.UTF_8).use { reader -> reader.readText() } - val configuration = FusKotlinSerializer().deserializeConfigurationV4(content) + val configuration = serializer.deserializeConfiguration(content) return configuration.findProductVersion(productVersion) } return null } } - val statsRequestResult = StatsHttpClient(httpClient).send(function, httpRequest) + val statsRequestResult = HttpClientProcessor.send(httpClient, httpRequest, processor) if (statsRequestResult.errorCode != -1) throw StatsResponseException( "Http response status code to get fus reporting " + @@ -73,4 +68,87 @@ internal class RemoteConfigurationClient internal constructor( ) return statsRequestResult.result } +} + + +internal interface ResponseProcessor { + /** + * ResponseProcessor matches [HttpResponse] to [T] on succeed + */ + @Throws(IOException::class, InterruptedException::class, SecurityException::class) + fun onSucceed(response: HttpResponse): T +} + + +internal object HttpClientProcessor { + /** + * Java http client sends http request [MAX_RETRIES] times, + * if response code isn't 200 with an interval between attempts [RETRY_INTERVAL]. + * + * @throws[java.io.IOException] if an I/O error occurs when sending request to the server or receiving response + * from the server. + * @throws[InterruptedException] if the http client operation is interrupted. + * @throws[SecurityException] If a security manager has been installed, and it denies + * {@link java.net.URLPermission access} to the URL in the given request, or proxy if one is configured. + * + */ + @Throws(IOException::class, InterruptedException::class, SecurityException::class) + fun send(httpClient: HttpClient, httpRequest: HttpRequest, processor: ResponseProcessor): RequestResult { + val response = trySend(httpClient, httpRequest) + val code = response?.statusCode + if (code == SUCCESS_CODE) { + val result: T = processor.onSucceed(response) + return RequestResult.succeed(result) + } else { + return RequestResult.error(code) + } + } + + @Throws(IOException::class, InterruptedException::class, SecurityException::class) + private fun trySend(httpClient: HttpClient, httpRequest: HttpRequest): HttpResponse? { + var retryCounter = 0 + while (retryCounter < MAX_RETRIES) { + val response = httpClient.send(httpRequest) + retryCounter++ + if (CAN_RETRY_CODES.contains(response.statusCode) && (retryCounter != MAX_RETRIES - 1)) { + Thread.sleep(RETRY_INTERVAL.toLong()) + } else { + return response + } + } + return null + } + + private const val SUCCESS_CODE = 200 + private val CAN_RETRY_CODES: List = mutableListOf( + // Standard status codes + 408, // Request Timeout + 429, // Too Many Requests (RFC 6585) + 500, // Internal Server Error + 502, // Bad Gateway + 503, // Service Unavailable + 504, // Gateway Timeout + // Unofficial codes + 598 // Some HTTP Proxies timeout + ) + private const val MAX_RETRIES = 10 + private const val RETRY_INTERVAL = 500 +} + +internal class RequestResult(val result: T, val errorCode: Int?) { + companion object { + /** + * Get a result of type [T] if server response code is 200, error code is -1 in this case + */ + fun succeed(result: T): RequestResult { + return RequestResult(result, -1) + } + + /** + * Get an error code if server response code isn't 200, result of type [T] is null in this case + */ + fun error(error: Int?): RequestResult { + return RequestResult(null, error) + } + } } \ No newline at end of file