From ed79cb2e249cf7728d0450f6e3a813885a9bc6d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Mar 2026 01:20:42 +0000 Subject: [PATCH] fix: resolve actual Windows Shell folder paths for shortcut removal during uninstall The uninstaller hardcoded Desktop/Start Menu paths as %USERPROFILE%\Desktop, but the installer creates shortcuts using the Windows Shell API which resolves the actual folder location. On systems with OneDrive Desktop backup, these paths differ, causing the uninstaller to miss the shortcuts. Now uses ShellLink to resolve the real paths and checks both resolved and hardcoded locations. https://claude.ai/code/session_01JbZdEobUPQcnNkQ3idkYp4 --- .../jdeploy/installer/win/InstallWindows.java | 17 ++- .../installer/win/UninstallWindows.java | 117 +++++++++++++----- 2 files changed, 99 insertions(+), 35 deletions(-) diff --git a/installer/src/main/java/ca/weblite/jdeploy/installer/win/InstallWindows.java b/installer/src/main/java/ca/weblite/jdeploy/installer/win/InstallWindows.java index b8fd4294..b34abf2d 100644 --- a/installer/src/main/java/ca/weblite/jdeploy/installer/win/InstallWindows.java +++ b/installer/src/main/java/ca/weblite/jdeploy/installer/win/InstallWindows.java @@ -465,19 +465,26 @@ private File installWindowsLink( link.setRunAsAdministrator(true); } link.save(); - - // Compute and return the shortcut file path based on type + + // Return the actual shortcut file path using ShellLink's resolved path, + // which correctly handles redirected folders (e.g., OneDrive Desktop) + String resolvedPath = link.getLinkPath(ShellLink.CURRENT_USER); + if (resolvedPath != null && !resolvedPath.isEmpty()) { + return new File(resolvedPath, appTitle + ".lnk"); + } + + // Fallback to hardcoded paths if ShellLink path resolution fails String userHome = System.getProperty("user.home"); String appData = System.getenv("APPDATA"); - + switch (type) { case ShellLink.DESKTOP: return new File(userHome, "Desktop" + File.separator + appTitle + ".lnk"); case ShellLink.PROGRAM_MENU: - return new File(appData, "Microsoft" + File.separator + "Windows" + File.separator + + return new File(appData, "Microsoft" + File.separator + "Windows" + File.separator + "Start Menu" + File.separator + "Programs" + File.separator + appTitle + ".lnk"); case ShellLink.START_MENU: - return new File(appData, "Microsoft" + File.separator + "Windows" + File.separator + + return new File(appData, "Microsoft" + File.separator + "Windows" + File.separator + "Start Menu" + File.separator + appTitle + ".lnk"); default: return null; diff --git a/installer/src/main/java/ca/weblite/jdeploy/installer/win/UninstallWindows.java b/installer/src/main/java/ca/weblite/jdeploy/installer/win/UninstallWindows.java index 47822617..7f2795c3 100644 --- a/installer/src/main/java/ca/weblite/jdeploy/installer/win/UninstallWindows.java +++ b/installer/src/main/java/ca/weblite/jdeploy/installer/win/UninstallWindows.java @@ -8,12 +8,15 @@ import ca.weblite.jdeploy.installer.services.ServiceDescriptorService; import ca.weblite.jdeploy.installer.services.ServiceDescriptorServiceFactory; import ca.weblite.tools.io.MD5; +import com.izforge.izpack.util.os.ShellLink; import org.apache.commons.io.FileUtils; import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; public class UninstallWindows { @@ -71,7 +74,24 @@ private File getPackagePath() { return PackagePathResolver.resolvePackagePath(packageName, version, source); } - private File getStartMenuPath() { + /** + * Resolves the actual Windows Shell folder path for a given link type using the + * Windows Shell API via ShellLink. Returns null if resolution fails. + */ + private String resolveShellFolderPath(int linkType) { + try { + ShellLink link = new ShellLink(linkType, "probe"); + String path = link.getLinkPath(ShellLink.CURRENT_USER); + if (path != null && !path.isEmpty()) { + return path; + } + } catch (Exception e) { + System.err.println("Warning: Could not resolve shell folder path for type " + linkType + ": " + e.getMessage()); + } + return null; + } + + private File getHardcodedStartMenuPath() { return new File(System.getProperty("user.home") + File.separator + "AppData" + File.separator + "Roaming" + File.separator + @@ -80,23 +100,48 @@ private File getStartMenuPath() { "Start Menu"); } - private File getStartMenuLink(String suffix) { - return new File(getStartMenuPath(), new File(appFileName + suffix).getName() + ".lnk"); + private File getHardcodedDesktopPath() { + return new File(System.getProperty("user.home") + File.separator + "Desktop"); } - private File getProgramsMenuPath() { - return new File(getStartMenuPath(), "Programs"); + /** + * Returns all possible Desktop directory paths where a shortcut might exist. + * Includes both the Windows Shell API-resolved path and the hardcoded fallback. + */ + private Set getDesktopPaths() { + Set paths = new LinkedHashSet<>(); + String resolved = resolveShellFolderPath(ShellLink.DESKTOP); + if (resolved != null) { + paths.add(new File(resolved)); + } + paths.add(getHardcodedDesktopPath()); + return paths; } - private File getProgramsMenuLink(String suffix) { - return new File(getProgramsMenuPath(), new File(appFileName + suffix).getName() + ".lnk"); + /** + * Returns all possible Start Menu directory paths where a shortcut might exist. + */ + private Set getStartMenuPaths() { + Set paths = new LinkedHashSet<>(); + String resolved = resolveShellFolderPath(ShellLink.START_MENU); + if (resolved != null) { + paths.add(new File(resolved)); + } + paths.add(getHardcodedStartMenuPath()); + return paths; } - private File getDesktopLink(String suffix) { - return new File( - System.getProperty("user.home") + File.separator + - "Desktop" + File.separator + new File(appFileName + suffix).getName() + ".lnk" - ); + /** + * Returns all possible Programs Menu directory paths where a shortcut might exist. + */ + private Set getProgramsMenuPaths() { + Set paths = new LinkedHashSet<>(); + String resolved = resolveShellFolderPath(ShellLink.PROGRAM_MENU); + if (resolved != null) { + paths.add(new File(resolved)); + } + paths.add(new File(getHardcodedStartMenuPath(), "Programs")); + return paths; } private File getAppDirPath() { @@ -271,12 +316,16 @@ private void scheduleDeleteUninstaller() throws IOException { private void removeDesktopAlias() { for (String suffix : new String[]{"", InstallWindows.RUN_AS_ADMIN_SUFFIX}) { - if (getDesktopLink(suffix).exists()) { - System.out.println("Deleting desktop link: "+getDesktopLink(suffix).getAbsolutePath()); - getDesktopLink(suffix).delete(); - if (installationLogger != null) { - installationLogger.logShortcut(InstallationLogger.FileOperation.DELETED, - getDesktopLink(suffix).getAbsolutePath(), null); + String linkName = new File(appFileName + suffix).getName() + ".lnk"; + for (File desktopDir : getDesktopPaths()) { + File link = new File(desktopDir, linkName); + if (link.exists()) { + System.out.println("Deleting desktop link: " + link.getAbsolutePath()); + link.delete(); + if (installationLogger != null) { + installationLogger.logShortcut(InstallationLogger.FileOperation.DELETED, + link.getAbsolutePath(), null); + } } } } @@ -284,12 +333,16 @@ private void removeDesktopAlias() { private void removeStartMenuLink() { for (String suffix : new String[]{"", InstallWindows.RUN_AS_ADMIN_SUFFIX}) { - if (getStartMenuLink(suffix).exists()) { - System.out.println("Deleting start menu link: " + getStartMenuLink(suffix).getAbsolutePath()); - getStartMenuLink(suffix).delete(); - if (installationLogger != null) { - installationLogger.logShortcut(InstallationLogger.FileOperation.DELETED, - getStartMenuLink(suffix).getAbsolutePath(), null); + String linkName = new File(appFileName + suffix).getName() + ".lnk"; + for (File startMenuDir : getStartMenuPaths()) { + File link = new File(startMenuDir, linkName); + if (link.exists()) { + System.out.println("Deleting start menu link: " + link.getAbsolutePath()); + link.delete(); + if (installationLogger != null) { + installationLogger.logShortcut(InstallationLogger.FileOperation.DELETED, + link.getAbsolutePath(), null); + } } } } @@ -297,12 +350,16 @@ private void removeStartMenuLink() { private void removeProgramsMenuLink() { for (String suffix : new String[]{"", InstallWindows.RUN_AS_ADMIN_SUFFIX}) { - if (getProgramsMenuLink(suffix).exists()) { - System.out.println("Deleting programs menu link: " + getProgramsMenuLink(suffix).getAbsolutePath()); - getProgramsMenuLink(suffix).delete(); - if (installationLogger != null) { - installationLogger.logShortcut(InstallationLogger.FileOperation.DELETED, - getProgramsMenuLink(suffix).getAbsolutePath(), null); + String linkName = new File(appFileName + suffix).getName() + ".lnk"; + for (File programsDir : getProgramsMenuPaths()) { + File link = new File(programsDir, linkName); + if (link.exists()) { + System.out.println("Deleting programs menu link: " + link.getAbsolutePath()); + link.delete(); + if (installationLogger != null) { + installationLogger.logShortcut(InstallationLogger.FileOperation.DELETED, + link.getAbsolutePath(), null); + } } } }