ClientUpdater.java 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. package com.mes.update;
  2. import org.slf4j.Logger;
  3. import org.slf4j.LoggerFactory;
  4. import javax.swing.JDialog;
  5. import javax.swing.JLabel;
  6. import javax.swing.JOptionPane;
  7. import javax.swing.JPanel;
  8. import javax.swing.JProgressBar;
  9. import javax.swing.SwingUtilities;
  10. import javax.swing.WindowConstants;
  11. import java.awt.BorderLayout;
  12. import java.awt.Component;
  13. import java.awt.Dimension;
  14. import java.awt.Frame;
  15. import java.io.File;
  16. import java.io.FileOutputStream;
  17. import java.io.OutputStreamWriter;
  18. import java.nio.charset.StandardCharsets;
  19. import java.util.concurrent.atomic.AtomicBoolean;
  20. /**
  21. * 启动时自动升级编排:检查 → 弹窗 → 下载 → 替换脚本 → 退出。
  22. */
  23. public final class ClientUpdater {
  24. private static final Logger log = LoggerFactory.getLogger(ClientUpdater.class);
  25. private ClientUpdater() {
  26. }
  27. /**
  28. * @return true 表示继续正常启动;false 表示应退出进程(已安排升级或强制更新失败)
  29. */
  30. public static boolean checkAndMaybeUpdate(String serverIp,
  31. String localVersionFromConfig,
  32. boolean enabled,
  33. String updatePath) {
  34. if (!enabled) {
  35. log.info("自动升级已关闭");
  36. return true;
  37. }
  38. File installDir = UpdateInstaller.resolveInstallDir();
  39. String localVersion = VersionUtil.resolveLocalVersion(installDir, localVersionFromConfig);
  40. log.info("本地版本={},安装目录={}", localVersion, installDir.getAbsolutePath());
  41. UpdateInfo info;
  42. try {
  43. info = new UpdateChecker().fetch(serverIp, updatePath);
  44. } catch (Exception e) {
  45. log.warn("升级检查失败,继续启动:{}", e.getMessage());
  46. return true;
  47. }
  48. if (info == null) {
  49. return true;
  50. }
  51. if (!VersionUtil.isNewer(info.getVersion(), localVersion)) {
  52. log.info("已是最新版本:local={} remote={}", localVersion, info.getVersion());
  53. return true;
  54. }
  55. log.info("发现新版本:{} -> {},force={}", localVersion, info.getVersion(), info.isForce());
  56. final boolean[] proceed = new boolean[]{false};
  57. try {
  58. runOnEdtWait(() -> proceed[0] = askUser(null, localVersion, info));
  59. } catch (Exception e) {
  60. log.warn("升级确认弹窗失败:{}", e.getMessage());
  61. return true;
  62. }
  63. if (!proceed[0]) {
  64. if (info.isForce()) {
  65. try {
  66. runOnEdtWait(() -> JOptionPane.showMessageDialog(null,
  67. "当前版本必须升级后才能使用。",
  68. "强制更新",
  69. JOptionPane.WARNING_MESSAGE));
  70. } catch (Exception ignored) {
  71. }
  72. return false;
  73. }
  74. return true;
  75. }
  76. boolean applied = downloadAndApply(installDir, info);
  77. if (!applied) {
  78. try {
  79. runOnEdtWait(() -> {
  80. if (info.isForce()) {
  81. JOptionPane.showMessageDialog(null,
  82. "强制更新失败,程序将退出。请检查网络或联系管理员。",
  83. "更新失败",
  84. JOptionPane.ERROR_MESSAGE);
  85. } else {
  86. JOptionPane.showMessageDialog(null,
  87. "更新失败,将继续使用当前版本。",
  88. "更新失败",
  89. JOptionPane.WARNING_MESSAGE);
  90. }
  91. });
  92. } catch (Exception ignored) {
  93. }
  94. return !info.isForce();
  95. }
  96. return false;
  97. }
  98. /** 已在 EDT 时直接执行,避免 invokeAndWait 死锁 */
  99. private static void runOnEdtWait(Runnable action) throws Exception {
  100. if (SwingUtilities.isEventDispatchThread()) {
  101. action.run();
  102. } else {
  103. SwingUtilities.invokeAndWait(action);
  104. }
  105. }
  106. private static boolean askUser(Component parent, String localVersion, UpdateInfo info) {
  107. String notes = info.getNotes() == null ? "" : info.getNotes().trim();
  108. StringBuilder msg = new StringBuilder();
  109. msg.append("发现新版本 ").append(info.getVersion())
  110. .append("(当前 ").append(localVersion).append(")\n\n");
  111. if (!notes.isEmpty()) {
  112. msg.append(notes).append("\n\n");
  113. }
  114. if (info.isForce()) {
  115. msg.append("本次为强制更新,必须升级后才能继续使用。");
  116. JOptionPane.showMessageDialog(parent, msg.toString(), "强制更新", JOptionPane.INFORMATION_MESSAGE);
  117. return true;
  118. }
  119. msg.append("是否立即更新?");
  120. int choice = JOptionPane.showConfirmDialog(parent, msg.toString(), "发现新版本",
  121. JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
  122. return choice == JOptionPane.YES_OPTION;
  123. }
  124. private static boolean downloadAndApply(File installDir, UpdateInfo info) {
  125. File tempRoot = new File(System.getProperty("java.io.tmpdir"),
  126. "mesclient-update-" + System.currentTimeMillis());
  127. File zipFile = new File(tempRoot, "package.zip");
  128. File extractDir = new File(tempRoot, "payload");
  129. if (!tempRoot.mkdirs()) {
  130. log.warn("无法创建临时目录:{}", tempRoot.getAbsolutePath());
  131. return false;
  132. }
  133. JProgressBar bar = new JProgressBar(0, 100);
  134. bar.setStringPainted(true);
  135. bar.setPreferredSize(new Dimension(360, 24));
  136. JLabel label = new JLabel("正在下载更新包...");
  137. JPanel panel = new JPanel(new BorderLayout(8, 8));
  138. panel.add(label, BorderLayout.NORTH);
  139. panel.add(bar, BorderLayout.CENTER);
  140. JDialog dialog = new JDialog((Frame) null, "正在更新", true);
  141. dialog.setContentPane(panel);
  142. dialog.pack();
  143. dialog.setLocationRelativeTo(null);
  144. dialog.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
  145. AtomicBoolean ok = new AtomicBoolean(false);
  146. Thread worker = new Thread(() -> {
  147. try {
  148. UpdateDownloader downloader = new UpdateDownloader();
  149. File downloaded = downloader.download(info.getPackageUrl(), zipFile, (downloadedBytes, total) -> {
  150. final int percent;
  151. if (total > 0) {
  152. percent = (int) Math.min(100, (downloadedBytes * 100) / total);
  153. } else {
  154. percent = -1;
  155. }
  156. SwingUtilities.invokeLater(() -> {
  157. if (percent < 0) {
  158. bar.setIndeterminate(true);
  159. bar.setString(formatSize(downloadedBytes));
  160. } else {
  161. bar.setIndeterminate(false);
  162. bar.setValue(percent);
  163. bar.setString(percent + "%");
  164. }
  165. });
  166. });
  167. if (downloaded == null) {
  168. return;
  169. }
  170. if (!downloader.verifySha256(downloaded, info.getSha256())) {
  171. SwingUtilities.invokeLater(() -> label.setText("校验失败"));
  172. return;
  173. }
  174. SwingUtilities.invokeLater(() -> {
  175. label.setText("正在解压...");
  176. bar.setIndeterminate(true);
  177. bar.setString("解压中");
  178. });
  179. UpdateInstaller installer = new UpdateInstaller();
  180. File payload = installer.unzip(downloaded, extractDir);
  181. File versionFile = new File(payload, "app.version");
  182. if (!versionFile.isFile()) {
  183. try (OutputStreamWriter w = new OutputStreamWriter(
  184. new FileOutputStream(versionFile), StandardCharsets.UTF_8)) {
  185. w.write(info.getVersion());
  186. }
  187. }
  188. String exeName = UpdateInstaller.resolveExeName(installDir);
  189. File[] payloadFiles = payload.listFiles();
  190. if (payloadFiles != null) {
  191. for (File f : payloadFiles) {
  192. if (f.isFile() && f.getName().toLowerCase().endsWith(".exe")) {
  193. exeName = f.getName();
  194. break;
  195. }
  196. }
  197. }
  198. SwingUtilities.invokeLater(() -> label.setText("准备替换并重启..."));
  199. installer.launchReplaceScript(installDir, payload, exeName);
  200. ok.set(true);
  201. } catch (Exception e) {
  202. log.warn("执行升级失败:{}", e.getMessage(), e);
  203. } finally {
  204. SwingUtilities.invokeLater(dialog::dispose);
  205. }
  206. }, "mes-client-updater");
  207. worker.setDaemon(true);
  208. worker.start();
  209. dialog.setVisible(true);
  210. try {
  211. worker.join(5 * 60 * 1000L);
  212. } catch (InterruptedException e) {
  213. Thread.currentThread().interrupt();
  214. }
  215. if (!ok.get()) {
  216. UpdateInstaller.deleteRecursively(tempRoot);
  217. }
  218. return ok.get();
  219. }
  220. private static String formatSize(long bytes) {
  221. if (bytes < 1024) {
  222. return bytes + " B";
  223. }
  224. if (bytes < 1024 * 1024) {
  225. return String.format("%.1f KB", bytes / 1024.0);
  226. }
  227. return String.format("%.1f MB", bytes / (1024.0 * 1024.0));
  228. }
  229. }