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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions companion/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<PendingNavigation> pendingNavigations = new ArrayList<>();
private static CompanionMcpServer mcpServer;
private static volatile DebuggerSessionController debuggerController;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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<PendingNavigation> 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<PendingNavigation> 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);
}
}

Expand Down Expand Up @@ -703,11 +695,10 @@ private static Map<String, Object> runtimeContext() {
Map<String, Object> 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);
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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);
}

Expand All @@ -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");
}
Expand All @@ -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");
}
Expand All @@ -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
Expand Down Expand Up @@ -1018,7 +1014,7 @@ private static List<DebuggerSessionController.BreakpointDefinition> restoreBreak

private static void persistBreakpoints(DebuggerSessionController controller) {
if (switchingProjects) return;
String runtimeSignature = activeRuntimeSignature;
String runtimeSignature = getActiveRuntimeSignature();
if (runtimeSignature == null || runtimeSignature.isBlank()) {
return;
}
Expand Down Expand Up @@ -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();
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -18,7 +18,7 @@ private DebuggerBreakpointResolver() {
public static Optional<DebugEngine.SourceBreakpoint> 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);
}

Expand Down
Loading