mirror of
https://gitlab.com/mangadex-pub/mangadex_at_home.git
synced 2024-11-17 00:22:32 +01:00
Merge branch 'graceful' into 'master'
Add graceful shutdown See merge request mangadex/mangadex_at_home!33
This commit is contained in:
commit
8aaefb9579
@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- [2020-06-16] Reworked graceful shutdown [@carbotaniuman].
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ plugins {
|
|||||||
|
|
||||||
group = "com.mangadex"
|
group = "com.mangadex"
|
||||||
version = "git describe --tags --dirty".execute().text.trim()
|
version = "git describe --tags --dirty".execute().text.trim()
|
||||||
mainClassName = "mdnet.base.MangaDexClient"
|
mainClassName = "mdnet.base.Main"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
104
src/main/java/mdnet/base/Main.java
Normal file
104
src/main/java/mdnet/base/Main.java
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
package mdnet.base;
|
||||||
|
|
||||||
|
import mdnet.base.settings.ClientSettings;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileReader;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import static mdnet.base.Constants.GSON;
|
||||||
|
|
||||||
|
public class Main {
|
||||||
|
private final static Logger LOGGER = LoggerFactory.getLogger(Main.class);
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println("Mangadex@Home Client " + Constants.CLIENT_VERSION + " (Build " + Constants.CLIENT_BUILD
|
||||||
|
+ ") initializing\n");
|
||||||
|
System.out.println("Copyright (c) 2020, MangaDex Network");
|
||||||
|
|
||||||
|
String file = "settings.json";
|
||||||
|
if (args.length == 1) {
|
||||||
|
file = args[0];
|
||||||
|
} else if (args.length != 0) {
|
||||||
|
dieWithError("Expected one argument: path to config file, or nothing");
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientSettings settings;
|
||||||
|
|
||||||
|
try {
|
||||||
|
settings = GSON.fromJson(new FileReader(file), ClientSettings.class);
|
||||||
|
} catch (FileNotFoundException ignored) {
|
||||||
|
settings = new ClientSettings();
|
||||||
|
LOGGER.warn("Settings file {} not found, generating file", file);
|
||||||
|
try (FileWriter writer = new FileWriter(file)) {
|
||||||
|
writer.write(GSON.toJson(settings));
|
||||||
|
} catch (IOException e) {
|
||||||
|
dieWithError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validateSettings(settings);
|
||||||
|
|
||||||
|
if (LOGGER.isInfoEnabled()) {
|
||||||
|
LOGGER.info("Client settings loaded: {}", settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
MangaDexClient client = new MangaDexClient(settings);
|
||||||
|
Runtime.getRuntime().addShutdownHook(new Thread(client::shutdown));
|
||||||
|
client.runLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void dieWithError(Throwable e) {
|
||||||
|
if (LOGGER.isErrorEnabled()) {
|
||||||
|
LOGGER.error("Critical Error", e);
|
||||||
|
}
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void dieWithError(String error) {
|
||||||
|
if (LOGGER.isErrorEnabled()) {
|
||||||
|
LOGGER.error("Critical Error: {}", error);
|
||||||
|
}
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void validateSettings(ClientSettings settings) {
|
||||||
|
if (!isSecretValid(settings.getClientSecret()))
|
||||||
|
dieWithError("Config Error: API Secret is invalid, must be 52 alphanumeric characters");
|
||||||
|
|
||||||
|
if (settings.getClientPort() == 0) {
|
||||||
|
dieWithError("Config Error: Invalid port number");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.getMaxCacheSizeMib() < 1024) {
|
||||||
|
dieWithError("Config Error: Invalid max cache size, must be >= 1024 MiB (1GiB)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.getThreads() < 4) {
|
||||||
|
dieWithError("Config Error: Invalid number of threads, must be >= 4");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.getMaxBandwidthMibPerHour() < 0) {
|
||||||
|
dieWithError("Config Error: Max bandwidth must be >= 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.getMaxBurstRateKibPerSecond() < 0) {
|
||||||
|
dieWithError("Config Error: Max burst rate must be >= 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.getWebSettings() != null) {
|
||||||
|
if (settings.getWebSettings().getUiPort() == 0) {
|
||||||
|
dieWithError("Config Error: Invalid UI port number");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isSecretValid(String clientSecret) {
|
||||||
|
final int CLIENT_KEY_LENGTH = 52;
|
||||||
|
return Pattern.matches("^[a-zA-Z0-9]{" + CLIENT_KEY_LENGTH + "}$", clientSecret);
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,5 @@
|
|||||||
package mdnet.base;
|
package mdnet.base;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import com.google.gson.GsonBuilder;
|
|
||||||
import mdnet.base.settings.ClientSettings;
|
import mdnet.base.settings.ClientSettings;
|
||||||
import mdnet.base.server.ApplicationKt;
|
import mdnet.base.server.ApplicationKt;
|
||||||
import mdnet.base.server.WebUiKt;
|
import mdnet.base.server.WebUiKt;
|
||||||
@ -18,15 +16,14 @@ import java.util.Map;
|
|||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
import static mdnet.base.Constants.GSON;
|
||||||
|
|
||||||
public class MangaDexClient {
|
public class MangaDexClient {
|
||||||
private final static Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
|
||||||
private final static Logger LOGGER = LoggerFactory.getLogger(MangaDexClient.class);
|
private final static Logger LOGGER = LoggerFactory.getLogger(MangaDexClient.class);
|
||||||
|
|
||||||
// This lock protects the Http4kServer from concurrent restart attempts
|
|
||||||
private final Object shutdownLock = new Object();
|
|
||||||
private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
|
private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
|
||||||
private final ServerHandler serverHandler;
|
private final ServerHandler serverHandler;
|
||||||
private final ClientSettings clientSettings;
|
private final ClientSettings clientSettings;
|
||||||
@ -39,6 +36,7 @@ public class MangaDexClient {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
private final AtomicReference<Statistics> statistics;
|
private final AtomicReference<Statistics> statistics;
|
||||||
|
private final AtomicBoolean isHandled;
|
||||||
|
|
||||||
private ServerSettings serverSettings;
|
private ServerSettings serverSettings;
|
||||||
private Http4kServer engine; // if this is null, then the server has shutdown
|
private Http4kServer engine; // if this is null, then the server has shutdown
|
||||||
@ -48,11 +46,15 @@ public class MangaDexClient {
|
|||||||
// these variables are for runLoop();
|
// these variables are for runLoop();
|
||||||
private int counter = 0;
|
private int counter = 0;
|
||||||
private long lastBytesSent = 0;
|
private long lastBytesSent = 0;
|
||||||
|
// a non-negative number here means we are shutting down
|
||||||
|
private int gracefulCounter = -1;
|
||||||
|
private Runnable gracefulAction;
|
||||||
|
|
||||||
public MangaDexClient(ClientSettings clientSettings) {
|
public MangaDexClient(ClientSettings clientSettings) {
|
||||||
this.clientSettings = clientSettings;
|
this.clientSettings = clientSettings;
|
||||||
this.serverHandler = new ServerHandler(clientSettings);
|
this.serverHandler = new ServerHandler(clientSettings);
|
||||||
this.statistics = new AtomicReference<>();
|
this.statistics = new AtomicReference<>();
|
||||||
|
this.isHandled = new AtomicBoolean();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
cache = DiskLruCache.open(new File("cache"), 1, 1,
|
cache = DiskLruCache.open(new File("cache"), 1, 1,
|
||||||
@ -66,7 +68,7 @@ public class MangaDexClient {
|
|||||||
statistics.set(new Statistics());
|
statistics.set(new Statistics());
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
MangaDexClient.dieWithError(e);
|
Main.dieWithError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,26 +95,8 @@ public class MangaDexClient {
|
|||||||
|
|
||||||
executorService.scheduleWithFixedDelay(() -> {
|
executorService.scheduleWithFixedDelay(() -> {
|
||||||
try {
|
try {
|
||||||
statistics.updateAndGet(n -> n.copy(n.getRequestsServed(), n.getCacheHits(), n.getCacheMisses(),
|
// Converting from 15 seconds loop to 45 second loop
|
||||||
n.getBrowserCached(), n.getBytesSent(), cache.size()));
|
if (counter / 3 == 80) {
|
||||||
|
|
||||||
statsMap.put(Instant.now(), statistics.get());
|
|
||||||
|
|
||||||
DiskLruCache.Editor editor = cache.edit("statistics");
|
|
||||||
if (editor != null) {
|
|
||||||
String json = GSON.toJson(statistics.get(), Statistics.class);
|
|
||||||
editor.setString(0, json);
|
|
||||||
editor.commit();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOGGER.warn("statistics update failed", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
}, 15, 15, TimeUnit.SECONDS);
|
|
||||||
|
|
||||||
executorService.scheduleAtFixedRate(() -> {
|
|
||||||
try {
|
|
||||||
if (counter == 80) {
|
|
||||||
counter = 0;
|
counter = 0;
|
||||||
lastBytesSent = statistics.get().getBytesSent();
|
lastBytesSent = statistics.get().getBytesSent();
|
||||||
|
|
||||||
@ -127,62 +111,120 @@ public class MangaDexClient {
|
|||||||
counter++;
|
counter++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the server is offline then don't try and refresh certs
|
if (gracefulCounter == 0) {
|
||||||
if (engine == null) {
|
logout();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
long currentBytesSent = statistics.get().getBytesSent() - lastBytesSent;
|
|
||||||
if (clientSettings.getMaxBandwidthMibPerHour() != 0 && clientSettings.getMaxBandwidthMibPerHour() * 1024
|
|
||||||
* 1024 /* MiB to bytes */ < currentBytesSent) {
|
|
||||||
if (LOGGER.isInfoEnabled()) {
|
if (LOGGER.isInfoEnabled()) {
|
||||||
LOGGER.info("Shutting down server as hourly bandwidth limit reached");
|
LOGGER.info("Waiting another 15 seconds for graceful shutdown ({} out of {} tries)",
|
||||||
|
gracefulCounter + 1, 4);
|
||||||
}
|
}
|
||||||
|
gracefulCounter++;
|
||||||
synchronized (shutdownLock) {
|
} else if (gracefulCounter > 0) {
|
||||||
logoutAndStopServer();
|
if (!isHandled.get() || gracefulCounter == 4) {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ServerSettings n = serverHandler.pingControl(serverSettings);
|
|
||||||
|
|
||||||
if (LOGGER.isInfoEnabled()) {
|
|
||||||
LOGGER.info("Server settings received: {}", n);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (n != null) {
|
|
||||||
if (n.getLatestBuild() > Constants.CLIENT_BUILD) {
|
|
||||||
if (LOGGER.isWarnEnabled()) {
|
|
||||||
LOGGER.warn("Outdated build detected! Latest: {}, Current: {}", n.getLatestBuild(),
|
|
||||||
Constants.CLIENT_BUILD);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (n.getTls() != null || !n.getImageServer().equals(serverSettings.getImageServer())) {
|
|
||||||
// certificates or upstream url must have changed, restart webserver
|
|
||||||
if (LOGGER.isInfoEnabled()) {
|
if (LOGGER.isInfoEnabled()) {
|
||||||
LOGGER.info("Doing internal restart of HTTP server to refresh certs/upstream URL");
|
if (!isHandled.get()) {
|
||||||
|
LOGGER.info("No requests received, shutting down");
|
||||||
|
} else {
|
||||||
|
LOGGER.info("Max tries attempted, shutting down");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized (shutdownLock) {
|
stopServer();
|
||||||
logoutAndStopServer();
|
if (gracefulAction != null) {
|
||||||
loginAndStartServer();
|
gracefulAction.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reset variables
|
||||||
|
gracefulCounter = -1;
|
||||||
|
gracefulAction = null;
|
||||||
|
} else {
|
||||||
|
if (LOGGER.isInfoEnabled()) {
|
||||||
|
LOGGER.info("Waiting another 15 seconds for graceful shutdown ({} out of {} tries)",
|
||||||
|
gracefulCounter + 1, 4);
|
||||||
|
}
|
||||||
|
gracefulCounter++;
|
||||||
}
|
}
|
||||||
|
isHandled.set(false);
|
||||||
|
} else {
|
||||||
|
if (counter % 3 == 0) {
|
||||||
|
pingControl();
|
||||||
|
}
|
||||||
|
updateStats();
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
|
||||||
LOGGER.warn("Server ping failed", e);
|
|
||||||
}
|
|
||||||
}, 45, 45, TimeUnit.SECONDS);
|
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("statistics update failed", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, 15, 15, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pingControl() {
|
||||||
|
// if the server is offline then don't try and refresh certs
|
||||||
|
if (engine == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long currentBytesSent = statistics.get().getBytesSent() - lastBytesSent;
|
||||||
|
if (clientSettings.getMaxBandwidthMibPerHour() != 0
|
||||||
|
&& clientSettings.getMaxBandwidthMibPerHour() * 1024 * 1024 /* MiB to bytes */ < currentBytesSent) {
|
||||||
|
if (LOGGER.isInfoEnabled()) {
|
||||||
|
LOGGER.info("Shutting down server as hourly bandwidth limit reached");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give enough time for graceful shutdown
|
||||||
|
if (240 - counter > 3) {
|
||||||
|
LOGGER.info("Graceful shutdown started");
|
||||||
|
gracefulCounter = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerSettings n = serverHandler.pingControl(serverSettings);
|
||||||
|
|
||||||
|
if (LOGGER.isInfoEnabled()) {
|
||||||
|
LOGGER.info("Server settings received: {}", n);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n != null) {
|
||||||
|
if (n.getLatestBuild() > Constants.CLIENT_BUILD) {
|
||||||
|
if (LOGGER.isWarnEnabled()) {
|
||||||
|
LOGGER.warn("Outdated build detected! Latest: {}, Current: {}", n.getLatestBuild(),
|
||||||
|
Constants.CLIENT_BUILD);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n.getTls() != null || !n.getImageServer().equals(serverSettings.getImageServer())) {
|
||||||
|
// certificates or upstream url must have changed, restart webserver
|
||||||
|
if (LOGGER.isInfoEnabled()) {
|
||||||
|
LOGGER.info("Doing internal restart of HTTP server to refresh certs/upstream URL");
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.info("Graceful shutdown started");
|
||||||
|
gracefulCounter = 0;
|
||||||
|
gracefulAction = this::loginAndStartServer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateStats() throws IOException {
|
||||||
|
statistics.updateAndGet(n -> n.copy(n.getRequestsServed(), n.getCacheHits(), n.getCacheMisses(),
|
||||||
|
n.getBrowserCached(), n.getBytesSent(), cache.size()));
|
||||||
|
|
||||||
|
statsMap.put(Instant.now(), statistics.get());
|
||||||
|
|
||||||
|
DiskLruCache.Editor editor = cache.edit("statistics");
|
||||||
|
if (editor != null) {
|
||||||
|
String json = GSON.toJson(statistics.get(), Statistics.class);
|
||||||
|
editor.setString(0, json);
|
||||||
|
editor.commit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loginAndStartServer() {
|
private void loginAndStartServer() {
|
||||||
serverSettings = serverHandler.loginToControl();
|
serverSettings = serverHandler.loginToControl();
|
||||||
if (serverSettings == null) {
|
if (serverSettings == null) {
|
||||||
MangaDexClient.dieWithError("Failed to get a login response from server - check API secret for validity");
|
Main.dieWithError("Failed to get a login response from server - check API secret for validity");
|
||||||
}
|
}
|
||||||
engine = ApplicationKt.getServer(cache, serverSettings, clientSettings, statistics);
|
engine = ApplicationKt.getServer(cache, serverSettings, clientSettings, statistics, isHandled);
|
||||||
engine.start();
|
engine.start();
|
||||||
|
|
||||||
if (LOGGER.isInfoEnabled()) {
|
if (LOGGER.isInfoEnabled()) {
|
||||||
@ -190,11 +232,14 @@ public class MangaDexClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void logoutAndStopServer() {
|
private void logout() {
|
||||||
if (LOGGER.isInfoEnabled()) {
|
|
||||||
LOGGER.info("Gracefully shutting down HTTP server");
|
|
||||||
}
|
|
||||||
serverHandler.logoutFromControl();
|
serverHandler.logoutFromControl();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopServer() {
|
||||||
|
if (LOGGER.isInfoEnabled()) {
|
||||||
|
LOGGER.info("Shutting down HTTP server");
|
||||||
|
}
|
||||||
engine.stop();
|
engine.stop();
|
||||||
if (LOGGER.isInfoEnabled()) {
|
if (LOGGER.isInfoEnabled()) {
|
||||||
LOGGER.info("Internal HTTP server has gracefully shut down");
|
LOGGER.info("Internal HTTP server has gracefully shut down");
|
||||||
@ -203,108 +248,22 @@ public class MangaDexClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
executorService.shutdown();
|
LOGGER.info("Graceful shutdown started");
|
||||||
synchronized (shutdownLock) {
|
gracefulCounter = 0;
|
||||||
if (engine == null) {
|
AtomicBoolean readyToExit = new AtomicBoolean(false);
|
||||||
return;
|
gracefulAction = () -> {
|
||||||
|
if (webUi != null) {
|
||||||
|
webUi.close();
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
logoutAndStopServer();
|
cache.close();
|
||||||
}
|
|
||||||
if (webUi != null) {
|
|
||||||
webUi.close();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
cache.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOGGER.error("Cache failed to close", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
System.out.println("Mangadex@Home Client " + Constants.CLIENT_VERSION + " (Build " + Constants.CLIENT_BUILD
|
|
||||||
+ ") initializing\n");
|
|
||||||
System.out.println("Copyright (c) 2020, MangaDex Network");
|
|
||||||
|
|
||||||
String file = "settings.json";
|
|
||||||
if (args.length == 1) {
|
|
||||||
file = args[0];
|
|
||||||
} else if (args.length != 0) {
|
|
||||||
MangaDexClient.dieWithError("Expected one argument: path to config file, or nothing");
|
|
||||||
}
|
|
||||||
|
|
||||||
ClientSettings settings;
|
|
||||||
|
|
||||||
try {
|
|
||||||
settings = GSON.fromJson(new FileReader(file), ClientSettings.class);
|
|
||||||
} catch (FileNotFoundException ignored) {
|
|
||||||
settings = new ClientSettings();
|
|
||||||
LOGGER.warn("Settings file {} not found, generating file", file);
|
|
||||||
try (FileWriter writer = new FileWriter(file)) {
|
|
||||||
writer.write(GSON.toJson(settings));
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
MangaDexClient.dieWithError(e);
|
LOGGER.error("Cache failed to close", e);
|
||||||
}
|
}
|
||||||
|
executorService.shutdown();
|
||||||
|
readyToExit.set(true);
|
||||||
|
};
|
||||||
|
while (!readyToExit.get()) {
|
||||||
}
|
}
|
||||||
|
|
||||||
validateSettings(settings);
|
|
||||||
|
|
||||||
if (LOGGER.isInfoEnabled()) {
|
|
||||||
LOGGER.info("Client settings loaded: {}", settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
MangaDexClient client = new MangaDexClient(settings);
|
|
||||||
Runtime.getRuntime().addShutdownHook(new Thread(client::shutdown));
|
|
||||||
client.runLoop();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void dieWithError(Throwable e) {
|
|
||||||
if (LOGGER.isErrorEnabled()) {
|
|
||||||
LOGGER.error("Critical Error", e);
|
|
||||||
}
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void dieWithError(String error) {
|
|
||||||
if (LOGGER.isErrorEnabled()) {
|
|
||||||
LOGGER.error("Critical Error: {}", error);
|
|
||||||
}
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void validateSettings(ClientSettings settings) {
|
|
||||||
if (!isSecretValid(settings.getClientSecret()))
|
|
||||||
MangaDexClient.dieWithError("Config Error: API Secret is invalid, must be 52 alphanumeric characters");
|
|
||||||
|
|
||||||
if (settings.getClientPort() == 0) {
|
|
||||||
MangaDexClient.dieWithError("Config Error: Invalid port number");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.getMaxCacheSizeMib() < 1024) {
|
|
||||||
MangaDexClient.dieWithError("Config Error: Invalid max cache size, must be >= 1024 MiB (1GiB)");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.getThreads() < 4) {
|
|
||||||
MangaDexClient.dieWithError("Config Error: Invalid number of threads, must be >= 4");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.getMaxBandwidthMibPerHour() < 0) {
|
|
||||||
MangaDexClient.dieWithError("Config Error: Max bandwidth must be >= 0");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.getMaxBurstRateKibPerSecond() < 0) {
|
|
||||||
MangaDexClient.dieWithError("Config Error: Max burst rate must be >= 0");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.getWebSettings() != null) {
|
|
||||||
if (settings.getWebSettings().getUiPort() == 0) {
|
|
||||||
MangaDexClient.dieWithError("Config Error: Invalid UI port number");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isSecretValid(String clientSecret) {
|
|
||||||
final int CLIENT_KEY_LENGTH = 52;
|
|
||||||
return Pattern.matches("^[a-zA-Z0-9]{" + CLIENT_KEY_LENGTH + "}$", clientSecret);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
package mdnet.base
|
package mdnet.base
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
|
||||||
object Constants {
|
object Constants {
|
||||||
const val CLIENT_BUILD = 8
|
const val CLIENT_BUILD = 8
|
||||||
const val CLIENT_VERSION = "1.0"
|
const val CLIENT_VERSION = "1.0"
|
||||||
val MAX_AGE_CACHE: Duration = Duration.ofDays(14)
|
val MAX_AGE_CACHE: Duration = Duration.ofDays(14)
|
||||||
|
@JvmField
|
||||||
|
val GSON: Gson = GsonBuilder().setPrettyPrinting().create()
|
||||||
}
|
}
|
||||||
|
@ -14,11 +14,12 @@ import org.http4k.routing.routes
|
|||||||
import org.http4k.server.Http4kServer
|
import org.http4k.server.Http4kServer
|
||||||
import org.http4k.server.asServer
|
import org.http4k.server.asServer
|
||||||
import org.jetbrains.exposed.sql.Database
|
import org.jetbrains.exposed.sql.Database
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
fun getServer(cache: DiskLruCache, serverSettings: ServerSettings, clientSettings: ClientSettings, statistics: AtomicReference<Statistics>): Http4kServer {
|
fun getServer(cache: DiskLruCache, serverSettings: ServerSettings, clientSettings: ClientSettings, statistics: AtomicReference<Statistics>, isHandled: AtomicBoolean): Http4kServer {
|
||||||
val database = Database.connect("jdbc:sqlite:cache/data.db", "org.sqlite.JDBC")
|
val database = Database.connect("jdbc:sqlite:cache/data.db", "org.sqlite.JDBC")
|
||||||
val imageServer = ImageServer(cache, statistics, serverSettings.imageServer, database)
|
val imageServer = ImageServer(cache, statistics, serverSettings.imageServer, database, isHandled)
|
||||||
|
|
||||||
return Timer
|
return Timer
|
||||||
.then(catchAllHideDetails())
|
.then(catchAllHideDetails())
|
||||||
|
@ -24,6 +24,7 @@ import java.io.File
|
|||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.CipherInputStream
|
import javax.crypto.CipherInputStream
|
||||||
@ -33,7 +34,7 @@ import javax.crypto.spec.SecretKeySpec
|
|||||||
private const val THREADS_TO_ALLOCATE = 262144 // 2**18 // Honestly, no reason to not just let 'er rip. Inactive connections will expire on their own :D
|
private const val THREADS_TO_ALLOCATE = 262144 // 2**18 // Honestly, no reason to not just let 'er rip. Inactive connections will expire on their own :D
|
||||||
private val LOGGER = LoggerFactory.getLogger(ImageServer::class.java)
|
private val LOGGER = LoggerFactory.getLogger(ImageServer::class.java)
|
||||||
|
|
||||||
class ImageServer(private val cache: DiskLruCache, private val statistics: AtomicReference<Statistics>, private val upstreamUrl: String, private val database: Database) {
|
class ImageServer(private val cache: DiskLruCache, private val statistics: AtomicReference<Statistics>, private val upstreamUrl: String, private val database: Database, private val handled: AtomicBoolean) {
|
||||||
init {
|
init {
|
||||||
transaction(database) {
|
transaction(database) {
|
||||||
SchemaUtils.create(ImageData)
|
SchemaUtils.create(ImageData)
|
||||||
@ -83,6 +84,7 @@ class ImageServer(private val cache: DiskLruCache, private val statistics: Atomi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handled.set(true)
|
||||||
if (snapshot != null && imageDatum != null) {
|
if (snapshot != null && imageDatum != null) {
|
||||||
request.handleCacheHit(sanitizedUri, getRc4(rc4Bytes), snapshot, imageDatum)
|
request.handleCacheHit(sanitizedUri, getRc4(rc4Bytes), snapshot, imageDatum)
|
||||||
.header("X-Uri", sanitizedUri)
|
.header("X-Uri", sanitizedUri)
|
||||||
|
Loading…
Reference in New Issue
Block a user