Skip to content

Commit 984e513

Browse files
committed
fix: harden local API server lifecycle
1 parent 54731b1 commit 984e513

2 files changed

Lines changed: 119 additions & 46 deletions

File tree

src/client/java/fr/sukikui/playercoordsapi/PlayerCoordsAPIClient.java

Lines changed: 117 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -15,38 +15,35 @@
1515

1616
import java.io.IOException;
1717
import java.io.OutputStream;
18+
import java.net.InetAddress;
1819
import java.net.InetSocketAddress;
20+
import java.nio.charset.StandardCharsets;
1921
import java.util.Locale;
22+
import java.util.concurrent.ExecutorService;
2023
import java.util.concurrent.Executors;
2124

2225
public class PlayerCoordsAPIClient implements ClientModInitializer {
26+
private static final int PORT = 25565;
27+
private static final long START_RETRY_DELAY_MS = 5_000L;
28+
2329
private HttpServer server;
30+
private ExecutorService serverExecutor;
2431
private boolean serverStarted = false;
32+
private boolean lastConfigEnabled;
33+
private long nextStartAttemptAt = 0L;
2534
private volatile PlayerSnapshot latestSnapshot;
26-
// Hardcoded port value - no longer in config
27-
private static final int PORT = 25565;
2835

2936
@Override
3037
public void onInitializeClient() {
31-
// Start server on init if enabled
32-
if (PlayerCoordsAPI.getConfig().enabled) {
33-
startServer();
38+
lastConfigEnabled = PlayerCoordsAPI.getConfig().enabled;
39+
40+
if (lastConfigEnabled) {
41+
tryStartServer();
3442
}
3543

36-
// Register tick event to constantly check config status
3744
ClientTickEvents.END_CLIENT_TICK.register(client -> {
3845
updateSnapshot(client);
39-
boolean configEnabled = PlayerCoordsAPI.getConfig().enabled;
40-
41-
// If enabled and server not started, start server
42-
if (configEnabled && !serverStarted) {
43-
startServer();
44-
}
45-
46-
// If disabled and server is running, stop server
47-
if (!configEnabled && serverStarted) {
48-
stopServer();
49-
}
46+
handleConfigState(PlayerCoordsAPI.getConfig().enabled);
5047
});
5148

5249
ClientWorldEvents.AFTER_CLIENT_WORLD_CHANGE.register((client, world) -> updateSnapshot(client));
@@ -59,6 +56,26 @@ public void onInitializeClient() {
5956
PlayerCoordsAPI.LOGGER.info("Registered config monitor");
6057
}
6158

59+
private void handleConfigState(boolean configEnabled) {
60+
if (configEnabled != lastConfigEnabled) {
61+
lastConfigEnabled = configEnabled;
62+
63+
if (configEnabled) {
64+
nextStartAttemptAt = 0L;
65+
tryStartServer();
66+
} else {
67+
nextStartAttemptAt = 0L;
68+
stopServer();
69+
}
70+
71+
return;
72+
}
73+
74+
if (configEnabled && !serverStarted) {
75+
tryStartServer();
76+
}
77+
}
78+
6279
private void updateSnapshot(MinecraftClient client) {
6380
PlayerEntity player = client.player;
6481
ClientWorld worldObj = client.world;
@@ -81,7 +98,7 @@ private void updateSnapshot(MinecraftClient client) {
8198
player.getPitch(),
8299
worldObj.getRegistryKey().getValue().toString(),
83100
biome,
84-
player.getUuid().toString(),
101+
player.getUuidAsString(),
85102
player.getName().getString()
86103
);
87104
}
@@ -90,39 +107,60 @@ private void clearSnapshot() {
90107
latestSnapshot = null;
91108
}
92109

93-
private void startServer() {
94-
if (serverStarted) return;
110+
private void tryStartServer() {
111+
if (serverStarted) {
112+
return;
113+
}
114+
115+
long now = System.currentTimeMillis();
116+
117+
if (now < nextStartAttemptAt) {
118+
return;
119+
}
95120

96121
try {
97122
PlayerCoordsAPI.LOGGER.info("Starting PlayerCoordsAPI HTTP server on port " + PORT);
98-
server = HttpServer.create(new InetSocketAddress(PORT), 0);
123+
server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), PORT), 0);
99124
server.createContext("/api/coords", this::handleCoordsRequest);
100-
server.setExecutor(Executors.newSingleThreadExecutor());
125+
serverExecutor = Executors.newSingleThreadExecutor();
126+
server.setExecutor(serverExecutor);
101127
server.start();
102128
serverStarted = true;
129+
nextStartAttemptAt = 0L;
103130
PlayerCoordsAPI.LOGGER.info("PlayerCoordsAPI HTTP server started successfully");
104131
} catch (IOException e) {
105-
PlayerCoordsAPI.LOGGER.error("Failed to start PlayerCoordsAPI HTTP server", e);
132+
cleanupServerResources();
133+
nextStartAttemptAt = now + START_RETRY_DELAY_MS;
134+
PlayerCoordsAPI.LOGGER.warn(
135+
"Failed to start PlayerCoordsAPI HTTP server, retrying in {} seconds",
136+
START_RETRY_DELAY_MS / 1000,
137+
e
138+
);
106139
}
107140
}
108141

109142
private void stopServer() {
143+
if (server == null && serverExecutor == null) {
144+
return;
145+
}
146+
147+
PlayerCoordsAPI.LOGGER.info("Stopping PlayerCoordsAPI HTTP server");
148+
cleanupServerResources();
149+
PlayerCoordsAPI.LOGGER.info("PlayerCoordsAPI HTTP server stopped successfully");
150+
}
151+
152+
private void cleanupServerResources() {
110153
if (server != null) {
111-
PlayerCoordsAPI.LOGGER.info("Stopping PlayerCoordsAPI HTTP server");
112-
113-
// Create a separate thread to stop the server to prevent blocking
114-
final HttpServer serverToStop = server; // Create a final reference for the thread
115-
Thread stopThread = new Thread(() -> {
116-
serverToStop.stop(0); // Stop with no delay
117-
PlayerCoordsAPI.LOGGER.info("PlayerCoordsAPI HTTP server stopped successfully");
118-
});
119-
stopThread.setDaemon(true);
120-
stopThread.start();
121-
122-
// Set variables immediately so we know the server is being stopped
154+
server.stop(0);
123155
server = null;
124-
serverStarted = false;
125156
}
157+
158+
if (serverExecutor != null) {
159+
serverExecutor.shutdown();
160+
serverExecutor = null;
161+
}
162+
163+
serverStarted = false;
126164
}
127165

128166
private void handleCoordsRequest(HttpExchange exchange) throws IOException {
@@ -133,8 +171,8 @@ private void handleCoordsRequest(HttpExchange exchange) throws IOException {
133171
}
134172

135173
// Check if the client is allowed to access (only localhost)
136-
String remoteAddress = exchange.getRemoteAddress().getAddress().getHostAddress();
137-
if (!remoteAddress.equals("127.0.0.1") && !remoteAddress.equals("0:0:0:0:0:0:0:1")) {
174+
InetAddress remoteAddress = exchange.getRemoteAddress().getAddress();
175+
if (remoteAddress == null || !remoteAddress.isLoopbackAddress()) {
138176
sendResponse(exchange, 403, "{\"error\": \"Access denied\"}");
139177
return;
140178
}
@@ -153,16 +191,43 @@ private void sendResponse(HttpExchange exchange, int statusCode, String response
153191
exchange.getResponseHeaders().set("Access-Control-Allow-Methods", "GET, OPTIONS");
154192
exchange.getResponseHeaders().set("Access-Control-Allow-Headers", "Content-Type, Authorization");
155193

156-
// Set content type if response is not null
157194
if (response != null) {
158-
exchange.getResponseHeaders().set("Content-Type", "application/json");
159-
exchange.sendResponseHeaders(statusCode, response.length());
195+
byte[] responseBytes = response.getBytes(StandardCharsets.UTF_8);
196+
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
197+
exchange.sendResponseHeaders(statusCode, responseBytes.length);
160198
try (OutputStream os = exchange.getResponseBody()) {
161-
os.write(response.getBytes());
199+
os.write(responseBytes);
162200
}
163201
} else {
164-
exchange.sendResponseHeaders(statusCode, -1); // No response body
202+
exchange.sendResponseHeaders(statusCode, -1);
203+
}
204+
}
205+
206+
private static String escapeJson(String value) {
207+
StringBuilder escaped = new StringBuilder(value.length() + 16);
208+
209+
for (int i = 0; i < value.length(); i++) {
210+
char ch = value.charAt(i);
211+
212+
switch (ch) {
213+
case '\\' -> escaped.append("\\\\");
214+
case '"' -> escaped.append("\\\"");
215+
case '\b' -> escaped.append("\\b");
216+
case '\f' -> escaped.append("\\f");
217+
case '\n' -> escaped.append("\\n");
218+
case '\r' -> escaped.append("\\r");
219+
case '\t' -> escaped.append("\\t");
220+
default -> {
221+
if (ch < 0x20) {
222+
escaped.append(String.format(Locale.ROOT, "\\u%04x", (int) ch));
223+
} else {
224+
escaped.append(ch);
225+
}
226+
}
227+
}
165228
}
229+
230+
return escaped.toString();
166231
}
167232

168233
private record PlayerSnapshot(
@@ -179,7 +244,15 @@ private record PlayerSnapshot(
179244
private String toJson() {
180245
return String.format(Locale.US,
181246
"{\"x\": %.2f, \"y\": %.2f, \"z\": %.2f, \"yaw\": %.2f, \"pitch\": %.2f, \"world\": \"%s\", \"biome\": \"%s\", \"uuid\": \"%s\", \"username\": \"%s\"}",
182-
x, y, z, yaw, pitch, world, biome, uuid, username
247+
x,
248+
y,
249+
z,
250+
yaw,
251+
pitch,
252+
escapeJson(world),
253+
escapeJson(biome),
254+
escapeJson(uuid),
255+
escapeJson(username)
183256
);
184257
}
185258
}

src/main/java/fr/sukikui/playercoordsapi/PlayerCoordsAPI.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ public void onInitialize() {
2828
// However, some things (like resources) may still be uninitialized.
2929
// Proceed with mild caution.
3030

31-
LOGGER.info("PlayerCoordsAPI initialized - API will be available at http://localhost:25565/api when enabled");
31+
LOGGER.info("PlayerCoordsAPI initialized - API will be available at http://localhost:25565/api/coords when enabled");
3232
}
3333

3434
public static ModConfig getConfig() {
3535
return config;
3636
}
37-
}
37+
}

0 commit comments

Comments
 (0)