Press "Enter" to skip to content

2 – MiniSqlAgent – Aggiungere alcuni parametri di configurazione ad un Servizio Windows

In questo post, che aggiunge un mattoncino al servizio che abbiamo iniziato a costruire nel post  Minisqlagent un Servizio Windows installabile con InnoSetup vediamo come inserire in una applicazione di servizio i parametri di configurazione dello stesso modificando il suo Business Context per permettere all’amministratore di sistema di parametrizzare il servizio in base alle sue esigenze.

La classe ServiceSettingsManager

Service_settings_01

Aggiungiamo al progetto del Service Layer del servizio una nuova classe dove andremo ad implementare il necessario a fornire al servizio quelli che sono i parametri di configurazione applicativi.

public class ServiceSettingsManager
{
 
	private const string LOGFILE = "Dnw\\Logs\\MiniSqlAgent\\MSA{0}.log";
	
	private const string APPSTT_File = "Dnw\\MiniSqlAgent\\MiniSqlAgent_APP.xml";
	
	private const string STT_CheckInterval = "CheckInterval";
	
	private const string STT_DataFolder = "DataFolder";
	
	private const string STT_LogVerbosity = "LogVerbosity";
	

Iniziamo la nostra classe di gestione dei parametri di configurazione con una serie di costanti, che rappresentano le cartelle per il log e per il file di configurazione e gli ID dei parametri.

private DnwSettingsCollection mAppSettings;

private static string mLogFileName;

private string mAppSettingsFileName;

Tre variabili member per la collezione dei dati di parametrizzazione ed i due nomi dei file che utilizzeremo nell’applicazione.

public string LogFileName
{
	get
	{
		if (mLogFileName == null)
		{
			mLogFileName = string.Format(
			Path.Combine(Environment.GetFolderPath(
			Environment.SpecialFolder.CommonDocuments), LOGFILE),
			DateTime.Now.ToString("yyyyMMdd"));
			Path.GetDirectoryName(mLogFileName).XDwCheckPath();
		}
		return mLogFileName;
	}
}

public string AppSettingsFileName
{
	get
	{
		if (mAppSettingsFileName == null)
		{
			mAppSettingsFileName = Path.Combine(Environment.GetFolderPath(
				Environment.SpecialFolder.CommonApplicationData),
				APPSTT_File);
			Path.GetDirectoryName(mAppSettingsFileName).XDwCheckPath();
		}
		return mAppSettingsFileName;
	}
}

Le due property che forniscono il nome del file dei parametri applicativi (anche se al momento è usata solo in questo manager la predisponiamo pubblica perchè ci servirà in seguito per la User Interface).
Ed il nome del file di Log dell’applicazione, viene costruito con la data al momento per semplicità lo lascieremo così, in seguito potremo fare in modo che il servizio lo aggiorni al cambio di data in modo da avere un file di log per ogni giorno di funzionamento. Con l’attuale configurazione il log ha la data in cui il servizio è partito, quindi se lo lasciamo sempre in funzione questo valore non cambierà mai.

public void GetSettings()
{
	GetApplicationSettings();
}

L’unico metodo pubblico, che per il momento si occupa di acquisire i parametri applicativi.

private void GetApplicationSettings()
{
	try
	{
		if (mAppSettings == null)
		{
			mAppSettings = new DnwSettingsCollection();
		}
		mAppSettings.Clear();

		mAppSettings.Add(new DnwSetting()
		{
			Category = Properties.Resources.txtSSMCatConfiguration,
			ID = STT_LogVerbosity,
			Description = Properties.Resources.txtSSMDesLogVerbosity,
			EditorType = EditorType.TextBox,
			Position = 0,
			Value = LogType.ExceptionsOnly.ToString(),

		});

		mAppSettings.Add(new DnwSetting()
		{
			Category = Properties.Resources.txtSSMCatConfiguration,
			ID = STT_CheckInterval,
			Description = Properties.Resources.txtSSMDesCheckInterval,
			EditorType = EditorType.NumericInteger,
			Position = 1,
			Value = "60",

		});

		mAppSettings.Add(new DnwSetting()
		{
			Category = Properties.Resources.txtSSMCatConfiguration,
			ID = STT_DataFolder,
			Description = Properties.Resources.txtSSMDesDataFolder,
			EditorType = EditorType.DirectoryName,
			Position = 1,
			Value = Path.Combine(Environment.GetFolderPath(
				Environment.SpecialFolder.CommonDocuments), "MiniSqlAgent"),

		});

		if (File.Exists(AppSettingsFileName))
		{
			//Only the managed settings are updated and loaded
			//From the text file any extra setting is ignored

			try
			{
				DnwSettingsCollection savedSettings = DnwSettingsCollection.ReadXml(AppSettingsFileName, false);
				foreach (DnwSetting stt in savedSettings)
				{
					DnwSetting item = mAppSettings[stt.ID];
					if (item != null)
					{
						item.Value = stt.Value;
					}
				}
			}
			catch (Exception ex)
			{
				EventLogger.SendMsg(ex);
				//If the file is corrupted
				//we ignore the error letting the default values on the settings
			}
		}
		else
		{

			mAppSettings.WriteXml(AppSettingsFileName);
		}

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

Il metodo che genera i parametri applicativi, in questo caso si tratta di un metodo che costruisce i setting in modo arbitrariamente deciso da noi, e la logica decisa è la seguente:

  • Generiamo tutti i parametri applicativi necessari all’applicazione assegnando i valori di default.
  • Leggiamo il file contenente i parametri se esiste
  • Aggiorniamo il valore dei parametri con quello letto dal file (se esistono)
  • Se il file non esiste lo creiamo vuoto (al momento questa creazione serve solo a fornirci il modo di modificare i parametri manualmente dal file XML)
public LogType LogVerbosity
{
	get
	{
		LogType type = LogType.AllMessages;
		Enum.TryParse<LogType>(mAppSettings[STT_LogVerbosity].Value, out type);
		return type;
	}
}

public int CheckInterval
{
	get
	{
		int interval = 60;
		int.TryParse(mAppSettings[STT_CheckInterval].Value, out interval);
		return interval;
	}
}

public string DataFolder
{
	get
	{
		Path.GetDirectoryName(mAppSettings[STT_DataFolder].Value).XDwCheckPath();
		return mAppSettings[STT_DataFolder].Value;
	}
}

Le tre property che espongono i parametri da noi creati verificando che siano validi.

Facciamo notare come questa classe non abbia alcuna predisposizione ad essere usata con la User Interface, infatti è solo una classe accessoria al contesto applicativo, il suo scopo non è fornire dati alla user interface ma fornire dati di contesto all’applicazione.

Ora infatti modifichiamo la classe ServiceContext ed aggiungiamogli i dati di parametrizzazione.

Le modifiche alle classi già introdotte

public ServiceSettingsManager SettingsManager
{
	get
	{
		if (mSettingsManager == null)
		{
			mSettingsManager = new ServiceSettingsManager();
			mSettingsManager.GetSettings();
		}
		return mSettingsManager;
	}
}

Aggiungiamo la property che espone i setting ed eliminiamo le property che avevamo definito nel precedente articolo che ora sono state spostate nel Manager di parametrizzazione.

public void ResetSettings()
{
	mSettingsManager = null;
}

Aggiungiamo un metodo che forza il caricamento dei setting se necessario.

Modifichiamo Program.cs togliendo ogni riferimento al nome delle cartelle e del file di configurazione temporaneo per il test della verbosità dei messaggi di log.

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
	{
		ServiceBase[] ServicesToRun;
		ServicesToRun = new ServiceBase[]  {  new MiniSqlAgentService()  };
		ServiceBase.Run(ServicesToRun);
	}
}

Il Main del progetto diviene molto lineare e semplice.

protected override void OnStart(string[] args)
{
	try
	{

#if DEBUG
		// Launch the debugger in case of need
		if (Debugger.IsAttached)
		{
			Debugger.Break();
		}
		else
		{
			Debugger.Launch();
		}


#endif
		//Set the trace file for logging purpouses
		System.IO.FileStream logTracer = new System.IO.FileStream(
			ServiceContext.Instance.SettingsManager.LogFileName,
			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);

		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();
	}
}

Modifichiamo l’event handler dell’evento Start del servizio andando a chiedere il nome del log file al ServiceContext.

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

Modifichiamo l’event handler del logger per andare a verificare la verbosità usando il ServiceContext.

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.SettingsManager.CheckInterval * 1000, false) != 0)
		{

			ExecJob();

		}

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

