Instalace ASP.NET aplikace probíhá obvykle stylem "nakopíruj tohle do rootu webu a modli se". Součástí aplikace ale často bývají různé pomocné utilitky, které při své instalaci a odinstalaci vyžadují různé úkony. Například vložení do GAC, instalaci služby (Windows service), vytvoření protoklu událostí (vlastní event log a nebo alespoň event log source). Microsoft .NET framework na to má infrastrukturu v podobě instalačních tříd: užitečného, leč často přehlíženého nástroje.
Hned na úvod je nutné upozornit na jednu důležitou věc: v tomto článku nebude řeč o vytváření instalačních balíčků, setup projektech, MSI a podobných věcech. Zaměříme se na specifické činnost, které konkrétní assemblies potřebují k životu.Ty lze vyvolat buďto ručně, pomocí utility InstallUtil.exe, která je součástí .NET Frameworku, nebo v rámci "velké" instalace, jako jsou např. různé formy setup projektů ve Visual Studiu.
Jak funguje InstallUtil.exe
Utilitka InstallUtil.exe je součástí .NET Frameworku. Slouží k instalaci a odinstalaci assembly, přičemž záleží pouze na assembly samotné, jaké konkrétní úkony bude instalace a odinstalace zahrnovat. Jedná se o konzolový program, který se spouští z příkazové řádky a ovládá se pomocí parametrů. Jeho nejtypičtější použití nicméně je pouze s názvem assembly, pro instalaci:
InstallUtil.exe C:\Cesta\k\assembly.dll
Odinstalace se pak provádí přidáním parametru /u:
InstallUtil.exe /u C:\Cesta\k\assembly.dll
Assembly přitom může být jak spustitelný program (.exe), tak cokoliv jiného, třeba class library (.dll). Toho jsem využil například ve svém modulu pro obcházení AES chyby v ASP.NET, o kterém jsem psal před pár dny. Je potřeba ho nainstalovat, konkrétně vložit tuto assembly do GAC a zaregistrovat HTTP modul na úrovni serveru.
Vytvářet pro tak jednoduchou aplikaci kompletní setup by byl poněkud overkill. Ruční instalace by byla zase dost komplikovaná, zejména pak proto, že GacUtil.exe, používaná pro ruční instalaci assembly do GAC, není součástí .NET Frameworku ale jen SDK, takže na serveru patrně nebude nainstalovaná. Proto jsem použil instalátory a InstallUtil.exe.
InstallUtil funguje tak, že v dané assembly najde všechy instalační třídy, označené atributem [RunInstaller(true)] a v nich spustí metodu Install, potažmo Uninstall.
Instalační třídy
Pro potřeby instalace se používají instalační třídy (installer classes). Ve své podstatě jde o běžnou třídu, která je poděděná od System.Configuration.Install.Installer. V ní přepíšeme metody Install a Uninstall a do nich doplníme logiku, kterou naše instalace vyžaduje.
Můžeme vytvořit například třídu AssemblyGacInstaller, která vloží aktuální assembly do Global Assembly Cache (a v případě odinstalace ji zase odstraní). Je velmi jednoduchá, nemá žádné parametry, a vypadá takto:
using System;
using System.Collections;
using System.Configuration.Install;
namespace Altairis.Web.OracleBugFix.Install {
public partial class AssemblyGacInstaller : Installer {
// Installer methods
public override void Install(IDictionary stateSaver) {
// Base install task
base.Install(stateSaver);
// Install current assembly to GAC
try {
Console.Write("Installing assembly to GAC...");
var mya = System.Reflection.Assembly.GetExecutingAssembly();
var p = new System.EnterpriseServices.Internal.Publish();
p.GacInstall(mya.Location);
Console.WriteLine("OK");
}
catch (Exception ex) {
Console.WriteLine("Failed!");
Console.WriteLine(ex.ToString());
}
}
public override void Uninstall(IDictionary savedState) {
// Base uninstall task
base.Uninstall(savedState);
// Remove current assembly from GAC
try {
Console.Write("Removing assembly from GAC...");
var mya = System.Reflection.Assembly.GetExecutingAssembly();
var p = new System.EnterpriseServices.Internal.Publish();
p.GacRemove(mya.Location);
Console.WriteLine("OK");
}
catch (Exception ex) {
Console.WriteLine("Failed!");
Console.WriteLine(ex.ToString());
}
}
}
}
InstallState
Instalátor může být i podstatně sofistikovanější. Často vyžaduje mezi instalací a odinstalací uchovávání nějakých informací, potřebných pro uvedení počítače do původního stavu. Takové informace si v metodě Install můžete ukládat do kolekce stateSaver. Instalační utilita je potom serializuje do souboru, který se standardně jmenuje JménoAssembly.InstallState. V případě odinstalace se pak informace deserializují a dostanete je v rámci parametru savedState metody Uninstall.
Následující instalační třída slouží k registraci HTTP modulu na úrovni celého web serveru. Do install state si uloží jméno, pod kterým byl modul do konfigurace vložen:
using System;
using System.Collections;
using Microsoft.Web.Administration;
namespace Altairis.Web.OracleBugFix.Install {
public partial class HttpModuleInstaller : System.Configuration.Install.Installer {
// Configuration properties
public string ModuleName { get; set; }
public Type ModuleType { get; set; }
// Installer methods
public override void Install(IDictionary stateSaver) {
// Base install tasks
base.Install(stateSaver);
// Validate properties
if (string.IsNullOrEmpty(this.ModuleName)) throw new ArgumentException("ModuleName is null or empty.");
if (this.ModuleType == null) throw new ArgumentNullException("ModuleType");
try {
// Install module to IIS
Console.Write("Loading server configuration...");
using (ServerManager serverManager = new ServerManager()) {
Configuration config = serverManager.GetApplicationHostConfiguration();
ConfigurationSection modulesSection = config.GetSection("system.webServer/modules");
ConfigurationElementCollection modulesCollection = modulesSection.GetCollection();
Console.WriteLine("OK");
Console.Write("Installing module...");
ConfigurationElement addElement = modulesCollection.CreateElement("add");
addElement["name"] = this.ModuleName;
addElement["type"] = this.ModuleType.AssemblyQualifiedName;
modulesCollection.Add(addElement);
Console.WriteLine("OK");
Console.Write("Saving changes...");
serverManager.CommitChanges();
Console.WriteLine("OK");
}
// Add module name to install state
stateSaver.Add("ModuleName", this.ModuleName);
}
catch (Exception ex) {
Console.WriteLine("Failed!");
Console.WriteLine(ex.ToString());
}
}
public override void Uninstall(IDictionary savedState) {
// Base uninstall task
base.Uninstall(savedState);
// Load installed module name
if (savedState == null) throw new ArgumentNullException("savedState");
var moduleName = savedState["ModuleName"] as string;
if (string.IsNullOrEmpty(moduleName)) throw new ArgumentException("Module name not available in saved state.");
try {
Console.Write("Loading server configuration...");
using (ServerManager serverManager = new ServerManager()) {
Configuration config = serverManager.GetApplicationHostConfiguration();
ConfigurationSection modulesSection = config.GetSection("system.webServer/modules");
ConfigurationElementCollection modulesCollection = modulesSection.GetCollection();
Console.WriteLine("OK");
Console.Write("Uninstalling module...");
ConfigurationElement element = FindElement(modulesCollection, "add", "name", moduleName);
if (element == null) {
Console.WriteLine("Not found - no work to do");
return;
}
modulesCollection.Remove(element);
Console.WriteLine("OK");
Console.Write("Saving changes...");
serverManager.CommitChanges();
Console.WriteLine("OK");
}
}
catch (Exception ex) {
Console.WriteLine("Failed!");
Console.WriteLine(ex.ToString());
}
}
// Helper methods
private static ConfigurationElement FindElement(ConfigurationElementCollection collection, string elementTagName, params string[] keyValues) {
foreach (ConfigurationElement element in collection) {
if (String.Equals(element.ElementTagName, elementTagName, StringComparison.OrdinalIgnoreCase)) {
bool matches = true;
for (int i = 0; i < keyValues.Length; i += 2) {
object o = element.GetAttributeValue(keyValues[i]);
string value = null;
if (o != null) {
value = o.ToString();
}
if (!String.Equals(value, keyValues[i + 1], StringComparison.OrdinalIgnoreCase)) {
matches = false;
break;
}
}
if (matches) {
return element;
}
}
}
return null;
}
}
}
Vestavěné instalátory
Výše uvedené instalátory jsou vcelku univerzální a můžete je snadno níže popsaným způsobem použít ve svých aplikacích. Součástí .NET Frameworku jsou již některé hotové instalátory:
- System.Diagnostics.EventLogInstaller – vytváří nový protokol událostí (event log), případně nový zdroj (source) pro existující log.
- System.Diagnostics.PerformanceCounterInstaller – vytváří performance countery (pro sledování výkonu a vytížení serveru)
- System.Messaging.MessageQueueInstaller – vytváří novou frontu v rámci Message Queuing Sevice
- System.ServiceProcess.ServiceInstaller – slouží k instalaci služby (Windows Service)
- System.ServiceProcess.ServiceProcessInstaller – slouží k instalaci procesu, který může hostovat několik služeb
Spouštění instalátorů
InstallUtil.exe spustí jenom ty instalátory, jejichž třídy jsou označené atributem [RunInstaller(true)]. Pokud byste ve své aplikaci měli jenom instalátory vlastní výroby, s parametry napsanými přímo v kódu, mohli byste je prostě označit všechny. V praxi to není příliš dobrý nápad, už jenom proto, že činí znovupoužitelnost instalačních tříd dost komplikovanou.
Instalační třídy mohou fungovat hierarchicky. Třída Installer má kolekci Installers, která může obsahovat další instalátory, které se spustí, je-li spuštěn jejich mateřský. V praxi se tedy obvykle postupuje tak, že si v aplikaci vytvoříte jednu třídu, obvykle ji nazývám ApplicationInstaller a v jejím konstruktoru naplníte kolekci Installers všemi dalšími instalátory, které chcete spustit, přičemž jim při této příležitosti také předáte potřebné parametry. Pouze tuto jedinou třídu pak označíte jako [RunInstaller(true)].
Následující kód ukazuje takovou třídu, která aktivuje oba dva výše popsané instalátory a jeden vestavěný, pro vytvoření vlastního event logu:
using System.ComponentModel;
namespace Altairis.Web.OracleBugFix.Install {
[RunInstaller(true)]
public class ApplicationInstaller : System.Configuration.Install.Installer {
public ApplicationInstaller() {
// Add AssemblyGacInstaller
this.Installers.Add(new AssemblyGacInstaller());
// Add HttpModuleInstaller
this.Installers.Add(new HttpModuleInstaller {
ModuleName = "AltairisOracleBugFixErrorHandlingModule",
ModuleType = typeof(ErrorHandlingModule),
});
// Add built-in event log installer
this.Installers.Add(new System.Diagnostics.EventLogInstaller {
Log = "MujLog",
Source = "MujSource"
});
}
}
}