package nl.andrewlalis.aos_server; import com.fasterxml.jackson.databind.ObjectMapper; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; import java.util.Base64; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; /** * The registry manager is responsible for keeping the server registry up to * date with this server's information, by sending periodic update HTTP messages. */ public class RegistryManager { /** * The list of retry timings that will be used if the registry server cannot * be reached. Using the retryTimingIndex, we'll start at 5, and increment * each time the connection fails. */ public static final long[] RETRY_TIMINGS = new long[]{5, 10, 30, 60, 120, 300}; private int retryTimingIndex = 0; private final ScheduledExecutorService executorService; private final Server server; private final ObjectMapper mapper; private final HttpClient httpClient; public RegistryManager(Server server) { this.server = server; this.mapper = new ObjectMapper(); this.executorService = Executors.newScheduledThreadPool(3); this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(3)).build(); this.executorService.submit(this::sendInfo); this.executorService.scheduleAtFixedRate( this::sendUpdate, server.getSettings().getRegistrySettings().getUpdateInterval(), server.getSettings().getRegistrySettings().getUpdateInterval(), TimeUnit.SECONDS ); } public void sendInfo() { try { Map<String, Object> data = new HashMap<>(); data.put("name", this.server.getSettings().getRegistrySettings().getName()); data.put("address", this.server.getSettings().getRegistrySettings().getAddress()); data.put("description", this.server.getSettings().getRegistrySettings().getDescription()); data.put("location", this.server.getSettings().getRegistrySettings().getLocation()); data.put("icon", this.getIconData()); data.put("maxPlayers", this.server.getSettings().getMaxPlayers()); data.put("currentPlayers", 0); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(this.server.getSettings().getRegistrySettings().getRegistryUri() + "/serverInfo")) .POST(HttpRequest.BodyPublishers.ofByteArray(this.mapper.writeValueAsBytes(data))) .header("Content-Type", "application/json") .build(); try { System.out.println("Sending server information to registry..."); var response = this.httpClient.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200) { System.err.println("Non-OK status when sending registry info:\n" + response.body() + "\nAttempting to send again in 10 seconds..."); this.executorService.schedule(this::sendInfo, 10, TimeUnit.SECONDS); } else if (this.retryTimingIndex > 0) { this.retryTimingIndex = 0; // Reset the retry timing index if we successfully sent our server info. } } catch (IOException e) { long retryTiming = RETRY_TIMINGS[this.retryTimingIndex]; System.err.println("Could not send info to registry server. Registry may be offline, or this server may not have internet access. Attempting to resend info in " + retryTiming + " seconds..."); this.executorService.schedule(this::sendInfo, retryTiming, TimeUnit.SECONDS); if (this.retryTimingIndex < RETRY_TIMINGS.length - 1) { this.retryTimingIndex++; } } } catch (Exception e) { e.printStackTrace(); } } private void sendUpdate() { try { Map<String, Object> data = new HashMap<>(); data.put("name", this.server.getSettings().getRegistrySettings().getName()); data.put("address", this.server.getSettings().getRegistrySettings().getAddress()); data.put("currentPlayers", server.getPlayerCount()); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(this.server.getSettings().getRegistrySettings().getRegistryUri() + "/serverInfo")) .PUT(HttpRequest.BodyPublishers.ofByteArray(this.mapper.writeValueAsBytes(data))) .header("Content-Type", "application/json") .build(); try { var response = this.httpClient.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200) { System.err.println("Received non-OK status when sending registry update:\n" + response.body()); } } catch (IOException e) { System.err.println("Error sending update to registry server: " + e); } } catch (Exception e) { e.printStackTrace(); } } private String getIconData() throws IOException { Path iconFile = Path.of("icon.png"); if (Files.exists(iconFile)) { byte[] imageBytes = Files.readAllBytes(iconFile); BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageBytes)); if (image.getWidth() == 64 && image.getHeight() == 64) { return Base64.getUrlEncoder().encodeToString(imageBytes); } else { System.err.println("icon.png must be 64 x 64."); } } return null; } public void shutdown() { this.executorService.shutdown(); try { while (!this.executorService.awaitTermination(3, TimeUnit.SECONDS)) { System.out.println("Waiting for scheduler to terminate."); } } catch (InterruptedException e) { e.printStackTrace(); } } }