1 /* 2 * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 package jdk.jpackage.test; 24 25 import java.io.IOException; 26 import java.nio.file.Files; 27 import java.nio.file.Path; 28 import java.util.*; 29 import java.util.function.BiConsumer; 30 import java.util.stream.Collectors; 31 import java.util.stream.Stream; 32 import jdk.jpackage.test.Functional.ThrowingRunnable; 33 import jdk.jpackage.test.PackageTest.PackageHandlers; 34 35 public class WindowsHelper { 36 37 static String getBundleName(JPackageCommand cmd) { 38 cmd.verifyIsOfType(PackageType.WINDOWS); 39 return String.format("%s-%s%s", cmd.name(), cmd.version(), 40 cmd.packageType().getSuffix()); 41 } 42 43 static Path getInstallationDirectory(JPackageCommand cmd) { 44 Path installSubDir = getInstallationSubDirectory(cmd); 45 if (isUserLocalInstall(cmd)) { 46 return USER_LOCAL.resolve(installSubDir); 47 } 48 return PROGRAM_FILES.resolve(installSubDir); 49 } 50 51 static Path getInstallationSubDirectory(JPackageCommand cmd) { 52 cmd.verifyIsOfType(PackageType.WINDOWS); 53 return Path.of(cmd.getArgumentValue("--install-dir", () -> cmd.name())); 54 } 55 56 private static void runMsiexecWithRetries(Executor misexec) { 57 Executor.Result result = null; 58 for (int attempt = 0; attempt != 3; ++attempt) { 59 result = misexec.executeWithoutExitCodeCheck(); 60 if (result.exitCode == 1618) { 61 // Another installation is already in progress. 62 // Wait a little and try again. 63 ThrowingRunnable.toRunnable(() -> Thread.sleep(3000)).run(); 64 continue; 65 } 66 break; 67 } 68 69 result.assertExitCodeIsZero(); 70 } 71 72 static PackageHandlers createMsiPackageHandlers() { 73 BiConsumer<JPackageCommand, Boolean> installMsi = (cmd, install) -> { 74 cmd.verifyIsOfType(PackageType.WIN_MSI); 75 runMsiexecWithRetries(Executor.of("msiexec", "/qn", "/norestart", 76 install ? "/i" : "/x").addArgument(cmd.outputBundle())); 77 }; 78 79 PackageHandlers msi = new PackageHandlers(); 80 msi.installHandler = cmd -> installMsi.accept(cmd, true); 81 msi.uninstallHandler = cmd -> installMsi.accept(cmd, false); 82 msi.unpackHandler = (cmd, destinationDir) -> { 83 cmd.verifyIsOfType(PackageType.WIN_MSI); 84 runMsiexecWithRetries(Executor.of("msiexec", "/a") 85 .addArgument(cmd.outputBundle().normalize()) 86 .addArguments("/qn", String.format("TARGETDIR=%s", 87 destinationDir.toAbsolutePath().normalize()))); 88 return destinationDir.resolve(getInstallationSubDirectory(cmd)); 89 }; 90 return msi; 91 } 92 93 static PackageHandlers createExePackageHandlers() { 94 PackageHandlers exe = new PackageHandlers(); 95 exe.installHandler = cmd -> { 96 cmd.verifyIsOfType(PackageType.WIN_EXE); 97 new Executor().setExecutable(cmd.outputBundle()).execute(); 98 }; 99 100 return exe; 101 } 102 103 public static String getMsiProperty(JPackageCommand cmd, String propertyName) { 104 cmd.verifyIsOfType(PackageType.WIN_MSI); 105 return Executor.of("cscript.exe", "//Nologo") 106 .addArgument(TKit.TEST_SRC_ROOT.resolve("resources/query-msi-property.js")) 107 .addArgument(cmd.outputBundle()) 108 .addArgument(propertyName) 109 .dumpOutput() 110 .executeAndGetOutput().stream().collect(Collectors.joining("\n")); 111 } 112 113 private static boolean isUserLocalInstall(JPackageCommand cmd) { 114 return cmd.hasArgument("--win-per-user-install"); 115 } 116 117 static class DesktopIntegrationVerifier { 118 119 DesktopIntegrationVerifier(JPackageCommand cmd) { 120 cmd.verifyIsOfType(PackageType.WINDOWS); 121 this.cmd = cmd; 122 verifyStartMenuShortcut(); 123 verifyDesktopShortcut(); 124 verifyFileAssociationsRegistry(); 125 } 126 127 private void verifyDesktopShortcut() { 128 boolean appInstalled = cmd.appLauncherPath().toFile().exists(); 129 if (cmd.hasArgument("--win-shortcut")) { 130 if (isUserLocalInstall(cmd)) { 131 verifyUserLocalDesktopShortcut(appInstalled); 132 verifySystemDesktopShortcut(false); 133 } else { 134 verifySystemDesktopShortcut(appInstalled); 135 verifyUserLocalDesktopShortcut(false); 136 } 137 } else { 138 verifySystemDesktopShortcut(false); 139 verifyUserLocalDesktopShortcut(false); 140 } 141 } 142 143 private Path desktopShortcutPath() { 144 return Path.of(cmd.name() + ".lnk"); 145 } 146 147 private void verifyShortcut(Path path, boolean exists) { 148 if (exists) { 149 TKit.assertFileExists(path); 150 } else { 151 TKit.assertPathExists(path, false); 152 } 153 } 154 155 private void verifySystemDesktopShortcut(boolean exists) { 156 Path dir = Path.of(queryRegistryValueCache( 157 SYSTEM_SHELL_FOLDERS_REGKEY, "Common Desktop")); 158 verifyShortcut(dir.resolve(desktopShortcutPath()), exists); 159 } 160 161 private void verifyUserLocalDesktopShortcut(boolean exists) { 162 Path dir = Path.of( 163 queryRegistryValueCache(USER_SHELL_FOLDERS_REGKEY, "Desktop")); 164 verifyShortcut(dir.resolve(desktopShortcutPath()), exists); 165 } 166 167 private void verifyStartMenuShortcut() { 168 boolean appInstalled = cmd.appLauncherPath().toFile().exists(); 169 if (cmd.hasArgument("--win-menu")) { 170 if (isUserLocalInstall(cmd)) { 171 verifyUserLocalStartMenuShortcut(appInstalled); 172 verifySystemStartMenuShortcut(false); 173 } else { 174 verifySystemStartMenuShortcut(appInstalled); 175 verifyUserLocalStartMenuShortcut(false); 176 } 177 } else { 178 verifySystemStartMenuShortcut(false); 179 verifyUserLocalStartMenuShortcut(false); 180 } 181 } 182 183 private Path startMenuShortcutPath() { 184 return Path.of(cmd.getArgumentValue("--win-menu-group", 185 () -> "Unknown"), cmd.name() + ".lnk"); 186 } 187 188 private void verifyStartMenuShortcut(Path shortcutsRoot, boolean exists) { 189 Path shortcutPath = shortcutsRoot.resolve(startMenuShortcutPath()); 190 verifyShortcut(shortcutPath, exists); 191 if (!exists) { 192 TKit.assertPathExists(shortcutPath.getParent(), false); 193 } 194 } 195 196 private void verifySystemStartMenuShortcut(boolean exists) { 197 verifyStartMenuShortcut(Path.of(queryRegistryValueCache( 198 SYSTEM_SHELL_FOLDERS_REGKEY, "Common Programs")), exists); 199 200 } 201 202 private void verifyUserLocalStartMenuShortcut(boolean exists) { 203 verifyStartMenuShortcut(Path.of(queryRegistryValueCache( 204 USER_SHELL_FOLDERS_REGKEY, "Programs")), exists); 205 } 206 207 private void verifyFileAssociationsRegistry() { 208 Stream.of(cmd.getAllArgumentValues("--file-associations")).map( 209 Path::of).forEach(this::verifyFileAssociationsRegistry); 210 } 211 212 private void verifyFileAssociationsRegistry(Path faFile) { 213 boolean appInstalled = cmd.appLauncherPath().toFile().exists(); 214 try { 215 TKit.trace(String.format( 216 "Get file association properties from [%s] file", 217 faFile)); 218 Map<String, String> faProps = Files.readAllLines(faFile).stream().filter( 219 line -> line.trim().startsWith("extension=") || line.trim().startsWith( 220 "mime-type=")).map( 221 line -> { 222 String[] keyValue = line.trim().split("=", 2); 223 return Map.entry(keyValue[0], keyValue[1]); 224 }).collect(Collectors.toMap( 225 entry -> entry.getKey(), 226 entry -> entry.getValue())); 227 String suffix = faProps.get("extension"); 228 String contentType = faProps.get("mime-type"); 229 TKit.assertNotNull(suffix, String.format( 230 "Check file association suffix [%s] is found in [%s] property file", 231 suffix, faFile)); 232 TKit.assertNotNull(contentType, String.format( 233 "Check file association content type [%s] is found in [%s] property file", 234 contentType, faFile)); 235 verifyFileAssociations(appInstalled, "." + suffix, contentType); 236 } catch (IOException ex) { 237 throw new RuntimeException(ex); 238 } 239 } 240 241 private void verifyFileAssociations(boolean exists, String suffix, 242 String contentType) { 243 String contentTypeFromRegistry = queryRegistryValue(Path.of( 244 "HKLM\\Software\\Classes", suffix).toString(), 245 "Content Type"); 246 String suffixFromRegistry = queryRegistryValue( 247 "HKLM\\Software\\Classes\\MIME\\Database\\Content Type\\" + contentType, 248 "Extension"); 249 250 if (exists) { 251 TKit.assertEquals(suffix, suffixFromRegistry, 252 "Check suffix in registry is as expected"); 253 TKit.assertEquals(contentType, contentTypeFromRegistry, 254 "Check content type in registry is as expected"); 255 } else { 256 TKit.assertNull(suffixFromRegistry, 257 "Check suffix in registry not found"); 258 TKit.assertNull(contentTypeFromRegistry, 259 "Check content type in registry not found"); 260 } 261 } 262 263 private final JPackageCommand cmd; 264 } 265 266 private static String queryRegistryValue(String keyPath, String valueName) { 267 var status = Executor.of("reg", "query", keyPath, "/v", valueName) 268 .saveOutput() 269 .executeWithoutExitCodeCheck(); 270 if (status.exitCode == 1) { 271 // Should be the case of no such registry value or key 272 String lookupString = "ERROR: The system was unable to find the specified registry key or value."; 273 TKit.assertTextStream(lookupString) 274 .predicate(String::equals) 275 .orElseThrow(() -> new RuntimeException(String.format( 276 "Failed to find [%s] string in the output", 277 lookupString))); 278 TKit.trace(String.format( 279 "Registry value [%s] at [%s] path not found", valueName, 280 keyPath)); 281 return null; 282 } 283 284 String value = status.assertExitCodeIsZero().getOutput().stream().skip(2).findFirst().orElseThrow(); 285 // Extract the last field from the following line: 286 // Common Desktop REG_SZ C:\Users\Public\Desktop 287 value = value.split(" REG_SZ ")[1]; 288 289 TKit.trace(String.format("Registry value [%s] at [%s] path is [%s]", 290 valueName, keyPath, value)); 291 292 return value; 293 } 294 295 private static String queryRegistryValueCache(String keyPath, 296 String valueName) { 297 String key = String.format("[%s][%s]", keyPath, valueName); 298 String value = REGISTRY_VALUES.get(key); 299 if (value == null) { 300 value = queryRegistryValue(keyPath, valueName); 301 REGISTRY_VALUES.put(key, value); 302 } 303 304 return value; 305 } 306 307 static final Set<Path> CRITICAL_RUNTIME_FILES = Set.of(Path.of( 308 "bin\\server\\jvm.dll")); 309 310 // jtreg resets %ProgramFiles% environment variable by some reason. 311 private final static Path PROGRAM_FILES = Path.of(Optional.ofNullable( 312 System.getenv("ProgramFiles")).orElse("C:\\Program Files")); 313 314 private final static Path USER_LOCAL = Path.of(System.getProperty( 315 "user.home"), 316 "AppData", "Local"); 317 318 private final static String SYSTEM_SHELL_FOLDERS_REGKEY = "HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders"; 319 private final static String USER_SHELL_FOLDERS_REGKEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders"; 320 321 private static final Map<String, String> REGISTRY_VALUES = new HashMap<>(); 322 }