Modifichiamo il timer del loop del servizio per leggere l’intervallo di lavoro nel ServiceContext.

Service_settings_02

Provando a creare, installare ed eseguire il servizio, vedremo che il Log viene generato come indicato nell’immagine qui sopra sulla cartella da noi predisposta. Abbiamo scelto di utilizzare la cartella Public\Documents del sistema per un motivo preciso, tutti gli utenti di una macchina hanno diritto di leggere e scrivere sulla cartella Public\Documents pertanto anche modificando l’utente del servizio e creandone uno ad hoc con i soli permessi strettamente indispensabili, l’amministratore di sistema non dovrà creare policy strane o dare permessi speciali all’utente. Inoltre questa cartella essendo accessibile a tutti gli utenti della macchina permette all’amministratore di consultarla senza problemi a trovarla e permette anche di assegnarne il controllo ad un utente con privilegi non amministrativi.

Service_settings_03

Il file contenente i parametri di configurazione lo troviamo invece sulla cartella indicata nell’immagine qui sopra.

Perchè usare questa cartella? Per un motivo preciso, perchè ProgramData è una cartella Standard indicata dalle linee guida di sistema come appositamente generata per ospitare dati collegati alle applicazioni installate che devono essere accessibili in lettura a tutti gli utenti (compreso quello del servizio) ma sono scrivibili solo a chi ha diritti amministrativi.

