diff --git a/build.gradle.kts b/build.gradle.kts index a6d0676..1569dc6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -526,6 +526,7 @@ fun packOnWindows(distributionDir: Directory, finalFilenameWithoutExtension: Str exec { commandLine( "iscc", + "/DMyAppId=${projectName}", "/DMyAppName=${projectName}", "/DMyAppVersion=${project.version}", "/DMyOutputDir=${distributionDir.asFile.absolutePath}", diff --git a/src/main/kotlin/app/termora/TermoraRestarter.kt b/src/main/kotlin/app/termora/TermoraRestarter.kt index e2adb2c..65546c6 100644 --- a/src/main/kotlin/app/termora/TermoraRestarter.kt +++ b/src/main/kotlin/app/termora/TermoraRestarter.kt @@ -48,13 +48,13 @@ class TermoraRestarter { ) } - private fun restart() { + private fun restart(commands: List) { 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 = 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) { - 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()) { diff --git a/src/main/kotlin/app/termora/UpdaterManager.kt b/src/main/kotlin/app/termora/UpdaterManager.kt index 40a712e..a9eccb4 100644 --- a/src/main/kotlin/app/termora/UpdaterManager.kt +++ b/src/main/kotlin/app/termora/UpdaterManager.kt @@ -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() { - - } } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/actions/ActionManager.kt b/src/main/kotlin/app/termora/actions/ActionManager.kt index 520a91a..6d4e0f7 100644 --- a/src/main/kotlin/app/termora/actions/ActionManager.kt +++ b/src/main/kotlin/app/termora/actions/ActionManager.kt @@ -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()) diff --git a/src/main/kotlin/app/termora/actions/AppUpdateAction.kt b/src/main/kotlin/app/termora/actions/AppUpdateAction.kt index 51dc3e2..903e20d 100644 --- a/src/main/kotlin/app/termora/actions/AppUpdateAction.kt +++ b/src/main/kotlin/app/termora/actions/AppUpdateAction.kt @@ -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 } } } \ No newline at end of file diff --git a/src/main/resources/termora.iss b/src/main/resources/termora.iss index e096b10..febad37 100644 --- a/src/main/resources/termora.iss +++ b/src/main/resources/termora.iss @@ -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;