From 3f6e8538e10b19c5364353bc92560e18ee6c7e2f Mon Sep 17 00:00:00 2001 From: Marc Lehner Date: Fri, 27 Feb 2026 10:06:13 +0100 Subject: [PATCH 1/5] AP-25397: use correct variable name for required modules in python command test AP-25397 () --- .../knime/python3/scripting/nodes/prefs/PythonKernelTester.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonKernelTester.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonKernelTester.java index e1276b936..95498e6e4 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonKernelTester.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonKernelTester.java @@ -223,7 +223,7 @@ private static Process runPythonKernelTester(final ExternalProcessProvider pytho } if (!additionalRequiredModules.isEmpty()) { commandArguments.add("-m"); // Flag for additional modules. - additionalOptionalModules.forEach(moduleSpec -> commandArguments.add(moduleSpec.toString())); + additionalRequiredModules.forEach(moduleSpec -> commandArguments.add(moduleSpec.toString())); } if (!additionalOptionalModules.isEmpty()) { commandArguments.add("-o"); From af1ecfca36b2e4a6d08418110cb22a90b277a0eb Mon Sep 17 00:00:00 2001 From: Marc Lehner Date: Fri, 27 Feb 2026 11:52:33 +0100 Subject: [PATCH 2/5] AP-25397: fix for windows python path with spaces AP-25397 () --- .../knime/python3/scripting/nodes/prefs/PythonKernelTester.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonKernelTester.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonKernelTester.java index 95498e6e4..e96c15bc2 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonKernelTester.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonKernelTester.java @@ -103,7 +103,7 @@ private static String getPythonKernelTesterPath() throws IOException { if (url == null) { throw new IOException("Could not locate the python kernel tester script"); } - return FileUtil.getFileFromURL(FileLocator.toFileURL(url)).getAbsolutePath(); + return FileUtil.getFileFromURL(FileLocator.toFileURL(url)).getCanonicalPath(); } private PythonKernelTester() { From dcc0873fa8e4ee8d812774687f53b3e4ad4be14c Mon Sep 17 00:00:00 2001 From: Marc Lehner Date: Fri, 27 Feb 2026 11:54:27 +0100 Subject: [PATCH 3/5] AP-25397: remove imp from the required python packages list, as it is not supported in python >=3.12 AP-25397 () --- .../python-kernel-tester/PythonKernelTester.py | 1 - 1 file changed, 1 deletion(-) diff --git a/org.knime.python3.scripting.nodes/python-kernel-tester/PythonKernelTester.py b/org.knime.python3.scripting.nodes/python-kernel-tester/PythonKernelTester.py index 3b9180791..9e027ad26 100644 --- a/org.knime.python3.scripting.nodes/python-kernel-tester/PythonKernelTester.py +++ b/org.knime.python3.scripting.nodes/python-kernel-tester/PythonKernelTester.py @@ -448,7 +448,6 @@ def check_required_modules(self, additional_required_modules=None): self.check_module("traceback") self.check_module("os") self.check_module("pickle") - self.check_module("imp") self.check_module("types") # Non-standard modules. self.check_module("numpy") From 9f93b435d42da3ecefd93e683db0691f55a3b8bf Mon Sep 17 00:00:00 2001 From: Marc Lehner Date: Fri, 27 Feb 2026 11:56:11 +0100 Subject: [PATCH 4/5] AP-25397: Read stdout and stderr concurrently to avoid a deadlock if python fills std:err AP-25397 () --- .../nodes/prefs/PythonKernelTester.java | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonKernelTester.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonKernelTester.java index e96c15bc2..fa19a8a17 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonKernelTester.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonKernelTester.java @@ -149,22 +149,32 @@ private static synchronized PythonKernelTestResult testPythonInstallation(final final var process = runPythonKernelTester(pythonCommand, majorVersion, minimumVersion, additionalRequiredModules, additionalOptionalModules, testLogger); - // Get error output. + // Read stdout and stderr concurrently to avoid a deadlock: if Python fills + // one pipe buffer while Java blocks draining the other, both sides hang. final var errorWriter = new StringWriter(); - try (var err = process.getErrorStream()) { - IOUtils.copy(err, errorWriter, StandardCharsets.UTF_8); + final var outputWriter = new StringWriter(); + final var stderrReader = new Thread(() -> { + try (var err = process.getErrorStream()) { + IOUtils.copy(err, errorWriter, StandardCharsets.UTF_8); + } catch (final IOException e) { + LOGGER.debug("Error reading stderr of Python kernel tester", e); + } + }, "PythonKernelTester-stderr"); + stderrReader.start(); + try (var in = process.getInputStream()) { + IOUtils.copy(in, outputWriter, StandardCharsets.UTF_8); + } + try { + stderrReader.join(); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); } + var errorOutput = errorWriter.toString(); if (!errorOutput.isEmpty()) { testLogger.append("Error during execution: " + errorOutput + "\n"); errorOutput = decorateErrorOutputForKnownProblems(errorOutput); } - - // Get regular output. - final var outputWriter = new StringWriter(); - try (var in = process.getInputStream()) { - IOUtils.copy(in, outputWriter, StandardCharsets.UTF_8); - } final String testOutput = outputWriter.toString(); testLogger.append("Raw test output: \n" + testOutput + "\n"); From 2a5034b5029b42f21329e3e767b62806565d1b7e Mon Sep 17 00:00:00 2001 From: Marc Lehner Date: Fri, 27 Feb 2026 11:58:08 +0100 Subject: [PATCH 5/5] AP-25397: For python >=3.12 the DLLs are not automatically loaded and we need to add the proper PATH manually on Windows AP-25397 () --- .../knime/python3/SimplePythonCommand.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/org.knime.python3/src/main/java/org/knime/python3/SimplePythonCommand.java b/org.knime.python3/src/main/java/org/knime/python3/SimplePythonCommand.java index 3148e8fa2..574e2d38e 100644 --- a/org.knime.python3/src/main/java/org/knime/python3/SimplePythonCommand.java +++ b/org.knime.python3/src/main/java/org/knime/python3/SimplePythonCommand.java @@ -48,6 +48,8 @@ */ package org.knime.python3; +import java.io.File; +import java.nio.file.Path; import java.util.List; import org.knime.externalprocessprovider.ExternalProcessProvider; @@ -78,4 +80,52 @@ public SimplePythonCommand(final String... commands) { public SimplePythonCommand(final List command) { super(command); } + + /** + * Creates a {@link ProcessBuilder} for this command, prepending the Python executable's conda environment + * directories to {@code PATH}. On Windows, conda/pixi environments require several sub-directories to be on + * {@code PATH} for proper process startup: + *
    + *
  • The env root (contains the Python DLL)
  • + *
  • {@code Library/bin} (contains vcruntime, openssl, and other SxS-manifest DLLs)
  • + *
  • {@code Library/mingw-w64/bin} (contains mingw toolchain DLLs)
  • + *
  • {@code Library/usr/bin} (contains misc tools)
  • + *
  • {@code Scripts} (contains pip, etc.)
  • + *
+ * Without these, Python 3.12+ fails at startup with a Windows SxS assembly error because VC++ runtime DLLs + * cannot be located. Only directories that actually exist on disk are prepended to avoid polluting {@code PATH} + * for non-conda environments. + */ + @Override + public ProcessBuilder createProcessBuilder() { + final var pb = super.createProcessBuilder(); + final Path executableDir = getExecutablePath().getParent(); + if (executableDir != null) { + final var env = pb.environment(); + // ProcessBuilder.environment() on Windows stores keys case-insensitively but + // preserves the original casing. Find the existing key to avoid duplicates. + final String pathKey = env.keySet().stream() // + .filter(k -> k.equalsIgnoreCase("PATH")) // + .findFirst() // + .orElse("PATH"); + final String existingPath = env.getOrDefault(pathKey, ""); + + // Standard conda/pixi activation paths for Windows, in activation order. + final var dirsToAdd = new StringBuilder(); + for (final String subDir : new String[]{"", "Library\\bin", "Library\\mingw-w64\\bin", + "Library\\usr\\bin", "Scripts"}) { + final var candidate = subDir.isEmpty() ? executableDir : executableDir.resolve(subDir); + if (candidate.toFile().isDirectory()) { + if (dirsToAdd.length() > 0) { + dirsToAdd.append(File.pathSeparator); + } + dirsToAdd.append(candidate); + } + } + if (dirsToAdd.length() > 0) { + env.put(pathKey, dirsToAdd + File.pathSeparator + existingPath); + } + } + return pb; + } }