Press "Enter" to skip to content

1 – MiniSqlAgent – Creare un Servizio Windows installabile con InnoSetup

In questo post spiegheremo come creare un Servizio Windows con C# e come implementare il necessario alla sua installazione tramite InnoSetup.

Installer e Visual Studio

L’implementazione del Setup di applicazioni Windows con .NET era abbastanza semplice con Visual Studio 2005, c’era il template per il Setup Wizard che creava un MSI ed un Setup.exe, imparando qualche nozione su come decidere quali erano i progetti da referenziare e come costruire shortcut e aggiungere dialog al wizard si poteva costruire un setup semplice e funzionale.

Da Visual Studio 2008 c’è stato un peggioramento nella funzionalitá perché se nei nostri progetti non veniva gestito correttamente il Versioning dei Files, poteva accadere che un setup non aggiornava correttamente le DLL o l’EXE e quindi gli aggiornamenti potevano non andare a buon fine. Pertanto ci siamo dovuti evolvere creando delle utility di aggiornamento versioni che quando facciamo un rilascio modificassero la versione del file di progetto in modo che il setup funzionasse correttamente.

Tutto è andato liscio fino a Visual Studio 2012, quando per ragioni certamente sensate anche se sconosciute, Microsoft ha deciso di non supportare piú i progetti di Setup standard ma di aggiungere la versione community di Installshield come supporto alla creazione di programmi di installazione. Non ci sono problemi, basta conservare una copia del Visual Studio 2010 o precedente da qualche parte ed utilizzarlo per compilare i progetti di setup. Ma magari è opportuno evolversi verso qualcosa di nuovo.

Nel mio caso, ho fatto un po’ di ricerche in rete ed ho verificato che lo strumento free piú usato è InnoSetup. E’ un sistema collaudato e funzionale, con molta documentazione in giro e con molti esempi. Ha come pro la sua flessibilitá, il fatto di essere governato da script in formato testo, e quello di essere chiamabile da riga di comando, ha come contro l’assenza di uno strumento di integrazione in visual studio che permetta di far creare automaticamente al sistema le dipendenze di una applicazione. Ma è uno scoglio superabile facilmente.

Creare un setup per Innosetup è semplice per una applicazione Winforms o Wpf, ma nel caso dei Servizi Windows di .Net, il Setup di Windows aveva un meccanismo che aggiungeva il servizio a quelli disponibili sul PC in modo automatico, bastava che il programmatore implementasse una classe Installer opportunamente configurata. Innosetup non è in grado di farlo in automatico, pertanto in questo post creeremo un servizio windows (per ora dedicato a scrivere su un log delle frasi in base ad un timer). Creeremo la classe Installer cosí come si faceva in precedenza e vedremo come fare in modo che Innosetup sia in grado di utilizzarla per configurare il nostro servizio.

Il progetto del servizio

msa_svc_01

Creiamo una nuova soluzione in Visual Studio 2010 e scegliamo come Template  Windows Service, lo chiamiamo MiniSqlAgent e aggiungiamo alla soluzione un ulteriore progetto di tipo Class Library che chiamiamo MniSqlAgentServiceLayer. Questo secondo progetto ospiterá tutte le funzionalitá del servizio, mentre come vedremo, il servizio conterrá solamente l’ossatura dell’applicazione.

Il nome del servizio mostra l’ambizione di arrivo di cui questo articolo è un primo passo, ma in questo preciso contesto, quello che l’applicazione fará sará semplicemente scrivere delle stringhe su un file di testo ad intervalli di tempo prestabiliti.

msa_svc_02

Nell’immagine qui sopra vediamo come abbiamo chiamato l’ Exe risultante e il Namespace assegnato, oltre all’icona che abbiamo personalizzato per questa applicazione.

Abbiamo inoltre modificato il platform target dell’applicazione a x86 (32 bit) perché lavorando su una macchina a 64 bit, non è possibile usare l’edit e continue in Debug e in seguito potrebbe tornarci utile.

msa_svc_03

Vediamo qui sopra i settaggi applicativi per la libreria di classi collegata al servizio.

