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 57e8e2a84a issue #7717: skip locked lib/core overwrite on Windows 
marketplace install (#7718)
57e8e2a84a is described below

commit 57e8e2a84a59eca0c06bb4fd3cbaf98ba6f6ff3b
Author: Matt Casters <[email protected]>
AuthorDate: Fri Jul 31 15:30:53 2026 +0200

    issue #7717: skip locked lib/core overwrite on Windows marketplace install 
(#7718)
    
    Windows locks jars already on the Hop system classpath (lib/core/*).
    Marketplace activation now skips identical shared jars on all platforms,
    never overwrites existing lib/core jars on Windows, and soft-fails copy
    errors so the rest of the plugin still installs. Missing lib/core jars
    are still installed (#7666).
---
 plugins/misc/marketplace/README.md                 |  6 ++
 .../hop/marketplace/install/PluginInstaller.java   | 84 ++++++++++++++++---
 .../marketplace/install/PluginInstallerTest.java   | 97 ++++++++++++++++++++++
 3 files changed, 176 insertions(+), 11 deletions(-)

diff --git a/plugins/misc/marketplace/README.md 
b/plugins/misc/marketplace/README.md
index 90f016de89..cb88d010d4 100644
--- a/plugins/misc/marketplace/README.md
+++ b/plugins/misc/marketplace/README.md
@@ -76,6 +76,12 @@ Coordinates:
 
 After install or uninstall, **restart Hop** so the plugin registry reloads.
 
+Shared jars under `lib/core/` (Maven `provided` scope from plugin zips) are 
installed so
+optional plugins match the full client classpath. Uninstall leaves them in 
place (sticky).
+On **Windows**, existing `lib/core` jars are never overwritten while Hop is 
running — the
+JVM locks those files on the system classpath. Identical content is skipped on 
all platforms;
+missing jars are still installed.
+
 ### hop-env.yaml
 
 See `config/projects/samples/marketplace/hop-env.example.yaml` (after install) 
or
diff --git 
a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/install/PluginInstaller.java
 
b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/install/PluginInstaller.java
index f7b635ecc2..79afdcbb42 100644
--- 
a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/install/PluginInstaller.java
+++ 
b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/install/PluginInstaller.java
@@ -32,6 +32,7 @@ import java.util.List;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.Const;
 import org.apache.hop.core.exception.HopException;
 import org.apache.hop.core.json.HopJson;
 import org.apache.hop.core.logging.ILogChannel;
@@ -258,18 +259,11 @@ public class PluginInstaller {
           Files.createDirectories(to);
         } else if (Files.isRegularFile(from)) {
           Files.createDirectories(to.getParent());
-          // Shared classpath jars (Maven provided → lib/core). Install them 
so marketplace
-          // packages match the full client assembly; warn when replacing 
different content.
-          if (isSharedCorePath(relative)
-              && Files.isRegularFile(to)
-              && Files.mismatch(from, to) != -1L) {
-            log.logBasic(
-                "Replacing shared lib/core jar with different content from "
-                    + artifactId
-                    + ": "
-                    + relative);
+          if (isSharedCorePath(relative)) {
+            activateSharedCoreJar(artifactId, relative, from, to);
+          } else {
+            Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING);
           }
-          Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING);
         }
       }
     } catch (IOException e) {
@@ -277,6 +271,49 @@ public class PluginInstaller {
     }
   }
 
+  /**
+   * Install shared classpath jars (Maven provided → {@code lib/core}). 
Missing jars are always
+   * copied. Existing jars that match the staged content are skipped. On 
Windows, existing jars are
+   * never overwritten (the running JVM locks {@code lib/core/*} on the system 
classpath). On other
+   * platforms, different content is replaced when possible; copy failures are 
logged and skipped so
+   * the rest of the plugin still activates.
+   */
+  private void activateSharedCoreJar(String artifactId, String relative, Path 
from, Path to)
+      throws IOException {
+    boolean targetExists = Files.isRegularFile(to);
+    Long mismatch = targetExists ? Files.mismatch(from, to) : null;
+
+    if (skipSharedCoreCopy(targetExists, mismatch, Const.isWindows())) {
+      if (targetExists && mismatch != null && mismatch != -1L) {
+        log.logBasic(
+            "Leaving existing shared lib/core jar in place (Windows locks 
system classpath jars): "
+                + relative
+                + " from "
+                + artifactId
+                + ". Content differs from the package; stop Hop and replace 
manually if needed.");
+      }
+      return;
+    }
+
+    if (targetExists && mismatch != null && mismatch != -1L) {
+      log.logBasic(
+          "Replacing shared lib/core jar with different content from "
+              + artifactId
+              + ": "
+              + relative);
+    }
+    try {
+      Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING);
+    } catch (IOException e) {
+      // File in use (Windows), antivirus, etc. — do not fail the whole plugin 
install.
+      log.logError(
+          "Could not write shared lib/core jar (file in use?): "
+              + relative
+              + " — leaving existing file. "
+              + e.getMessage());
+    }
+  }
+
   /**
    * Paths under {@code lib/core/} hold shared system-classpath jars (plugin 
assembly {@code
    * provided} scope). Marketplace <strong>installs</strong> these jars so 
optional plugins work the
@@ -287,6 +324,31 @@ public class PluginInstaller {
     return normalized.equals("lib/core") || normalized.startsWith("lib/core/");
   }
 
+  /**
+   * Whether activation should skip copying a staged lib/core file onto an 
existing target.
+   *
+   * <ul>
+   *   <li>Missing target → never skip (install new shared jars; see #7666).
+   *   <li>Identical content → always skip (avoids Windows lock failures when 
re-shipping the same
+   *       jar, e.g. avro).
+   *   <li>Different content on Windows → skip (cannot overwrite locked 
classpath jars).
+   *   <li>Different content elsewhere → do not skip (try replace).
+   * </ul>
+   *
+   * @param targetExists whether the destination file already exists
+   * @param mismatch result of {@link Files#mismatch} when the target exists; 
ignored when missing
+   * @param windows whether the host is Windows
+   */
+  static boolean skipSharedCoreCopy(boolean targetExists, Long mismatch, 
boolean windows) {
+    if (!targetExists) {
+      return false;
+    }
+    if (mismatch != null && mismatch == -1L) {
+      return true;
+    }
+    return windows;
+  }
+
   private List<String> unzipAllowedEntries(Path zipFile, Path stageRoot)
       throws IOException, HopException {
     List<String> paths = new ArrayList<>();
diff --git 
a/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/install/PluginInstallerTest.java
 
b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/install/PluginInstallerTest.java
index 9a67d9f43d..44ac307892 100644
--- 
a/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/install/PluginInstallerTest.java
+++ 
b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/install/PluginInstallerTest.java
@@ -62,6 +62,21 @@ class PluginInstallerTest {
     
assertFalse(PluginInstaller.isSharedCorePath("plugins/tech/parquet/lib/x.jar"));
   }
 
+  @Test
+  void skipSharedCoreCopyRules() {
+    // Missing target: always install (covers Azure / first-time shared deps, 
#7666)
+    assertFalse(PluginInstaller.skipSharedCoreCopy(false, null, false));
+    assertFalse(PluginInstaller.skipSharedCoreCopy(false, null, true));
+
+    // Identical content: always skip (Windows classpath locks + unnecessary 
I/O)
+    assertTrue(PluginInstaller.skipSharedCoreCopy(true, -1L, false));
+    assertTrue(PluginInstaller.skipSharedCoreCopy(true, -1L, true));
+
+    // Different content: skip only on Windows
+    assertFalse(PluginInstaller.skipSharedCoreCopy(true, 0L, false));
+    assertTrue(PluginInstaller.skipSharedCoreCopy(true, 0L, true));
+  }
+
   @Test
   void installAndUninstallFromLocalHttpRepo() throws Exception {
     byte[] zipBytes = buildPluginZip();
@@ -115,6 +130,88 @@ class PluginInstallerTest {
     }
   }
 
+  @Test
+  void installSkipsIdenticalPreexistingSharedCoreJar() throws Exception {
+    byte[] zipBytes = buildPluginZip();
+    HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
+    String path = 
"/org/apache/hop/hop-test-plugin/1.0.0/hop-test-plugin-1.0.0.zip";
+    server.createContext(
+        path,
+        exchange -> {
+          exchange.getResponseHeaders().add("Content-Type", "application/zip");
+          exchange.sendResponseHeaders(200, zipBytes.length);
+          exchange.getResponseBody().write(zipBytes);
+          exchange.close();
+        });
+    server.start();
+    try {
+      int port = server.getAddress().getPort();
+      Path hopHome = tempDir.resolve("hop-identical-core");
+      Files.createDirectories(hopHome.resolve("plugins"));
+      // Simulate slim client already shipping the same shared jar (e.g. avro 
on Windows)
+      Path sharedJar = hopHome.resolve("lib/core/shared.jar");
+      Files.createDirectories(sharedJar.getParent());
+      Files.writeString(sharedJar, "shared-lib");
+
+      MarketplaceConfig config = new MarketplaceConfig();
+      config.getRepositories().clear();
+      MarketplaceRepository local =
+          new MarketplaceRepository("local", "http://127.0.0.1:"; + port + "/", 
true);
+      config.getRepositories().add(local);
+
+      new PluginInstaller(new LogChannel("test"), hopHome, config)
+          .install(new MavenCoordinates("org.apache.hop", "hop-test-plugin", 
"1.0.0"), true);
+
+      
assertTrue(Files.isRegularFile(hopHome.resolve("plugins/tech/test/plugin.jar")));
+      assertEquals("shared-lib", Files.readString(sharedJar));
+    } finally {
+      server.stop(0);
+    }
+  }
+
+  @Test
+  void installHandlesPreexistingDifferentSharedCoreJar() throws Exception {
+    byte[] zipBytes = buildPluginZip();
+    HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
+    String path = 
"/org/apache/hop/hop-test-plugin/1.0.0/hop-test-plugin-1.0.0.zip";
+    server.createContext(
+        path,
+        exchange -> {
+          exchange.getResponseHeaders().add("Content-Type", "application/zip");
+          exchange.sendResponseHeaders(200, zipBytes.length);
+          exchange.getResponseBody().write(zipBytes);
+          exchange.close();
+        });
+    server.start();
+    try {
+      int port = server.getAddress().getPort();
+      Path hopHome = tempDir.resolve("hop-different-core");
+      Files.createDirectories(hopHome.resolve("plugins"));
+      Path sharedJar = hopHome.resolve("lib/core/shared.jar");
+      Files.createDirectories(sharedJar.getParent());
+      Files.writeString(sharedJar, "old-shared-lib");
+
+      MarketplaceConfig config = new MarketplaceConfig();
+      config.getRepositories().clear();
+      MarketplaceRepository local =
+          new MarketplaceRepository("local", "http://127.0.0.1:"; + port + "/", 
true);
+      config.getRepositories().add(local);
+
+      new PluginInstaller(new LogChannel("test"), hopHome, config)
+          .install(new MavenCoordinates("org.apache.hop", "hop-test-plugin", 
"1.0.0"), true);
+
+      // Plugin tree always activates; shared core replace depends on OS 
(#7717).
+      
assertTrue(Files.isRegularFile(hopHome.resolve("plugins/tech/test/plugin.jar")));
+      if (org.apache.hop.core.Const.isWindows()) {
+        assertEquals("old-shared-lib", Files.readString(sharedJar));
+      } else {
+        assertEquals("shared-lib", Files.readString(sharedJar));
+      }
+    } finally {
+      server.stop(0);
+    }
+  }
+
   @Test
   void installWithBasicAuth() throws Exception {
     byte[] zipBytes = buildPluginZip();

Reply via email to