Skip to content
Open
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 @@ -124,30 +124,38 @@ public static AetherResolver of(@NotNull String repository) {
String id = file.getParentFile().getParentFile().getPath()
+ ":" + PrimitiveSettings.IS_ISOLATED_MODE // 区分类加载器 (隔离类加载器或插件类加载器)
+ ":" + (relocation != null ? relocation.hashCode() : 0); // 区分不同的重定向规则
if (injectedDependencies.contains(id)) return null;
else injectedDependencies.add(id);
// 如果没有重定向规则,直接注入
if (relocation == null || relocation.isEmpty()) {
return ClassAppender.addPath(file.toPath(), PrimitiveSettings.IS_ISOLATED_MODE, isExternal);
} else {
// 获取重定向后的文件
String name = file.getName().substring(0, file.getName().lastIndexOf('.'));
File rel = new File(file.getParentFile(), name + "_r2_" + Math.abs(relocation.hashCode()) + ".jar");
// 如果文件不存在或者文件大小为 0,就执行重定向逻辑
if (!rel.exists() || rel.length() == 0) {
try {
// 获取重定向规则
List<Relocation> rules = relocation.stream().map(JarRelocation::toRelocation).collect(Collectors.toList());
// 获取临时文件
File tempSourceFile = PrimitiveIO.copyFile(file, File.createTempFile(file.getName(), ".jar"));
// 运行
new JarRelocator(tempSourceFile, rel, rules).run();
} catch (IOException e) {
throw new IllegalStateException(String.format("Unable to relocate %s%n", file), e);
if (!injectedDependencies.add(id)) return null;
try {
// 如果没有重定向规则,直接注入
if (relocation == null || relocation.isEmpty()) {
return ClassAppender.addPath(file.toPath(), PrimitiveSettings.IS_ISOLATED_MODE, isExternal);
} else {
// 获取重定向后的文件
String name = file.getName().substring(0, file.getName().lastIndexOf('.'));
File rel = new File(file.getParentFile(), name + "_r2_" + Math.abs(relocation.hashCode()) + ".jar");
// 如果文件不存在或者文件大小为 0,就执行重定向逻辑
if (!rel.exists() || rel.length() == 0) {
File tempSourceFile = File.createTempFile(file.getName(), ".jar");
try {
// 获取重定向规则
List<Relocation> rules = relocation.stream().map(JarRelocation::toRelocation).collect(Collectors.toList());
PrimitiveIO.copyFile(file, tempSourceFile);
new JarRelocator(tempSourceFile, rel, rules).run();
} catch (IOException e) {
throw new IllegalStateException(String.format("Unable to relocate %s%n", file), e);
} finally {
if (!tempSourceFile.delete()) {
tempSourceFile.deleteOnExit();
}
}
}
// 注入重定向后的文件
return ClassAppender.addPath(rel.toPath(), PrimitiveSettings.IS_ISOLATED_MODE, isExternal);
}
// 注入重定向后的文件
return ClassAppender.addPath(rel.toPath(), PrimitiveSettings.IS_ISOLATED_MODE, isExternal);
} catch (Throwable ex) {
// 注入失败后允许后续调用重试,避免失败状态永久污染缓存。
injectedDependencies.remove(id);
throw ex;
}
}
}
3 changes: 2 additions & 1 deletion common-legacy-api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
dependencies {
compileOnly(project(":common"))
compileOnly(project(":common-env"))
testImplementation(project(":common"))
compileOnly(project(":common-platform-api"))
compileOnly(project(":common-util"))
}
}
97 changes: 66 additions & 31 deletions common-legacy-api/src/main/java/taboolib/common5/FileWatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;

/**
Expand Down Expand Up @@ -50,6 +51,11 @@ public class FileWatcher {
*/
private final WatchService watchService;

/**
* 监听器是否已经释放
*/
private final AtomicBoolean released = new AtomicBoolean(false);

public FileWatcher(int interval) {
WatchService ws;
try {
Expand All @@ -64,29 +70,41 @@ public FileWatcher(int interval) {
this.watchService = ws;
if (this.watchService != null) {
this.executorService.scheduleAtFixedRate(() -> {
WatchKey key;
while ((key = watchService.poll()) != null) {
WatchKey finalKey = key;
key.pollEvents().forEach(event -> {
if (event.context() instanceof Path) {
Path changedPath = (Path) event.context();
// 通过 WatchKey 获取监听的目录,构建完整路径
Path watchedPath = (Path) finalKey.watchable();
Path fullChangedPath = watchedPath.resolve(changedPath);
try {
WatchKey key;
while ((key = watchService.poll()) != null) {
WatchKey finalKey = key;
key.pollEvents().forEach(event -> {
if (event.context() instanceof Path) {
Path changedPath = (Path) event.context();
// 通过 WatchKey 获取监听的目录,构建完整路径
Path watchedPath = (Path) finalKey.watchable();
Path fullChangedPath = watchedPath.resolve(changedPath).toAbsolutePath().normalize();
fileListenerMap.forEach((file, listener) -> {
try {
listener.handleEvent(fullChangedPath);
} catch (Throwable ex) {
ex.printStackTrace();
}
});
}
});
if (!key.reset()) {
fileListenerMap.forEach((file, listener) -> {
try {
listener.handleEvent(fullChangedPath);
} catch (Throwable ex) {
ex.printStackTrace();
if (listener.watchKey == finalKey) {
fileListenerMap.remove(file, listener);
}
});
}
});
key.reset();
}
} catch (ClosedWatchServiceException ignored) {
// 正常释放时关闭 WatchService,会终止后续轮询
}
}, 1000, interval, TimeUnit.MILLISECONDS);
// 注册关闭回调
TabooLib.registerLifeCycleTask(LifeCycle.DISABLE, 0, this::release);
} else {
this.executorService.shutdownNow();
}
}

Expand All @@ -108,14 +126,19 @@ public void addSimpleListener(File file, Consumer<File> runnable) {
* @param runImmediately 是否在添加监听器时立即执行一次
*/
public void addSimpleListener(File file, Consumer<File> runnable, boolean runImmediately) {
if (watchService == null) {
if (watchService == null || released.get()) {
return;
}
if (runImmediately) {
runnable.accept(file);
}
try {
fileListenerMap.put(file, new FileListener(file, runnable, this));
File canonicalFile = file.getCanonicalFile();
FileListener listener = new FileListener(canonicalFile, runnable, this);
FileListener previous = fileListenerMap.put(canonicalFile, listener);
if (previous != null) {
previous.cancel();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
Expand All @@ -127,7 +150,13 @@ public void addSimpleListener(File file, Consumer<File> runnable, boolean runImm
* @param file 要移除监听的文件
*/
public void removeListener(File file) {
FileListener listener = fileListenerMap.remove(file);
File canonicalFile;
try {
canonicalFile = file.getCanonicalFile();
} catch (IOException ignored) {
canonicalFile = file.getAbsoluteFile();
}
FileListener listener = fileListenerMap.remove(canonicalFile);
if (listener != null) {
listener.cancel();
}
Expand All @@ -137,8 +166,18 @@ public void removeListener(File file) {
* 释放资源
*/
public void release() {
executorService.shutdown();
if (!released.compareAndSet(false, true)) {
return;
}
fileListenerMap.values().forEach(FileListener::cancel);
fileListenerMap.clear();
if (watchService != null) {
try {
watchService.close();
} catch (IOException ignored) {
}
}
executorService.shutdownNow();
}

/**
Expand Down Expand Up @@ -170,29 +209,25 @@ static class FileListener {
}

public void handleEvent(Path fullChangedPath) {
Path watchedFile = file.toPath().toAbsolutePath().normalize();
Path changedFile = fullChangedPath.toAbsolutePath().normalize();
// 监听目录
if (file.isDirectory()) {
try {
// 使用 relativize 检查路径关系,更加准确
file.toPath().relativize(fullChangedPath);
callback.accept(fullChangedPath.toFile());
} catch (IllegalArgumentException ignored) {
// 如果不是子路径,会抛出异常,直接忽略
if (changedFile.startsWith(watchedFile)) {
callback.accept(changedFile.toFile());
}
}
// 监听文件
else if (isSameFile(fullChangedPath, file.toPath())) {
callback.accept(fullChangedPath.toFile());
// 监听文件。删除事件发生时目标文件已不存在,Files.isSameFile 会失败,
// 因此先比较规范化路径,再用 isSameFile 兼容符号链接。
else if (changedFile.equals(watchedFile) || isSameFile(changedFile, watchedFile)) {
callback.accept(changedFile.toFile());
}
}

public boolean isSameFile(Path path1, Path path2) {
try {
// 使用 Files.isSameFile() 判断两个路径是否指向同一个文件
// 该方法会考虑符号链接等情况
return Files.isSameFile(path1, path2);
} catch (IOException e) {
// 如果出现 IO 异常则返回 false
return false;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package taboolib.common5

import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.nio.file.Files
import java.nio.file.Path
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

class FileWatcherTest {

@TempDir
lateinit var tempDirectory: Path

@Test
fun `file deletion is reported and watcher can be released repeatedly`() {
val file = Files.write(tempDirectory.resolve("watched.txt"), byteArrayOf(1)).toFile()
val deleted = CountDownLatch(1)
val watcher = FileWatcher(20)
try {
watcher.addSimpleListener(file, { changed ->
if (changed.absoluteFile == file.absoluteFile && !changed.exists()) {
deleted.countDown()
}
})
Files.delete(file.toPath())

assertTrue(deleted.await(5, TimeUnit.SECONDS))
} finally {
watcher.release()
watcher.release()
FileWatcher.INSTANCE.release()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ import taboolib.common.platform.function.isPrimaryThread
import taboolib.common.platform.function.submit
import java.util.concurrent.CompletableFuture

internal fun <T> CompletableFuture<T>.completeWith(func: () -> T) {
try {
complete(func())
} catch (ex: Throwable) {
completeExceptionally(ex)
}
}

/**
* 在异步线程执行一个同步任务,并等待其完成
*
Expand All @@ -15,7 +23,7 @@ fun <T> sync(func: () -> T): T {
error("Cannot run sync task in main thread.")
}
val future = CompletableFuture<T>()
submit { future.complete(func()) }
submit { future.completeWith(func) }
return future.join()
}

Expand All @@ -30,6 +38,6 @@ fun <T> runSync(func: () -> T): T {
return func()
}
val future = CompletableFuture<T>()
submit { future.complete(func()) }
submit { future.completeWith(func) }
return future.join()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package taboolib.common.util

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertSame
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionException

class SyncExecutorTest {

@Test
fun `completeWith completes successful result`() {
val future = CompletableFuture<Int>()

future.completeWith { 42 }

assertEquals(42, future.join())
}

@Test
fun `completeWith propagates task failure`() {
val future = CompletableFuture<Int>()
val failure = IllegalStateException("boom")

future.completeWith { throw failure }

val thrown = assertThrows<CompletionException> { future.join() }
assertSame(failure, thrown.cause)
}
}
Loading
Loading