From 72c9dba806d5566e689f5e6f5653c3b029e64324 Mon Sep 17 00:00:00 2001 From: hstyi Date: Sun, 23 Feb 2025 11:32:44 +0800 Subject: [PATCH] feat: support restart (#299) --- THIRDPARTY | 4 + build.gradle.kts | 54 ++++++- gradle/libs.versions.toml | 2 + .../kotlin/app/termora/ApplicationRunner.kt | 10 +- src/main/kotlin/app/termora/Main.kt | 5 + .../kotlin/app/termora/SettingsOptionsPane.kt | 8 +- .../kotlin/app/termora/TermoraRestarter.kt | 142 ++++++++++++++++++ 7 files changed, 208 insertions(+), 17 deletions(-) create mode 100644 src/main/kotlin/app/termora/TermoraRestarter.kt diff --git a/THIRDPARTY b/THIRDPARTY index ce43a80..8f89b6b 100644 --- a/THIRDPARTY +++ b/THIRDPARTY @@ -134,6 +134,10 @@ kotlin-stdlib-jdk8 1.9.10 Apache License 2.0 https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt +restart4j 0.0.1 +Apache License 2.0 +https://github.com/hstyi/restart4j/blob/main/LICENSE + kotlinx-coroutines-core-jvm 1.10.1 Apache License 2.0 https://www.apache.org/licenses/LICENSE-2.0 diff --git a/build.gradle.kts b/build.gradle.kts index 2b6f21e..de55b10 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,7 @@ import org.gradle.kotlin.dsl.support.uppercaseFirstChar import org.gradle.nativeplatform.platform.internal.ArchitectureInternal import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform import org.jetbrains.kotlin.org.apache.commons.io.FileUtils +import org.jetbrains.kotlin.org.apache.commons.io.filefilter.FileFilterUtils import org.jetbrains.kotlin.org.apache.commons.lang3.StringUtils import java.nio.file.Files @@ -109,6 +110,7 @@ dependencies { implementation(libs.mixpanel) implementation(libs.jSerialComm) implementation(libs.ini4j) + implementation(libs.restart4j) } application { @@ -147,11 +149,13 @@ tasks.register("copy-dependencies") { val jna = libs.jna.asProvider().get() val pty4j = libs.pty4j.get() val jSerialComm = libs.jSerialComm.get() + val restart4j = libs.restart4j.get() // 对 JNA 和 PTY4J 的本地库提取 // 提取出来是为了单独签名,不然无法通过公证 if (os.isMacOsX && macOSSign) { doLast { + val archName = if (arch.isArm) "aarch64" else "x86_64" val dylib = dir.get().dir("dylib").asFile for (file in dir.get().asFile.listFiles() ?: emptyArray()) { if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) { @@ -178,7 +182,6 @@ tasks.register("copy-dependencies") { // 删除所有二进制类库 exec { commandLine("zip", "-d", file.absolutePath, "resources/*") } } else if ("${jSerialComm.name}-${jSerialComm.version}" == file.nameWithoutExtension) { - val archName = if (arch.isArm) "aarch64" else "x86_64" val targetDir = FileUtils.getFile(dylib, jSerialComm.name, "OSX", archName) FileUtils.forceMkdir(targetDir) // @formatter:off @@ -192,6 +195,24 @@ tasks.register("copy-dependencies") { exec { commandLine("zip", "-d", file.absolutePath, "OSX/*") } exec { commandLine("zip", "-d", file.absolutePath, "Solaris/*") } exec { commandLine("zip", "-d", file.absolutePath, "Windows/*") } + } else if ("${restart4j.name}-${restart4j.version}" == file.nameWithoutExtension) { + val targetDir = FileUtils.getFile(dylib, restart4j.name) + FileUtils.forceMkdir(targetDir) + // @formatter:off + exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "darwin/${archName}/*", "-d", targetDir.absolutePath) } + // @formatter:on + // 删除所有二进制类库 + exec { commandLine("zip", "-d", file.absolutePath, "win32/*") } + exec { commandLine("zip", "-d", file.absolutePath, "darwin/*") } + exec { commandLine("zip", "-d", file.absolutePath, "linux/*") } + // 设置可执行权限 + for (e in FileUtils.listFiles( + targetDir, + FileFilterUtils.trueFileFilter(), + FileFilterUtils.falseFileFilter() + )) { + e.setExecutable(true) + } } } @@ -216,6 +237,12 @@ tasks.register("copy-dependencies") { exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/aix-*") } if (os.isWindows) { exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/linux-*") } + if (arch.isArm) { + exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-x86*") } + } else { + exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-aarch64/*") } + exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-x86/*") } + } } else if (os.isLinux) { exec { commandLine("zip", "-d", file.absolutePath, "com/sun/jna/win32-*") } } @@ -224,6 +251,13 @@ tasks.register("copy-dependencies") { exec { commandLine("zip", "-d", file.absolutePath, "resources/*freebsd*") } if (os.isWindows) { exec { commandLine("zip", "-d", file.absolutePath, "resources/*linux*") } + if (arch.isArm) { + exec { commandLine("zip", "-d", file.absolutePath, "resources/*win/x86/*") } + exec { commandLine("zip", "-d", file.absolutePath, "resources/*win/x86-64*") } + } else { + exec { commandLine("zip", "-d", file.absolutePath, "resources/*win/x86/*") } + exec { commandLine("zip", "-d", file.absolutePath, "resources/*win/aarch64/*") } + } } else if (os.isLinux) { exec { commandLine("zip", "-d", file.absolutePath, "resources/*win*") } } @@ -238,6 +272,23 @@ tasks.register("copy-dependencies") { } else if (os.isLinux) { exec { commandLine("zip", "-d", file.absolutePath, "Windows/*") } } + } else if ("${restart4j.name}-${restart4j.version}" == file.nameWithoutExtension) { + exec { commandLine("zip", "-d", file.absolutePath, "darwin/*") } + if (os.isWindows) { + exec { commandLine("zip", "-d", file.absolutePath, "linux/*") } + if (arch.isArm) { + exec { commandLine("zip", "-d", file.absolutePath, "win32/x86_64/*") } + } else { + exec { commandLine("zip", "-d", file.absolutePath, "win32/aarch64/*") } + } + } else if (os.isLinux) { + exec { commandLine("zip", "-d", file.absolutePath, "win32/*") } + if (arch.isArm) { + exec { commandLine("zip", "-d", file.absolutePath, "linux/x86_64/*") } + } else { + exec { commandLine("zip", "-d", file.absolutePath, "linux/aarch64/*") } + } + } } } } @@ -489,6 +540,7 @@ Terminal=false sb.append("#!/bin/sh").appendLine() sb.append("SELF=$(readlink -f \"$0\")").appendLine() sb.append("HERE=\${SELF%/*}").appendLine() + sb.append("export LinuxAppImage=true").appendLine() sb.append("exec \"\${HERE}/bin/${termoraName}\" \"$@\"") appRun.writeText(sb.toString()) appRun.setExecutable(true) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index efe29f5..868a6db 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,6 +44,7 @@ testcontainers = "1.20.4" mixpanel = "1.5.3" jSerialComm = "2.11.0" ini4j = "0.5.5-2" +restart4j = "0.0.1" [libraries] kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } @@ -76,6 +77,7 @@ versioncompare = { module = "io.github.g00fy2:versioncompare", version.ref = "ve jfa = { module = "de.jangassen:jfa", version.ref = "jfa" } oshi-core = { module = "com.github.oshi:oshi-core", version.ref = "oshi" } commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" } +restart4j = { module = "com.github.hstyi:restart4j", version.ref = "restart4j" } jbr-api = { module = "com.jetbrains:jbr-api", version.ref = "jbr-api" } flatlaf-swingx = { module = "com.formdev:flatlaf-swingx", version.ref = "flatlaf" } leveldb = { module = "org.iq80.leveldb:leveldb", version.ref = "leveldb" } diff --git a/src/main/kotlin/app/termora/ApplicationRunner.kt b/src/main/kotlin/app/termora/ApplicationRunner.kt index 7930d9e..a8dcff0 100644 --- a/src/main/kotlin/app/termora/ApplicationRunner.kt +++ b/src/main/kotlin/app/termora/ApplicationRunner.kt @@ -104,16 +104,8 @@ class ApplicationRunner { @Suppress("OPT_IN_USAGE") private fun clearTemporary() { GlobalScope.launch(Dispatchers.IO) { - // 启动时清除 FileUtils.cleanDirectory(Application.getTemporaryDir()) - - // 关闭时清除 - Disposer.register(ApplicationScope.forApplicationScope(), object : Disposable { - override fun dispose() { - FileUtils.cleanDirectory(Application.getTemporaryDir()) - } - }) } } @@ -195,7 +187,7 @@ class ApplicationRunner { themeManager.change(theme, true) if (Application.isUnknownVersion()) - FlatInspector.install("ctrl shift alt X"); + FlatInspector.install("ctrl shift alt X") UIManager.put(FlatClientProperties.FULL_WINDOW_CONTENT, true) UIManager.put(FlatClientProperties.USE_WINDOW_DECORATIONS, false) diff --git a/src/main/kotlin/app/termora/Main.kt b/src/main/kotlin/app/termora/Main.kt index b61eb52..c9a4902 100644 --- a/src/main/kotlin/app/termora/Main.kt +++ b/src/main/kotlin/app/termora/Main.kt @@ -50,4 +50,9 @@ private fun setupNativeLibraries() { if (jSerialComm.exists()) { System.setProperty("jSerialComm.library.path", jSerialComm.absolutePath) } + + val restart4j = FileUtils.getFile(dylib, "restart4j", "restarter") + if (restart4j.exists()) { + System.setProperty("restarter.path", restart4j.absolutePath) + } } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/SettingsOptionsPane.kt b/src/main/kotlin/app/termora/SettingsOptionsPane.kt index a0dc4f6..c77247c 100644 --- a/src/main/kotlin/app/termora/SettingsOptionsPane.kt +++ b/src/main/kotlin/app/termora/SettingsOptionsPane.kt @@ -36,7 +36,6 @@ import com.jthemedetecor.OsThemeDetector import com.sun.jna.LastErrorException import kotlinx.coroutines.* import kotlinx.coroutines.swing.Swing -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.* import org.apache.commons.codec.binary.Base64 import org.apache.commons.io.IOUtils @@ -199,12 +198,7 @@ class SettingsOptionsPane : OptionsPane() { if (it.stateChange == ItemEvent.SELECTED) { appearance.language = languageComboBox.selectedItem as String SwingUtilities.invokeLater { - OptionPane.showMessageDialog( - owner, - I18n.getString("termora.settings.restart.message"), - I18n.getString("termora.settings.restart.title"), - messageType = JOptionPane.INFORMATION_MESSAGE, - ) + TermoraRestarter.getInstance().scheduleRestart(owner) } } } diff --git a/src/main/kotlin/app/termora/TermoraRestarter.kt b/src/main/kotlin/app/termora/TermoraRestarter.kt new file mode 100644 index 0000000..22a2309 --- /dev/null +++ b/src/main/kotlin/app/termora/TermoraRestarter.kt @@ -0,0 +1,142 @@ +package app.termora + +import com.formdev.flatlaf.util.SystemInfo +import com.github.hstyi.restart4j.Restarter +import org.apache.commons.io.FileUtils +import org.apache.commons.lang3.StringUtils +import org.slf4j.LoggerFactory +import java.awt.Component +import java.util.concurrent.atomic.AtomicBoolean +import javax.swing.JOptionPane +import javax.swing.SwingUtilities +import kotlin.jvm.optionals.getOrNull + +class TermoraRestarter { + companion object { + private val log = LoggerFactory.getLogger(TermoraRestarter::class.java) + + fun getInstance(): TermoraRestarter { + return ApplicationScope.forApplicationScope().getOrCreate(TermoraRestarter::class) { TermoraRestarter() } + } + + init { + Restarter.setProcessHandler { ProcessHandle.current().pid().toInt() } + } + + } + + private val restarting = AtomicBoolean(false) + private val isSupported get() = !restarting.get() && checkIsSupported() + private val isLinuxAppImage by lazy { System.getenv("LinuxAppImage")?.toBoolean() == true } + private val startupCommand by lazy { ProcessHandle.current().info().command().getOrNull() } + private val macOSApplicationPath by lazy { + StringUtils.removeEndIgnoreCase( + Application.getAppPath(), + "/Contents/MacOS/Termora" + ) + } + + private fun restart() { + if (!isSupported) return + if (!restarting.compareAndSet(false, true)) return + + SwingUtilities.invokeLater { + try { + doRestart() + } catch (e: Exception) { + if (log.isErrorEnabled) { + log.error(e.message, e) + } + } + } + } + + /** + * 计划重启,如果当前进程支持重启,那么会询问用户是否重启。如果不支持重启,那么弹窗提示需要手动重启。 + */ + fun scheduleRestart(owner: Component) { + + if (isSupported) { + if (OptionPane.showConfirmDialog( + owner, + I18n.getString("termora.settings.restart.message"), + I18n.getString("termora.settings.restart.title"), + messageType = JOptionPane.QUESTION_MESSAGE, + optionType = JOptionPane.YES_NO_OPTION, + options = arrayOf( + I18n.getString("termora.settings.restart.title"), + I18n.getString("termora.cancel") + ), + initialValue = I18n.getString("termora.settings.restart.title") + ) == JOptionPane.YES_OPTION + ) { + restart() + } + } else { + OptionPane.showMessageDialog( + owner, + I18n.getString("termora.settings.restart.message"), + I18n.getString("termora.settings.restart.title"), + messageType = JOptionPane.INFORMATION_MESSAGE, + ) + } + + } + + private fun doRestart() { + + for (window in TermoraFrameManager.getInstance().getWindows()) { + window.dispose() + } + + if (SystemInfo.isMacOS) { + Restarter.restart(arrayOf("open", "-n", macOSApplicationPath)) + } else if (SystemInfo.isWindows && startupCommand != null) { + Restarter.restart(arrayOf(startupCommand)) + } else if (SystemInfo.isLinux) { + if (isLinuxAppImage) { + Restarter.restart(arrayOf(System.getenv("APPIMAGE"))) + } else if (startupCommand != null) { + Restarter.restart(arrayOf(startupCommand)) + } + } + } + + + private fun checkIsSupported(): Boolean { + val appPath = Application.getAppPath() + if (appPath.isBlank() || Application.isUnknownVersion()) { + if (log.isWarnEnabled) { + log.warn("Restart not supported") + } + return false + } + + log.info("startupCommand: ${startupCommand}") + log.info("apppath: ${Application.getAppPath()}") + + if (SystemInfo.isWindows && startupCommand == null) { + if (log.isWarnEnabled) { + log.warn("Restart not supported , ProcessHandle#info#command is null.") + } + return false + } + + if (SystemInfo.isLinux) { + if (isLinuxAppImage) { + val appImage = System.getenv("APPIMAGE") ?: StringUtils.EMPTY + return appImage.isNotBlank() && FileUtils.getFile(appImage).exists() + } + return startupCommand != null + } + + if (SystemInfo.isMacOS) { + return Application.getAppPath().isNotBlank() + } + + + return true + } + + +} \ No newline at end of file