Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66a2ea07bd | ||
|
|
042e28216d | ||
|
|
9f486f5549 | ||
|
|
3e5a4b1d49 | ||
|
|
f023339a38 | ||
|
|
9a68c36cc6 | ||
|
|
703c7f7d0d | ||
|
|
46d502a5fd | ||
|
|
697ec45620 | ||
|
|
12c0165cdf |
No files matched your search
@@ -109,7 +109,7 @@ public final class LauncherHelper {
|
|||||||
try {
|
try {
|
||||||
checkGameState(profile, setting, version, () -> {
|
checkGameState(profile, setting, version, () -> {
|
||||||
Controllers.dialog(launchingStepsPane);
|
Controllers.dialog(launchingStepsPane);
|
||||||
Schedulers.newThread().execute(this::launch0);
|
Schedulers.defaultScheduler().execute(this::launch0);
|
||||||
});
|
});
|
||||||
} catch (InterruptedException | RejectedExecutionException ignore) {
|
} catch (InterruptedException | RejectedExecutionException ignore) {
|
||||||
}
|
}
|
||||||
@@ -199,7 +199,7 @@ public final class LauncherHelper {
|
|||||||
Controllers.dialog(i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
|
Controllers.dialog(i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).thenRunAsync(Schedulers.defaultScheduler(), () -> {
|
}).thenRunAsync(() -> {
|
||||||
launchingLatch.await();
|
launchingLatch.await();
|
||||||
}).withStage("launch.state.waiting_launching"))
|
}).withStage("launch.state.waiting_launching"))
|
||||||
.withStagesHint(Lang.immutableListOf(
|
.withStagesHint(Lang.immutableListOf(
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import javafx.fxml.FXML;
|
|||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
|
import org.jackhuang.hmcl.task.DownloadManager;
|
||||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||||
import org.jackhuang.hmcl.task.TaskListener;
|
import org.jackhuang.hmcl.task.TaskListener;
|
||||||
@@ -37,7 +38,7 @@ import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
|||||||
public class TaskExecutorDialogPane extends StackPane {
|
public class TaskExecutorDialogPane extends StackPane {
|
||||||
private TaskExecutor executor;
|
private TaskExecutor executor;
|
||||||
private Consumer<Region> onCancel;
|
private Consumer<Region> onCancel;
|
||||||
private final Consumer<FileDownloadTask.SpeedEvent> speedEventHandler;
|
private final Consumer<DownloadManager.SpeedEvent> speedEventHandler;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Label lblTitle;
|
private Label lblTitle;
|
||||||
@@ -75,7 +76,7 @@ public class TaskExecutorDialogPane extends StackPane {
|
|||||||
lblProgress.setText(String.format("%.1f %s", finalSpeed, finalUnit));
|
lblProgress.setText(String.format("%.1f %s", finalSpeed, finalUnit));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
FileDownloadTask.speedEvent.channel(FileDownloadTask.SpeedEvent.class).registerWeak(speedEventHandler);
|
DownloadManager.speedEvent.channel(DownloadManager.SpeedEvent.class).registerWeak(speedEventHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setExecutor(TaskExecutor executor) {
|
public void setExecutor(TaskExecutor executor) {
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ public final class TaskListPane extends StackPane {
|
|||||||
|
|
||||||
public void setThrowable(Throwable throwable) {
|
public void setThrowable(Throwable throwable) {
|
||||||
unbind();
|
unbind();
|
||||||
state.setText(throwable.getLocalizedMessage());
|
state.setText(throwable == null ? "" : throwable.getLocalizedMessage());
|
||||||
bar.setProgress(0);
|
bar.setProgress(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ public final class ModpackSelectionPage extends StackPane implements WizardPage
|
|||||||
resolve.run();
|
resolve.run();
|
||||||
|
|
||||||
Controllers.taskDialog(
|
Controllers.taskDialog(
|
||||||
new FileDownloadTask(url, modpack.toFile(), null)
|
new FileDownloadTask(url, modpack.toFile())
|
||||||
.whenComplete(Schedulers.javafx(), e -> {
|
.whenComplete(Schedulers.javafx(), e -> {
|
||||||
if (e == null) {
|
if (e == null) {
|
||||||
resolve.run();
|
resolve.run();
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
package org.jackhuang.hmcl.upgrade;
|
package org.jackhuang.hmcl.upgrade;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||||
|
import org.jackhuang.hmcl.task.Task;
|
||||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||||
import org.tukaani.xz.XZInputStream;
|
import org.tukaani.xz.XZInputStream;
|
||||||
|
|
||||||
@@ -25,25 +26,31 @@ import java.io.ByteArrayInputStream;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.jar.JarOutputStream;
|
import java.util.jar.JarOutputStream;
|
||||||
import java.util.jar.Pack200;
|
import java.util.jar.Pack200;
|
||||||
|
|
||||||
class HMCLDownloadTask extends FileDownloadTask {
|
class HMCLDownloadTask extends Task<Void> {
|
||||||
|
|
||||||
private RemoteVersion.Type archiveFormat;
|
private final FileDownloadTask task;
|
||||||
|
private final RemoteVersion.Type archiveFormat;
|
||||||
|
private final Path target;
|
||||||
|
|
||||||
public HMCLDownloadTask(RemoteVersion version, Path target) {
|
public HMCLDownloadTask(RemoteVersion version, Path target) {
|
||||||
super(NetworkUtils.toURL(version.getUrl()), target.toFile(), version.getIntegrityCheck());
|
this.task = new FileDownloadTask(NetworkUtils.toURL(version.getUrl()), target.toFile(), version.getIntegrityCheck());
|
||||||
archiveFormat = version.getType();
|
this.archiveFormat = version.getType();
|
||||||
|
this.target = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Task<?>> getDependents() {
|
||||||
|
return Collections.singletonList(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute() throws Exception {
|
public void execute() throws Exception {
|
||||||
super.execute();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Path target = getFile().toPath();
|
|
||||||
|
|
||||||
switch (archiveFormat) {
|
switch (archiveFormat) {
|
||||||
case JAR:
|
case JAR:
|
||||||
break;
|
break;
|
||||||
@@ -60,7 +67,7 @@ class HMCLDownloadTask extends FileDownloadTask {
|
|||||||
throw new IllegalArgumentException("Unknown format: " + archiveFormat);
|
throw new IllegalArgumentException("Unknown format: " + archiveFormat);
|
||||||
}
|
}
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
getFile().delete();
|
Files.deleteIfExists(target);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ public class YggdrasilService {
|
|||||||
try {
|
try {
|
||||||
return GSON.fromJson(text, typeOfT);
|
return GSON.fromJson(text, typeOfT);
|
||||||
} catch (JsonParseException e) {
|
} catch (JsonParseException e) {
|
||||||
throw new ServerResponseMalformedException(e);
|
throw new ServerResponseMalformedException("Server response: " + text, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import org.jackhuang.hmcl.download.VersionMismatchException;
|
|||||||
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
|
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
|
||||||
import org.jackhuang.hmcl.game.GameVersion;
|
import org.jackhuang.hmcl.game.GameVersion;
|
||||||
import org.jackhuang.hmcl.game.Version;
|
import org.jackhuang.hmcl.game.Version;
|
||||||
|
import org.jackhuang.hmcl.task.DownloadManager;
|
||||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||||
import org.jackhuang.hmcl.task.Task;
|
import org.jackhuang.hmcl.task.Task;
|
||||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||||
@@ -72,8 +73,11 @@ public final class ForgeInstallTask extends Task<Version> {
|
|||||||
installer = Files.createTempFile("forge-installer", ".jar");
|
installer = Files.createTempFile("forge-installer", ".jar");
|
||||||
|
|
||||||
dependent = new FileDownloadTask(
|
dependent = new FileDownloadTask(
|
||||||
dependencyManager.getDownloadProvider().injectURLsWithCandidates(remote.getUrls()),
|
new DownloadManager.DownloadTaskStateBuilder()
|
||||||
installer.toFile(), null);
|
.setUrls(dependencyManager.getDownloadProvider().injectURLsWithCandidates(remote.getUrls()))
|
||||||
|
.setFile(installer.toFile())
|
||||||
|
.setInitialParts(3)
|
||||||
|
.build());
|
||||||
dependent.setCacheRepository(dependencyManager.getCacheRepository());
|
dependent.setCacheRepository(dependencyManager.getCacheRepository());
|
||||||
dependent.setCaching(true);
|
dependent.setCaching(true);
|
||||||
dependent.addIntegrityCheckHandler(FileDownloadTask.ZIP_INTEGRITY_CHECK_HANDLER);
|
dependent.addIntegrityCheckHandler(FileDownloadTask.ZIP_INTEGRITY_CHECK_HANDLER);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.download.game;
|
|||||||
|
|
||||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||||
import org.jackhuang.hmcl.game.Version;
|
import org.jackhuang.hmcl.game.Version;
|
||||||
|
import org.jackhuang.hmcl.task.DownloadManager;
|
||||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||||
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
|
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
|
||||||
import org.jackhuang.hmcl.task.Task;
|
import org.jackhuang.hmcl.task.Task;
|
||||||
@@ -56,10 +57,11 @@ public final class GameDownloadTask extends Task<Void> {
|
|||||||
public void execute() {
|
public void execute() {
|
||||||
File jar = dependencyManager.getGameRepository().getVersionJar(version);
|
File jar = dependencyManager.getGameRepository().getVersionJar(version);
|
||||||
|
|
||||||
FileDownloadTask task = new FileDownloadTask(
|
FileDownloadTask task = new FileDownloadTask(new DownloadManager.DownloadTaskStateBuilder()
|
||||||
dependencyManager.getDownloadProvider().injectURLWithCandidates(version.getDownloadInfo().getUrl()),
|
.setUrls(dependencyManager.getDownloadProvider().injectURLWithCandidates(version.getDownloadInfo().getUrl()))
|
||||||
jar,
|
.setFile(jar)
|
||||||
IntegrityCheck.of(CacheRepository.SHA1, version.getDownloadInfo().getSha1()));
|
.setInitialParts(3)
|
||||||
|
.build(), IntegrityCheck.of(CacheRepository.SHA1, version.getDownloadInfo().getSha1()));
|
||||||
task.setCaching(true);
|
task.setCaching(true);
|
||||||
task.setCacheRepository(dependencyManager.getCacheRepository());
|
task.setCacheRepository(dependencyManager.getCacheRepository());
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -18,11 +18,12 @@
|
|||||||
package org.jackhuang.hmcl.download.game;
|
package org.jackhuang.hmcl.download.game;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.game.Library;
|
import org.jackhuang.hmcl.game.Library;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
public class LibraryDownloadException extends Exception {
|
public class LibraryDownloadException extends Exception {
|
||||||
private final Library library;
|
private final Library library;
|
||||||
|
|
||||||
public LibraryDownloadException(Library library, Throwable cause) {
|
public LibraryDownloadException(Library library, @NotNull Throwable cause) {
|
||||||
super("Unable to download library " + library, cause);
|
super("Unable to download library " + library, cause);
|
||||||
|
|
||||||
this.library = library;
|
this.library = library;
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.CancellationException;
|
||||||
import java.util.jar.*;
|
import java.util.jar.*;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
@@ -95,6 +96,8 @@ public class LibraryDownloadTask extends Task<Void> {
|
|||||||
Exception t = task.getException();
|
Exception t = task.getException();
|
||||||
if (t instanceof DownloadException)
|
if (t instanceof DownloadException)
|
||||||
throw new LibraryDownloadException(library, t.getCause());
|
throw new LibraryDownloadException(library, t.getCause());
|
||||||
|
else if (t == null)
|
||||||
|
throw new CancellationException();
|
||||||
else
|
else
|
||||||
throw new LibraryDownloadException(library, t);
|
throw new LibraryDownloadException(library, t);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import com.google.gson.JsonParseException;
|
|||||||
import org.jackhuang.hmcl.util.Lang;
|
import org.jackhuang.hmcl.util.Lang;
|
||||||
import org.jackhuang.hmcl.util.Logging;
|
import org.jackhuang.hmcl.util.Logging;
|
||||||
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
|
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.CancellationException;
|
||||||
@@ -111,9 +112,9 @@ public final class AsyncTaskExecutor extends TaskExecutor {
|
|||||||
return CompletableFuture.runAsync(this::checkCancellation);
|
return CompletableFuture.runAsync(this::checkCancellation);
|
||||||
|
|
||||||
return CompletableFuture.allOf(tasks.stream()
|
return CompletableFuture.allOf(tasks.stream()
|
||||||
.map(task -> CompletableFuture.completedFuture(null)
|
.map(task -> ((CompletableFuture) CompletableFuture.completedFuture(null))
|
||||||
.thenComposeAsync(unused2 -> executeTask(parentTask, task))
|
.thenComposeAsync(unused2 -> (CompletableFuture) executeTask(parentTask, task))
|
||||||
).toArray(CompletableFuture<?>[]::new));
|
).toArray(CompletableFuture[]::new));
|
||||||
})
|
})
|
||||||
.thenApplyAsync(unused -> (Exception) null)
|
.thenApplyAsync(unused -> (Exception) null)
|
||||||
.exceptionally(throwable -> {
|
.exceptionally(throwable -> {
|
||||||
@@ -127,9 +128,61 @@ public final class AsyncTaskExecutor extends TaskExecutor {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private CompletableFuture<?> executeTask(Task<?> parentTask, Task<?> task) {
|
private <T> CompletableFuture<?> executeTask(Task<?> parentTask, Task<T> task) {
|
||||||
checkCancellation();
|
checkCancellation();
|
||||||
|
|
||||||
|
if (task instanceof CompletableFutureTask) {
|
||||||
|
return CompletableFuture.completedFuture(null)
|
||||||
|
.thenComposeAsync(unused -> {
|
||||||
|
checkCancellation();
|
||||||
|
|
||||||
|
task.setCancelled(this::isCancelled);
|
||||||
|
task.setState(Task.TaskState.READY);
|
||||||
|
if (parentTask != null && task.getStage() == null)
|
||||||
|
task.setStage(parentTask.getStage());
|
||||||
|
|
||||||
|
if (task.getSignificance().shouldLog())
|
||||||
|
Logging.LOG.log(Level.FINE, "Executing task: " + task.getName());
|
||||||
|
|
||||||
|
taskListeners.forEach(it -> it.onRunning(task));
|
||||||
|
taskListeners.forEach(it -> it.onReady(task));
|
||||||
|
|
||||||
|
return ((CompletableFutureTask<T>) task).getCompletableFuture();
|
||||||
|
}).whenComplete((result, throwable) -> {
|
||||||
|
Throwable resolved = resolveException(throwable);
|
||||||
|
task.setResult(result);
|
||||||
|
task.setState(Task.TaskState.EXECUTED);
|
||||||
|
if (isCancelled() || resolved instanceof InterruptedException || resolved instanceof CancellationException) {
|
||||||
|
task.setException(null);
|
||||||
|
if (task.getSignificance().shouldLog()) {
|
||||||
|
Logging.LOG.log(Level.FINE, "Task aborted: " + task.getName());
|
||||||
|
}
|
||||||
|
task.onDone().fireEvent(new TaskEvent(this, task, true));
|
||||||
|
taskListeners.forEach(it -> it.onFailed(task, resolved));
|
||||||
|
} else if (resolved == null) {
|
||||||
|
if (task.getSignificance().shouldLog()) {
|
||||||
|
Logging.LOG.log(Level.FINER, "Task finished: " + task.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
task.onDone().fireEvent(new TaskEvent(this, task, false));
|
||||||
|
taskListeners.forEach(it -> it.onFinished(task));
|
||||||
|
|
||||||
|
task.setState(Task.TaskState.SUCCEEDED);
|
||||||
|
} else if (resolved instanceof Exception) {
|
||||||
|
Exception e = (Exception) resolved;
|
||||||
|
task.setException(e);
|
||||||
|
exception = e;
|
||||||
|
if (task.getSignificance().shouldLog()) {
|
||||||
|
Logging.LOG.log(Level.FINE, "Task failed: " + task.getName(), e);
|
||||||
|
}
|
||||||
|
task.onDone().fireEvent(new TaskEvent(this, task, true));
|
||||||
|
taskListeners.forEach(it -> it.onFailed(task, e));
|
||||||
|
} else {
|
||||||
|
throw new CompletionException(resolved); // rethrow error
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return CompletableFuture.completedFuture(null)
|
return CompletableFuture.completedFuture(null)
|
||||||
.thenComposeAsync(unused -> {
|
.thenComposeAsync(unused -> {
|
||||||
checkCancellation();
|
checkCancellation();
|
||||||
@@ -233,14 +286,14 @@ public final class AsyncTaskExecutor extends TaskExecutor {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Throwable resolveException(Throwable e) {
|
private static Throwable resolveException(@Nullable Throwable e) {
|
||||||
if (e instanceof ExecutionException || e instanceof CompletionException)
|
if (e instanceof ExecutionException || e instanceof CompletionException)
|
||||||
return resolveException(e.getCause());
|
return resolveException(e.getCause());
|
||||||
else
|
else
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void rethrow(Throwable e) {
|
public static void rethrow(Throwable e) {
|
||||||
if (e == null)
|
if (e == null)
|
||||||
return;
|
return;
|
||||||
if (e instanceof ExecutionException || e instanceof CompletionException) { // including UncheckedException and UncheckedThrowable
|
if (e instanceof ExecutionException || e instanceof CompletionException) { // including UncheckedException and UncheckedThrowable
|
||||||
@@ -252,7 +305,7 @@ public final class AsyncTaskExecutor extends TaskExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Runnable wrap(ExceptionalRunnable<?> runnable) {
|
public static Runnable wrap(ExceptionalRunnable<?> runnable) {
|
||||||
return () -> {
|
return () -> {
|
||||||
try {
|
try {
|
||||||
runnable.run();
|
runnable.run();
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2020 huangyuhui <[email protected]> and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.jackhuang.hmcl.task;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public abstract class CompletableFutureTask<T> extends Task<T> {
|
||||||
|
|
||||||
|
public abstract CompletableFuture<T> getCompletableFuture();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute() throws Exception {
|
||||||
|
setResult(getCompletableFuture().get());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,8 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.task;
|
package org.jackhuang.hmcl.task;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
|
||||||
@@ -26,7 +28,7 @@ public class DownloadException extends IOException {
|
|||||||
|
|
||||||
private final URL url;
|
private final URL url;
|
||||||
|
|
||||||
public DownloadException(URL url, Throwable cause) {
|
public DownloadException(URL url, @NotNull Throwable cause) {
|
||||||
super("Unable to download " + url + ", " + cause.getMessage(), requireNonNull(cause));
|
super("Unable to download " + url + ", " + cause.getMessage(), requireNonNull(cause));
|
||||||
|
|
||||||
this.url = url;
|
this.url = url;
|
||||||
|
|||||||
@@ -0,0 +1,732 @@
|
|||||||
|
/*
|
||||||
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2020 huangyuhui <[email protected]> and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.jackhuang.hmcl.task;
|
||||||
|
|
||||||
|
import com.google.gson.JsonParseException;
|
||||||
|
import org.jackhuang.hmcl.event.Event;
|
||||||
|
import org.jackhuang.hmcl.event.EventBus;
|
||||||
|
import org.jackhuang.hmcl.util.CacheRepository;
|
||||||
|
import org.jackhuang.hmcl.util.ToStringBuilder;
|
||||||
|
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||||
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||||
|
|
||||||
|
public class DownloadManager {
|
||||||
|
|
||||||
|
public static class DownloadTaskStateBuilder {
|
||||||
|
private List<URL> urls;
|
||||||
|
private Path file;
|
||||||
|
private int retry = 3;
|
||||||
|
private int initialParts = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the url of remote file to be downloaded.
|
||||||
|
* @param url url of the remote file to be downloaded
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public DownloadTaskStateBuilder setUrl(URL url) {
|
||||||
|
this.urls = Collections.singletonList(url);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set urls of the remote file to be downloaded, will be attempted in order.
|
||||||
|
* @param urls urls of remote files to be downloaded
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public DownloadTaskStateBuilder setUrls(List<URL> urls) {
|
||||||
|
this.urls = urls;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set location to save remote file.
|
||||||
|
* @param file location to save the remote file.
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public DownloadTaskStateBuilder setFile(Path file) {
|
||||||
|
this.file = file;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set location to save remote file.
|
||||||
|
* @param file location to save the remote file.
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public DownloadTaskStateBuilder setFile(File file) {
|
||||||
|
this.file = file.toPath();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set retry times of one url.
|
||||||
|
* @param retry retry times of one url.
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public DownloadTaskStateBuilder setRetry(int retry) {
|
||||||
|
this.retry = retry;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits the remote file into multiple parts, and download in different
|
||||||
|
* threads.
|
||||||
|
*
|
||||||
|
* @param initialParts number of threads to download the file.
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public DownloadTaskStateBuilder setInitialParts(int initialParts) {
|
||||||
|
this.initialParts = initialParts;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DownloadTaskState build() {
|
||||||
|
if (file == null) {
|
||||||
|
return DownloadTaskState.newWithLengthUnknown(urls, null, retry, initialParts);
|
||||||
|
}
|
||||||
|
|
||||||
|
Path downloadingFile = file.resolveSibling(FileUtils.getName(file) + ".download");
|
||||||
|
Path stateFile = file.resolveSibling(FileUtils.getName(file) + ".status");
|
||||||
|
DownloadState state = null;
|
||||||
|
if (Files.exists(downloadingFile) && Files.exists(stateFile)) {
|
||||||
|
// Resume downloading from state
|
||||||
|
try {
|
||||||
|
String status = FileUtils.readText(stateFile);
|
||||||
|
state = JsonUtils.fromNonNullJson(status, DownloadState.class);
|
||||||
|
} catch (JsonParseException | IOException e) {
|
||||||
|
LOG.log(Level.WARNING, "Failed to parse download state file", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == null || !urls.equals(state.urls)) {
|
||||||
|
return DownloadTaskState.newWithLengthUnknown(urls, file, retry, initialParts);
|
||||||
|
} else {
|
||||||
|
return new DownloadTaskState(state, file, retry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static class SafeRegion implements AutoCloseable {
|
||||||
|
final ReentrantLock lock = new ReentrantLock();
|
||||||
|
|
||||||
|
void begin() {
|
||||||
|
lock.lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
void end() {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static abstract class Downloader<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* do something before connection.
|
||||||
|
*
|
||||||
|
* @param segment segment
|
||||||
|
* @param url currently ready URL
|
||||||
|
*/
|
||||||
|
protected void onBeforeConnection(DownloadSegment segment, URL url) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup downloading environment, creates files, etc.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurred.
|
||||||
|
*/
|
||||||
|
protected abstract void onStart() throws IOException;
|
||||||
|
|
||||||
|
protected abstract void onContentLengthChanged(long contentLength) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accept recently downloaded data segment.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurred.
|
||||||
|
*/
|
||||||
|
protected abstract void write(long pos, byte[] buffer, int offset, int len) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify if we should check etag, or even it is already cached,
|
||||||
|
* and make use of it.
|
||||||
|
*
|
||||||
|
* @return whether we should check etag, or stop downloading if cached.
|
||||||
|
*/
|
||||||
|
protected abstract DownloadTask.EnumCheckETag shouldCheckETag();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make cached file as result of downloader.
|
||||||
|
*
|
||||||
|
* This method can fails, if failed, downloading should continue.
|
||||||
|
*
|
||||||
|
* @param cachedFile verified cached file
|
||||||
|
* @throws IOException if an I/O error occurred.
|
||||||
|
*/
|
||||||
|
protected abstract T finishWithCachedResult(Path cachedFile) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inform downloader that downloading finished.
|
||||||
|
*
|
||||||
|
* Check state.isFinished(), and do necessary cleaning if downloading failed.
|
||||||
|
*
|
||||||
|
* @return result of downloader.
|
||||||
|
* @throws IOException if an I/O error occurred.
|
||||||
|
*/
|
||||||
|
protected abstract T finish() throws IOException;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static abstract class DownloadTask<T> extends CompletableFutureTask<T> {
|
||||||
|
protected final DownloadTaskState state;
|
||||||
|
protected boolean caching = false;
|
||||||
|
private boolean doFinish = true;
|
||||||
|
protected CacheRepository repository = CacheRepository.getInstance();
|
||||||
|
private final CompletableFuture<T> future = new CompletableFuture<>();
|
||||||
|
private EnumCheckETag checkETag;
|
||||||
|
private Downloader<T> downloader;
|
||||||
|
|
||||||
|
public DownloadTask(DownloadTaskState state) {
|
||||||
|
this.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void setCaching(boolean caching) {
|
||||||
|
this.caching = caching;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void setCacheRepository(CacheRepository repository) {
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final DownloadTaskState getDownloadState() {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Downloader<T> createDownloader();
|
||||||
|
|
||||||
|
protected final EnumCheckETag getCheckETag() { return checkETag; }
|
||||||
|
|
||||||
|
protected synchronized final void setContentLength(int contentLength) throws IOException {
|
||||||
|
if (state.setContentLength(contentLength)) {
|
||||||
|
downloader.onContentLengthChanged(contentLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected synchronized final void finishWithCachedResult(Path cachedFile) throws IOException {
|
||||||
|
setResult(downloader.finishWithCachedResult(cachedFile));
|
||||||
|
|
||||||
|
state.finished = true;
|
||||||
|
doFinish = false;
|
||||||
|
future.complete(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized final void onSegmentFinished(DownloadSegment finishedSegment) {
|
||||||
|
assert(state.segments.contains(finishedSegment));
|
||||||
|
finishedSegment.finished = true;
|
||||||
|
|
||||||
|
int max = 0;
|
||||||
|
|
||||||
|
for (DownloadSegment segment : state.segments) {
|
||||||
|
if (segment.endPosition <= max) {
|
||||||
|
segment.setDownloaded();
|
||||||
|
}
|
||||||
|
if (!segment.isFinished())
|
||||||
|
return;
|
||||||
|
max = Math.max(max, segment.currentPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
// All segments have finished downloading.
|
||||||
|
state.finished = true;
|
||||||
|
future.complete(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized final void onSegmentFailed(DownloadSegment failedSegment, Throwable throwable) {
|
||||||
|
assert(state.segments.contains(failedSegment));
|
||||||
|
failedSegment.finished = true;
|
||||||
|
|
||||||
|
// All segments have finished downloading.
|
||||||
|
state.finished = true;
|
||||||
|
future.completeExceptionally(throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final CompletableFuture<T> getCompletableFuture() {
|
||||||
|
return CompletableFuture.runAsync(AsyncTaskExecutor.wrap(() -> {
|
||||||
|
downloader = createDownloader();
|
||||||
|
checkETag = downloader.shouldCheckETag();
|
||||||
|
|
||||||
|
downloader.onStart();
|
||||||
|
|
||||||
|
for (DownloadSegment segment : state.segments)
|
||||||
|
segment.download(this, downloader);
|
||||||
|
}))
|
||||||
|
.thenCompose(unused -> future)
|
||||||
|
.whenCompleteAsync((unused, exception) -> {
|
||||||
|
if (doFinish) {
|
||||||
|
try {
|
||||||
|
setResult(downloader.finish());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new CompletionException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AsyncTaskExecutor.rethrow(exception);
|
||||||
|
})
|
||||||
|
.thenApplyAsync(unused -> getResult());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected enum EnumCheckETag {
|
||||||
|
CHECK_E_TAG,
|
||||||
|
NOT_CHECK_E_TAG,
|
||||||
|
CACHED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static final class DownloadTaskState {
|
||||||
|
private final List<URL> urls;
|
||||||
|
@Nullable
|
||||||
|
private final Path file;
|
||||||
|
private final List<DownloadSegment> segments;
|
||||||
|
private URL fastestUrl;
|
||||||
|
private boolean segmentSupported = false;
|
||||||
|
private final int retry;
|
||||||
|
private int retryUrl = 0;
|
||||||
|
private boolean cancelled = false;
|
||||||
|
private boolean finished = false;
|
||||||
|
private int contentLength;
|
||||||
|
private final int initialParts;
|
||||||
|
private boolean waitingForContentLength;
|
||||||
|
|
||||||
|
private final SafeRegion connectionCheckRegion = new SafeRegion();
|
||||||
|
private final SafeRegion writeRegion = new SafeRegion();
|
||||||
|
|
||||||
|
DownloadTaskState(DownloadState state, @NotNull Path file, int retry) {
|
||||||
|
this.urls = new ArrayList<>(state.urls);
|
||||||
|
this.file = file;
|
||||||
|
this.retry = retry;
|
||||||
|
this.segments = new ArrayList<>(state.segments);
|
||||||
|
this.contentLength = state.getContentLength();
|
||||||
|
this.initialParts = state.getSegments().size();
|
||||||
|
this.waitingForContentLength = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadTaskState(List<URL> urls, @Nullable Path file, int retry, int contentLength, int initialParts) {
|
||||||
|
if (urls == null || urls.size() == 0) {
|
||||||
|
throw new IllegalArgumentException("DownloadTaskState requires at least one url candidate");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.urls = new ArrayList<>(urls);
|
||||||
|
this.file = file;
|
||||||
|
this.retry = retry;
|
||||||
|
this.initialParts = initialParts;
|
||||||
|
this.segments = new ArrayList<>(initialParts);
|
||||||
|
int partLength = contentLength / initialParts;
|
||||||
|
for (int i = 0; i < initialParts; i++) {
|
||||||
|
int begin = partLength * i;
|
||||||
|
int end = Math.min((partLength + 1) * i, contentLength);
|
||||||
|
segments.add(new DownloadSegment(i, begin, end, 0));
|
||||||
|
}
|
||||||
|
this.waitingForContentLength = contentLength == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DownloadTaskState newWithLengthUnknown(List<URL> urls, Path file, int retry, int initialParts) {
|
||||||
|
return new DownloadTaskState(urls, file, retry, 0, initialParts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized List<URL> getUrls() {
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path getFile() {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path getDownloadingFile() {
|
||||||
|
return file == null ? null : file.resolveSibling(FileUtils.getName(file) + ".download");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path getStateFile() {
|
||||||
|
return file == null ? null : file.resolveSibling(FileUtils.getName(file) + ".status");
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DownloadSegment> getSegments() {
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Figure out actually content length
|
||||||
|
* @param contentLength new content length
|
||||||
|
* @return if content length changed
|
||||||
|
*/
|
||||||
|
protected synchronized boolean setContentLength(int contentLength) {
|
||||||
|
if (!waitingForContentLength) {
|
||||||
|
throw new IllegalStateException("ContentLength already set");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.contentLength == contentLength) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
waitingForContentLength = false;
|
||||||
|
|
||||||
|
this.contentLength = contentLength;
|
||||||
|
if (contentLength < 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int partLength = contentLength / initialParts;
|
||||||
|
for (int i = 0; i < segments.size(); i++) {
|
||||||
|
int begin = partLength * i;
|
||||||
|
int end = Math.min(partLength * (i + 1), contentLength);
|
||||||
|
segments.get(i).setDownloadRange(begin, end);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized int getContentLength() {
|
||||||
|
return contentLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isWaitingForContentLength() {
|
||||||
|
return waitingForContentLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRetry() {
|
||||||
|
return retry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean isSegmentSupported() {
|
||||||
|
return segmentSupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setSegmentSupported(boolean segmentSupported) {
|
||||||
|
this.segmentSupported = segmentSupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized URL getFirstUrl() {
|
||||||
|
return urls.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean isFinished() {
|
||||||
|
return finished;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected synchronized void setFinished(boolean finished) {
|
||||||
|
this.finished = finished;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Next url for download runnable to retry.
|
||||||
|
*
|
||||||
|
* If some download runnable fails to connect to url, it will call this method
|
||||||
|
* to acquire next url for retry. Making all download runnable try different
|
||||||
|
* candidates concurrently to speed up finding fastest download source.
|
||||||
|
*
|
||||||
|
* If all URLs are tried and tested definitely negative for downloading,
|
||||||
|
* returns null.
|
||||||
|
*
|
||||||
|
* @return next url to retry
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public synchronized URL getNextUrlToRetry() {
|
||||||
|
if (retryUrl < 0 || retryUrl >= urls.size()) return null;
|
||||||
|
URL url = urls.get(retryUrl);
|
||||||
|
retryUrl = (retryUrl + 1) % urls.size();
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One candidate that is accessible and best network connection qualified.
|
||||||
|
*
|
||||||
|
* When some download runnable have started downloading, DownloadManager will
|
||||||
|
* monitor download speed and make failed download runnable connect to the
|
||||||
|
* fastest url directly without retry.
|
||||||
|
*
|
||||||
|
* In some times, the fastest url may be the first url suceeded in connection.
|
||||||
|
*
|
||||||
|
* @return fastest url, null if no url have successfully connected yet.
|
||||||
|
*/
|
||||||
|
public synchronized URL getFastestUrl() {
|
||||||
|
return fastestUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setFastestUrl(URL fastestUrl) {
|
||||||
|
if (this.fastestUrl == null)
|
||||||
|
this.fastestUrl = fastestUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void cancel() {
|
||||||
|
cancelled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean isCancelled() {
|
||||||
|
return cancelled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SafeRegion checkingConnection() {
|
||||||
|
connectionCheckRegion.begin();
|
||||||
|
return connectionCheckRegion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SafeRegion writing() {
|
||||||
|
writeRegion.begin();
|
||||||
|
return writeRegion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void forbidURL(URL url) {
|
||||||
|
int index;
|
||||||
|
while ((index = urls.indexOf(url)) != -1) {
|
||||||
|
if (retryUrl >= index) {
|
||||||
|
retryUrl--;
|
||||||
|
}
|
||||||
|
urls.remove(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.equals(fastestUrl)) {
|
||||||
|
fastestUrl = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static final class DownloadState {
|
||||||
|
private final List<URL> urls;
|
||||||
|
private final List<DownloadSegment> segments;
|
||||||
|
private final int contentLength;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for Gson
|
||||||
|
*/
|
||||||
|
public DownloadState() {
|
||||||
|
this(Collections.emptyList(), Collections.emptyList(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DownloadState(List<URL> urls, List<DownloadSegment> segments, int contentLength) {
|
||||||
|
this.urls = urls;
|
||||||
|
this.segments = segments;
|
||||||
|
this.contentLength = contentLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<URL> getUrls() {
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DownloadSegment> getSegments() {
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getContentLength() {
|
||||||
|
return contentLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static final class DownloadSegment {
|
||||||
|
private int index;
|
||||||
|
private int startPosition;
|
||||||
|
private int endPosition;
|
||||||
|
private int currentPosition;
|
||||||
|
private boolean finished;
|
||||||
|
private URLConnection connection;
|
||||||
|
private Future<?> future;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for Gson
|
||||||
|
*/
|
||||||
|
public DownloadSegment() {
|
||||||
|
this(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DownloadSegment(int index, int startPosition, int endPosition, int currentPosition) {
|
||||||
|
if (startPosition > endPosition) {
|
||||||
|
throw new IllegalArgumentException("Illegal download state: start " + startPosition + ", end " + endPosition + ", current " + currentPosition);
|
||||||
|
}
|
||||||
|
this.index = index;
|
||||||
|
this.startPosition = startPosition;
|
||||||
|
this.endPosition = endPosition;
|
||||||
|
this.currentPosition = currentPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getIndex() {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getStartPosition() {
|
||||||
|
return startPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getEndPosition() {
|
||||||
|
return endPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDownloadRange(int start, int end) {
|
||||||
|
this.startPosition = start;
|
||||||
|
this.endPosition = end;
|
||||||
|
this.currentPosition = start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current downloaded position
|
||||||
|
*
|
||||||
|
* CurrentPosition may be less than startPosition or larger than endPosition
|
||||||
|
* when segment unsupported.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public int getCurrentPosition() {
|
||||||
|
return currentPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDownloaded() {
|
||||||
|
this.currentPosition = endPosition - startPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrentPosition(int currentPosition) {
|
||||||
|
this.currentPosition = currentPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLength() {
|
||||||
|
return endPosition - startPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public URLConnection getConnection() {
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setConnection(URLConnection connection) {
|
||||||
|
this.connection = connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFinished() {
|
||||||
|
return finished || currentPosition >= endPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Future<?> download(DownloadTask<?> task, Downloader<?> downloader) {
|
||||||
|
if (future == null) {
|
||||||
|
future = DownloadManager.download().submit(new DownloadSegmentTask(task, downloader, this));
|
||||||
|
}
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "DownloadSegment{" +
|
||||||
|
"startPosition=" + startPosition +
|
||||||
|
", endPosition=" + endPosition +
|
||||||
|
", hash=" + hashCode() +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Timer timer = new Timer("DownloadSpeedRecorder", true);
|
||||||
|
private static final AtomicInteger downloadSpeed = new AtomicInteger(0);
|
||||||
|
public static final EventBus speedEvent = new EventBus();
|
||||||
|
|
||||||
|
static {
|
||||||
|
timer.schedule(new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
speedEvent.channel(SpeedEvent.class).fireEvent(new SpeedEvent(speedEvent, downloadSpeed.getAndSet(0)));
|
||||||
|
}
|
||||||
|
}, 0, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void updateDownloadSpeed(int speed) {
|
||||||
|
downloadSpeed.addAndGet(speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SpeedEvent extends Event {
|
||||||
|
private final int speed;
|
||||||
|
|
||||||
|
public SpeedEvent(Object source, int speed) {
|
||||||
|
super(source);
|
||||||
|
|
||||||
|
this.speed = speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download speed in byte/sec.
|
||||||
|
* @return download speed
|
||||||
|
*/
|
||||||
|
public int getSpeed() {
|
||||||
|
return speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return new ToStringBuilder(this).append("speed", speed).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int downloadExecutorConcurrency = Math.min(Runtime.getRuntime().availableProcessors() * 4, 64);
|
||||||
|
private static volatile ThreadPoolExecutor DOWNLOAD_EXECUTOR;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get singleton instance of the thread pool for file downloading.
|
||||||
|
*
|
||||||
|
* @return Thread pool for FetchTask
|
||||||
|
*/
|
||||||
|
protected static ExecutorService download() {
|
||||||
|
if (DOWNLOAD_EXECUTOR == null) {
|
||||||
|
synchronized (Schedulers.class) {
|
||||||
|
if (DOWNLOAD_EXECUTOR == null) {
|
||||||
|
DOWNLOAD_EXECUTOR = new ThreadPoolExecutor(0, downloadExecutorConcurrency, 10, TimeUnit.SECONDS, new SynchronousQueue<>(),
|
||||||
|
runnable -> {
|
||||||
|
Thread thread = Executors.defaultThreadFactory().newThread(runnable);
|
||||||
|
thread.setDaemon(true);
|
||||||
|
return thread;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DOWNLOAD_EXECUTOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setDownloadExecutorConcurrency(int concurrency) {
|
||||||
|
synchronized (Schedulers.class) {
|
||||||
|
downloadExecutorConcurrency = concurrency;
|
||||||
|
if (DOWNLOAD_EXECUTOR != null) {
|
||||||
|
DOWNLOAD_EXECUTOR.setMaximumPoolSize(concurrency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getDownloadExecutorConcurrency() {
|
||||||
|
synchronized (Schedulers.class) {
|
||||||
|
return downloadExecutorConcurrency;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
/*
|
||||||
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2020 huangyuhui <[email protected]> and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.jackhuang.hmcl.task;
|
||||||
|
|
||||||
|
import org.jackhuang.hmcl.util.Logging;
|
||||||
|
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||||
|
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||||
|
import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||||
|
|
||||||
|
class DownloadSegmentTask implements Runnable {
|
||||||
|
|
||||||
|
private final DownloadManager.DownloadTask<?> task;
|
||||||
|
private final DownloadManager.Downloader<?> downloader;
|
||||||
|
private final DownloadManager.DownloadTaskState state;
|
||||||
|
private int tryTime = 0;
|
||||||
|
private URL lastURL;
|
||||||
|
private DownloadManager.DownloadSegment segment;
|
||||||
|
|
||||||
|
public DownloadSegmentTask(DownloadManager.DownloadTask<?> task, DownloadManager.Downloader<?> downloader, DownloadManager.DownloadSegment segment) {
|
||||||
|
this.task = task;
|
||||||
|
this.downloader = downloader;
|
||||||
|
this.state = task.getDownloadState();
|
||||||
|
this.segment = segment;
|
||||||
|
}
|
||||||
|
|
||||||
|
private URLConnection createConnection(URL url, int startPosition, int endPosition) throws IOException {
|
||||||
|
URLConnection conn = NetworkUtils.createConnection(url, 4000);
|
||||||
|
if (startPosition != endPosition) {
|
||||||
|
conn.setRequestProperty("Range", "bytes=" + startPosition + "-" + (endPosition - 1));
|
||||||
|
}
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private URLConnection createConnection(boolean retryLastConnection, int startPosition, int endPosition) throws IOException {
|
||||||
|
if (retryLastConnection && lastURL != null) {
|
||||||
|
return createConnection(lastURL, startPosition, endPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. If we don't know content length now, DownloadSegmentTasks should try
|
||||||
|
// different candidates.
|
||||||
|
// Ensure first segment always try url with highest priority first.
|
||||||
|
if (state.isWaitingForContentLength() && segment.getIndex() != 0) {
|
||||||
|
URL nextUrlToRetry = state.getNextUrlToRetry();
|
||||||
|
if (nextUrlToRetry == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
lastURL = nextUrlToRetry;
|
||||||
|
tryTime++;
|
||||||
|
return createConnection(lastURL, startPosition, endPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. try suggested URL at the first time
|
||||||
|
if (tryTime == 0) {
|
||||||
|
lastURL = state.getFirstUrl();
|
||||||
|
tryTime++;
|
||||||
|
return createConnection(lastURL, startPosition, endPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. try fastest URL if measured
|
||||||
|
URL fastestURL = state.getFastestUrl();
|
||||||
|
if (fastestURL != null) {
|
||||||
|
lastURL = fastestURL;
|
||||||
|
return createConnection(lastURL, startPosition, endPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tryTime >= state.getRetry()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. try other URL, DownloadTaskState will make all DownloadSegmentTask
|
||||||
|
// try different URL to speed up connection.
|
||||||
|
URL nextURLToTry = state.getNextUrlToRetry();
|
||||||
|
if (nextURLToTry == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
tryTime++;
|
||||||
|
lastURL = nextURLToTry;
|
||||||
|
return createConnection(lastURL, startPosition, endPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
Exception exception = null;
|
||||||
|
URL failedURL = null;
|
||||||
|
boolean checkETag;
|
||||||
|
switch (task.getCheckETag()) {
|
||||||
|
case CHECK_E_TAG:
|
||||||
|
checkETag = true;
|
||||||
|
break;
|
||||||
|
case NOT_CHECK_E_TAG:
|
||||||
|
checkETag = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean retryLastConnection = false;
|
||||||
|
loop: while (true) {
|
||||||
|
if (state.isCancelled() || state.isFinished()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
boolean detectRange = state.isWaitingForContentLength();
|
||||||
|
boolean connectionSegmented = false;
|
||||||
|
URLConnection conn = createConnection(retryLastConnection, segment.getStartPosition(), segment.getEndPosition());
|
||||||
|
if (conn == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
URL url = conn.getURL();
|
||||||
|
|
||||||
|
if (checkETag) task.repository.injectConnection(conn);
|
||||||
|
|
||||||
|
LOG.log(Level.INFO, "URL " + url + " " + this);
|
||||||
|
downloader.onBeforeConnection(segment, lastURL);
|
||||||
|
|
||||||
|
if (conn instanceof HttpURLConnection) {
|
||||||
|
conn = NetworkUtils.resolveConnection((HttpURLConnection) conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
try (DownloadManager.SafeRegion region = state.checkingConnection()) {
|
||||||
|
// If other DownloadSegmentTask finishedWithCachedResult
|
||||||
|
// then this task should stop.
|
||||||
|
if (state.isFinished()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conn instanceof HttpURLConnection) {
|
||||||
|
int responseCode = ((HttpURLConnection) conn).getResponseCode();
|
||||||
|
|
||||||
|
if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
|
||||||
|
// Handle cache
|
||||||
|
try {
|
||||||
|
Path cache = task.repository.getCachedRemoteFile(conn);
|
||||||
|
task.finishWithCachedResult(cache);
|
||||||
|
return;
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.log(Level.WARNING, "Unable to use cached file, re-download " + lastURL, e);
|
||||||
|
task.repository.removeRemoteEntry(conn);
|
||||||
|
// Now we must reconnect the server since 304 may result in empty content,
|
||||||
|
// if we want to re-download the file, we must reconnect the server without etag settings.
|
||||||
|
retryLastConnection = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if (responseCode / 100 == 4) {
|
||||||
|
// 404 may occurs when we hit some mirror that does not have the file,
|
||||||
|
// but other mirrors may have, so we should try other URLs.
|
||||||
|
state.forbidURL(lastURL);
|
||||||
|
continue;
|
||||||
|
} else if (responseCode / 100 != 2) {
|
||||||
|
throw new ResponseCodeException(lastURL, responseCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: maybe some server supports partial content, other servers does not support,
|
||||||
|
// there should be a way to pick fastest server.
|
||||||
|
if (conn.getHeaderField("Content-Range") != null && responseCode == 206) {
|
||||||
|
state.setSegmentSupported(true);
|
||||||
|
connectionSegmented = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.getContentLength() == 0) {
|
||||||
|
task.setContentLength(conn.getContentLength());
|
||||||
|
}
|
||||||
|
|
||||||
|
int expectedLength = connectionSegmented ? segment.getLength() : state.getContentLength();
|
||||||
|
if (expectedLength != conn.getContentLength()) {
|
||||||
|
// If content length is not expected, forbids this URL
|
||||||
|
LOG.warning("Content length mismatch " + segment + ", expected: " + expectedLength + ", actual: " + conn.getContentLength());
|
||||||
|
state.forbidURL(lastURL);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Currently we mark first successfully connected URL as "fastest" URL.
|
||||||
|
state.setFastestUrl(conn.getURL());
|
||||||
|
|
||||||
|
if (!state.isSegmentSupported() && segment.getStartPosition() != 0) {
|
||||||
|
// Now we have not figured if URL supports segment downloading,
|
||||||
|
// and have successfully fetched content length.
|
||||||
|
// We should check states of non-first DownloadSegmentTasks.
|
||||||
|
// First DownloadSegmentTask will continue downloading whatever segment is supported or not.
|
||||||
|
if (detectRange) {
|
||||||
|
// If this DownloadSegmentTask detects content length,
|
||||||
|
// reconnect to same URL with header Range, detecting if segment supported.
|
||||||
|
retryLastConnection = true;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
// We already tested Range and found segment not supported.
|
||||||
|
// Make only first DownloadSegmentTask continue.
|
||||||
|
task.onSegmentFinished(segment);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try (InputStream stream = conn.getInputStream()) {
|
||||||
|
int startPosition, lastPosition, position;
|
||||||
|
position = lastPosition = startPosition = connectionSegmented ? segment.getStartPosition() : 0;
|
||||||
|
segment.setCurrentPosition(startPosition);
|
||||||
|
byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
|
||||||
|
while (true) {
|
||||||
|
if (state.isCancelled()) break;
|
||||||
|
|
||||||
|
// For first DownloadSegmentTask, if other DownloadSegmentTask have figured out
|
||||||
|
// that segment is supported, and this segment have already be finished,
|
||||||
|
// stop downloading.
|
||||||
|
if (state.isSegmentSupported() && segment.isFinished()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If some non-first segment started downloading without segment,
|
||||||
|
// stop it.
|
||||||
|
if (state.isSegmentSupported() && position < segment.getStartPosition() && segment.getIndex() != 0) {
|
||||||
|
continue loop;
|
||||||
|
}
|
||||||
|
|
||||||
|
int len = stream.read(buffer);
|
||||||
|
if (len == -1) break;
|
||||||
|
|
||||||
|
try (DownloadManager.SafeRegion region = state.writing()) {
|
||||||
|
System.err.println("Write " + url + " segment " + segment + ",pos=" + position + ",len=" + len + ", segmented?=" + connectionSegmented);
|
||||||
|
downloader.write(position, buffer, 0, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
position += len;
|
||||||
|
|
||||||
|
if (conn.getContentLength() >= 0) {
|
||||||
|
// Update progress information per second
|
||||||
|
segment.setCurrentPosition(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadManager.updateDownloadSpeed(position - lastPosition);
|
||||||
|
lastPosition = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.isCancelled()) break;
|
||||||
|
|
||||||
|
if (conn.getContentLength() >= 0 && !segment.isFinished())
|
||||||
|
throw new IOException("Unexpected segment size: " + (position - startPosition) + ", expected: " + segment.getLength());
|
||||||
|
}
|
||||||
|
|
||||||
|
segment.setConnection(conn);
|
||||||
|
task.onSegmentFinished(segment);
|
||||||
|
|
||||||
|
return;
|
||||||
|
} catch (IOException ex) {
|
||||||
|
failedURL = lastURL;
|
||||||
|
exception = ex;
|
||||||
|
LOG.log(Level.WARNING, "Failed to download " + failedURL + ", repeat times: " + tryTime, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exception != null)
|
||||||
|
throw new DownloadException(failedURL, exception);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
task.onSegmentFailed(segment, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
/*
|
|
||||||
* Hello Minecraft! Launcher
|
|
||||||
* Copyright (C) 2020 huangyuhui <[email protected]> and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package org.jackhuang.hmcl.task;
|
|
||||||
|
|
||||||
import org.jackhuang.hmcl.event.Event;
|
|
||||||
import org.jackhuang.hmcl.event.EventBus;
|
|
||||||
import org.jackhuang.hmcl.util.CacheRepository;
|
|
||||||
import org.jackhuang.hmcl.util.Logging;
|
|
||||||
import org.jackhuang.hmcl.util.ToStringBuilder;
|
|
||||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
|
||||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
|
||||||
import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
|
||||||
|
|
||||||
import java.io.Closeable;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.net.URLConnection;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
import java.util.logging.Level;
|
|
||||||
|
|
||||||
public abstract class FetchTask<T> extends Task<T> {
|
|
||||||
protected final List<URL> urls;
|
|
||||||
protected final int retry;
|
|
||||||
protected boolean caching;
|
|
||||||
protected CacheRepository repository = CacheRepository.getInstance();
|
|
||||||
|
|
||||||
public FetchTask(List<URL> urls, int retry) {
|
|
||||||
if (urls == null || urls.isEmpty())
|
|
||||||
throw new IllegalArgumentException("At least one URL is required");
|
|
||||||
|
|
||||||
this.urls = new ArrayList<>(urls);
|
|
||||||
this.retry = retry;
|
|
||||||
|
|
||||||
setExecutor(Schedulers.io());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCaching(boolean caching) {
|
|
||||||
this.caching = caching;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCacheRepository(CacheRepository repository) {
|
|
||||||
this.repository = repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void beforeDownload(URL url) throws IOException {}
|
|
||||||
|
|
||||||
protected abstract void useCachedResult(Path cachedFile) throws IOException;
|
|
||||||
|
|
||||||
protected abstract EnumCheckETag shouldCheckETag();
|
|
||||||
|
|
||||||
protected abstract Context getContext(URLConnection conn, boolean checkETag) throws IOException;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute() throws Exception {
|
|
||||||
Exception exception = null;
|
|
||||||
URL failedURL = null;
|
|
||||||
boolean checkETag;
|
|
||||||
switch (shouldCheckETag()) {
|
|
||||||
case CHECK_E_TAG: checkETag = true; break;
|
|
||||||
case NOT_CHECK_E_TAG: checkETag = false; break;
|
|
||||||
default: return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int repeat = 0;
|
|
||||||
download: for (URL url : urls) {
|
|
||||||
for (int retryTime = 0; retryTime < retry; retryTime++) {
|
|
||||||
if (isCancelled()) {
|
|
||||||
break download;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
beforeDownload(url);
|
|
||||||
|
|
||||||
updateProgress(0);
|
|
||||||
|
|
||||||
URLConnection conn = NetworkUtils.createConnection(url);
|
|
||||||
if (checkETag) repository.injectConnection(conn);
|
|
||||||
|
|
||||||
if (conn instanceof HttpURLConnection) {
|
|
||||||
conn = NetworkUtils.resolveConnection((HttpURLConnection) conn);
|
|
||||||
int responseCode = ((HttpURLConnection) conn).getResponseCode();
|
|
||||||
|
|
||||||
if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
|
|
||||||
// Handle cache
|
|
||||||
try {
|
|
||||||
Path cache = repository.getCachedRemoteFile(conn);
|
|
||||||
useCachedResult(cache);
|
|
||||||
return;
|
|
||||||
} catch (IOException e) {
|
|
||||||
Logging.LOG.log(Level.WARNING, "Unable to use cached file, redownload " + url, e);
|
|
||||||
repository.removeRemoteEntry(conn);
|
|
||||||
// Now we must reconnect the server since 304 may result in empty content,
|
|
||||||
// if we want to redownload the file, we must reconnect the server without etag settings.
|
|
||||||
retryTime--;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else if (responseCode / 100 == 4) {
|
|
||||||
break; // we will not try this URL again
|
|
||||||
} else if (responseCode / 100 != 2) {
|
|
||||||
throw new ResponseCodeException(url, responseCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
long contentLength = conn.getContentLength();
|
|
||||||
try (Context context = getContext(conn, checkETag); InputStream stream = conn.getInputStream()) {
|
|
||||||
int lastDownloaded = 0, downloaded = 0;
|
|
||||||
byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
|
|
||||||
while (true) {
|
|
||||||
if (isCancelled()) break;
|
|
||||||
|
|
||||||
int len = stream.read(buffer);
|
|
||||||
if (len == -1) break;
|
|
||||||
|
|
||||||
context.write(buffer, 0, len);
|
|
||||||
|
|
||||||
downloaded += len;
|
|
||||||
|
|
||||||
if (contentLength >= 0) {
|
|
||||||
// Update progress information per second
|
|
||||||
updateProgress(downloaded, contentLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDownloadSpeed(downloaded - lastDownloaded);
|
|
||||||
lastDownloaded = downloaded;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCancelled()) break download;
|
|
||||||
|
|
||||||
updateDownloadSpeed(downloaded - lastDownloaded);
|
|
||||||
|
|
||||||
if (contentLength >= 0 && downloaded != contentLength)
|
|
||||||
throw new IOException("Unexpected file size: " + downloaded + ", expected: " + contentLength);
|
|
||||||
|
|
||||||
context.withResult(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
} catch (IOException ex) {
|
|
||||||
failedURL = url;
|
|
||||||
exception = ex;
|
|
||||||
Logging.LOG.log(Level.WARNING, "Failed to download " + url + ", repeat times: " + (++repeat), ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exception != null)
|
|
||||||
throw new DownloadException(failedURL, exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final Timer timer = new Timer("DownloadSpeedRecorder", true);
|
|
||||||
private static final AtomicInteger downloadSpeed = new AtomicInteger(0);
|
|
||||||
public static final EventBus speedEvent = new EventBus();
|
|
||||||
|
|
||||||
static {
|
|
||||||
timer.schedule(new TimerTask() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
speedEvent.channel(SpeedEvent.class).fireEvent(new SpeedEvent(speedEvent, downloadSpeed.getAndSet(0)));
|
|
||||||
}
|
|
||||||
}, 0, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void updateDownloadSpeed(int speed) {
|
|
||||||
downloadSpeed.addAndGet(speed);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class SpeedEvent extends Event {
|
|
||||||
private final int speed;
|
|
||||||
|
|
||||||
public SpeedEvent(Object source, int speed) {
|
|
||||||
super(source);
|
|
||||||
|
|
||||||
this.speed = speed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download speed in byte/sec.
|
|
||||||
* @return download speed
|
|
||||||
*/
|
|
||||||
public int getSpeed() {
|
|
||||||
return speed;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return new ToStringBuilder(this).append("speed", speed).toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static abstract class Context implements Closeable {
|
|
||||||
private boolean success;
|
|
||||||
|
|
||||||
public abstract void write(byte[] buffer, int offset, int len) throws IOException;
|
|
||||||
|
|
||||||
public final void withResult(boolean success) {
|
|
||||||
this.success = success;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean isSuccess() {
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected enum EnumCheckETag {
|
|
||||||
CHECK_E_TAG,
|
|
||||||
NOT_CHECK_E_TAG,
|
|
||||||
CACHED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,18 +17,15 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.task;
|
package org.jackhuang.hmcl.task;
|
||||||
|
|
||||||
|
import org.jackhuang.hmcl.util.DigestUtils;
|
||||||
import org.jackhuang.hmcl.util.Logging;
|
import org.jackhuang.hmcl.util.Logging;
|
||||||
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
|
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
|
||||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.*;
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.RandomAccessFile;
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLConnection;
|
|
||||||
import java.nio.file.FileSystem;
|
import java.nio.file.FileSystem;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -44,11 +41,11 @@ import static org.jackhuang.hmcl.util.DigestUtils.getDigest;
|
|||||||
*
|
*
|
||||||
* @author huangyuhui
|
* @author huangyuhui
|
||||||
*/
|
*/
|
||||||
public class FileDownloadTask extends FetchTask<Void> {
|
public class FileDownloadTask extends DownloadManager.DownloadTask<Void> {
|
||||||
|
|
||||||
public static class IntegrityCheck {
|
public static class IntegrityCheck {
|
||||||
private String algorithm;
|
private final String algorithm;
|
||||||
private String checksum;
|
private final String checksum;
|
||||||
|
|
||||||
public IntegrityCheck(String algorithm, String checksum) {
|
public IntegrityCheck(String algorithm, String checksum) {
|
||||||
this.algorithm = requireNonNull(algorithm);
|
this.algorithm = requireNonNull(algorithm);
|
||||||
@@ -80,15 +77,17 @@ public class FileDownloadTask extends FetchTask<Void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final File file;
|
|
||||||
private final IntegrityCheck integrityCheck;
|
private final IntegrityCheck integrityCheck;
|
||||||
private Path candidate;
|
private Path candidate;
|
||||||
|
// destination
|
||||||
|
private final Path file;
|
||||||
|
// temporary location to save remote file, which will be removed after downloading finished.
|
||||||
|
private final Path downloadingFile;
|
||||||
private RandomAccessFile rFile;
|
private RandomAccessFile rFile;
|
||||||
private InputStream stream;
|
|
||||||
private final ArrayList<IntegrityCheckHandler> integrityCheckHandlers = new ArrayList<>();
|
private final ArrayList<IntegrityCheckHandler> integrityCheckHandlers = new ArrayList<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param url the URL of remote file.
|
* @param url the URL of remote file.
|
||||||
* @param file the location that download to.
|
* @param file the location that download to.
|
||||||
*/
|
*/
|
||||||
public FileDownloadTask(URL url, File file) {
|
public FileDownloadTask(URL url, File file) {
|
||||||
@@ -96,8 +95,8 @@ public class FileDownloadTask extends FetchTask<Void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param url the URL of remote file.
|
* @param url the URL of remote file.
|
||||||
* @param file the location that download to.
|
* @param file the location that download to.
|
||||||
* @param integrityCheck the integrity check to perform, null if no integrity check is to be performed
|
* @param integrityCheck the integrity check to perform, null if no integrity check is to be performed
|
||||||
*/
|
*/
|
||||||
public FileDownloadTask(URL url, File file, IntegrityCheck integrityCheck) {
|
public FileDownloadTask(URL url, File file, IntegrityCheck integrityCheck) {
|
||||||
@@ -105,10 +104,10 @@ public class FileDownloadTask extends FetchTask<Void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param url the URL of remote file.
|
* @param url the URL of remote file.
|
||||||
* @param file the location that download to.
|
* @param file the location that download to.
|
||||||
* @param integrityCheck the integrity check to perform, null if no integrity check is to be performed
|
* @param integrityCheck the integrity check to perform, null if no integrity check is to be performed
|
||||||
* @param retry the times for retrying if downloading fails.
|
* @param retry the times for retrying if downloading fails.
|
||||||
*/
|
*/
|
||||||
public FileDownloadTask(URL url, File file, IntegrityCheck integrityCheck, int retry) {
|
public FileDownloadTask(URL url, File file, IntegrityCheck integrityCheck, int retry) {
|
||||||
this(Collections.singletonList(url), file, integrityCheck, retry);
|
this(Collections.singletonList(url), file, integrityCheck, retry);
|
||||||
@@ -116,6 +115,7 @@ public class FileDownloadTask extends FetchTask<Void> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
|
*
|
||||||
* @param urls urls of remote file, will be attempted in order.
|
* @param urls urls of remote file, will be attempted in order.
|
||||||
* @param file the location that download to.
|
* @param file the location that download to.
|
||||||
*/
|
*/
|
||||||
@@ -125,8 +125,9 @@ public class FileDownloadTask extends FetchTask<Void> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
* @param urls urls of remote file, will be attempted in order.
|
*
|
||||||
* @param file the location that download to.
|
* @param urls urls of remote file, will be attempted in order.
|
||||||
|
* @param file the location that download to.
|
||||||
* @param integrityCheck the integrity check to perform, null if no integrity check is to be performed
|
* @param integrityCheck the integrity check to perform, null if no integrity check is to be performed
|
||||||
*/
|
*/
|
||||||
public FileDownloadTask(List<URL> urls, File file, IntegrityCheck integrityCheck) {
|
public FileDownloadTask(List<URL> urls, File file, IntegrityCheck integrityCheck) {
|
||||||
@@ -135,26 +136,44 @@ public class FileDownloadTask extends FetchTask<Void> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
* @param urls urls of remote file, will be attempted in order.
|
*
|
||||||
* @param file the location that download to.
|
* @param urls urls of remote file, will be attempted in order.
|
||||||
|
* @param file the location that download to.
|
||||||
* @param integrityCheck the integrity check to perform, null if no integrity check is to be performed
|
* @param integrityCheck the integrity check to perform, null if no integrity check is to be performed
|
||||||
* @param retry the times for retrying if downloading fails.
|
* @param retry the times for retrying if downloading fails.
|
||||||
*/
|
*/
|
||||||
public FileDownloadTask(List<URL> urls, File file, IntegrityCheck integrityCheck, int retry) {
|
public FileDownloadTask(List<URL> urls, File file, IntegrityCheck integrityCheck, int retry) {
|
||||||
super(urls, retry);
|
this(new DownloadManager.DownloadTaskStateBuilder()
|
||||||
this.file = file;
|
.setUrls(urls).setFile(file).setRetry(retry).build(), integrityCheck);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileDownloadTask(DownloadManager.DownloadTaskState state) {
|
||||||
|
this(state, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileDownloadTask(DownloadManager.DownloadTaskState state, IntegrityCheck integrityCheck) {
|
||||||
|
super(state);
|
||||||
|
|
||||||
|
if (state.getFile() == null) {
|
||||||
|
throw new IllegalArgumentException("FileDownloadTask requires a file path to save remote file");
|
||||||
|
}
|
||||||
|
|
||||||
this.integrityCheck = integrityCheck;
|
this.integrityCheck = integrityCheck;
|
||||||
|
this.file = state.getFile();
|
||||||
|
this.downloadingFile = state.getDownloadingFile();
|
||||||
|
|
||||||
setName(file.getName());
|
setName(FileUtils.getName(state.getFile()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public File getFile() {
|
/**
|
||||||
return file;
|
* Set candidate that the content should equal to the file to be downloaded.
|
||||||
}
|
* <p>
|
||||||
|
* If candidate set and verified, the file will be taken, and download will not happen.
|
||||||
public FileDownloadTask setCandidate(Path candidate) {
|
*
|
||||||
|
* @param candidate path to candidate
|
||||||
|
*/
|
||||||
|
public void setCandidate(Path candidate) {
|
||||||
this.candidate = candidate;
|
this.candidate = candidate;
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addIntegrityCheckHandler(IntegrityCheckHandler handler) {
|
public void addIntegrityCheckHandler(IntegrityCheckHandler handler) {
|
||||||
@@ -162,98 +181,110 @@ public class FileDownloadTask extends FetchTask<Void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected EnumCheckETag shouldCheckETag() {
|
protected DownloadManager.Downloader<Void> createDownloader() {
|
||||||
// Check cache
|
return new DownloadManager.Downloader<Void>() {
|
||||||
if (integrityCheck != null && caching) {
|
@Override
|
||||||
Optional<Path> cache = repository.checkExistentFile(candidate, integrityCheck.getAlgorithm(), integrityCheck.getChecksum());
|
protected EnumCheckETag shouldCheckETag() {
|
||||||
if (cache.isPresent()) {
|
// Check cache
|
||||||
try {
|
if (integrityCheck != null && caching) {
|
||||||
FileUtils.copyFile(cache.get().toFile(), file);
|
Optional<Path> cache = repository.checkExistentFile(candidate, integrityCheck.getAlgorithm(), integrityCheck.getChecksum());
|
||||||
Logging.LOG.log(Level.FINER, "Successfully verified file " + file + " from " + urls.get(0));
|
if (cache.isPresent()) {
|
||||||
return EnumCheckETag.CACHED;
|
try {
|
||||||
} catch (IOException e) {
|
FileUtils.copyFile(cache.get(), state.getFile());
|
||||||
Logging.LOG.log(Level.WARNING, "Failed to copy cache files", e);
|
Logging.LOG.log(Level.FINER, "Successfully verified file " + state.getFile() + " from " + state.getFirstUrl());
|
||||||
|
return EnumCheckETag.CACHED;
|
||||||
|
} catch (IOException e) {
|
||||||
|
Logging.LOG.log(Level.WARNING, "Failed to copy cache files", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return EnumCheckETag.NOT_CHECK_E_TAG;
|
||||||
|
} else {
|
||||||
|
return EnumCheckETag.CHECK_E_TAG;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return EnumCheckETag.NOT_CHECK_E_TAG;
|
|
||||||
} else {
|
|
||||||
return EnumCheckETag.CHECK_E_TAG;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void beforeDownload(URL url) {
|
|
||||||
Logging.LOG.log(Level.FINER, "Downloading " + url + " to " + file);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void useCachedResult(Path cache) throws IOException {
|
|
||||||
FileUtils.copyFile(cache.toFile(), file);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Context getContext(URLConnection conn, boolean checkETag) throws IOException {
|
|
||||||
Path temp = Files.createTempFile(null, null);
|
|
||||||
RandomAccessFile rFile = new RandomAccessFile(temp.toFile(), "rw");
|
|
||||||
MessageDigest digest = integrityCheck == null ? null : integrityCheck.createDigest();
|
|
||||||
|
|
||||||
return new Context() {
|
|
||||||
@Override
|
@Override
|
||||||
public void write(byte[] buffer, int offset, int len) throws IOException {
|
protected void onBeforeConnection(DownloadManager.DownloadSegment segment, URL url) {
|
||||||
if (digest != null) {
|
Logging.LOG.log(Level.FINER, "Downloading segment " + segment.getStartPosition() + "~" + segment.getEndPosition() + " of " + url + " to " + state.getFile());
|
||||||
digest.update(buffer, offset, len);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Void finishWithCachedResult(Path cache) throws IOException {
|
||||||
|
FileUtils.copyFile(cache, state.getFile());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void write(long pos, byte[] buffer, int offset, int len) throws IOException {
|
||||||
|
rFile.seek(pos);
|
||||||
rFile.write(buffer, offset, len);
|
rFile.write(buffer, offset, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws IOException {
|
protected void onStart() throws IOException {
|
||||||
|
rFile = new RandomAccessFile(downloadingFile.toFile(), "rw");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onContentLengthChanged(long contentLength) throws IOException {
|
||||||
|
if (contentLength > 0) {
|
||||||
|
rFile.setLength(contentLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Void finish() throws IOException {
|
||||||
try {
|
try {
|
||||||
rFile.close();
|
rFile.close();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Logging.LOG.log(Level.WARNING, "Failed to close file: " + rFile, e);
|
Logging.LOG.log(Level.WARNING, "Failed to close file: " + rFile, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isSuccess()) {
|
if (!state.isFinished()) {
|
||||||
try {
|
try {
|
||||||
Files.delete(temp);
|
Files.delete(downloadingFile);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Logging.LOG.log(Level.WARNING, "Failed to delete file: " + rFile, e);
|
Logging.LOG.log(Level.WARNING, "Failed to delete file: " + rFile, e);
|
||||||
}
|
}
|
||||||
return;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
for (IntegrityCheckHandler handler : integrityCheckHandlers) {
|
|
||||||
handler.checkIntegrity(temp, file.toPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
Files.deleteIfExists(file.toPath());
|
|
||||||
if (!FileUtils.makeDirectory(file.getAbsoluteFile().getParentFile()))
|
|
||||||
throw new IOException("Unable to make parent directory " + file);
|
|
||||||
|
|
||||||
try {
|
|
||||||
FileUtils.moveFile(temp.toFile(), file);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new IOException("Unable to move temp file from " + temp + " to " + file, e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Integrity check
|
// Integrity check
|
||||||
if (integrityCheck != null) {
|
if (integrityCheck != null) {
|
||||||
integrityCheck.performCheck(digest);
|
try (InputStream is = Files.newInputStream(downloadingFile)) {
|
||||||
}
|
MessageDigest digest = integrityCheck.createDigest();
|
||||||
|
DigestUtils.updateDigest(digest, is);
|
||||||
|
integrityCheck.performCheck(digest);
|
||||||
|
}
|
||||||
|
|
||||||
if (caching && integrityCheck != null) {
|
if (caching) {
|
||||||
try {
|
try {
|
||||||
repository.cacheFile(file.toPath(), integrityCheck.getAlgorithm(), integrityCheck.getChecksum());
|
repository.cacheFile(downloadingFile, integrityCheck.getAlgorithm(), integrityCheck.getChecksum());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Logging.LOG.log(Level.WARNING, "Failed to cache file", e);
|
Logging.LOG.log(Level.WARNING, "Failed to cache file", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (checkETag) {
|
for (IntegrityCheckHandler handler : integrityCheckHandlers) {
|
||||||
repository.cacheRemoteFile(file.toPath(), conn);
|
handler.checkIntegrity(downloadingFile, state.getFile());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Files.deleteIfExists(file);
|
||||||
|
if (!FileUtils.makeDirectory(file.toAbsolutePath().getParent().toFile()))
|
||||||
|
throw new IOException("Unable to make parent directory " + state.getFile());
|
||||||
|
|
||||||
|
try {
|
||||||
|
FileUtils.moveFile(downloadingFile.toFile(), file.toFile());
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IOException("Unable to move temp file from " + state.getDownloadingFile() + " to " + file, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getCheckETag() == EnumCheckETag.CHECK_E_TAG) {
|
||||||
|
repository.cacheRemoteFile(file, state.getSegments().get(0).getConnection());
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -261,7 +292,8 @@ public class FileDownloadTask extends FetchTask<Void> {
|
|||||||
public interface IntegrityCheckHandler {
|
public interface IntegrityCheckHandler {
|
||||||
/**
|
/**
|
||||||
* Check whether the file is corrupted or not.
|
* Check whether the file is corrupted or not.
|
||||||
* @param filePath the file locates in (maybe in temp directory)
|
*
|
||||||
|
* @param filePath the file locates in (maybe in temp directory)
|
||||||
* @param destinationPath for real file name
|
* @param destinationPath for real file name
|
||||||
* @throws IOException if the file is corrupted
|
* @throws IOException if the file is corrupted
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -21,19 +21,25 @@ import org.jackhuang.hmcl.util.io.FileUtils;
|
|||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLConnection;
|
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.*;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author huangyuhui
|
* @author huangyuhui
|
||||||
*/
|
*/
|
||||||
public final class GetTask extends FetchTask<String> {
|
public final class GetTask extends DownloadManager.DownloadTask<String> {
|
||||||
|
|
||||||
private final Charset charset;
|
private final Charset charset;
|
||||||
|
|
||||||
@@ -54,42 +60,165 @@ public final class GetTask extends FetchTask<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public GetTask(List<URL> urls, Charset charset, int retry) {
|
public GetTask(List<URL> urls, Charset charset, int retry) {
|
||||||
super(urls, retry);
|
this(new DownloadManager.DownloadTaskStateBuilder()
|
||||||
|
.setUrls(urls).setRetry(retry).build(), charset);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GetTask(DownloadManager.DownloadTaskState state) {
|
||||||
|
this(state, UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GetTask(DownloadManager.DownloadTaskState state, Charset charset) {
|
||||||
|
super(state);
|
||||||
this.charset = charset;
|
this.charset = charset;
|
||||||
|
|
||||||
setName(urls.get(0).toString());
|
setName(state.getFirstUrl().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ByteArrayList {
|
||||||
|
protected byte[] buf;
|
||||||
|
|
||||||
|
protected int count;
|
||||||
|
|
||||||
|
public ByteArrayList() {
|
||||||
|
this(32);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteArrayList(int size) {
|
||||||
|
if (size < 0) {
|
||||||
|
throw new IllegalArgumentException("Negative initial size: "
|
||||||
|
+ size);
|
||||||
|
}
|
||||||
|
buf = new byte[size];
|
||||||
|
}
|
||||||
|
|
||||||
|
void ensureCapacity(int minCapacity) {
|
||||||
|
// overflow-conscious code
|
||||||
|
if (minCapacity - buf.length > 0)
|
||||||
|
grow(minCapacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
|
||||||
|
|
||||||
|
private void grow(int minCapacity) {
|
||||||
|
// overflow-conscious code
|
||||||
|
int oldCapacity = buf.length;
|
||||||
|
int newCapacity = oldCapacity << 1;
|
||||||
|
if (newCapacity - minCapacity < 0)
|
||||||
|
newCapacity = minCapacity;
|
||||||
|
if (newCapacity - MAX_ARRAY_SIZE > 0)
|
||||||
|
newCapacity = hugeCapacity(minCapacity);
|
||||||
|
buf = Arrays.copyOf(buf, newCapacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int hugeCapacity(int minCapacity) {
|
||||||
|
if (minCapacity < 0) // overflow
|
||||||
|
throw new OutOfMemoryError();
|
||||||
|
return (minCapacity > MAX_ARRAY_SIZE) ?
|
||||||
|
Integer.MAX_VALUE :
|
||||||
|
MAX_ARRAY_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void write(int b) {
|
||||||
|
ensureCapacity(count + 1);
|
||||||
|
buf[count] = (byte) b;
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void write(byte b[], int off, int len) {
|
||||||
|
if (off < 0 || len < 0 || off + len > b.length) {
|
||||||
|
throw new IllegalArgumentException("write " + b.length + ", off=" + off + ", len=" + len);
|
||||||
|
}
|
||||||
|
ensureCapacity(count + len);
|
||||||
|
System.arraycopy(b, off, buf, count, len);
|
||||||
|
count += len;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void write(int pos, byte b[], int off, int len) {
|
||||||
|
ensureCapacity(pos + len);
|
||||||
|
System.arraycopy(b, off, buf, pos, len);
|
||||||
|
count = Math.max(count, pos + len);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void reset() {
|
||||||
|
count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized byte[] toByteArray() {
|
||||||
|
return Arrays.copyOf(buf, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized int size() {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized String toString() {
|
||||||
|
return new String(buf, 0, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized String toString(String charsetName)
|
||||||
|
throws UnsupportedEncodingException
|
||||||
|
{
|
||||||
|
return new String(buf, 0, count, charsetName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized String toString(Charset charset) {
|
||||||
|
return new String(buf, 0, count, charset);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected EnumCheckETag shouldCheckETag() {
|
protected DownloadManager.Downloader<String> createDownloader() {
|
||||||
return EnumCheckETag.CHECK_E_TAG;
|
return new DownloadManager.Downloader<String>() {
|
||||||
}
|
private ByteArrayList bytes;
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void useCachedResult(Path cachedFile) throws IOException {
|
|
||||||
setResult(FileUtils.readText(cachedFile));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Context getContext(URLConnection conn, boolean checkETag) {
|
|
||||||
return new Context() {
|
|
||||||
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void write(byte[] buffer, int offset, int len) {
|
protected EnumCheckETag shouldCheckETag() {
|
||||||
baos.write(buffer, offset, len);
|
return EnumCheckETag.CHECK_E_TAG;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws IOException {
|
protected String finishWithCachedResult(Path cachedFile) throws IOException {
|
||||||
if (!isSuccess()) return;
|
return FileUtils.readText(cachedFile);
|
||||||
|
}
|
||||||
|
|
||||||
String result = baos.toString(charset.name());
|
@Override
|
||||||
setResult(result);
|
protected void write(long pos, byte[] buffer, int offset, int len) {
|
||||||
|
if (pos > Integer.MAX_VALUE/* check overflow */ || pos + len > Integer.MAX_VALUE) {
|
||||||
if (checkETag) {
|
throw new IllegalArgumentException("pos too large");
|
||||||
repository.cacheText(result, conn);
|
|
||||||
}
|
}
|
||||||
|
bytes.ensureCapacity((int)pos + len);
|
||||||
|
bytes.write((int)pos, buffer, offset, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStart() {
|
||||||
|
bytes = new ByteArrayList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onContentLengthChanged(long contentLength) {
|
||||||
|
if (contentLength > Integer.MAX_VALUE) {
|
||||||
|
throw new IllegalArgumentException("Content length too large");
|
||||||
|
}
|
||||||
|
bytes.ensureCapacity((int)contentLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String finish() throws IOException {
|
||||||
|
if (!state.isFinished()) return null;
|
||||||
|
|
||||||
|
String result = bytes.toString(charset.name());
|
||||||
|
|
||||||
|
if (getCheckETag() == EnumCheckETag.CHECK_E_TAG) {
|
||||||
|
try {
|
||||||
|
repository.cacheText(result, state.getSegments().get(0).getConnection());
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.log(Level.WARNING, "Failed to cache text", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,27 +32,29 @@ public final class Schedulers {
|
|||||||
private Schedulers() {
|
private Schedulers() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static volatile ThreadPoolExecutor CACHED_EXECUTOR;
|
|
||||||
|
|
||||||
public static synchronized ThreadPoolExecutor newThread() {
|
|
||||||
if (CACHED_EXECUTOR == null)
|
|
||||||
CACHED_EXECUTOR = new ThreadPoolExecutor(0, Integer.MAX_VALUE,
|
|
||||||
60, TimeUnit.SECONDS, new SynchronousQueue<>(), Executors.defaultThreadFactory());
|
|
||||||
|
|
||||||
return CACHED_EXECUTOR;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static volatile ExecutorService IO_EXECUTOR;
|
private static volatile ExecutorService IO_EXECUTOR;
|
||||||
|
|
||||||
public static synchronized ExecutorService io() {
|
/**
|
||||||
|
* Get singleton instance of the thread pool for I/O operations,
|
||||||
|
* usually for reading files from disk, or Internet connections.
|
||||||
|
*
|
||||||
|
* This thread pool has no more than 4 threads, and number of threads will get
|
||||||
|
* reduced if concurrency is less than thread number.
|
||||||
|
*
|
||||||
|
* @return Thread pool for I/O operations.
|
||||||
|
*/
|
||||||
|
public static ExecutorService io() {
|
||||||
if (IO_EXECUTOR == null) {
|
if (IO_EXECUTOR == null) {
|
||||||
int threads = Math.min(Runtime.getRuntime().availableProcessors() * 4, 64);
|
synchronized (Schedulers.class) {
|
||||||
IO_EXECUTOR = Executors.newFixedThreadPool(threads,
|
if (IO_EXECUTOR == null) {
|
||||||
runnable -> {
|
IO_EXECUTOR = new ThreadPoolExecutor(0, 4, 10, TimeUnit.SECONDS, new SynchronousQueue<>(),
|
||||||
Thread thread = Executors.defaultThreadFactory().newThread(runnable);
|
runnable -> {
|
||||||
thread.setDaemon(true);
|
Thread thread = Executors.defaultThreadFactory().newThread(runnable);
|
||||||
return thread;
|
thread.setDaemon(true);
|
||||||
});
|
return thread;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return IO_EXECUTOR;
|
return IO_EXECUTOR;
|
||||||
@@ -67,7 +69,7 @@ public final class Schedulers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static Executor defaultScheduler() {
|
public static Executor defaultScheduler() {
|
||||||
return newThread();
|
return ForkJoinPool.commonPool();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static synchronized void shutdown() {
|
public static synchronized void shutdown() {
|
||||||
@@ -77,16 +79,8 @@ public final class Schedulers {
|
|||||||
// So when we want to close the app, no threads need to be waited for finish.
|
// So when we want to close the app, no threads need to be waited for finish.
|
||||||
// Sometimes it resolves the problem that the app does not exit.
|
// Sometimes it resolves the problem that the app does not exit.
|
||||||
|
|
||||||
if (CACHED_EXECUTOR != null)
|
|
||||||
CACHED_EXECUTOR.shutdownNow();
|
|
||||||
|
|
||||||
if (IO_EXECUTOR != null)
|
if (IO_EXECUTOR != null)
|
||||||
IO_EXECUTOR.shutdownNow();
|
IO_EXECUTOR.shutdownNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Future<?> schedule(Executor executor, Runnable command) {
|
|
||||||
FutureTask<?> future = new FutureTask<Void>(command, null);
|
|
||||||
executor.execute(future);
|
|
||||||
return future;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -31,6 +31,7 @@ import org.jackhuang.hmcl.util.function.ExceptionalConsumer;
|
|||||||
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
|
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
|
||||||
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
|
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
|
||||||
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
|
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
@@ -131,6 +132,7 @@ public abstract class Task<T> {
|
|||||||
*
|
*
|
||||||
* @return the exception thrown during execution, possibly from dependents or dependencies.
|
* @return the exception thrown during execution, possibly from dependents or dependencies.
|
||||||
*/
|
*/
|
||||||
|
@Nullable
|
||||||
public final Exception getException() {
|
public final Exception getException() {
|
||||||
return exception;
|
return exception;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.util.io;
|
package org.jackhuang.hmcl.util.io;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.net.*;
|
import java.net.*;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -55,23 +57,27 @@ public final class NetworkUtils {
|
|||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static URLConnection createConnection(URL url) throws IOException {
|
public static URLConnection createConnection(@NotNull URL url) throws IOException {
|
||||||
|
return createConnection(url, 15000);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static URLConnection createConnection(@NotNull URL url, int timeout) throws IOException {
|
||||||
URLConnection connection = url.openConnection();
|
URLConnection connection = url.openConnection();
|
||||||
connection.setUseCaches(false);
|
connection.setUseCaches(false);
|
||||||
connection.setConnectTimeout(15000);
|
connection.setConnectTimeout(timeout);
|
||||||
connection.setReadTimeout(15000);
|
connection.setReadTimeout(timeout);
|
||||||
connection.setRequestProperty("Accept-Language", Locale.getDefault().toString());
|
connection.setRequestProperty("Accept-Language", Locale.getDefault().toString());
|
||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static HttpURLConnection createHttpConnection(URL url) throws IOException {
|
public static HttpURLConnection createHttpConnection(@NotNull URL url) throws IOException {
|
||||||
return (HttpURLConnection) createConnection(url);
|
return (HttpURLConnection) createConnection(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see <a href="https://github.com/curl/curl/blob/3f7b1bb89f92c13e69ee51b710ac54f775aab320/lib/transfer.c#L1427-L1461">Curl</a>
|
|
||||||
* @param location the url to be URL encoded
|
* @param location the url to be URL encoded
|
||||||
* @return encoded URL
|
* @return encoded URL
|
||||||
|
* @see <a href="https://github.com/curl/curl/blob/3f7b1bb89f92c13e69ee51b710ac54f775aab320/lib/transfer.c#L1427-L1461">Curl</a>
|
||||||
*/
|
*/
|
||||||
public static String encodeLocation(String location) {
|
public static String encodeLocation(String location) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
@@ -98,18 +104,31 @@ public final class NetworkUtils {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is a work-around that aims to solve problem when "Location" in stupid server's response is not encoded.
|
* This method is a work-around that aims to solve problem when "Location" in stupid server's response is not encoded.
|
||||||
* @see <a href="https://github.com/curl/curl/issues/473">Issue with libcurl</a>
|
*
|
||||||
* @param conn the stupid http connection.
|
* @param conn the stupid http connection.
|
||||||
* @return manually redirected http connection.
|
* @return manually redirected http connection.
|
||||||
* @throws IOException if an I/O error occurs.
|
* @throws IOException if an I/O error occurs.
|
||||||
|
* @see <a href="https://github.com/curl/curl/issues/473">Issue with libcurl</a>
|
||||||
*/
|
*/
|
||||||
public static HttpURLConnection resolveConnection(HttpURLConnection conn) throws IOException {
|
public static HttpURLConnection resolveConnection(HttpURLConnection conn) throws IOException {
|
||||||
|
return resolveConnection(conn, 15000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is a work-around that aims to solve problem when "Location" in stupid server's response is not encoded.
|
||||||
|
*
|
||||||
|
* @param conn the stupid http connection.
|
||||||
|
* @param timeout timeout in milliseconds in once connection.
|
||||||
|
* @return manually redirected http connection.
|
||||||
|
* @throws IOException if an I/O error occurs.
|
||||||
|
* @see <a href="https://github.com/curl/curl/issues/473">Issue with libcurl</a>
|
||||||
|
*/
|
||||||
|
public static HttpURLConnection resolveConnection(HttpURLConnection conn, int timeout) throws IOException {
|
||||||
int redirect = 0;
|
int redirect = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
||||||
conn.setUseCaches(false);
|
conn.setUseCaches(false);
|
||||||
conn.setConnectTimeout(15000);
|
conn.setConnectTimeout(timeout);
|
||||||
conn.setReadTimeout(15000);
|
conn.setReadTimeout(timeout);
|
||||||
conn.setInstanceFollowRedirects(false);
|
conn.setInstanceFollowRedirects(false);
|
||||||
Map<String, List<String>> properties = conn.getRequestProperties();
|
Map<String, List<String>> properties = conn.getRequestProperties();
|
||||||
String method = conn.getRequestMethod();
|
String method = conn.getRequestMethod();
|
||||||
|
|||||||
Reference in new issue
Block a user