msa_svc_04

Nella libreria Service Layer andremo a definire due classi, il ServiceContext, che, se ricordate i post dedicati al contesto, sará una classe comune a tutte le DLL che formeranno il nostro servizio e fornirá a ciascuna i dati di Contesto applicativo, e il JobManager, la classe che svolgerá il lavoro per cui il servizio è stato costruito.

La classe ServiceContext

public class ServiceContext : IBusinessContext
{
	private static volatile ServiceContext mInstance;
	private static object syncRoot = new Object();

Se ricordate i post dedicati alla discussione del Contesto applicativo abbiamo creato un’interfaccia IBusinessContext per identificare le classi che fanno questo servizio per le applicazioni, pertanto implementiamo l’interfaccia all’interno della nostra classe. Definiamo poi un paio di variabili che forniranno contesto al servizio.

La classe ServiceContext sará una classe Singleton e al suo interno gestiremo i controlli necessari a renderla Thread Safe (faremo in modo che anche lavorando in multithreading non piú di un thread modifichi i dati comuni contemporaneamente.)

public static ServiceContext Instance
{
	get
	{
		if (mInstance == null)
		{
			lock (syncRoot)
			{
				if (mInstance == null)
					mInstance = new ServiceContext();
			}
		}
		return mInstance;
	}
}

L’implementazione del Singleton, i dati richiesti al contesto saranno sempre forniti utilizzando la property statica Instance.

public string DataFolder
{
	get
	{
		if (mDataFolder == null)
		{
			mDataFolder = Path.Combine(Environment.GetFolderPath(
				Environment.SpecialFolder.CommonDocuments), "MiniSqlAgent");
		}
		return mDataFolder;
	}
}

La prima informazione di contesto che utilizzeremo nel nostro servizio in bozza, il nome della cartella in cui andremo a memorizzare il Log del servizio e dove andremo a recuperare l’informazione relativa alla Verbositá del Log stesso.

private int mCheckInterval = 60;

public int CheckInterval
{
	get
	{
		return mCheckInterval;
	}
	set
	{
		mCheckInterval = value;
	}
}

Un’altra informazione di contesto per il nostro servizio, per ora la imposteremo da codice, poi sará uno dei parametri di configurazione del servizio, indica l’intervallo in secondi ogni quanto il servizio esegue la sua funzione primaria.

public LogType LogVerbosity
{
	get
	{
		return mLogVerbosity;
	}
	set
	{
		mLogVerbosity = value;
	}
}

L’informazione relativa al tipo di verbositá che vogliamo per il log, questa informazione la gestiremo in modo primitivo utilizzando un file di configurazione per fare un test del nostro log di servizio, in seguito raffineremo il suo uso.

public string ContextName
{
	get
	{
		return "MiniSqlAgent";
	}
}

public string ContextType
{
	get
	{
		return "SVC";
	}
}

Due informazioni sul contesto, il suo nome ed il suo tipo, che avevamo inserito nell’interfaccia di base per i contesti.

La classe JobManager

La struttura di questa classe fornisce lo scheletro per un Servizio che effettua un polling temporizzato, ovvero testa ad intervalli prestabiliti qualcosa ed in base a dei parametri esegue una operazione. Nel caso del nostro servizio, l’operazione eseguita è scrivere sul log qualcosa ogni sessanta secondi.

Il Loop primario del nostro servizio viene instanziato su un thread diverso dal thread principale dell’applicazione, utilizziamo poi un WaintHandle, ovvero un Threading Timer per stabilire il ciclo di Loop del servizio. Il timer di Thread viene fermato, facendo terminare il servizio, con l’uso di un AutoResetEvent, ovvero un “flag” che ci permette di bloccare il loop del timer quando necessario.

Se la domanda che vi ponete è Perché non hai usato un Timer, la risposta è una domanda: perché usare un Timer quando gli sviluppatori del Framework hanno messo a disposizione qualcosa di molto piú efficiente?
Inoltre, questo è un tutorial ed è opportuno introdurre anche degli elementi che vanno oltre il Thread.Sleep per fornire accessori alla canna da pesca dei Dotnetworkers.

Vediamo nel dettaglio il codice:

public class JobManager
{
	[Category("Control"), Description("Exit application")]
	public event EventHandler ApplicationExitRequested;
	
