diff --git a/src/main/kotlin/app/termora/DialogWrapper.kt b/src/main/kotlin/app/termora/DialogWrapper.kt index 967b672..b97369b 100644 --- a/src/main/kotlin/app/termora/DialogWrapper.kt +++ b/src/main/kotlin/app/termora/DialogWrapper.kt @@ -127,12 +127,19 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) { } protected open fun createSouthPanel(): JComponent? { + val westSourcePanel = createWestSourcePanel() val box = Box.createHorizontalBox() + + if (westSourcePanel != null) { + box.add(westSourcePanel) + } else { + box.add(Box.createHorizontalGlue()) + } + box.border = BorderFactory.createCompoundBorder( BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor), BorderFactory.createEmptyBorder(8, 12, 8, 12) ) - box.add(Box.createHorizontalGlue()) val actions = createActions() for (i in actions.size - 1 downTo 0) { @@ -145,6 +152,10 @@ abstract class DialogWrapper(owner: Window?) : JDialog(owner) { return box } + protected open fun createWestSourcePanel(): JComponent? { + return null + } + protected open fun createActions(): List { return listOf(createOkAction(), createCancelAction()) } diff --git a/src/main/kotlin/app/termora/MyTermoraToolbar.kt b/src/main/kotlin/app/termora/MyTermoraToolbar.kt index 8963947..07a61cf 100644 --- a/src/main/kotlin/app/termora/MyTermoraToolbar.kt +++ b/src/main/kotlin/app/termora/MyTermoraToolbar.kt @@ -2,7 +2,6 @@ package app.termora import app.termora.actions.StateAction import app.termora.findeverywhere.FindEverywhereAction -import app.termora.plugin.internal.update.AppUpdateAction import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.extras.components.FlatPopupMenu import com.formdev.flatlaf.extras.components.FlatToolBar @@ -84,9 +83,6 @@ internal class MyTermoraToolbar(private val windowScope: WindowScope, private va add(Box.createHorizontalGlue()) - // update - add(redirectUpdateAction(disposable)) - for (action in model.getActions()) { if (action.visible.not()) continue val action = actionManager.getAction(action.id) ?: continue @@ -122,34 +118,6 @@ internal class MyTermoraToolbar(private val windowScope: WindowScope, private va toolbar.add(spacing) } - private fun redirectUpdateAction(disposable: Disposable): AbstractButton { - val action = AppUpdateAction.getInstance() - val button = JButton(action.smallIcon) - button.toolTipText = (action.getValue(Action.SHORT_DESCRIPTION) as? String) - ?: action.getValue(Action.NAME) as? String - button.isVisible = action.isEnabled - button.addActionListener(object : AbstractAction() { - override fun actionPerformed(e: ActionEvent) { - action.actionPerformed(e) - } - }) - - val listener = object : PropertyChangeListener, Disposable { - override fun propertyChange(evt: PropertyChangeEvent) { - button.isVisible = action.isEnabled - } - - override fun dispose() { - action.removePropertyChangeListener(this) - } - } - - action.addPropertyChangeListener(listener) - Disposer.register(disposable, listener) - - return button - } - private fun redirectAction(action: Action, disposable: Disposable): AbstractButton { val button = if (action is StateAction) JToggleButton() else JButton() button.toolTipText = (action.getValue(Action.SHORT_DESCRIPTION) as? String) diff --git a/src/main/kotlin/app/termora/plugin/PluginManager.kt b/src/main/kotlin/app/termora/plugin/PluginManager.kt index 16999ad..1dfda35 100644 --- a/src/main/kotlin/app/termora/plugin/PluginManager.kt +++ b/src/main/kotlin/app/termora/plugin/PluginManager.kt @@ -12,7 +12,7 @@ import app.termora.plugin.internal.rdp.RDPInternalPlugin import app.termora.plugin.internal.sftppty.SFTPPtyInternalPlugin import app.termora.plugin.internal.ssh.SSHInternalPlugin import app.termora.plugin.internal.telnet.TelnetInternalPlugin -import app.termora.plugin.internal.update.UpdatePlugin +import app.termora.plugin.internal.updater.UpdaterPlugin import app.termora.plugin.internal.wsl.WSLInternalPlugin import app.termora.swingCoroutineScope import app.termora.terminal.panel.vw.FloatingToolbarPlugin @@ -111,7 +111,7 @@ internal class PluginManager private constructor() { // badge plugin plugins.add(PluginDescriptor(BadgePlugin(), origin = PluginOrigin.Internal, version = version)) // update plugin - plugins.add(PluginDescriptor(UpdatePlugin(), origin = PluginOrigin.Internal, version = version)) + plugins.add(PluginDescriptor(UpdaterPlugin(), origin = PluginOrigin.Internal, version = version)) // frame plugin plugins.add(PluginDescriptor(FramePlugin(), origin = PluginOrigin.Internal, version = version)) diff --git a/src/main/kotlin/app/termora/plugin/internal/update/AppUpdateAction.kt b/src/main/kotlin/app/termora/plugin/internal/update/AppUpdateAction.kt deleted file mode 100644 index de85efc..0000000 --- a/src/main/kotlin/app/termora/plugin/internal/update/AppUpdateAction.kt +++ /dev/null @@ -1,134 +0,0 @@ -package app.termora.plugin.internal.update - -import app.termora.* -import app.termora.actions.AnAction -import app.termora.actions.AnActionEvent -import com.formdev.flatlaf.util.SystemInfo -import com.sun.jna.platform.win32.Advapi32 -import com.sun.jna.platform.win32.WinError -import com.sun.jna.platform.win32.WinNT -import com.sun.jna.platform.win32.WinReg -import org.apache.commons.lang3.StringUtils -import org.jdesktop.swingx.JXEditorPane -import org.slf4j.LoggerFactory -import java.awt.Dimension -import java.awt.KeyboardFocusManager -import java.net.URI -import javax.swing.BorderFactory -import javax.swing.JOptionPane -import javax.swing.JScrollPane -import javax.swing.UIManager -import javax.swing.event.HyperlinkEvent - -internal class AppUpdateAction private constructor() : AnAction(StringUtils.EMPTY, Icons.ideUpdate) { - - companion object { - private val log = LoggerFactory.getLogger(AppUpdateAction::class.java) - - fun getInstance(): AppUpdateAction { - return ApplicationScope.forApplicationScope().getOrCreate(AppUpdateAction::class) { AppUpdateAction() } - } - } - - private val updaterManager get() = UpdaterManager.getInstance() - - init { - isEnabled = false - } - - - override fun actionPerformed(evt: AnActionEvent) { - showUpdateDialog() - } - - - private fun showUpdateDialog() { - val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow - val lastVersion = updaterManager.lastVersion - val editorPane = JXEditorPane() - editorPane.contentType = "text/html" - editorPane.text = lastVersion.htmlBody - editorPane.isEditable = false - editorPane.addHyperlinkListener { - if (it.eventType == HyperlinkEvent.EventType.ACTIVATED) { - Application.browse(it.url.toURI()) - } - } - editorPane.background = DynamicColor("window") - val scrollPane = JScrollPane(editorPane) - scrollPane.border = BorderFactory.createEmptyBorder() - scrollPane.preferredSize = Dimension( - UIManager.getInt("Dialog.width") - 100, - UIManager.getInt("Dialog.height") - 100 - ) - - val option = OptionPane.showConfirmDialog( - owner, - scrollPane, - title = I18n.getString("termora.update.title"), - messageType = JOptionPane.PLAIN_MESSAGE, - optionType = JOptionPane.OK_CANCEL_OPTION, - options = arrayOf( - I18n.getString("termora.update.update"), - I18n.getString("termora.cancel") - ), - initialValue = I18n.getString("termora.update.update") - ) - if (option == JOptionPane.CANCEL_OPTION) { - return - } else if (option == JOptionPane.OK_OPTION) { - updateSelf(lastVersion) - } - } - - private fun updateSelf(latestVersion: UpdaterManager.LatestVersion) { - val pkg = Updater.getInstance().getLatestPkg() - if (SystemInfo.isLinux || pkg == null) { - Application.browse(URI.create("https://github.com/TermoraDev/termora/releases/tag/${latestVersion.version}")) - return - } - val file = pkg.file - val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner - val commands = if (SystemInfo.isMacOS) listOf("open", "-n", file.absolutePath) - // 如果安装过,那么直接静默安装和自动启动 - else if (isAppInstalled()) listOf( - file.absolutePath, - "/SILENT", - "/AUTOSTART", - "/NORESTART", - "/FORCECLOSEAPPLICATIONS" - ) - // 没有安装过 则打开安装向导 - else listOf(file.absolutePath) - - if (log.isInfoEnabled) { - log.info("restart {}", commands.joinToString(StringUtils.SPACE)) - } - - TermoraRestarter.getInstance().scheduleRestart(owner, true, commands) - - } - - private fun isAppInstalled(): Boolean { - val keyPath = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${Application.getName()}_is1" - val phkKey = WinReg.HKEYByReference() - - // 尝试打开注册表键 - val result = Advapi32.INSTANCE.RegOpenKeyEx( - WinReg.HKEY_LOCAL_MACHINE, - keyPath, - 0, - WinNT.KEY_READ, - phkKey - ) - - if (result == WinError.ERROR_SUCCESS) { - // 键存在,关闭句柄 - Advapi32.INSTANCE.RegCloseKey(phkKey.getValue()) - return true - } else { - // 键不存在或无权限 - return false - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/plugin/internal/update/MyApplicationRunnerExtension.kt b/src/main/kotlin/app/termora/plugin/internal/update/MyApplicationRunnerExtension.kt deleted file mode 100644 index 8d1a0e8..0000000 --- a/src/main/kotlin/app/termora/plugin/internal/update/MyApplicationRunnerExtension.kt +++ /dev/null @@ -1,13 +0,0 @@ -package app.termora.plugin.internal.update - -import app.termora.ApplicationRunnerExtension - -internal class MyApplicationRunnerExtension private constructor() : ApplicationRunnerExtension { - companion object { - val instance = MyApplicationRunnerExtension() - } - - override fun ready() { - Updater.getInstance().scheduleUpdate() - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/plugin/internal/update/Updater.kt b/src/main/kotlin/app/termora/plugin/internal/update/Updater.kt deleted file mode 100644 index fed4b2b..0000000 --- a/src/main/kotlin/app/termora/plugin/internal/update/Updater.kt +++ /dev/null @@ -1,166 +0,0 @@ -package app.termora.plugin.internal.update - -import app.termora.* -import app.termora.Application.httpClient -import com.formdev.flatlaf.util.SystemInfo -import kotlinx.coroutines.* -import okhttp3.Request -import org.apache.commons.io.FileUtils -import org.apache.commons.io.IOUtils -import org.semver4j.Semver -import org.slf4j.LoggerFactory -import java.io.File -import java.net.ProxySelector -import java.util.* -import java.util.concurrent.TimeUnit -import javax.swing.SwingUtilities -import kotlin.time.Duration.Companion.hours -import kotlin.time.Duration.Companion.seconds - -internal class Updater private constructor() : Disposable { - - companion object { - private val log = LoggerFactory.getLogger(Updater::class.java) - fun getInstance(): Updater { - return ApplicationScope.forApplicationScope().getOrCreate(Updater::class) { Updater() } - } - } - - private val updaterManager get() = UpdaterManager.getInstance() - private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - private var isRemindMeNextTime = false - private val disabledUpdater get() = Application.getLayout() == AppLayout.Appx - - /** - * 安装包位置 - */ - private var pkg: LatestPkg? = null - - fun scheduleUpdate() { - - if (disabledUpdater) { - if (coroutineScope.isActive) { - coroutineScope.cancel() - } - return - } - - coroutineScope.launch(Dispatchers.IO) { - // 启动 3 分钟后才是检查 - if (Application.isUnknownVersion().not()) { - delay(3.seconds) - } - - while (coroutineScope.isActive) { - // 下次提醒我 - if (isRemindMeNextTime) break - - try { - checkUpdate() - } catch (e: Exception) { - if (log.isWarnEnabled) { - log.warn(e.message, e) - } - } - - // 之后每 3 小时检查一次 - delay(3.hours.inWholeMilliseconds) - - } - } - } - - private fun checkUpdate() { - - // Windows 应用商店 - if (disabledUpdater) return - - val latestVersion = updaterManager.fetchLatestVersion() - if (latestVersion.isSelf) { - return - } - - // 之所以放到后面检查是不是开发版本,是需要发起一次检测请求,以方便调试 - if (Application.isUnknownVersion()) { - return - } - - - val newVersion = Semver.parse(latestVersion.version) ?: return - val version = Semver.parse(Application.getVersion()) ?: return - if (newVersion <= version) { - return - } - - try { - downloadLatestPkg(latestVersion) - } catch (e: Exception) { - if (log.isErrorEnabled) { - log.error(e.message, e) - } - } - - - } - - - private fun downloadLatestPkg(latestVersion: UpdaterManager.LatestVersion) { - if (SystemInfo.isLinux) return - - setLatestPkg(null) - - val arch = if (SystemInfo.isAARCH64) "aarch64" else "x86-64" - val osName = if (SystemInfo.isWindows) "windows" else "osx" - val suffix = if (SystemInfo.isWindows) "exe" else "dmg" - val filename = "termora-${latestVersion.version}-${osName}-${arch}.${suffix}" - val asset = latestVersion.assets.find { it.name == filename } ?: return - - val response = httpClient - .newBuilder() - .callTimeout(15, TimeUnit.MINUTES) - .readTimeout(15, TimeUnit.MINUTES) - .proxySelector(ProxySelector.getDefault()) - .build() - .newCall(Request.Builder().url(asset.downloadUrl).build()) - .execute() - if (response.isSuccessful.not()) { - if (log.isErrorEnabled) { - log.warn("Failed to download latest version ${latestVersion.version}, response code ${response.code}") - } - IOUtils.closeQuietly(response) - return - } - - val body = response.body - val input = body.byteStream() - val file = FileUtils.getFile(Application.getTemporaryDir(), "${UUID.randomUUID()}-${filename}") - val output = file.outputStream() - - val downloaded = runCatching { IOUtils.copy(input, output) }.isSuccess - IOUtils.closeQuietly(input, output, body, response) - - if (!downloaded) { - if (log.isErrorEnabled) { - log.error("Failed to download latest version to $filename") - } - return - } - - if (log.isInfoEnabled) { - log.info("Successfully downloaded latest version to $file") - } - - setLatestPkg(LatestPkg(latestVersion.version, file)) - } - - private fun setLatestPkg(pkg: LatestPkg?) { - this.pkg = pkg - SwingUtilities.invokeLater { AppUpdateAction.getInstance().isEnabled = pkg != null } - } - - fun getLatestPkg(): LatestPkg? { - return pkg - } - - data class LatestPkg(val version: String, val file: File) -} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/plugin/internal/updater/MyApplicationRunnerExtension.kt b/src/main/kotlin/app/termora/plugin/internal/updater/MyApplicationRunnerExtension.kt new file mode 100644 index 0000000..46a7301 --- /dev/null +++ b/src/main/kotlin/app/termora/plugin/internal/updater/MyApplicationRunnerExtension.kt @@ -0,0 +1,57 @@ +package app.termora.plugin.internal.updater + +import app.termora.* +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.semver4j.Semver +import org.slf4j.LoggerFactory +import java.awt.KeyboardFocusManager +import kotlin.time.Duration.Companion.seconds + +internal class MyApplicationRunnerExtension private constructor() : ApplicationRunnerExtension { + companion object { + val instance = MyApplicationRunnerExtension() + + private val log = LoggerFactory.getLogger(MyApplicationRunnerExtension::class.java) + } + + private val disabledUpdater get() = Application.getLayout() == AppLayout.Appx + private val updaterManager get() = UpdaterManager.getInstance() + + + override fun ready() { + swingCoroutineScope.launch { + try { + delay(3.seconds) + scheduleUpdate() + } catch (e: Exception) { + log.error(e.message, e) + } + } + } + + + private fun scheduleUpdate() { + if (disabledUpdater) return + + val latestVersion = updaterManager.fetchLatestVersion() + if (latestVersion.isSelf) { + return + } + + val newVersion = Semver.parse(latestVersion.version) ?: return + val version = Semver.parse(Application.getVersion()) ?: return + if (newVersion <= version) { + return + } + + val owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow + ?: TermoraFrameManager.getInstance().getWindows().firstOrNull() + if (owner == null) return + + val dialog = UpdaterDialog(owner, latestVersion) + dialog.isModal = true + dialog.isVisible = true + + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/plugin/internal/updater/UpdaterDialog.kt b/src/main/kotlin/app/termora/plugin/internal/updater/UpdaterDialog.kt new file mode 100644 index 0000000..29d8b63 --- /dev/null +++ b/src/main/kotlin/app/termora/plugin/internal/updater/UpdaterDialog.kt @@ -0,0 +1,317 @@ +package app.termora.plugin.internal.updater + +import app.termora.* +import app.termora.Application.httpClient +import com.formdev.flatlaf.util.SystemInfo +import com.sun.jna.platform.win32.Advapi32 +import com.sun.jna.platform.win32.WinError +import com.sun.jna.platform.win32.WinNT +import com.sun.jna.platform.win32.WinReg +import kotlinx.coroutines.* +import kotlinx.coroutines.swing.Swing +import okhttp3.Request +import okhttp3.internal.closeQuietly +import org.apache.commons.io.FileUtils +import org.apache.commons.lang3.StringUtils +import org.apache.commons.lang3.Strings +import org.apache.commons.lang3.exception.ExceptionUtils +import org.apache.commons.net.io.CopyStreamEvent +import org.apache.commons.net.io.CopyStreamListener +import org.apache.commons.net.io.Util +import org.jdesktop.swingx.JXEditorPane +import org.slf4j.LoggerFactory +import java.awt.Dimension +import java.awt.Window +import java.net.URI +import java.util.* +import java.util.concurrent.Executors +import javax.swing.* +import javax.swing.event.HyperlinkEvent +import kotlin.math.floor + +internal class UpdaterDialog(owner: Window, private val latestVersion: UpdaterManager.LatestVersion) : + DialogWrapper(owner) { + companion object { + private val log = LoggerFactory.getLogger(UpdaterDialog::class.java) + } + + private enum class State { + Ready, + Downloading, + Downloaded, + } + + private val progressBar = JProgressBar() + private val okAction = OkAction(I18n.getString("termora.update.update")) + private val okButton = JButton(okAction) + private var state = State.Ready + private val westSourcePanel = Box.createHorizontalBox() + private val glue = Box.createHorizontalGlue() + private val layout = Application.getLayout() + private val dialog get() = this + + private val executorService = Executors.newVirtualThreadPerTaskExecutor() + private val coroutineDispatcher = executorService.asCoroutineDispatcher() + private val coroutineScope = CoroutineScope(coroutineDispatcher) + + init { + size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height")) + isModal = true + controlsVisible = false + isResizable = false + escapeDispose = false + title = I18n.getString("termora.update.title") + setLocationRelativeTo(owner) + + init() + initView() + initEvents() + } + + private fun initView() { + progressBar.isIndeterminate = true + progressBar.isStringPainted = true + + westSourcePanel.add(glue) + westSourcePanel.add(progressBar) + westSourcePanel.add(Box.createHorizontalStrut(20)) + progressBar.isVisible = false + } + + private fun initEvents() { + Disposer.register(disposable, object : Disposable { + override fun dispose() { + coroutineScope.cancel() + coroutineDispatcher.close() + executorService.shutdownNow() + } + }) + } + + override fun createCenterPanel(): JComponent { + val editorPane = JXEditorPane() + editorPane.contentType = "text/html" + editorPane.text = latestVersion.htmlBody + editorPane.isEditable = false + editorPane.addHyperlinkListener { + if (it.eventType == HyperlinkEvent.EventType.ACTIVATED) { + Application.browse(it.url.toURI()) + } + } + editorPane.background = DynamicColor("window") + val scrollPane = JScrollPane(editorPane) + scrollPane.border = BorderFactory.createEmptyBorder() + return scrollPane + } + + override fun createWestSourcePanel(): JComponent { + return westSourcePanel + } + + + override fun createActions(): List { + return listOf(okAction, createCancelAction()) + } + + override fun createJButtonForAction(action: Action): JButton { + if (action == okAction) { + rootPane.defaultButton = okButton + return okButton + } + return super.createJButtonForAction(action) + } + + @Suppress("CascadeIf") + override fun doOKAction() { + if (state == State.Ready) { + okButton.text = "${okButton.text}..." + okButton.isEnabled = false + progressBar.isVisible = true + glue.isVisible = false + state = State.Downloading + + coroutineScope.launch { + try { + downloadPkg() + } catch (_: LatestReleaseException) { + Application.browse(URI.create("https://github.com/TermoraDev/termora/releases/latest")) + SwingUtilities.invokeLater { doCancelAction() } + } catch (e: Exception) { + if (log.isErrorEnabled) log.error(e.message, e) + withContext(Dispatchers.Swing) { + + state = State.Ready + okButton.isEnabled = true + okButton.text = okAction.name + progressBar.isVisible = false + glue.isVisible = true + + OptionPane.showMessageDialog( + dialog, + StringUtils.defaultIfBlank(e.message, ExceptionUtils.getRootCauseMessage(e)).toString(), + messageType = JOptionPane.ERROR_MESSAGE + ) + + } + } finally { + withContext(Dispatchers.Swing) { + progressBar.isVisible = false + glue.isVisible = true + okButton.isEnabled = true + } + } + } + return + } else if (state == State.Downloading) { + return + } else if (state == State.Downloaded) { + super.doOKAction() + } + + } + + + private suspend fun downloadPkg() { + val arch = if (SystemInfo.isAARCH64) "aarch64" else "x86-64" + val osName = if (SystemInfo.isWindows) "windows" else if (SystemInfo.isLinux) "linux" else "osx" + val suffix = when (layout) { + AppLayout.Zip -> "zip" + AppLayout.Exe -> "exe" + + AppLayout.App -> "dmg" + + AppLayout.TarGz -> "tar.gz" + AppLayout.AppImage -> "AppImage" + AppLayout.Deb -> "deb" + else -> throw LatestReleaseException() + } + + val filename = "termora-${latestVersion.version}-${osName}-${arch}.${suffix}" + val asset = latestVersion.assets.find { it.name == filename } ?: throw LatestReleaseException() + + var url = asset.downloadUrl + + if (I18n.isChinaMainland()) { + url = Strings.CI.replace( + url, + "https://github.com/TermoraDev/termora/releases/download/", + "https://dl.termora.cn/termora/" + ) + } + + val response = httpClient.newCall(Request.Builder().url(url).get().build()) + .execute() + + if (response.isSuccessful.not()) { + response.closeQuietly() + throw IllegalStateException("Failed to download asset $filename") + } + + withContext(Dispatchers.Swing) { + progressBar.isIndeterminate = false + } + + val listener = object : CopyStreamListener { + override fun bytesTransferred(event: CopyStreamEvent?) { + TODO("Not yet implemented") + } + + override fun bytesTransferred( + totalBytesTransferred: Long, + bytesTransferred: Int, + streamSize: Long + ) { + SwingUtilities.invokeLater { + val progress = 1.0 * totalBytesTransferred / asset.size * 100 + progressBar.value = floor(progress).toInt() + progressBar.string = formatBytes(totalBytesTransferred) + " / " + formatBytes(asset.size) + } + } + } + + val file = FileUtils.getFile(Application.getTemporaryDir(), "${UUID.randomUUID()}-${filename}") + + response.use { + response.body.byteStream().use { input -> + file.outputStream().use { output -> + Util.copyStream( + input, output, Util.DEFAULT_COPY_BUFFER_SIZE, + asset.size, listener + ) + } + } + } + + withContext(Dispatchers.Swing) { + state = State.Downloaded + } + + val commands = mutableListOf() + if (SystemInfo.isMacOS) { + commands.addAll(listOf("open", "-n", file.absolutePath)) + } else if (layout == AppLayout.Zip) { + commands.addAll(listOf("explorer", "/select," + file.absolutePath)) + } else if (layout == AppLayout.Exe) { + // 如果安装过,那么直接静默安装和自动启动 + if (isAppInstalled()) { + commands.addAll( + listOf( + file.absolutePath, + "/SILENT", + "/AUTOSTART", + "/NORESTART", + "/FORCECLOSEAPPLICATIONS" + ) + ) + } else { + commands.addAll(listOf(file.absolutePath)) + } + } else if (SystemInfo.isLinux) { + commands.addAll(listOf("xdg-open", file.parentFile.absolutePath)) + } + + if (log.isInfoEnabled) { + log.info("commands: {}", commands.joinToString(StringUtils.SPACE)) + } + + SwingUtilities.invokeLater { + super.doOKAction() + TermoraRestarter.getInstance().scheduleRestart(owner, true, commands) + } + + + } + + private class LatestReleaseException() : RuntimeException() + + private fun isAppInstalled(): Boolean { + try { + val keyPath = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${Application.getName()}_is1" + val phkKey = WinReg.HKEYByReference() + + // 尝试打开注册表键 + val result = Advapi32.INSTANCE.RegOpenKeyEx( + WinReg.HKEY_LOCAL_MACHINE, + keyPath, + 0, + WinNT.KEY_READ, + phkKey + ) + + if (result == WinError.ERROR_SUCCESS) { + // 键存在,关闭句柄 + Advapi32.INSTANCE.RegCloseKey(phkKey.getValue()) + return true + } else { + // 键不存在或无权限 + return false + } + } catch (_: Exception) { + return false + } + } + + override fun addNotify() { + super.addNotify() + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/plugin/internal/update/UpdatePlugin.kt b/src/main/kotlin/app/termora/plugin/internal/updater/UpdaterPlugin.kt similarity index 78% rename from src/main/kotlin/app/termora/plugin/internal/update/UpdatePlugin.kt rename to src/main/kotlin/app/termora/plugin/internal/updater/UpdaterPlugin.kt index 4e351db..f34e7c0 100644 --- a/src/main/kotlin/app/termora/plugin/internal/update/UpdatePlugin.kt +++ b/src/main/kotlin/app/termora/plugin/internal/updater/UpdaterPlugin.kt @@ -1,21 +1,22 @@ -package app.termora.plugin.internal.update +package app.termora.plugin.internal.updater import app.termora.ApplicationRunnerExtension import app.termora.plugin.Extension import app.termora.plugin.InternalPlugin -internal class UpdatePlugin : InternalPlugin() { - +internal class UpdaterPlugin : InternalPlugin() { init { support.addExtension(ApplicationRunnerExtension::class.java) { MyApplicationRunnerExtension.instance } } override fun getName(): String { - return "Update" + return "Updater" } override fun getExtensions(clazz: Class): List { return support.getExtensions(clazz) } + + } \ No newline at end of file