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 authored Jun 15, 2024
1 parent 61cc949 commit 3b48ad5
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 106 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ public class DescriptionListWidget extends EntryListWidget<DescriptionListWidget

private static final Text HAS_UPDATE_TEXT = Text.translatable("modmenu.hasUpdate");
private static final Text EXPERIMENTAL_TEXT = Text.translatable("modmenu.experimental").formatted(Formatting.GOLD);
private static final Text MODRINTH_TEXT = Text.translatable("modmenu.modrinth");
private static final Text DOWNLOAD_TEXT = Text.translatable("modmenu.downloadLink").formatted(Formatting.BLUE).formatted(Formatting.UNDERLINE);
private static final Text CHILD_HAS_UPDATE_TEXT = Text.translatable("modmenu.childHasUpdate");
private static final Text LINKS_TEXT = Text.translatable("modmenu.links");
Expand Down Expand Up @@ -114,30 +113,20 @@ public void renderList(DrawContext DrawContext, int mouseX, int mouseY, float de
children().add(new DescriptionEntry(line, 8));
}

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

for (OrderedText line : textRenderer.wrapLines(updateText, 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().formatted(Formatting.BLUE).formatted(Formatting.UNDERLINE);
}
if (downloadLink != null) {
updateMessage = updateMessage.copy().formatted(Formatting.BLUE).formatted(Formatting.UNDERLINE);
}
for (OrderedText line : textRenderer.wrapLines(updateMessage, wrapWidth - 16)) {
if (downloadLink != null) {
children().add(new LinkEntry(line, downloadLink, 8));
} else {
children().add(new DescriptionEntry(line, 8));
}
}
for (OrderedText line : textRenderer.wrapLines(updateMessage, 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 r) {
var index = COUNT.incrementAndGet();
return Thread.ofVirtual().name("ModMenu/Update Checker/%s".formatted(index)).unstarted(r);
}
}
228 changes: 181 additions & 47 deletions src/main/java/com/terraformersmc/modmenu/util/UpdateCheckerUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,29 @@
import com.terraformersmc.modmenu.ModMenu;
import com.terraformersmc.modmenu.api.UpdateChannel;
import com.terraformersmc.modmenu.api.UpdateChecker;
import com.terraformersmc.modmenu.api.UpdateInfo;
import com.terraformersmc.modmenu.config.ModMenuConfig;
import com.terraformersmc.modmenu.util.mod.Mod;
import com.terraformersmc.modmenu.util.mod.ModrinthUpdateInfo;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.SharedConstants;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.toast.SystemToast;
import net.minecraft.text.Text;
import net.minecraft.util.Util;

import net.minecraft.util.Util;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;

public class UpdateCheckerUtil {
public static final Logger LOGGER = LoggerFactory.getLogger("Mod Menu/Update Checker");
Expand All @@ -40,53 +45,180 @@ public static void checkForUpdates() {
}

LOGGER.info("Checking mod updates...");
Util.getMainWorkerExecutor().execute(UpdateCheckerUtil::checkForModrinthUpdates);
checkForCustomUpdates();
Util.getMainWorkerExecutor().execute(UpdateCheckerUtil::checkForUpdates0);
}

public static void checkForCustomUpdates() {
ModMenu.MODS.values().stream().filter(UpdateCheckerUtil::allowsUpdateChecks).forEach(mod -> {
UpdateChecker updateChecker = mod.getUpdateChecker();
private static void checkForUpdates0() {
try (var executor = Executors.newThreadPerTaskExecutor(new UpdateCheckerThreadFactory())) {
List<Mod> withoutUpdateChecker = new ArrayList<>();

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

if (updateChecker == null) {
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/%s".formatted(mod.getName()));

var update = updateChecker.checkForUpdates();

if (update == null) {
return;
}

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

if (modrinthApiV2Deprecated) {
return;
}

var modHashes = getModHashes(withoutUpdateChecker);

var currentVersionsFuture = executor.submit(() -> getCurrentVersions(modHashes.keySet()));
var 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 (updateChecker == null) {
if (currentVersions == null || updatedVersions == null) {
return;
}

UpdateCheckerThread.run(mod, () -> {
var update = updateChecker.checkForUpdates();
for (var hash : modHashes.keySet()) {
var date = currentVersions.get(hash);
var data = updatedVersions.get(hash);

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

mod.setUpdateInfo(update);
LOGGER.info("Update available for '{}@{}'", mod.getId(), mod.getVersion());
});
});
}
// Current version is still the newest
if (Objects.equals(hash, data.hash)) {
continue;
}

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

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

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

for (var 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);
}
});
};

return results;
}

public static void triggerV2DeprecatedToast() {
if (modrinthApiV2Deprecated && ModMenuConfig.UPDATE_CHECKER.getValue()) {
MinecraftClient.getInstance().getToastManager().add(new SystemToast(
SystemToast.Type.PERIODIC_NOTIFICATION,
Text.translatable("modmenu.modrinth.v2_deprecated.title"),
Text.translatable("modmenu.modrinth.v2_deprecated.description")
));
}
}

/**
* @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));

var request = HttpRequest.newBuilder()
.POST(HttpRequest.BodyPublishers.ofString(body))
.header("Content-Type", "application/json")
.uri(URI.create("https://api.modrinth.com/v2/version_files"));

try {
var response = HttpUtil.request(request, HttpResponse.BodyHandlers.ofString());

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

data.asMap().forEach((hash, inner) -> {
Instant date;
var version = inner.getAsJsonObject();

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

results.put(hash, date);
});

return results;
}
} catch (IOException | InterruptedException 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 = SharedConstants.getGameVersion().getName();
List<String> loaders = ModMenu.runningQuilt ? List.of("fabric", "quilt") : List.of("fabric");

Expand All @@ -101,7 +233,7 @@ public static void checkForModrinthUpdates() {
updateChannels = List.of(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);
var latestVersionsRequest = HttpRequest.newBuilder()
Expand All @@ -118,6 +250,7 @@ public static void checkForModrinthUpdates() {
modrinthApiV2Deprecated = true;
LOGGER.warn("Modrinth API v2 is deprecated, unable to check for mod updates.");
} else if (status == 200) {
Map<String, VersionUpdate> results = new HashMap<>();
JsonObject responseObject = JsonParser.parseString(latestVersionsResponse.body()).getAsJsonObject();
LOGGER.debug(String.valueOf(responseObject));
responseObject.asMap().forEach((lookupHash, versionJson) -> {
Expand All @@ -133,38 +266,39 @@ public static void checkForModrinthUpdates() {
return;
}

Instant date;

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

var updateChannel = UpdateCheckerUtil.getUpdateChannel(versionType);
var 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 | InterruptedException e) {
LOGGER.error("Error checking for updates: ", e);
}
}

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

public static void triggerV2DeprecatedToast() {
if (modrinthApiV2Deprecated && ModMenuConfig.UPDATE_CHECKER.getValue()) {
MinecraftClient.getInstance().getToastManager().add(new SystemToast(
SystemToast.Type.PERIODIC_NOTIFICATION,
Text.translatable("modmenu.modrinth.v2_deprecated.title"),
Text.translatable("modmenu.modrinth.v2_deprecated.description")
));
private record VersionUpdate(
String projectId,
String versionId,
String versionNumber,
Instant releaseDate,
UpdateChannel updateChannel,
String hash
) {
private UpdateInfo asUpdateInfo() {
return new ModrinthUpdateInfo(this.projectId, this.versionId, this.versionNumber, this.updateChannel);
}
}

Expand Down
Loading

0 comments on commit 3b48ad5

Please sign in to comment.