fix(items): watcher registers split-tier subdirs, real stop()/close, key.reset guard

This commit is contained in:
simoleo89
2026-06-04 21:56:01 +02:00
parent 8fb117ae73
commit 4944d41410
2 changed files with 47 additions and 10 deletions
@@ -6,7 +6,11 @@ import com.eu.habbo.messages.outgoing.furniture.FurnitureDataReloadComposer;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.ClosedWatchServiceException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystems; import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardWatchEventKinds; import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchKey; import java.nio.file.WatchKey;
@@ -17,7 +21,8 @@ import java.util.List;
* Watches the furnidata source on a single daemon thread. On change (debounced), * Watches the furnidata source on a single daemon thread. On change (debounced),
* re-indexes via the provider and broadcasts only the delta — or a compact * re-indexes via the provider and broadcasts only the delta — or a compact
* reload-hint when the delta exceeds the cap. A minimum interval throttles bursts. * reload-hint when the delta exceeds the cap. A minimum interval throttles bursts.
* Never throws out of the loop. * For the split-tier directory layout, the base dir AND its immediate
* subdirectories are registered. Never throws out of the loop.
*/ */
public class FurnidataWatcher { public class FurnidataWatcher {
@@ -25,17 +30,20 @@ public class FurnidataWatcher {
private final FurnitureTextProvider provider; private final FurnitureTextProvider provider;
private final Path watchDir; private final Path watchDir;
private final boolean sourceIsDir;
private final long maxBytes; private final long maxBytes;
private final long debounceMs; private final long debounceMs;
private final long minIntervalMs; private final long minIntervalMs;
private final int deltaCap; private final int deltaCap;
private volatile boolean running = false; private volatile boolean running = false;
private volatile WatchService ws;
private long lastBroadcast = 0L; private long lastBroadcast = 0L;
public FurnidataWatcher(FurnitureTextProvider provider, Path source, long maxBytes) { public FurnidataWatcher(FurnitureTextProvider provider, Path source, long maxBytes) {
this.provider = provider; this.provider = provider;
this.watchDir = java.nio.file.Files.isDirectory(source) ? source : source.getParent(); this.sourceIsDir = Files.isDirectory(source);
this.watchDir = this.sourceIsDir ? source : source.getParent();
this.maxBytes = maxBytes; this.maxBytes = maxBytes;
this.debounceMs = Long.parseLong(Emulator.getConfig().getValue("items.furnidata.watch.debounce.ms", "750")); this.debounceMs = Long.parseLong(Emulator.getConfig().getValue("items.furnidata.watch.debounce.ms", "750"));
this.minIntervalMs = Long.parseLong(Emulator.getConfig().getValue("items.furnidata.watch.min.interval.ms", "5000")); this.minIntervalMs = Long.parseLong(Emulator.getConfig().getValue("items.furnidata.watch.min.interval.ms", "5000"));
@@ -52,20 +60,30 @@ public class FurnidataWatcher {
public void stop() { public void stop() {
this.running = false; this.running = false;
WatchService local = this.ws;
if (local != null) {
try { local.close(); } catch (IOException ignored) { }
}
} }
private void run() { private void run() {
try (WatchService ws = FileSystems.getDefault().newWatchService()) { try {
this.watchDir.register(ws, StandardWatchEventKinds.ENTRY_MODIFY, this.ws = FileSystems.getDefault().newWatchService();
StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE); } catch (IOException e) {
LOGGER.warn("FurnidataWatcher: could not create WatchService", e);
return;
}
try (WatchService service = this.ws) {
registerDirs(service);
while (this.running) { while (this.running) {
WatchKey key = ws.take(); WatchKey key = service.take();
key.pollEvents(); key.pollEvents();
Thread.sleep(this.debounceMs); Thread.sleep(this.debounceMs);
key.pollEvents(); key.pollEvents();
key.reset(); if (!key.reset()) {
LOGGER.warn("FurnidataWatcher: watch key invalidated (directory removed?) — stopping");
break;
}
try { try {
onChange(); onChange();
} catch (Exception e) { } catch (Exception e) {
@@ -74,11 +92,29 @@ public class FurnidataWatcher {
} }
} catch (InterruptedException ignored) { } catch (InterruptedException ignored) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} catch (ClosedWatchServiceException ignored) {
// stop() closed the service — normal shutdown
} catch (Exception e) { } catch (Exception e) {
LOGGER.warn("FurnidataWatcher stopped", e); LOGGER.warn("FurnidataWatcher stopped", e);
} }
} }
/** Register the base dir, plus one level of subdirectories for the split-tier layout. */
private void registerDirs(WatchService service) throws IOException {
this.watchDir.register(service, StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE);
if (this.sourceIsDir) {
try (DirectoryStream<Path> ds = Files.newDirectoryStream(this.watchDir)) {
for (Path child : ds) {
if (Files.isDirectory(child)) {
child.register(service, StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE);
}
}
}
}
}
private void onChange() { private void onChange() {
Path source = this.provider.getSource(); Path source = this.provider.getSource();
if (source == null) return; if (source == null) return;
@@ -88,7 +124,7 @@ public class FurnidataWatcher {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
if (now - this.lastBroadcast < this.minIntervalMs) { if (now - this.lastBroadcast < this.minIntervalMs) {
LOGGER.info("FurnidataWatcher: {} changes throttled (min interval)", delta.size()); LOGGER.info("FurnidataWatcher: {} changes indexed but broadcast skipped (min interval) — clients update on next change or reconnect", delta.size());
return; return;
} }
this.lastBroadcast = now; this.lastBroadcast = now;
@@ -51,6 +51,7 @@ public class FurnitureTextProvider {
LOGGER.info("FurnitureTextProvider: indexed {} furnidata names from {}", this.index.size(), this.source); LOGGER.info("FurnitureTextProvider: indexed {} furnidata names from {}", this.index.size(), this.source);
if (Boolean.parseBoolean(Emulator.getConfig().getValue("items.furnidata.watch.enabled", "true"))) { if (Boolean.parseBoolean(Emulator.getConfig().getValue("items.furnidata.watch.enabled", "true"))) {
if (this.watcher != null) this.watcher.stop();
this.watcher = new FurnidataWatcher(this, this.source, DEFAULT_MAX_BYTES); this.watcher = new FurnidataWatcher(this, this.source, DEFAULT_MAX_BYTES);
this.watcher.start(); this.watcher.start();
} }