Skip to content

Commit 51f85af

Browse files
soul2zimateclaude
andauthored
fix: prevent IDE freeze and threading violations in Stack Analysis and Component Analysis (#245)
* fix: replace runReadAction with invokeLater for DaemonCodeAnalyzer.restart() calls * fix: dispatch refreshAndFindFileByPath to background thread to avoid SlowOperations on EDT * fix: handle project disposal race in SA report opening and avoid redundant VFS refresh * fix: add disposal guards and user-visible error for async editor operations Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f0ffd4d commit 51f85af

7 files changed

Lines changed: 59 additions & 20 deletions

File tree

src/main/java/org/jboss/tools/intellij/componentanalysis/CAAnnotator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public abstract class CAAnnotator extends ExternalAnnotator<CAAnnotator.Info, Ma
7979
dependencies,
8080
info.getFile());
8181

82-
ApplicationManager.getApplication().runReadAction(() -> {
82+
ApplicationManager.getApplication().invokeLater(() -> {
8383
if (updated) {
8484
LOG.info("Refresh dependencies");
8585
try {

src/main/java/org/jboss/tools/intellij/componentanalysis/SAIntentionAction.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws
6262
if (manifestDetails != null) {
6363
try {
6464
AnalyticsReportUtils analyticsReportUtils = new AnalyticsReportUtils();
65-
analyticsReportUtils.openCustomEditor(FileEditorManager.getInstance(project), manifestDetails);
65+
analyticsReportUtils.openCustomEditor(FileEditorManager.getInstance(project), manifestDetails, project);
6666
} catch (IOException ex) {
6767
throw new RuntimeException(ex);
6868
}

src/main/java/org/jboss/tools/intellij/image/DockerfileAnnotator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ private static HighlightSeverity getHighlightSeverity(AnalysisReport report, Str
287287

288288
var updated = imageService.performAnalysis(new HashSet<>(images), info.getFile().getProject());
289289

290-
ApplicationManager.getApplication().runReadAction(() -> {
290+
ApplicationManager.getApplication().invokeLater(() -> {
291291
if (updated) {
292292
LOG.info("Refresh vulnerabilities");
293293
try {

src/main/java/org/jboss/tools/intellij/image/ImageService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ void openAnalysisHtmlReport(Project project, PsiFile dockerfile) {
232232
ApplicationManager.getApplication().invokeLater(() -> {
233233
try {
234234
var analyticsReportUtils = new AnalyticsReportUtils();
235-
analyticsReportUtils.openCustomEditor(FileEditorManager.getInstance(project), manifestDetails);
235+
analyticsReportUtils.openCustomEditor(FileEditorManager.getInstance(project), manifestDetails, project);
236236
} catch (Exception e) {
237237
LOGGER.error(e);
238238
Messages.showErrorDialog(project,

src/main/java/org/jboss/tools/intellij/report/AnalyticsReportUtils.java

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,16 @@
1212

1313
import com.google.gson.Gson;
1414
import com.google.gson.JsonObject;
15+
import com.intellij.openapi.application.ApplicationManager;
16+
import com.intellij.openapi.application.ModalityState;
17+
import com.intellij.openapi.diagnostic.Logger;
1518
import com.intellij.openapi.fileEditor.FileEditorManager;
19+
import com.intellij.openapi.project.Project;
20+
import com.intellij.openapi.ui.Messages;
1621
import com.intellij.openapi.vfs.LocalFileSystem;
1722
import com.intellij.openapi.vfs.VfsUtilCore;
1823
import com.intellij.openapi.vfs.VirtualFile;
24+
import com.intellij.serviceContainer.AlreadyDisposedException;
1925

2026
import java.io.BufferedWriter;
2127
import java.io.File;
@@ -26,22 +32,30 @@
2632

2733
public class AnalyticsReportUtils {
2834

35+
private static final Logger LOG = Logger.getInstance(AnalyticsReportUtils.class);
36+
2937
/**
3038
* <p>Open a custom editor window.</p>
3139
*
3240
* <p>The custom editor window will open a file which will have browser attached to it.</p>
3341
*
42+
* <p>Must be called on the EDT. The slow VFS operation ({@code refreshAndFindFileByPath})
43+
* is dispatched internally to a background thread to avoid blocking the EDT.</p>
44+
*
3445
* @param instance An instance of FileEditorManager.
3546
* @param manifestDetails Manifest file details.
47+
* @param project The project owning the editor; used as a disposal guard.
3648
* @throws IOException In case of process failure
3749
*/
38-
public void openCustomEditor(FileEditorManager instance, JsonObject manifestDetails) throws IOException {
50+
public void openCustomEditor(FileEditorManager instance, JsonObject manifestDetails, Project project) throws IOException {
3951

4052
// Close custom editor if already opened in previous run,
4153
// if it's a different manifest file with same name then don't close existing one and open a new tab.
54+
// NOTE: must run before file creation so that showParent flag is included in the written content.
4255
manifestDetails = closeCustomEditor(instance, manifestDetails);
4356

4457
// Create a temp file in which is registered with AnalyticsReportEditorProvider.
58+
// Standard Java file I/O — not a VFS operation, safe on EDT.
4559
File reportFile = File.createTempFile("exhort-", "_" + manifestDetails.get("manifestName").getAsString() + ".ar");
4660

4761
//Save the Analytics Report URL in file, which will be loaded in browser
@@ -59,18 +73,32 @@ public void openCustomEditor(FileEditorManager instance, JsonObject manifestDeta
5973
);
6074
}
6175

62-
// Create a virtual file from report file
63-
VirtualFile virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(reportFile.getAbsolutePath());
64-
65-
// Refresh the cached file from the physical file system.
66-
// if Virtual file is already opened in editor window from previous run then refresh file from physical file
67-
// else old web page will be shown in editor window.
68-
if (virtualFile == null) {
69-
throw new PlatformDetectionException("Dependency Analytics Report file is not created.");
70-
}
71-
virtualFile.refresh(true, true, () -> {
72-
// Open the virtual file in editor window, which will show the analytics report.
73-
instance.openFile(virtualFile, true, false);
76+
// refreshAndFindFileByPath is a slow VFS operation prohibited on EDT.
77+
// Dispatch to a background thread; once the VirtualFile is obtained, switch back to EDT to open it.
78+
ApplicationManager.getApplication().executeOnPooledThread(() -> {
79+
if (project.isDisposed()) {
80+
return;
81+
}
82+
VirtualFile virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(reportFile.getAbsolutePath());
83+
if (virtualFile == null) {
84+
LOG.error("Dependency Analytics Report file is not created.");
85+
ApplicationManager.getApplication().invokeLater(() ->
86+
Messages.showErrorDialog(project,
87+
"Dependency Analytics Report file could not be created.",
88+
"Error"),
89+
ModalityState.defaultModalityState());
90+
return;
91+
}
92+
// refreshAndFindFileByPath already performed a synchronous refresh and fully
93+
// registered the file with the VFS — no second refresh is needed.
94+
// Switch to EDT to open the editor tab, guarded by project disposal.
95+
ApplicationManager.getApplication().invokeLater(() -> {
96+
try {
97+
instance.openFile(virtualFile, true, false);
98+
} catch (AlreadyDisposedException e) {
99+
// Project was disposed after the runnable was scheduled — ignore.
100+
}
101+
}, ModalityState.defaultModalityState(), project.getDisposed());
74102
});
75103
}
76104

src/main/java/org/jboss/tools/intellij/settings/ApiSettingsConfigurable.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import com.intellij.openapi.project.Project;
1717
import com.intellij.openapi.project.ProjectManager;
1818
import com.intellij.openapi.util.NlsContexts;
19+
import com.intellij.serviceContainer.AlreadyDisposedException;
1920
import org.jetbrains.annotations.Nullable;
2021

2122
import javax.swing.JComponent;
@@ -116,11 +117,15 @@ public void apply() {
116117
}
117118

118119
private void refreshComponentAnalysis() {
119-
ApplicationManager.getApplication().runReadAction(() -> {
120+
ApplicationManager.getApplication().invokeLater(() -> {
120121
Project[] openProjects = ProjectManager.getInstance().getOpenProjects();
121122
for (Project project : openProjects) {
122123
if (!project.isDisposed()) {
123-
DaemonCodeAnalyzer.getInstance(project).restart();
124+
try {
125+
DaemonCodeAnalyzer.getInstance(project).restart();
126+
} catch (AlreadyDisposedException e) {
127+
// Project was disposed between the isDisposed() check and restart() — ignore.
128+
}
124129
}
125130
}
126131
});

src/main/java/org/jboss/tools/intellij/stackanalysis/SaAction.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.intellij.openapi.ui.Messages;
2525
import com.intellij.openapi.vfs.VirtualFile;
2626
import com.intellij.psi.PsiFile;
27+
import com.intellij.serviceContainer.AlreadyDisposedException;
2728
import org.jboss.tools.intellij.report.AnalyticsReportUtils;
2829
import org.jetbrains.annotations.NotNull;
2930

@@ -82,10 +83,15 @@ public void actionPerformed(@NotNull AnActionEvent event) {
8283

8384
if (manifestDetails != null) {
8485
ApplicationManager.getApplication().invokeLater(() -> {
86+
if (project.isDisposed()) {
87+
return;
88+
}
8589
try {
8690
AnalyticsReportUtils analyticsReportUtils = new AnalyticsReportUtils();
8791
// Open custom editor window which will load SA Report in browser attached to it.
88-
analyticsReportUtils.openCustomEditor(FileEditorManager.getInstance(project), manifestDetails);
92+
analyticsReportUtils.openCustomEditor(FileEditorManager.getInstance(project), manifestDetails, project);
93+
} catch (AlreadyDisposedException e) {
94+
// Project was disposed between the isDisposed() check and getInstance() — ignore silently.
8995
} catch (Exception e) {
9096
logger.error(e);
9197
Messages.showErrorDialog(event.getProject(),

0 commit comments

Comments
 (0)