From 05a5d161fb6c5b5a109b858088a406ac06e0571c Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Tue, 7 Jan 2025 16:52:01 +0700 Subject: [PATCH] desktop: saving settings in a safer way to handle process death (#4687) * desktop: saving settings in a safer way to handle process death * enhancements * unused * changes * rename --- .../simplex/common/platform/Files.android.kt | 2 + .../chat/simplex/common/model/SimpleXAPI.kt | 8 +++- .../chat/simplex/common/platform/Files.kt | 28 +++++++++---- .../simplex/common/ui/theme/ThemeManager.kt | 4 +- .../simplex/common/views/helpers/Utils.kt | 5 ++- .../common/views/onboarding/WhatsNewView.kt | 6 ++- .../commonMain/resources/MR/base/strings.xml | 1 + .../simplex/common/platform/Files.desktop.kt | 2 + .../common/platform/Resources.desktop.kt | 41 +++++++++++++++---- 9 files changed, 73 insertions(+), 24 deletions(-) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt index bfe961a512..03012e318c 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt @@ -19,6 +19,8 @@ actual val wallpapersDir: File = File(filesDir.absolutePath + File.separator + " actual val coreTmpDir: File = File(filesDir.absolutePath + File.separator + "temp_files") actual val dbAbsolutePrefixPath: String = dataDir.absolutePath + File.separator + "files" actual val preferencesDir = File(dataDir.absolutePath + File.separator + "shared_prefs") +actual val preferencesTmpDir = File(tmpDir, "prefs_tmp") + .also { it.deleteRecursively() } actual val chatDatabaseFileName: String = "files_chat.db" actual val agentDatabaseFileName: String = "files_agent.db" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index e2ff76f53f..ac9c1a131b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -3168,8 +3168,12 @@ class SharedPreference(val get: () -> T, set: (T) -> Unit) { init { this.set = { value -> - set(value) - _state.value = value + try { + set(value) + _state.value = value + } catch (e: Exception) { + Log.e(TAG, "Error saving settings: ${e.stackTraceToString()}") + } } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt index e9fc8c97f9..0a4f670fe0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt @@ -3,7 +3,7 @@ package chat.simplex.common.platform import androidx.compose.runtime.Composable import chat.simplex.common.model.* import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.helpers.generalGetString +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import com.charleskorn.kaml.* import kotlinx.serialization.encodeToString @@ -11,6 +11,8 @@ import java.io.* import java.net.URI import java.net.URLDecoder import java.net.URLEncoder +import java.nio.file.Files +import java.nio.file.StandardCopyOption expect val dataDir: File expect val tmpDir: File @@ -20,6 +22,7 @@ expect val wallpapersDir: File expect val coreTmpDir: File expect val dbAbsolutePrefixPath: String expect val preferencesDir: File +expect val preferencesTmpDir: File expect val chatDatabaseFileName: String expect val agentDatabaseFileName: String @@ -142,16 +145,23 @@ fun readThemeOverrides(): List { } } +private const val lock = "themesWriter" + fun writeThemeOverrides(overrides: List): Boolean = - try { - File(getPreferenceFilePath("themes.yaml")).outputStream().use { - val string = yaml.encodeToString(ThemesFile(themes = overrides)) - it.bufferedWriter().use { it.write(string) } + synchronized(lock) { + try { + val themesFile = File(getPreferenceFilePath("themes.yaml")) + createTmpFileAndDelete(preferencesTmpDir) { tmpFile -> + val string = yaml.encodeToString(ThemesFile(themes = overrides)) + tmpFile.bufferedWriter().use { it.write(string) } + themesFile.parentFile.mkdirs() + Files.move(tmpFile.toPath(), themesFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + } + true + } catch (e: Exception) { + Log.e(TAG, "Error writing themes file: ${e.stackTraceToString()}") + false } - true - } catch (e: Throwable) { - Log.e(TAG, "Error while writing themes file: ${e.stackTraceToString()}") - false } private fun fileReady(file: CIFile, filePath: String) = diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt index a5293b6a24..07f2b678cf 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt @@ -102,7 +102,9 @@ object ThemeManager { } fun applyTheme(theme: String) { - appPrefs.currentTheme.set(theme) + if (appPrefs.currentTheme.get() != theme) { + appPrefs.currentTheme.set(theme) + } CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) platform.androidSetNightModeIfSupported() val c = CurrentColors.value.colors diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt index 39611361e3..d5bbe4b352 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt @@ -316,8 +316,9 @@ fun removeWallpaperFile(fileName: String? = null) { WallpaperType.cachedImages.remove(fileName) } -fun createTmpFileAndDelete(onCreated: (File) -> T): T { - val tmpFile = File(tmpDir, UUID.randomUUID().toString()) +fun createTmpFileAndDelete(dir: File = tmpDir, onCreated: (File) -> T): T { + val tmpFile = File(dir, UUID.randomUUID().toString()) + tmpFile.parentFile.mkdirs() tmpFile.deleteOnExit() ChatModel.filesToDelete.add(tmpFile) try { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt index a5cb944f0a..a66ef9ff7a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt @@ -16,6 +16,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.model.ChatModel import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.setConditionsNotified import chat.simplex.common.model.ServerOperator.Companion.dummyOperatorInfo @@ -766,7 +768,9 @@ private val versionDescriptions: List = listOf( private val lastVersion = versionDescriptions.last().version fun setLastVersionDefault(m: ChatModel) { - m.controller.appPrefs.whatsNewVersion.set(lastVersion) + if (appPrefs.whatsNewVersion.get() != lastVersion) { + appPrefs.whatsNewVersion.set(lastVersion) + } } fun shouldShowWhatsNew(m: ChatModel): Boolean { diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index ece68481fa..04b025718a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -943,6 +943,7 @@ Show slow API calls Shutdown? Notifications will stop working until you re-launch the app + Error saving settings Create address diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt index eeeb13e5cc..f7a87e3ced 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt @@ -17,6 +17,8 @@ actual val wallpapersDir: File = File(dataDir.absolutePath + File.separator + "s actual val coreTmpDir: File = File(dataDir.absolutePath + File.separator + "tmp") actual val dbAbsolutePrefixPath: String = dataDir.absolutePath + File.separator + "simplex_v1" actual val preferencesDir = File(desktopPlatform.configPath).also { it.parentFile.mkdirs() } +actual val preferencesTmpDir = File(desktopPlatform.configPath, "tmp") + .also { it.deleteRecursively() } actual val chatDatabaseFileName: String = "simplex_v1_chat.db" actual val agentDatabaseFileName: String = "simplex_v1_agent.db" diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt index 951185dc98..4e7594f998 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt @@ -9,15 +9,16 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.* import chat.simplex.common.simplexWindowState import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR import com.jthemedetecor.OsThemeDetector import com.russhwolf.settings.* import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.desc.desc -import kotlinx.coroutines.* import java.io.File +import java.nio.file.Files +import java.nio.file.StandardCopyOption import java.util.* -import java.util.concurrent.Executors @Composable actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font = @@ -37,10 +38,8 @@ catch (e: Exception) { private val settingsFile = File(desktopPlatform.configPath + File.separator + "settings.properties") - .also { it.parentFile.mkdirs() } private val settingsThemesFile = File(desktopPlatform.configPath + File.separator + "themes.properties") - .also { it.parentFile.mkdirs() } private val settingsProps = Properties() @@ -61,11 +60,35 @@ private val settingsThemesProps = Properties() .also { props -> try { settingsThemesFile.reader().use { props.load(it) } } catch (e: Exception) { /**/ } } - -private val settingsWriterThread = Executors.newSingleThreadExecutor().asCoroutineDispatcher() - -actual val settings: Settings = PropertiesSettings(settingsProps) { CoroutineScope(settingsWriterThread).launch { settingsFile.writer().use { settingsProps.store(it, "") } } } -actual val settingsThemes: Settings = PropertiesSettings(settingsThemesProps) { CoroutineScope(settingsWriterThread).launch { settingsThemesFile.writer().use { settingsThemesProps.store(it, "") } } } +private const val lock = "settingsSaver" +actual val settings: Settings = PropertiesSettings(settingsProps) { + synchronized(lock) { + try { + createTmpFileAndDelete(preferencesTmpDir) { tmpFile -> + tmpFile.writer().use { settingsProps.store(it, "") } + settingsFile.parentFile.mkdirs() + Files.move(tmpFile.toPath(), settingsFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + } + } catch (e: Exception) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.prefs_error_saving_settings), e.stackTraceToString()) + throw e + } + } +} +actual val settingsThemes: Settings = PropertiesSettings(settingsThemesProps) { + synchronized(lock) { + try { + createTmpFileAndDelete(preferencesTmpDir) { tmpFile -> + tmpFile.writer().use { settingsThemesProps.store(it, "") } + settingsThemesFile.parentFile.mkdirs() + Files.move(tmpFile.toPath(), settingsThemesFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + } + } catch (e: Exception) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.prefs_error_saving_settings), e.stackTraceToString()) + throw e + } + } +} actual fun windowOrientation(): WindowOrientation = if (simplexWindowState.windowState.size.width > simplexWindowState.windowState.size.height) {