| 1 | // Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. |
| 2 | |
| 3 | namespace WixToolsetTest.BurnE2E |
| 4 | { |
| 5 | using System; |
| 6 | using System.ComponentModel; |
| 7 | using System.IO; |
| 8 | using System.Runtime.InteropServices; |
| 9 | using Microsoft.Win32; |
| 10 | using WixInternal.TestSupport; |
| 11 | using WixTestTools; |
| 12 | using WixToolset.BootstrapperApplicationApi; |
| 13 | using Xunit; |
| 14 | using Xunit.Abstractions; |
| 15 | |
| 16 | public class LongPathTests : BurnE2ETests |
| 17 | { |
| 18 | public LongPathTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { } |
| 19 | |
| 20 | [RuntimeFact] |
| 21 | public void CanInstallAndUninstallSimpleBundle_x86_wixstdba() |
| 22 | { |
| 23 | this.CanInstallAndUninstallSimpleBundle("PackageA", "BundleA"); |
| 24 | } |
| 25 | |
| 26 | [RuntimeFact] |
| 27 | public void CanInstallAndUninstallSimpleBundle_x86_testba() |
| 28 | { |
| 29 | this.CanInstallAndUninstallSimpleBundle("PackageA", "BundleB"); |
| 30 | } |
| 31 | |
| 32 | [RuntimeFact] |
| 33 | public void CanInstallAndUninstallSimpleBundle_x86_dnctestba() |
| 34 | { |
| 35 | this.CanInstallAndUninstallSimpleBundle("PackageA", "BundleC"); |
| 36 | } |
| 37 | |
| 38 | [RuntimeFact] |
| 39 | public void CanInstallAndUninstallSimpleBundle_x86_wixba() |
| 40 | { |
| 41 | this.CanInstallAndUninstallSimpleBundle("PackageA", "BundleD"); |
| 42 | } |
| 43 | |
| 44 | [RuntimeFact] |
| 45 | public void CanInstallAndUninstallSimpleBundle_x64_wixstdba() |
| 46 | { |
| 47 | this.CanInstallAndUninstallSimpleBundle("PackageA_x64", "BundleA_x64"); |
| 48 | } |
| 49 | |
| 50 | [RuntimeFact(Skip = "This seems to be returning a different exit code now that the BA is out of proc. Needs more investigation.")] |
| 51 | public void CanInstallAndUninstallSimplePerUserBundle_x64_wixstdba() |
| 52 | { |
| 53 | this.CanInstallAndUninstallSimpleBundle("PackageApu_x64", "BundleApu_x64", "PackagePerUser.wxs", unchecked((int)0xc0000005)); |
| 54 | } |
| 55 | |
| 56 | [RuntimeFact] |
| 57 | public void CanInstallAndUninstallSimpleBundle_x64_testba() |
| 58 | { |
| 59 | this.CanInstallAndUninstallSimpleBundle("PackageA_x64", "BundleB_x64"); |
| 60 | } |
| 61 | |
| 62 | [RuntimeFact] |
| 63 | public void CanInstallAndUninstallSimpleBundle_x64_dnctestba() |
| 64 | { |
| 65 | this.CanInstallAndUninstallSimpleBundle("PackageA_x64", "BundleC_x64"); |
| 66 | } |
| 67 | |
| 68 | [RuntimeFact] |
| 69 | public void CanInstallAndUninstallSimpleBundle_x64_dncwixba() |
| 70 | { |
| 71 | this.CanInstallAndUninstallSimpleBundle("PackageA_x64", "BundleD_x64"); |
| 72 | } |
| 73 | |
| 74 | private void CanInstallAndUninstallSimpleBundle(string packageName, string bundleName, string fileName = "Package.wxs", int? alternateExitCode = null) |
| 75 | { |
| 76 | var package = this.CreatePackageInstaller(Path.Combine("..", "BasicFunctionalityTests", packageName)); |
| 77 | |
| 78 | var bundle = this.CreateBundleInstaller(Path.Combine("..", "BasicFunctionalityTests", bundleName)); |
| 79 | bundle.AlternateExitCode = alternateExitCode; |
| 80 | |
| 81 | using var dfs = new DisposableFileSystem(); |
| 82 | var baseFolder = GetLongPath(dfs.GetFolder()); |
| 83 | |
| 84 | var packageSourceCodeInstalled = package.GetInstalledFilePath(fileName); |
| 85 | |
| 86 | // Source file should *not* be installed |
| 87 | Assert.False(File.Exists(packageSourceCodeInstalled), $"{packageName} payload should not be there on test start: {packageSourceCodeInstalled}"); |
| 88 | |
| 89 | var bundleFileInfo = new FileInfo(bundle.Bundle); |
| 90 | var bundleCopiedPath = Path.Combine(baseFolder, bundleFileInfo.Name); |
| 91 | bundleFileInfo.CopyTo(bundleCopiedPath); |
| 92 | |
| 93 | bundle.Install(bundleCopiedPath); |
| 94 | bundle.VerifyRegisteredAndInPackageCache(); |
| 95 | |
| 96 | // Source file should be installed |
| 97 | Assert.True(File.Exists(packageSourceCodeInstalled), $"Should have found {packageName} payload installed at: {packageSourceCodeInstalled}"); |
| 98 | |
| 99 | if (alternateExitCode == bundle.LastExitCode) |
| 100 | { |
| 101 | WixAssert.Skip($"Install exited with {bundle.LastExitCode}"); |
| 102 | } |
| 103 | |
| 104 | bundle.Uninstall(bundleCopiedPath); |
| 105 | |
| 106 | // Source file should *not* be installed |
| 107 | Assert.False(File.Exists(packageSourceCodeInstalled), $"{packageName} payload should have been removed by uninstall from: {packageSourceCodeInstalled}"); |
| 108 | |
| 109 | bundle.VerifyUnregisteredAndRemovedFromPackageCache(); |
| 110 | |
| 111 | if (alternateExitCode == bundle.LastExitCode) |
| 112 | { |
| 113 | WixAssert.Skip($"Uninstall exited with {bundle.LastExitCode}"); |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | [RuntimeFact] |
| 118 | public void CanLayoutNonCompressedBundleToLongPath() |
| 119 | { |
| 120 | var nonCompressedBundle = this.CreateBundleInstaller("NonCompressedBundle"); |
| 121 | var testBAController = this.CreateTestBAController(); |
| 122 | |
| 123 | testBAController.SetPackageRequestedState("NetFx48Web", RequestState.None); |
| 124 | |
| 125 | using var dfs = new DisposableFileSystem(); |
| 126 | var layoutDirectory = GetLongPath(dfs.GetFolder()); |
| 127 | |
| 128 | nonCompressedBundle.Layout(layoutDirectory); |
| 129 | nonCompressedBundle.VerifyUnregisteredAndRemovedFromPackageCache(); |
| 130 | |
| 131 | Assert.True(File.Exists(Path.Combine(layoutDirectory, "NonCompressedBundle.exe"))); |
| 132 | Assert.True(File.Exists(Path.Combine(layoutDirectory, "PackageA.msi"))); |
| 133 | Assert.True(File.Exists(Path.Combine(layoutDirectory, "1a.cab"))); |
| 134 | Assert.False(File.Exists(Path.Combine(layoutDirectory, @"redist\ndp48-web.exe"))); |
| 135 | } |
| 136 | |
| 137 | [RuntimeFact] |
| 138 | public void CanInstallNonCompressedBundleWithLongTempPath() |
| 139 | { |
| 140 | this.InstallNonCompressedBundle(longTemp: true, useOriginalTempForLog: true); |
| 141 | } |
| 142 | |
| 143 | [RuntimeFact] |
| 144 | public void CannotInstallNonCompressedBundleWithLongPackageCachePath() |
| 145 | { |
| 146 | var installLogPath = this.InstallNonCompressedBundle((int)MSIExec.MSIExecReturnCode.ERROR_INSTALL_FAILURE, longPackageCache: true); |
| 147 | Assert.True(LogVerifier.MessageInLogFile(installLogPath, @"Error 0x80070643: Failed to install MSI package")); |
| 148 | } |
| 149 | |
| 150 | [RuntimeFact(Skip = "Temporarily disable since this test seems to get stuck in CI at the moment.")] |
| 151 | public void CannotInstallNonCompressedBundleWithLongWorkingPath() |
| 152 | { |
| 153 | var installLogPath = this.InstallNonCompressedBundle((int)MSIExec.MSIExecReturnCode.ERROR_FILENAME_EXCED_RANGE | unchecked((int)0x80070000), longWorkingPath: true); |
| 154 | Assert.True(LogVerifier.MessageInLogFile(installLogPath, @"Error 0x800700ce: Failed to load BA DLL")); |
| 155 | } |
| 156 | |
| 157 | public string InstallNonCompressedBundle(int expectedExitCode = 0, bool longTemp = false, bool useOriginalTempForLog = false, bool longWorkingPath = false, bool longPackageCache = false, int? alternateExitCode = null) |
| 158 | { |
| 159 | var deletePolicyKey = false; |
| 160 | string originalEngineWorkingDirectoryValue = null; |
| 161 | string originalPackageCacheValue = null; |
| 162 | var originalTemp = Environment.GetEnvironmentVariable("TMP"); |
| 163 | var packageA = this.CreatePackageInstaller("PackageA"); |
| 164 | var nonCompressedBundle = this.CreateBundleInstaller("NonCompressedBundle"); |
| 165 | var policyPath = nonCompressedBundle.GetFullBurnPolicyRegistryPath(); |
| 166 | string installLogPath = null; |
| 167 | |
| 168 | try |
| 169 | { |
| 170 | using var dfs = new DisposableFileSystem(); |
| 171 | var originalBaseFolder = dfs.GetFolder(); |
| 172 | var baseFolder = GetLongPath(originalBaseFolder); |
| 173 | var sourceFolder = Path.Combine(baseFolder, "source"); |
| 174 | var workingFolder = Path.Combine(baseFolder, "working"); |
| 175 | var tempFolder = Path.Combine(originalBaseFolder, new string('d', 260 - originalBaseFolder.Length - 2)); |
| 176 | var packageCacheFolder = Path.Combine(baseFolder, "package cache"); |
| 177 | |
| 178 | var copyResult = TestDataFolderFileSystem.RobocopyFolder(this.TestContext.TestDataFolder, sourceFolder); |
| 179 | Assert.True(copyResult.ExitCode >= 0 && copyResult.ExitCode < 8, $"Exit code: {copyResult.ExitCode}\r\nOutput: {String.Join("\r\n", copyResult.StandardOutput)}\r\nError: {String.Join("\r\n", copyResult.StandardError)}"); |
| 180 | |
| 181 | var bundleFileInfo = new FileInfo(nonCompressedBundle.Bundle); |
| 182 | var bundleCopiedPath = Path.Combine(sourceFolder, bundleFileInfo.Name); |
| 183 | |
| 184 | var policyKey = Registry.LocalMachine.OpenSubKey(policyPath, writable: true); |
| 185 | if (policyKey == null) |
| 186 | { |
| 187 | policyKey = Registry.LocalMachine.CreateSubKey(policyPath, writable: true); |
| 188 | deletePolicyKey = true; |
| 189 | } |
| 190 | |
| 191 | using (policyKey) |
| 192 | { |
| 193 | originalEngineWorkingDirectoryValue = policyKey.GetValue("EngineWorkingDirectory") as string; |
| 194 | originalPackageCacheValue = policyKey.GetValue("PackageCache") as string; |
| 195 | |
| 196 | if (longWorkingPath) |
| 197 | { |
| 198 | policyKey.SetValue("EngineWorkingDirectory", workingFolder); |
| 199 | } |
| 200 | |
| 201 | if (longPackageCache) |
| 202 | { |
| 203 | policyKey.SetValue("PackageCache", packageCacheFolder); |
| 204 | } |
| 205 | } |
| 206 | |
| 207 | if (longTemp) |
| 208 | { |
| 209 | Environment.SetEnvironmentVariable("TMP", tempFolder); |
| 210 | |
| 211 | if (useOriginalTempForLog) |
| 212 | { |
| 213 | nonCompressedBundle.LogDirectory = originalTemp; |
| 214 | } |
| 215 | } |
| 216 | |
| 217 | try |
| 218 | { |
| 219 | nonCompressedBundle.AlternateExitCode = alternateExitCode; |
| 220 | installLogPath = nonCompressedBundle.Install(bundleCopiedPath, expectedExitCode); |
| 221 | |
| 222 | if (alternateExitCode == nonCompressedBundle.LastExitCode) |
| 223 | { |
| 224 | WixAssert.Skip($"Install exited with {nonCompressedBundle.LastExitCode}"); |
| 225 | } |
| 226 | } |
| 227 | finally |
| 228 | { |
| 229 | TestDataFolderFileSystem.RobocopyFolder(tempFolder, originalTemp); |
| 230 | } |
| 231 | |
| 232 | installLogPath = Path.Combine(originalTemp, Path.GetFileName(installLogPath)); |
| 233 | |
| 234 | if (expectedExitCode == 0) |
| 235 | { |
| 236 | var registration = nonCompressedBundle.VerifyRegisteredAndInPackageCache(); |
| 237 | packageA.VerifyInstalled(true); |
| 238 | |
| 239 | nonCompressedBundle.Uninstall(registration.CachePath); |
| 240 | |
| 241 | if (alternateExitCode == nonCompressedBundle.LastExitCode) |
| 242 | { |
| 243 | WixAssert.Skip($"Uninstall exited with {nonCompressedBundle.LastExitCode}"); |
| 244 | } |
| 245 | } |
| 246 | |
| 247 | nonCompressedBundle.VerifyUnregisteredAndRemovedFromPackageCache(); |
| 248 | packageA.VerifyInstalled(false); |
| 249 | |
| 250 | return installLogPath; |
| 251 | } |
| 252 | finally |
| 253 | { |
| 254 | Environment.SetEnvironmentVariable("TMP", originalTemp); |
| 255 | |
| 256 | if (deletePolicyKey) |
| 257 | { |
| 258 | Registry.LocalMachine.DeleteSubKeyTree(policyPath); |
| 259 | } |
| 260 | else |
| 261 | { |
| 262 | using (var policyKey = Registry.LocalMachine.OpenSubKey(policyPath, writable: true)) |
| 263 | { |
| 264 | policyKey?.SetValue("EngineWorkingDirectory", originalEngineWorkingDirectoryValue); |
| 265 | policyKey?.SetValue("PackageCache", originalPackageCacheValue); |
| 266 | } |
| 267 | } |
| 268 | } |
| 269 | } |
| 270 | |
| 271 | private static string GetLongPath(string baseFolder) |
| 272 | { |
| 273 | Directory.CreateDirectory(baseFolder); |
| 274 | |
| 275 | // Try to create a directory that is longer than MAX_PATH but without the \\?\ prefix to detect OS support for long paths. |
| 276 | // Need to PInvoke CreateDirectoryW directly because .NET methods will append the \\?\ prefix. |
| 277 | foreach (var c in new char[] { 'a', 'b', 'c' }) |
| 278 | { |
| 279 | baseFolder = Path.Combine(baseFolder, new string(c, 100)); |
| 280 | if (!CreateDirectoryW(baseFolder, IntPtr.Zero)) |
| 281 | { |
| 282 | int lastError = Marshal.GetLastWin32Error(); |
| 283 | if (lastError == 206) |
| 284 | { |
| 285 | WixAssert.Skip($"MAX_PATH is being enforced ({baseFolder})"); |
| 286 | } |
| 287 | else if (lastError == 3) |
| 288 | { |
| 289 | // TODO: figure out why Windows Sandbox returns this error instead of 206 like all other environments. |
| 290 | WixAssert.Skip($"The system cannot find the path specified ({baseFolder})"); |
| 291 | } |
| 292 | throw new Win32Exception(lastError); |
| 293 | } |
| 294 | } |
| 295 | |
| 296 | return baseFolder; |
| 297 | } |
| 298 | |
| 299 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)] |
| 300 | [return: MarshalAs(UnmanagedType.Bool)] |
| 301 | private extern static bool CreateDirectoryW(string lpPathName, IntPtr lpSecurityAttributes); |
| 302 | } |
| 303 | } |