add: redownload corrupt files when game crashed

This commit is contained in:
huanghongxun
2020-02-20 00:02:56 +08:00
parent cd9180c69a
commit 91682aeafa
16 changed files with 122 additions and 76 deletions

View File

@@ -28,6 +28,7 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.*;
import java.util.logging.Level;
@@ -221,6 +222,20 @@ public class HMCLGameRepository extends DefaultGameRepository {
beingModpackVersions.remove(id);
}
public void markVersionLaunchedAbnormally(String id) {
try {
Files.createFile(getVersionRoot(id).toPath().resolve(".abnormal"));
} catch (IOException ignored) {
}
}
public boolean unmarkVersionLaunchedAbnormally(String id) {
File file = new File(getVersionRoot(id), ".abnormal");
boolean result = file.isFile();
file.delete();
return result;
}
private static final Gson GSON = new GsonBuilder()
.setPrettyPrinting()
.create();

View File

@@ -135,7 +135,7 @@ public final class LauncherHelper {
if (setting.isNotCheckGame())
return null;
else
return dependencyManager.checkGameCompletionAsync(version);
return dependencyManager.checkGameCompletionAsync(version, repository.unmarkVersionLaunchedAbnormally(selectedVersion));
}), Task.composeAsync(null, () -> {
try {
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
@@ -168,7 +168,7 @@ public final class LauncherHelper {
setting.toLaunchOptions(profile.getGameDir(), !setting.isNotCheckJVM()),
launcherVisibility == LauncherVisibility.CLOSE
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
: new HMCLProcessListener(authInfo, gameVersion.isPresent())
: new HMCLProcessListener(repository, selectedVersion, authInfo, gameVersion.isPresent())
);
}).thenComposeAsync(launcher -> { // launcher is prev task's result
if (scriptFile == null) {
@@ -209,12 +209,12 @@ public final class LauncherHelper {
@Override
public void onStop(boolean success, TaskExecutor executor) {
if (!success && !Controllers.isStopped()) {
Platform.runLater(() -> {
// Check if the application has stopped
// because onStop will be invoked if tasks fail when the executor service shut down.
if (!Controllers.isStopped()) {
launchingStepsPane.fireEvent(new DialogCloseEvent());
if (!success) {
Exception ex = executor.getException();
if (ex != null) {
String message;
@@ -264,9 +264,9 @@ public final class LauncherHelper {
MessageType.ERROR);
}
}
});
}
launchingStepsPane.setExecutor(null);
});
}
});
@@ -444,6 +444,8 @@ public final class LauncherHelper {
*/
class HMCLProcessListener implements ProcessListener {
private final HMCLGameRepository repository;
private final String version;
private final Map<String, String> forbiddenTokens;
private ManagedProcess process;
private boolean lwjgl;
@@ -452,7 +454,9 @@ public final class LauncherHelper {
private final LinkedList<Pair<String, Log4jLevel>> logs;
private final CountDownLatch latch = new CountDownLatch(1);
public HMCLProcessListener(AuthInfo authInfo, boolean detectWindow) {
public HMCLProcessListener(HMCLGameRepository repository, String version, AuthInfo authInfo, boolean detectWindow) {
this.repository = repository;
this.version = version;
this.detectWindow = detectWindow;
if (authInfo == null)
@@ -559,6 +563,12 @@ public final class LauncherHelper {
// Game crashed before opening the game window.
if (!lwjgl) finishLaunch();
launchingLatch.countDown();
if (exitType != ExitType.NORMAL) {
repository.markVersionLaunchedAbnormally(version);
}
if (exitType != ExitType.NORMAL && logWindow == null)
Platform.runLater(() -> {
logWindow = new LogWindow();

View File

@@ -132,9 +132,9 @@ public final class TaskListPane extends StackPane {
task.setName(i18n("modpack.scan"));
}
Platform.runLater(() -> {
ProgressListNode node = new ProgressListNode(task);
nodes.put(task, node);
Platform.runLater(() -> {
StageNode stageNode = stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().orElse(null);
listBox.add(listBox.indexOf(stageNode) + 1, node);
});
@@ -148,11 +148,11 @@ public final class TaskListPane extends StackPane {
});
}
Platform.runLater(() -> {
ProgressListNode node = nodes.remove(task);
if (node == null)
return;
node.unbind();
Platform.runLater(() -> {
listBox.remove(node);
});
}

View File

@@ -76,7 +76,7 @@ public class Versions {
}
public static void updateGameAssets(Profile profile, String version) {
TaskExecutor executor = new GameAssetDownloadTask(profile.getDependency(), profile.getRepository().getVersion(version), GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY)
TaskExecutor executor = new GameAssetDownloadTask(profile.getDependency(), profile.getRepository().getVersion(version), GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY, true)
.executor();
Controllers.taskDialog(executor, i18n("version.manage.redownload_assets_index"));
executor.start();

View File

@@ -25,7 +25,6 @@ import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
import java.io.IOException;
import java.nio.file.Path;
@@ -76,7 +75,7 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
}
@Override
public Task<?> checkGameCompletionAsync(Version original) {
public Task<?> checkGameCompletionAsync(Version original, boolean integrityCheck) {
Version version = original.resolve(repository);
return Task.allOf(
Task.composeAsync(() -> {
@@ -85,14 +84,14 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
else
return null;
}),
new GameAssetDownloadTask(this, version, GameAssetDownloadTask.DOWNLOAD_INDEX_IF_NECESSARY),
new GameLibrariesTask(this, version)
new GameAssetDownloadTask(this, version, GameAssetDownloadTask.DOWNLOAD_INDEX_IF_NECESSARY, integrityCheck),
new GameLibrariesTask(this, version, integrityCheck)
);
}
@Override
public Task<?> checkLibraryCompletionAsync(Version version) {
return new GameLibrariesTask(this, version, version.getLibraries());
public Task<?> checkLibraryCompletionAsync(Version version, boolean integrityCheck) {
return new GameLibrariesTask(this, version, integrityCheck, version.getLibraries());
}
@Override

View File

@@ -46,7 +46,7 @@ public interface DependencyManager {
*
* @return the task to check game completion.
*/
Task<?> checkGameCompletionAsync(Version version);
Task<?> checkGameCompletionAsync(Version version, boolean integrityCheck);
/**
* Check if the game is complete.
@@ -54,7 +54,7 @@ public interface DependencyManager {
*
* @return the task to check game completion.
*/
Task<?> checkLibraryCompletionAsync(Version version);
Task<?> checkLibraryCompletionAsync(Version version, boolean integrityCheck);
/**
* The builder to build a brand new game then libraries such as Forge, LiteLoader and OptiFine.

View File

@@ -84,7 +84,7 @@ public final class FabricInstallTask extends Task<Version> {
public void execute() {
setResult(getPatch(JsonUtils.GSON.fromJson(launchMetaTask.getResult(), FabricInfo.class), remote.getGameVersion(), remote.getSelfVersion()));
dependencies.add(dependencyManager.checkLibraryCompletionAsync(getResult()));
dependencies.add(dependencyManager.checkLibraryCompletionAsync(getResult(), true));
}
private static String getLaunchMetaUrl(String gameVersion, String loaderVersion) {

View File

@@ -45,14 +45,7 @@ import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.*;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.zip.ZipException;
@@ -133,7 +126,7 @@ public class ForgeNewInstallTask extends Task<Version> {
}
}
dependents.add(new GameLibrariesTask(dependencyManager, version, profile.getLibraries()));
dependents.add(new GameLibrariesTask(dependencyManager, version, true, profile.getLibraries()));
}
@Override
@@ -273,7 +266,7 @@ public class ForgeNewInstallTask extends Task<Version> {
.setPriority(30000)
.setId(LibraryAnalyzer.LibraryType.FORGE.getPatchId())
.setVersion(selfVersion));
dependencies.add(dependencyManager.checkLibraryCompletionAsync(forgeVersion));
dependencies.add(dependencyManager.checkLibraryCompletionAsync(forgeVersion, true));
FileUtils.deleteDirectory(temp.toFile());
}

View File

@@ -27,11 +27,7 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.*;
import java.nio.file.Path;
import java.util.LinkedList;
import java.util.List;
@@ -90,7 +86,7 @@ public class ForgeOldInstallTask extends Task<Version> {
.setPriority(30000)
.setId(LibraryAnalyzer.LibraryType.FORGE.getPatchId())
.setVersion(selfVersion));
dependencies.add(dependencyManager.checkLibraryCompletionAsync(installProfile.getVersionInfo()));
dependencies.add(dependencyManager.checkLibraryCompletionAsync(installProfile.getVersionInfo(), true));
} catch (ZipException ex) {
throw new ArtifactMalformedException("Malformed forge installer file", ex);
}

View File

@@ -27,6 +27,7 @@ import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
@@ -37,6 +38,7 @@ import java.net.URL;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.stream.Collectors;
/**
@@ -49,6 +51,7 @@ public final class GameAssetDownloadTask extends Task<Void> {
private final Version version;
private final AssetIndexInfo assetIndexInfo;
private final File assetIndexFile;
private final boolean integrityCheck;
private final List<Task<?>> dependents = new LinkedList<>();
private final List<Task<?>> dependencies = new LinkedList<>();
@@ -58,11 +61,12 @@ public final class GameAssetDownloadTask extends Task<Void> {
* @param dependencyManager the dependency manager that can provides {@link org.jackhuang.hmcl.game.GameRepository}
* @param version the game version
*/
public GameAssetDownloadTask(AbstractDependencyManager dependencyManager, Version version, boolean forceDownloadingIndex) {
public GameAssetDownloadTask(AbstractDependencyManager dependencyManager, Version version, boolean forceDownloadingIndex, boolean integrityCheck) {
this.dependencyManager = dependencyManager;
this.version = version.resolve(dependencyManager.getGameRepository());
this.assetIndexInfo = this.version.getAssetIndex();
this.assetIndexFile = dependencyManager.getGameRepository().getIndexFile(version.getId(), assetIndexInfo.getId());
this.integrityCheck = integrityCheck;
if (!assetIndexFile.exists() || forceDownloadingIndex) {
dependents.add(new GameAssetIndexDownloadTask(dependencyManager, this.version));
@@ -101,9 +105,14 @@ public final class GameAssetDownloadTask extends Task<Void> {
throw new InterruptedException();
File file = dependencyManager.getGameRepository().getAssetObject(version.getId(), assetIndexInfo.getId(), assetObject);
if (file.isFile())
dependencyManager.getCacheRepository().tryCacheFile(file.toPath(), CacheRepository.SHA1, assetObject.getHash());
else {
boolean download = !file.isFile();
try {
if (!download && integrityCheck && !assetObject.validateChecksum(file.toPath(), true))
download = true;
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Unable to calc hash value of file " + file.toPath(), e);
}
if (download) {
List<URL> urls = dependencyManager.getPreferredDownloadProviders().stream()
.map(downloadProvider -> downloadProvider.getAssetBaseURL() + assetObject.getLocation())
.map(NetworkUtils::toURL)
@@ -116,6 +125,8 @@ public final class GameAssetDownloadTask extends Task<Void> {
.setCaching(true)
.setCandidate(dependencyManager.getCacheRepository().getCommonDirectory()
.resolve("assets").resolve("objects").resolve(assetObject.getLocation())));
} else {
dependencyManager.getCacheRepository().tryCacheFile(file.toPath(), CacheRepository.SHA1, assetObject.getHash());
}
updateProgress(++progress, index.getObjects().size());

View File

@@ -71,8 +71,8 @@ public class GameInstallTask extends Task<Version> {
Version version = new Version(this.version.getId()).addPatch(patch);
dependencies.add(new GameDownloadTask(dependencyManager, remote.getGameVersion(), version)
.thenComposeAsync(Task.allOf(
new GameAssetDownloadTask(dependencyManager, version, GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY),
new GameLibrariesTask(dependencyManager, version)
new GameAssetDownloadTask(dependencyManager, version, GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY, true),
new GameLibrariesTask(dependencyManager, version, true)
).withStage("hmcl.install.assets").withComposeAsync(gameRepository.save(version))));
}

View File

@@ -21,10 +21,14 @@ import org.jackhuang.hmcl.download.AbstractDependencyManager;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Logging;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
/**
* This task is to download game libraries.
@@ -36,6 +40,7 @@ public final class GameLibrariesTask extends Task<Void> {
private final AbstractDependencyManager dependencyManager;
private final Version version;
private final boolean integrityCheck;
private final List<Library> libraries;
private final List<Task<?>> dependencies = new LinkedList<>();
@@ -45,8 +50,8 @@ public final class GameLibrariesTask extends Task<Void> {
* @param dependencyManager the dependency manager that can provides {@link org.jackhuang.hmcl.game.GameRepository}
* @param version the game version
*/
public GameLibrariesTask(AbstractDependencyManager dependencyManager, Version version) {
this(dependencyManager, version, version.resolve(dependencyManager.getGameRepository()).getLibraries());
public GameLibrariesTask(AbstractDependencyManager dependencyManager, Version version, boolean integrityCheck) {
this(dependencyManager, version, integrityCheck, version.resolve(dependencyManager.getGameRepository()).getLibraries());
}
/**
@@ -55,9 +60,10 @@ public final class GameLibrariesTask extends Task<Void> {
* @param dependencyManager the dependency manager that can provides {@link org.jackhuang.hmcl.game.GameRepository}
* @param version the game version
*/
public GameLibrariesTask(AbstractDependencyManager dependencyManager, Version version, List<Library> libraries) {
public GameLibrariesTask(AbstractDependencyManager dependencyManager, Version version, boolean integrityCheck, List<Library> libraries) {
this.dependencyManager = dependencyManager;
this.version = version;
this.integrityCheck = integrityCheck;
this.libraries = libraries;
setSignificance(TaskSignificance.MODERATE);
@@ -72,10 +78,22 @@ public final class GameLibrariesTask extends Task<Void> {
public void execute() {
libraries.stream().filter(Library::appliesToCurrentEnvironment).forEach(library -> {
File file = dependencyManager.getGameRepository().getLibraryFile(version, library);
if (!file.exists())
Path jar = file.toPath();
boolean download = !file.isFile();
try {
if (!download && integrityCheck && !library.getDownload().validateChecksum(jar, true)) download = true;
if (!download && integrityCheck &&
library.getChecksums() != null && !library.getChecksums().isEmpty() &&
!LibraryDownloadTask.checksumValid(jar.toFile(), library.getChecksums())) download = true;
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Unable to calc hash value of file " + jar, e);
}
if (download) {
dependencies.add(new LibraryDownloadTask(dependencyManager, file, library));
else
} else {
dependencyManager.getCacheRepository().tryCacheLibrary(library, file.toPath());
}
});
}

View File

@@ -19,12 +19,7 @@ package org.jackhuang.hmcl.download.liteloader;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.game.Artifact;
import org.jackhuang.hmcl.game.LibrariesDownloadInfo;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.LibraryDownloadInfo;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Lang;
@@ -79,7 +74,7 @@ public final class LiteLoaderInstallTask extends Task<Version> {
.setLogging(Collections.emptyMap()) // Mods may log in malformed format, causing XML parser to crash. So we suppress using official log4j configuration
);
dependencies.add(dependencyManager.checkLibraryCompletionAsync(getResult()));
dependencies.add(dependencyManager.checkLibraryCompletionAsync(getResult(), true));
}
}

View File

@@ -20,14 +20,7 @@ package org.jackhuang.hmcl.download.optifine;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.VersionMismatchException;
import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.game.Artifact;
import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.game.LibrariesDownloadInfo;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.LibraryDownloadInfo;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.io.CompressingUtils;
@@ -184,7 +177,7 @@ public final class OptiFineInstallTask extends Task<Version> {
libraries
));
dependencies.add(dependencyManager.checkLibraryCompletionAsync(getResult()));
dependencies.add(dependencyManager.checkLibraryCompletionAsync(getResult(), true));
}
public static class UnsupportedOptiFineInstallationException extends Exception {

View File

@@ -18,9 +18,14 @@
package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.Hex;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.Validation;
import java.io.IOException;
import java.nio.file.Path;
/**
*
* @author huangyuhui
@@ -56,4 +61,9 @@ public final class AssetObject implements Validation {
if (StringUtils.isBlank(hash) || hash.length() < 2)
throw new JsonParseException("AssetObject hash cannot be blank.");
}
public boolean validateChecksum(Path file, boolean defaultValue) throws IOException {
if (hash == null) return defaultValue;
return Hex.encodeHex(DigestUtils.digest("SHA-1", file)).equalsIgnoreCase(hash);
}
}

View File

@@ -19,12 +19,13 @@ package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation;
import java.io.IOException;
import java.nio.file.Path;
/**
*
* @author huangyuhui
@@ -79,4 +80,9 @@ public class DownloadInfo implements Validation {
if (StringUtils.isBlank(url))
throw new TolerableValidationException();
}
public boolean validateChecksum(Path file, boolean defaultValue) throws IOException {
if (getSha1() == null) return defaultValue;
return Hex.encodeHex(DigestUtils.digest("SHA-1", file)).equalsIgnoreCase(getSha1());
}
}