diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 605c5b81b..7b5d29471 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -14,6 +14,8 @@ Release with new features and bugfixes: * https://github.com/devonfw/IDEasy/issues/1517[#1517]: Fix IDEasy MSI does not configure Windows Terminal for git-bash * https://github.com/devonfw/IDEasy/issues/1227[#1227]: Added Setup instrcutions to the Windows installer * https://github.com/devonfw/IDEasy/issues/865[#865]: az not working on Mac +* 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]. 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 acff7cc21..0960d58e4 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 e07630ccf..cf69b1e52 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,10 @@ 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()) { + return new ProcessResultImpl(this.tool, this.tool, 0, List.of()); + } } return runTool(request.getProcessContext(), processMode, args); } @@ -344,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/main/java/com/devonfw/tools/ide/tool/ToolInstallation.java b/cli/src/main/java/com/devonfw/tools/ide/tool/ToolInstallation.java index b8f69dfec..83417e1ed 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); + } } 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 000000000..76b2efbb6 --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/tool/GlobalToolCommandletTest.java @@ -0,0 +1,100 @@ +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; +import com.devonfw.tools.ide.tool.ToolEdition; +import com.devonfw.tools.ide.tool.ToolEditionAndVersion; + +/** + * 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 (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 { + + AsyncInstallerToolCommandlet(IdeContext context) { + + super(context, TOOL_NAME, Set.of(Tag.DOCKER)); + } + + @Override + 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); + } + + @Override + protected ToolInstallation doInstall(ToolInstallRequest request) { + + VersionIdentifier version = VersionIdentifier.of(TOOL_VERSION); + return new ToolInstallation(null, null, null, version, true, true); + } + } + + /** + * 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 testInstallLogsWarningAndRunToolAbortsWhenInstallationIsAsynchronous() { + + // 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: runTool returns 0 without crashing with "command not found" + assertThat(result.getExitCode()).isEqualTo(0); + // 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."); + } +}