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:
@@ -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}",
|
||||||
|
|||||||
@@ -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,8 +95,9 @@ class TermoraRestarter {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doRestart() {
|
private fun doRestart(commands: List<String>) {
|
||||||
|
|
||||||
|
if (commands.isEmpty()) {
|
||||||
if (SystemInfo.isMacOS) {
|
if (SystemInfo.isMacOS) {
|
||||||
Restarter.restart(arrayOf("open", "-n", macOSApplicationPath))
|
Restarter.restart(arrayOf("open", "-n", macOSApplicationPath))
|
||||||
} else if (SystemInfo.isWindows && startupCommand != null) {
|
} else if (SystemInfo.isWindows && startupCommand != null) {
|
||||||
@@ -108,6 +109,9 @@ class TermoraRestarter {
|
|||||||
Restarter.restart(arrayOf(startupCommand))
|
Restarter.restart(arrayOf(startupCommand))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Restarter.restart(commands.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
for (window in TermoraFrameManager.getInstance().getWindows()) {
|
for (window in TermoraFrameManager.getInstance().getWindows()) {
|
||||||
window.dispose()
|
window.dispose()
|
||||||
|
|||||||
@@ -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() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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())
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user