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();