Skip to content

Commit

Permalink
Fix update checker promoting downgrades, other followup improvements …
Browse files Browse the repository at this point in the history
…(#716)

- Fixed update checker promoting downgrades
  • Loading branch information
LostLuma committed Jun 15, 2024
1 parent e0d220d commit 7c4ac5f
Show file tree
Hide file tree
Showing 8 changed files with 257 additions and 97 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ public class DescriptionListWidget extends EntryListWidget<DescriptionListWidget

private static final Text HAS_UPDATE_TEXT = new TranslatableText("modmenu.hasUpdate");
private static final Text EXPERIMENTAL_TEXT = new TranslatableText("modmenu.experimental").setFormatting(Formatting.GOLD);
private static final Text MODRINTH_TEXT = new TranslatableText("modmenu.modrinth");
private static final Text DOWNLOAD_TEXT = new TranslatableText("modmenu.downloadLink").setFormatting(Formatting.BLUE, Formatting.UNDERLINE);
private static final Text CHILD_HAS_UPDATE_TEXT = new TranslatableText("modmenu.childHasUpdate");
private static final Text LINKS_TEXT = new TranslatableText("modmenu.links");
Expand Down Expand Up @@ -105,30 +104,20 @@ public void render(int mouseX, int mouseY, float delta) {
children().add(new DescriptionEntry(line, 8));
}

if (updateInfo instanceof ModrinthUpdateInfo) {
ModrinthUpdateInfo modrinthUpdateInfo = (ModrinthUpdateInfo) updateInfo;
Text updateText = new TranslatableText("modmenu.updateText", VersionUtil.stripPrefix(modrinthUpdateInfo.getVersionNumber()), MODRINTH_TEXT)
.setFormatting(Formatting.BLUE, Formatting.UNDERLINE);

for (String line : textRenderer.split(updateText.getFormattedString(), wrapWidth - 16)) {
children().add(new LinkEntry(line, modrinthUpdateInfo.getDownloadLink(), 8));
}
Text updateMessage = updateInfo.getUpdateMessage();
String downloadLink = updateInfo.getDownloadLink();
if (updateMessage == null) {
updateMessage = DOWNLOAD_TEXT;
} else {
Text updateMessage = updateInfo.getUpdateMessage();
String downloadLink = updateInfo.getDownloadLink();
if (updateMessage == null) {
updateMessage = DOWNLOAD_TEXT;
} else {
if (downloadLink != null) {
updateMessage = updateMessage.copy().setFormatting(Formatting.BLUE, Formatting.UNDERLINE);
}
if (downloadLink != null) {
updateMessage = updateMessage.copy().setFormatting(Formatting.BLUE, Formatting.UNDERLINE);
}
for (String line : textRenderer.split(updateMessage.getFormattedString(), wrapWidth - 16)) {
if (downloadLink != null) {
children().add(new LinkEntry(line, downloadLink, 8));
} else {
children().add(new DescriptionEntry(line, 8));
}
}
for (String line : textRenderer.split(updateMessage.getFormattedString(), wrapWidth - 16)) {
if (downloadLink != null) {
children().add(new LinkEntry(line, downloadLink, 8));
} else {
children().add(new DescriptionEntry(line, 8));
}
}
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.terraformersmc.modmenu.util;

import org.jetbrains.annotations.NotNull;

import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

public class UpdateCheckerThreadFactory implements ThreadFactory {
static final AtomicInteger COUNT = new AtomicInteger(-1);

@Override
public Thread newThread(@NotNull Runnable runnable) {
int index = COUNT.incrementAndGet();
return new Thread(runnable, "ModMenu/Update Checker/" + index);
}
}
219 changes: 181 additions & 38 deletions src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,18 @@
import org.apache.http.util.EntityUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.net.URI;
import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class UpdateCheckerUtil {
public static final Logger LOGGER = LogManager.getLogger("Mod Menu/Update Checker");
Expand All @@ -40,58 +47,173 @@ public static void checkForUpdates() {
}

LOGGER.info("Checking mod updates...");

CompletableFuture.runAsync(UpdateCheckerUtil::checkForModrinthUpdates);
checkForCustomUpdates();
CompletableFuture.runAsync(UpdateCheckerUtil::checkForUpdates0);
}

public static void checkForCustomUpdates() {
private static void checkForUpdates0() {
ExecutorService executor = Executors.newCachedThreadPool(new UpdateCheckerThreadFactory());
List<Mod> withoutUpdateChecker = new ArrayList<>();

ModMenu.MODS.values().stream().filter(UpdateCheckerUtil::allowsUpdateChecks).forEach(mod -> {
UpdateChecker updateChecker = mod.getUpdateChecker();

if (updateChecker == null) {
return;
}
withoutUpdateChecker.add(mod); // Fall back to update checking via Modrinth
} else {
executor.submit(() -> {
// We don't know which mod the thread is for yet in the thread factory
Thread.currentThread().setName("ModMenu/Update Checker/" + mod.getName());

UpdateCheckerThread.run(mod, () -> {
UpdateInfo update = updateChecker.checkForUpdates();
UpdateInfo update = updateChecker.checkForUpdates();

if (update == null) {
return;
}
if (update == null) {
return;
}

mod.setUpdateInfo(update);
LOGGER.info("Update available for '{}@{}'", mod.getId(), mod.getVersion());
});
mod.setUpdateInfo(update);
LOGGER.info("Update available for '{}@{}'", mod.getId(), mod.getVersion());
});
}
});
}

public static void checkForModrinthUpdates() {
if (modrinthApiV2Deprecated) {
return;
}

Map<String, Set<Mod>> modHashes = new HashMap<>();
new ArrayList<>(ModMenu.MODS.values()).stream().filter(UpdateCheckerUtil::allowsUpdateChecks).filter(mod -> mod.getUpdateChecker() == null).forEach(mod -> {
Map<String, Set<Mod>> modHashes = getModHashes(withoutUpdateChecker);

Future<Map<String, Instant>> currentVersionsFuture = executor.submit(() -> getCurrentVersions(modHashes.keySet()));
Future<Map<String, VersionUpdate>> updatedVersionsFuture = executor.submit(() -> getUpdatedVersions(modHashes.keySet()));

Map<String, Instant> currentVersions = null;
Map<String, VersionUpdate> updatedVersions = null;

try {
currentVersions = currentVersionsFuture.get();
updatedVersions = updatedVersionsFuture.get();
} catch (ExecutionException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}

if (currentVersions == null || updatedVersions == null) {
return;
}

for (String hash : modHashes.keySet()) {
Instant date = currentVersions.get(hash);
VersionUpdate data = updatedVersions.get(hash);

if (date == null || data == null) {
continue;
}

// Current version is still the newest
if (Objects.equals(hash, data.hash)) {
continue;
}

// Current version is newer than what's
// Available on our preferred update channel
if (date.compareTo(data.releaseDate) >= 0) {
continue;
}

for (Mod mod : modHashes.get(hash)) {
mod.setUpdateInfo(data.asUpdateInfo());
LOGGER.info("Update available for '{}@{}', (-> {})", mod.getId(), mod.getVersion(), data.versionNumber);
}
}
}

private static Map<String, Set<Mod>> getModHashes(Collection<Mod> mods) {
Map<String, Set<Mod>> results = new HashMap<>();

for (Mod mod : mods) {
String modId = mod.getId();

try {
String hash = mod.getSha512Hash();

if (hash != null) {
LOGGER.debug("Hash for {} is {}", modId, hash);
modHashes.putIfAbsent(hash, new HashSet<>());
modHashes.get(hash).add(mod);
results.putIfAbsent(hash, new HashSet<>());
results.get(hash).add(mod);
}
} catch (IOException e) {
LOGGER.error("Error getting mod hash for mod {}: ", modId, e);
}
});
}

List<String> loaders = ModMenu.runningQuilt ? Arrays.asList("fabric", "quilt") : Arrays.asList("fabric");
return results;
}

/**
* @return a map of file hash to its release date on Modrinth.
*/
private static @Nullable Map<String, Instant> getCurrentVersions(Collection<String> modHashes) {
String body = ModMenu.GSON_MINIFIED.toJson(new CurrentVersionsFromHashes(modHashes));

try {
RequestBuilder request = RequestBuilder.post()
.setEntity(new StringEntity(body))
.addHeader("Content-Type", "application/json")
.setUri(URI.create("https://api.modrinth.com/v2/version_files"));

HttpResponse response = HttpUtil.request(request);
int status = response.getStatusLine().getStatusCode();

if (status == 410) {
modrinthApiV2Deprecated = true;
LOGGER.warn("Modrinth API v2 is deprecated, unable to check for mod updates.");
} else if (status == 200) {
Map<String, Instant> results = new HashMap<>();
JsonObject data = new JsonParser().parse(EntityUtils.toString(response.getEntity())).getAsJsonObject();

data.entrySet().forEach((Map.Entry<String, JsonElement> entry) -> {
Instant date;
JsonObject version = entry.getValue().getAsJsonObject();

try {
date = Instant.parse(version.get("date_published").getAsString());
} catch (DateTimeParseException e) {
return;
}

results.put(entry.getKey(), date);
});

return results;
}
} catch (IOException e) {
LOGGER.error("Error checking for versions: ", e);
}

return null;
}

public static class CurrentVersionsFromHashes {
public Collection<String> hashes;
public String algorithm = "sha512";

public CurrentVersionsFromHashes(Collection<String> hashes) {
this.hashes = hashes;
}
}

private static UpdateChannel getUpdateChannel(String versionType) {
try {
return UpdateChannel.valueOf(versionType.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException | NullPointerException e) {
return UpdateChannel.RELEASE;
}
}

private static @Nullable Map<String, VersionUpdate> getUpdatedVersions(Collection<String> modHashes) {
String mcVer = FabricLoader.getInstance().getModContainer("minecraft").get()
.getMetadata().getVersion().getFriendlyString();
List<String> loaders = ModMenu.runningQuilt ? Arrays.asList("fabric", "quilt") : Arrays.asList("fabric");

List<UpdateChannel> updateChannels;
UpdateChannel preferredChannel = UpdateChannel.getUserPreference();
Expand All @@ -104,7 +226,7 @@ public static void checkForModrinthUpdates() {
updateChannels = Arrays.asList(UpdateChannel.ALPHA, UpdateChannel.BETA, UpdateChannel.RELEASE);
}

String body = ModMenu.GSON_MINIFIED.toJson(new LatestVersionsFromHashesBody(modHashes.keySet(), loaders, mcVer, updateChannels));
String body = ModMenu.GSON_MINIFIED.toJson(new LatestVersionsFromHashesBody(modHashes, loaders, mcVer, updateChannels));

LOGGER.debug("Body: " + body);

Expand All @@ -114,15 +236,16 @@ public static void checkForModrinthUpdates() {
.addHeader("Content-Type", "application/json")
.setUri(URI.create("https://api.modrinth.com/v2/version_files/update"));

HttpResponse latestVersionsResponse = HttpUtil.request(latestVersionsRequest);
HttpResponse response = HttpUtil.request(latestVersionsRequest);

int status = latestVersionsResponse.getStatusLine().getStatusCode();
int status = response.getStatusLine().getStatusCode();
LOGGER.debug("Status: " + status);
if (status == 410) {
modrinthApiV2Deprecated = true;
LOGGER.warn("Modrinth API v2 is deprecated, unable to check for mod updates.");
} else if (status == 200) {
JsonObject responseObject = new JsonParser().parse(EntityUtils.toString(latestVersionsResponse.getEntity())).getAsJsonObject();
Map<String, VersionUpdate> results = new HashMap<>();
JsonObject responseObject = new JsonParser().parse(EntityUtils.toString(response.getEntity())).getAsJsonObject();
LOGGER.debug(String.valueOf(responseObject));
responseObject.entrySet().forEach(entry -> {
String lookupHash = entry.getKey();
Expand All @@ -140,28 +263,48 @@ public static void checkForModrinthUpdates() {
return;
}

Instant date;

try {
date = Instant.parse(versionObj.get("date_published").getAsString());
} catch (DateTimeParseException e) {
return;
}

UpdateChannel updateChannel = UpdateCheckerUtil.getUpdateChannel(versionType);
String versionHash = primaryFile.get().getAsJsonObject().get("hashes").getAsJsonObject().get("sha512").getAsString();

if (!Objects.equals(versionHash, lookupHash)) {
// hashes different, there's an update.
modHashes.get(lookupHash).forEach(mod -> {
LOGGER.info("Update available for '{}@{}', (-> {})", mod.getId(), mod.getVersion(), versionNumber);
mod.setUpdateInfo(new ModrinthUpdateInfo(projectId, versionId, versionNumber, updateChannel));
});
}
results.put(lookupHash, new VersionUpdate(projectId, versionId, versionNumber, date, updateChannel, versionHash));
});

return results;
}
} catch (IOException e) {
LOGGER.error("Error checking for updates: ", e);
}

return null;
}

private static UpdateChannel getUpdateChannel(String versionType) {
try {
return UpdateChannel.valueOf(versionType.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException | NullPointerException e) {
return UpdateChannel.RELEASE;
private static class VersionUpdate {
String projectId;
String versionId;
String versionNumber;
Instant releaseDate;
UpdateChannel updateChannel;
String hash;

public VersionUpdate(String projectId, String versionId, String versionNumber, Instant releaseDate, UpdateChannel updateChannel, String has) {
this.projectId = projectId;
this.versionId = versionId;
this.versionNumber = versionNumber;
this.releaseDate = releaseDate;
this.updateChannel = updateChannel;
this.hash = has;
}

private UpdateInfo asUpdateInfo() {
return new ModrinthUpdateInfo(this.projectId, this.versionId, this.versionNumber, this.updateChannel);
}
}

Expand Down
Loading

0 comments on commit 7c4ac5f

Please sign in to comment.