Questo serve ad evitare che un utente non amministratore possa modificare i parametri funzionali del nostro servizio (o di una qualsiasi delle nostre applicazioni).

Quando introdurremo dei parametri di configurazione Utente (Molto più utili nelle applicazioni con interazione utente che nei servizi) li metteremo in una cartella che permetta di salvarli per ognuno degli utenti che eventualmente condividono la macchina e permetta all’utente di modificarli a proprio piacere.

Service_settings_04

Qui sopra l’immagine del file XML di configurazione che abbiamo manualmente modificato inserendo il valore “AllMessages” per la verbosità del log diminuendo a 10 secondi il valore del Ciclo di servizio ottenendo quindi il log seguente:

21/05/2013 14:55:52: Mini Sql Agent Start Main Thread
21/05/2013 14:55:52: Start the Mini Sql Agent Service
21/05/2013 14:55:53: Mini Sql Agent Start Job Processing
21/05/2013 14:56:03: Job Executed (info message )
+++ WARNING! +++
21/05/2013 14:56:04: Job Executed (warning message )
*** ERROR! ***
21/05/2013 14:56:05: Job Executed (error message )
21/05/2013 14:56:15: Job Executed (info message )
+++ WARNING! +++
21/05/2013 14:56:16: Job Executed (warning message )
*** ERROR! ***
21/05/2013 14:56:17: Job Executed (error message )
21/05/2013 14:56:27: Job Executed (info message )
+++ WARNING! +++
21/05/2013 14:56:28: Job Executed (warning message )
*** ERROR! ***
21/05/2013 14:56:29: Job Executed (error message )

.....

21/05/2013 15:05:15: Job Executed (info message )
+++ WARNING! +++
21/05/2013 15:05:16: Job Executed (warning message )
*** ERROR! ***
21/05/2013 15:05:17: Job Executed (error message )
21/05/2013 15:05:21: Mini Sql Agent End Job Processing
21/05/2013 15:05:31: Stop the Mini Sql Agent Service

Testando il funzionamento del servizio per alcuni minuti.

Conclusioni

Con questo post abbiamo visto come iniziare a parametrizzare il funzionamento di un servizio e come gestire il contesto funzionale di una applicazione tramite le classi ServiceSettingsManager e ServiceContext, ed abbiamo illustrato come configurare il log di sistema e i parametri applicativi utilizzando le cartelle forniteci dal sistema.

Il codice del progetto esempio è disponibile al link seguente:

Il codice delle librerie comuni con le classi utilizzate per la gestione dei Setting sono disponibili al link seguente:

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