in src/installer/BeatPackageCompiler/BeatPackageCompiler.cs [18:219]
static void Main(string[] args)
{
var opts = CmdLineOptions.Parse(args);
var config = BuildConfiguration.Read(
Path.Combine(opts.ConfigDir, MagicStrings.Files.ConfigYaml));
Directory.CreateDirectory(opts.PackageOutDir);
if (!ArtifactPackage.FromFilename(opts.PackageName, out var ap))
throw new Exception("Unable to parse file name: " + opts.PackageName);
var pc = config.GetProductConfig(ap.TargetName);
var companyName = MagicStrings.Elastic;
var productSetName = MagicStrings.Beats.Name;
// A product can define a display name to be used.
// At the time of writing this line, elastic-agent is the only product that used it
var displayName = !string.IsNullOrEmpty(pc.DisplayName) ? pc.DisplayName : MagicStrings.Beats.Name + " " + ap.TargetName;
var exeName = ap.CanonicalTargetName + MagicStrings.Ext.DotExe;
// Generate UUID v5 from product properties.
// This UUID *must* be stable and unique between Beats.
var upgradeCode = Uuid5.FromString(ap.CanonicalTargetName);
string version = SupportBuildVersionForAgent(ap.Version);
var project = new Project(displayName)
{
InstallerVersion = 500,
GUID = upgradeCode,
Name = $"{displayName} {ap.SemVer} ({ap.Architecture})",
Description = pc.Description,
OutFileName = Path.Combine(opts.PackageOutDir, opts.PackageName),
Version = new Version(version),
// We massage LICENSE.txt into .rtf below
LicenceFile = Path.Combine(
opts.PackageOutDir,
MagicStrings.Files.PackageLicenseRtf(opts.PackageName)),
Platform = Platform.x64,
InstallScope = InstallScope.perMachine,
UI = WUI.WixUI_Minimal,
// TODO: Custom images?
BannerImage = Path.Combine(opts.ResDir, MagicStrings.Files.TopBannerBmp),
BackgroundImage = Path.Combine(opts.ResDir, MagicStrings.Files.LeftBannerBmp),
MajorUpgrade = new MajorUpgrade
{
AllowDowngrades = false,
AllowSameVersionUpgrades = false,
DowngradeErrorMessage = MagicStrings.Errors.NewerVersionInstalled,
},
};
project.Include(WixExtension.UI);
project.Include(WixExtension.Util);
project.ControlPanelInfo = new ProductInfo
{
Contact = companyName,
Manufacturer = companyName,
UrlInfoAbout = "https://www.elastic.co",
Comments = pc.Description + ". " + MagicStrings.Beats.Description,
ProductIcon = Path.Combine(
opts.ResDir,
Path.GetFileNameWithoutExtension(exeName) + MagicStrings.Ext.DotIco),
NoRepair = true,
};
// Convert LICENSE.txt to something richedit control can render
System.IO.File.WriteAllText(
Path.Combine(
opts.PackageOutDir,
MagicStrings.Files.PackageLicenseRtf(opts.PackageName)),
MagicStrings.Content.WrapWithRtf(
System.IO.File.ReadAllText(
Path.Combine(opts.PackageInDir, MagicStrings.Files.LicenseTxt))));
var textInfo = new CultureInfo("en-US", false).TextInfo;
var serviceDisplayName = $"{companyName} {textInfo.ToTitleCase(ap.TargetName)} {ap.SemVer}";
WixSharp.File service = null;
if (pc.IsWindowsService)
{
service = new WixSharp.File(Path.Combine(opts.PackageInDir, exeName));
// [INSTALLDIR] is evaluated at runtime so we cannot easily manipulate it here to remove the trailing slash
string installedPath = ("[INSTALLDIR]");
// TODO: CNDL1150 : ServiceConfig functionality is documented in the Windows Installer SDK to
// "not [work] as expected." Consider replacing ServiceConfig with the
// WixUtilExtension ServiceConfig element.
service.ServiceInstaller = new ServiceInstaller
{
Interactive = false,
Name = ap.CanonicalTargetName,
DisplayName = serviceDisplayName,
Description = pc.Description,
DependsOn = new[]
{
new ServiceDependency(MagicStrings.Services.Tcpip),
new ServiceDependency(MagicStrings.Services.Dnscache),
},
Arguments =
" --path.home " + (installedPath + ".").Quote() + // trailing dot is because installedpath ends in a slash
" --path.config " + (installedPath + ".").Quote() + // trailing dot is because installedpath ends in a slash
" --path.data " + (installedPath + "data").Quote() +
" --path.logs " + (installedPath + "logs").Quote() +
" -E logging.files.redirect_stderr=true",
DelayedAutoStart = false,
Start = SvcStartType.auto,
// Don't start on install, config file is likely not ready yet
//StartOn = SvcEvent.Install,
StopOn = SvcEvent.InstallUninstall_Wait,
RemoveOn = SvcEvent.InstallUninstall_Wait,
};
}
var packageContents = new List<WixEntity>
{
new Files(Path.Combine(opts.PackageInDir, MagicStrings.Files.All), path =>
{
var itm = path.ToLower();
bool isConfigFile = itm.EndsWith(ap.CanonicalTargetName + MagicStrings.Ext.DotYml, StringComparison.OrdinalIgnoreCase);
bool exclude =
// .exe must be excluded for service configuration to work
(pc.IsWindowsService && itm.EndsWith(exeName, StringComparison.OrdinalIgnoreCase))
// beats config file is handled further down
|| (!pc.IsAgent && isConfigFile);
// this is an "include" filter
return ! exclude;
})
};
packageContents.Add(pc.IsWindowsService ? service : null);
// For agent, the MSI installer copies the contents of the MSI to a temp folder
// and then shall call the 'elastic-agent install' command.
// When uninstalling, the 'elastic-agent uninstall' command.
if (pc.IsAgent)
{
// https://stackoverflow.com/a/311837
project.LaunchConditions.Add(new LaunchCondition("Privileged", "Elastic Agent MSI must run as an administrator"));
project.AddProperty(new Property("MSIUSEREALADMINDETECTION", "1"));
project.AddAction(new ManagedAction(AgentCustomAction.InstallAction, Return.check, When.After, Step.InstallExecute, Condition.NOT_Installed));
// https://stackoverflow.com/questions/320921/how-to-add-a-wix-custom-action-that-happens-only-on-uninstall-via-msi
// We invoke the custom action before the "RemoveFiles" step so in case the action fails we can fail the whole MSI uninstall flow
project.AddAction(new ManagedAction(AgentCustomAction.UnInstallAction, Return.check, When.Before, Step.RemoveFiles, Condition.BeingUninstalledAndNotBeingUpgraded));
// Upgrade custom action. Found that "AppSearch" is the first step after WIX_UPGRADE_DETECTED is set
project.AddAction(new ManagedAction(AgentCustomAction.UpgradeAction, Return.check, When.Before, Step.AppSearch, "WIX_UPGRADE_DETECTED AND NOT (REMOVE=\"ALL\")"));
}
if (!pc.IsAgent)
{
// Add a note to the final screen and a checkbox to open the directory of .example.yml file
var beatConfigExampleFileName = ap.CanonicalTargetName.Replace("-", "_") + ".example" + MagicStrings.Ext.DotYml;
var beatConfigExampleFileId = beatConfigExampleFileName + "_" + (uint) beatConfigExampleFileName.GetHashCode32();
project.AddProperty(new Property("WIXUI_EXITDIALOGOPTIONALTEXT",
$"NOTE: Only Administrators can modify configuration files! We put an example configuration file " +
$"in the data directory named {beatConfigExampleFileName}. Please copy this example file to " +
$"{ap.CanonicalTargetName}.yml and make changes according to your environment. Once {ap.CanonicalTargetName}.yml " +
$"is created, you can configure {ap.CanonicalTargetName} from your favorite shell (in an elevated prompt) " +
$"and then start {serviceDisplayName} Windows service.\r\n"));
HandleOpenExplorer(ap, project, beatConfigExampleFileId);
RenameConfigFile(opts, ap, packageContents, beatConfigExampleFileName, beatConfigExampleFileId);
}
// Drop CLI shim on disk
var cliShimScriptPath = Path.Combine(
opts.PackageOutDir,
MagicStrings.Files.ProductCliShim(ap.CanonicalTargetName));
System.IO.File.WriteAllText(cliShimScriptPath, Resources.GenericCliShim);
var beatsInstallPath =
$"[ProgramFiles{(ap.Is64Bit ? "64" : string.Empty)}Folder]\\" +