feat: support automatic download of update packages (#305)

This commit is contained in:
hstyi
2025-02-24 12:38:30 +08:00
committed by GitHub
parent a2d7f3b5bb
commit b85bdf840e
6 changed files with 192 additions and 30 deletions

View File

@@ -526,6 +526,7 @@ fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: Str
exec {
commandLine(
"iscc",
"/DMyAppId=${projectName}",
"/DMyAppName=${projectName}",
"/DMyAppVersion=${project.version}",
"/DMyOutputDir=${distributionDir.asFile.absolutePath}",

View File

@@ -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()) {

View File

@@ -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() {
}
}

View File

@@ -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())

View File

@@ -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
}
}
}

View File

@@ -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;