From c618541c83a238f50b0baef45e64097d6ca762c3 Mon Sep 17 00:00:00 2001 From: Stephen Burman Date: Wed, 6 Sep 2023 10:12:24 +1000 Subject: [PATCH 1/3] Adding extra testing for both net48 and net6.0 installers --- .nuke/build.schema.json | 4 +- build/Build.Tests.cs | 197 ++++++++++++------ .../Octopus.Tentacle.Installer/Product.wxs | 2 +- 3 files changed, 131 insertions(+), 72 deletions(-) diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index 3145368bf..8c4f0944f 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -123,7 +123,7 @@ "TestLinuxPackages", "TestOsx", "TestWindows", - "TestWindowsInstallerPermissions" + "TestWindowsInstallers" ] } }, @@ -166,7 +166,7 @@ "TestLinuxPackages", "TestOsx", "TestWindows", - "TestWindowsInstallerPermissions" + "TestWindowsInstallers" ] } }, diff --git a/build/Build.Tests.cs b/build/Build.Tests.cs index ea05ea3c0..7e6eb8800 100644 --- a/build/Build.Tests.cs +++ b/build/Build.Tests.cs @@ -1,6 +1,7 @@ // ReSharper disable RedundantUsingDirective using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Security.AccessControl; @@ -111,76 +112,10 @@ void RunLinuxPackageTestsFor(TestConfigurationOnLinuxDistribution testConfigurat [PublicAPI] //todo: move this out of the build script to a proper test project ("smoke tests"?) - Target TestWindowsInstallerPermissions => _ => _ + Target TestWindowsInstallers => _ => _ .Executes(() => { - string GetTestName(AbsolutePath installerPath) => Path.GetFileName(installerPath).Replace(".msi", ""); - - void TestInstallerPermissions(AbsolutePath installerPath) - { - var destination = TestDirectory / "install" / GetTestName(installerPath); - FileSystemTasks.EnsureExistingDirectory(destination); - - InstallMsi(installerPath, destination); - - try - { - var builtInUsersHaveWriteAccess = DoesSidHaveRightsToDirectory(destination, WellKnownSidType.BuiltinUsersSid, FileSystemRights.AppendData, FileSystemRights.CreateFiles); - if (builtInUsersHaveWriteAccess) - { - throw new Exception($"The installation destination {destination} has write permissions for the user BUILTIN\\Users. Expected write permissions to be removed by the installer."); - } - } - finally - { - UninstallMsi(installerPath); - } - - Log.Information($"BUILTIN\\Users do not have write access to {destination}. Hooray!"); - } - - void InstallMsi(AbsolutePath installerPath, AbsolutePath destination) - { - var installLogName = Path.Combine(TestDirectory, $"{GetTestName(installerPath)}.install.log"); - - Log.Information($"Installing {installerPath} to {destination}"); - - var arguments = $"/i {installerPath} /QN INSTALLLOCATION={destination} /L*V {installLogName}"; - Log.Information($"Running msiexec {arguments}"); - var installationProcess = ProcessTasks.StartProcess("msiexec", arguments); - installationProcess.WaitForExit(); - FileSystemTasks.CopyFileToDirectory(installLogName, ArtifactsDirectory); - if (installationProcess.ExitCode != 0) { - throw new Exception($"The installation process exited with a non-zero exit code ({installationProcess.ExitCode}). Check the log {installLogName} for details."); - } - } - - void UninstallMsi(AbsolutePath installerPath) - { - Log.Information($"Uninstalling {installerPath}"); - var uninstallLogName = Path.Combine(TestDirectory, $"{GetTestName(installerPath)}.uninstall.log"); - - var arguments = $"/x {installerPath} /QN /L*V {uninstallLogName}"; - Log.Information($"Running msiexec {arguments}"); - var uninstallProcess = ProcessTasks.StartProcess("msiexec", arguments); - uninstallProcess.WaitForExit(); - FileSystemTasks.CopyFileToDirectory(uninstallLogName, ArtifactsDirectory); - } - - bool DoesSidHaveRightsToDirectory(string directory, WellKnownSidType sid, params FileSystemRights[] rights) - { - var destinationInfo = new DirectoryInfo(directory); - var acl = destinationInfo.GetAccessControl(); - var identifier = new SecurityIdentifier(sid, null); - return acl - .GetAccessRules(true, true, typeof(SecurityIdentifier)) - .Cast() - .Where(r => r.IdentityReference.Value == identifier.Value) - .Where(r => r.AccessControlType == AccessControlType.Allow) - .Any(r => rights.Any(right => r.FileSystemRights.HasFlag(right))); - } - - Logging.InTest(nameof(TestWindowsInstallerPermissions), () => + Logging.InTest(nameof(TestWindowsInstaller), () => { FileSystemTasks.EnsureExistingDirectory(TestDirectory); FileSystemTasks.EnsureCleanDirectory(TestDirectory); @@ -194,10 +129,134 @@ bool DoesSidHaveRightsToDirectory(string directory, WellKnownSidType sid, params foreach (var installer in installers) { - TestInstallerPermissions(installer); + TestWindowsInstaller(installer); } }); }); + + string GetTestName(AbsolutePath installerPath) => Path.GetFileName(installerPath).Replace(".msi", ""); + + void TestWindowsInstaller(AbsolutePath installerPath) + { + Log.Information($"\n--------------------------------------\nTesting Installer {GetTestName(installerPath)}\n--------------------------------------"); + + var destination = TestDirectory / "install" / GetTestName(installerPath); + FileSystemTasks.EnsureExistingDirectory(destination); + + InstallMsi(installerPath, destination); + + try + { + ThenTentacleShouldHaveBeenInstalled(destination); + ThenTentacleShouldBeRunnable(destination); + ThenBuildInUserShouldNotHaveWritePermissions(destination); + } + finally + { + UninstallMsi(installerPath); + } + } + + void ThenTentacleShouldHaveBeenInstalled(string destination) + { + var expectedTentacleExe = Path.Combine(destination, "Tentacle.exe"); + + if (!File.Exists(expectedTentacleExe)) + { + throw new Exception($"Tentacle was not installed at {expectedTentacleExe}"); + } + Log.Information($"Tentacle was Installed at {expectedTentacleExe}"); + } + + void ThenTentacleShouldBeRunnable(string destination) + { + var tentacleExe = Path.Combine(destination, "Tentacle.exe"); + + Log.Information("Checking if Tentacle can run successfully..."); + + using var tentacleProcess = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = tentacleExe, + Arguments = "help", + UseShellExecute = false, + RedirectStandardOutput = true, + CreateNoWindow = true + } + }; + + tentacleProcess.Start(); + var output = new List(); + while (!tentacleProcess.StandardOutput.EndOfStream) + { + output.Add(tentacleProcess.StandardOutput.ReadLine() ?? ""); + } + + if (tentacleProcess.ExitCode != 0) + { + throw new Exception($"Tentacle exited with exit code {tentacleProcess.ExitCode}"); + } + + if (!output.Any(o => o.StartsWith("Usage: Tentacle"))) + { + throw new Exception($"Tentacle output does not look like '{tentacleProcess.StartInfo.Arguments}' argument was provided"); + } + + Log.Information($"Tentacle successfully ran using '{tentacleProcess.StartInfo.Arguments}' argument"); + } + + void ThenBuildInUserShouldNotHaveWritePermissions(string destination) + { + var builtInUsersHaveWriteAccess = DoesSidHaveRightsToDirectory(destination, WellKnownSidType.BuiltinUsersSid, FileSystemRights.AppendData, FileSystemRights.CreateFiles); + if (builtInUsersHaveWriteAccess) + { + throw new Exception($"The installation destination {destination} has write permissions for the user BUILTIN\\Users. Expected write permissions to be removed by the installer."); + } + Log.Information($"BUILTIN\\Users do not have write access to {destination}. Hooray!"); + } + + void InstallMsi(AbsolutePath installerPath, AbsolutePath destination) + { + var installLogName = Path.Combine(TestDirectory, $"{GetTestName(installerPath)}.install.log"); + + Log.Information($"Installing {installerPath} to {destination}"); + + var arguments = $"/i {installerPath} /QN INSTALLLOCATION={destination} /L*V {installLogName}"; + Log.Information($"Running msiexec {arguments}"); + var installationProcess = ProcessTasks.StartProcess("msiexec", arguments); + installationProcess.WaitForExit(); + + FileSystemTasks.CopyFileToDirectory(installLogName, ArtifactsDirectory, FileExistsPolicy.Overwrite); + if (installationProcess.ExitCode != 0) { + throw new Exception($"The installation process exited with a non-zero exit code ({installationProcess.ExitCode}). Check the log {installLogName} for details."); + } + } + + void UninstallMsi(AbsolutePath installerPath) + { + Log.Information($"Uninstalling {installerPath}"); + var uninstallLogName = Path.Combine(TestDirectory, $"{GetTestName(installerPath)}.uninstall.log"); + + var arguments = $"/x {installerPath} /QN /L*V {uninstallLogName}"; + Log.Information($"Running msiexec {arguments}"); + var uninstallProcess = ProcessTasks.StartProcess("msiexec", arguments); + uninstallProcess.WaitForExit(); + FileSystemTasks.CopyFileToDirectory(uninstallLogName, ArtifactsDirectory, FileExistsPolicy.Overwrite); + } + + bool DoesSidHaveRightsToDirectory(string directory, WellKnownSidType sid, params FileSystemRights[] rights) + { + var destinationInfo = new DirectoryInfo(directory); + var acl = destinationInfo.GetAccessControl(); + var identifier = new SecurityIdentifier(sid, null); + return acl + .GetAccessRules(true, true, typeof(SecurityIdentifier)) + .Cast() + .Where(r => r.IdentityReference.Value == identifier.Value) + .Where(r => r.AccessControlType == AccessControlType.Allow) + .Any(r => rights.Any(right => r.FileSystemRights.HasFlag(right))); + } void RunTests(string testFramework, string testRuntime) { diff --git a/installer/Octopus.Tentacle.Installer/Product.wxs b/installer/Octopus.Tentacle.Installer/Product.wxs index 70048c4cf..e3443fc78 100644 --- a/installer/Octopus.Tentacle.Installer/Product.wxs +++ b/installer/Octopus.Tentacle.Installer/Product.wxs @@ -14,7 +14,7 @@ - Product/Version : Change this every major build - Product/UpgradeCode : Never change this --> - + From b8ce42710caf706eccacd80f8a9e34d79ad4f3a4 Mon Sep 17 00:00:00 2001 From: Stephen Burman Date: Wed, 6 Sep 2023 10:20:23 +1000 Subject: [PATCH 2/3] Typos... --- build/Build.Tests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/Build.Tests.cs b/build/Build.Tests.cs index 7e6eb8800..514235181 100644 --- a/build/Build.Tests.cs +++ b/build/Build.Tests.cs @@ -149,7 +149,7 @@ void TestWindowsInstaller(AbsolutePath installerPath) { ThenTentacleShouldHaveBeenInstalled(destination); ThenTentacleShouldBeRunnable(destination); - ThenBuildInUserShouldNotHaveWritePermissions(destination); + ThenBuiltInUserShouldNotHaveWritePermissions(destination); } finally { @@ -165,7 +165,7 @@ void ThenTentacleShouldHaveBeenInstalled(string destination) { throw new Exception($"Tentacle was not installed at {expectedTentacleExe}"); } - Log.Information($"Tentacle was Installed at {expectedTentacleExe}"); + Log.Information($"Tentacle was successfully installed at {expectedTentacleExe}"); } void ThenTentacleShouldBeRunnable(string destination) @@ -206,7 +206,7 @@ void ThenTentacleShouldBeRunnable(string destination) Log.Information($"Tentacle successfully ran using '{tentacleProcess.StartInfo.Arguments}' argument"); } - void ThenBuildInUserShouldNotHaveWritePermissions(string destination) + void ThenBuiltInUserShouldNotHaveWritePermissions(string destination) { var builtInUsersHaveWriteAccess = DoesSidHaveRightsToDirectory(destination, WellKnownSidType.BuiltinUsersSid, FileSystemRights.AppendData, FileSystemRights.CreateFiles); if (builtInUsersHaveWriteAccess) From aa1a15da2b806b6c29761c6cc96fff00f671e856 Mon Sep 17 00:00:00 2001 From: Stephen Burman Date: Wed, 6 Sep 2023 12:07:20 +1000 Subject: [PATCH 3/3] Clearing out the installer directory so that files from previous installers do not make their way into subsequent installers --- .gitignore | 1 + build/Build.Pack.cs | 1 + installer/Octopus.Tentacle.Installer/Product.wxs | 2 +- installer/Octopus.Tentacle.Installer/Tentacle.Generated.wxs | 3 +++ 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ff701ad53..8268734a6 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ tools _artifacts _build +_test .DS_Store .env diff --git a/build/Build.Pack.cs b/build/Build.Pack.cs index c1fbe2279..61f0e4e56 100644 --- a/build/Build.Pack.cs +++ b/build/Build.Pack.cs @@ -158,6 +158,7 @@ void PackWindowsInstallers(MSBuildTargetPlatform platform, AbsolutePath wixNuget { var installerDirectory = BuildDirectory / "Installer"; FileSystemTasks.EnsureExistingDirectory(installerDirectory); + FileSystemTasks.EnsureCleanDirectory(installerDirectory); if (framework != NetCore) { diff --git a/installer/Octopus.Tentacle.Installer/Product.wxs b/installer/Octopus.Tentacle.Installer/Product.wxs index e3443fc78..e9a7298eb 100644 --- a/installer/Octopus.Tentacle.Installer/Product.wxs +++ b/installer/Octopus.Tentacle.Installer/Product.wxs @@ -14,7 +14,7 @@ - Product/Version : Change this every major build - Product/UpgradeCode : Never change this --> - + diff --git a/installer/Octopus.Tentacle.Installer/Tentacle.Generated.wxs b/installer/Octopus.Tentacle.Installer/Tentacle.Generated.wxs index fc51f0455..3f4e0b87d 100644 --- a/installer/Octopus.Tentacle.Installer/Tentacle.Generated.wxs +++ b/installer/Octopus.Tentacle.Installer/Tentacle.Generated.wxs @@ -1,4 +1,7 @@ + + +