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 { exec {
commandLine( commandLine(
"iscc", "iscc",
"/DMyAppId=${projectName}",
"/DMyAppName=${projectName}", "/DMyAppName=${projectName}",
"/DMyAppVersion=${project.version}", "/DMyAppVersion=${project.version}",
"/DMyOutputDir=${distributionDir.asFile.absolutePath}", "/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 (!isSupported) return
if (!restarting.compareAndSet(false, true)) return if (!restarting.compareAndSet(false, true)) return
SwingUtilities.invokeLater { SwingUtilities.invokeLater {
try { try {
doRestart() doRestart(commands)
} catch (e: Exception) { } catch (e: Exception) {
if (log.isErrorEnabled) { if (log.isErrorEnabled) {
log.error(e.message, e) 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 (isSupported) {
if (OptionPane.showConfirmDialog( if (OptionPane.showConfirmDialog(
@@ -82,7 +82,7 @@ class TermoraRestarter {
initialValue = I18n.getString("termora.settings.restart.title") initialValue = I18n.getString("termora.settings.restart.title")
) == JOptionPane.YES_OPTION ) == JOptionPane.YES_OPTION
) { ) {
restart() restart(commands)
} }
} else { } else {
OptionPane.showMessageDialog( OptionPane.showMessageDialog(
@@ -95,18 +95,22 @@ class TermoraRestarter {
} }
private fun doRestart() { private fun doRestart(commands: List<String>) {
if (SystemInfo.isMacOS) { if (commands.isEmpty()) {
Restarter.restart(arrayOf("open", "-n", macOSApplicationPath)) if (SystemInfo.isMacOS) {
} else if (SystemInfo.isWindows && startupCommand != null) { Restarter.restart(arrayOf("open", "-n", macOSApplicationPath))
Restarter.restart(arrayOf(startupCommand)) } else if (SystemInfo.isWindows && startupCommand != null) {
} else if (SystemInfo.isLinux) {
if (isLinuxAppImage) {
Restarter.restart(arrayOf(System.getenv("APPIMAGE")))
} else if (startupCommand != null) {
Restarter.restart(arrayOf(startupCommand)) 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()) { for (window in TermoraFrameManager.getInstance().getWindows()) {

View File

@@ -70,6 +70,9 @@ class UpdaterManager private constructor() {
.build() .build()
val response = Application.httpClient.newCall(request).execute() val response = Application.httpClient.newCall(request).execute()
if (!response.isSuccessful) { if (!response.isSuccessful) {
if (log.isErrorEnabled) {
log.error("Failed to fetch latest version, response was ${response.code}")
}
return LatestVersion.self return LatestVersion.self
} }
@@ -151,8 +154,4 @@ class UpdaterManager private constructor() {
fun ignore(version: String) { fun ignore(version: String) {
properties.putString("ignored.version.$version", "true") 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(FindEverywhereAction.FIND_EVERYWHERE, FindEverywhereAction())
addAction(Actions.MULTIPLE, MultipleAction()) addAction(Actions.MULTIPLE, MultipleAction())
addAction(Actions.APP_UPDATE, AppUpdateAction()) addAction(Actions.APP_UPDATE, AppUpdateAction.getInstance())
addAction(Actions.KEYWORD_HIGHLIGHT, KeywordHighlightAction()) addAction(Actions.KEYWORD_HIGHLIGHT, KeywordHighlightAction())
addAction(Actions.TERMINAL_LOGGER, TerminalLoggerAction()) addAction(Actions.TERMINAL_LOGGER, TerminalLoggerAction())
addAction(Actions.SFTP, SFTPAction()) addAction(Actions.SFTP, SFTPAction())

View File

@@ -1,14 +1,28 @@
package app.termora.actions package app.termora.actions
import app.termora.* 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 io.github.g00fy2.versioncompare.Version
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing 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.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.JXEditorPane import org.jdesktop.swingx.JXEditorPane
import org.slf4j.LoggerFactory
import java.awt.Dimension import java.awt.Dimension
import java.awt.KeyboardFocusManager import java.awt.KeyboardFocusManager
import java.io.File
import java.net.ProxySelector
import java.net.URI import java.net.URI
import java.util.*
import java.util.concurrent.TimeUnit
import javax.swing.BorderFactory import javax.swing.BorderFactory
import javax.swing.JOptionPane import javax.swing.JOptionPane
import javax.swing.JScrollPane import javax.swing.JScrollPane
@@ -18,11 +32,20 @@ import kotlin.concurrent.fixedRateTimer
import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
class AppUpdateAction : AnAction( class AppUpdateAction private constructor() : AnAction(
StringUtils.EMPTY, StringUtils.EMPTY,
Icons.ideUpdate 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() private val updaterManager get() = UpdaterManager.getInstance()
init { init {
@@ -63,11 +86,75 @@ class AppUpdateAction : AnAction(
return return
} }
withContext(Dispatchers.Swing) { try {
ActionManager.getInstance() downloadLatestPkg(latestVersion)
.setEnabled(Actions.APP_UPDATE, true) } 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() { private fun showUpdateDialog() {
@@ -106,12 +193,59 @@ class AppUpdateAction : AnAction(
if (option == JOptionPane.CANCEL_OPTION) { if (option == JOptionPane.CANCEL_OPTION) {
return return
} else if (option == JOptionPane.NO_OPTION) { } else if (option == JOptionPane.NO_OPTION) {
ActionManager.getInstance().setEnabled(Actions.APP_UPDATE, false) isEnabled = false
updaterManager.ignore(updaterManager.lastVersion.version) updaterManager.ignore(lastVersion.version)
} else if (option == JOptionPane.YES_OPTION) { } else if (option == JOptionPane.YES_OPTION) {
ActionManager.getInstance() updateSelf(lastVersion)
.setEnabled(Actions.APP_UPDATE, false) }
Application.browse(URI.create("https://github.com/TermoraDev/termora/releases/tag/${lastVersion.version}")) }
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. ; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#define MyAppId "14F2657D-1B3F-4EA1-A1AA-AB6B5E4FD465"
#define MyAppPublisher "TermoraDev" #define MyAppPublisher "TermoraDev"
#define MyAppURL "https://github.com/TermoraDev/termora" #define MyAppURL "https://github.com/TermoraDev/termora"
#define MyAppSupportURL "https://github.com/TermoraDev/termora/issues" #define MyAppSupportURL "https://github.com/TermoraDev/termora/issues"
@@ -12,7 +11,7 @@
[Setup] [Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. ; 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.) ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={{{#MyAppId}} AppId={#MyAppId}
AppName={#MyAppName} AppName={#MyAppName}
AppVersion={#MyAppVersion} AppVersion={#MyAppVersion}
AppVerName={#MyAppName} {#MyAppVersion} AppVerName={#MyAppName} {#MyAppVersion}
@@ -57,5 +56,30 @@ Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Run] [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;