	AutoResetEvent mEvQuit = new AutoResetEvent(false);
	
	DateTime mAbortTimer = DateTime.MinValue;
	
	private Thread mProcessJobsMainThread;

Le variabili member della classe Manager del servizio:

Abbiamo un Evento ApplicationExitRequested che verrá scatenato nel caso di problemi a fermare il servizio, tale evento sarà gestito come fallback nel caso lo stop del servizio per via normale non funzioni correttamente.
L’ AutoResetEvent, questo è l’oggetto che ferma il Threading Timer, viene inizializzato con il valore false, quando vorremo fermare il timer, modificheremo il suo stato a true e fermeremo il loop.
La variabile mAbortTimer  è un oggetto di sicurezza, se alla richiesta di chiusura del Loop di lavoro e uscita dal servizio l’applicazione non risponde entro un tempo ragionevole, il valore di questa variabile ci servirá a provocare un Thread.Kill.
Ed infine, mProcessJobsMainThread la variabile che conterrá il Thread Principale di servizio.

public void StartThreading()
{
	try
	{

		string message = "Mini Sql Agent Start Main Thread";
		EventLogger.SendMsg(message, MessageType.Info);

		mProcessJobsMainThread = new Thread(new ThreadStart(this.ThreadingLoop));
		mProcessJobsMainThread.Name = "MSAMainThread";
		mProcessJobsMainThread.Start();
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
	}
}

Il metodo Start, istanzia il thread di servizio e lo esegue.

public void StopThreading()
{
	try
	{
		//Disable the threading timer to stop the loop
		mEvQuit.Set();

		if (mProcessJobsMainThread != null)
		{
			mAbortTimer = DateTime.Now.AddMinutes(1);
			//Wait for the thread executing to stop if it does not end in a fashionable time the thread is aborted
			while (mProcessJobsMainThread.ThreadState != System.Threading.ThreadState.Stopped)
			{
				Thread.Sleep(10000);
				if (DateTime.Now > mAbortTimer)
				{
					mProcessJobsMainThread.Abort();
				}
			}
		}

	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		ExitApp();
	}
}

Il metodo di Stop del servizio, quando viene richiesto di fermare il servizio per prima cosa viene settato il Flag di stop del Timer di loop, poi viene fatto un ciclo di attesa, mentre il Thread eventualmente in corso viene terminato ed esce. Se il thread in corso non termina entro un minuto, viene lanciato un Abort del thread.

private void ThreadingLoop()
{
	try
	{
		WaitHandle[] evArray = new WaitHandle[] { mEvQuit };

		EventLogger.SendMsg("Mini Sql Agent Start Job Processing", MessageType.Info);

		while (WaitHandle.WaitAny(evArray, ServiceContext.Instance.CheckInterval * 1000, false) != 0)
		{

			ExecJob();

		}

		EventLogger.SendMsg("Mini Sql Agent End Job Processing", MessageType.Info);
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
	}
}

Il loop di servizio, utilizziamo un WaitHandle, ovvero un timer della sezione di Threading di .Net, il timer ogni sessanta secondi effettua un ciclo del nostro while. Abbiamo inserito il Flag di stop nei parametri forniti al Timer, in questo caso il flag è uno solo, ma in applicazioni piú complesse, potrebbero esserci piú eventi che scatenano la chiusura di un Loop, ecco perché l’array di WaitHandle invece del singolo elemento.

public void ExecJob()
{
	try
	{
		EventLogger.SendMsg("Job Executed (info message )", MessageType.Info);
		Thread.Sleep(1000);
		EventLogger.SendMsg("Job Executed (warning message )", MessageType.Warning);
		Thread.Sleep(1000);
		EventLogger.SendMsg("Job Executed (error message )", MessageType.Error);
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
	}
}

Il metodo che fisicamente esegue il lavoro richiesto dal servizio, per ora semplicemente un test di scrittura sul log, in seguito qui dentro metteremo del codice molto piú interessante.

La classe del servizio MiniSqlAgentService

Questa classe ospita il Servizio Windows Vero e proprio, potrete notare nella sua stesura che  si occupa solo delle operazioni fondamentali, ovvero lo Start, lo Stop e il Log.
Perché il manager di servizio lo abbiamo implementato in un altra classe? Non solo, perché addirittura in un’altra libreria?

In questo caso non si tratta di necessitá funzionali ma di regole di business arbitrarie, ovvero si tratta del mio modo di creare applicazioni di servizio. Io ho deciso che l’implementazione del servizio dovesse essere su una classe esterna alla classe del servizio stesso, Io ho deciso che questa classe fosse su una libreria diversa. Non è un obbligo ma un pattern, ovviamente vi devo dare delle giustificazioni.

