diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java index 8daf0f419..641bed514 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java @@ -41,6 +41,7 @@ import net.kdt.pojavlaunch.services.ProgressServiceKeeper; import net.kdt.pojavlaunch.tasks.AsyncMinecraftDownloader; import net.kdt.pojavlaunch.tasks.AsyncVersionList; import net.kdt.pojavlaunch.lifecycle.ContextAwareDoneListener; +import net.kdt.pojavlaunch.tasks.NewMinecraftDownloader; import net.kdt.pojavlaunch.utils.NotificationUtils; import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles; import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile; @@ -132,7 +133,7 @@ public class LauncherActivity extends BaseActivity { } String normalizedVersionId = AsyncMinecraftDownloader.normalizeVersionId(prof.lastVersionId); JMinecraftVersionList.Version mcVersion = AsyncMinecraftDownloader.getListedVersion(normalizedVersionId); - new AsyncMinecraftDownloader().start( + new NewMinecraftDownloader().start( this, mcVersion, normalizedVersionId, diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java index 90fe12a02..2fff72563 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java @@ -161,7 +161,7 @@ public final class Tools { DIR_HOME_LIBRARY = DIR_GAME_NEW + "/libraries"; DIR_HOME_CRASH = DIR_GAME_NEW + "/crash-reports"; ASSETS_PATH = DIR_GAME_NEW + "/assets"; - OBSOLETE_RESOURCES_PATH= DIR_GAME_NEW + "/resources"; + OBSOLETE_RESOURCES_PATH = DIR_GAME_NEW + "/resources"; CTRLMAP_PATH = DIR_GAME_HOME + "/controlmap"; CTRLDEF_FILE = DIR_GAME_HOME + "/controlmap/default.json"; NATIVE_LIB_DIR = ctx.getApplicationInfo().nativeLibraryDir; @@ -676,7 +676,7 @@ public final class Tools { return true; // allow if none match } - private static void preProcessLibraries(DependentLibrary[] libraries) { + public static void preProcessLibraries(DependentLibrary[] libraries) { for (int i = 0; i < libraries.length; i++) { DependentLibrary libItem = libraries[i]; String[] version = libItem.name.split(":")[2].split("\\."); @@ -856,6 +856,10 @@ public final class Tools { return read(new FileInputStream(path)); } + public static String read(File path) throws IOException { + return read(new FileInputStream(path)); + } + public static void write(String path, String content) throws IOException { File file = new File(path); FileUtils.ensureParentDirectory(file); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/mirrors/DownloadMirror.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/mirrors/DownloadMirror.java index ad95dcfff..99244da98 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/mirrors/DownloadMirror.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/mirrors/DownloadMirror.java @@ -53,6 +53,26 @@ public class DownloadMirror { DownloadUtils.downloadFileMonitored(urlInput, outputFile, buffer, monitor); } + /** + * Download a file with the current mirror. If the file is missing on the mirror, + * fall back to the official source. + * @param downloadClass Class of the download. Can either be DOWNLOAD_CLASS_LIBRARIES, + * DOWNLOAD_CLASS_METADATA or DOWNLOAD_CLASS_ASSETS + * @param urlInput The original (Mojang) URL for the download + * @param outputFile The output file for the download + */ + public static void downloadFileMirrored(int downloadClass, String urlInput, File outputFile) throws IOException { + try { + DownloadUtils.downloadFile(getMirrorMapping(downloadClass, urlInput), + outputFile); + return; + }catch (FileNotFoundException e) { + Log.w("DownloadMirror", "Cannot find the file on the mirror", e); + Log.i("DownloadMirror", "Failling back to default source"); + } + DownloadUtils.downloadFile(urlInput, outputFile); + } + /** * Check if the current download source is a mirror and not an official source. * @return true if the source is a mirror, false otherwise diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/OptiFineDownloadTask.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/OptiFineDownloadTask.java index da47e654f..762beddd5 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/OptiFineDownloadTask.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/OptiFineDownloadTask.java @@ -7,6 +7,7 @@ import net.kdt.pojavlaunch.R; import net.kdt.pojavlaunch.Tools; import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper; import net.kdt.pojavlaunch.tasks.AsyncMinecraftDownloader; +import net.kdt.pojavlaunch.tasks.NewMinecraftDownloader; import net.kdt.pojavlaunch.utils.DownloadUtils; import java.io.File; @@ -88,7 +89,7 @@ public class OptiFineDownloadTask implements Runnable, Tools.DownloaderFeedback, if(minecraftJsonVersion == null) return false; try { synchronized (mMinecraftDownloadLock) { - new AsyncMinecraftDownloader().start(null, minecraftJsonVersion, minecraftVersion, this); + new NewMinecraftDownloader().start(null, minecraftJsonVersion, minecraftVersion, this); mMinecraftDownloadLock.wait(); } }catch (InterruptedException e) { diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncMinecraftDownloader.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncMinecraftDownloader.java index dd688b69f..d197b4142 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncMinecraftDownloader.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncMinecraftDownloader.java @@ -1,396 +1,10 @@ package net.kdt.pojavlaunch.tasks; -import static net.kdt.pojavlaunch.PojavApplication.sExecutorService; -import static net.kdt.pojavlaunch.Tools.BYTE_TO_MB; -import static net.kdt.pojavlaunch.mirrors.DownloadMirror.downloadFileMirrored; - -import android.app.Activity; -import android.util.Log; - -import androidx.annotation.NonNull; - -import com.kdt.mcgui.ProgressLayout; - -import net.kdt.pojavlaunch.JAssetInfo; -import net.kdt.pojavlaunch.JAssets; import net.kdt.pojavlaunch.JMinecraftVersionList; -import net.kdt.pojavlaunch.JRE17Util; -import net.kdt.pojavlaunch.R; -import net.kdt.pojavlaunch.Tools; import net.kdt.pojavlaunch.extra.ExtraConstants; import net.kdt.pojavlaunch.extra.ExtraCore; -import net.kdt.pojavlaunch.mirrors.DownloadMirror; -import net.kdt.pojavlaunch.mirrors.MirrorTamperedException; -import net.kdt.pojavlaunch.prefs.LauncherPreferences; -import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper; -import net.kdt.pojavlaunch.value.DependentLibrary; -import net.kdt.pojavlaunch.value.MinecraftClientInfo; -import net.kdt.pojavlaunch.value.MinecraftLibraryArtifact; - -import org.apache.commons.io.IOUtils; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; public class AsyncMinecraftDownloader { - - public static final String MINECRAFT_RES = "https://resources.download.minecraft.net/"; - - /* Allows each downloading thread to have its own RECYCLED buffer */ - private final ConcurrentHashMap mThreadBuffers = new ConcurrentHashMap<>(5); - - public void start(Activity activity, JMinecraftVersionList.Version version, - String realVersion, // this was there for a reason - @NonNull DoneListener listener) { - sExecutorService.execute(() -> { - try { - downloadGame(activity, version, realVersion); - listener.onDownloadDone(); - }catch (DownloaderException e) { - listener.onDownloadFailed(e.getCause()); - } - }); - } - /* we do the throws DownloaderException thing to avoid blanket-catching Exception as a form of anti-lazy-developer protection */ - private void downloadGame(Activity activity, JMinecraftVersionList.Version verInfo, String versionName) throws DownloaderException { - final String downVName = "/" + versionName + "/" + versionName; - - //Downloading libraries - String minecraftMainJar = Tools.DIR_HOME_VERSION + downVName + ".jar"; - JAssets assets = null; - try { - File verJsonDir = new File(Tools.DIR_HOME_VERSION + downVName + ".json"); - if (verInfo != null && verInfo.url != null) { - downloadVersionJson(versionName, verJsonDir, verInfo); - } - JMinecraftVersionList.Version originalVersion = Tools.getVersionInfo(versionName, true); - if(Tools.isValidString(originalVersion.inheritsFrom)) { - Log.i("Downloader", "probe: inheritsFrom="+originalVersion.inheritsFrom); - String version = originalVersion.inheritsFrom; - String downName = Tools.DIR_HOME_VERSION+"/"+version+"/"+version+".json"; - JMinecraftVersionList.Version listedVersion = getListedVersion(originalVersion.inheritsFrom); - if(listedVersion != null) { - Log.i("Downloader", "probe: verifying "+version); - downloadVersionJson(version, new File(downName), listedVersion); - }else{ - Log.i("Downloader", "failed to test source version before downloading."); - Log.i("Downloader", "Inheriting from versions not in the Mojang list?"); - Log.i("Downloader", "If so, feel free to open a PR at our GitHub repository to add this feature!"); - } - } - - verInfo = Tools.getVersionInfo(versionName); - - // THIS one function need the activity in the case of an error - if(activity != null && !JRE17Util.installNewJreIfNeeded(activity, verInfo)){ - ProgressLayout.clearProgress(ProgressLayout.DOWNLOAD_MINECRAFT); - throw new DownloaderException(new Exception(activity.getString(R.string.exception_failed_to_unpack_jre17))); - } - - try { - if(verInfo.assets != null) - assets = downloadIndex(verInfo, new File(Tools.ASSETS_PATH, "indexes/" + verInfo.assets + ".json")); - } catch (IOException e) { - Log.e("AsyncMcDownloader", e.toString(), e); - throw new DownloaderException(e); - } - - File outLib; - - // Patch the Log4J RCE (CVE-2021-44228) - if (verInfo.logging != null) { - outLib = new File(Tools.DIR_DATA + "/security", verInfo.logging.client.file.id.replace("client", "log4j-rce-patch")); - boolean useLocal = outLib.exists(); - if (!useLocal) { - outLib = new File(Tools.DIR_GAME_NEW, verInfo.logging.client.file.id); - } - if (outLib.exists() && !useLocal) { - if(LauncherPreferences.PREF_CHECK_LIBRARY_SHA) { - if(!Tools.compareSHA1(outLib,verInfo.logging.client.file.sha1)) { - outLib.delete(); - ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, 0, R.string.dl_library_sha_fail,verInfo.logging.client.file.id); - }else{ - ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, 0, R.string.dl_library_sha_pass,verInfo.logging.client.file.id); - } - } else if (outLib.length() != verInfo.logging.client.file.size) { - // force updating anyways - outLib.delete(); - } - } - if (!outLib.exists()) { - ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, 0, R.string.mcl_launch_downloading, verInfo.logging.client.file.id); - JMinecraftVersionList.Version finalVerInfo = verInfo; - downloadFileMirrored( - DownloadMirror.DOWNLOAD_CLASS_METADATA, - verInfo.logging.client.file.url, outLib, getByteBuffer(), - (curr, max) -> ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, - (int) Math.max((float)curr/max*100,0), R.string.mcl_launch_downloading_progress, finalVerInfo.logging.client.file.id, curr/BYTE_TO_MB, max/BYTE_TO_MB) - ); - } - } - - - for (final DependentLibrary libItem : verInfo.libraries) { - if(libItem.name.startsWith("org.lwjgl")){ //Black list - ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, 0, "Ignored " + libItem.name); - continue; - } - - String libArtifact = Tools.artifactToPath(libItem); - outLib = new File(Tools.DIR_HOME_LIBRARY + "/" + libArtifact); - outLib.getParentFile().mkdirs(); - - if (!outLib.exists()) { - downloadLibrary(libItem,libArtifact,outLib); - }else{ - if(libItem.downloads != null && libItem.downloads.artifact != null && libItem.downloads.artifact.sha1 != null && !libItem.downloads.artifact.sha1.isEmpty()) { - if(!Tools.compareSHA1(outLib,libItem.downloads.artifact.sha1)) { - outLib.delete(); - ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, 0, R.string.dl_library_sha_fail,libItem.name); - downloadLibrary(libItem,libArtifact,outLib); - }else{ - ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, 0, R.string.dl_library_sha_pass,libItem.name); - } - }else{ - ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, 0, R.string.dl_library_sha_unknown,libItem.name); - } - } - } - File minecraftMainFile = new File(minecraftMainJar); - Log.i("Downloader", "originalVersion.inheritsFrom="+originalVersion.inheritsFrom); - Log.i("Downloader", "originalVersion.downloads="+originalVersion.downloads); - MinecraftClientInfo originalClientInfo; - if(originalVersion.inheritsFrom == null) { - if (originalVersion.downloads != null && (originalClientInfo = originalVersion.downloads.get("client")) != null) { - verifyAndDownloadMainJar(originalClientInfo.url, originalClientInfo.sha1, minecraftMainFile); - } - }else if(!minecraftMainFile.exists() || minecraftMainFile.length() == 0) { - File minecraftSourceFile = new File(Tools.DIR_HOME_VERSION + "/" + verInfo.id + "/" + verInfo.id + ".jar"); - MinecraftClientInfo inheritedClientInfo; - if(verInfo.downloads != null && (inheritedClientInfo = verInfo.downloads.get("client")) != null) { - verifyAndDownloadMainJar(inheritedClientInfo.url, inheritedClientInfo.sha1, minecraftSourceFile); - } - if(minecraftSourceFile.exists()) { - FileInputStream is = new FileInputStream(minecraftSourceFile); - FileOutputStream os = new FileOutputStream(minecraftMainFile); - IOUtils.copy(is, os); - is.close(); - os.close(); - } - } - } catch (DownloaderException e) { - ProgressLayout.clearProgress(ProgressLayout.DOWNLOAD_MINECRAFT); - throw e; - } catch (Throwable e) { - Log.e("AsyncMcDownloader", e.toString(),e ); - ProgressLayout.clearProgress(ProgressLayout.DOWNLOAD_MINECRAFT); - throw new DownloaderException(e); - } - - ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, 0, R.string.mcl_launch_cleancache); - new File(Tools.DIR_HOME_VERSION).mkdir(); - for (File f : new File(Tools.DIR_HOME_VERSION).listFiles()) { - if(f.getName().endsWith(".part")) { - Log.d(Tools.APP_NAME, "Cleaning cache: " + f); - f.delete(); - } - } - - - try { - if(assets != null) - downloadAssets(assets, assets.mapToResources ? new File(Tools.OBSOLETE_RESOURCES_PATH) : new File(Tools.ASSETS_PATH)); - } catch (Exception e) { - Log.e("AsyncMcDownloader", e.toString(), e); - ProgressLayout.clearProgress(ProgressLayout.DOWNLOAD_MINECRAFT); - throw new DownloaderException(e); - } - ProgressLayout.clearProgress(ProgressLayout.DOWNLOAD_MINECRAFT); - } - - public void verifyAndDownloadMainJar(String url, String sha1, File destination) throws Exception{ - while(!destination.exists() || (destination.exists() && !Tools.compareSHA1(destination, sha1))) downloadFileMirrored(DownloadMirror.DOWNLOAD_CLASS_LIBRARIES, - url, - destination, getByteBuffer(), - (curr, max) -> ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, - (int) Math.max((float)curr/max*100,0), R.string.mcl_launch_downloading_progress, destination.getName(), curr/BYTE_TO_MB, max/BYTE_TO_MB)); - } - - public void downloadAssets(final JAssets assets, final File outputDir) { - LinkedBlockingQueue workQueue = new LinkedBlockingQueue<>(); - final ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 500, TimeUnit.MILLISECONDS, workQueue); - - - Log.i("AsyncMcDownloader","Assets begin time: " + System.currentTimeMillis()); - ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, 0, R.string.mcl_launch_download_assets); - - Map assetsObjects = assets.objects; - int assetsSizeBytes = 0; - AtomicInteger downloadedSize = new AtomicInteger(0); - AtomicBoolean localInterrupt = new AtomicBoolean(false); - - File objectsDir = new File(outputDir, "objects"); - ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, 0, R.string.mcl_launch_downloading, "assets"); - - for(String assetKey : assetsObjects.keySet()) { - JAssetInfo asset = assetsObjects.get(assetKey); - assetsSizeBytes += asset.size; - String assetPath = asset.hash.substring(0, 2) + "/" + asset.hash; - File outFile = (assets.mapToResources || assets.virtual) ? new File(outputDir,"/"+assetKey) : new File(objectsDir , assetPath); - boolean skip = outFile.exists();// skip if the file exists - - if(LauncherPreferences.PREF_CHECK_LIBRARY_SHA && skip) - skip = Tools.compareSHA1(outFile, asset.hash); - - if(skip) { - downloadedSize.addAndGet(asset.size); - continue; - } - - if(outFile.exists()) - ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, 0, R.string.dl_library_sha_fail, assetKey); - - executor.execute(()->{ - try { - downloadAssetFile(outFile, assetPath, downloadedSize); - }catch (IOException e) { - Log.e("AsyncMcManager", e.toString()); - localInterrupt.set(true); - } - }); - } - executor.shutdown(); - - try { - Log.i("AsyncMcDownloader","Queue size: " + workQueue.size()); - while ((!executor.awaitTermination(1000, TimeUnit.MILLISECONDS))&&(!localInterrupt.get()) /*&&mActivity.mIsAssetsProcessing*/) { - int DLSize = downloadedSize.get(); - ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT,(int) Math.max((float) DLSize/assetsSizeBytes*100, 0), - R.string.mcl_launch_downloading_progress, "assets", (float)DLSize/BYTE_TO_MB, (float)assetsSizeBytes/BYTE_TO_MB); - } - - - executor.shutdownNow(); - while (!executor.awaitTermination(250, TimeUnit.MILLISECONDS)) {} - Log.i("AsyncMcDownloader","Fully shut down!"); - }catch(InterruptedException e) { - Log.e("AsyncMcDownloader", e.toString()); - } - Log.i("AsyncMcDownloader", "Assets end time: " + System.currentTimeMillis()); - } - - - - public void downloadAssetFile(File outFile, String assetPath, AtomicInteger downloadCounter) throws IOException { - downloadFileMirrored(DownloadMirror.DOWNLOAD_CLASS_ASSETS, MINECRAFT_RES + assetPath, outFile, getByteBuffer(), - new Tools.DownloaderFeedback() { - int prevCurr; - @Override - public void updateProgress(int curr, int max) { - downloadCounter.addAndGet(curr - prevCurr); - prevCurr = curr; - } - } - ); - } - - protected void downloadLibrary(DependentLibrary libItem, String libArtifact, File outLib) throws Throwable{ - String libPathURL; - boolean skipIfFailed = false; - - if (libItem.downloads == null || libItem.downloads.artifact == null) { - MinecraftLibraryArtifact artifact = new MinecraftLibraryArtifact(); - artifact.url = (libItem.url == null - ? "https://libraries.minecraft.net/" - : libItem.url.replace("http://","https://")) + libArtifact; - libItem.downloads = new DependentLibrary.LibraryDownloads(artifact); - - skipIfFailed = true; - } - - try { - libPathURL = libItem.downloads.artifact.url; - boolean isFileGood = false; - byte timesChecked=0; - while(!isFileGood) { - timesChecked++; - if(timesChecked > 5) throw new RuntimeException("Library download failed after 5 retries"); - - downloadFileMirrored(DownloadMirror.DOWNLOAD_CLASS_LIBRARIES, libPathURL, outLib, getByteBuffer(), - (curr, max) -> ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, - (int) Math.max((float)curr/max*100,0), R.string.mcl_launch_downloading_progress, outLib.getName(), curr/BYTE_TO_MB, max/BYTE_TO_MB) - ); - - isFileGood = (libItem.downloads.artifact.sha1 == null - || LauncherPreferences.PREF_CHECK_LIBRARY_SHA) - || Tools.compareSHA1(outLib,libItem.downloads.artifact.sha1); - - ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, 0, - isFileGood ? R.string.dl_library_sha_pass : R.string.dl_library_sha_unknown - ,outLib.getName()); - } - } catch (Throwable th) { - Log.e("AsyncMcDownloader", th.toString(), th); - if (!skipIfFailed) { - throw th; - } else { - th.printStackTrace(); - } - } - } - - public JAssets downloadIndex(JMinecraftVersionList.Version version, File output) throws IOException { - if (!output.exists()) { - output.getParentFile().mkdirs(); - downloadFileMirrored(DownloadMirror.DOWNLOAD_CLASS_METADATA, version.assetIndex != null - ? version.assetIndex.url - : "https://s3.amazonaws.com/Minecraft.Download/indexes/" + version.assets + ".json", output, null, - (curr, max) -> ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, - (int) Math.max((float)curr/max*100,0), R.string.mcl_launch_downloading_progress, output.getName(), curr/BYTE_TO_MB, max/BYTE_TO_MB) - ); - } - - return Tools.GLOBAL_GSON.fromJson(Tools.read(output.getAbsolutePath()), JAssets.class); - } - - public void downloadVersionJson(String versionName, File verJsonDir, JMinecraftVersionList.Version verInfo) throws IOException, DownloaderException { - if(!LauncherPreferences.PREF_CHECK_LIBRARY_SHA) Log.w("Chk","Checker is off"); - - boolean isManifestGood = verifyManifest(verJsonDir, verInfo); - byte retryCount = 0; - while(!isManifestGood && retryCount < 5) { - ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, 0, R.string.mcl_launch_downloading, versionName + ".json"); - downloadFileMirrored(DownloadMirror.DOWNLOAD_CLASS_METADATA, verInfo.url, verJsonDir, getByteBuffer(), - (curr, max) -> ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, - (int) Math.max((float)curr/max*100,0), R.string.mcl_launch_downloading_progress, versionName + ".json", curr/BYTE_TO_MB, max/BYTE_TO_MB) - ); - isManifestGood = verifyManifest(verJsonDir, verInfo); - retryCount++; - // Always do the first verification. But skip all the errors from further ones. - if(!LauncherPreferences.PREF_VERIFY_MANIFEST) return; - } - if(!isManifestGood) { - if(DownloadMirror.isMirrored()) throw new DownloaderException(new MirrorTamperedException()); - else throw new DownloaderException(new IOException("Manifest check failed after 5 tries")); - } - } - - private boolean verifyManifest(File verJsonDir, JMinecraftVersionList.Version verInfo) { - return verJsonDir.exists() - && (!LauncherPreferences.PREF_CHECK_LIBRARY_SHA - || verInfo.sha1 == null - || Tools.compareSHA1(verJsonDir, verInfo.sha1)); - } - public static String normalizeVersionId(String versionString) { JMinecraftVersionList versionList = (JMinecraftVersionList) ExtraCore.getValue(ExtraConstants.RELEASE_TABLE); if(versionList == null || versionList.versions == null) return versionString; @@ -408,31 +22,8 @@ public class AsyncMinecraftDownloader { return null; } - - /**@return A byte buffer bound to a thread, useful to recycle it across downloads */ - private byte[] getByteBuffer(){ - byte[] buffer = mThreadBuffers.get(Thread.currentThread()); - if (buffer == null){ - buffer = new byte[8192]; - mThreadBuffers.put(Thread.currentThread(), buffer); - } - - return buffer; - } - - - - - public interface DoneListener{ void onDownloadDone(); void onDownloadFailed(Throwable throwable); } - - private static class DownloaderException extends Exception { - public DownloaderException() {} - public DownloaderException(Throwable e) { - super(e); - } - } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/NewMinecraftDownloader.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/NewMinecraftDownloader.java new file mode 100644 index 000000000..1998176d2 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/NewMinecraftDownloader.java @@ -0,0 +1,373 @@ +package net.kdt.pojavlaunch.tasks; + +import static net.kdt.pojavlaunch.PojavApplication.sExecutorService; + +import android.app.Activity; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.kdt.mcgui.ProgressLayout; + +import net.kdt.pojavlaunch.JAssetInfo; +import net.kdt.pojavlaunch.JAssets; +import net.kdt.pojavlaunch.JMinecraftVersionList; +import net.kdt.pojavlaunch.JRE17Util; +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.mirrors.DownloadMirror; +import net.kdt.pojavlaunch.prefs.LauncherPreferences; +import net.kdt.pojavlaunch.utils.DownloadUtils; +import net.kdt.pojavlaunch.utils.FileUtils; +import net.kdt.pojavlaunch.value.DependentLibrary; +import net.kdt.pojavlaunch.value.MinecraftClientInfo; +import net.kdt.pojavlaunch.value.MinecraftLibraryArtifact; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +// TODO: implement mirror tamper checking. +public class NewMinecraftDownloader { + public static final String MINECRAFT_RES = "https://resources.download.minecraft.net/"; + private AtomicReference mThrownDownloaderException; + private ArrayList mScheduledDownloadTasks; + private AtomicLong mDownloadFileCounter; + private AtomicLong mDownloadSizeCounter; + private long mDownloadFileCount; + + private static final ThreadLocal sThreadLocalDownloadBuffer = new ThreadLocal<>(); + + /** + * Start the game version download process on the global executor service. + * @param activity Activity, used for automatic installation of JRE 17 if needed + * @param version The JMinecraftVersionList.Version from the version list, if available + * @param realVersion The version ID (necessary) + * @param listener The download status listener + */ + public void start(@Nullable Activity activity, @Nullable JMinecraftVersionList.Version version, + @NonNull String realVersion, // this was there for a reason + @NonNull AsyncMinecraftDownloader.DoneListener listener) { + sExecutorService.execute(() -> { + try { + downloadGame(activity, version, realVersion); + listener.onDownloadDone(); + }catch (Exception e) { + listener.onDownloadFailed(e); + } + ProgressLayout.clearProgress(ProgressLayout.DOWNLOAD_MINECRAFT); + }); + } + + /** + * Download the game version. + * @param activity Activity, used for automatic installation of JRE 17 if needed + * @param verInfo The JMinecraftVersionList.Version from the version list, if available + * @param versionName The version ID (necessary) + * @throws Exception when an exception occurs in the function body or in any of the downloading threads. + */ + private void downloadGame(Activity activity, JMinecraftVersionList.Version verInfo, String versionName) throws Exception { + // Put up a dummy progress line, for the activity to start the service and do all the other necessary + // work to keep the launcher alive. We will replace this line when we will start downloading stuff. + ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, 0, R.string.newdl_starting); + + mScheduledDownloadTasks = new ArrayList<>(); + mDownloadFileCounter = new AtomicLong(0); + mDownloadSizeCounter = new AtomicLong(0); + mThrownDownloaderException = new AtomicReference<>(null); + + if(!downloadAndProcessMetadata(activity, verInfo, versionName)) { + throw new RuntimeException(activity.getString(R.string.exception_failed_to_unpack_jre17)); + } + + ArrayBlockingQueue taskQueue = + new ArrayBlockingQueue<>(mScheduledDownloadTasks.size(), false); + ThreadPoolExecutor downloaderPool = + new ThreadPoolExecutor(4, 4, 500, TimeUnit.MILLISECONDS, taskQueue); + + // I have tried pre-filling the queue directly instead of doing this, but it didn't work. + // What a shame. + for(DownloaderTask scheduledTask : mScheduledDownloadTasks) downloaderPool.execute(scheduledTask); + downloaderPool.shutdown(); + + try { + while (mThrownDownloaderException.get() == null && + !downloaderPool.awaitTermination(33, TimeUnit.MILLISECONDS)) { + long dlFileCounter = mDownloadFileCounter.get(); + int progress = (int)((dlFileCounter * 100L) / mDownloadFileCount); + ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, progress, + R.string.newdl_downloading_game_files, dlFileCounter, + mDownloadFileCount, (double)mDownloadSizeCounter.get() / (1024d * 1024d)); + } + Exception thrownException = mThrownDownloaderException.get(); + if(thrownException != null) throw thrownException; + }catch (InterruptedException e) { + // Interrupted while waiting, which means that the download was cancelled. + // Kill all downloading threads immediately, and ignore any exceptions thrown by them + downloaderPool.shutdownNow(); + } + } + + private File createGameJsonPath(String versionId) { + return new File(Tools.DIR_HOME_VERSION, versionId + File.separator + versionId + ".json"); + } + + private File createGameJarPath(String versionId) { + return new File(Tools.DIR_HOME_VERSION, versionId + File.separator + versionId + ".jar"); + } + + private File downloadGameJson(JMinecraftVersionList.Version verInfo) throws IOException { + File targetFile = createGameJsonPath(verInfo.id); + if(verInfo.sha1 == null && targetFile.canRead() && targetFile.isFile()) + return targetFile; + FileUtils.ensureParentDirectory(targetFile); + DownloadUtils.ensureSha1(targetFile, LauncherPreferences.PREF_VERIFY_MANIFEST ? verInfo.sha1 : null, ()-> { + ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, 0, + R.string.newdl_downloading_metadata, targetFile.getName()); + DownloadMirror.downloadFileMirrored(DownloadMirror.DOWNLOAD_CLASS_METADATA, verInfo.url, targetFile); + return null; + }); + return targetFile; + } + + private JAssets downloadAssetsIndex(JMinecraftVersionList.Version verInfo) throws IOException{ + JMinecraftVersionList.AssetIndex assetIndex = verInfo.assetIndex; + if(assetIndex == null || verInfo.assets == null) return null; + File targetFile = new File(Tools.ASSETS_PATH, "indexes"+ File.separator + verInfo.assets + ".json"); + FileUtils.ensureParentDirectory(targetFile); + DownloadUtils.ensureSha1(targetFile, assetIndex.sha1, ()-> { + ProgressLayout.setProgress(ProgressLayout.DOWNLOAD_MINECRAFT, 0, + R.string.newdl_downloading_metadata, targetFile.getName()); + DownloadMirror.downloadFileMirrored(DownloadMirror.DOWNLOAD_CLASS_METADATA, assetIndex.url, targetFile); + return null; + }); + return Tools.GLOBAL_GSON.fromJson(Tools.read(targetFile), JAssets.class); + } + + private MinecraftClientInfo getClientInfo(JMinecraftVersionList.Version verInfo) { + Map downloads = verInfo.downloads; + if(downloads == null) return null; + return downloads.get("client"); + } + + /** + * Download (if necessary) and process a version's metadata, scheduling all downloads that this + * version needs. + * @param activity Activity, used for automatic installation of JRE 17 if needed + * @param verInfo The JMinecraftVersionList.Version from the version list, if available + * @param versionName The version ID (necessary) + * @return false if JRE17 installation failed, true otherwise + * @throws IOException if the download of any of the metadata files fails + */ + private boolean downloadAndProcessMetadata(Activity activity, JMinecraftVersionList.Version verInfo, String versionName) throws IOException { + File versionJsonFile; + if(verInfo != null) versionJsonFile = downloadGameJson(verInfo); + else versionJsonFile = createGameJsonPath(versionName); + if(versionJsonFile.canRead()) { + verInfo = Tools.GLOBAL_GSON.fromJson(Tools.read(versionJsonFile), JMinecraftVersionList.Version.class); + } else { + throw new IOException("Unable to read Version JSON for version "+versionName); + } + + if(activity != null && !JRE17Util.installNewJreIfNeeded(activity, verInfo)){ + return false; + } + + JAssets assets = downloadAssetsIndex(verInfo); + if(assets != null) scheduleAssetDownloads(assets); + + + MinecraftClientInfo minecraftClientInfo = getClientInfo(verInfo); + if(minecraftClientInfo != null) { + growDownloadList(1); + scheduleDownload(createGameJarPath(verInfo.id), + DownloadMirror.DOWNLOAD_CLASS_LIBRARIES, + minecraftClientInfo.url, + minecraftClientInfo.sha1, + minecraftClientInfo.size, + false + ); + } + + if(verInfo.libraries != null) scheduleLibraryDownloads(verInfo.libraries); + + if(verInfo.logging != null) scheduleLoggingAssetDownloadIfNeeded(verInfo.logging); + + if(Tools.isValidString(verInfo.inheritsFrom)) { + JMinecraftVersionList.Version inheritedVersion = AsyncMinecraftDownloader.getListedVersion(verInfo.inheritsFrom); + // Infinite inheritance !?! :noway: + return downloadAndProcessMetadata(activity, inheritedVersion, verInfo.inheritsFrom); + } + return true; + } + + private void growDownloadList(int addedElementCount) { + mScheduledDownloadTasks.ensureCapacity(mScheduledDownloadTasks.size() + addedElementCount); + } + + private void scheduleDownload(File targetFile, int downloadClass, String url, String sha1, + long size, boolean skipIfFailed) { + mDownloadFileCount++; + mScheduledDownloadTasks.add( + new DownloaderTask(targetFile, downloadClass, url, sha1, size, skipIfFailed) + ); + } + + private void scheduleLibraryDownloads(DependentLibrary[] dependentLibraries) { + Tools.preProcessLibraries(dependentLibraries); + growDownloadList(dependentLibraries.length); + for(DependentLibrary dependentLibrary : dependentLibraries) { + String libArtifactPath = Tools.artifactToPath(dependentLibrary); + String sha1 = null, url = null; + long size = 0; + boolean skipIfFailed = false; + if(dependentLibrary.downloads != null && dependentLibrary.downloads.artifact != null) { + MinecraftLibraryArtifact artifact = dependentLibrary.downloads.artifact; + sha1 = artifact.sha1; + url = artifact.url; + size = artifact.size; + } + if(url == null) { + url = (dependentLibrary.url == null + ? "https://libraries.minecraft.net/" + : dependentLibrary.url.replace("http://","https://")) + libArtifactPath; + skipIfFailed = true; + } + if(!LauncherPreferences.PREF_CHECK_LIBRARY_SHA) sha1 = null; + scheduleDownload(new File(Tools.DIR_HOME_LIBRARY, libArtifactPath), + DownloadMirror.DOWNLOAD_CLASS_LIBRARIES, + url, sha1, size, skipIfFailed + ); + } + } + + private void scheduleAssetDownloads(JAssets assets) { + Map assetObjects = assets.objects; + if(assetObjects == null) return; + Set assetNames = assetObjects.keySet(); + growDownloadList(assetNames.size()); + for(String asset : assetNames) { + JAssetInfo assetInfo = assetObjects.get(asset); + if(assetInfo == null) continue; + File targetFile; + String hashedPath = assetInfo.hash.substring(0, 2) + File.separator + assetInfo.hash; + if(assets.virtual || assets.mapToResources) { + targetFile = new File(Tools.OBSOLETE_RESOURCES_PATH, asset); + } else { + targetFile = new File(Tools.ASSETS_PATH, "objects" + File.separator + hashedPath); + } + String sha1 = LauncherPreferences.PREF_CHECK_LIBRARY_SHA ? assetInfo.hash : null; + scheduleDownload(targetFile, + DownloadMirror.DOWNLOAD_CLASS_ASSETS, + MINECRAFT_RES + hashedPath, + sha1, + assetInfo.size, + false); + } + } + + private void scheduleLoggingAssetDownloadIfNeeded(JMinecraftVersionList.LoggingConfig loggingConfig) { + if(loggingConfig.client == null || loggingConfig.client.file == null) return; + JMinecraftVersionList.FileProperties loggingFileProperties = loggingConfig.client.file; + File internalLoggingConfig = new File(Tools.DIR_DATA + File.separator + "security", + loggingFileProperties.id.replace("client", "log4j-rce-patch")); + if(internalLoggingConfig.exists()) return; + File destination = new File(Tools.DIR_GAME_NEW, loggingFileProperties.id); + scheduleDownload(destination, + DownloadMirror.DOWNLOAD_CLASS_LIBRARIES, + loggingFileProperties.url, + loggingFileProperties.sha1, + loggingFileProperties.size, + false); + } + + private static byte[] getLocalBuffer() { + byte[] tlb = sThreadLocalDownloadBuffer.get(); + if(tlb != null) return tlb; + tlb = new byte[65535]; + sThreadLocalDownloadBuffer.set(tlb); + return tlb; + } + + class DownloaderTask implements Runnable, Tools.DownloaderFeedback { + private final File mTargetPath; + private final String mTargetUrl; + private String mTargetSha1; + private final int mDownloadClass; + private final boolean mSkipIfFailed; + private int mLastCurr; + private final long mDownloadSize; + + DownloaderTask(File targetPath, int downloadClass, String targetUrl, String targetSha1, + long downloadSize, boolean skipIfFailed) { + this.mTargetPath = targetPath; + this.mTargetUrl = targetUrl; + this.mTargetSha1 = targetSha1; + this.mDownloadClass = downloadClass; + this.mDownloadSize = downloadSize; + this.mSkipIfFailed = skipIfFailed; + } + + @Override + public void run() { + try { + runCatching(); + }catch (Exception e) { + mThrownDownloaderException.set(e); + } + } + + private void runCatching() throws Exception { + FileUtils.ensureParentDirectory(mTargetPath); + if(Tools.isValidString(mTargetSha1)) { + verifyFileSha1(); + }else { + mTargetSha1 = null; // Nullify SHA1 as DownloadUtils.ensureSha1 only checks for null, + // not for string validity + if(mTargetPath.exists()) finishWithoutDownloading(); + else downloadFile(); + } + } + + private void verifyFileSha1() throws Exception { + if(mTargetPath.isFile() && mTargetPath.canRead() && Tools.compareSHA1(mTargetPath, mTargetSha1)) { + finishWithoutDownloading(); + } else { + // Rely on the download function to throw an IOE in case if the file is not + // writable/not a file/etc... + downloadFile(); + } + } + + private void downloadFile() throws Exception { + try { + DownloadUtils.ensureSha1(mTargetPath, mTargetSha1, () -> { + DownloadMirror.downloadFileMirrored(mDownloadClass, mTargetUrl, mTargetPath, + getLocalBuffer(), this); + return null; + }); + }catch (Exception e) { + if(!mSkipIfFailed) throw e; + } + mDownloadFileCounter.incrementAndGet(); + } + + private void finishWithoutDownloading() { + mDownloadFileCounter.incrementAndGet(); + mDownloadSizeCounter.addAndGet(mDownloadSize); + } + + @Override + public void updateProgress(int curr, int max) { + mDownloadSizeCounter.addAndGet(curr - mLastCurr); + mLastCurr = curr; + } + } +} diff --git a/app_pojavlauncher/src/main/res/values/strings.xml b/app_pojavlauncher/src/main/res/values/strings.xml index bc02be55f..de54889ea 100644 --- a/app_pojavlauncher/src/main/res/values/strings.xml +++ b/app_pojavlauncher/src/main/res/values/strings.xml @@ -368,4 +368,7 @@ Allow V-Sync with Zink Allows the launcher to use internal system APIs to enable V-Sync for Zink. Turn this off if your launcher suddenly crashes with Zink after a system update. Failed to install JRE 17 + Reading game metadata… + Downloading game metadata (%s) + Downloading game files… (%d/%d, %.2f MB)