IDEA-291623 Show a warning if the settings pack exceeds some reasonably big limit

GitOrigin-RevId: 75a34604baff0474b930c5f88c5ea61cc3539976
This commit is contained in:
Filipp Vakhitov
2023-07-11 18:40:37 +03:00
committed by intellij-monorepo-bot
parent b20b0dcb54
commit 39672a62c5
9 changed files with 134 additions and 50 deletions

View File

@@ -83,5 +83,6 @@
<orderEntry type="module" module-name="intellij.platform.workspace.jps" />
<orderEntry type="module" module-name="intellij.platform.testFramework.junit5" scope="TEST" />
<orderEntry type="module" module-name="intellij.performanceTesting" />
<orderEntry type="library" scope="TEST" name="mockito" level="project" />
</component>
</module>

View File

@@ -21,6 +21,8 @@
<applicationService serviceImplementation="com.intellij.settingsSync.SettingsSyncSettings"/>
<applicationService serviceImplementation="com.intellij.settingsSync.SettingsSyncEvents"/>
<applicationService serviceImplementation="com.intellij.settingsSync.plugins.SettingsSyncPluginManager"/>
<applicationService serviceImplementation="com.intellij.settingsSync.notification.NotificationServiceImpl"
serviceInterface="com.intellij.settingsSync.notification.NotificationService"/>
<!--suppress PluginXmlDynamicPlugin -->
<applicationInitializedListener implementation="com.intellij.settingsSync.SettingsSynchronizer"/>
<applicationConfigurable id="settings.sync"

View File

@@ -85,6 +85,9 @@ sync.status.login.not.available=To enable Setting Sync, install
sync.status.restart.required=To enable Setting Sync, restart {0}
sync.status.restart.ide.button=Restart
sync.notification.size.exceed.title=Settings pack exceeds maximum allowed limit
sync.notification.size.exceed.text=Please reach out to us at https://youtrack.jetbrains.com/ and attach your logs and settings to address the issue
settings.cross.product.sync=Sync settings across:
# {0} is the current product name, e.g. 'IntelliJ IDEA Community Edition' or 'WebStorm'
settings.cross.product.sync.choice.only.this.product={0} instances only

View File

