feat: robust update with .update suffix + bootstrap self-update
This commit is contained in:
@@ -10,6 +10,7 @@ import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class Bootstrap {
|
||||
private static final String VERSION_FILE = "build.version";
|
||||
@@ -30,6 +31,8 @@ public class Bootstrap {
|
||||
boolean cliMode = argList.contains("--cli");
|
||||
boolean jfxMode = !cliMode;
|
||||
|
||||
cleanupStaleUpdates();
|
||||
|
||||
String currentVersion = readCurrentVersion();
|
||||
String serverVersion = getServerVersion();
|
||||
|
||||
@@ -38,7 +41,17 @@ public class Bootstrap {
|
||||
|
||||
if (isNewer(serverVersion, currentVersion)) {
|
||||
log("Update available!");
|
||||
downloadUpdate(serverVersion);
|
||||
updateJar(serverVersion);
|
||||
|
||||
Path ownExe = getOwnExe();
|
||||
if (ownExe != null) {
|
||||
log("Self-updating bootstrap...");
|
||||
selfUpdate(ownExe);
|
||||
return;
|
||||
}
|
||||
|
||||
Files.writeString(baseDir.resolve(VERSION_FILE), serverVersion);
|
||||
log("Updated to v" + serverVersion);
|
||||
} else {
|
||||
log("Version is up to date");
|
||||
}
|
||||
@@ -50,13 +63,107 @@ public class Bootstrap {
|
||||
}
|
||||
}
|
||||
|
||||
private static void log(String msg) {
|
||||
String entry = "[" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + "] " + msg;
|
||||
System.out.println(entry);
|
||||
private static void cleanupStaleUpdates() {
|
||||
try (Stream<Path> files = Files.list(baseDir)) {
|
||||
files.filter(p -> p.toString().endsWith(".update"))
|
||||
.forEach(p -> {
|
||||
try {
|
||||
Files.writeString(logDir.resolve("launcher.log"), entry + "\n",
|
||||
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
|
||||
Files.deleteIfExists(p);
|
||||
log("Cleaned stale: " + p.getFileName());
|
||||
} catch (Exception ignored) {}
|
||||
});
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
private static void updateJar(String newVersion) throws Exception {
|
||||
Path jarFile = baseDir.resolve(JAR_NAME);
|
||||
Path jarUpdate = baseDir.resolve(JAR_NAME + ".update");
|
||||
|
||||
if (Files.exists(jarFile)) {
|
||||
try {
|
||||
Files.move(jarFile, jarUpdate, StandardCopyOption.REPLACE_EXISTING);
|
||||
log("Renamed " + JAR_NAME + " → " + JAR_NAME + ".update");
|
||||
} catch (Exception e) {
|
||||
log("Could not rename " + JAR_NAME + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
downloadFile(BASE_URL + "/launcher/download/jar", jarFile);
|
||||
log("Downloaded new " + JAR_NAME);
|
||||
|
||||
try {
|
||||
Files.deleteIfExists(jarUpdate);
|
||||
log("Removed " + JAR_NAME + ".update");
|
||||
} catch (Exception e) {
|
||||
log("Could not remove " + JAR_NAME + ".update: " + e.getMessage());
|
||||
}
|
||||
|
||||
Files.writeString(baseDir.resolve(VERSION_FILE), newVersion);
|
||||
}
|
||||
|
||||
private static void selfUpdate(Path exePath) throws Exception {
|
||||
String exeName = exePath.getFileName().toString();
|
||||
Path exeUpdate = baseDir.resolve(exeName + ".update");
|
||||
|
||||
try {
|
||||
Files.move(exePath, exeUpdate, StandardCopyOption.REPLACE_EXISTING);
|
||||
log("Renamed " + exeName + " → " + exeName + ".update");
|
||||
} catch (Exception e) {
|
||||
log("Could not rename " + exeName + ": " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
downloadFile(BASE_URL + "/launcher/download/exe", exePath);
|
||||
|
||||
log("Launching new " + exeName + "...");
|
||||
new ProcessBuilder(exePath.toAbsolutePath().toString())
|
||||
.directory(baseDir.toFile())
|
||||
.inheritIO()
|
||||
.start();
|
||||
|
||||
Thread.sleep(500);
|
||||
log("Exiting old process");
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
private static Path getOwnExe() {
|
||||
try {
|
||||
String cmd = ProcessHandle.current().info().command().orElse(null);
|
||||
if (cmd != null) {
|
||||
Path p = Paths.get(cmd);
|
||||
String name = p.getFileName().toString().toLowerCase();
|
||||
if (name.contains("zernmc")) return p;
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
for (String name : Arrays.asList("zernmc.exe", "zernmc-cli.exe")) {
|
||||
Path p = baseDir.resolve(name);
|
||||
if (Files.exists(p)) return p;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void downloadFile(String urlStr, Path target) throws Exception {
|
||||
URL url = new URL(urlStr);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
|
||||
if (conn.getResponseCode() == 200) {
|
||||
try (InputStream in = conn.getInputStream();
|
||||
OutputStream out = new FileOutputStream(target.toFile())) {
|
||||
byte[] buf = new byte[8192];
|
||||
int len;
|
||||
long total = 0;
|
||||
while ((len = in.read(buf)) > 0) {
|
||||
out.write(buf, 0, len);
|
||||
total += len;
|
||||
System.out.print("\rDownloaded: " + (total / 1024 / 1024) + " MB");
|
||||
}
|
||||
}
|
||||
System.out.println();
|
||||
} else {
|
||||
throw new IOException("Server returned code: " + conn.getResponseCode());
|
||||
}
|
||||
}
|
||||
|
||||
private static String readCurrentVersion() {
|
||||
@@ -100,39 +207,13 @@ public class Bootstrap {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void downloadUpdate(String newVersion) throws Exception {
|
||||
URL url = new URL(BASE_URL + "/launcher/download/jar");
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
|
||||
if (conn.getResponseCode() == 200) {
|
||||
Path jarFile = baseDir.resolve(JAR_NAME);
|
||||
Path tmp = jarFile.resolveSibling("zernmc-launcher-new.jar");
|
||||
|
||||
try (InputStream in = conn.getInputStream();
|
||||
OutputStream out = new FileOutputStream(tmp.toFile())) {
|
||||
byte[] buf = new byte[8192];
|
||||
int len;
|
||||
long total = 0;
|
||||
while ((len = in.read(buf)) > 0) {
|
||||
out.write(buf, 0, len);
|
||||
total += len;
|
||||
System.out.print("\rDownloaded: " + (total/1024/1024) + " MB");
|
||||
}
|
||||
}
|
||||
log("Downloaded");
|
||||
|
||||
Path backup = jarFile.resolveSibling(JAR_NAME + ".old");
|
||||
|
||||
if (Files.exists(jarFile)) Files.move(jarFile, backup, StandardCopyOption.REPLACE_EXISTING);
|
||||
Files.move(tmp, jarFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
if (Files.exists(backup)) Files.delete(backup);
|
||||
|
||||
Files.writeString(baseDir.resolve(VERSION_FILE), newVersion);
|
||||
log("Updated to v" + newVersion);
|
||||
} else {
|
||||
throw new IOException("Server returned code: " + conn.getResponseCode());
|
||||
}
|
||||
private static void log(String msg) {
|
||||
String entry = "[" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + "] " + msg;
|
||||
System.out.println(entry);
|
||||
try {
|
||||
Files.writeString(logDir.resolve("launcher.log"), entry + "\n",
|
||||
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
private static void launchJFX() throws Exception {
|
||||
|
||||
+2
-2
@@ -18,8 +18,8 @@
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
<revision>1.0.12</revision>
|
||||
<hotfix>1</hotfix>
|
||||
<revision>1.0.13</revision>
|
||||
<hotfix>0</hotfix>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
TODO / ideas backlog
|
||||
====================
|
||||
|
||||
Java Agent + IPC (low priority, overkill now)
|
||||
- Заменить прямой ProcessBuilder.start() на прокси-класс (-javaagent или -cp прокси)
|
||||
- Прокси класс: подключается к localhost RPC, ждёт "launch", потом reflection вызывает реальный main
|
||||
- Даёт: двустороннюю связь лаунчер-игра, контроль жизненного цикла, crash-репорты из процесса игры,
|
||||
передачу настроек/токенов на лету, патчинг классов через Instrumentation API
|
||||
- Для ZernMC сейчас оверхед — но запомнить на будущее
|
||||
Reference in New Issue
Block a user