  • Debuggare un servizio è complicato, bisogna usare il remote debugger e una serie di funzionalitá ed accorgimenti non banali.
  • Debuggare una Dll è piú facile, basta simulare lo start e lo stop del servizio usando una applicazione WPF o Windows Forms. Quando il Job Manager dovesse fare cose piú complesse che scrivere un log, probabilmente sará molto piú facile testarlo in varie situazioni.
  • Avere il gestore del servizio su una classe esterna alla libreria del servizio stesso ci permette anche di poter creare una console con interfaccia utente che permette di far funzionare quello che fa il servizio in modalitá interattiva con l’utente, non è necessario per tutti i servizi è vero, ma visto il nome del nostro servizio, una modalitá interattiva sono sicura ci tornerá utile.
public partial class MiniSqlAgentService : ServiceBase
{
	private const string LOGFILE = "MSA.log";
	
	private JobManager mJobManager;
	
	private readonly static string mLogTraceName =  Path.Combine(ServiceContext.Instance.DataFolder, LOGFILE);
	public MiniSqlAgentService()
	{
		InitializeComponent();
	
	}

La definizione delle variabili di classe, in questo caso abbiamo due oggetti, il Manager e una variabile con il nome del file di log, il costruttore chiama un InitializeComponent, perché I servizi Windows derivano da Component. Questo significa che l’oggetto MiniSqlAgentService ha anche un “designer” ed ha una serie di property che possono essere inizializzate a Design Time. Una sola di queste è fondamentale per il nostro lavoro.

msa_svc_05

La property piú importante per il nostro servizio è ServiceName, perché con questo nome il servizio sará installato nel sistema, con questo nome potremo fare lo Start e lo Stop del servizio dal Service Manager di Windows.

protected override void OnStart(string[] args)
{
	try
	{
#if DEBUG
		//Se sono in debug elimino il log
		if (File.Exists(mLogTraceName))
		{
			File.Delete(mLogTraceName);
		}
#endif
		//Set the trace file for logging purpouses
		System.IO.FileStream logTracer = new System.IO.FileStream(mLogTraceName, System.IO.FileMode.OpenOrCreate);
		// Creates the new trace listener.
		System.Diagnostics.TextWriterTraceListener logListener = new System.Diagnostics.TextWriterTraceListener(logTracer);
		Trace.Listeners.Add(logListener);

		EventLogger.LogNewEntry += new SendMessageEventHandler(EventLogger_LogNewEntry);
		//EventLogger.Verbosity = ForYou.Base.Logger.LogVerbosity.OnlyErrors;

		mJobManager = new JobManager();
		mJobManager.ApplicationExitRequested += new EventHandler(JobManager_ApplicationExitRequested);
		mJobManager.StartThreading();

		EventLogger.SendMsg("Start the Mini Sql Agent Service", MessageType.Info);
	}
	catch (Exception ex)
	{
		string msg = ex.XDwBuildExceptionMessage();
		Trace.WriteLine(msg);
		Trace.Flush();
	}
}

L’evento di Start del servizio, viene scatenato quando il servizio parte, quello che facciamo all’interno di questo metodo è predisporre quello che ci serve per attivare il Log e poi facciamo partire il Loop del Manager.

protected override void OnStop()
{
	try
	{
		if (this.mJobManager != null)
		{
			this.mJobManager.StopThreading();
		}
		EventLogger.SendMsg("Stop the Mini Sql Agent Service", MessageType.Info);
	}
	catch (Exception ex)
	{
		string msg = ex.XDwBuildExceptionMessage();
		Trace.WriteLine(msg);
		Trace.Flush();
	}
}

L’evento di stop del servizio, viene scatenato quando viene richiesto lo stop dal service manager di Windows, questo evento chiede al manager di uscire dal Loop.

void JobManager_ApplicationExitRequested(object sender, EventArgs e)
{
	new Thread(new ThreadStart(StopService)).Start();
}

private void StopService()
{
	try
	{
		ServiceController sc = new ServiceController(this.ServiceName);
		sc.WaitForStatus(ServiceControllerStatus.Running, new TimeSpan(0, 5, 0));
		Stop();
	}
	catch (Exception)
	{
		//If there is an error stopping the service which is something that
		//should not happen then the application commit suicide 😀
		System.Diagnostics.Process myself =
			System.Diagnostics.Process.GetCurrentProcess();
		myself.Kill(); //Suicide
	}
}

Il codice qui sopra è stato predisposto solo per ragioni di sicurezza, viene eseguito solo se qualcosa va storto nell’esecuzione dello Stop del Manager richiesta dall’evento OnStop. In questo caso, viene forzata una richiesta di Stop al manager del servizio stesso e se anche in questa richiesta si verifica un errore viene effettuato un suicidio, ovvero la richiesta di uccisione del thread principale.

void EventLogger_LogNewEntry(object sender, SendMessageEventArgs e)
{
	if (e.MessageType == MessageType.Error)
	{
		WriteLog(e);
	}
	else
	{
		if (ServiceContext.Instance.LogVerbosity == LogType.AllMessages)
		{
			WriteLog(e);
		}
		else
		{
			if (ServiceContext.Instance.LogVerbosity == LogType.WarningsAndExceptions)
			{
				if (e.MessageType == MessageType.Warning)
				{
					WriteLog(e);
				}
			}
		}
	}
}

private static void WriteLog(SendMessageEventArgs e)
{
	if (e.MessageType == MessageType.Error)
	{
		Trace.WriteLine("*** ERROR! ***");
	}
	if (e.MessageType == MessageType.Warning)
	{
		Trace.WriteLine("+++ WARNING! +++");
	}
	Trace.WriteLine(string.Format("{0} {1}: {2}", DateTime.Now.ToShortDateString(), DateTime.Now.ToLongTimeString(), e.MessageToLog));
	Trace.Flush();
}

La gestione del log degli eventi, che scrive utilizzando un Trace Listener sul file di log applicativo.

Il nostro servizio è completato ed ha tutto quello che serve per poter funzionare, peró per poterlo installare e collaudare ci servono ancora alcune cose.

La classe MiniSqlAgentInstaller

Questa classe, derivata da System.Configuration.Install.Installer è quella che svolge il compito di Installare o Disinstallare il servizio in modo che sia utilizzabile da Windows quando eseguiamo il Setup,

[RunInstaller(true)]
public partial class MiniSqlAgentInstaller : System.Configuration.Install.Installer
{
	private System.ServiceProcess.ServiceProcessInstaller MiniSqlAgentServiceProcessInstaller;
	private System.ServiceProcess.ServiceInstaller MiniSqlAgentServiceInstaller;


