mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-15 18:02:58 +08:00
chore: improve upgrade
This commit is contained in:
@@ -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<AbstractAction> {
|
||||
return listOf(createOkAction(), createCancelAction())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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<AbstractAction> {
|
||||
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<String>()
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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 <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||
return support.getExtensions(clazz)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user