mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
feat: support automatic download of update packages (#305)
This commit is contained in:
@@ -48,13 +48,13 @@ class TermoraRestarter {
|
||||
)
|
||||
}
|
||||
|
||||
private fun restart() {
|
||||
private fun restart(commands: List<String>) {
|
||||
if (!isSupported) return
|
||||
if (!restarting.compareAndSet(false, true)) return
|
||||
|
||||
SwingUtilities.invokeLater {
|
||||
try {
|
||||
doRestart()
|
||||
doRestart(commands)
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
@@ -66,7 +66,7 @@ class TermoraRestarter {
|
||||
/**
|
||||
* 计划重启,如果当前进程支持重启,那么会询问用户是否重启。如果不支持重启,那么弹窗提示需要手动重启。
|
||||
*/
|
||||
fun scheduleRestart(owner: Component) {
|
||||
fun scheduleRestart(owner: Component?, commands: List<String> = emptyList()) {
|
||||
|
||||
if (isSupported) {
|
||||
if (OptionPane.showConfirmDialog(
|
||||
@@ -82,7 +82,7 @@ class TermoraRestarter {
|
||||
initialValue = I18n.getString("termora.settings.restart.title")
|
||||
) == JOptionPane.YES_OPTION
|
||||
) {
|
||||
restart()
|
||||
restart(commands)
|
||||
}
|
||||
} else {
|
||||
OptionPane.showMessageDialog(
|
||||
@@ -95,18 +95,22 @@ class TermoraRestarter {
|
||||
|
||||
}
|
||||
|
||||
private fun doRestart() {
|
||||
private fun doRestart(commands: List<String>) {
|
||||
|
||||
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) {
|
||||
if (commands.isEmpty()) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Restarter.restart(commands.toTypedArray())
|
||||
}
|
||||
|
||||
for (window in TermoraFrameManager.getInstance().getWindows()) {
|
||||
|
||||
@@ -70,6 +70,9 @@ class UpdaterManager private constructor() {
|
||||
.build()
|
||||
val response = Application.httpClient.newCall(request).execute()
|
||||
if (!response.isSuccessful) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error("Failed to fetch latest version, response was ${response.code}")
|
||||
}
|
||||
return LatestVersion.self
|
||||
}
|
||||
|
||||
@@ -151,8 +154,4 @@ class UpdaterManager private constructor() {
|
||||
fun ignore(version: String) {
|
||||
properties.putString("ignored.version.$version", "true")
|
||||
}
|
||||
|
||||
private fun doGetLatestVersion() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ class ActionManager : org.jdesktop.swingx.action.ActionManager() {
|
||||
addAction(FindEverywhereAction.FIND_EVERYWHERE, FindEverywhereAction())
|
||||
|
||||
addAction(Actions.MULTIPLE, MultipleAction())
|
||||
addAction(Actions.APP_UPDATE, AppUpdateAction())
|
||||
addAction(Actions.APP_UPDATE, AppUpdateAction.getInstance())
|
||||
addAction(Actions.KEYWORD_HIGHLIGHT, KeywordHighlightAction())
|
||||
addAction(Actions.TERMINAL_LOGGER, TerminalLoggerAction())
|
||||
addAction(Actions.SFTP, SFTPAction())
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
package app.termora.actions
|
||||
|
||||
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 io.github.g00fy2.versioncompare.Version
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import okhttp3.Request
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.io.IOUtils
|
||||
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.io.File
|
||||
import java.net.ProxySelector
|
||||
import java.net.URI
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.swing.BorderFactory
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.JScrollPane
|
||||
@@ -18,11 +32,20 @@ import kotlin.concurrent.fixedRateTimer
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
class AppUpdateAction : AnAction(
|
||||
class AppUpdateAction private constructor() : AnAction(
|
||||
StringUtils.EMPTY,
|
||||
Icons.ideUpdate
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(AppUpdateAction::class.java)
|
||||
private const val PKG_FILE_KEY = "pkgFile"
|
||||
|
||||
fun getInstance(): AppUpdateAction {
|
||||
return ApplicationScope.forApplicationScope().getOrCreate(AppUpdateAction::class) { AppUpdateAction() }
|
||||
}
|
||||
}
|
||||
|
||||
private val updaterManager get() = UpdaterManager.getInstance()
|
||||
|
||||
init {
|
||||
@@ -63,11 +86,75 @@ class AppUpdateAction : AnAction(
|
||||
return
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Swing) {
|
||||
ActionManager.getInstance()
|
||||
.setEnabled(Actions.APP_UPDATE, true)
|
||||
try {
|
||||
downloadLatestPkg(latestVersion)
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
withContext(Dispatchers.Swing) { isEnabled = true }
|
||||
|
||||
}
|
||||
|
||||
|
||||
private suspend fun downloadLatestPkg(latestVersion: UpdaterManager.LatestVersion) {
|
||||
if (SystemInfo.isLinux) return
|
||||
|
||||
super.putValue(PKG_FILE_KEY, 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) {
|
||||
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")
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Swing) { setLatestPkgFile(file) }
|
||||
|
||||
}
|
||||
|
||||
private fun setLatestPkgFile(file: File) {
|
||||
putValue(PKG_FILE_KEY, file)
|
||||
}
|
||||
|
||||
private fun getLatestPkgFile(): File? {
|
||||
return getValue(PKG_FILE_KEY) as? File
|
||||
}
|
||||
|
||||
private fun showUpdateDialog() {
|
||||
@@ -106,12 +193,59 @@ class AppUpdateAction : AnAction(
|
||||
if (option == JOptionPane.CANCEL_OPTION) {
|
||||
return
|
||||
} else if (option == JOptionPane.NO_OPTION) {
|
||||
ActionManager.getInstance().setEnabled(Actions.APP_UPDATE, false)
|
||||
updaterManager.ignore(updaterManager.lastVersion.version)
|
||||
isEnabled = false
|
||||
updaterManager.ignore(lastVersion.version)
|
||||
} else if (option == JOptionPane.YES_OPTION) {
|
||||
ActionManager.getInstance()
|
||||
.setEnabled(Actions.APP_UPDATE, false)
|
||||
Application.browse(URI.create("https://github.com/TermoraDev/termora/releases/tag/${lastVersion.version}"))
|
||||
updateSelf(lastVersion)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSelf(latestVersion: UpdaterManager.LatestVersion) {
|
||||
val file = getLatestPkgFile()
|
||||
if (SystemInfo.isLinux || file == null) {
|
||||
isEnabled = false
|
||||
Application.browse(URI.create("https://github.com/TermoraDev/termora/releases/tag/${latestVersion.version}"))
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
println(commands)
|
||||
TermoraRestarter.getInstance().scheduleRestart(owner, 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,7 +1,6 @@
|
||||
; Script generated by the Inno Setup Script Wizard.
|
||||
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
||||
|
||||
#define MyAppId "14F2657D-1B3F-4EA1-A1AA-AB6B5E4FD465"
|
||||
#define MyAppPublisher "TermoraDev"
|
||||
#define MyAppURL "https://github.com/TermoraDev/termora"
|
||||
#define MyAppSupportURL "https://github.com/TermoraDev/termora/issues"
|
||||
@@ -12,7 +11,7 @@
|
||||
[Setup]
|
||||
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
|
||||
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
|
||||
AppId={{{#MyAppId}}
|
||||
AppId={#MyAppId}
|
||||
AppName={#MyAppName}
|
||||
AppVersion={#MyAppVersion}
|
||||
AppVerName={#MyAppName} {#MyAppVersion}
|
||||
@@ -57,5 +56,30 @@ Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
||||
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
|
||||
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall; Check: ShouldPromptStart
|
||||
Filename: "{app}\{#MyAppExeName}"; Flags: nowait runhidden; Check: ShouldAutoStart
|
||||
|
||||
[Code]
|
||||
function CmdLineParamExists(const Value: string): Boolean;
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
Result := False;
|
||||
for I := 1 to ParamCount do
|
||||
if CompareText(ParamStr(I), Value) = 0 then
|
||||
begin
|
||||
Result := True;
|
||||
Exit;
|
||||
end;
|
||||
end;
|
||||
|
||||
function ShouldAutoStart: Boolean;
|
||||
begin
|
||||
// 静默模式下且包含 /AUTOSTART 参数时自动启动
|
||||
Result := WizardSilent and CmdLineParamExists('/AUTOSTART');
|
||||
end;
|
||||
|
||||
function ShouldPromptStart: Boolean;
|
||||
begin
|
||||
Result := not WizardSilent;
|
||||
end;
|
||||
|
||||
Reference in New Issue
Block a user