| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274 |
- package com.mes.update;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import java.io.BufferedOutputStream;
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.FileOutputStream;
- import java.io.OutputStreamWriter;
- import java.io.PrintWriter;
- import java.lang.management.ManagementFactory;
- import java.nio.charset.Charset;
- import java.util.Enumeration;
- import java.util.zip.ZipEntry;
- import java.util.zip.ZipFile;
- import java.util.zip.ZipInputStream;
- /**
- * 解压升级包并生成 Windows 替换脚本(进程退出后覆盖文件并重启)。
- */
- public class UpdateInstaller {
- private static final Logger log = LoggerFactory.getLogger(UpdateInstaller.class);
- /**
- * 解压 zip 到目标目录(已存在则清空重建)。
- */
- public File unzip(File zipFile, File extractDir) throws Exception {
- if (extractDir.exists()) {
- deleteRecursively(extractDir);
- }
- if (!extractDir.mkdirs()) {
- throw new IllegalStateException("无法创建解压目录: " + extractDir.getAbsolutePath());
- }
- try {
- unzipWithZipFile(zipFile, extractDir);
- } catch (Exception encodingIssue) {
- log.warn("ZipFile 解压失败,回退 ZipInputStream:{}", encodingIssue.getMessage());
- if (extractDir.exists()) {
- deleteRecursively(extractDir);
- }
- if (!extractDir.mkdirs()) {
- throw new IllegalStateException("无法创建解压目录: " + extractDir.getAbsolutePath());
- }
- unzipFallback(zipFile, extractDir);
- }
- return flattenIfSingleRoot(extractDir);
- }
- private void unzipWithZipFile(File zipFile, File extractDir) throws Exception {
- try (ZipFile zip = new ZipFile(zipFile, Charset.forName("UTF-8"))) {
- Enumeration<? extends ZipEntry> entries = zip.entries();
- byte[] buffer = new byte[8192];
- while (entries.hasMoreElements()) {
- ZipEntry entry = entries.nextElement();
- writeZipEntry(extractDir, entry, zip.getInputStream(entry), buffer);
- }
- }
- }
- private void unzipFallback(File zipFile, File extractDir) throws Exception {
- byte[] buffer = new byte[8192];
- try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) {
- ZipEntry entry;
- while ((entry = zis.getNextEntry()) != null) {
- writeZipEntry(extractDir, entry, zis, buffer);
- zis.closeEntry();
- }
- }
- }
- private void writeZipEntry(File extractDir, ZipEntry entry, java.io.InputStream in, byte[] buffer) throws Exception {
- File outFile = new File(extractDir, entry.getName());
- if (!isSafeExtractPath(extractDir, outFile)) {
- throw new IllegalStateException("非法压缩条目: " + entry.getName());
- }
- if (entry.isDirectory()) {
- if (!outFile.exists() && !outFile.mkdirs()) {
- throw new IllegalStateException("无法创建目录: " + outFile.getAbsolutePath());
- }
- return;
- }
- File parent = outFile.getParentFile();
- if (parent != null && !parent.exists() && !parent.mkdirs()) {
- throw new IllegalStateException("无法创建目录: " + parent.getAbsolutePath());
- }
- try (FileOutputStream out = new FileOutputStream(outFile);
- BufferedOutputStream bos = new BufferedOutputStream(out)) {
- int n;
- while ((n = in.read(buffer)) >= 0) {
- bos.write(buffer, 0, n);
- }
- }
- }
- private File flattenIfSingleRoot(File extractDir) throws Exception {
- File[] children = extractDir.listFiles();
- if (children == null || children.length != 1 || !children[0].isDirectory()) {
- return extractDir;
- }
- File nested = children[0];
- if (!hasAppPayload(nested)) {
- return extractDir;
- }
- File flat = new File(extractDir.getParentFile(), extractDir.getName() + "-flat");
- if (flat.exists()) {
- deleteRecursively(flat);
- }
- if (!nested.renameTo(flat)) {
- copyDirectory(nested, flat);
- deleteRecursively(extractDir);
- return flat;
- }
- deleteRecursively(extractDir);
- return flat;
- }
- private boolean hasAppPayload(File dir) {
- File[] files = dir.listFiles();
- if (files == null) {
- return false;
- }
- for (File f : files) {
- String name = f.getName().toLowerCase();
- if (name.endsWith(".jar") || name.endsWith(".exe") || name.equals("app.version")) {
- return true;
- }
- }
- return false;
- }
- /**
- * 生成并启动 update.bat:等待当前 PID 退出后覆盖安装目录并启动 EXE。
- */
- public void launchReplaceScript(File installDir, File payloadDir, String exeName) throws Exception {
- if (installDir == null || !installDir.isDirectory()) {
- throw new IllegalStateException("安装目录无效");
- }
- if (payloadDir == null || !payloadDir.isDirectory()) {
- throw new IllegalStateException("升级包内容目录无效");
- }
- String exe = (exeName == null || exeName.trim().isEmpty()) ? resolveExeName(installDir) : exeName.trim();
- long pid = currentPid();
- File script = new File(System.getProperty("java.io.tmpdir"), "mesclient-update-" + pid + ".bat");
- writeBat(script, pid, installDir, payloadDir, exe);
- ProcessBuilder pb = new ProcessBuilder(
- "cmd.exe", "/c", "start", "\"mesclient-update\"", "/min", script.getAbsolutePath());
- pb.directory(installDir);
- pb.start();
- log.info("已启动升级脚本:{}", script.getAbsolutePath());
- }
- private void writeBat(File script, long pid, File installDir, File payloadDir, String exeName) throws Exception {
- Charset charset = Charset.defaultCharset();
- try (PrintWriter out = new PrintWriter(new OutputStreamWriter(new FileOutputStream(script), charset))) {
- out.println("@echo off");
- out.println("setlocal EnableExtensions");
- out.println("set \"PID=" + pid + "\"");
- out.println("set \"INSTALL_DIR=" + installDir.getAbsolutePath() + "\"");
- out.println("set \"SOURCE_DIR=" + payloadDir.getAbsolutePath() + "\"");
- out.println("set \"EXE_NAME=" + exeName + "\"");
- out.println("echo [mes-update] waiting process %PID% ...");
- out.println(":wait");
- out.println("tasklist /FI \"PID eq %PID%\" 2>NUL | findstr /I /C:\" %PID% \" >NUL");
- out.println("if not errorlevel 1 (");
- out.println(" timeout /t 1 /nobreak >NUL");
- out.println(" goto wait");
- out.println(")");
- out.println("echo [mes-update] copying files ...");
- out.println("xcopy /E /Y /I /Q \"%SOURCE_DIR%\\*\" \"%INSTALL_DIR%\\\" >NUL");
- out.println("if errorlevel 1 (");
- out.println(" echo [mes-update] copy failed");
- out.println(" pause");
- out.println(" exit /b 1");
- out.println(")");
- out.println("echo [mes-update] starting %EXE_NAME%");
- out.println("start \"\" \"%INSTALL_DIR%\\%EXE_NAME%\"");
- out.println("rmdir /S /Q \"%SOURCE_DIR%\" >NUL 2>&1");
- out.println("del \"%~f0\" >NUL 2>&1");
- out.println("endlocal");
- out.println("exit /b 0");
- }
- }
- public static long currentPid() {
- try {
- String name = ManagementFactory.getRuntimeMXBean().getName();
- int at = name.indexOf('@');
- if (at > 0) {
- return Long.parseLong(name.substring(0, at));
- }
- } catch (Exception ignored) {
- }
- return 0L;
- }
- public static String resolveExeName(File installDir) {
- File preferred = new File(installDir, "MesClient.exe");
- if (preferred.isFile()) {
- return preferred.getName();
- }
- File[] files = installDir.listFiles();
- if (files != null) {
- for (File f : files) {
- if (f.isFile() && f.getName().toLowerCase().endsWith(".exe")) {
- return f.getName();
- }
- }
- }
- return "MesClient.exe";
- }
- public static File resolveInstallDir() {
- try {
- java.net.URL loc = UpdateInstaller.class.getProtectionDomain().getCodeSource().getLocation();
- File code = new File(loc.toURI());
- if (code.isFile()) {
- return code.getParentFile();
- }
- return new File(System.getProperty("user.dir"));
- } catch (Exception e) {
- return new File(System.getProperty("user.dir"));
- }
- }
- private static boolean isSafeExtractPath(File baseDir, File target) throws Exception {
- String base = baseDir.getCanonicalPath();
- String path = target.getCanonicalPath();
- return path.startsWith(base + File.separator) || path.equals(base);
- }
- private static void copyDirectory(File src, File dest) throws Exception {
- if (!dest.exists() && !dest.mkdirs()) {
- throw new IllegalStateException("无法创建目录: " + dest.getAbsolutePath());
- }
- File[] files = src.listFiles();
- if (files == null) {
- return;
- }
- byte[] buffer = new byte[8192];
- for (File file : files) {
- File target = new File(dest, file.getName());
- if (file.isDirectory()) {
- copyDirectory(file, target);
- } else {
- try (FileInputStream in = new FileInputStream(file);
- FileOutputStream out = new FileOutputStream(target)) {
- int n;
- while ((n = in.read(buffer)) >= 0) {
- out.write(buffer, 0, n);
- }
- }
- }
- }
- }
- public static void deleteRecursively(File file) {
- if (file == null || !file.exists()) {
- return;
- }
- if (file.isDirectory()) {
- File[] children = file.listFiles();
- if (children != null) {
- for (File child : children) {
- deleteRecursively(child);
- }
- }
- }
- // noinspection ResultOfMethodCallIgnored
- file.delete();
- }
- }
|