	/// <summary>
	/// Initializes a new instance of the <see cref="MiniSqlAgentInstaller"/> class.
	/// </summary>
	public MiniSqlAgentInstaller()
	{
		InitializeComponent();
		this.MiniSqlAgentServiceProcessInstaller = new System.ServiceProcess.ServiceProcessInstaller();
		this.MiniSqlAgentServiceInstaller = new System.ServiceProcess.ServiceInstaller();
		// 
		// MiniSqlAgentServiceProcessInstaller
		// 
		this.MiniSqlAgentServiceProcessInstaller.Account = System.ServiceProcess.ServiceAccount.LocalSystem;
		this.MiniSqlAgentServiceProcessInstaller.Password = null;
		this.MiniSqlAgentServiceProcessInstaller.Username = null;
		// 
		// MiniSqlAgentServiceInstaller
		// 
		this.MiniSqlAgentServiceInstaller.Description = "Dotnetwork.it MiniSqlAgent";
		this.MiniSqlAgentServiceInstaller.DisplayName = "MiniSqlAgent";
		this.MiniSqlAgentServiceInstaller.ServiceName = "MiniSqlAgent";
		//this.MiniSqlAgentServiceInstaller.ServicesDependedOn = new string[] {
		//    "lanmanserver",
		//    "lanmanworkstation", "mssqlserver"};
		// 
		// MiniSqlAgentInstaller
		// 
		this.Installers.AddRange(new System.Configuration.Install.Installer[] {
		this.MiniSqlAgentServiceProcessInstaller,
		this.MiniSqlAgentServiceInstaller});
	}

}

La classe installer come possiamo vedere è un oggetto molto semplice, ne modifichiamo solo il costruttore in quanto i metodi Install, e Commit della classe padre che possono essere modificati con un override fanno giá quanto a noi serve.

Che cosa inizializziamo nel costruttre della classe:

