Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
*/
package de.bluecolored.bluemap.common.config;

import lombok.Getter;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;

import java.net.InetAddress;
Expand All @@ -33,6 +34,7 @@

@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"})
@ConfigSerializable
@Getter
public class WebserverConfig {

private boolean enabled = true;
Expand All @@ -41,19 +43,9 @@ public class WebserverConfig {
private String ip = "0.0.0.0";
private int port = 8100;

private LogConfig log = new LogConfig();

public boolean isEnabled() {
return enabled;
}

public Path getWebroot() {
return webroot;
}
private boolean sseEnabled = true;

public String getIp() {
return ip;
}
private LogConfig log = new LogConfig();

public InetAddress resolveIp() throws UnknownHostException {
if (ip.isEmpty() || ip.equals("0.0.0.0") || ip.equals("::0")) {
Expand All @@ -65,33 +57,14 @@ public InetAddress resolveIp() throws UnknownHostException {
}
}

public int getPort() {
return port;
}

public LogConfig getLog() {
return log;
}

@ConfigSerializable
@Getter
public static class LogConfig {

private String file = null;
private boolean append = false;
private String format = "%1$s \"%3$s %4$s %5$s\" %6$s %7$s";

public String getFile() {
return file;
}

public boolean isAppend() {
return append;
}

public String getFormat() {
return format;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ private void load(@Nullable ResourcePack preloadedResourcePack) throws IOExcepti
null;
LiveMarkersDataSupplier liveMarkersDataSupplier = new LiveMarkersDataSupplier(map.getMarkerSets());

mapRequestHandler = new MapRequestHandler(map.getStorage(), livePlayersDataSupplier, liveMarkersDataSupplier);
mapRequestHandler = new MapRequestHandler(map, livePlayersDataSupplier, liveMarkersDataSupplier, webserverConfig.isSseEnabled());
} else {
Storage storage = blueMap.getOrLoadStorage(mapConfig.getStorage());
mapRequestHandler = new MapRequestHandler(storage.map(id));
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.web;

import de.bluecolored.bluemap.core.BlueMap;

import java.io.Closeable;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Supplier;

/**
* Polls a {@link Supplier} and notifies registered listeners whenever the
* returned value changes.
* <p>
* Polling and updating the data is done by the {@link BlueMap#SCHEDULER} and
* {@link BlueMap#THREAD_POOL}.
* <p>
* Call {@link #update()} to get the current value (rate-limited by the polling rate)
* Call {@link #close()} to stop the background polling.
*/
public class LiveDataSupplierBroadcaster<T> implements Supplier<T>, Closeable {

private final Supplier<T> dataSupplier;
private final long pollIntervalMillis;
private final Set<Consumer<T>> listeners = ConcurrentHashMap.newKeySet();
private ScheduledFuture<?> pollTask = null;
private boolean closed = false;
private long lastUpdate = -1;
private T data = null;

public LiveDataSupplierBroadcaster(Supplier<T> dataSupplier, long pollIntervalMillis) {
this.dataSupplier = dataSupplier;
this.pollIntervalMillis = pollIntervalMillis;
}

public synchronized void addUpdateListener(Consumer<T> listener) {
listeners.add(listener);

// have a listener - ensure scheduled poll task is running
if (!closed && pollTask == null) {
pollTask = BlueMap.SCHEDULER.scheduleWithFixedDelay(
() -> BlueMap.THREAD_POOL.execute(this::update),
0,
pollIntervalMillis,
TimeUnit.MILLISECONDS
);
}
}

public synchronized void removeUpdateListener(Consumer<T> listener) {
listeners.remove(listener);

// stop automatically updating if nothing is listening anymore
if (listeners.isEmpty() && pollTask != null) {
pollTask.cancel(false);
pollTask = null;
}
}

/**
* Ensure the data is up to date and return it.
* <p>
* Note that this will only get new data from the supplier if the current
* cached data is stale (according to {@code pollIntervalMillis}).
*/
@Override
public T get() {
update();
return this.data;
}

private void update() {
synchronized (this) {
long now = System.currentTimeMillis();
if (this.data != null && now < this.lastUpdate + this.pollIntervalMillis) return;
this.lastUpdate = now;

T newdata = dataSupplier.get();
if (newdata == null || newdata.equals(this.data)) return;

// new data, update listeners
this.data = newdata;
}
for (Consumer<T> listener : listeners) {
listener.accept(this.data);
}
}

/**
* Stops the background polling.
*/
@Override
public synchronized void close() {
closed = true;
if (pollTask != null) {
pollTask.cancel(false);
pollTask = null;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,35 +24,104 @@
*/
package de.bluecolored.bluemap.common.web;

import de.bluecolored.bluemap.common.web.http.HttpResponse;
import de.bluecolored.bluemap.common.web.http.HttpStatusCode;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.storage.MapStorage;
import org.jetbrains.annotations.Nullable;

import com.flowpowered.math.vector.Vector2i;

import java.io.IOException;
import java.util.function.Consumer;
import java.util.function.Supplier;

public class MapRequestHandler extends RoutingRequestHandler {

private final SseConnectionManager sseConnections = new SseConnectionManager();

public MapRequestHandler(
BmMap map,
@Nullable Supplier<String> livePlayersDataSupplier,
@Nullable Supplier<String> liveMarkerDataSupplier,
boolean useSSE
) {
this(map.getStorage(), livePlayersDataSupplier, liveMarkerDataSupplier, useSSE);

if (useSSE) {
map.getHiresModelManager().addTileUpdateListener(tile -> onTileUpdate(tile, 0));
map.getLowresTileManager().addTileUpdateListener(this::onTileUpdate);
}
}

public MapRequestHandler(MapStorage mapStorage) {
this(mapStorage, null, null);
this(mapStorage, null, null, false);
}

public MapRequestHandler(
MapStorage mapStorage,
@Nullable Supplier<String> livePlayersDataSupplier,
@Nullable Supplier<String> liveMarkerDataSupplier
@Nullable Supplier<String> liveMarkerDataSupplier,
boolean useSSE
) {
register(".*", new MapStorageRequestHandler(mapStorage));

if (useSSE) {
register("live/sse", "", _ -> {
HttpResponse response = new HttpResponse(HttpStatusCode.OK);
response.addHeader("Content-Type", "text/event-stream");
response.addHeader("Cache-Control", "no-cache");

// attempt to turn off buffering in upstream proxy
response.addHeader("X-Accel-Buffering", "no");

try {
response.setBody(sseConnections.openConnection());
} catch (IOException e) {
return new HttpResponse(HttpStatusCode.INTERNAL_SERVER_ERROR);
}
return response;
});
}

if (livePlayersDataSupplier != null) {
register("live/players\\.json", "", new JsonDataRequestHandler(
new CachedRateLimitDataSupplier(livePlayersDataSupplier,1000)
));
LiveDataSupplierBroadcaster<String> playerDataBroadcaster = new LiveDataSupplierBroadcaster<>(livePlayersDataSupplier, 1000);
if (useSSE) registerSseCallback(playerDataBroadcaster, this::onPlayerUpdate);
register("live/players\\.json", "", new JsonDataRequestHandler(playerDataBroadcaster));
}

if (liveMarkerDataSupplier != null) {
register("live/markers\\.json", "", new JsonDataRequestHandler(
new CachedRateLimitDataSupplier(liveMarkerDataSupplier,10000)
));
LiveDataSupplierBroadcaster<String>markerDataBroadcaster = new LiveDataSupplierBroadcaster<>(liveMarkerDataSupplier, 10000);
if (useSSE) registerSseCallback(markerDataBroadcaster, this::onMarkerUpdate);
register("live/markers\\.json", "", new JsonDataRequestHandler(markerDataBroadcaster));
}
}

/**
* Helper function to subscribe to updates from a broadcaster (forcing it to auto-refresh)
* only if the SSE manager has a connection.
*/
private void registerSseCallback(LiveDataSupplierBroadcaster<String> broadcaster, Consumer<String> callback){
sseConnections.addHasConnectionsListener(hasConnections -> {
if (hasConnections) {
broadcaster.addUpdateListener(callback);
} else {
broadcaster.removeUpdateListener(callback);
}
});
}

private void onTileUpdate(Vector2i tile, int lod) {
// since the data is all ints there's no escaping issues so just build the JSON the hacky fast way
sseConnections.broadcast("tile", "{\"x\":" + tile.getX() + ",\"y\":" + tile.getY() + ",\"lod\":" + lod + "}");
}

private void onPlayerUpdate(String data) {
sseConnections.broadcast("player", data);
}

private void onMarkerUpdate(String data) {
sseConnections.broadcast("marker", data);
}

}
Loading
Loading