UpdateInstaller.java 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. package com.mes.update;
  2. import org.slf4j.Logger;
  3. import org.slf4j.LoggerFactory;
  4. import java.io.BufferedOutputStream;
  5. import java.io.File;
  6. import java.io.FileInputStream;
  7. import java.io.FileOutputStream;
  8. import java.io.OutputStreamWriter;
  9. import java.io.PrintWriter;
  10. import java.lang.management.ManagementFactory;
  11. import java.nio.charset.Charset;
  12. import java.util.Enumeration;
  13. import java.util.zip.ZipEntry;
  14. import java.util.zip.ZipFile;
  15. import java.util.zip.ZipInputStream;
  16. /**
  17. * 解压升级包并生成 Windows 替换脚本(进程退出后覆盖文件并重启)。
  18. */
  19. public class UpdateInstaller {
  20. private static final Logger log = LoggerFactory.getLogger(UpdateInstaller.class);
  21. /**
  22. * 解压 zip 到目标目录(已存在则清空重建)。
  23. */
  24. public File unzip(File zipFile, File extractDir) throws Exception {
  25. if (extractDir.exists()) {
  26. deleteRecursively(extractDir);
  27. }
  28. if (!extractDir.mkdirs()) {
  29. throw new IllegalStateException("无法创建解压目录: " + extractDir.getAbsolutePath());
  30. }
  31. try {
  32. unzipWithZipFile(zipFile, extractDir);
  33. } catch (Exception encodingIssue) {
  34. log.warn("ZipFile 解压失败,回退 ZipInputStream:{}", encodingIssue.getMessage());
  35. if (extractDir.exists()) {
  36. deleteRecursively(extractDir);
  37. }
  38. if (!extractDir.mkdirs()) {
  39. throw new IllegalStateException("无法创建解压目录: " + extractDir.getAbsolutePath());
  40. }
  41. unzipFallback(zipFile, extractDir);
  42. }
  43. return flattenIfSingleRoot(extractDir);
  44. }
  45. private void unzipWithZipFile(File zipFile, File extractDir) throws Exception {
  46. try (ZipFile zip = new ZipFile(zipFile, Charset.forName("UTF-8"))) {
  47. Enumeration<? extends ZipEntry> entries = zip.entries();
  48. byte[] buffer = new byte[8192];
  49. while (entries.hasMoreElements()) {
  50. ZipEntry entry = entries.nextElement();
  51. writeZipEntry(extractDir, entry, zip.getInputStream(entry), buffer);
  52. }
  53. }
  54. }
  55. private void unzipFallback(File zipFile, File extractDir) throws Exception {
  56. byte[] buffer = new byte[8192];
  57. try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) {
  58. ZipEntry entry;
  59. while ((entry = zis.getNextEntry()) != null) {
  60. writeZipEntry(extractDir, entry, zis, buffer);
  61. zis.closeEntry();
  62. }
  63. }
  64. }
  65. private void writeZipEntry(File extractDir, ZipEntry entry, java.io.InputStream in, byte[] buffer) throws Exception {
  66. File outFile = new File(extractDir, entry.getName());
  67. if (!isSafeExtractPath(extractDir, outFile)) {
  68. throw new IllegalStateException("非法压缩条目: " + entry.getName());
  69. }
  70. if (entry.isDirectory()) {
  71. if (!outFile.exists() && !outFile.mkdirs()) {
  72. throw new IllegalStateException("无法创建目录: " + outFile.getAbsolutePath());
  73. }
  74. return;
  75. }
  76. File parent = outFile.getParentFile();
  77. if (parent != null && !parent.exists() && !parent.mkdirs()) {
  78. throw new IllegalStateException("无法创建目录: " + parent.getAbsolutePath());
  79. }
  80. try (FileOutputStream out = new FileOutputStream(outFile);
  81. BufferedOutputStream bos = new BufferedOutputStream(out)) {
  82. int n;
  83. while ((n = in.read(buffer)) >= 0) {
  84. bos.write(buffer, 0, n);
  85. }
  86. }
  87. }
  88. private File flattenIfSingleRoot(File extractDir) throws Exception {
  89. File[] children = extractDir.listFiles();
  90. if (children == null || children.length != 1 || !children[0].isDirectory()) {
  91. return extractDir;
  92. }
  93. File nested = children[0];
  94. if (!hasAppPayload(nested)) {
  95. return extractDir;
  96. }
  97. File flat = new File(extractDir.getParentFile(), extractDir.getName() + "-flat");
  98. if (flat.exists()) {
  99. deleteRecursively(flat);
  100. }
  101. if (!nested.renameTo(flat)) {
  102. copyDirectory(nested, flat);
  103. deleteRecursively(extractDir);
  104. return flat;
  105. }
  106. deleteRecursively(extractDir);
  107. return flat;
  108. }
  109. private boolean hasAppPayload(File dir) {
  110. File[] files = dir.listFiles();
  111. if (files == null) {
  112. return false;
  113. }
  114. for (File f : files) {
  115. String name = f.getName().toLowerCase();
  116. if (name.endsWith(".jar") || name.endsWith(".exe") || name.equals("app.version")) {
  117. return true;
  118. }
  119. }
  120. return false;
  121. }
  122. /**
  123. * 生成并启动 update.bat:等待当前 PID 退出后覆盖安装目录并启动 EXE。
  124. */
  125. public void launchReplaceScript(File installDir, File payloadDir, String exeName) throws Exception {
  126. if (installDir == null || !installDir.isDirectory()) {
  127. throw new IllegalStateException("安装目录无效");
  128. }
  129. if (payloadDir == null || !payloadDir.isDirectory()) {
  130. throw new IllegalStateException("升级包内容目录无效");
  131. }
  132. String exe = (exeName == null || exeName.trim().isEmpty()) ? resolveExeName(installDir) : exeName.trim();
  133. long pid = currentPid();
  134. File script = new File(System.getProperty("java.io.tmpdir"), "mesclient-update-" + pid + ".bat");
  135. writeBat(script, pid, installDir, payloadDir, exe);
  136. ProcessBuilder pb = new ProcessBuilder(
  137. "cmd.exe", "/c", "start", "\"mesclient-update\"", "/min", script.getAbsolutePath());
  138. pb.directory(installDir);
  139. pb.start();
  140. log.info("已启动升级脚本:{}", script.getAbsolutePath());
  141. }
  142. private void writeBat(File script, long pid, File installDir, File payloadDir, String exeName) throws Exception {
  143. Charset charset = Charset.defaultCharset();
  144. try (PrintWriter out = new PrintWriter(new OutputStreamWriter(new FileOutputStream(script), charset))) {
  145. out.println("@echo off");
  146. out.println("setlocal EnableExtensions");
  147. out.println("set \"PID=" + pid + "\"");
  148. out.println("set \"INSTALL_DIR=" + installDir.getAbsolutePath() + "\"");
  149. out.println("set \"SOURCE_DIR=" + payloadDir.getAbsolutePath() + "\"");
  150. out.println("set \"EXE_NAME=" + exeName + "\"");
  151. out.println("echo [mes-update] waiting process %PID% ...");
  152. out.println(":wait");
  153. out.println("tasklist /FI \"PID eq %PID%\" 2>NUL | findstr /I /C:\" %PID% \" >NUL");
  154. out.println("if not errorlevel 1 (");
  155. out.println(" timeout /t 1 /nobreak >NUL");
  156. out.println(" goto wait");
  157. out.println(")");
  158. out.println("echo [mes-update] copying files ...");
  159. out.println("xcopy /E /Y /I /Q \"%SOURCE_DIR%\\*\" \"%INSTALL_DIR%\\\" >NUL");
  160. out.println("if errorlevel 1 (");
  161. out.println(" echo [mes-update] copy failed");
  162. out.println(" pause");
  163. out.println(" exit /b 1");
  164. out.println(")");
  165. out.println("echo [mes-update] starting %EXE_NAME%");
  166. out.println("start \"\" \"%INSTALL_DIR%\\%EXE_NAME%\"");
  167. out.println("rmdir /S /Q \"%SOURCE_DIR%\" >NUL 2>&1");
  168. out.println("del \"%~f0\" >NUL 2>&1");
  169. out.println("endlocal");
  170. out.println("exit /b 0");
  171. }
  172. }
  173. public static long currentPid() {
  174. try {
  175. String name = ManagementFactory.getRuntimeMXBean().getName();
  176. int at = name.indexOf('@');
  177. if (at > 0) {
  178. return Long.parseLong(name.substring(0, at));
  179. }
  180. } catch (Exception ignored) {
  181. }
  182. return 0L;
  183. }
  184. public static String resolveExeName(File installDir) {
  185. File preferred = new File(installDir, "MesClient.exe");
  186. if (preferred.isFile()) {
  187. return preferred.getName();
  188. }
  189. File[] files = installDir.listFiles();
  190. if (files != null) {
  191. for (File f : files) {
  192. if (f.isFile() && f.getName().toLowerCase().endsWith(".exe")) {
  193. return f.getName();
  194. }
  195. }
  196. }
  197. return "MesClient.exe";
  198. }
  199. public static File resolveInstallDir() {
  200. try {
  201. java.net.URL loc = UpdateInstaller.class.getProtectionDomain().getCodeSource().getLocation();
  202. File code = new File(loc.toURI());
  203. if (code.isFile()) {
  204. return code.getParentFile();
  205. }
  206. return new File(System.getProperty("user.dir"));
  207. } catch (Exception e) {
  208. return new File(System.getProperty("user.dir"));
  209. }
  210. }
  211. private static boolean isSafeExtractPath(File baseDir, File target) throws Exception {
  212. String base = baseDir.getCanonicalPath();
  213. String path = target.getCanonicalPath();
  214. return path.startsWith(base + File.separator) || path.equals(base);
  215. }
  216. private static void copyDirectory(File src, File dest) throws Exception {
  217. if (!dest.exists() && !dest.mkdirs()) {
  218. throw new IllegalStateException("无法创建目录: " + dest.getAbsolutePath());
  219. }
  220. File[] files = src.listFiles();
  221. if (files == null) {
  222. return;
  223. }
  224. byte[] buffer = new byte[8192];
  225. for (File file : files) {
  226. File target = new File(dest, file.getName());
  227. if (file.isDirectory()) {
  228. copyDirectory(file, target);
  229. } else {
  230. try (FileInputStream in = new FileInputStream(file);
  231. FileOutputStream out = new FileOutputStream(target)) {
  232. int n;
  233. while ((n = in.read(buffer)) >= 0) {
  234. out.write(buffer, 0, n);
  235. }
  236. }
  237. }
  238. }
  239. }
  240. public static void deleteRecursively(File file) {
  241. if (file == null || !file.exists()) {
  242. return;
  243. }
  244. if (file.isDirectory()) {
  245. File[] children = file.listFiles();
  246. if (children != null) {
  247. for (File child : children) {
  248. deleteRecursively(child);
  249. }
  250. }
  251. }
  252. // noinspection ResultOfMethodCallIgnored
  253. file.delete();
  254. }
  255. }