//******************************************************************************************************
// CustomActions.cs - Gbtc
//
// Copyright © 2013, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
// not use this file except in compliance with the License. You may obtain a copy of the License at:
//
// http://www.opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 04/08/2013 - Stephen C. Wills
// Generated original version of source code. Portions of code derived from Code Project article:
// http://www.codeproject.com/Articles/6164/A-ServiceInstaller-Extension-That-Enables-Recovery
// written by Neil Baliga
//
//******************************************************************************************************
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Principal;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows.Forms;
using System.Xml.Linq;
using System.Xml.XPath;
using GSF.Data;
using GSF.Identity;
using GSF.Interop;
using GSF.Security.Cryptography;
using GSF.ServiceProcess;
using Microsoft.Deployment.WindowsInstaller;
namespace GSF.InstallerActions
{
///
/// Class that contains custom actions for a WiX installer.
///
public class CustomActions
{
///
/// Custom action to attempt to authenticate the service account.
///
/// Session object containing data from the installer.
/// Result of the custom action.
[CustomAction]
public static ActionResult AuthenticateServiceAccountAction(Session session)
{
Logger logger = new Logger(session);
string serviceDomain = string.Empty;
string serviceUser = string.Empty;
logger.Log("Begin AuthenticateServiceAccountAction");
string serviceAccount = GetPropertyValue(session, "ServiceAccount");
string servicePassword = GetPropertyValue(session, "ServicePassword");
string[] splitServiceAccount = serviceAccount.Split('\\');
switch (splitServiceAccount.Length)
{
case 1:
serviceDomain = UserInfo.CurrentUserID.Split('\\')[0];
serviceUser = splitServiceAccount[0];
break;
case 2:
serviceDomain = splitServiceAccount[0];
serviceUser = splitServiceAccount[1];
break;
}
bool isSystemAccount =
serviceAccount.Equals("LocalSystem", StringComparison.OrdinalIgnoreCase) ||
serviceAccount.StartsWith(@"NT AUTHORITY\", StringComparison.OrdinalIgnoreCase) ||
serviceAccount.StartsWith(@"NT SERVICE\", StringComparison.OrdinalIgnoreCase);
bool isManagedServiceAccount = serviceAccount.EndsWith("$", StringComparison.Ordinal);
if (isSystemAccount)
{
session["SERVICEPASSWORD"] = string.Empty;
session["SERVICEAUTHENTICATED"] = "yes";
}
else if (isManagedServiceAccount)
{
using (UserInfo serviceAccountInfo = new UserInfo(serviceAccount))
{
bool isManagedServiceAccountValid = serviceAccountInfo.Exists &&
!serviceAccountInfo.AccountIsDisabled &&
!serviceAccountInfo.AccountIsLockedOut &&
serviceAccountInfo.GetUserPropertyValue("msDS-HostServiceAccountBL").Split(',')[0].
Equals("CN=" + Environment.MachineName, StringComparison.CurrentCultureIgnoreCase);
if (isManagedServiceAccountValid)
{
session["SERVICEPASSWORD"] = string.Empty;
session["SERVICEAUTHENTICATED"] = "yes";
}
else
{
session["SERVICEAUTHENTICATED"] = null;
}
}
}
else
{
IPrincipal servicePrincipal = UserInfo.AuthenticateUser(serviceDomain, serviceUser, servicePassword);
session["SERVICEAUTHENTICATED"] = servicePrincipal?.Identity.IsAuthenticated == true ? "yes" : null;
}
logger.Log("End AuthenticateServiceAccountAction");
return ActionResult.Success;
}
///
/// Custom action to write to an XML file.
///
/// Session object containing data from the installer.
/// Result of the custom action.
[CustomAction]
public static ActionResult XmlFileAction(Session session)
{
Logger logger = new Logger(session);
logger.Log("Begin XmlFileAction");
string filePath = GetPropertyValue(session, "FilePath");
string mode = GetPropertyValue(session, "Mode").ToUpper();
string mappings = GetPropertyValue(session, "PropertyMappings");
if (File.Exists(filePath))
{
// ReSharper disable PossibleMultipleEnumeration
try
{
XDocument document = XDocument.Load(filePath);
foreach (KeyValuePair mapping in mappings.ParseKeyValuePairs())
{
IEnumerable query = (IEnumerable)document.XPathEvaluate(mapping.Value);
if (mode != "WRITE")
{
session[mapping.Key] = query.OfType().Select(element => element.Value)
.Concat(query.OfType().Select(attribute => attribute.Value))
.FirstOrDefault();
}
else
{
string value = GetPropertyValue(session, mapping.Key);
query.OfType().ToList().ForEach(attribute =>
{
if (!string.IsNullOrEmpty(value))
attribute.Value = value;
else
attribute.Remove();
});
query.OfType().ToList().ForEach(element =>
{
if (!string.IsNullOrEmpty(value))
element.Value = value;
else
element.Remove();
});
document.Save(filePath);
}
}
}
catch (Exception ex)
{
string action = mode == "WRITE" ? "update" : "read";
string message = $"Failed to {action} XML file: {ex.Message}";
logger.Log(InstallMessage.Error, message);
logger.Log(EventLogEntryType.Error, ex);
logger.Log($"FilePath = {filePath}");
logger.Log($"PropertyMappings = {mappings}");
return ActionResult.Failure;
}
// ReSharper restore PossibleMultipleEnumeration
}
logger.Log("End XmlFileAction");
return ActionResult.Success;
}
[CustomAction]
public static ActionResult ConnectionStringAction(Session session)
{
Logger logger = new Logger(session);
logger.Log("Begin ConnectionStringAction");
string connectionString = GetPropertyValue(session, "ConnectionString");
string mappings = GetPropertyValue(session, "PropertyMappings");
Dictionary settings = connectionString.ParseKeyValuePairs();
foreach (KeyValuePair kvp in mappings.ParseKeyValuePairs())
{
if (settings.TryGetValue(kvp.Value, out string setting))
session[kvp.Key] = setting;
else
session[kvp.Key] = null;
}
logger.Log("End ConnectionStringAction");
return ActionResult.Success;
}
///
/// Custom action to load any previously defined CompanyName, CompanyAcronym and SystemName settings from the configuration file of an installed service.
///
/// Session object containing data from the installer.
/// Result of the custom action.
[CustomAction]
public static ActionResult LoadExistingCompanyInfoAction(Session session)
{
Logger logger = new Logger(session);
logger.Log("Begin LoadExistingCompanyInfoAction");
string serviceName = GetPropertyValue(session, "SERVICENAME");
string configPath = Path.Combine(GetPropertyValue(session, "INSTALLFOLDER"), $"{serviceName}.exe.config");
void setPropertyValue(XElement systemSettings, string settingName, string propertyName)
{
XElement setting = systemSettings.Elements("add").FirstOrDefault(element => string.Compare(element.Attributes("name").FirstOrDefault()?.Value, settingName, StringComparison.OrdinalIgnoreCase) == 0);
string value = setting?.Attributes("value").FirstOrDefault()?.Value;
if (!string.IsNullOrEmpty(value))
session[propertyName] = value;
}
if (File.Exists(configPath))
{
XDocument config = XDocument.Load(configPath);
XElement systemSettings = config.Descendants("systemSettings").FirstOrDefault();
if (systemSettings != null)
{
setPropertyValue(systemSettings, "CompanyName", "COMPANYNAME");
setPropertyValue(systemSettings, "CompanyAcronym", "COMPANYACRONYM");
setPropertyValue(systemSettings, "SystemName", "SYSTEMNAME");
}
}
logger.Log("End LoadExistingCompanyInfoAction");
return ActionResult.Success;
}
///
/// Custom action to write CompanyName, CompanyAcronym and SystemName settings to the configuration file of an installed service.
///
/// Session object containing data from the installer.
/// Result of the custom action.
[CustomAction]
public static ActionResult CompanyInfoAction(Session session)
{
const string DefaultCompanyName = "Grid Protection Alliance";
const string DefaultCompanyAcronym = "GPA";
const string DefaultSystemName = " ";
Logger logger = new Logger(session);
logger.Log("Begin CompanyInfoAction");
string validAcronym(string propertyValue)
{
try
{
return Regex.Replace(propertyValue.Trim().ToUpperInvariant(), @"[^A-Z0-9\-!_\.@#\$]+", "_", RegexOptions.Compiled);
}
catch (Exception ex)
{
logger.Log(EventLogEntryType.Warning, ex);
return propertyValue;
}
}
string serviceName = GetPropertyValue(session, "SERVICENAME");
string configPath = Path.Combine(GetPropertyValue(session, "INSTALLDIR"), $"{serviceName}.exe.config");
string companyName = GetPropertyValue(session, "COMPANYNAME") ?? DefaultCompanyName;
string companyAcronym = validAcronym(GetPropertyValue(session, "COMPANYACRONYM") ?? DefaultCompanyAcronym);
string systemName = validAcronym(GetPropertyValue(session, "SYSTEMNAME") ?? DefaultSystemName);
if (File.Exists(configPath))
{
XDocument config = XDocument.Load(configPath);
foreach (XElement systemSettings in config.Descendants("systemSettings"))
{
// Search for existing CompanyName settings and update their values
foreach (XElement companyNameElement in systemSettings.Elements("add").Where(element => element.Attributes("name").Any(nameAttribute => string.Compare(nameAttribute.Value, "CompanyName", StringComparison.OrdinalIgnoreCase) == 0)))
companyNameElement.Attributes("value").ToList().ForEach(valueAttribute => valueAttribute.Value = companyName);
// Search for existing CompanyAcronym settings and update their values
foreach (XElement companyAcronymElement in systemSettings.Elements("add").Where(element => element.Attributes("name").Any(nameAttribute => string.Compare(nameAttribute.Value, "CompanyAcronym", StringComparison.OrdinalIgnoreCase) == 0)))
companyAcronymElement.Attributes("value").ToList().ForEach(valueAttribute => valueAttribute.Value = companyAcronym);
// Search for existing SystemName settings and update their values
foreach (XElement systemNameElement in systemSettings.Elements("add").Where(element => element.Attributes("name").Any(nameAttribute => string.Compare(nameAttribute.Value, "SystemName", StringComparison.OrdinalIgnoreCase) == 0)))
systemNameElement.Attributes("value").ToList().ForEach(valueAttribute => valueAttribute.Value = systemName);
// Add CompanyName setting if no such setting exists
if (!systemSettings.Elements("add").Any(element => element.Attributes("name").Any(nameAttribute => string.Compare(nameAttribute.Value, "CompanyName", StringComparison.OrdinalIgnoreCase) == 0)))
{
systemSettings.Add(new XElement("add",
new XAttribute("name", "CompanyName"),
new XAttribute("value", companyName),
new XAttribute("description", $"The name of the company who owns this instance of the {serviceName}."),
new XAttribute("encrypted", "false")
));
}
// Add CompanyAcronym setting if no such setting exists
if (!systemSettings.Elements("add").Any(element => element.Attributes("name").Any(nameAttribute => string.Compare(nameAttribute.Value, "CompanyAcronym", StringComparison.OrdinalIgnoreCase) == 0)))
{
systemSettings.Add(new XElement("add",
new XAttribute("name", "CompanyAcronym"),
new XAttribute("value", companyAcronym),
new XAttribute("description", $"The acronym representing the company who owns this instance of the {serviceName}."),
new XAttribute("encrypted", "false")
));
}
// Add SystemName setting if no such setting exists
if (!systemSettings.Elements("add").Any(element => element.Attributes("name").Any(nameAttribute => string.Compare(nameAttribute.Value, "SystemName", StringComparison.OrdinalIgnoreCase) == 0)))
{
systemSettings.Add(new XElement("add",
new XAttribute("name", "SystemName"),
new XAttribute("value", systemName),
new XAttribute("description", $"Name of system for this instance of the {serviceName} that will be prefixed to system level tags, when defined. Value should follow tag naming conventions, e.g., no spaces and all upper case."),
new XAttribute("encrypted", "false")
));
}
}
config.Save(configPath);
}
logger.Log("End CompanyInfoAction");
return ActionResult.Success;
}
///
/// Custom action to configure various failure settings of an installed service.
///
/// Session object containing data from the installer.
/// Result of the custom action.
[CustomAction]
public static ActionResult ConfigureServiceAction(Session session)
{
Logger logger = new Logger(session);
logger.Log("Begin ConfigureServiceAction");
UpdateServiceConfig(session, logger);
logger.Log("End ConfigureServiceAction");
return ActionResult.Success;
}
///
/// Custom action to set up the group and necessary permissions for the service account.
///
/// Session object containing data from the installer.
/// Result of the custom action.
[CustomAction]
public static ActionResult ServiceAccountAction(Session session)
{
Logger logger = new Logger(session);
logger.Log("Begin ServiceAccountAction");
// Get properties from the installer session
string serviceName = GetPropertyValue(session, "SERVICENAME");
string serviceAccount = GetPropertyValue(session, "SERVICEACCOUNT");
string servicePorts = GetPropertyValue(session, "HTTPSERVICEPORTS");
string serviceAdminsGroupName = $"{serviceName} Admins";
// Create service admins group
try
{
logger.Log($"Creating {serviceAdminsGroupName} group...");
UserInfo.CreateLocalGroup(serviceAdminsGroupName, $"Members in this group have the necessary rights to administrate the {serviceName} service.");
logger.Log($"Done creating {serviceAdminsGroupName} group...");
}
catch (Exception ex)
{
logger.Log(InstallMessage.Error, $"Failed to create {serviceAdminsGroupName} group!");
logger.Log(EventLogEntryType.Error, ex);
}
void addServiceAccountToGroup(string groupName)
{
try
{
logger.Log($"Adding {serviceAccount} user to {groupName} group...");
UserInfo.AddUserToLocalGroup(groupName, serviceAccount);
logger.Log($"Done adding {serviceAccount} user to {groupName} group.");
}
catch (InvalidOperationException ex)
{
// This exception is thrown when group doesn't exist, just treat this as a warning
logger.Log(EventLogEntryType.Warning, ex);
}
catch (Exception ex)
{
logger.Log(InstallMessage.Error, $"Failed to add {serviceAccount} user to {groupName} group!");
logger.Log(EventLogEntryType.Error, ex);
}
}
// Determine if we're dealing with the local system account
bool isLocalSystem = serviceAccount == "LocalSystem";
// Don't attempt grant rights to the local system account because it already has them
if (!isLocalSystem)
{
// Create service admins group and add service account to that group as well as the Performance Log Users group
addServiceAccountToGroup(serviceAdminsGroupName);
addServiceAccountToGroup("Performance Monitor Users");
addServiceAccountToGroup("Performance Log Users");
try
{
logger.Log($"Adding {serviceAccount} user right to logon as a service...");
AddPrivileges(serviceAccount, WindowsApi.SE_SERVICE_LOGON_NAME);
logger.Log($"Done adding {serviceAccount} user right to logon as a service...");
}
catch (Exception ex)
{
logger.Log(InstallMessage.Error, $"Failed to add {serviceAccount} user right to logon as a service!");
logger.Log(EventLogEntryType.Error, ex);
}
try
{
logger.Log($"Adding {serviceAccount} user right to change system time...");
AddPrivileges(serviceAccount, WindowsApi.SE_SYSTEMTIME_NAME);
logger.Log($"Done adding {serviceAccount} user right to change system time...");
}
catch (Exception ex)
{
// This right is only needed for updating system clock, so its treated as a warning
logger.Log(EventLogEntryType.Warning, ex);
}
}
// Attempt to grant rights to start and stop the service
try
{
string accountSID = UserInfo.UserNameToSID(serviceAccount);
string groupSID = UserInfo.GroupNameToSID(serviceAdminsGroupName);
string acl;
using (Process process = new Process())
{
process.StartInfo.FileName = "sc";
process.StartInfo.Arguments = $"sdshow {serviceName}";
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
process.Start();
acl = process.StandardOutput.ReadToEnd().Trim();
process.WaitForExit();
if (process.ExitCode != 0)
throw new Exception($"Non-zero exit code ({process.ExitCode}) returned from 'sc sdshow {serviceName}'.");
}
int index = acl.IndexOf("S:", StringComparison.Ordinal);
// Don't attempt grant rights to the local system account because it already has them
if (!isLocalSystem && accountSID.StartsWith("S-") && !acl.Contains(accountSID))
acl = acl.Insert(index, $"(A;;RPWPCR;;;{accountSID})");
if (groupSID.StartsWith("S-") && !acl.Contains(groupSID))
acl = acl.Insert(index, $"(A;;RPWPCR;;;{groupSID})");
using (Process process = Process.Start("sc", $"sdset {serviceName} \"{acl}\""))
{
if (process == null)
throw new Exception($"Process start failed for 'sc sdset {serviceName} \"{acl}\"'.");
process.WaitForExit();
if (process.ExitCode != 0)
throw new Exception($"Non-zero exit code ({process.ExitCode}) returned from 'sc sdset {serviceName} \"{acl}\"'.");
}
}
catch (Exception ex)
{
string message = $"Failed to grant {serviceAccount} rights to restart the service!";
logger.Log(InstallMessage.Error, message);
logger.Log(EventLogEntryType.Error, ex);
}
// Don't attempt grant rights to the local system account because it already has them
if (!isLocalSystem && !string.IsNullOrEmpty(servicePorts))
{
logger.Log("Adding namespace reservations for default web services...");
foreach (string endPoint in servicePorts.Split(',').Select(p => GetEndPoint(p.Trim())))
{
// Remove existing HTTP namespace reservations in case
// the current user does not match the service account
RemoveHttpNamespaceReservation(endPoint);
AddHttpNamespaceReservation(serviceAccount, endPoint);
}
logger.Log("Done adding namespace reservations for default web services.");
}
logger.Log("End ServiceAccountAction");
return ActionResult.Success;
}
///
/// Prompts the user to select a file via the open file dialog.
///
/// Session object containing data from the installer.
/// Result of the custom action.
[CustomAction]
public static ActionResult BrowseFileAction(Session session)
{
Logger logger = new Logger(session);
Thread staThread;
logger.Log("Begin BrowseFileAction");
staThread = new Thread(() =>
{
using (OpenFileDialog openFileDialog = new OpenFileDialog())
{
openFileDialog.CheckFileExists = true;
openFileDialog.FileName = GetPropertyValue(session, "SelectedFile");
openFileDialog.DefaultExt = GetPropertyValue(session, "BrowseFileExtension");
openFileDialog.Filter = $"{GetPropertyValue(session, "BrowseFileExtension").ToUpper()} Files|*.{GetPropertyValue(session, "BrowseFileExtension")}|All Files|*.*";
if (openFileDialog.ShowDialog() == DialogResult.OK)
session["SELECTEDFILE"] = openFileDialog.FileName;
else
session["SELECTEDFILE"] = null;
}
});
staThread.SetApartmentState(ApartmentState.STA);
staThread.Start();
staThread.Join();
logger.Log("End BrowseFileAction");
return ActionResult.Success;
}
///
/// Determines whether a file exists.
///
/// Session object containing data from the installer.
/// Result of the custom action.
[CustomAction]
public static ActionResult CheckFileExistenceAction(Session session)
{
Logger logger = new Logger(session);
logger.Log("Begin CheckFileExistenceAction");
session["FILEEXISTS"] = File.Exists(GetPropertyValue(session, "FILEPATH")) ? "yes" : null;
logger.Log("End CheckFileExistenceAction");
return ActionResult.Success;
}
///
/// Extracts a zip file to a destination folder.
///
/// Session object containing data from the installer.
/// Result of the custom action.
[CustomAction]
public static ActionResult UnzipAction(Session session)
{
Logger logger = new Logger(session);
logger.Log("Begin UnzipAction");
string zipFile = GetPropertyValue(session, "ZIPFILE");
string sourceDir = GetPropertyValue(session, "SOURCEDIR") ?? string.Empty;
string destinationDir = GetPropertyValue(session, "DESTINATIONDIR") ?? string.Empty;
sourceDir = sourceDir.Replace('\\', '/').EnsureEnd('/');
ZipArchive archive = ZipFile.OpenRead(zipFile);
foreach (ZipArchiveEntry entry in archive.Entries.Where(entry => entry.FullName.StartsWith(sourceDir)))
{
if (entry.FullName.EndsWith("/"))
continue;
string filePath = Path.Combine(destinationDir, entry.FullName.Substring(sourceDir.Length).Replace('/', '\\'));
string directoryPath = Path.GetDirectoryName(filePath);
if (string.IsNullOrWhiteSpace(directoryPath))
continue;
if (!Directory.Exists(directoryPath))
Directory.CreateDirectory(directoryPath);
using (Stream stream = entry.Open())
using (FileStream fileStream = File.Create(filePath))
{
stream.CopyTo(fileStream);
}
}
logger.Log("End UnzipAction");
return ActionResult.Success;
}
///
/// Tests a connection to a database server.
///
/// Session object containing data from the installer.
/// Result of the custom action.
[CustomAction]
public static ActionResult PasswordGenerationAction(Session session)
{
Logger logger = new Logger(session);
logger.Log("Begin PasswordGenerationAction");
if (int.TryParse(GetPropertyValue(session, "GenPasswordLength"), out int passwordLength))
session["GENERATEDPASSWORD"] = PasswordGenerator.Default.GeneratePassword(passwordLength);
else
session["GENERATEDPASSWORD"] = PasswordGenerator.Default.GeneratePassword();
logger.Log("End PasswordGenerationAction");
return ActionResult.Success;
}
///
/// Tests a connection to a database server.
///
/// Session object containing data from the installer.
/// Result of the custom action.
[CustomAction]
public static ActionResult TestDatabaseConnectionAction(Session session)
{
Logger logger = new Logger(session);
logger.Log("Begin TestDatabaseConnectionAction");
// Get properties from the installer session
string connectionString = GetPropertyValue(session, "ConnectionString");
string dataProviderString = GetPropertyValue(session, "DataProviderString");
try
{
// Execute the database script
using (new AdoDataConnection(connectionString, dataProviderString))
{
}
session["DATABASECONNECTED"] = "yes";
}
catch (Exception ex)
{
// Log the error and return failure code
logger.Log(EventLogEntryType.Error, ex);
logger.Log(EventLogEntryType.Error, $"Connection string: {connectionString}{Environment.NewLine}Data provider string: {dataProviderString}");
session["DATABASECONNECTED"] = null;
}
logger.Log("End TestDatabaseConnectionAction");
return ActionResult.Success;
}
///
/// Custom action to execute a database script during installation.
///
/// Session object containing data from the installer.
/// Result of the custom action.
[CustomAction]
public static ActionResult DatabaseQueryAction(Session session)
{
Logger logger = new Logger(session);
string dllPath;
logger.Log("Begin DatabaseQueryAction");
// Get properties from the installer session
string connectionString = GetPropertyValue(session, "CONNECTIONSTRING");
string dataProviderString = GetPropertyValue(session, "DATAPROVIDERSTRING");
string query = GetPropertyValue(session, "DBQUERY");
try
{
dllPath = GetPropertyValue(session, "LIBPATH");
}
catch
{
dllPath = "";
}
try
{
// Load the DLL for a data connection if neccesary
if (!string.IsNullOrWhiteSpace(dllPath))
Assembly.Load(File.ReadAllBytes(dllPath));
// Execute the database script
using (AdoDataConnection connection = new AdoDataConnection(connectionString, dataProviderString))
{
connection.ExecuteNonQuery(query);
}
}
catch (Exception ex)
{
// Log the error and return failure code
string message = $"Failed to execute database query: {ex.Message}.";
logger.Log(InstallMessage.Error, message);
logger.Log(EventLogEntryType.Error, ex);
logger.Log(InstallMessage.Error,
$"Library Path: {dllPath}{Environment.NewLine}" +
$"Database Query: {query}{Environment.NewLine}" +
$"Connection string: {connectionString}{Environment.NewLine}" +
$"Data provider string: {dataProviderString}");
return ActionResult.Failure;
}
logger.Log("End DatabaseQueryAction");
return ActionResult.Success;
}
///
/// Custom action to execute a database script during installation.
///
/// Session object containing data from the installer.
/// Result of the custom action.
[CustomAction]
public static ActionResult DatabaseScriptAction(Session session)
{
Logger logger = new Logger(session);
logger.Log("Begin DatabaseScriptAction");
// Get properties from the installer session
string connectionString = GetPropertyValue(session, "CONNECTIONSTRING");
string dataProviderString = GetPropertyValue(session, "DATAPROVIDERSTRING");
string scriptPath = GetPropertyValue(session, "SCRIPTPATH");
try
{
// Execute the database script
using (AdoDataConnection connection = new AdoDataConnection(connectionString, dataProviderString))
{
connection.ExecuteScript(scriptPath);
}
}
catch (Exception ex)
{
// Log the error and return failure code
string message = $"Failed to execute database script: {ex.Message}.";
logger.Log(InstallMessage.Error, message);
logger.Log(EventLogEntryType.Error, ex);
logger.Log(InstallMessage.Error,
$"Database Script: {scriptPath}{Environment.NewLine}" +
$"Connection string: {connectionString}{Environment.NewLine}" +
$"Data provider string: {dataProviderString}");
return ActionResult.Failure;
}
logger.Log("End DatabaseScriptAction");
return ActionResult.Success;
}
///
/// Custom action to start a process.
///
/// Session object containing data from the installer.
/// Result of the custom action.
[CustomAction]
public static ActionResult StartProcessAction(Session session)
{
Logger logger = new Logger(session);
Dictionary infoLookup;
ProcessStartInfo info;
logger.Log("Begin StartProcessAction");
// Get properties from the installer session
string processStartInfo = GetPropertyValue(session, "ProcessStartInfo");
try
{
infoLookup = processStartInfo.ParseKeyValuePairs();
void findAndExecute(string key, Action action)
{
if (infoLookup.TryGetValue(key, out string value))
action(value);
}
info = new ProcessStartInfo();
findAndExecute("FileName", value => info.FileName = value);
findAndExecute("Arguments", value => info.Arguments = value);
findAndExecute("WorkingDirectory", value => info.WorkingDirectory = value);
findAndExecute("Verb", value => info.Verb = value);
findAndExecute("WindowStyle", value => info.WindowStyle = (ProcessWindowStyle)Enum.Parse(typeof(ProcessWindowStyle), value));
findAndExecute("UserName", value => info.UserName = value);
findAndExecute("Password", value => info.Password = value.ToSecureString());
findAndExecute("CreateNoWindow", value => info.CreateNoWindow = Convert.ToBoolean(value));
findAndExecute("LoadUserProfile", value => info.LoadUserProfile = Convert.ToBoolean(value));
findAndExecute("UseShellExecute", value => info.UseShellExecute = Convert.ToBoolean(value));
// Start the process
using (Process process = Process.Start(info))
{
if (process == null)
throw new Exception($"Failed to start process \"{info.FileName}\".");
findAndExecute("WaitForExit", value =>
{
// ReSharper disable AccessToDisposedClosure
process.OutputDataReceived += (sender, args) => logger.Log(args.Data);
process.ErrorDataReceived += (sender, args) =>
{
string message = $"Error in executing process: {args.Data}";
logger.Log(InstallMessage.Error, message);
logger.Log(EventLogEntryType.Error, message);
};
process.WaitForExit();
// ReSharper restore AccessToDisposedClosure
});
}
}
catch (Exception ex)
{
// Log the error and return failure code
string message = $"Failed to start process: {ex.Message}";
logger.Log(InstallMessage.Error, message);
logger.Log(EventLogEntryType.Error, ex);
return ActionResult.Failure;
}
logger.Log("End StartProcessAction");
return ActionResult.Success;
}
// Optionally port specification can include interface, e.g.: 127.0.0.1:8005 or +:8181,
// when only a port is specified, "+" is assumed meaning bind to all interfaces
private static string GetEndPoint(string servicePort) => servicePort.IndexOf(':') == -1 ? $"+:{servicePort}" : servicePort;
// Create an http namespace reservation
private static void AddHttpNamespaceReservation(string serviceAccount, string endPoint)
{
// Vista, Windows 2008, Window 7, etc use "netsh" for reservations
string parameters = $@"http add urlacl url=http://{endPoint}/ user=""{serviceAccount}""";
ProcessStartInfo psi = new ProcessStartInfo("netsh", parameters)
{
Verb = "runas",
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = false,
Arguments = parameters
};
using (Process shell = Process.Start(psi))
{
if (shell != null && !shell.WaitForExit(5000))
shell.Kill();
}
}
// Delete an http namespace reservation
private static void RemoveHttpNamespaceReservation(string endPoint)
{
// Vista, Windows 2008, Window 7, etc use "netsh" for reservations
string parameters = $@"http delete urlacl url=http://{endPoint}";
ProcessStartInfo psi = new ProcessStartInfo("netsh", parameters)
{
Verb = "runas",
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = false,
Arguments = parameters
};
using (Process shell = Process.Start(psi))
{
if (shell != null && !shell.WaitForExit(5000))
shell.Kill();
}
}
// Method to get the value of a property
private static string GetPropertyValue(Session session, string name)
{
if (session.CustomActionData.TryGetValue(name, out string value))
return value;
try
{
return !string.IsNullOrEmpty(session[name]) ? session[name] : session[name.ToUpper()];
}
catch (Exception ex)
{
Logger logger = new Logger(session);
logger.Log(EventLogEntryType.Warning, ex);
}
return null;
}
// Method to determine whether the custom action is immediate
private static bool IsImmediate(Session session)
{
return
!session.GetMode(InstallRunMode.Scheduled) &&
!session.GetMode(InstallRunMode.Rollback) &&
!session.GetMode(InstallRunMode.Commit);
}
private class Logger
{
private readonly Session m_session;
private readonly string m_serviceName;
private readonly bool m_isImmediate;
public Logger(Session session)
{
m_session = session;
m_serviceName = GetPropertyValue(session, "SERVICENAME");
m_isImmediate = IsImmediate(session);
}
public void Log(string message) => Log(InstallMessage.Info, message);
public void Log(InstallMessage messageType, string message)
{
using (Record record = new Record(0))
{
// Square brackets and curly braces are evaluated upon logging the message so we escape them here
record.FormatString = Regex.Replace(message, @"[\[\]{}]", @"[\$&]");
// session.Log and session.Message both make use of MsiProcessMessage, which cannot be used during a DoAction ControlEvent.
// https://msdn.microsoft.com/en-us/library/aa368322(VS.85).aspx
//
// Setting the value of a property generates a message in the log file so we use that as a workaround.
// However, you cannot set session properties unless the custom action is an immediate custom action.
if (m_session.Message(messageType, record) == MessageResult.None && m_isImmediate)
m_session["LOGMESSAGE"] = message;
}
}
public void Log(EventLogEntryType messageType, Exception ex) => Log(messageType, ex.ToString());
public void Log(EventLogEntryType messageType, string message)
{
try
{
EventLog.WriteEntry(m_serviceName, message, messageType);
}
catch (Exception ex)
{
Log(InstallMessage.Error, ex.ToString());
}
}
}
#region [ Interop ]
// Interop code below derived from Code Project article:
// http://www.codeproject.com/Articles/6164/A-ServiceInstaller-Extension-That-Enables-Recovery
// written by Neil Baliga
private const int SC_MANAGER_ALL_ACCESS = 0xF003F;
private const int SERVICE_ALL_ACCESS = 0xF01FF;
private const int SERVICE_CONFIG_FAILURE_ACTIONS = 0x2;
private const int SERVICE_CONFIG_FAILURE_ACTIONS_FLAG = 0x4;
// The worker method to set all the extension properties for the service
private static void UpdateServiceConfig(Session session, Logger logger)
{
// The failure actions to be defined for the service
List failureActionsList = new List
{
new WindowsApi.SC_ACTION { Type = (WindowsApi.SC_ACTION_TYPE)(uint)RecoverAction.Restart, Delay = 2000 },
new WindowsApi.SC_ACTION { Type = (uint)RecoverAction.None, Delay = 2000 }
};
// We've got work to do
IntPtr serviceManagerHandle = IntPtr.Zero;
IntPtr serviceHandle = IntPtr.Zero;
IntPtr serviceLockHandle = IntPtr.Zero;
IntPtr actionsPtr = IntPtr.Zero;
IntPtr failureActionsPtr = IntPtr.Zero;
// Name of the service
string serviceName = GetPropertyValue(session, "SERVICENAME");
// Place all our code in a try block
try
{
// Open the service control manager
serviceManagerHandle = WindowsApi.OpenSCManager(null, null, SC_MANAGER_ALL_ACCESS);
if (serviceManagerHandle.ToInt32() <= 0)
{
const string message = "UpdateServiceConfig: Failed to Open Service Control Manager";
logger.Log(InstallMessage.Error, message);
logger.Log(EventLogEntryType.Error, message);
return;
}
// Lock the Service Database
serviceLockHandle = WindowsApi.LockServiceDatabase(serviceManagerHandle);
if (serviceLockHandle.ToInt32() <= 0)
{
const string message = "UpdateServiceConfig: Failed to Lock Service Database for Write";
logger.Log(InstallMessage.Error, message);
logger.Log(EventLogEntryType.Error, message);
return;
}
// Open the service
serviceHandle = WindowsApi.OpenService(serviceManagerHandle, serviceName, SERVICE_ALL_ACCESS);
if (serviceHandle.ToInt32() <= 0)
{
const string message = "UpdateServiceConfig: Failed to Open Service";
logger.Log(InstallMessage.Error, message);
logger.Log(EventLogEntryType.Error, message);
return;
}
// Need to set service failure actions. Note that the API lets us set as many as
// we want, yet the Service Control Manager GUI only lets us see the first 3.
// Bill is aware of this and has promised no fixes. Also note that the API allows
// granularity of seconds whereas GUI only shows days and minutes.
// Calculate size of SC_ACTION structure
int scActionSize = Marshal.SizeOf(typeof(WindowsApi.SC_ACTION));
bool needShutdownPrivilege = false;
// Allocate memory for the array of individual actions
actionsPtr = Marshal.AllocHGlobal(scActionSize * failureActionsList.Count);
// Set up the restart actions array by copying in each failure action structure
for (int i = 0; i < failureActionsList.Count; i++)
{
// Handle pointer math as 64-bit, cast will convert back to 32-bit if needed
Marshal.StructureToPtr(failureActionsList[i], (IntPtr)((long)actionsPtr + i * scActionSize), false);
if (failureActionsList[i].Type == WindowsApi.SC_ACTION_TYPE.SC_ACTION_REBOOT)
needShutdownPrivilege = true;
}
// If we need shutdown privilege, then grant it to this process
bool result;
if (needShutdownPrivilege)
{
result = GrantShutdownPrivilege(logger);
if (!result)
return;
}
// Set up the failure actions
WindowsApi.SERVICE_FAILURE_ACTIONS failureActions = new WindowsApi.SERVICE_FAILURE_ACTIONS
{
cActions = failureActionsList.Count,
dwResetPeriod = 120,
lpCommand = null,
lpRebootMsg = null,
lpsaActions = actionsPtr
};
failureActionsPtr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(WindowsApi.SERVICE_FAILURE_ACTIONS)));
Marshal.StructureToPtr(failureActions, failureActionsPtr, false);
// Make the change
result = WindowsApi.ChangeServiceConfig2(serviceHandle, SERVICE_CONFIG_FAILURE_ACTIONS, failureActionsPtr);
// Check the return
if (!result)
{
int err = WindowsApi.GetLastError();
if (err == WindowsApi.ERROR_ACCESS_DENIED)
throw new Exception("UpdateServiceConfig: Access Denied while setting service failure actions");
}
logger.Log(EventLogEntryType.Information, "UpdateServiceConfig: Successfully configured service failure actions");
// Failure actions flag only applies on Vista / 2008 or better
if (Environment.OSVersion.Platform == PlatformID.Win32NT && Environment.OSVersion.Version.Major >= 6)
{
WindowsApi.SERVICE_FAILURE_ACTIONS_FLAG failureActionFlag = new WindowsApi.SERVICE_FAILURE_ACTIONS_FLAG();
failureActionFlag.bFailureAction = true;
result = WindowsApi.ChangeServiceConfig2(serviceHandle, SERVICE_CONFIG_FAILURE_ACTIONS_FLAG, ref failureActionFlag);
// Error setting description?
if (!result)
throw new Exception("UpdateServiceConfig: Failed to set failure actions flag");
}
}
// Catch all exceptions
catch (Exception ex)
{
logger.Log(InstallMessage.Error, ex.Message);
logger.Log(EventLogEntryType.Error, ex.Message);
}
finally
{
if (serviceManagerHandle != IntPtr.Zero)
{
// Unlock the service database
if (serviceLockHandle != IntPtr.Zero)
WindowsApi.UnlockServiceDatabase(serviceLockHandle);
// Close the service control manager handle
WindowsApi.CloseServiceHandle(serviceManagerHandle);
}
// Close the service handle
if (serviceHandle != IntPtr.Zero)
WindowsApi.CloseServiceHandle(serviceHandle);
// Free allocated memory
if (failureActionsPtr != IntPtr.Zero)
Marshal.FreeHGlobal(failureActionsPtr);
if (actionsPtr != IntPtr.Zero)
Marshal.FreeHGlobal(actionsPtr);
}
}
// This code mimics the MSDN defined way to adjust privilege for shutdown
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa376871(v=vs.85).aspx
private static bool GrantShutdownPrivilege(Logger logger)
{
bool grantSuccess = false;
IntPtr processToken = IntPtr.Zero;
WindowsApi.TOKEN_PRIVILEGES tokenPrivileges = new WindowsApi.TOKEN_PRIVILEGES();
long luid = 0;
int returnLen = 0;
try
{
IntPtr processHandle = WindowsApi.GetCurrentProcess();
bool result = WindowsApi.OpenProcessToken(processHandle, WindowsApi.TOKEN_ADJUST_PRIVILEGES | WindowsApi.TOKEN_QUERY, ref processToken);
if (!result)
return false;
WindowsApi.LookupPrivilegeValue(null, WindowsApi.SE_SHUTDOWN_NAME, ref luid);
tokenPrivileges.PrivilegeCount = 1;
tokenPrivileges.Privileges.Luid = luid;
tokenPrivileges.Privileges.Attributes = WindowsApi.SE_PRIVILEGE_ENABLED;
WindowsApi.AdjustTokenPrivileges(processToken, false, ref tokenPrivileges, 0, IntPtr.Zero, ref returnLen);
if (WindowsApi.GetLastError() != 0)
throw new Exception("Failed to grant shutdown privilege");
grantSuccess = true;
}
catch (Exception ex)
{
string message = "GrantShutdownPrivilege: " + ex.Message;
logger.Log(InstallMessage.Error, message);
logger.Log(EventLogEntryType.Error, ex);
}
finally
{
if (processToken != IntPtr.Zero)
WindowsApi.CloseHandle(processToken);
}
return grantSuccess;
}
private static void AddPrivileges(string account, string privilege)
{
// Pointer and size for the SID
IntPtr sid = IntPtr.Zero;
int sidSize = 0;
// StringBuilder and size for the domain name
StringBuilder domainName = new StringBuilder();
int nameSize = 0;
// Account-type variable for lookup
int accountType = 0;
// Get required buffer size
WindowsApi.LookupAccountName(string.Empty, account, sid, ref sidSize, domainName, ref nameSize, ref accountType);
// Allocate buffers
domainName = new StringBuilder(nameSize);
sid = Marshal.AllocHGlobal(sidSize);
// Look up SID for the account
if (WindowsApi.LookupAccountName(string.Empty, account, sid, ref sidSize, domainName, ref nameSize, ref accountType))
{
// Initialize an empty Unicode-string
WindowsApi.LSA_UNICODE_STRING systemName = new WindowsApi.LSA_UNICODE_STRING();
// These attributes are not used, but LsaOpenPolicy wants them to exist
WindowsApi.LSA_OBJECT_ATTRIBUTES objectAttributes = new WindowsApi.LSA_OBJECT_ATTRIBUTES();
// Get a policy handle
uint result = WindowsApi.LsaOpenPolicy(ref systemName, ref objectAttributes, (int)WindowsApi.LsaAccess.POLICY_ALL_ACCESS, out IntPtr policyHandle);
if (result == 0)
{
// Initialize a Unicode-string for the privilege name
WindowsApi.LSA_UNICODE_STRING[] userRights = new WindowsApi.LSA_UNICODE_STRING[1];
userRights[0] = new WindowsApi.LSA_UNICODE_STRING();
userRights[0].Buffer = Marshal.StringToHGlobalUni(privilege);
userRights[0].Length = (ushort)(privilege.Length * UnicodeEncoding.CharSize);
userRights[0].MaximumLength = (ushort)((privilege.Length + 1) * UnicodeEncoding.CharSize);
// Add the privilege to the account
WindowsApi.LsaAddAccountRights(policyHandle, sid, userRights, 1);
// Close LSA policy handle
WindowsApi.LsaClose(policyHandle);
}
// Free SID
WindowsApi.FreeSid(sid);
}
}
#endregion
}
}