diff --git a/companion/README.md b/companion/README.md index c9552084..3cf0a697 100644 --- a/companion/README.md +++ b/companion/README.md @@ -46,3 +46,11 @@ Contact sheets and individual captures are written under `companion/build/ui-scr ## Integrations and storage The [MCP API](MCP.md) exposes source queries, Java execution and debugger operations to trusted local clients. The [storage guide](https://github.com/Minecraft-TA/TotalDebug/blob/1.21.1/docs/STORAGE.md) describes scripts, settings, persisted debugger state and generated caches. + +## Runtime ownership + +`CompanionApp` publishes one `RuntimeBinding` for the installed inventory. The binding groups its identity, source catalog, classpath, decompiler and reference search, and owns the native index after installation succeeds. `CompanionClassIndex` is only JDT's process-wide lookup hook; setting or clearing it never closes an index. + +The index loader retains ownership while a candidate is prepared. The application detaches the previous runtime, attaches the new compiler/insight bindings, and completes publication under the existing lifecycle lock. Debugger and UI follow-up runs afterward and cannot return an installed index to the loader's failure cleanup. Closing a runtime detaches its consumers before releasing the index, and is idempotent. A rejected candidate closes its own prepared consumers while leaving index disposal to the loader. + +The script compiler and code-insight worker remain application-lived. Open local editors retain the code-insight service, so a runtime changes its binding rather than replacing that service instance. Project admission and navigation invalidation retain their existing guards; the planned project-scope migration is a separate slice. Stateless JDT parsing is in `JavaAst`; editor analysis/listeners remain in `ASTCache`. diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/CompanionApp.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/CompanionApp.java index c98bd4c7..48e9d417 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/CompanionApp.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/CompanionApp.java @@ -33,6 +33,7 @@ import com.github.minecraft_ta.totalDebugCompanion.navigation.NavigationTarget; import com.github.minecraft_ta.totalDebugCompanion.navigation.NavigationTargets; import com.github.minecraft_ta.totalDebugCompanion.runtime.RuntimeIndexService; +import com.github.minecraft_ta.totalDebugCompanion.runtime.RuntimeBinding; import com.github.minecraft_ta.totaldebug.storage.RuntimeInventory; import com.github.minecraft_ta.totalDebugCompanion.runtime.RuntimeSourceCatalog; import com.github.minecraft_ta.totalDebugCompanion.search.insight.CodeInsightService; @@ -89,16 +90,12 @@ public final class CompanionApp { private static CompanionSession session; private static CompanionLaunchConfiguration launchConfiguration; private static volatile CompanionProfile profile; - private static volatile CompanionDecompilationService decompilationService; private static volatile InstanceState instanceState = InstanceState.inMemory(); - private static volatile ReferenceSearchService referenceSearchService; - private static volatile CodeInsightService codeInsightService; - private static volatile RuntimeSourceCatalog runtimeSourceCatalog = RuntimeSourceCatalog.empty(); + private static volatile RuntimeBinding runtime; + private static final CodeInsightService codeInsightService = new CodeInsightService( + () -> { throw new IllegalStateException("Runtime class index is not ready"); }, RuntimeSourceCatalog.empty()); private static RuntimeIndexService runtimeIndexService; private static final ScriptCompilationService scriptCompiler = new ScriptCompilationService(CompanionApp::send, CompanionApp::send); - private static volatile String evaluationClasspath; - private static volatile Path activeIndexFile; - private static volatile String activeRuntimeSignature; private static final List pendingNavigations = new ArrayList<>(); private static CompanionMcpServer mcpServer; private static volatile DebuggerSessionController debuggerController; @@ -279,11 +276,9 @@ public void debugTarget(DebugTargetMessage message) { if (debuggerController != null) { RuntimePhase.run("close.debugger", debuggerController::close); } - RuntimePhase.run("close.decompilation", CompanionApp::closeDecompilationService); - RuntimePhase.run("close.references", CompanionApp::closeReferenceSearchService); - RuntimePhase.run("close.code-insight", CompanionApp::closeCodeInsightService); + RuntimePhase.run("close.runtime", CompanionApp::closeRuntime); + RuntimePhase.run("close.code-insight", codeInsightService::close); RuntimePhase.run("close.script-compiler", scriptCompiler::close); - RuntimePhase.run("close.index", CompanionClassIndex::close); RuntimePhase.run("close.ui", CompanionApp::stopUiAfterFailure); try (var state = RuntimePhase.start("close.state")) { GlobalConfig.getInstance().saveNow(); @@ -434,14 +429,7 @@ private static void activateProfile(CompanionProfile requested) throws IOExcepti getDebuggerController().setBreakpointsMuted(instanceState.debuggerBreakpointsMuted()).join(); getDebuggerController().setExceptionBreakpoints(instanceState.breakOnCaughtExceptions(), instanceState.breakOnUncaughtExceptions()).join(); - closeDecompilationService(); - closeReferenceSearchService(); - invalidateCodeInsightService(); - scriptCompiler.bind(null); - CompanionClassIndex.close(); - activeIndexFile = null; - activeRuntimeSignature = null; - runtimeSourceCatalog = RuntimeSourceCatalog.empty(); + closeRuntime(); } profile = requested; setupDataDirectories(); @@ -489,52 +477,56 @@ private static void installRuntimeSnapshot(RuntimeIndexService.ReadySnapshot sna private static synchronized void installRuntimeSnapshot(RuntimeIndexService.ReadySnapshot snapshot, RuntimeSnapshotBytecodeSource bytecodeSource) { + if (switchingProjects) throw new IllegalStateException("Project is switching"); CompanionProfile current = requireProfile(); - closeDecompilationService(); - CompanionDecompilationService replacement; + RuntimeBinding replacement; try { - replacement = new CompanionDecompilationService( - snapshot.signature(), - current.dataDirectory(), - bytecodeSource - ); - } catch (IOException | RuntimeException exception) { - throw new IllegalStateException("Unable to activate the runtime class index", exception); - } - - closeReferenceSearchService(); - RuntimeSourceCatalog sourceCatalog = new RuntimeSourceCatalog(snapshot.sources()); - runtimeSourceCatalog = sourceCatalog; - evaluationClasspath = snapshot.sources().stream().map(source -> source.path().toString()) - .collect(java.util.stream.Collectors.joining(java.io.File.pathSeparator)); - CodeInsightService currentInsightService = codeInsightService; - if (currentInsightService != null) { - currentInsightService.rebind(snapshot::index, sourceCatalog); - } - scriptCompiler.bind(snapshot); - CompanionClassIndex.replace(snapshot.index()); - decompilationService = replacement; - referenceSearchService = new ReferenceSearchService( - CompanionClassIndex::get, - sourceCatalog - ); - if (currentInsightService == null) { - codeInsightService = new CodeInsightService(snapshot::index, sourceCatalog); + replacement = new RuntimeBinding(snapshot, current.dataDirectory(), bytecodeSource, scriptCompiler, codeInsightService); + } catch (IOException exception) { + throw new IllegalStateException("Unable to prepare the runtime class index", exception); } - activeIndexFile = snapshot.indexFile(); - activeRuntimeSignature = snapshot.signature(); - getDebuggerController().replaceBreakpointDefinitions( - restoreBreakpoints(snapshot.signature()) - ).join(); - if (uiStarted) { - MainWindow.INSTANCE.refreshRuntimeSources(); + try { + closeRuntime(); + replacement.attach(); + // Queue before publication: rejected scheduling still leaves ownership with the loader. + // The follow-up acquires this lock after the loader finishes its installation callback. + projectWorker.execute(() -> finishRuntimeInstallation(replacement, current)); + CompanionClassIndex.set(snapshot.index()); + runtime = replacement; + replacement.acceptOwnership(); + } catch (RuntimeException failure) { + replacement.close(); + throw failure; } - prewarmJavaParser(); + } - List queued = List.copyOf(pendingNavigations); - pendingNavigations.clear(); - for (PendingNavigation pending : queued) { - MainWindow.INSTANCE.navigation().navigate(pending.target(), pending.activation()); + private static void finishRuntimeInstallation(RuntimeBinding installed, CompanionProfile selected) { + try { + CompletableFuture breakpoints; + synchronized (CompanionApp.class) { + if (switchingProjects || runtime != installed || profile != selected) return; + breakpoints = getDebuggerController().replaceBreakpointDefinitions( + restoreBreakpoints(installed.snapshot().signature())); + } + // A failed debugger/UI refresh must never return ownership of an installed index to its loader. + breakpoints.join(); + SwingUtilities.invokeLater(() -> { + if (switchingProjects || runtime != installed || profile != selected) return; + if (uiStarted) MainWindow.INSTANCE.refreshRuntimeSources(); + List queued; + synchronized (CompanionApp.class) { + if (switchingProjects || runtime != installed || profile != selected) return; + queued = List.copyOf(pendingNavigations); + pendingNavigations.clear(); + } + for (PendingNavigation pending : queued) { + MainWindow.INSTANCE.navigation().navigate(pending.target(), pending.activation()); + } + }); + prewarmJavaParser(); + } catch (RuntimeException failure) { + System.getLogger(CompanionApp.class.getName()).log(System.Logger.Level.WARNING, + "Runtime installed, but debugger refresh failed", failure); } } @@ -703,11 +695,10 @@ private static Map runtimeContext() { Map context = new java.util.LinkedHashMap<>(); context.put("profile_id", current.id()); context.put("workspace_directory", current.workspaceDirectory().toString()); - if (activeRuntimeSignature != null) { - context.put("runtime_signature", activeRuntimeSignature); - } - if (activeIndexFile != null) { - context.put("index_file", activeIndexFile.toString()); + RuntimeBinding installed = runtime; + if (installed != null) { + context.put("runtime_signature", installed.snapshot().signature()); + context.put("index_file", installed.snapshot().indexFile().toString()); } return Map.copyOf(context); } @@ -820,7 +811,8 @@ public static boolean hasProfile() { } public static String getActiveRuntimeSignature() { - return activeRuntimeSignature; + RuntimeBinding current = runtime; + return current == null ? null : current.snapshot().signature(); } public static boolean send(AbstractMessage message) { @@ -860,10 +852,10 @@ public static void openClass(String binaryName, int targetType, String targetIde private static void openOrQueue(NavigationTarget target, NavigationService.Activation activation) { if (switchingProjects) return; - if (decompilationService == null) { + if (runtime == null) { synchronized (CompanionApp.class) { if (switchingProjects) return; - if (decompilationService == null) { + if (runtime == null) { pendingNavigations.add(new PendingNavigation(target, activation)); return; } @@ -888,7 +880,8 @@ public static void openDebugFrame( } private static DebugEngine.Source loadDebugSource(String binaryName) throws IOException { - CompanionDecompilationService service = decompilationService; + RuntimeBinding current = runtime; + CompanionDecompilationService service = current == null ? null : current.decompiler(); return service == null ? null : service.loadDebugSource(binaryName); } @@ -913,7 +906,8 @@ public static Path getWorkspaceDirectory() { } public static ReferenceSearchService getReferenceSearchService() { - ReferenceSearchService service = referenceSearchService; + RuntimeBinding current = runtime; + ReferenceSearchService service = current == null ? null : current.references(); if (service == null) { throw new IllegalStateException("Reference search is unavailable"); } @@ -922,18 +916,20 @@ public static ReferenceSearchService getReferenceSearchService() { public static CodeInsightService getCodeInsightService() { CodeInsightService service = codeInsightService; - if (service == null) { + if (runtime == null) { throw new IllegalStateException("Code insight is unavailable"); } return service; } public static RuntimeSourceCatalog getRuntimeSourceCatalog() { - return runtimeSourceCatalog; + RuntimeBinding current = runtime; + return current == null ? RuntimeSourceCatalog.empty() : current.sources(); } public static CompanionDecompilationService getDecompilationService() { - CompanionDecompilationService service = decompilationService; + RuntimeBinding current = runtime; + CompanionDecompilationService service = current == null ? null : current.decompiler(); if (service == null) { throw new IllegalStateException("Decompilation is unavailable"); } @@ -959,7 +955,7 @@ public static boolean isDebuggerConnected() { } private static DebuggerSessionController createDebuggerController() { - DebuggerSessionController controller = new DebuggerSessionController(CompanionApp::loadDebugSource, () -> evaluationClasspath, CompanionApp::loadBreakpointScript); + DebuggerSessionController controller = new DebuggerSessionController(CompanionApp::loadDebugSource, () -> { RuntimeBinding current = runtime; return current == null ? null : current.classpath(); }, CompanionApp::loadBreakpointScript); controller.setBreakpointsMuted(instanceState().debuggerBreakpointsMuted()).join(); controller.addListener(new DebuggerSessionController.Listener() { @Override @@ -1018,7 +1014,7 @@ private static List restoreBreak private static void persistBreakpoints(DebuggerSessionController controller) { if (switchingProjects) return; - String runtimeSignature = activeRuntimeSignature; + String runtimeSignature = getActiveRuntimeSignature(); if (runtimeSignature == null || runtimeSignature.isBlank()) { return; } @@ -1080,42 +1076,14 @@ private static CompanionProfile requireProfile() { return current; } - private static void closeReferenceSearchService() { - ReferenceSearchService service = referenceSearchService; - referenceSearchService = null; - if (service != null) { - service.close(); - } - } - - private static void closeCodeInsightService() { - CodeInsightService service = codeInsightService; - codeInsightService = null; - if (service != null) { - service.close(); - } - } - - private static void invalidateCodeInsightService() { - CodeInsightService service = codeInsightService; - if (service != null) { - service.rebind( - () -> { - throw new IllegalStateException("Runtime class index is not ready"); - }, - RuntimeSourceCatalog.empty() - ); - } - } - - private static void closeDecompilationService() { - evaluationClasspath = null; - CompanionDecompilationService service = decompilationService; - decompilationService = null; - if (service != null) { - service.close(); - if (uiStarted) { - MainWindow.INSTANCE.navigation().runtimeChanged(); + private static void closeRuntime() { + RuntimeBinding previous = runtime; + runtime = null; + CompanionClassIndex.clear(); + if (previous != null) { + try { previous.close(); } + finally { + if (uiStarted) MainWindow.INSTANCE.navigation().runtimeChanged(); } } } diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/debugger/DebuggerBreakpointResolver.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/debugger/DebuggerBreakpointResolver.java index c8818dd4..4520fb17 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/debugger/DebuggerBreakpointResolver.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/debugger/DebuggerBreakpointResolver.java @@ -1,6 +1,6 @@ package com.github.minecraft_ta.totalDebugCompanion.debugger; -import com.github.minecraft_ta.totalDebugCompanion.jdt.diagnostics.ASTCache; +import com.github.minecraft_ta.totalDebugCompanion.jdt.JavaAst; import com.github.minecraft_ta.totalDebugCompanion.jdt.symbol.CodeSymbol; import com.github.minecraft_ta.totalDebugCompanion.jdt.symbol.JavaSymbolResolver; import org.eclipse.jdt.core.dom.ASTVisitor; @@ -18,7 +18,7 @@ private DebuggerBreakpointResolver() { public static Optional resolve( DebugEngine.Source source, int line, String condition, String hitCondition ) { - return resolve(source, ASTCache.rawParse("DebuggerBreakpoint", source.contents()), + return resolve(source, JavaAst.parse("DebuggerBreakpoint", source.contents()), line, condition, hitCondition); } diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/decompile/SourceFileNavigation.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/decompile/SourceFileNavigation.java index e2da6de6..1212e7d9 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/decompile/SourceFileNavigation.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/decompile/SourceFileNavigation.java @@ -1,6 +1,6 @@ package com.github.minecraft_ta.totalDebugCompanion.decompile; -import com.github.minecraft_ta.totalDebugCompanion.jdt.diagnostics.ASTCache; +import com.github.minecraft_ta.totalDebugCompanion.jdt.JavaAst; import com.github.minecraft_ta.totalDebugCompanion.bytecode.RuntimeSnapshotBytecodeSource; import com.github.minecraft_ta.totalDebugCompanion.bytecode.reference.ReferenceLocation; import com.github.minecraft_ta.totalDebugCompanion.bytecode.reference.ReferenceQuery; @@ -53,7 +53,7 @@ public static int lineOffset(String source, int line) { public static int topLevelTypeOffset(String source) { Objects.requireNonNull(source, "source"); - var ast = ASTCache.rawParse(COMPILATION_UNIT_NAME, source); + var ast = JavaAst.parse(COMPILATION_UNIT_NAME, source); if (ast.types().isEmpty() || !(ast.types().getFirst() instanceof AbstractTypeDeclaration type)) { throw new IllegalStateException("Decompiled source has no top-level type"); } @@ -85,7 +85,7 @@ public static int usageOffset(String source, ReferenceLocation location, Referen Objects.requireNonNull(location, "location"); Objects.requireNonNull(query, "query"); - var ast = ASTCache.rawParse(COMPILATION_UNIT_NAME, source); + var ast = JavaAst.parse(COMPILATION_UNIT_NAME, source); if (ast.types().isEmpty() || !(ast.types().getFirst() instanceof AbstractTypeDeclaration type)) { throw new IllegalStateException("Decompiled source has no top-level type"); } @@ -187,7 +187,7 @@ private static int fieldOffset(Object declaration, String targetIdentifier) { public static int memberOffset(String source, RuntimeMember member) { Objects.requireNonNull(source, "source"); Objects.requireNonNull(member, "member"); - var ast = ASTCache.rawParse(COMPILATION_UNIT_NAME, source); + var ast = JavaAst.parse(COMPILATION_UNIT_NAME, source); if (ast.types().isEmpty() || !(ast.types().getFirst() instanceof AbstractTypeDeclaration type)) { throw new IllegalStateException("Decompiled source has no top-level type"); } diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/CompanionClassIndex.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/CompanionClassIndex.java index 0228f3b9..1bedd54f 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/CompanionClassIndex.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/CompanionClassIndex.java @@ -4,19 +4,15 @@ import java.util.Objects; +/** Process-wide lookup hook for JDT. Installed runtime resources own the native index. */ public final class CompanionClassIndex { private static volatile ClassIndex classIndex; private CompanionClassIndex() { } - public static synchronized void replace(ClassIndex replacement) { - Objects.requireNonNull(replacement, "replacement"); - ClassIndex previous = classIndex; - classIndex = replacement; - if (previous != null) { - previous.close(); - } + public static void set(ClassIndex replacement) { + classIndex = Objects.requireNonNull(replacement, "replacement"); } public static boolean isOpen() { @@ -31,12 +27,7 @@ public static ClassIndex get() { return index; } - public static synchronized void close() { - ClassIndex index = classIndex; + public static void clear() { classIndex = null; - if (index != null) { - index.close(); - } } - } diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/JavaAst.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/JavaAst.java new file mode 100644 index 00000000..04890cd2 --- /dev/null +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/JavaAst.java @@ -0,0 +1,20 @@ +package com.github.minecraft_ta.totalDebugCompanion.jdt; + +import com.github.minecraft_ta.totalDebugCompanion.jdt.impls.CompilationUnitImpl; +import org.eclipse.jdt.core.dom.ASTParser; +import org.eclipse.jdt.core.dom.CompilationUnit; + +/** Stateless parsing against the current JDT environment, independent of editor cache entries. */ +public final class JavaAst { + private JavaAst() {} + + public static CompilationUnit parse(String className, String contents) { + ASTParser parser = JdtConfiguration.createParser(); + parser.setSource(new CompilationUnitImpl(className, contents)); + parser.setResolveBindings(true); + parser.setStatementsRecovery(true); + parser.setKind(ASTParser.K_COMPILATION_UNIT); + return (CompilationUnit) parser.createAST(null); + } + +} diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/diagnostics/ASTCache.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/diagnostics/ASTCache.java index 41055560..23d2b0f7 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/diagnostics/ASTCache.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/jdt/diagnostics/ASTCache.java @@ -1,8 +1,6 @@ package com.github.minecraft_ta.totalDebugCompanion.jdt.diagnostics; -import com.github.minecraft_ta.totalDebugCompanion.jdt.impls.CompilationUnitImpl; -import com.github.minecraft_ta.totalDebugCompanion.jdt.JdtConfiguration; -import org.eclipse.jdt.core.dom.ASTParser; +import com.github.minecraft_ta.totalDebugCompanion.jdt.JavaAst; import org.eclipse.jdt.core.dom.CompilationUnit; import java.util.*; @@ -34,7 +32,7 @@ public static CompletableFuture update(String key, String className, Strin synchronized (CACHE) { if (CACHE.get(key) != selected || selected.version != finalVersion) return; } - var ast = rawParse(className, source.text()); + var ast = JavaAst.parse(className, source.text()); List> listeners; synchronized (CACHE) { var entry = CACHE.get(key); @@ -56,15 +54,6 @@ public static CompletableFuture update(String key, String className, Strin }); } - public static CompilationUnit rawParse(String className, String contents) { - ASTParser parser = JdtConfiguration.createParser(); - parser.setSource(new CompilationUnitImpl(className, contents)); - parser.setResolveBindings(true); - parser.setStatementsRecovery(true); - parser.setKind(ASTParser.K_COMPILATION_UNIT); - return (CompilationUnit) parser.createAST(null); - } - public static Runnable addChangeListener(String key, BiConsumer listener) { Objects.requireNonNull(key, "key"); Objects.requireNonNull(listener, "listener"); diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/mcp/CompanionMcpRuntimeSource.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/mcp/CompanionMcpRuntimeSource.java index ca32dcde..420a8783 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/mcp/CompanionMcpRuntimeSource.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/mcp/CompanionMcpRuntimeSource.java @@ -2,7 +2,7 @@ import com.github.minecraft_ta.totalDebugCompanion.decompile.CompanionDecompilationService; import com.github.minecraft_ta.totalDebugCompanion.decompile.DecompiledSource; -import com.github.minecraft_ta.totalDebugCompanion.jdt.diagnostics.ASTCache; +import com.github.minecraft_ta.totalDebugCompanion.jdt.JavaAst; import com.github.minecraft_ta.totalDebugCompanion.jdt.symbol.CodeSymbol; import com.github.minecraft_ta.totalDebugCompanion.jdt.symbol.JavaSymbolResolver; import org.eclipse.jdt.core.dom.ASTNode; @@ -53,7 +53,7 @@ Map source(Map requestedTarget) { } String contents = decompiled.contents(); - CompilationUnit unit = ASTCache.rawParse(COMPILATION_UNIT_NAME, contents); + CompilationUnit unit = JavaAst.parse(COMPILATION_UNIT_NAME, contents); AbstractTypeDeclaration type = findType(unit, decompiled.binaryName(), target.binaryName()); ASTNode scope = switch (target.kind()) { case "class" -> type; diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/runtime/RuntimeBinding.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/runtime/RuntimeBinding.java new file mode 100644 index 00000000..338c73a9 --- /dev/null +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/runtime/RuntimeBinding.java @@ -0,0 +1,85 @@ +package com.github.minecraft_ta.totalDebugCompanion.runtime; + +import com.github.minecraft_ta.totalDebugCompanion.bytecode.RuntimeSnapshotBytecodeSource; +import com.github.minecraft_ta.totalDebugCompanion.decompile.CompanionDecompilationService; +import com.github.minecraft_ta.totalDebugCompanion.script.ScriptCompilationService; +import com.github.minecraft_ta.totalDebugCompanion.search.insight.CodeInsightService; +import com.github.minecraft_ta.totalDebugCompanion.search.reference.ReferenceSearchService; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.stream.Collectors; + +/** Prepared runtime resources. The loader owns the index until acceptOwnership completes the handoff. */ +public final class RuntimeBinding implements AutoCloseable { + private final RuntimeIndexService.ReadySnapshot snapshot; + private final RuntimeSourceCatalog sources; + private final CompanionDecompilationService decompiler; + private final ReferenceSearchService references; + private final String classpath; + // Open local editors retain these application-lived services; only their bindings change here. + private final ScriptCompilationService compiler; + private final CodeInsightService insights; + private boolean attached; + private boolean ownsIndex; + private boolean closed; + + public RuntimeBinding(RuntimeIndexService.ReadySnapshot snapshot, Path dataDirectory, + RuntimeSnapshotBytecodeSource bytecode, ScriptCompilationService compiler, + CodeInsightService insights) throws IOException { + this.snapshot = snapshot; + this.compiler = compiler; + this.insights = insights; + this.sources = new RuntimeSourceCatalog(snapshot.sources()); + this.classpath = snapshot.sources().stream().map(source -> source.path().toString()) + .collect(Collectors.joining(File.pathSeparator)); + try { + this.decompiler = new CompanionDecompilationService(snapshot.signature(), dataDirectory, bytecode); + } catch (IOException | RuntimeException failure) { + bytecode.close(); + throw failure; + } + this.references = new ReferenceSearchService(snapshot::index, this.sources); + } + + /** Called after the previous runtime has detached, under the application lifecycle lock. */ + public void attach() { + if (closed || attached) throw new IllegalStateException("Runtime binding cannot be attached"); + attached = true; + compiler.bind(snapshot); + insights.rebind(snapshot::index, sources); + } + + /** Last, non-failing step of publication. No fallible follow-up may precede returning to the loader. */ + public void acceptOwnership() { + ownsIndex = true; + } + + public RuntimeIndexService.ReadySnapshot snapshot() { return snapshot; } + public RuntimeSourceCatalog sources() { return sources; } + public CompanionDecompilationService decompiler() { return decompiler; } + public ReferenceSearchService references() { return references; } + public String classpath() { return classpath; } + + @Override + public void close() { + if (closed) return; + closed = true; + try { + if (attached) { + try { compiler.bind(null); } + finally { + insights.rebind(() -> { throw new IllegalStateException("Runtime class index is not ready"); }, + RuntimeSourceCatalog.empty()); + } + } + } finally { + try { decompiler.close(); } + finally { + try { references.close(); } + finally { if (ownsIndex) snapshot.close(); } + } + } + } +} diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/script/SnippetExpressionSupport.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/script/SnippetExpressionSupport.java index 5741be1c..23975f47 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/script/SnippetExpressionSupport.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/script/SnippetExpressionSupport.java @@ -7,7 +7,7 @@ import com.github.minecraft_ta.totalDebugCompanion.jdt.completion.CompletionItemKind; import com.github.minecraft_ta.totalDebugCompanion.jdt.completion.CustomCompletionRequestor; import com.github.minecraft_ta.totalDebugCompanion.jdt.completion.CustomTextEdit; -import com.github.minecraft_ta.totalDebugCompanion.jdt.diagnostics.ASTCache; +import com.github.minecraft_ta.totalDebugCompanion.jdt.JavaAst; import com.github.minecraft_ta.totalDebugCompanion.jdt.impls.CompilationUnitImpl; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.jdt.core.dom.ASTVisitor; @@ -171,7 +171,7 @@ private List tokensNow(String expression) { String combined = combined(expression); int prefix = combined.length() - expression.length(); JavaSnippetSource.GeneratedSource generated = JavaSnippetSource.build(this.className, combined, mode(expression)); - var ast = ASTCache.rawParse(this.className, generated.source()); + var ast = JavaAst.parse(this.className, generated.source()); List result = new ArrayList<>(); ast.accept(new ASTVisitor() { @Override diff --git a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/debugger/BreakpointsWindow.java b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/debugger/BreakpointsWindow.java index f54e06dc..fa2b1358 100644 --- a/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/debugger/BreakpointsWindow.java +++ b/companion/src/main/java/com/github/minecraft_ta/totalDebugCompanion/ui/views/debugger/BreakpointsWindow.java @@ -4,6 +4,7 @@ import com.github.minecraft_ta.totalDebugCompanion.debugger.DebuggerSessionController; import com.github.minecraft_ta.totalDebugCompanion.navigation.NavigationTarget; import com.github.minecraft_ta.totalDebugCompanion.jdt.diagnostics.ASTCache; +import com.github.minecraft_ta.totalDebugCompanion.jdt.JavaAst; import com.github.minecraft_ta.totalDebugCompanion.jdt.insight.ExpressionScopeAnalyzer; import com.github.minecraft_ta.totalDebugCompanion.ui.UiMetrics; import com.github.minecraft_ta.totalDebugCompanion.ui.components.ExpressionCompletionSemantics; @@ -419,7 +420,7 @@ private ExpressionCompletionSupport.CompletionProvider completionProvider( : entry.sourceUri().toString(); var unit = ASTCache.getFromCache(key); if (unit == null) { - unit = ASTCache.rawParse(entry.binaryName(), source.contents()); + unit = JavaAst.parse(entry.binaryName(), source.contents()); } int contextOffset = sourceOffset(source.contents(), entry.breakpoint().line()); return ExpressionScopeAnalyzer.complete(unit, contextOffset, text, caret); diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/RuntimeInstallationTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/RuntimeInstallationTest.java new file mode 100644 index 00000000..1f4ef8dd --- /dev/null +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/RuntimeInstallationTest.java @@ -0,0 +1,86 @@ +package com.github.minecraft_ta.totalDebugCompanion; + +import com.github.minecraft_ta.totalDebugCompanion.bytecode.RuntimeSnapshotBytecodeSource; +import com.github.minecraft_ta.totalDebugCompanion.runtime.RuntimeIndexService.ReadySnapshot; +import com.github.minecraft_ta.totalDebugCompanion.session.CompanionLaunchConfiguration; +import com.github.minecraft_ta.totalDebugCompanion.session.CompanionProfile; +import com.github.minecraft_ta.totaldebug.storage.RuntimeInventory; +import com.github.tth05.jindex.ClassIndex; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.lang.reflect.InvocationTargetException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.*; + +class RuntimeInstallationTest { + @TempDir Path directory; + + @Test + void candidateFailureAndPostPublicationFailureRespectTheOwnershipBoundary() throws Exception { + Path log = directory.resolve("install.log"); + Process process = new ProcessBuilder(Path.of(System.getProperty("java.home"), "bin", "java").toString(), + "-cp", System.getProperty("totaldebug.testClasspath", System.getProperty("java.class.path")), + getClass().getName(), directory.toString()).redirectErrorStream(true).redirectOutput(log.toFile()).start(); + try { + assertTrue(process.waitFor(25, TimeUnit.SECONDS), () -> read(log)); + assertEquals(0, process.exitValue(), () -> read(log)); + } finally { if (process.isAlive()) process.destroyForcibly(); } + } + + public static void main(String[] args) { + try { + Path root = Path.of(args[0]); + var config = CompanionApp.class.getDeclaredField("launchConfiguration"); + config.setAccessible(true); + config.set(null, new CompanionLaunchConfiguration(Files.createDirectories(root.resolve("app")))); + CompanionApp.configureWithoutSession(new CompanionProfile("test", Files.createDirectories(root.resolve("data")), + Files.createDirectories(root.resolve("game")))); + // Force debugger restoration to fail after the index is published. + CompanionApp.getDebuggerController().close(); + var install = CompanionApp.class.getDeclaredMethod("installRuntimeSnapshot", ReadySnapshot.class, + RuntimeSnapshotBytecodeSource.class); + install.setAccessible(true); + var queue = CompanionApp.class.getDeclaredField("projectWorker"); + queue.setAccessible(true); + var close = CompanionApp.class.getDeclaredMethod("closeRuntime"); + close.setAccessible(true); + try (var accepted = snapshot(root, "accepted"); var rejected = snapshot(root, "")) { + var bytes = RuntimeSnapshotBytecodeSource.fromIndexedSources(accepted.sources(), accepted.index()); + install.invoke(null, accepted, bytes); + ((ExecutorService) queue.get(null)).submit(() -> {}).get(10, TimeUnit.SECONDS); + assertFalse(accepted.index().isDestroyed(), "Post-publication failure must not close the installed index"); + var decompiler = CompanionApp.getDecompilationService(); + var candidateBytes = RuntimeSnapshotBytecodeSource.fromIndexedSources(rejected.sources(), rejected.index()); + var failed = assertThrows(InvocationTargetException.class, () -> install.invoke(null, rejected, candidateBytes)); + assertInstanceOf(IllegalArgumentException.class, failed.getCause()); + assertSame(decompiler, CompanionApp.getDecompilationService()); + assertTrue(bytes.hasClass("java.lang.Object"), "Failed preparation must not close the previous bytecode source"); + assertFalse(rejected.index().isDestroyed(), "Loader still owns the rejected candidate"); + close.invoke(null); + assertTrue(accepted.index().isDestroyed()); + } + System.exit(0); + } catch (Throwable failure) { failure.printStackTrace(); System.exit(1); } + } + + private static ReadySnapshot snapshot(Path root, String signature) throws Exception { + Path classes = Files.createDirectories(root.resolve("classes")); + var module = new RuntimeInventory.RuntimeModule("test", "Test", RuntimeInventory.ModuleKind.LIBRARY); + var source = new RuntimeSnapshotBytecodeSource.Source(0, classes, classes.toUri().toString(), module); + try (var input = Object.class.getResourceAsStream("Object.class")) { + return new ReadySnapshot("test", signature, root.resolve("runtime/index.jindex"), List.of(source), + ClassIndex.fromBytes(List.of(input.readAllBytes()))); + } + } + + private static String read(Path path) { + try { return Files.readString(path); } + catch (Exception failure) { return failure.toString(); } + } +} diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/debugger/DebuggerBreakpointResolverTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/debugger/DebuggerBreakpointResolverTest.java index fc0e3e38..e367a3a6 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/debugger/DebuggerBreakpointResolverTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/debugger/DebuggerBreakpointResolverTest.java @@ -1,6 +1,6 @@ package com.github.minecraft_ta.totalDebugCompanion.debugger; -import com.github.minecraft_ta.totalDebugCompanion.jdt.diagnostics.ASTCache; +import com.github.minecraft_ta.totalDebugCompanion.jdt.JavaAst; import com.github.minecraft_ta.totalDebugCompanion.jdt.CompanionClassIndex; import com.github.tth05.jindex.ClassIndex; import org.junit.jupiter.api.BeforeAll; @@ -16,13 +16,14 @@ class DebuggerBreakpointResolverTest { @BeforeAll static void initializeIndex() throws Exception { try (var stream = Object.class.getResourceAsStream("/java/lang/Object.class")) { - CompanionClassIndex.replace(ClassIndex.fromBytes(java.util.List.of(stream.readAllBytes()))); + CompanionClassIndex.set(ClassIndex.fromBytes(java.util.List.of(stream.readAllBytes()))); } } @AfterAll static void closeIndex() { - CompanionClassIndex.close(); + CompanionClassIndex.get().close(); + CompanionClassIndex.clear(); } private static final String SOURCE = """ @@ -37,7 +38,7 @@ public int run() { @Test void editorAndRemoteCallsResolveTheSameMethodEntryAndExecutableLine() { DebugEngine.Source source = source(SourceLineMap.fromOriginalToDisplayed(new int[]{40, 4})); - var unit = ASTCache.rawParse("Test", SOURCE); + var unit = JavaAst.parse("Test", SOURCE); var remote = DebuggerBreakpointResolver.resolve(source, 3, "true", "5").orElseThrow(); assertEquals(remote, DebuggerBreakpointResolver.resolve(source, unit, 3, "true", "5").orElseThrow()); assertTrue(remote.isMethodEntry()); diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/decompile/SourceFileNavigationTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/decompile/SourceFileNavigationTest.java index fa9774c7..a963ffa1 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/decompile/SourceFileNavigationTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/decompile/SourceFileNavigationTest.java @@ -51,12 +51,13 @@ void hidden(Target hidden) { @BeforeAll static void initializeClassIndex() throws IOException { - CompanionClassIndex.replace(ClassIndex.fromBytes(List.of(classBytes(Object.class)))); + CompanionClassIndex.set(ClassIndex.fromBytes(List.of(classBytes(Object.class)))); } @AfterAll static void closeClassIndex() { - CompanionClassIndex.close(); + CompanionClassIndex.get().close(); + CompanionClassIndex.clear(); } @Test diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/JavaSymbolResolverTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/JavaSymbolResolverTest.java index 884bc173..078f53f3 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/JavaSymbolResolverTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/JavaSymbolResolverTest.java @@ -51,7 +51,7 @@ static boolean callsExternalMethod() { @BeforeAll static void initializeClassIndex() throws IOException { - CompanionClassIndex.replace(ClassIndex.fromBytes(List.of( + CompanionClassIndex.set(ClassIndex.fromBytes(List.of( classBytes(Object.class), classBytes(String.class), classBytes(List.class) @@ -64,7 +64,8 @@ static void closeClassIndex() { ASTCache.removeFromCache("constructor"); ASTCache.removeFromCache("local"); ASTCache.removeFromCache("navigation"); - CompanionClassIndex.close(); + CompanionClassIndex.get().close(); + CompanionClassIndex.clear(); } @Test diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/JdtConfigurationTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/JdtConfigurationTest.java index ba20d29a..7ce86fbe 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/JdtConfigurationTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/JdtConfigurationTest.java @@ -1,6 +1,6 @@ package com.github.minecraft_ta.totalDebugCompanion.jdt; -import com.github.minecraft_ta.totalDebugCompanion.jdt.diagnostics.ASTCache; +import com.github.minecraft_ta.totalDebugCompanion.jdt.JavaAst; import com.github.tth05.jindex.ClassIndex; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.IType; @@ -26,7 +26,7 @@ class JdtConfigurationTest { @BeforeAll static void initializeClassIndex() throws IOException { - CompanionClassIndex.replace(ClassIndex.fromBytes(List.of( + CompanionClassIndex.set(ClassIndex.fromBytes(List.of( classBytes(Object.class), classBytes(String.class), classBytes(List.class), @@ -42,7 +42,8 @@ static void initializeClassIndex() throws IOException { @AfterAll static void closeClassIndex() { - CompanionClassIndex.close(); + CompanionClassIndex.get().close(); + CompanionClassIndex.clear(); } @Test @@ -73,7 +74,7 @@ static String render(Shape shape) { } """; - var unit = ASTCache.rawParse("Renderer", source); + var unit = JavaAst.parse("Renderer", source); var syntaxErrors = Arrays.stream(unit.getProblems()) .filter(problem -> problem.isError() && (problem.getID() & IProblem.Syntax) != 0) .map(IProblem::getMessage) @@ -114,7 +115,7 @@ String render() { } } """; - var unit = ASTCache.rawParse("Renderer", source); + var unit = JavaAst.parse("Renderer", source); int stringOffset = source.indexOf("String"); var elements = assertDoesNotThrow(() -> unit.getTypeRoot().codeSelect(stringOffset, 0)); @@ -135,7 +136,7 @@ final class Test { } """; - var unit = ASTCache.rawParse("Test", source); + var unit = JavaAst.parse("Test", source); var errors = Arrays.stream(unit.getProblems()) .filter(IProblem::isError) .map(IProblem::getMessage) @@ -159,7 +160,7 @@ Field find(Class type) { } """; - var errors = Arrays.stream(ASTCache.rawParse("Test", source).getProblems()) + var errors = Arrays.stream(JavaAst.parse("Test", source).getProblems()) .filter(IProblem::isError) .map(IProblem::getMessage) .toList(); diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/SourceDeclarationAnalyzerTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/SourceDeclarationAnalyzerTest.java index 9bcf7c2f..ac1beaa6 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/SourceDeclarationAnalyzerTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/SourceDeclarationAnalyzerTest.java @@ -1,6 +1,6 @@ package com.github.minecraft_ta.totalDebugCompanion.jdt; -import com.github.minecraft_ta.totalDebugCompanion.jdt.diagnostics.ASTCache; +import com.github.minecraft_ta.totalDebugCompanion.jdt.JavaAst; import com.github.minecraft_ta.totalDebugCompanion.jdt.insight.SourceDeclaration; import com.github.minecraft_ta.totalDebugCompanion.jdt.insight.SourceDeclarationAnalyzer; import com.github.minecraft_ta.totalDebugCompanion.jdt.symbol.CodeSymbol; @@ -53,18 +53,19 @@ final class Inner { static void initializeClassIndex() throws Exception { String resource = "/java/lang/Object.class"; try (var stream = Objects.requireNonNull(Object.class.getResourceAsStream(resource), resource)) { - CompanionClassIndex.replace(ClassIndex.fromBytes(List.of(stream.readAllBytes()))); + CompanionClassIndex.set(ClassIndex.fromBytes(List.of(stream.readAllBytes()))); } } @AfterAll static void closeClassIndex() { - CompanionClassIndex.close(); + CompanionClassIndex.get().close(); + CompanionClassIndex.clear(); } @Test void extractsTypesMethodsAndFieldsWithHeaderAnchors() { - var unit = ASTCache.rawParse("Implementation", SOURCE); + var unit = JavaAst.parse("Implementation", SOURCE); List declarations = SourceDeclarationAnalyzer.analyze(unit, SOURCE); assertTrue(declarations.stream().anyMatch(declaration -> @@ -93,7 +94,7 @@ void extractsTypesMethodsAndFieldsWithHeaderAnchors() { @Test void includesCompilerParametersInNestedConstructorDescriptors() { - var unit = ASTCache.rawParse("Outer", CONSTRUCTOR_SOURCE); + var unit = JavaAst.parse("Outer", CONSTRUCTOR_SOURCE); List symbols = SourceDeclarationAnalyzer.analyze(unit, CONSTRUCTOR_SOURCE).stream() .map(SourceDeclaration::symbol) .toList(); diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/completion/CompletionPresentationTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/completion/CompletionPresentationTest.java index 0f2362d1..25e7ba51 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/completion/CompletionPresentationTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/completion/CompletionPresentationTest.java @@ -25,12 +25,13 @@ static void index() throws Exception { classes.add(stream.readAllBytes()); } } - CompanionClassIndex.replace(ClassIndex.fromBytes(classes)); + CompanionClassIndex.set(ClassIndex.fromBytes(classes)); } @AfterAll static void closeIndex() { - CompanionClassIndex.close(); + CompanionClassIndex.get().close(); + CompanionClassIndex.clear(); } @Test diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/insight/ExpressionScopeAnalyzerTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/insight/ExpressionScopeAnalyzerTest.java index 5df90cfa..cde387ee 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/insight/ExpressionScopeAnalyzerTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/insight/ExpressionScopeAnalyzerTest.java @@ -4,6 +4,7 @@ import com.github.minecraft_ta.totalDebugCompanion.debugger.fixture.ExternalCompletionType; import com.github.minecraft_ta.totalDebugCompanion.jdt.CompanionClassIndex; import com.github.minecraft_ta.totalDebugCompanion.jdt.diagnostics.ASTCache; +import com.github.minecraft_ta.totalDebugCompanion.jdt.JavaAst; import com.github.tth05.jindex.ClassIndex; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -40,7 +41,7 @@ static void staticRun() { @BeforeAll static void initializeClassIndex() throws IOException { - CompanionClassIndex.replace(ClassIndex.fromBytes(List.of( + CompanionClassIndex.set(ClassIndex.fromBytes(List.of( classBytes(Object.class), classBytes(String.class), classBytes(ExternalCompletionType.class), @@ -51,12 +52,13 @@ static void initializeClassIndex() throws IOException { @AfterAll static void closeClassIndex() { - CompanionClassIndex.close(); + CompanionClassIndex.get().close(); + CompanionClassIndex.clear(); } @Test void keepsOnlyNamesVisibleInTheSelectedLexicalScope() { - var unit = ASTCache.rawParse("Sample", SOURCE); + var unit = JavaAst.parse("Sample", SOURCE); int innerUse = SOURCE.indexOf("field++"); int outerUse = SOURCE.indexOf("field++", innerUse + 1); @@ -70,7 +72,7 @@ void keepsOnlyNamesVisibleInTheSelectedLexicalScope() { @Test void excludesInstanceNamesFromAStaticMethod() { - var unit = ASTCache.rawParse("Sample", SOURCE); + var unit = JavaAst.parse("Sample", SOURCE); List names = names(ExpressionScopeAnalyzer.analyze(unit, SOURCE.lastIndexOf("staticField++"))); @@ -93,7 +95,7 @@ void run() { } } """; - var unit = ASTCache.rawParse("Sample", source); + var unit = JavaAst.parse("Sample", source); int context = source.indexOf("target."); List completions = ExpressionScopeAnalyzer.complete( unit, context, "target.", "target.".length() @@ -114,7 +116,7 @@ void run() { } } """; - var unit = ASTCache.rawParse("Sample", source); + var unit = JavaAst.parse("Sample", source); int context = source.indexOf("target ="); String expression = "target.own + target."; @@ -138,7 +140,7 @@ void run(ExternalCompletionType parameter) { } } """; - var unit = ASTCache.rawParse("Sample", source); + var unit = JavaAst.parse("Sample", source); int context = source.indexOf("parameter."); List completions = ExpressionScopeAnalyzer.complete( unit, context, "parameter.", "parameter.".length() @@ -169,7 +171,7 @@ void run(ExternalCompletionType parameter) { } } """; - var unit = ASTCache.rawParse("Sample", source); + var unit = JavaAst.parse("Sample", source); List completions = names(ExpressionScopeAnalyzer.complete( unit, source.indexOf("parameter."), "parameter.", "parameter.".length() )); @@ -193,7 +195,7 @@ void run() { } } """; - var unit = ASTCache.rawParse("Sample", source); + var unit = JavaAst.parse("Sample", source); int context = source.indexOf("void run"); List completions = ExpressionScopeAnalyzer.complete( diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/semanticHighlighting/SemanticHighlightingPublicationTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/semanticHighlighting/SemanticHighlightingPublicationTest.java index 0fd9857c..b2b5c9ea 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/semanticHighlighting/SemanticHighlightingPublicationTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/jdt/semanticHighlighting/SemanticHighlightingPublicationTest.java @@ -42,12 +42,13 @@ void invoke() { @BeforeAll static void initializeClassIndex() throws IOException { - CompanionClassIndex.replace(ClassIndex.fromBytes(List.of(classBytes(Object.class)))); + CompanionClassIndex.set(ClassIndex.fromBytes(List.of(classBytes(Object.class)))); } @AfterAll static void closeClassIndex() { - CompanionClassIndex.close(); + CompanionClassIndex.get().close(); + CompanionClassIndex.clear(); } @Test diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/mcp/CompanionMcpRuntimeSourceTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/mcp/CompanionMcpRuntimeSourceTest.java index fc2dde6a..d37b93a2 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/mcp/CompanionMcpRuntimeSourceTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/mcp/CompanionMcpRuntimeSourceTest.java @@ -56,7 +56,7 @@ enum Choice { @BeforeAll static void initializeClassIndex() throws IOException { - CompanionClassIndex.replace(ClassIndex.fromBytes(List.of( + CompanionClassIndex.set(ClassIndex.fromBytes(List.of( classBytes(Object.class), classBytes(String.class), classBytes(Record.class), @@ -68,7 +68,8 @@ static void initializeClassIndex() throws IOException { @AfterAll static void closeClassIndex() { - CompanionClassIndex.close(); + CompanionClassIndex.get().close(); + CompanionClassIndex.clear(); } @Test diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/mcp/DebuggerMcpIntegrationTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/mcp/DebuggerMcpIntegrationTest.java index c19c83ee..a5bb6874 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/mcp/DebuggerMcpIntegrationTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/mcp/DebuggerMcpIntegrationTest.java @@ -45,12 +45,13 @@ static void initializeIndex() throws Exception { bytes.add(stream.readAllBytes()); } } - CompanionClassIndex.replace(ClassIndex.fromBytes(bytes)); + CompanionClassIndex.set(ClassIndex.fromBytes(bytes)); } @AfterAll static void closeIndex() { - CompanionClassIndex.close(); + CompanionClassIndex.get().close(); + CompanionClassIndex.clear(); } @Test diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/navigation/JavaBreadcrumbResolverTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/navigation/JavaBreadcrumbResolverTest.java index f772623d..3f3d33b0 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/navigation/JavaBreadcrumbResolverTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/navigation/JavaBreadcrumbResolverTest.java @@ -1,6 +1,6 @@ package com.github.minecraft_ta.totalDebugCompanion.navigation; -import com.github.minecraft_ta.totalDebugCompanion.jdt.diagnostics.ASTCache; +import com.github.minecraft_ta.totalDebugCompanion.jdt.JavaAst; import com.github.minecraft_ta.totalDebugCompanion.jdt.CompanionClassIndex; import com.github.tth05.jindex.ClassIndex; import org.junit.jupiter.api.AfterAll; @@ -36,17 +36,18 @@ public void update() { @BeforeAll static void initializeClassIndex() throws IOException { - CompanionClassIndex.replace(ClassIndex.fromBytes(List.of(classBytes(Object.class)))); + CompanionClassIndex.set(ClassIndex.fromBytes(List.of(classBytes(Object.class)))); } @AfterAll static void closeClassIndex() { - CompanionClassIndex.close(); + CompanionClassIndex.get().close(); + CompanionClassIndex.clear(); } @Test void resolvesTheEnclosingLocalMethodAtItsDeclaration() { - var unit = ASTCache.rawParse("Sample", SOURCE); + var unit = JavaAst.parse("Sample", SOURCE); Path file = this.temporaryDirectory.resolve("Sample.java"); JavaBreadcrumbResolver.Member member = JavaBreadcrumbResolver.resolve( @@ -64,7 +65,7 @@ void resolvesTheEnclosingLocalMethodAtItsDeclaration() { @Test void resolvesAnExactRuntimeMemberTarget() { - var unit = ASTCache.rawParse("Sample", SOURCE); + var unit = JavaAst.parse("Sample", SOURCE); JavaBreadcrumbResolver.Member member = JavaBreadcrumbResolver.resolve( unit, @@ -85,7 +86,7 @@ void resolvesAnExactRuntimeMemberTarget() { @Test void resolvesAFieldButNotClassLevelWhitespace() { - var unit = ASTCache.rawParse("Sample", SOURCE); + var unit = JavaAst.parse("Sample", SOURCE); NavigationTarget.LocalFile target = new NavigationTarget.LocalFile( this.temporaryDirectory.resolve("Sample.java") ); diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/runtime/RuntimeBindingTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/runtime/RuntimeBindingTest.java new file mode 100644 index 00000000..4457ab01 --- /dev/null +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/runtime/RuntimeBindingTest.java @@ -0,0 +1,101 @@ +package com.github.minecraft_ta.totalDebugCompanion.runtime; + +import com.github.minecraft_ta.totalDebugCompanion.bytecode.RuntimeSnapshotBytecodeSource; +import com.github.minecraft_ta.totalDebugCompanion.jdt.CompanionClassIndex; +import com.github.minecraft_ta.totalDebugCompanion.script.ScriptCompilationService; +import com.github.minecraft_ta.totalDebugCompanion.search.insight.CodeInsightService; +import com.github.minecraft_ta.totalDebugCompanion.bytecode.reference.ReferenceQuery; +import com.github.tth05.jindex.ClassIndex; +import com.github.minecraft_ta.totaldebug.storage.RuntimeInventory; +import com.github.minecraft_ta.totalDebugCompanion.search.reference.ReferenceSearchService; +import com.github.minecraft_ta.totalDebugCompanion.bytecode.reference.ReferenceUsagePage; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class RuntimeBindingTest { + @TempDir Path directory; + + @Test + void acceptedRuntimeDetachesConsumersBeforeClosingItsIndexAndCanCloseTwice() throws Exception { + try (var compiler = new ScriptCompilationService(message -> false, request -> false); + var insights = new CodeInsightService(() -> null, RuntimeSourceCatalog.empty()); + var snapshot = snapshot()) { + var bytecode = RuntimeSnapshotBytecodeSource.fromIndexedSources(snapshot.sources(), snapshot.index()); + var binding = new RuntimeBinding(snapshot, directory, bytecode, compiler, insights); + binding.attach(); + binding.acceptOwnership(); + assertTrue(compiler.isCurrentInventory("test")); + binding.close(); + binding.close(); + assertFalse(compiler.isCurrentInventory("test")); + assertTrue(snapshot.index().isDestroyed()); + assertThrows(IllegalStateException.class, () -> bytecode.hasClass("java.lang.Object")); + assertThrows(IllegalStateException.class, () -> binding.decompiler().load("java.lang.Object")); + assertThrows(IllegalStateException.class, () -> binding.references().search( + ReferenceQuery.classReference("java.lang.Object"), 1, new ReferenceSearchService.Listener() { + public void onCompleted(ReferenceUsagePage result) {} + public void onFailed(Throwable failure) {} + })); + // Local editors retain this service; closing the binding must not destroy its worker. + assertDoesNotThrow(() -> insights.rebind(() -> null, RuntimeSourceCatalog.empty())); + } + } + + @Test + void rejectedCandidateLeavesIndexDisposalToTheLoader() throws Exception { + try (var compiler = new ScriptCompilationService(message -> false, request -> false); + var insights = new CodeInsightService(() -> null, RuntimeSourceCatalog.empty()); + var snapshot = snapshot()) { + var binding = new RuntimeBinding(snapshot, directory, + RuntimeSnapshotBytecodeSource.fromIndexedSources(snapshot.sources(), snapshot.index()), compiler, insights); + binding.close(); + assertFalse(snapshot.index().isDestroyed()); + } + } + + @Test + void failedPreparationClosesBytecodeSourceButNotTheLoadersIndex() throws Exception { + try (var compiler = new ScriptCompilationService(message -> false, request -> false); + var insights = new CodeInsightService(() -> null, RuntimeSourceCatalog.empty()); + var snapshot = snapshot()) { + Path invalidHome = Files.writeString(directory.resolve("not-a-directory"), "x"); + var bytecode = RuntimeSnapshotBytecodeSource.fromIndexedSources(snapshot.sources(), snapshot.index()); + assertThrows(IOException.class, () -> new RuntimeBinding(snapshot, invalidHome, bytecode, compiler, insights)); + assertFalse(snapshot.index().isDestroyed()); + assertThrows(IllegalStateException.class, () -> bytecode.hasClass("java.lang.Object")); + } + } + + @Test + void jdtHookDoesNotDisposeThePreviousOrCurrentIndex() throws Exception { + try (var first = snapshot(); var second = snapshot()) { + CompanionClassIndex.set(first.index()); + CompanionClassIndex.set(second.index()); + assertFalse(first.index().isDestroyed()); + CompanionClassIndex.clear(); + assertFalse(second.index().isDestroyed()); + } finally { CompanionClassIndex.clear(); } + } + + private RuntimeIndexService.ReadySnapshot snapshot() throws Exception { + Path cache = Files.createDirectories(directory.resolve("runtime")); + Files.writeString(cache.resolve("inventory.json"), "{\"id\":\"test\"}"); + try (var input = Object.class.getResourceAsStream("Object.class")) { + byte[] bytes = input.readAllBytes(); + Path classes = Files.createDirectories(directory.resolve("classes")); + Files.createDirectories(classes.resolve("java/lang")); + Files.write(classes.resolve("java/lang/Object.class"), bytes); + var module = new RuntimeInventory.RuntimeModule("test", "Test", RuntimeInventory.ModuleKind.LIBRARY); + var source = new RuntimeSnapshotBytecodeSource.Source(0, classes, classes.toUri().toString(), module); + return new RuntimeIndexService.ReadySnapshot("test", "signature", cache.resolve("index.jindex"), List.of(source), + ClassIndex.fromBytes(List.of(bytes))); + } + } +} diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/runtime/RuntimeIndexServiceTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/runtime/RuntimeIndexServiceTest.java index 2fdde8c6..b28f7002 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/runtime/RuntimeIndexServiceTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/runtime/RuntimeIndexServiceTest.java @@ -17,6 +17,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -232,8 +233,8 @@ void closingDuringNativeLoadDiscardsItsResultWithoutWaitingOrInstalling() throws var paths = cachedFixture("closing"); var loading = new CountDownLatch(1); var release = new CountDownLatch(1); - var loaded = new java.util.concurrent.atomic.AtomicReference(); - var workerThread = new java.util.concurrent.atomic.AtomicReference(); + var loaded = new AtomicReference(); + var workerThread = new AtomicReference(); var installations = new AtomicInteger(); try (var service = new RuntimeIndexService(new Object(), snapshot -> installations.incrementAndGet(), file -> { workerThread.set(Thread.currentThread()); @@ -264,7 +265,7 @@ void aDifferentInstanceSupersedesTheLoadingSnapshotAndClosesIt() throws Exceptio var loading = new CountDownLatch(1); var release = new CountDownLatch(1); var installed = new CountDownLatch(1); - var discarded = new java.util.concurrent.atomic.AtomicReference(); + var discarded = new AtomicReference(); var snapshots = new java.util.concurrent.CopyOnWriteArrayList(); try (var service = new RuntimeIndexService(new Object(), snapshot -> { snapshots.add(snapshot); @@ -291,6 +292,37 @@ void aDifferentInstanceSupersedesTheLoadingSnapshotAndClosesIt() throws Exceptio } } + @Test + void throwingReadyHandlerLeavesTheLoaderResponsibleForDisposal() throws Exception { + var paths = cachedFixture("rejected-handler"); + var candidate = new AtomicReference(); + var failed = new CountDownLatch(1); + try (var service = new RuntimeIndexService(new Object(), snapshot -> { + candidate.set(snapshot); + throw new IllegalStateException("Rejected installation"); + })) { + service.addStatusListener(status -> { if (status.phase() == RuntimeIndexService.Phase.FAILED) failed.countDown(); }); + service.restore(paths.home()); + assertTrue(failed.await(5, TimeUnit.SECONDS)); + assertTrue(candidate.get().index().isDestroyed()); + } + } + + @Test + void acceptedSnapshotOutlivesItsLoader() throws Exception { + var paths = cachedFixture("accepted-handler"); + var candidate = new AtomicReference(); + var ready = new CountDownLatch(1); + try { + try (var service = new RuntimeIndexService(new Object(), candidate::set)) { + service.addStatusListener(status -> { if (status.phase() == RuntimeIndexService.Phase.READY) ready.countDown(); }); + service.restore(paths.home()); + assertTrue(ready.await(5, TimeUnit.SECONDS)); + } + assertTrue(!candidate.get().index().isDestroyed()); + } finally { if (candidate.get() != null) candidate.get().close(); } + } + private com.github.minecraft_ta.totaldebug.storage.InstancePaths cachedFixture(String id) throws Exception { var paths = new com.github.minecraft_ta.totaldebug.storage.InstancePaths(temporaryDirectory.resolve(id)); Path jar = Files.write(temporaryDirectory.resolve(id + ".jar"), archive(RuntimeIndexServiceTest.class, null)); diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/script/SnippetExpressionSupportTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/script/SnippetExpressionSupportTest.java index cb9107c8..7a54611f 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/script/SnippetExpressionSupportTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/script/SnippetExpressionSupportTest.java @@ -24,7 +24,7 @@ class SnippetExpressionSupportTest { @BeforeAll static void initializeClassIndex() throws IOException { - CompanionClassIndex.replace(ClassIndex.fromBytes(List.of( + CompanionClassIndex.set(ClassIndex.fromBytes(List.of( classBytes(Object.class), classBytes(com.github.minecraft_ta.totaldebug.TotalDebug.class), classBytes(ExternalCompletionType.class), @@ -36,7 +36,8 @@ static void initializeClassIndex() throws IOException { @AfterAll static void closeClassIndex() { - CompanionClassIndex.close(); + CompanionClassIndex.get().close(); + CompanionClassIndex.clear(); } @Test diff --git a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/editors/DebuggerInlineValueHintsTest.java b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/editors/DebuggerInlineValueHintsTest.java index 969adeac..d1a81aa3 100644 --- a/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/editors/DebuggerInlineValueHintsTest.java +++ b/companion/src/test/java/com/github/minecraft_ta/totalDebugCompanion/ui/components/editors/DebuggerInlineValueHintsTest.java @@ -2,7 +2,7 @@ import com.github.minecraft_ta.totalDebugCompanion.debugger.DebugEngine; import com.github.minecraft_ta.totalDebugCompanion.jdt.CompanionClassIndex; -import com.github.minecraft_ta.totalDebugCompanion.jdt.diagnostics.ASTCache; +import com.github.minecraft_ta.totalDebugCompanion.jdt.JavaAst; import com.github.tth05.jindex.ClassIndex; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -21,12 +21,13 @@ class DebuggerInlineValueHintsTest { @BeforeAll static void initializeClassIndex() throws IOException { - CompanionClassIndex.replace(ClassIndex.fromBytes(List.of(classBytes(Object.class), classBytes(String.class)))); + CompanionClassIndex.set(ClassIndex.fromBytes(List.of(classBytes(Object.class), classBytes(String.class)))); } @AfterAll static void closeClassIndex() { - CompanionClassIndex.close(); + CompanionClassIndex.get().close(); + CompanionClassIndex.clear(); } @Test @@ -62,7 +63,7 @@ void unrelated(String query) { )); Map hints = DebuggerInlineValueHints.create( - ASTCache.rawParse("Sample", source), source, snapshot + JavaAst.parse("Sample", source), source, snapshot ); assertTrue(text(hints.get(2)).contains("query: \"stone\""), hints.toString()); @@ -102,7 +103,7 @@ int inspect(Object pos) { ); Map hints = DebuggerInlineValueHints.create( - ASTCache.rawParse("Sample", source), source, snapshot + JavaAst.parse("Sample", source), source, snapshot ); assertTrue(hints.isEmpty(), hints.toString()); @@ -116,7 +117,7 @@ int inspect(Object pos) { )) ); Map resolvedHints = DebuggerInlineValueHints.create( - ASTCache.rawParse("Sample", source), source, resolved + JavaAst.parse("Sample", source), source, resolved ); assertEquals("pos: x=1, y=64, z=2", text(resolvedHints.get(2))); assertEquals("pos: x=1, y=64, z=2", text(resolvedHints.get(3)));