  • Due classi, un ServiceProcessInstaller ed un ServiceInstaller di cui inizializziamo alcune proprietá.
  • Account: indica con quale account il servizio partirá per default, nel nostro caso usiamo il Local System sará poi l’amministratore che potrá modificarlo creando un utente ad hoc per il servizio.
  • Description: La descrizione che apparirá sul service manager di windows per il nostro servizio.
  • DisplayName: Nome con cui il servizio sará visualizzato sul service manager di windows e potrá essere fatto partire o fermato con Net Start o Net Stop.
  • ServiceName: Nome con cui il servizio è identificato dal sistema, Fate molta attenzione, se questo nome non corrisponde a quello che abbiamo indicato sulle property della classe del servizio per la property ServiceName, l’installazione non andrá a buon fine e il servizio non partirá.
  • ServicesDependentOn: Lo abbiamo lasciato commentato, ma è una lista dei servizi da cui il nostro servizio dipende, nell’esempio commentato abbiamo indicato che il servizio deve partire dopo che tre servizi sono partiti, il servizio lanmanserver (server di rete) il servizio lanmanworkstation (rete della workstation) ed il servizio mssqlserver (SqlServer), è importante scegliere correttamente i servizi da cui un servizio dipende per evitare che possa avere problemi se installato su una macchina su cui non tutti questi servizi sono presenti

L’ultima operazione che fa il costruttore è aggiungere all’installer le due classi che abbiamo definito.

Se utilizzassimo il setup standard di Visual Studio 2010, non dovremo fare altro, infatti Windows Installer riconosce la classe installer presente nel servizio quando gli viene indicato di eseguire gli eventi di Install e Commit al termine dell’installazione e quindi quanto indicato in questa classe viene eseguito ed il servizio risulta installato sul sistema.

Se vogliamo usare InnoSetup invece tutto questo non basta, perché InnoSetup non è in grado di verificare la presenza di un Installer in una applicazione, peró, Innosetup è in grado di Eseguire una applicazione al termine di una installazione, questo ci permetterá di ovviare al problema.

La classe Program

Cosí come tutte le applicazioni .Net, anche un servizio ha il suo Entry Point, il suo Main che viene generato automaticamente nella classe Program.

static class Program
{
	
