Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions cli/src/main/java/com/devonfw/tools/ide/os/WindowsHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,30 @@ public interface WindowsHelper {
*/
String getRegistryValue(String path, String key);

/**
* @param appName the application name to search for in the Windows registry.
* @return the DisplayName entry if the application is found in the Windows registry or {@code null} if nothing was found.
Comment thread
jakozian marked this conversation as resolved.
*/
String getDisplayVersionFromRegistry(String appName);

/**
* @param appName the application name to search for in the Windows registry.
* @return the DisplayIcon entry if the application is found in the Windows registry or {@code null} if nothing was found.
*/
String getDisplayIconFromRegistry(String appName);

/**
* @param appName the application name to search for in the Windows registry.
* @return the UninstallString entry if the application is found in the Windows registry or {@code null} if nothing was found.
*/
String getUninstallStringFromRegistry(String appName);

/**
* @param appName the application name to search for in the Windows registry.
* @return the InstallLocation entry if the application is found in the Windows registry or {@code null} if nothing was found.
*/
String getInstallLocationFromRegistry(String appName);

/**
* @param context the {@link IdeContext}.
* @return the instance of {@link WindowsHelper}.
Expand Down
80 changes: 77 additions & 3 deletions cli/src/main/java/com/devonfw/tools/ide/os/WindowsHelperImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ public class WindowsHelperImpl implements WindowsHelper {
/** Registry key for the users environment variables. */
public static final String HKCU_ENVIRONMENT = "HKCU\\Environment";

/** Common Windows registry base paths containing (uninstall) information for installed applications (system-wide and per-user). */
private static final String[] REGISTRY_BASE_PATHS = {
"HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
"HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
"HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
};

private final IdeContext context;

/**
Expand Down Expand Up @@ -60,13 +67,80 @@ public String getUserEnvironmentValue(String key) {
@Override
public String getRegistryValue(String path, String key) {

ProcessResult result = this.context.newProcess().errorHandling(ProcessErrorHandling.LOG_WARNING).executable("reg").addArgs("query", path, "/v", key)
List<String> out = runReg("query", path, "/v", key);
if (out != null) {
return retrieveRegString(key, out);
}
return null;
}

@Override
public String getDisplayVersionFromRegistry(String appName) {
return getRegistryValueBySearch(appName, "DisplayVersion");
}

@Override
public String getDisplayIconFromRegistry(String appName) {
return getRegistryValueBySearch(appName, "DisplayIcon");
}

@Override
public String getUninstallStringFromRegistry(String appName) {
return getRegistryValueBySearch(appName, "UninstallString");
}

@Override
public String getInstallLocationFromRegistry(String appName) {
return getRegistryValueBySearch(appName, "InstallLocation");
}

private String getRegistryValueBySearch(String appName, String key) {

String uninstallKey = findUninstallKey(appName);
if (uninstallKey == null) {
return null;
}
List<String> out = runReg("query", uninstallKey);
if (out != null) {
return retrieveRegString(key, out);
}
return null;
}

private String findUninstallKey(String appName) {

for (String registryBasePath : REGISTRY_BASE_PATHS) {
List<String> out = runReg("query", registryBasePath, "/s", "/f", appName);
Comment thread
hohwille marked this conversation as resolved.
if (out == null) {
continue;
}
for (String line : out) {
line = line.trim();
if (line.startsWith("HKEY_")) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if registryBasePath was "HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall" this will never be true? So it that entry in the constant array pointless then?
Should this rather be like this:

Suggested change
if (line.startsWith("HKEY_")) {
if (line.startsWith(registryBasePath)) {

Copy link
Copy Markdown
Contributor Author

@jakozian jakozian May 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The output of reg query <path> /s looks something like this:
image

That means the HKCU of the path is written out in the output and in this case HK = HKEY and CU=Current User. For that reason i did not check for line.startsWith(registryBasePath).
And the goal is to find the path to the tool and every path (doesn't matter which base path, LM or CU) begins with HKEY_ so that seemed to be the right decision in my opinion.

return line; // exact registry path (key) for tool
}
}
}
return null;

}

/**
* Executes a Windows registry command and returns its output.
*
* @param args the registry command arguments.
* @return the command output lines, or {@code null} if the command failed
*/
protected List<String> runReg(String... args) {
ProcessResult result = this.context.newProcess()
.errorHandling(ProcessErrorHandling.LOG_WARNING)
.executable("reg")
.addArgs(args)
.run(ProcessMode.DEFAULT_CAPTURE);
if (!result.isSuccessful()) {
return null;
}
List<String> out = result.getOut();
return retrieveRegString(key, out);
return result.getOut();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
*/
class WindowsHelperImplTest extends AbstractIdeContextTest {

private static final String TEST_APP_NAME = "TestApp";

private static final String UNKNOWN_TEST_APP_NAME = "UnknownApp";

/**
* Tests if the USER_PATH registry entry can be parsed properly.
*/
Expand Down Expand Up @@ -48,4 +52,47 @@ void testWindowsHelperParseEmptyRegStringReturnsNull() {
assertThat(regString).isNull();
}

/**
* Tests if correct keys can be found in registry output for app name filter.
*/
@Test
void testRegistryLookupReturnsCorrectEntryIfFound() {
Comment thread
jakozian marked this conversation as resolved.
// arrange
AbstractIdeTestContext context = new IdeTestContext();
WindowsHelperImpl helper = new WindowsHelperImplTestable(context);

// act
String displayVersion = helper.getDisplayVersionFromRegistry(TEST_APP_NAME);
String icon = helper.getDisplayIconFromRegistry(TEST_APP_NAME);
String uninstall = helper.getUninstallStringFromRegistry(TEST_APP_NAME);
String location = helper.getInstallLocationFromRegistry(TEST_APP_NAME);

// assert
assertThat(displayVersion).isEqualTo("1.1.1");
assertThat(icon).isEqualTo("C:\\Program Files\\TestApp\\testapp.exe,0");
assertThat(uninstall).isEqualTo("\"C:\\Program Files\\TestApp\\uninstall.exe\"");
assertThat(location).isEqualTo("C:\\Program Files\\TestApp");
}

/**
* Tests if registry lookup return nulls on unknown app name filter.
*/
@Test
void testRegistryLookupReturnsNullIfNotFound() {
// arrange
AbstractIdeTestContext context = new IdeTestContext();
WindowsHelperImpl helper = new WindowsHelperImplTestable(context);

// act
String displayVersion = helper.getDisplayVersionFromRegistry(UNKNOWN_TEST_APP_NAME);
String icon = helper.getDisplayIconFromRegistry(UNKNOWN_TEST_APP_NAME);
String uninstall = helper.getUninstallStringFromRegistry(UNKNOWN_TEST_APP_NAME);
String location = helper.getInstallLocationFromRegistry(UNKNOWN_TEST_APP_NAME);

// assert
assertThat(displayVersion).isNull();
assertThat(icon).isNull();
assertThat(uninstall).isNull();
assertThat(location).isNull();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.devonfw.tools.ide.os;

import java.util.List;

import com.devonfw.tools.ide.context.IdeContext;

/**
* Test-specific subclass of {@link WindowsHelperImpl}.
*
* <p>
* Mainly used as a test seam to simulate the reg.exe command for test purposes.
* </p>
*/
public class WindowsHelperImplTestable extends WindowsHelperImpl {

/**
* The constructor.
*
* @param context the {@link IdeContext}.
*/
public WindowsHelperImplTestable(IdeContext context) {

super(context);
}

@Override
protected List<String> runReg(String... args) {
Comment thread
jakozian marked this conversation as resolved.

String searchValue = extractFilterValue(args);
// Case: reg query <basePath> /s /f <appName>
if (searchValue != null) {
if (!"TestApp".equalsIgnoreCase(searchValue)) {
return List.of();
}
return List.of(
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\TestApp",
" DisplayName REG_SZ TestApp"
);
}
// Case: reg query <exactKey>
if (args.length >= 2 &&
args[0].equalsIgnoreCase("query") &&
args[1].endsWith("\\Uninstall\\TestApp")) {

return List.of(
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\TestApp",
" DisplayName REG_SZ TestApp",
" DisplayVersion REG_SZ 1.1.1",
" DisplayIcon REG_SZ C:\\Program Files\\TestApp\\testapp.exe,0",
" InstallLocation REG_SZ C:\\Program Files\\TestApp",
" UninstallString REG_SZ \"C:\\Program Files\\TestApp\\uninstall.exe\""
);
}

return List.of();
}


private static String extractFilterValue(String[] args) {

for (int i = 0; i < args.length - 1; i++) {
if ("/f".equalsIgnoreCase(args[i])) {
return args[i + 1];
}
}
return null;
}
}
80 changes: 80 additions & 0 deletions cli/src/test/java/com/devonfw/tools/ide/os/WindowsHelperMock.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.devonfw.tools.ide.os;

import java.util.List;
import java.util.Properties;

import com.devonfw.tools.ide.context.IdeContext;
Expand All @@ -11,6 +12,18 @@ public class WindowsHelperMock extends WindowsHelperImpl {

private final Properties env;

private static final String MOCK_APP_NAME = "TestApp";

private static final String MOCK_DISPLAY_VERSION = "1.1.1";

private static final String MOCK_INSTALL_LOCATION = "C:\\Program Files\\TestApp";

private static final String MOCK_DISPLAY_ICON =
"C:\\Program Files\\TestApp\\testapp.exe,0";
private static final String MOCK_UNINSTALL_STRING =
"\"C:\\Program Files\\TestApp\\uninstall.exe\"";
Comment on lines +15 to +24
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for mocking we should use a Map where mock data for tests can be registered instead of hard-coding random values.
Assuming you want to test PGAdmin now with mocked Windows on any OS then you could first register for the key pgadmin some object (Java record) containing the displayIcon, appName, and displayVersion, etc.



/**
* The constructor.
*/
Expand Down Expand Up @@ -41,6 +54,26 @@ public String getUserEnvironmentValue(String key) {
return this.env.getProperty(key);
}

@Override
public String getDisplayVersionFromRegistry(String appName) {
return matchesApp(appName) ? MOCK_DISPLAY_VERSION : null;
}

@Override
public String getDisplayIconFromRegistry(String appName) {
return matchesApp(appName) ? MOCK_DISPLAY_ICON : null;
}

@Override
public String getUninstallStringFromRegistry(String appName) {
return matchesApp(appName) ? MOCK_UNINSTALL_STRING : null;
}

@Override
public String getInstallLocationFromRegistry(String appName) {
return matchesApp(appName) ? MOCK_INSTALL_LOCATION : null;
}

@Override
public String getRegistryValue(String path, String key) {

Expand All @@ -51,4 +84,51 @@ public String getRegistryValue(String path, String key) {
}
return null;
}

private boolean matchesApp(String appName) {
return appName != null
&& appName.equalsIgnoreCase(MOCK_APP_NAME);
}

@Override
protected List<String> runReg(String... args) {

String searchValue = extractFilterValue(args);
// Case: reg query <basePath> /s /f <appName>
if (searchValue != null) {
if (!"TestApp".equalsIgnoreCase(searchValue)) {
return List.of();
}
return List.of(
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\TestApp",
" DisplayName REG_SZ TestApp"
);
}
// Case: reg query <exactKey>
if (args.length >= 2 &&
args[0].equalsIgnoreCase("query") &&
args[1].endsWith("\\Uninstall\\TestApp")) {

return List.of(
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\TestApp",
" DisplayName REG_SZ TestApp",
" DisplayVersion REG_SZ 1.1.1",
" DisplayIcon REG_SZ C:\\Program Files\\TestApp\\testapp.exe,0",
" InstallLocation REG_SZ C:\\Program Files\\TestApp",
" UninstallString REG_SZ \"C:\\Program Files\\TestApp\\uninstall.exe\""
);
}

return List.of();
}

private static String extractFilterValue(String[] args) {

for (int i = 0; i < args.length - 1; i++) {
if ("/f".equalsIgnoreCase(args[i])) {
return args[i + 1];
}
}
return null;
}
}
Loading