@@ -6,6 +6,7 @@ import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.util.JDOMUtil
import com.intellij.openapi.util.io.FileUtil
import com.intellij.settingsSync.auth.SettingsSyncAuthService
import com.intellij.settingsSync.notification.NotificationService
import com.intellij.util.io.HttpRequests
import com.intellij.util.io.delete
import com.intellij.util.io.inputStream
@@ -188,6 +189,11 @@ internal open class CloudConfigServerCommunicator : SettingsSyncRemoteCommunicat
val zip = try {
SettingsSnapshotZipSerializer.serializeToZip(snapshot)
}
catch (e: SettingsSnapshotZipSerializer.ZipSizeExceedException) {
LOG.warn(e)
NotificationService.getInstance().notifyZipSizeExceed()
return SettingsSyncPushResult.Error(e.message ?: "Couldn't prepare zip file")
}
catch (e: Throwable) {
LOG.warn(e)
return SettingsSyncPushResult.Error(e.message ?: "Couldn't prepare zip file")

View File

@@ -7,10 +7,10 @@ import com.intellij.openapi.util.BuildNumber
import com.intellij.openapi.util.io.FileUtil
import com.intellij.settingsSync.plugins.SettingsSyncPluginsState
import com.intellij.util.io.*
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.OutputStream
import java.lang.RuntimeException
import java.nio.file.Files
import java.nio.file.Path
import java.time.Instant
@@ -27,6 +27,8 @@ internal object SettingsSnapshotZipSerializer {
private const val INFO = "info.json"
private const val PLUGINS = "plugins.json"
private const val MAX_ZIP_SIZE = 524288 // bytes
private val LOG = logger<SettingsSnapshotZipSerializer>()
private val json = Json { prettyPrint = true }
@@ -34,6 +36,9 @@ internal object SettingsSnapshotZipSerializer {
fun serializeToZip(snapshot: SettingsSnapshot): Path {
val file = FileUtil.createTempFile(SETTINGS_SYNC_SNAPSHOT_ZIP, null)
serialize(snapshot, Compressor.Zip(file))
if (file.length() > MAX_ZIP_SIZE) {
throw ZipSizeExceedException(file.length())
}
return file.toPath()
}
@@ -183,4 +188,10 @@ internal object SettingsSnapshotZipSerializer {
var configFolder: String = ""
var isDeleted: Boolean = false
}
class ZipSizeExceedException(private val size: Long): RuntimeException() {
override fun toString(): String {
return "Zip size $size excesses maximum allowed zip size"
}
}
}

View File

@@ -4,14 +4,7 @@ import com.intellij.concurrency.ConcurrentCollectionFactory
import com.intellij.configurationStore.*
import com.intellij.configurationStore.schemeManager.SchemeManagerFactoryBase
import com.intellij.configurationStore.schemeManager.SchemeManagerImpl
import com.intellij.notification.Notification
import com.intellij.notification.NotificationAction
import com.intellij.notification.NotificationGroupManager
import com.intellij.notification.NotificationType
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ApplicationNamesInfo
import com.intellij.openapi.application.PathManager.OPTIONS_DIRECTORY
import com.intellij.openapi.application.ex.ApplicationEx
import com.intellij.openapi.application.invokeAndWaitIfNeeded
import com.intellij.openapi.components.RoamingType
import com.intellij.openapi.components.StateStorage
@@ -21,11 +14,10 @@ import com.intellij.openapi.editor.colors.EditorColorsManager
import com.intellij.openapi.options.SchemeManagerFactory
import com.intellij.openapi.util.registry.Registry
import com.intellij.settingsSync.SettingsSnapshot.MetaInfo
import com.intellij.settingsSync.notification.NotificationService
import com.intellij.settingsSync.plugins.SettingsSyncPluginManager
import com.intellij.util.io.*
import org.jdom.Element
import java.io.InputStream
import java.lang.RuntimeException
import java.nio.file.FileVisitResult
import java.nio.file.Files
import java.nio.file.Path
@@ -35,8 +27,6 @@ import java.time.Instant
import java.util.concurrent.locks.ReadWriteLock
import java.util.concurrent.locks.ReentrantReadWriteLock
import java.util.function.Predicate
import kotlin.collections.ArrayList
import kotlin.collections.LinkedHashSet
import kotlin.concurrent.withLock
import kotlin.io.path.exists
import kotlin.io.path.name
@@ -105,43 +95,9 @@ internal class SettingsSyncIdeMediatorImpl(private val componentStore: Component
notifyRestartNeeded()
}
private fun notifyRestartNeeded() {
if (restartRequiredReasons.isEmpty())
return
val notification = buildRestartNeededNotification()
notification.addAction(NotificationAction.create(
SettingsSyncBundle.message("sync.restart.notification.action", ApplicationNamesInfo.getInstance().fullProductName),
com.intellij.util.Consumer {
val app = ApplicationManager.getApplication() as ApplicationEx
app.restart(true)
}))
notification.notify(null)
}
private fun buildRestartNeededNotification(): Notification {
fun getMultiReasonRestartMessage(): String {
assert(restartRequiredReasons.size > 1)
val message = StringBuilder(SettingsSyncBundle.message("sync.restart.notification.message.subtitle")).append('\n')
val sortedRestartReasons = restartRequiredReasons.sorted()
for ((counter, reason) in sortedRestartReasons.withIndex()) {
message.append(reason.getMultiReasonNotificationListEntry(counter + 1))
}
message.dropLast(0) // we do not need the new line in the end
return message.toString()
}
val message = when {
restartRequiredReasons.isEmpty() -> throw RuntimeException("No restart reasons found")
restartRequiredReasons.size == 1 -> restartRequiredReasons.first().getSingleReasonNotificationMessage()
else -> getMultiReasonRestartMessage()
}
return NotificationGroupManager.getInstance().getNotificationGroup(NOTIFICATION_GROUP)
.createNotification(SettingsSyncBundle.message("sync.restart.notification.title"),
message,
NotificationType.INFORMATION)
if (restartRequiredReasons.isEmpty()) return
NotificationService.getInstance().notifyRestartNeeded(restartRequiredReasons)
}
override fun activateStreamProvider() {

View File

@@ -0,0 +1,16 @@
package com.intellij.settingsSync.notification
import com.intellij.notification.Notification
import com.intellij.openapi.application.ApplicationManager
import com.intellij.settingsSync.RestartReason
interface NotificationService {
companion object {
fun getInstance(): NotificationService = ApplicationManager.getApplication().getService(NotificationService::class.java)
}
fun notifyZipSizeExceed()
fun buildZipSizeExceedNotification(): Notification
fun notifyRestartNeeded(reasons: Collection<RestartReason>)
fun buildRestartNeededNotification(reasons: Collection<RestartReason>): Notification
}

View File

@@ -0,0 +1,63 @@
package com.intellij.settingsSync.notification
import com.intellij.notification.Notification
import com.intellij.notification.NotificationAction
import com.intellij.notification.NotificationGroupManager
import com.intellij.notification.NotificationType
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ApplicationNamesInfo
import com.intellij.openapi.application.ex.ApplicationEx
import com.intellij.settingsSync.NOTIFICATION_GROUP
import com.intellij.settingsSync.RestartReason
import com.intellij.settingsSync.SettingsSyncBundle
import java.lang.RuntimeException
internal class NotificationServiceImpl: NotificationService {
override fun notifyZipSizeExceed() {
val notification = buildZipSizeExceedNotification()
notification.notify(null)
}
override fun buildZipSizeExceedNotification(): Notification {
return NotificationGroupManager.getInstance().getNotificationGroup(NOTIFICATION_GROUP)
.createNotification(SettingsSyncBundle.message("sync.notification.size.exceed.title"),
SettingsSyncBundle.message("sync.notification.size.exceed.text"),
NotificationType.ERROR)
}
override fun notifyRestartNeeded(reasons: Collection<RestartReason>) {
val notification = buildRestartNeededNotification(reasons)
notification.addAction(NotificationAction.create(
SettingsSyncBundle.message("sync.restart.notification.action", ApplicationNamesInfo.getInstance().fullProductName),
com.intellij.util.Consumer {
val app = ApplicationManager.getApplication() as ApplicationEx
app.restart(true)
}))
notification.notify(null)
}
override fun buildRestartNeededNotification(reasons: Collection<RestartReason>): Notification {
fun getMultiReasonRestartMessage(): String {
assert(reasons.size > 1)
val message = StringBuilder(SettingsSyncBundle.message("sync.restart.notification.message.subtitle")).append('\n')
val sortedRestartReasons = reasons.sorted()
for ((counter, reason) in sortedRestartReasons.withIndex()) {
message.append(reason.getMultiReasonNotificationListEntry(counter + 1))
}
message.dropLast(0) // we do not need the new line in the end
return message.toString()
}
val message = when {
reasons.isEmpty() -> throw RuntimeException("No restart reasons provided")
reasons.size == 1 -> reasons.first().getSingleReasonNotificationMessage()
else -> getMultiReasonRestartMessage()
}
return NotificationGroupManager.getInstance().getNotificationGroup(NOTIFICATION_GROUP)
.createNotification(SettingsSyncBundle.message("sync.restart.notification.title"),
message,
NotificationType.INFORMATION)
}
}

View File

@@ -3,24 +3,36 @@ package com.intellij.settingsSync
import com.intellij.configurationStore.getPerOsSettingsStorageFolderName
import com.intellij.ide.GeneralSettings
import com.intellij.ide.ui.UISettings
import com.intellij.idea.TestFor
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.*
import com.intellij.openapi.editor.ex.EditorSettingsExternalizable
import com.intellij.openapi.keymap.impl.KeymapImpl
import com.intellij.openapi.keymap.impl.KeymapManagerImpl
import com.intellij.openapi.util.Disposer
import com.intellij.settingsSync.SettingsSnapshot.MetaInfo
import com.intellij.settingsSync.notification.NotificationService
import com.intellij.settingsSync.notification.NotificationServiceImpl
import com.intellij.testFramework.replaceService
import com.intellij.util.io.readText
import com.intellij.util.toByteArray
import com.intellij.util.xmlb.annotations.Attribute
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.Mockito
import org.mockito.Mockito.verify
import java.nio.charset.Charset
import java.time.Instant
import java.util.*
import kotlin.io.path.div
import kotlin.io.path.exists
@@ -287,8 +299,8 @@ internal class SettingsSyncRealIdeTest : SettingsSyncRealIdeTestBase() {
//assertTrue("Didn't await for the push request", cdl.await(5, TIMEOUT_UNIT))
}
// temporarily disabled: the failure needs to be investigated
//@Test
@Test
@Ignore // TODO investigate
fun `local and remote changes in different files are both applied`() {
val generalSettings = GeneralSettings.getInstance().init()
initSettingsSync(SettingsSyncBridge.InitMode.JustInit)
@@ -331,6 +343,20 @@ internal class SettingsSyncRealIdeTest : SettingsSyncRealIdeTestBase() {
assertFalse(EditorSettingsExternalizable.getInstance().isShowIntentionBulb)
}
@TestFor(issues = ["IDEA-291623"])
@Test
fun `zip file size limit exceed`() {
val notificationServiceSpy = Mockito.spy<NotificationServiceImpl>()
ApplicationManager.getApplication().replaceService(NotificationService::class.java, notificationServiceSpy, disposable)
EditorSettingsExternalizable.getInstance().initModifyAndSave {
languageBreadcrumbsMap = (1..100000).associate { UUID.randomUUID().toString() to true } // please FIXME if you now a better way to make a fat file
}
initSettingsSync(SettingsSyncBridge.InitMode.JustInit)
verify(notificationServiceSpy).notifyZipSizeExceed()
}
//@Test
fun `only changed components should be reloaded`() {
TODO()