	static void Main(string[] args)
	{
		if (args.Length > 0)
		{

			string parameter = args[0];
			switch (parameter)
			{
				case "/install":
					ManagedInstallerClass.InstallHelper(new string[] { Assembly.GetExecutingAssembly().Location });
					break;
				case "/uninstall":
					ManagedInstallerClass.InstallHelper(new string[] { "/u", Assembly.GetExecutingAssembly().Location });
					break;
			}
		}

		else
		{
			LogType val = LogType.ExceptionsOnly;
			string verbosityFile = Path.Combine(ServiceContext.Instance.DataFolder, "verbosity.txt");
			ifFile.Exists(verbosityFile))
			{
				//LogType.AllMessages
				//LogType.ExceptionsOnly	
				//LogType.WarningsAndExceptions	
				Enum.TryParse<LogType>(File.ReadAllText(verbosityFile),true, out val);
			}
			ServiceContext.Instance.LogVerbosity =  val;

			ServiceBase[] ServicesToRun;
			ServicesToRun = new ServiceBase[]  {  new MiniSqlAgentService()  };
			ServiceBase.Run(ServicesToRun);
		}
	}
}

Se osserviamo la prima parte del metodo Main, vediamo che abbiamo inserito un controllo sui parametri da linea di comando forniti all’avvio del programma, pertanto, se eseguiamo il servizio passando il parametro:

