From 83529975bfad25b05ee3f0eaf0b84703713ee919 Mon Sep 17 00:00:00 2001 From: Ali Shariati Najafabadi Date: Sun, 5 Jul 2026 21:22:40 +0200 Subject: [PATCH 1/4] #1659: abort runTool with warning when global tool installer runs in background --- .../tools/ide/tool/GlobalToolCommandlet.java | 3 +-- .../devonfw/tools/ide/tool/ToolCommandlet.java | 9 ++++++++- .../tools/ide/tool/ToolInstallation.java | 18 +++++++++++++++++- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/GlobalToolCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/tool/GlobalToolCommandlet.java index acff7cc211..0960d58e42 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/GlobalToolCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/GlobalToolCommandlet.java @@ -174,8 +174,7 @@ protected ToolInstallation doInstall(ToolInstallRequest request) { } installationPath = getInstallationPath(toolEdition.edition(), resolvedVersion); if (installationPath == null) { - throw new CliException("The tool " + this.tool + " is about to be installed. Please complete the installation and if required " - + "reboot your machine. Then rerun the command to start the tool.", 2); + return new ToolInstallation(null, null, null, resolvedVersion, true, true); } return createToolInstallation(installationPath, resolvedVersion, true, pc, false); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/ToolCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/tool/ToolCommandlet.java index e07630ccf1..2f68c1ad06 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/ToolCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/ToolCommandlet.java @@ -30,6 +30,7 @@ import com.devonfw.tools.ide.process.ProcessErrorHandling; import com.devonfw.tools.ide.process.ProcessMode; import com.devonfw.tools.ide.process.ProcessResult; +import com.devonfw.tools.ide.process.ProcessResultImpl; import com.devonfw.tools.ide.property.Property; import com.devonfw.tools.ide.property.ToolArgumentsProperty; import com.devonfw.tools.ide.security.ToolVersionChoice; @@ -272,7 +273,13 @@ public ProcessResult runTool(ToolInstallRequest request, ProcessMode processMode // we render this warning so the error gets detected and can be fixed but we do not block the user by skipping the installation. LOG.warn("Preventing infinity loop during installation of {}", request.getRequested(), new RuntimeException()); } else { - install(request); + ToolInstallation installation = install(request); + if (installation != null && installation.installedAsynchronously()) { + LOG.warn( + "The installation of {} is currently running in the background!\nYou need to complete the installation, potentially reboot and rerun your 'ide' command in a new terminal session after the installation has completed.", + request.getRequested()); + return new ProcessResultImpl(this.tool, this.tool, 0, List.of()); + } } return runTool(request.getProcessContext(), processMode, args); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/ToolInstallation.java b/cli/src/main/java/com/devonfw/tools/ide/tool/ToolInstallation.java index b8f69dfec2..83417e1edc 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/ToolInstallation.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/ToolInstallation.java @@ -14,7 +14,23 @@ * "bin"). * @param resolvedVersion the {@link VersionIdentifier} of the resolved tool version installed in {@code rootDir}. * @param newInstallation {@code true} if the tool has been newly installed, {@code false} otherwise (the tool was already installed before). + * @param installedAsynchronously {@code true} if the installation was launched in the background and the tool is not yet available, {@code false} otherwise. */ -public record ToolInstallation(Path rootDir, Path linkDir, Path binDir, VersionIdentifier resolvedVersion, boolean newInstallation) { +public record ToolInstallation(Path rootDir, Path linkDir, Path binDir, VersionIdentifier resolvedVersion, boolean newInstallation, + boolean installedAsynchronously) { + + /** + * Creates a {@link ToolInstallation} with {@code installedAsynchronously} set to {@code false}. + * + * @param rootDir see {@link #rootDir()}. + * @param linkDir see {@link #linkDir()}. + * @param binDir see {@link #binDir()}. + * @param resolvedVersion see {@link #resolvedVersion()}. + * @param newInstallation see {@link #newInstallation()}. + */ + public ToolInstallation(Path rootDir, Path linkDir, Path binDir, VersionIdentifier resolvedVersion, boolean newInstallation) { + + this(rootDir, linkDir, binDir, resolvedVersion, newInstallation, false); + } } From e47d7d412a35f72c998865bce928765c37dd7789 Mon Sep 17 00:00:00 2001 From: Ali Shariati Najafabadi Date: Sun, 5 Jul 2026 21:24:42 +0200 Subject: [PATCH 2/4] #1659: Add test for {GlobalToolCommandlet}. --- .../ide/tool/GlobalToolCommandletTest.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 cli/src/test/java/com/devonfw/tools/ide/tool/GlobalToolCommandletTest.java diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/GlobalToolCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/GlobalToolCommandletTest.java new file mode 100644 index 0000000000..858af15889 --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/tool/GlobalToolCommandletTest.java @@ -0,0 +1,76 @@ +package com.devonfw.tools.ide.tool; + +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.devonfw.tools.ide.common.Tag; +import com.devonfw.tools.ide.context.AbstractIdeContextTest; +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.context.IdeTestContext; +import com.devonfw.tools.ide.os.SystemInfoMock; +import com.devonfw.tools.ide.process.ProcessResult; +import com.devonfw.tools.ide.version.VersionIdentifier; + +/** + * Test of {@link GlobalToolCommandlet}. + */ +class GlobalToolCommandletTest extends AbstractIdeContextTest { + + private static final String TOOL_NAME = "docker"; + + private static final String TOOL_VERSION = "1.21.0"; + + /** + * Dummy {@link GlobalToolCommandlet} that simulates a background GUI installer. + */ + static class AsyncInstallerToolCommandlet extends GlobalToolCommandlet { + + AsyncInstallerToolCommandlet(IdeContext context) { + + super(context, TOOL_NAME, Set.of(Tag.DOCKER)); + } + + @Override + public ToolInstallation install(ToolInstallRequest request) { + + VersionIdentifier version = VersionIdentifier.of(TOOL_VERSION); + ToolEdition edition = new ToolEdition(TOOL_NAME, "rancher"); + ToolEditionAndVersion requested = new ToolEditionAndVersion(edition, version); + requested.setResolvedVersion(version); + request.setRequested(requested); + return new ToolInstallation(null, null, null, version, true, true); + } + + @Override + protected ToolInstallation doInstall(ToolInstallRequest request) { + + throw new UnsupportedOperationException("should not be called in this test"); + } + } + + /** + * Verifies that when a global tool installer runs in the background (asynchronously), {@link ToolCommandlet#runTool} logs a warning mentioning the tool + * name and version, and returns exit code 0 without trying to execute the not-yet-available binary. + */ + @Test + void testRunToolAbortsWithWarningWhenInstallationIsAsynchronous() { + + // arrange + IdeTestContext context = newContext(PROJECT_BASIC); + context.setSystemInfo(SystemInfoMock.WINDOWS_X64); + AsyncInstallerToolCommandlet commandlet = new AsyncInstallerToolCommandlet(context); + + // act + ProcessResult result = commandlet.runTool(List.of("ps")); + + // assert + assertThat(result.getExitCode()).isEqualTo(0); + assertThat(context).logAtWarning().hasMessageContaining(TOOL_NAME); + assertThat(context).logAtWarning().hasMessageContaining(TOOL_VERSION); + assertThat(context).logAtWarning().hasMessageContaining("is currently running in the background!"); + assertThat(context).logAtWarning() + .hasMessageContaining("rerun your 'ide' command in a new terminal session after the installation has completed."); + } +} From 6f544c495d09aeaba1c637e8b90e586833c893d3 Mon Sep 17 00:00:00 2001 From: Ali Shariati Najafabadi Date: Sun, 5 Jul 2026 21:54:16 +0200 Subject: [PATCH 3/4] #1659: Update CHANGELOG for release 2026.07.001 --- CHANGELOG.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index b95c6b8a9f..7f507310d0 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -6,6 +6,7 @@ This file documents all notable changes to https://github.com/devonfw/IDEasy[IDE Release with new features and bugfixes: +* https://github.com/devonfw/IDEasy/issues/1659[#1659]: Abort runTool with warning when global tool installer runs in background The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/46?closed=1[milestone 2026.07.001]. From e0ff246eeabcc0765848c51c6447aa27e2fd4cd3 Mon Sep 17 00:00:00 2001 From: Ali Shariati Najafabadi Date: Tue, 7 Jul 2026 00:05:15 +0200 Subject: [PATCH 4/4] #1659: move async-install warning from runTool to install to cover the all callers --- .../tools/ide/tool/ToolCommandlet.java | 11 +++-- .../ide/tool/GlobalToolCommandletTest.java | 44 ++++++++++++++----- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/ToolCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/tool/ToolCommandlet.java index 2f68c1ad06..cf69b1e522 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/ToolCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/ToolCommandlet.java @@ -275,9 +275,6 @@ public ProcessResult runTool(ToolInstallRequest request, ProcessMode processMode } else { ToolInstallation installation = install(request); if (installation != null && installation.installedAsynchronously()) { - LOG.warn( - "The installation of {} is currently running in the background!\nYou need to complete the installation, potentially reboot and rerun your 'ide' command in a new terminal session after the installation has completed.", - request.getRequested()); return new ProcessResultImpl(this.tool, this.tool, 0, List.of()); } } @@ -351,7 +348,13 @@ public ToolInstallation install(ToolInstallRequest request) { if (request.isInstallLoop()) { return toolAlreadyInstalled(request); } - return doInstall(request); + ToolInstallation installation = doInstall(request); + if (installation != null && installation.installedAsynchronously()) { + LOG.warn( + "The installation of {} is currently running in the background!\nYou need to complete the installation, potentially reboot and rerun your 'ide' command in a new terminal session after the installation has completed.", + request.getRequested()); + } + return installation; } /** diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/GlobalToolCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/GlobalToolCommandletTest.java index 858af15889..76b2efbb6a 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/tool/GlobalToolCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/tool/GlobalToolCommandletTest.java @@ -12,6 +12,8 @@ import com.devonfw.tools.ide.os.SystemInfoMock; import com.devonfw.tools.ide.process.ProcessResult; import com.devonfw.tools.ide.version.VersionIdentifier; +import com.devonfw.tools.ide.tool.ToolEdition; +import com.devonfw.tools.ide.tool.ToolEditionAndVersion; /** * Test of {@link GlobalToolCommandlet}. @@ -23,7 +25,8 @@ class GlobalToolCommandletTest extends AbstractIdeContextTest { private static final String TOOL_VERSION = "1.21.0"; /** - * Dummy {@link GlobalToolCommandlet} that simulates a background GUI installer. + * Dummy {@link GlobalToolCommandlet} that simulates a background GUI installer (e.g. Rancher Desktop on Windows). + * Only {@code doInstall} is overridden so the warning-check inside the real {@code install()} is exercised. */ static class AsyncInstallerToolCommandlet extends GlobalToolCommandlet { @@ -33,29 +36,29 @@ static class AsyncInstallerToolCommandlet extends GlobalToolCommandlet { } @Override - public ToolInstallation install(ToolInstallRequest request) { + protected void completeRequest(ToolInstallRequest request) { VersionIdentifier version = VersionIdentifier.of(TOOL_VERSION); ToolEdition edition = new ToolEdition(TOOL_NAME, "rancher"); ToolEditionAndVersion requested = new ToolEditionAndVersion(edition, version); requested.setResolvedVersion(version); request.setRequested(requested); - return new ToolInstallation(null, null, null, version, true, true); } @Override protected ToolInstallation doInstall(ToolInstallRequest request) { - throw new UnsupportedOperationException("should not be called in this test"); + VersionIdentifier version = VersionIdentifier.of(TOOL_VERSION); + return new ToolInstallation(null, null, null, version, true, true); } } /** - * Verifies that when a global tool installer runs in the background (asynchronously), {@link ToolCommandlet#runTool} logs a warning mentioning the tool - * name and version, and returns exit code 0 without trying to execute the not-yet-available binary. + * Verifies that when {@code doInstall} signals an asynchronous background installation, the real {@code install()} logs the warning and {@code runTool} + * returns exit code 0 without trying to execute the not-yet-available binary. */ @Test - void testRunToolAbortsWithWarningWhenInstallationIsAsynchronous() { + void testInstallLogsWarningAndRunToolAbortsWhenInstallationIsAsynchronous() { // arrange IdeTestContext context = newContext(PROJECT_BASIC); @@ -65,10 +68,31 @@ void testRunToolAbortsWithWarningWhenInstallationIsAsynchronous() { // act ProcessResult result = commandlet.runTool(List.of("ps")); - // assert + // assert: runTool returns 0 without crashing with "command not found" assertThat(result.getExitCode()).isEqualTo(0); - assertThat(context).logAtWarning().hasMessageContaining(TOOL_NAME); - assertThat(context).logAtWarning().hasMessageContaining(TOOL_VERSION); + // assert: warning was emitted by install() covering both "ide install docker" and "ide docker ps" paths + assertThat(context).logAtWarning().hasMessageContaining("is currently running in the background!"); + assertThat(context).logAtWarning() + .hasMessageContaining("rerun your 'ide' command in a new terminal session after the installation has completed."); + } + + /** + * Verifies that calling {@code install()} directly (as done by {@code ide install docker}) also logs the background-installation warning. + */ + @Test + void testInstallDirectlyAlsoLogsWarningWhenInstallationIsAsynchronous() { + + // arrange + IdeTestContext context = newContext(PROJECT_BASIC); + context.setSystemInfo(SystemInfoMock.WINDOWS_X64); + AsyncInstallerToolCommandlet commandlet = new AsyncInstallerToolCommandlet(context); + + // act + ToolInstallation installation = commandlet.install(); + + // assert: the async flag is set + assertThat(installation.installedAsynchronously()).isTrue(); + // assert: warning was logged even without runTool being called assertThat(context).logAtWarning().hasMessageContaining("is currently running in the background!"); assertThat(context).logAtWarning() .hasMessageContaining("rerun your 'ide' command in a new terminal session after the installation has completed.");