package com.mes.update; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JProgressBar; import javax.swing.SwingUtilities; import javax.swing.WindowConstants; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; import java.awt.Frame; import java.io.File; import java.io.FileOutputStream; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; import java.util.concurrent.atomic.AtomicBoolean; /** * 启动时自动升级编排:检查 → 弹窗 → 下载 → 替换脚本 → 退出。 */ public final class ClientUpdater { private static final Logger log = LoggerFactory.getLogger(ClientUpdater.class); private ClientUpdater() { } /** * @return true 表示继续正常启动;false 表示应退出进程(已安排升级或强制更新失败) */ public static boolean checkAndMaybeUpdate(String serverIp, String localVersionFromConfig, boolean enabled, String updatePath) { if (!enabled) { log.info("自动升级已关闭"); return true; } File installDir = UpdateInstaller.resolveInstallDir(); String localVersion = VersionUtil.resolveLocalVersion(installDir, localVersionFromConfig); log.info("本地版本={},安装目录={}", localVersion, installDir.getAbsolutePath()); UpdateInfo info; try { info = new UpdateChecker().fetch(serverIp, updatePath); } catch (Exception e) { log.warn("升级检查失败,继续启动:{}", e.getMessage()); return true; } if (info == null) { return true; } if (!VersionUtil.isNewer(info.getVersion(), localVersion)) { log.info("已是最新版本:local={} remote={}", localVersion, info.getVersion()); return true; } log.info("发现新版本:{} -> {},force={}", localVersion, info.getVersion(), info.isForce()); final boolean[] proceed = new boolean[]{false}; try { runOnEdtWait(() -> proceed[0] = askUser(null, localVersion, info)); } catch (Exception e) { log.warn("升级确认弹窗失败:{}", e.getMessage()); return true; } if (!proceed[0]) { if (info.isForce()) { try { runOnEdtWait(() -> JOptionPane.showMessageDialog(null, "当前版本必须升级后才能使用。", "强制更新", JOptionPane.WARNING_MESSAGE)); } catch (Exception ignored) { } return false; } return true; } boolean applied = downloadAndApply(installDir, info); if (!applied) { try { runOnEdtWait(() -> { if (info.isForce()) { JOptionPane.showMessageDialog(null, "强制更新失败,程序将退出。请检查网络或联系管理员。", "更新失败", JOptionPane.ERROR_MESSAGE); } else { JOptionPane.showMessageDialog(null, "更新失败,将继续使用当前版本。", "更新失败", JOptionPane.WARNING_MESSAGE); } }); } catch (Exception ignored) { } return !info.isForce(); } return false; } /** 已在 EDT 时直接执行,避免 invokeAndWait 死锁 */ private static void runOnEdtWait(Runnable action) throws Exception { if (SwingUtilities.isEventDispatchThread()) { action.run(); } else { SwingUtilities.invokeAndWait(action); } } private static boolean askUser(Component parent, String localVersion, UpdateInfo info) { String notes = info.getNotes() == null ? "" : info.getNotes().trim(); StringBuilder msg = new StringBuilder(); msg.append("发现新版本 ").append(info.getVersion()) .append("(当前 ").append(localVersion).append(")\n\n"); if (!notes.isEmpty()) { msg.append(notes).append("\n\n"); } if (info.isForce()) { msg.append("本次为强制更新,必须升级后才能继续使用。"); JOptionPane.showMessageDialog(parent, msg.toString(), "强制更新", JOptionPane.INFORMATION_MESSAGE); return true; } msg.append("是否立即更新?"); int choice = JOptionPane.showConfirmDialog(parent, msg.toString(), "发现新版本", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); return choice == JOptionPane.YES_OPTION; } private static boolean downloadAndApply(File installDir, UpdateInfo info) { File tempRoot = new File(System.getProperty("java.io.tmpdir"), "mesclient-update-" + System.currentTimeMillis()); File zipFile = new File(tempRoot, "package.zip"); File extractDir = new File(tempRoot, "payload"); if (!tempRoot.mkdirs()) { log.warn("无法创建临时目录:{}", tempRoot.getAbsolutePath()); return false; } JProgressBar bar = new JProgressBar(0, 100); bar.setStringPainted(true); bar.setPreferredSize(new Dimension(360, 24)); JLabel label = new JLabel("正在下载更新包..."); JPanel panel = new JPanel(new BorderLayout(8, 8)); panel.add(label, BorderLayout.NORTH); panel.add(bar, BorderLayout.CENTER); JDialog dialog = new JDialog((Frame) null, "正在更新", true); dialog.setContentPane(panel); dialog.pack(); dialog.setLocationRelativeTo(null); dialog.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); AtomicBoolean ok = new AtomicBoolean(false); Thread worker = new Thread(() -> { try { UpdateDownloader downloader = new UpdateDownloader(); File downloaded = downloader.download(info.getPackageUrl(), zipFile, (downloadedBytes, total) -> { final int percent; if (total > 0) { percent = (int) Math.min(100, (downloadedBytes * 100) / total); } else { percent = -1; } SwingUtilities.invokeLater(() -> { if (percent < 0) { bar.setIndeterminate(true); bar.setString(formatSize(downloadedBytes)); } else { bar.setIndeterminate(false); bar.setValue(percent); bar.setString(percent + "%"); } }); }); if (downloaded == null) { return; } if (!downloader.verifySha256(downloaded, info.getSha256())) { SwingUtilities.invokeLater(() -> label.setText("校验失败")); return; } SwingUtilities.invokeLater(() -> { label.setText("正在解压..."); bar.setIndeterminate(true); bar.setString("解压中"); }); UpdateInstaller installer = new UpdateInstaller(); File payload = installer.unzip(downloaded, extractDir); File versionFile = new File(payload, "app.version"); if (!versionFile.isFile()) { try (OutputStreamWriter w = new OutputStreamWriter( new FileOutputStream(versionFile), StandardCharsets.UTF_8)) { w.write(info.getVersion()); } } String exeName = UpdateInstaller.resolveExeName(installDir); File[] payloadFiles = payload.listFiles(); if (payloadFiles != null) { for (File f : payloadFiles) { if (f.isFile() && f.getName().toLowerCase().endsWith(".exe")) { exeName = f.getName(); break; } } } SwingUtilities.invokeLater(() -> label.setText("准备替换并重启...")); installer.launchReplaceScript(installDir, payload, exeName); ok.set(true); } catch (Exception e) { log.warn("执行升级失败:{}", e.getMessage(), e); } finally { SwingUtilities.invokeLater(dialog::dispose); } }, "mes-client-updater"); worker.setDaemon(true); worker.start(); dialog.setVisible(true); try { worker.join(5 * 60 * 1000L); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } if (!ok.get()) { UpdateInstaller.deleteRecursively(tempRoot); } return ok.get(); } private static String formatSize(long bytes) { if (bytes < 1024) { return bytes + " B"; } if (bytes < 1024 * 1024) { return String.format("%.1f KB", bytes / 1024.0); } return String.format("%.1f MB", bytes / (1024.0 * 1024.0)); } }