  • /install viene chiamato il metodo InstallHelper della classe ManagedInstallerClass, indicandogli di utilizzare il nome dell’assembly corrente.
  • /uninstall viene chiamato lo stesso metodo ma con due parametri, uno dei quali è /u per la disinstallazione del servizio.

Queste chiamate sono quelle che esegue automaticamente il Setup di Windows per installare o disinstallare un servizio (eseguendo quanto indicato sulla classe Installer).

Se facciamo partire il servizio senza alcun parametro, il sistema instanzia e fa partire il servizio vero e proprio.

Gli script di installazione per InnoSetup

Nel progetto di esempio troverete 3 file con estensione .iss gli script di InnoSetup

MiniSqlAgentVersion.iss

#define MyAppVersion "2013.0.0.0"

Contiene una sola riga, che ci permetterá di gestire il versioning del nostro setup.

MiniSqlAgentFiles.iss

Source: "bin\x86\Debug\MiniSqlAgent.exe"; DestDir: {app}; Flags: IgnoreVersion;
Source: "bin\x86\Debug\Dnw.Base.v4.0.dll"; DestDir: {app}; Flags: IgnoreVersion;
Source: "bin\x86\Debug\Dnw.MiniSqlAgent.ServiceLayer.dll"; DestDir: {app}; Flags: IgnoreVersion;

Contiene i nomi dei 3 files che compongono il nostro servizio, L’exe e le due DLL che referenzia, una presente sulla soluzione, l’altra è la libreria di base che fornisce i servizi di Log.

MiniSqlAgentSetup.iss

; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
 
#define MyAppName "MiniSqlAgent"
#include "MiniSqlAgentVersion.iss";
#define MyAppPublisher "Dotnetwork.it"
#define MyAppURL "http://www.Dotnetwork.it"
#define MyAppExeName "MiniSqlAgent.exe"
 
[Setup]
; NOTE: The value of AppId uniquely identifies this application.
; Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={{EE89D9C8-DCE0-42F9-A9DB-8A7B37AEDE4D}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
;AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={pf32}\Dnw\MiniSqlAgent
DefaultGroupName=Dnw
DisableProgramGroupPage=yes
OutputDir=..\MiniSqlAgentInnoSetup
OutputBaseFilename=MiniSqlAgentSetup
SetupIconFile=msaico.ico
Compression=lzma
SolidCompression=yes
UninstallDisplayIcon=msaico.ico
WizardImageBackColor=$ffffff
WizardImageFile=MsaLogoInstall.bmp
WizardSmallImageFile=MsaLogoInstallsmall.bmp
 
 
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "italian"; MessagesFile: "compiler:Languages\Italian.isl"
 
[Files]
#include "MiniSqlAgentFiles.iss"
 
 
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
 
 
[Run]
Filename: "{app}/MiniSqlAgent.exe"; Parameters: "/install"
 
[UninstallRun]
Filename: "{app}/MiniSqlAgent.exe"; Parameters: "/uninstall"

Il piú corposo dei 3, contiene le istruzioni per la compilazione del setup da parte di InnoSetup, non ne discuteremo tutto il contenuto, ma solo i punti importanti.

Ci sono 2 clausole #include che inseriscono nel punto giusto i due files accessori da noi prodotti.

E ci sono due sezioni: Run e UninstallRun che eseguono l’exe con i parametri /install e /uninstall da noi predisposti allo scopo. Questi due semplici comandi fanno in modo che il servizio sia installato sul sistema quando eseguiamo il Setup e sia rimosso correttamente quando disinstalliamo l’applicazione da Windows.

Come facciamo a far generare il setup del programma da  Visual Studio?

msa_svc_06

Utilizziamo il Post Build Event di Visual Studio per lanciare da command line la creazione del setup del nostro progetto, il comando è standard, perché lo abbiamo costruito utilizzando le macro che forniscono il nome di progetto e la cartella di progetto per fare in modo che il compilatore di InnoSetup trovi correttamente quello che gli serve. Pertanto vedrete che lo copieremo ed incolleremo anche su altri progetti che prevedono un setup.

msa_svc_07

Questo è il file di setup che ci viene generato automaticamente. Ed una volta eseguito il setup ecco cosa troveremo sul Service Manager di Windows:

msa_svc_08

msa_svc_09

msa_svc_10

Quando lanciate questo servizio, troverete queste informazioni sui Public Documents della macchina:

msa_svc_11

E’ stato scelto di usare i Public Documents perché il Servizio puó scrivere su questa cartella e gli utenti possono allo stesso modo accedervi e leggerlo o sovrascriverlo. Fate sempre attenzione a dove mettete i file dati da far scrivere ai servizi, assicuratevi che l’utente usato per eseguirli possa accedervi e modificarli oppure potreste trovare dei problemi in installazione se l’amministratore di sistema modifica i parametri di esecuzione (come sarebbe auspicabile sempre).

Nell’immagine qui sopra possiamo vedere che c’è un secondo file, si chiama verbosity.txt, che cos’è?

LogType val = LogType.ExceptionsOnly;
string verbosityFile = Path.Combine(ServiceContext.Instance.DataFolder, "verbosity.txt");
ifFile.Exists(verbosityFile))
{
	//LogType.AllMessages
	//LogType.ExceptionsOnly	
	//LogType.WarningsAndExceptions	
	Enum.TryParse<LogType>(File.ReadAllText(verbosityFile),true, out val);
}
ServiceContext.Instance.LogVerbosity =  val;

Nella porzione che non abbiamo commentato del Main del servizio, troviamo le righe qui sopra, verbosity.txt è un primo timido tentativo da parte nostra di parametrizzare l’applicazione, se in questo file scriviamo una delle tre opzioni relative al log, possiamo decidere quale sará il dettaglio fornito dal log.

AllMessages

msa_svc_12

Verbositá massima, tutti i tipi di informazioni di esecuzione sono loggati (Informazioni, Avvisi, Eccezioni) Questo è utile per studiare il comportamento del servizio inserendo messaggi informativi nell’esecuzione dello stesso.

WarningsAndExceptions

msa_svc_13

Verbositá media, vengono loggati i messaggi di avviso e le eccezioni, puó essere utile per segnalare situazioni anomale non bloccanti e verificare il comportamento del servizio.

ExceptionsOnly

msa_svc_14

Verbositá minima, vengono loggate solo le eccezioni, sará la modalitá di funzionamento normale una volta a regime, da modificare in caso di anomalie per approfondire il problema.

Conclusioni

In questo post piuttosto complesso, abbiamo spiegato come costruire un servizio Windows con C#, come predisporre il necessario a permettere la sua installazione in modo automatico sul sistema, ed abbiamo creato un setup per il servizio utilizzando InnoSetup. Abbiamo inoltre visto come poter attivare un log di sistema con verbositá diversa in base alle esigenze di controllo che abbiamo.

Il codice del progetto collegato a questo articolo può essere scaricato al link qui sotto

Per qualsiasi domanda, curiosità approfondimento, potete usare il link al modulo di contatto in cima alla pagina.

Aggiungo i riferimenti alle librerie di supporto che saranno utilizzate anche negli articoli successivi.