This is an automated email from the ASF dual-hosted git repository.

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new 723435d231 make the lint plugin session aware in Hop Web, fixes #8232 
(#8236)
723435d231 is described below

commit 723435d231ddf8b2dc144d8b2f08923793e2c1ed
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Wed Sep 2 14:11:08 2026 +0200

    make the lint plugin session aware in Hop Web, fixes #8232 (#8236)
---
 .../org/apache/hop/lint/BackgroundLintService.java |  74 ++--
 .../org/apache/hop/lint/ExplorerLintGuiPlugin.java | 386 +++++++++++----------
 .../apache/hop/lint/LintCanvasOverlayRefresh.java  |  24 +-
 .../org/apache/hop/lint/LintNavigationHelper.java  |   2 +-
 .../apache/hop/lint/LintProblemsBarManager.java    |  51 ++-
 .../org/apache/hop/lint/LintProblemsBarPlugin.java |   2 +-
 .../org/apache/hop/lint/LintResultsManager.java    |  27 +-
 .../java/org/apache/hop/lint/LintResultsPanel.java |   5 +-
 .../java/org/apache/hop/lint/LintResultsUi.java    |   2 +-
 .../org/apache/hop/lint/LintStatusFilePainter.java | 163 ++++-----
 .../org/apache/hop/lint/LinterConfigPlugin.java    |   2 +-
 .../java/org/apache/hop/lint/LinterGuiPlugin.java  | 146 ++++----
 .../org/apache/hop/lint/LinterProgressDialog.java  |   6 +-
 .../apache/hop/lint/PreCommitLintExtension.java    |   2 +-
 .../hop/lint/ProjectLoadedLintExtension.java       |   2 +-
 .../apache/hop/lint/WorkflowVerifyLintService.java |   2 +-
 16 files changed, 487 insertions(+), 409 deletions(-)

diff --git 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/BackgroundLintService.java
 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/BackgroundLintService.java
index 3437461888..63753adb0c 100644
--- 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/BackgroundLintService.java
+++ 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/BackgroundLintService.java
@@ -33,6 +33,7 @@ import org.apache.hop.core.util.Utils;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.metadata.api.IHopMetadataProvider;
 import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.ui.hopgui.BackgroundThreadFacade;
 import org.apache.hop.ui.hopgui.HopGui;
 import org.apache.hop.ui.hopgui.file.pipeline.HopGuiPipelineGraph;
 import org.apache.hop.ui.hopgui.file.shared.HopGuiAbstractGraph;
@@ -46,29 +47,47 @@ public class BackgroundLintService {
   private static final ILogChannel log = LogChannel.GENERAL;
   private static final int DEFERRED_CHECK_DELAY_MS = 500;
 
-  private static BackgroundLintService instance;
+  /**
+   * Shared by every session: this is CPU bound file parsing, and a pool per 
Hop Web session would
+   * multiply the threads by the number of people logged in.
+   */
+  private static final ExecutorService executor =
+      Executors.newFixedThreadPool(
+          Math.max(2, Runtime.getRuntime().availableProcessors() / 2),
+          r -> {
+            Thread t = new Thread(r, "HopLinter-Background");
+            t.setDaemon(true);
+            return t;
+          });
+
+  /** Used when there is no GUI to own this: the command line, and unit tests. 
*/
+  private static BackgroundLintService fallback;
 
-  private final ExecutorService executor;
   private final LintCheckTracker tracker;
   private final Map<String, AtomicInteger> deferredGenerations = new 
ConcurrentHashMap<>();
 
   private BackgroundLintService() {
-    this.executor =
-        Executors.newFixedThreadPool(
-            Math.max(2, Runtime.getRuntime().availableProcessors() / 2),
-            r -> {
-              Thread t = new Thread(r, "HopLinter-Background");
-              t.setDaemon(true);
-              return t;
-            });
     this.tracker = new LintCheckTracker();
   }
 
-  public static synchronized BackgroundLintService getInstance() {
-    if (instance == null) {
-      instance = new BackgroundLintService();
+  /**
+   * The service belonging to the GUI that asks for it.
+   *
+   * <p>Hop Web serves many people from one JVM, each with their own editors 
and their own findings,
+   * so what has already been linted and which editors are waiting for an 
answer is per session. The
+   * desktop has one HopGui and therefore one of these.
+   */
+  public static BackgroundLintService getInstance() {
+    HopGui hopGui = HopGui.peekInstance();
+    if (hopGui != null) {
+      return hopGui.getSessionSingleton(BackgroundLintService.class, 
BackgroundLintService::new);
+    }
+    synchronized (BackgroundLintService.class) {
+      if (fallback == null) {
+        fallback = new BackgroundLintService();
+      }
+      return fallback;
     }
-    return instance;
   }
 
   public LintCheckTracker getTracker() {
@@ -95,7 +114,7 @@ public class BackgroundLintService {
       LintProblemsBarManager.getInstance().updateProblemsBar(filePath);
       return;
     }
-    executor.submit(() -> lintFileInternal(filePath, graphId));
+    submit(() -> lintFileInternal(filePath, graphId));
   }
 
   public void scheduleGraphLint(HopGuiAbstractGraph graph, boolean force) {
@@ -125,7 +144,7 @@ public class BackgroundLintService {
     if (!isEnabled() || Utils.isEmpty(projectPath)) {
       return;
     }
-    executor.submit(
+    submit(
         () -> {
           try {
             HopLinter linter = new HopLinter();
@@ -184,14 +203,14 @@ public class BackgroundLintService {
       return;
     }
 
-    HopGui hopGuiForSnapshot = HopGui.getInstance();
+    HopGui hopGuiForSnapshot = HopGui.peekInstance();
     EditorSnapshot snapshot =
         snapshotOf(graph, hopGuiForSnapshot != null ? 
hopGuiForSnapshot.getVariables() : null);
 
-    executor.submit(
+    submit(
         () -> {
           try {
-            HopGui hopGui = HopGui.getInstance();
+            HopGui hopGui = HopGui.peekInstance();
             IHopMetadataProvider metadataProvider =
                 hopGui != null ? hopGui.getMetadataProvider() : null;
             IVariables variables = hopGui != null ? hopGui.getVariables() : 
null;
@@ -296,7 +315,7 @@ public class BackgroundLintService {
 
   private void lintFileInternal(String filePath, String graphId) {
     try {
-      HopGui hopGui = HopGui.getInstance();
+      HopGui hopGui = HopGui.peekInstance();
       IHopMetadataProvider metadataProvider = hopGui != null ? 
hopGui.getMetadataProvider() : null;
       IVariables variables = hopGui != null ? hopGui.getVariables() : null;
 
@@ -331,8 +350,19 @@ public class BackgroundLintService {
     return linter.lintFile(filePath, metadataProvider, variables);
   }
 
-  public void shutdown() {
-    executor.shutdown();
+  /**
+   * Hand work to the pool without losing the session it belongs to.
+   *
+   * <p>Everything the work then reaches for - the HopGui of the person who 
asked, its metadata
+   * provider, the editors to report into - belongs to a RAP {@code UISession} 
in Hop Web, and a
+   * pooled thread has none of its own: the lookup fails with "Invalid thread 
access" and the lint
+   * dies before it starts. {@link BackgroundThreadFacade} carries the session 
of the thread that
+   * schedules the work over to the thread that runs it, and is a no-op on the 
desktop.
+   *
+   * <p>Call this from the UI thread: that is where the session is read.
+   */
+  private static void submit(Runnable work) {
+    executor.submit(BackgroundThreadFacade.bind(work));
   }
 
   private boolean includeMetadataInGuiLint() {
diff --git 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/ExplorerLintGuiPlugin.java
 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/ExplorerLintGuiPlugin.java
index c66c9b0cd3..d86bac1c7f 100644
--- 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/ExplorerLintGuiPlugin.java
+++ 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/ExplorerLintGuiPlugin.java
@@ -32,6 +32,7 @@ import org.apache.hop.metadata.api.IHopMetadataProvider;
 import org.apache.hop.pipeline.PipelineMeta;
 import org.apache.hop.ui.core.dialog.ErrorDialog;
 import org.apache.hop.ui.core.dialog.MessageBox;
+import org.apache.hop.ui.hopgui.BackgroundThreadFacade;
 import org.apache.hop.ui.hopgui.HopGui;
 import org.apache.hop.ui.hopgui.file.IHopFileTypeHandler;
 import org.apache.hop.ui.hopgui.file.pipeline.HopGuiPipelineGraph;
@@ -49,33 +50,64 @@ import org.eclipse.swt.widgets.Display;
 public class ExplorerLintGuiPlugin {
 
   private static final ILogChannel log = LogChannel.GENERAL;
-  private static ExplorerLintGuiPlugin instance;
-  private static LintStatusFilePainter filePainter;
 
+  /** Used when there is no GUI to own this: unit tests. */
+  private static ExplorerLintGuiPlugin fallback;
+
+  private LintStatusFilePainter filePainter;
+
+  /**
+   * The plugin state of the GUI that asks for it. Hop Web serves many people 
from one JVM, each
+   * with an Explorer of their own; the desktop has one HopGui and therefore 
one of these.
+   */
   public static ExplorerLintGuiPlugin getInstance() {
-    if (instance == null) {
-      instance = new ExplorerLintGuiPlugin();
+    HopGui hopGui = HopGui.peekInstance();
+    if (hopGui != null) {
+      return hopGui.getSessionSingleton(ExplorerLintGuiPlugin.class, 
ExplorerLintGuiPlugin::new);
+    }
+    synchronized (ExplorerLintGuiPlugin.class) {
+      if (fallback == null) {
+        fallback = new ExplorerLintGuiPlugin();
+      }
+      return fallback;
     }
-    return instance;
   }
 
   public ExplorerLintGuiPlugin() {
-    instance = this;
+    // The GUI plugin registry builds one of these for its callbacks, and 
getInstance() builds one
+    // per session. Both go through the static helpers below, so this holds 
nothing itself.
   }
 
   @GuiCallback(callbackId = 
ExplorerPerspective.GUI_TOOLBAR_CREATED_CALLBACK_ID)
   public void registerExplorerPaintListener() {
-    if (filePainter == null) {
-      filePainter = new LintStatusFilePainter();
-    }
+    LintStatusFilePainter painter = getFilePainter();
     ExplorerPerspective perspective = ExplorerPerspective.getInstance();
-    if (!perspective.getFilePaintListeners().contains(filePainter)) {
-      perspective.getFilePaintListeners().add(filePainter);
+    if (!perspective.getFilePaintListeners().contains(painter)) {
+      perspective.getFilePaintListeners().add(painter);
     }
   }
 
-  /** Get the file painter instance */
+  /**
+   * The painter of the GUI that asks for it.
+   *
+   * <p>It caches the icons it composites, and an image belongs to the display 
that created it: in
+   * Hop Web a shared painter hands one session images another session's 
display disposed, which SWT
+   * reports as "Argument not valid" (issue #3508). It also remembers the 
Explorer tree it last
+   * painted, and that tree is one session's widget.
+   */
   public static LintStatusFilePainter getFilePainter() {
+    return getInstance().filePainter();
+  }
+
+  /**
+   * Built here rather than by the session singleton map itself: the painter 
subscribes to that
+   * session's lint results, and asking for one singleton while another is 
being built is a
+   * recursive update of the map they both live in.
+   */
+  private synchronized LintStatusFilePainter filePainter() {
+    if (filePainter == null) {
+      filePainter = new LintStatusFilePainter();
+    }
     return filePainter;
   }
 
@@ -85,12 +117,13 @@ public class ExplorerLintGuiPlugin {
    * the painter has not yet been attached.
    */
   public static void refreshExplorerIcons() {
+    LintStatusFilePainter painter = getFilePainter();
     Display.getDefault()
         .asyncExec(
             () -> {
               try {
-                if (filePainter != null) {
-                  filePainter.repaintExplorerIcons();
+                if (painter != null) {
+                  painter.repaintExplorerIcons();
                   return;
                 }
                 ExplorerPerspective perspective = 
HopGui.getExplorerPerspective();
@@ -223,7 +256,7 @@ public class ExplorerLintGuiPlugin {
   }
 
   private static void runLintOnFile(String filePath) {
-    HopGui hopGui = HopGui.getInstance();
+    HopGui hopGui = HopGui.peekInstance();
     if (hopGui == null) {
       showMessage("Hop GUI Not Available", "Could not access Hop GUI 
instance.", SWT.ICON_ERROR);
       return;
@@ -274,38 +307,36 @@ public class ExplorerLintGuiPlugin {
     final HopGuiAbstractGraph graph = (HopGuiAbstractGraph) handler;
     final IHopFileTypeHandler openHandler = handler;
 
-    Thread linterThread =
-        new Thread(
-            () -> {
-              try {
-                List<LintResult> results =
-                    lintFileResults(normalizedPath, openHandler, 
metadataProvider, variables);
-
-                
LintResultsManager.getInstance().updateResultsForFile(normalizedPath, results);
-
-                Display.getDefault()
-                    .asyncExec(
-                        () -> {
-                          // Populates the editor Problems tab + toolbar badge 
(UI thread).
-                          
LintProblemsBarManager.getInstance().updateProblemsBar(normalizedPath);
-                          refreshExplorerIcons();
-                          LintResultsUi.logSummary(results, new 
File(normalizedPath).getName());
-                          // Bring the Problems tab to the front once it has 
been populated.
-                          Display.getDefault().asyncExec(() -> 
bringProblemsTabToFront(graph));
-                        });
-              } catch (Exception e) {
-                log.logError("Error during file linting: " + e.getMessage(), 
e);
-                Display.getDefault()
-                    .asyncExec(
-                        () ->
-                            showErrorDialog(
-                                "Linting Error",
-                                "An error occurred during linting: " + 
e.getMessage(),
-                                e));
-              }
-            },
-            "HopLinter-File");
-    linterThread.start();
+    BackgroundThreadFacade.start(
+        () -> {
+          try {
+            List<LintResult> results =
+                lintFileResults(normalizedPath, openHandler, metadataProvider, 
variables);
+
+            
LintResultsManager.getInstance().updateResultsForFile(normalizedPath, results);
+
+            Display.getDefault()
+                .asyncExec(
+                    () -> {
+                      // Populates the editor Problems tab + toolbar badge (UI 
thread).
+                      
LintProblemsBarManager.getInstance().updateProblemsBar(normalizedPath);
+                      refreshExplorerIcons();
+                      LintResultsUi.logSummary(results, new 
File(normalizedPath).getName());
+                      // Bring the Problems tab to the front once it has been 
populated.
+                      Display.getDefault().asyncExec(() -> 
bringProblemsTabToFront(graph));
+                    });
+          } catch (Exception e) {
+            log.logError("Error during file linting: " + e.getMessage(), e);
+            Display.getDefault()
+                .asyncExec(
+                    () ->
+                        showErrorDialog(
+                            "Linting Error",
+                            "An error occurred during linting: " + 
e.getMessage(),
+                            e));
+          }
+        },
+        "HopLinter-File");
   }
 
   /** Bring the editor's check/Problems tab to the front, creating it if 
needed. */
@@ -353,36 +384,34 @@ public class ExplorerLintGuiPlugin {
     final IVariables variables = hopGui.getVariables();
     final IHopMetadataProvider metadataProvider = hopGui.getMetadataProvider();
 
-    Thread linterThread =
-        new Thread(
-            () -> {
-              try {
-                List<LintResult> results =
-                    lintFileResults(normalizedPath, openHandler, 
metadataProvider, variables);
-
-                
LintResultsManager.getInstance().updateResultsForFile(normalizedPath, results);
-
-                Display.getDefault()
-                    .asyncExec(
-                        () -> {
-                          
LintProblemsBarManager.getInstance().updateProblemsBar(normalizedPath);
-                          refreshExplorerIcons();
-                          LintResultsUi.logSummary(results, new 
File(normalizedPath).getName());
-                          LintResultsUi.showResultsForFile(normalizedPath);
-                        });
-              } catch (Exception e) {
-                log.logError("Error during file linting: " + e.getMessage(), 
e);
-                Display.getDefault()
-                    .asyncExec(
-                        () ->
-                            showErrorDialog(
-                                "Linting Error",
-                                "An error occurred during linting: " + 
e.getMessage(),
-                                e));
-              }
-            },
-            "HopLinter-File");
-    linterThread.start();
+    BackgroundThreadFacade.start(
+        () -> {
+          try {
+            List<LintResult> results =
+                lintFileResults(normalizedPath, openHandler, metadataProvider, 
variables);
+
+            
LintResultsManager.getInstance().updateResultsForFile(normalizedPath, results);
+
+            Display.getDefault()
+                .asyncExec(
+                    () -> {
+                      
LintProblemsBarManager.getInstance().updateProblemsBar(normalizedPath);
+                      refreshExplorerIcons();
+                      LintResultsUi.logSummary(results, new 
File(normalizedPath).getName());
+                      LintResultsUi.showResultsForFile(normalizedPath);
+                    });
+          } catch (Exception e) {
+            log.logError("Error during file linting: " + e.getMessage(), e);
+            Display.getDefault()
+                .asyncExec(
+                    () ->
+                        showErrorDialog(
+                            "Linting Error",
+                            "An error occurred during linting: " + 
e.getMessage(),
+                            e));
+          }
+        },
+        "HopLinter-File");
   }
 
   private static List<LintResult> lintFileResults(
@@ -456,7 +485,7 @@ public class ExplorerLintGuiPlugin {
   }
 
   private static void runLintOnFolder(String folderPath) {
-    HopGui hopGui = HopGui.getInstance();
+    HopGui hopGui = HopGui.peekInstance();
     if (hopGui == null) {
       showMessage("Hop GUI Not Available", "Could not access Hop GUI 
instance.", SWT.ICON_ERROR);
       return;
@@ -467,116 +496,113 @@ public class ExplorerLintGuiPlugin {
 
     final LinterProgressDialog progressDialog = new 
LinterProgressDialog(hopGui.getShell());
 
-    Thread linterThread =
-        new Thread(
-            () -> {
-              try {
-                HopLinter linter = new HopLinter();
-                linter.loadConfigurationForContext(new File(folderPath));
-
-                List<String> hopFilePaths = 
linter.findLintableFiles(folderPath, true);
-
-                if (hopFilePaths.isEmpty()) {
-                  Display.getDefault()
-                      .asyncExec(
-                          () -> {
-                            progressDialog.close();
-                            showMessage(
-                                "No Hop Files",
-                                "No .hpl or .hwf files found in the selected 
folder.",
-                                SWT.ICON_INFORMATION);
-                          });
-                  return;
-                }
+    BackgroundThreadFacade.start(
+        () -> {
+          try {
+            HopLinter linter = new HopLinter();
+            linter.loadConfigurationForContext(new File(folderPath));
+
+            List<String> hopFilePaths = linter.findLintableFiles(folderPath, 
true);
+
+            if (hopFilePaths.isEmpty()) {
+              Display.getDefault()
+                  .asyncExec(
+                      () -> {
+                        progressDialog.close();
+                        showMessage(
+                            "No Hop Files",
+                            "No .hpl or .hwf files found in the selected 
folder.",
+                            SWT.ICON_INFORMATION);
+                      });
+              return;
+            }
+
+            progressDialog.updateProgress(
+                "Found " + hopFilePaths.size() + " files to analyze", 0, 
hopFilePaths.size());
+
+            Display.getDefault().asyncExec(progressDialog::show);
+
+            List<LintResult> results = new java.util.ArrayList<>();
+            int processedFilesCount = 0;
+
+            for (String filePath : hopFilePaths) {
+              File file = new File(filePath);
+              if (progressDialog.isCancelled()) {
+                log.logDetailed("Folder linting cancelled by user");
+                return;
+              }
 
+              try {
                 progressDialog.updateProgress(
-                    "Found " + hopFilePaths.size() + " files to analyze", 0, 
hopFilePaths.size());
-
-                Display.getDefault().asyncExec(progressDialog::show);
-
-                List<LintResult> results = new java.util.ArrayList<>();
-                int processedFilesCount = 0;
-
-                for (String filePath : hopFilePaths) {
-                  File file = new File(filePath);
-                  if (progressDialog.isCancelled()) {
-                    log.logDetailed("Folder linting cancelled by user");
-                    return;
-                  }
-
-                  try {
-                    progressDialog.updateProgress(
-                        "Processing: " + file.getName(), processedFilesCount, 
hopFilePaths.size());
-                    String normalizedPath = 
LintPathUtils.normalizePath(file.getAbsolutePath());
-                    List<LintResult> fileResults;
-                    if (normalizedPath.toLowerCase().endsWith(".hpl")) {
-                      PipelineMeta pipelineMeta =
-                          new PipelineMeta(file.getAbsolutePath(), 
metadataProvider, variables);
-                      fileResults =
-                          PipelineLintResultsBuilder.build(
-                              pipelineMeta, normalizedPath, metadataProvider, 
variables);
-                    } else if 
(HopMetadataFileLoader.isMetadataJsonFile(normalizedPath)) {
-                      fileResults = linter.processFile(file, metadataProvider, 
variables);
-                    } else {
-                      fileResults = linter.processFile(file, metadataProvider, 
variables);
-                    }
-                    results.addAll(fileResults);
-                    LintResultsManager.getInstance()
-                        .updateResultsForFile(normalizedPath, fileResults);
-                    processedFilesCount++;
-                  } catch (Exception e) {
-                    log.logError("Error processing file: " + 
file.getAbsolutePath(), e);
-                    LintResult errorResult =
-                        new LintResult(
-                            "SYSTEM-001",
-                            "File Processing Error",
-                            "ERROR",
-                            "Failed to process file: " + e.getMessage(),
-                            
LintPathUtils.normalizePath(file.getAbsolutePath()));
-                    results.add(errorResult);
-                    LintResultsManager.getInstance()
-                        .updateResultsForFile(
-                            
LintPathUtils.normalizePath(file.getAbsolutePath()),
-                            List.of(errorResult));
-                    processedFilesCount++;
-                  }
+                    "Processing: " + file.getName(), processedFilesCount, 
hopFilePaths.size());
+                String normalizedPath = 
LintPathUtils.normalizePath(file.getAbsolutePath());
+                List<LintResult> fileResults;
+                if (normalizedPath.toLowerCase().endsWith(".hpl")) {
+                  PipelineMeta pipelineMeta =
+                      new PipelineMeta(file.getAbsolutePath(), 
metadataProvider, variables);
+                  fileResults =
+                      PipelineLintResultsBuilder.build(
+                          pipelineMeta, normalizedPath, metadataProvider, 
variables);
+                } else if 
(HopMetadataFileLoader.isMetadataJsonFile(normalizedPath)) {
+                  fileResults = linter.processFile(file, metadataProvider, 
variables);
+                } else {
+                  fileResults = linter.processFile(file, metadataProvider, 
variables);
                 }
-
-                progressDialog.setComplete("Completed. Found " + 
results.size() + " issues");
-
-                LintProblemsBarManager.getInstance().refreshAllOpenEditors();
-
-                Display.getDefault()
-                    .asyncExec(
-                        () -> {
-                          progressDialog.close();
-                          refreshExplorerIcons();
-                          LintResultsUi.logSummary(results, new 
File(folderPath).getName());
-                          // A folder has no editor to put findings in, so 
this is one of the cases
-                          // the results window exists for. Without this the 
run finished with
-                          // nothing to show for it but a line in the log.
-                          LintResultsUi.showResultsForFolder(folderPath);
-                        });
-
+                results.addAll(fileResults);
+                
LintResultsManager.getInstance().updateResultsForFile(normalizedPath, 
fileResults);
+                processedFilesCount++;
               } catch (Exception e) {
-                log.logError("Error during folder linting: " + e.getMessage(), 
e);
-                Display.getDefault()
-                    .asyncExec(
-                        () -> {
-                          progressDialog.close();
-                          showErrorDialog(
-                              "Linting Error",
-                              "An error occurred during linting: " + 
e.getMessage(),
-                              e);
-                        });
+                log.logError("Error processing file: " + 
file.getAbsolutePath(), e);
+                LintResult errorResult =
+                    new LintResult(
+                        "SYSTEM-001",
+                        "File Processing Error",
+                        "ERROR",
+                        "Failed to process file: " + e.getMessage(),
+                        LintPathUtils.normalizePath(file.getAbsolutePath()));
+                results.add(errorResult);
+                LintResultsManager.getInstance()
+                    .updateResultsForFile(
+                        LintPathUtils.normalizePath(file.getAbsolutePath()), 
List.of(errorResult));
+                processedFilesCount++;
               }
-            });
-    linterThread.start();
+            }
+
+            progressDialog.setComplete("Completed. Found " + results.size() + 
" issues");
+
+            LintProblemsBarManager.getInstance().refreshAllOpenEditors();
+
+            Display.getDefault()
+                .asyncExec(
+                    () -> {
+                      progressDialog.close();
+                      refreshExplorerIcons();
+                      LintResultsUi.logSummary(results, new 
File(folderPath).getName());
+                      // A folder has no editor to put findings in, so this is 
one of the cases
+                      // the results window exists for. Without this the run 
finished with
+                      // nothing to show for it but a line in the log.
+                      LintResultsUi.showResultsForFolder(folderPath);
+                    });
+
+          } catch (Exception e) {
+            log.logError("Error during folder linting: " + e.getMessage(), e);
+            Display.getDefault()
+                .asyncExec(
+                    () -> {
+                      progressDialog.close();
+                      showErrorDialog(
+                          "Linting Error",
+                          "An error occurred during linting: " + 
e.getMessage(),
+                          e);
+                    });
+          }
+        },
+        "HopLinter-Folder");
   }
 
   private static void showMessage(String title, String message, int style) {
     log.logBasic(title + ": " + message);
-    HopGui hopGui = HopGui.getInstance();
+    HopGui hopGui = HopGui.peekInstance();
     if (hopGui != null && hopGui.getShell() != null) {
       MessageBox box = new MessageBox(hopGui.getShell(), style | SWT.OK);
       box.setText(title);
@@ -586,7 +612,7 @@ public class ExplorerLintGuiPlugin {
   }
 
   private static void showErrorDialog(String title, String message, Exception 
e) {
-    HopGui hopGui = HopGui.getInstance();
+    HopGui hopGui = HopGui.peekInstance();
     if (hopGui != null && hopGui.getShell() != null) {
       new ErrorDialog(hopGui.getShell(), title, message, e);
     }
diff --git 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintCanvasOverlayRefresh.java
 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintCanvasOverlayRefresh.java
index bec9662df3..d54b1e7439 100644
--- 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintCanvasOverlayRefresh.java
+++ 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintCanvasOverlayRefresh.java
@@ -16,6 +16,9 @@
  */
 package org.apache.hop.lint;
 
+import java.util.Collections;
+import java.util.Set;
+import java.util.WeakHashMap;
 import org.apache.hop.ui.hopgui.HopGui;
 import org.apache.hop.ui.hopgui.file.IHopFileTypeHandler;
 import org.apache.hop.ui.hopgui.file.shared.HopGuiAbstractGraph;
@@ -28,20 +31,25 @@ import org.eclipse.swt.widgets.Display;
  */
 public final class LintCanvasOverlayRefresh {
 
-  private static volatile boolean registered;
+  /**
+   * The results this listens to, one entry per set of findings we have 
subscribed to.
+   *
+   * <p>A single flag registered with the first session's results and left 
every later session
+   * without canvas overlays: Hop Web gives each of them findings of their 
own. Weakly held so a
+   * session that goes away takes its entry with it.
+   */
+  private static final Set<LintResultsManager> registered =
+      Collections.newSetFromMap(Collections.synchronizedMap(new 
WeakHashMap<>()));
 
   private LintCanvasOverlayRefresh() {}
 
   public static void ensureRegistered() {
-    if (registered) {
-      return;
-    }
+    LintResultsManager results = LintResultsManager.getInstance();
     synchronized (LintCanvasOverlayRefresh.class) {
-      if (registered) {
+      if (!registered.add(results)) {
         return;
       }
-      
LintResultsManager.getInstance().addListener(LintCanvasOverlayRefresh::onResultsUpdated);
-      registered = true;
+      results.addListener(LintCanvasOverlayRefresh::onResultsUpdated);
     }
   }
 
@@ -50,7 +58,7 @@ public final class LintCanvasOverlayRefresh {
   }
 
   public static void redrawOpenGraphs() {
-    HopGui hopGui = HopGui.getInstance();
+    HopGui hopGui = HopGui.peekInstance();
     if (hopGui == null) {
       return;
     }
diff --git 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintNavigationHelper.java 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintNavigationHelper.java
index 5c5359cf2b..332fbb370d 100644
--- 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintNavigationHelper.java
+++ 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintNavigationHelper.java
@@ -35,7 +35,7 @@ public final class LintNavigationHelper {
       return;
     }
 
-    HopGui hopGui = HopGui.getInstance();
+    HopGui hopGui = HopGui.peekInstance();
     if (hopGui == null) {
       return;
     }
diff --git 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintProblemsBarManager.java
 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintProblemsBarManager.java
index 42752f63b8..640b4012be 100644
--- 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintProblemsBarManager.java
+++ 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintProblemsBarManager.java
@@ -21,6 +21,7 @@ import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import org.apache.hop.core.logging.ILogChannel;
 import org.apache.hop.core.logging.LogChannel;
+import org.apache.hop.ui.hopgui.HopGui;
 import org.apache.hop.ui.hopgui.file.shared.HopGuiAbstractGraph;
 import org.eclipse.swt.widgets.Display;
 
@@ -33,18 +34,33 @@ public class LintProblemsBarManager {
   private static final int EDITOR_WAIT_ATTEMPTS = 12;
 
   private static final int EDITOR_WAIT_INTERVAL_MS = 250;
-  private static LintProblemsBarManager instance;
+
+  /** Used when there is no GUI to own the editors: the command line, and unit 
tests. */
+  private static LintProblemsBarManager fallback;
 
   private final Map<String, HopGuiAbstractGraph> graphsById = new 
ConcurrentHashMap<>();
   private final Map<String, String> filePathByGraphId = new 
ConcurrentHashMap<>();
 
   private LintProblemsBarManager() {}
 
-  public static synchronized LintProblemsBarManager getInstance() {
-    if (instance == null) {
-      instance = new LintProblemsBarManager();
+  /**
+   * The editors of the GUI that asks for them.
+   *
+   * <p>This holds widgets, and in Hop Web the widgets of one session may not 
be touched from
+   * another: a shared map had {@link #refreshAllOpenEditors()} reaching into 
every session that
+   * happened to be logged in. The desktop has one HopGui and therefore one of 
these.
+   */
+  public static LintProblemsBarManager getInstance() {
+    HopGui hopGui = HopGui.peekInstance();
+    if (hopGui != null) {
+      return hopGui.getSessionSingleton(LintProblemsBarManager.class, 
LintProblemsBarManager::new);
+    }
+    synchronized (LintProblemsBarManager.class) {
+      if (fallback == null) {
+        fallback = new LintProblemsBarManager();
+      }
+      return fallback;
     }
-    return instance;
   }
 
   public void attachToGraph(HopGuiAbstractGraph graph) {
@@ -123,8 +139,12 @@ public class LintProblemsBarManager {
     // This touches SWT widgets, so make sure it runs on the UI thread 
regardless of which
     // thread the caller is on (background lint threads call this too).
     if (Display.getCurrent() == null) {
-      Display display = Display.getDefault();
+      Display display = sessionDisplay();
       if (display == null || display.isDisposed()) {
+        log.logDetailed(
+            "Not syncing the Problems tab for "
+                + filePath
+                + ": this thread has no display to do it on");
         return;
       }
       display.asyncExec(() -> updateProblemsBar(filePath));
@@ -161,6 +181,25 @@ public class LintProblemsBarManager {
     display.timerExec(EDITOR_WAIT_INTERVAL_MS, () -> 
updateProblemsBar(filePath, attempt + 1));
   }
 
+  /**
+   * The display to get onto the UI thread with.
+   *
+   * <p>An editor we already know about answers first: {@code 
Display.getDefault()} only knows the
+   * session bound to the calling thread, which is nothing at all on a thread 
that was started
+   * without one - and in Hop Web the wrong session's display would be worse 
than none.
+   */
+  private Display sessionDisplay() {
+    for (HopGuiAbstractGraph graph : graphsById.values()) {
+      if (!graph.isDisposed()) {
+        Display display = graph.getDisplay();
+        if (display != null && !display.isDisposed()) {
+          return display;
+        }
+      }
+    }
+    return Display.getDefault();
+  }
+
   public void refreshAllOpenEditors() {
     for (Map.Entry<String, String> entry : filePathByGraphId.entrySet()) {
       updateProblemsBar(entry.getValue());
diff --git 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintProblemsBarPlugin.java
 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintProblemsBarPlugin.java
index 51a94e260e..21447f928a 100644
--- 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintProblemsBarPlugin.java
+++ 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintProblemsBarPlugin.java
@@ -29,7 +29,7 @@ public class LintProblemsBarPlugin {
 
   public static void showLintResults() {
     try {
-      HopGui hopGui = HopGui.getInstance();
+      HopGui hopGui = HopGui.peekInstance();
       if (hopGui != null && hopGui.getShell() != null) {
         String filename = LintEditorGraphHelper.getActiveEditorFilename();
         if (filename != null) {
diff --git 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintResultsManager.java 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintResultsManager.java
index ce5060734a..d101da665a 100644
--- 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintResultsManager.java
+++ 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintResultsManager.java
@@ -24,12 +24,15 @@ import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import org.apache.hop.core.logging.ILogChannel;
 import org.apache.hop.core.logging.LogChannel;
+import org.apache.hop.ui.hopgui.HopGui;
 
 /** Manages lint results and provides access for GUI components */
 public class LintResultsManager {
 
   private static final ILogChannel log = LogChannel.GENERAL;
-  private static LintResultsManager instance;
+
+  /** Used when there is no GUI to own the findings: the command line, and 
unit tests. */
+  private static LintResultsManager fallback;
 
   // Store results by file path for quick lookup
   private final Map<String, List<LintResult>> resultsByFile = new 
ConcurrentHashMap<>();
@@ -58,11 +61,25 @@ public class LintResultsManager {
     // Singleton
   }
 
-  public static synchronized LintResultsManager getInstance() {
-    if (instance == null) {
-      instance = new LintResultsManager();
+  /**
+   * The findings of the GUI that asks for them.
+   *
+   * <p>Findings are what one person's editors show, and Hop Web serves many 
of them from one JVM: a
+   * single set of results would let one user's project decide what another 
sees, and would send
+   * every session's Problems tab a refresh whenever anybody linted anything. 
The desktop has one
+   * HopGui, so there it is still one set.
+   */
+  public static LintResultsManager getInstance() {
+    HopGui hopGui = HopGui.peekInstance();
+    if (hopGui != null) {
+      return hopGui.getSessionSingleton(LintResultsManager.class, 
LintResultsManager::new);
+    }
+    synchronized (LintResultsManager.class) {
+      if (fallback == null) {
+        fallback = new LintResultsManager();
+      }
+      return fallback;
     }
-    return instance;
   }
 
   /**
diff --git 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintResultsPanel.java 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintResultsPanel.java
index ec4f900bb9..c5c4fec09c 100644
--- a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintResultsPanel.java
+++ b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintResultsPanel.java
@@ -30,7 +30,6 @@ import org.eclipse.swt.layout.FillLayout;
 import org.eclipse.swt.layout.GridData;
 import org.eclipse.swt.layout.GridLayout;
 import org.eclipse.swt.widgets.Composite;
-import org.eclipse.swt.widgets.Display;
 import org.eclipse.swt.widgets.Group;
 import org.eclipse.swt.widgets.Label;
 import org.eclipse.swt.widgets.Shell;
@@ -174,7 +173,9 @@ public class LintResultsPanel extends Composite implements 
LintResultsManager.Li
   }
 
   private void refreshResults() {
-    Display.getDefault()
+    // The panel's own display, not Display.getDefault(): results arrive on 
lint threads, and in
+    // Hop Web a thread that was started without a session has no default 
display to find.
+    getDisplay()
         .asyncExec(
             () -> {
               if (isDisposed()) {
diff --git 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintResultsUi.java 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintResultsUi.java
index 40ae32f85d..43b281e68b 100644
--- a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintResultsUi.java
+++ b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintResultsUi.java
@@ -66,7 +66,7 @@ public final class LintResultsUi {
   }
 
   public static void showResultsForFile(String filePath) {
-    HopGui hopGui = HopGui.getInstance();
+    HopGui hopGui = HopGui.peekInstance();
     if (hopGui == null || hopGui.getShell() == null) {
       return;
     }
diff --git 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintStatusFilePainter.java
 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintStatusFilePainter.java
index 062a1b4a02..60043a9839 100644
--- 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintStatusFilePainter.java
+++ 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintStatusFilePainter.java
@@ -19,12 +19,11 @@ package org.apache.hop.lint;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import org.apache.hop.core.logging.ILogChannel;
 import org.apache.hop.core.logging.LogChannel;
 import org.apache.hop.core.util.Utils;
+import org.apache.hop.ui.core.gui.GuiResource;
 import org.apache.hop.ui.hopgui.HopGui;
 import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerPerspective;
 import 
org.apache.hop.ui.hopgui.perspective.explorer.IExplorerFilePaintListener;
@@ -48,23 +47,23 @@ public class LintStatusFilePainter implements 
IExplorerFilePaintListener {
   private static final String BASE_ICON_KEY = "lintBaseIcon";
   private static final String APPLIED_STATUS_KEY = "lintAppliedStatus";
 
+  /** Size of the status badge, matching the small icons the Explorer tree 
draws. */
+  private static final int BADGE_SIZE = 12;
+
   private final Map<String, LintStatus> fileStatusCache = new 
ConcurrentHashMap<>();
   private final Map<String, Long> cacheTimestamps = new ConcurrentHashMap<>();
 
   // Bounded cache of composited (base icon + badge) images, keyed by 
base/badge identity.
   // Reused across paints and tree items so we never allocate a new Image per 
paint.
   private final Map<String, Image> compositeIconCache = new 
ConcurrentHashMap<>();
-  private final ScheduledExecutorService cleanupExecutor =
-      Executors.newSingleThreadScheduledExecutor(
-          r -> {
-            Thread t = new Thread(r, "LintStatusFilePainter-Cleanup");
-            t.setDaemon(true);
-            return t;
-          });
-
-  private Image errorIcon;
-  private Image warningIcon;
-  private Image cleanIcon;
+
+  /**
+   * The display of the GUI this painter belongs to, read when it is built on 
the UI thread.
+   *
+   * <p>{@code Display.getDefault()} answers for the session bound to the 
calling thread, and the
+   * results this repaints for arrive on lint threads that may have none.
+   */
+  private final Display display;
 
   // Last Explorer tree we painted into; lets us repaint icons on result 
changes without a full
   // perspective.refresh() (which rebuilds the tree and loses 
selection/expansion state).
@@ -78,66 +77,45 @@ public class LintStatusFilePainter implements 
IExplorerFilePaintListener {
   }
 
   public LintStatusFilePainter() {
-    initializeIcons();
+    this.display = Display.getCurrent() != null ? Display.getCurrent() : 
Display.getDefault();
     updateFileStatusCache();
 
     LintResultsManager.getInstance()
         .addListener(
             () -> {
               updateFileStatusCache();
-              Display display = Display.getDefault();
               if (display != null && !display.isDisposed()) {
                 display.asyncExec(this::repaintExplorerIcons);
               }
             });
 
-    // Start periodic cache cleanup
-    startCacheCleanupScheduler();
-
     log.logBasic("Lint Status File Painter initialized");
   }
 
-  private void initializeIcons() {
-    Display display = Display.getDefault();
-    if (display != null && !display.isDisposed()) {
-      try {
-        errorIcon = new Image(display, 12, 12);
-        org.eclipse.swt.graphics.GC gc = new 
org.eclipse.swt.graphics.GC(errorIcon);
-        gc.setBackground(display.getSystemColor(SWT.COLOR_RED));
-        gc.fillOval(0, 0, 11, 11);
-        gc.setForeground(display.getSystemColor(SWT.COLOR_WHITE));
-        org.eclipse.swt.graphics.Font smallFont =
-            new org.eclipse.swt.graphics.Font(display, "Arial", 8, SWT.BOLD);
-        gc.setFont(smallFont);
-        gc.drawString("!", 4, -1, true);
-        smallFont.dispose();
-        gc.dispose();
-
-        warningIcon = new Image(display, 12, 12);
-        gc = new org.eclipse.swt.graphics.GC(warningIcon);
-        gc.setBackground(display.getSystemColor(SWT.COLOR_YELLOW));
-        int[] triangle = {6, 0, 0, 11, 11, 11};
-        gc.fillPolygon(triangle);
-        gc.setForeground(display.getSystemColor(SWT.COLOR_BLACK));
-        smallFont = new org.eclipse.swt.graphics.Font(display, "Arial", 8, 
SWT.BOLD);
-        gc.setFont(smallFont);
-        gc.drawString("!", 4, 2, true);
-        smallFont.dispose();
-        gc.dispose();
-
-        cleanIcon = new Image(display, 12, 12);
-        gc = new org.eclipse.swt.graphics.GC(cleanIcon);
-        gc.setBackground(display.getSystemColor(SWT.COLOR_GREEN));
-        gc.fillOval(0, 0, 11, 11);
-        gc.setForeground(display.getSystemColor(SWT.COLOR_WHITE));
-        smallFont = new org.eclipse.swt.graphics.Font(display, "Arial", 8, 
SWT.BOLD);
-        gc.setFont(smallFont);
-        gc.drawString("✓", 3, -1, true);
-        smallFont.dispose();
-        gc.dispose();
-      } catch (Exception e) {
-        log.logError("Error creating lint status icons: " + e.getMessage(), e);
-      }
+  /**
+   * The badge for a status, or null when there is none to draw.
+   *
+   * <p>These used to be drawn here into off-screen images with a {@code GC}, 
which Hop Web cannot
+   * do: RWT only draws on a control, so the very first call failed and the 
Explorer showed no lint
+   * status at all. Hop ships the three icons as SVG, and {@link GuiResource} 
loads and caches them
+   * per session, which also settles who disposes them - not us.
+   */
+  private Image badgeIcon(LintStatus status) {
+    String location =
+        switch (status) {
+          case ERROR -> "ui/images/error.svg";
+          case WARNING -> "ui/images/warning.svg";
+          case CLEAN -> "ui/images/success.svg";
+          case UNKNOWN -> null;
+        };
+    if (location == null) {
+      return null;
+    }
+    try {
+      return GuiResource.getInstance().getImage(location, BADGE_SIZE, 
BADGE_SIZE);
+    } catch (Exception e) {
+      log.logDetailed("No lint status icon available for " + status + ": " + 
e.getMessage());
+      return null;
     }
   }
 
@@ -203,24 +181,22 @@ public class LintStatusFilePainter implements 
IExplorerFilePaintListener {
       }
     }
 
+    Image badge = badgeIcon(status);
+    if (badge == null || badge.isDisposed()) {
+      return;
+    }
     switch (status) {
       case ERROR:
-        if (errorIcon != null && !errorIcon.isDisposed()) {
-          addOverlayIcon(treeItem, errorIcon, status);
-          addLintTooltip(treeItem, name, "Linter errors");
-        }
+        addOverlayIcon(treeItem, badge, status);
+        addLintTooltip(treeItem, name, "Linter errors");
         break;
       case WARNING:
-        if (warningIcon != null && !warningIcon.isDisposed()) {
-          addOverlayIcon(treeItem, warningIcon, status);
-          addLintTooltip(treeItem, name, "Linter warnings");
-        }
+        addOverlayIcon(treeItem, badge, status);
+        addLintTooltip(treeItem, name, "Linter warnings");
         break;
       case CLEAN:
-        if (cleanIcon != null && !cleanIcon.isDisposed()) {
-          addOverlayIcon(treeItem, cleanIcon, status);
-          addLintTooltip(treeItem, name, "No linter issues");
-        }
+        addOverlayIcon(treeItem, badge, status);
+        addLintTooltip(treeItem, name, "No linter issues");
         break;
       default:
         break;
@@ -319,8 +295,13 @@ public class LintStatusFilePainter implements 
IExplorerFilePaintListener {
         return;
       }
 
+      // No composite (Hop Web cannot draw one): leave the file's own icon 
alone. The item's
+      // colour already says what the status is, and replacing the icon with a 
bare badge would
+      // cost more than it tells.
       Image compositeIcon = getOrCreateComposite(base, lintIcon);
-      treeItem.setImage(compositeIcon != null ? compositeIcon : lintIcon);
+      if (compositeIcon != null) {
+        treeItem.setImage(compositeIcon);
+      }
       treeItem.setData(APPLIED_STATUS_KEY, status);
     } catch (Exception e) {
       log.logError("Error creating overlay icon: " + e.getMessage(), e);
@@ -344,7 +325,6 @@ public class LintStatusFilePainter implements 
IExplorerFilePaintListener {
 
   private Image createCompositeIcon(Image originalIcon, Image lintIcon) {
     try {
-      Display display = Display.getDefault();
       if (display == null || display.isDisposed()) {
         return null;
       }
@@ -409,17 +389,13 @@ public class LintStatusFilePainter implements 
IExplorerFilePaintListener {
     return LintEditorGraphHelper.isLintableFilename(filePath);
   }
 
-  /** Start the cache cleanup scheduler */
-  private void startCacheCleanupScheduler() {
-    // Clean up cache every 30 minutes
-    cleanupExecutor.scheduleAtFixedRate(
-        this::cleanupExpiredCacheEntries,
-        CACHE_EXPIRATION_MINUTES,
-        CACHE_EXPIRATION_MINUTES,
-        TimeUnit.MINUTES);
-  }
-
-  /** Clean up expired and excess cache entries */
+  /**
+   * Clean up expired and excess cache entries.
+   *
+   * <p>Done when the cache is rebuilt rather than on a timer of its own: a 
thread per painter is a
+   * thread per Hop Web session, and one holding a reference to the painter 
keeps that session's
+   * images alive long after the session is gone.
+   */
   private void cleanupExpiredCacheEntries() {
     try {
       long currentTime = System.currentTimeMillis();
@@ -484,21 +460,14 @@ public class LintStatusFilePainter implements 
IExplorerFilePaintListener {
         fileStatusCache.put(normalizedPath, status);
         cacheTimestamps.put(normalizedPath, System.currentTimeMillis());
       }
+      cleanupExpiredCacheEntries();
     } catch (Exception e) {
       log.logError("Error updating file status cache: " + e.getMessage(), e);
     }
   }
 
   public void dispose() {
-    if (errorIcon != null && !errorIcon.isDisposed()) {
-      errorIcon.dispose();
-    }
-    if (warningIcon != null && !warningIcon.isDisposed()) {
-      warningIcon.dispose();
-    }
-    if (cleanIcon != null && !cleanIcon.isDisposed()) {
-      cleanIcon.dispose();
-    }
+    // The badge icons belong to GuiResource, which disposes them with the 
session.
 
     // Dispose cached composite images
     for (Image composite : compositeIconCache.values()) {
@@ -511,13 +480,5 @@ public class LintStatusFilePainter implements 
IExplorerFilePaintListener {
     // Clean up cache
     fileStatusCache.clear();
     cacheTimestamps.clear();
-
-    // Shutdown cleanup executor
-    cleanupExecutor.shutdown();
-    try {
-      cleanupExecutor.awaitTermination(5, TimeUnit.SECONDS);
-    } catch (InterruptedException e) {
-      log.logError("Interrupted while shutting down cache cleanup executor", 
e);
-    }
   }
 }
diff --git 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LinterConfigPlugin.java 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LinterConfigPlugin.java
index 9d1850891e..d4250fbbef 100644
--- 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LinterConfigPlugin.java
+++ 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LinterConfigPlugin.java
@@ -436,7 +436,7 @@ public class LinterConfigPlugin implements IConfigOptions, 
IGuiPluginCompositeWi
 
     // Try to get project path from Hop variables
     try {
-      HopGui hopGui = HopGui.getInstance();
+      HopGui hopGui = HopGui.peekInstance();
       if (hopGui != null && hopGui.getVariables() != null) {
         String projectPath = hopGui.getVariables().getVariable("PROJECT_HOME");
         if (Utils.isEmpty(projectPath)) {
diff --git 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LinterGuiPlugin.java 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LinterGuiPlugin.java
index 2a7dbf9fc6..487aa78b91 100644
--- a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LinterGuiPlugin.java
+++ b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LinterGuiPlugin.java
@@ -33,6 +33,7 @@ import org.apache.hop.lint.registry.RuleRegistry;
 import org.apache.hop.metadata.api.IHopMetadataProvider;
 import org.apache.hop.ui.core.dialog.ErrorDialog;
 import org.apache.hop.ui.core.dialog.MessageBox;
+import org.apache.hop.ui.hopgui.BackgroundThreadFacade;
 import org.apache.hop.ui.hopgui.HopGui;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.widgets.Display;
@@ -81,7 +82,7 @@ public class LinterGuiPlugin {
   public static void lintProject() {
     // Add debug output at the very start
     LogChannel.GENERAL.logBasic("Lint Project menu item selected");
-    HopGui hopGui = HopGui.getInstance();
+    HopGui hopGui = HopGui.peekInstance();
 
     try {
       // Null-safety check for HopGui
@@ -124,79 +125,72 @@ public class LinterGuiPlugin {
       final String finalProjectName = projectName != null ? projectName : 
"Unknown Project";
 
       // Run linter in background thread
-      Thread linterThread =
-          new Thread(
-              () -> {
-                try {
-                  long lintStartTime = System.currentTimeMillis();
-                  HopLinter linter = new HopLinter();
-                  IHopMetadataProvider metadataProvider = 
finalHopGui.getMetadataProvider();
-
-                  List<LintResult> results =
-                      linter.run(
-                          finalProjectPath, metadataProvider, finalVariables, 
progressDialog);
-                  long lintEndTime = System.currentTimeMillis();
-                  long totalLintTime = lintEndTime - lintStartTime;
-
-                  // Check if cancelled
-                  if (progressDialog.isCancelled()) {
-                    LogChannel.GENERAL.logBasic("Linting cancelled by user");
-                    return;
-                  }
-
-                  // Generate rule summary
-                  Map<String, HopLinter.RuleSummary> ruleSummary =
-                      linter.generateRuleSummary(results);
-
-                  // Process and display results on UI thread
-                  Display.getDefault()
-                      .asyncExec(
-                          () -> {
-                            try {
-                              // Update results manager for GUI integration
-                              
LintResultsManager.getInstance().updateResults(results);
-                              
LintProblemsBarManager.getInstance().refreshAllOpenEditors();
-
-                              LinterGuiPlugin plugin = new LinterGuiPlugin();
-                              plugin.displayResults(
-                                  finalHopGui,
-                                  results,
-                                  ruleSummary,
-                                  totalLintTime,
-                                  finalProjectName);
-                            } catch (Exception e) {
-                              LogChannel.GENERAL.logError(
-                                  "Error displaying results: " + 
e.getMessage(), e);
-                            }
-                          });
-
-                } catch (Exception e) {
-                  LogChannel.GENERAL.logError("Error in linter thread: " + 
e.getMessage(), e);
-                  Display.getDefault()
-                      .asyncExec(
-                          () -> {
-                            if (finalHopGui != null && finalHopGui.getShell() 
!= null) {
-                              new ErrorDialog(
-                                  finalHopGui.getShell(),
-                                  "Linting Error",
-                                  "An error occurred while running the linter: 
" + e.getMessage(),
-                                  e);
-                            }
-                          });
-                } finally {
-                  // Ensure progress dialog closes
-                  Display.getDefault()
-                      .asyncExec(
-                          () -> {
-                            if (!progressDialog.isComplete()) {
-                              progressDialog.close();
-                            }
-                          });
-                }
-              });
-
-      // Start the thread and show progress dialog
-      linterThread.start();
+      BackgroundThreadFacade.start(
+          () -> {
+            try {
+              long lintStartTime = System.currentTimeMillis();
+              HopLinter linter = new HopLinter();
+              IHopMetadataProvider metadataProvider = 
finalHopGui.getMetadataProvider();
+
+              List<LintResult> results =
+                  linter.run(finalProjectPath, metadataProvider, 
finalVariables, progressDialog);
+              long lintEndTime = System.currentTimeMillis();
+              long totalLintTime = lintEndTime - lintStartTime;
+
+              // Check if cancelled
+              if (progressDialog.isCancelled()) {
+                LogChannel.GENERAL.logBasic("Linting cancelled by user");
+                return;
+              }
+
+              // Generate rule summary
+              Map<String, HopLinter.RuleSummary> ruleSummary = 
linter.generateRuleSummary(results);
+
+              // Process and display results on UI thread
+              Display.getDefault()
+                  .asyncExec(
+                      () -> {
+                        try {
+                          // Update results manager for GUI integration
+                          
LintResultsManager.getInstance().updateResults(results);
+                          
LintProblemsBarManager.getInstance().refreshAllOpenEditors();
+
+                          LinterGuiPlugin plugin = new LinterGuiPlugin();
+                          plugin.displayResults(
+                              finalHopGui, results, ruleSummary, 
totalLintTime, finalProjectName);
+                        } catch (Exception e) {
+                          LogChannel.GENERAL.logError(
+                              "Error displaying results: " + e.getMessage(), 
e);
+                        }
+                      });
+
+            } catch (Exception e) {
+              LogChannel.GENERAL.logError("Error in linter thread: " + 
e.getMessage(), e);
+              Display.getDefault()
+                  .asyncExec(
+                      () -> {
+                        if (finalHopGui != null && finalHopGui.getShell() != 
null) {
+                          new ErrorDialog(
+                              finalHopGui.getShell(),
+                              "Linting Error",
+                              "An error occurred while running the linter: " + 
e.getMessage(),
+                              e);
+                        }
+                      });
+            } finally {
+              // Ensure progress dialog closes
+              Display.getDefault()
+                  .asyncExec(
+                      () -> {
+                        if (!progressDialog.isComplete()) {
+                          progressDialog.close();
+                        }
+                      });
+            }
+          },
+          "HopLinter-Project");
+
+      // Show the progress dialog while the linter runs
       progressDialog.open();
 
     } catch (Exception e) {
@@ -278,7 +272,7 @@ public class LinterGuiPlugin {
       String projectName) {
     try {
       // Get project path from HopGui variables
-      HopGui hopGui = HopGui.getInstance();
+      HopGui hopGui = HopGui.peekInstance();
       IVariables variables = hopGui.getVariables();
       String projectPath = variables.getVariable("PROJECT_HOME");
 
@@ -434,7 +428,7 @@ public class LinterGuiPlugin {
       image = "lint-check.svg")
   public static void manageCustomRules() {
     try {
-      HopGui hopGui = HopGui.getInstance();
+      HopGui hopGui = HopGui.peekInstance();
       if (hopGui == null) {
         LogChannel.GENERAL.logError("HopGui instance not available");
         return;
diff --git 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LinterProgressDialog.java 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LinterProgressDialog.java
index bf700e799a..2dc4a97c46 100644
--- 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LinterProgressDialog.java
+++ 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LinterProgressDialog.java
@@ -144,7 +144,8 @@ public class LinterProgressDialog implements 
ProgressCallback {
   @Override
   public void updateProgress(String message, int completed, int total) {
     if (!dialog.isDisposed()) {
-      Display.getDefault()
+      dialog
+          .getDisplay()
           .asyncExec(
               () -> {
                 if (!dialog.isDisposed()) {
@@ -167,7 +168,8 @@ public class LinterProgressDialog implements 
ProgressCallback {
   public void setComplete(String message) {
     complete = true;
     if (!dialog.isDisposed()) {
-      Display.getDefault()
+      dialog
+          .getDisplay()
           .asyncExec(
               () -> {
                 if (!dialog.isDisposed()) {
diff --git 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/PreCommitLintExtension.java
 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/PreCommitLintExtension.java
index 2c5b496e37..b81d28db31 100644
--- 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/PreCommitLintExtension.java
+++ 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/PreCommitLintExtension.java
@@ -54,7 +54,7 @@ public class PreCommitLintExtension implements 
IExtensionPoint<HopGuiFileBeforeC
         return;
       }
 
-      HopGui hopGui = HopGui.getInstance();
+      HopGui hopGui = HopGui.peekInstance();
       if (hopGui == null) {
         log.logError("HopGui instance not available for pre-commit linting");
         return;
diff --git 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/ProjectLoadedLintExtension.java
 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/ProjectLoadedLintExtension.java
index d1aa761106..bdb1b3b9ab 100644
--- 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/ProjectLoadedLintExtension.java
+++ 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/ProjectLoadedLintExtension.java
@@ -37,7 +37,7 @@ public class ProjectLoadedLintExtension implements 
IExtensionPoint<Object> {
     if (Utils.isEmpty(projectPath)) {
       return;
     }
-    HopGui hopGui = HopGui.getInstance();
+    HopGui hopGui = HopGui.peekInstance();
     BackgroundLintService.getInstance()
         .lintProjectAsync(
             projectPath, hopGui != null ? hopGui.getMetadataProvider() : null, 
variables);
diff --git 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/WorkflowVerifyLintService.java
 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/WorkflowVerifyLintService.java
index 56e54ed9a9..efde4a1453 100644
--- 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/WorkflowVerifyLintService.java
+++ 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/WorkflowVerifyLintService.java
@@ -46,7 +46,7 @@ public final class WorkflowVerifyLintService {
       delegate.addWorkflowCheck();
       graph.extraViewTabFolder.setSelection(delegate.getWorkflowCheckTab());
 
-      HopGui hopGui = HopGui.getInstance();
+      HopGui hopGui = HopGui.peekInstance();
       WorkflowMeta workflowMeta = graph.getWorkflowMeta();
       String fileName = 
LintPathUtils.normalizePath(workflowMeta.getFilename());
 

Reply via email to