Introduction
Manage services in Windows that also have a client that interacts with them as well as an icon in the status bar of Windows and that allows -adays- that is not only used by a specific service but by various solutions that one can implement is a task that allows the saving of much work.
In this article we will implement a dynamic DNS update client for DucDNS that can run as a service / application of windows using the developed framework.
Follow or fork me in github
Background
I have been using dynamic dns for my clients for years.
Our software (MTCGestion) allows branches of a company to be distributed in a distributed way and to be interconnected by means of (for example) the use of virtual private networks.
For the latter we use the great openvpnwhich works wonderfully.
Vpns clients, however, need to know the address of the server which constantly changes with normal internet operators. This is where the Dynamic dns comes into play.
There are several providers (non-ip, DynDNS, to name a few) which provide free and paid services.
As for the free ones always come with limitations. One of them is that every X amount of time must be reconfirmed that the service is being used, but the account is discharged.
Most of my clients do not have an IT department or staff that is responsible for managing the network and infrastructure) so most of the time this "warning email" is never read, the account is deleted, the address IP of the provider changes, the vpn stops working, and finally the stores lose the communication. There, customers begin to call indicating that the software does not work because the network does not work.
A few weeks ago start looking for an alternative to this because my business is not infrastructure but software development. Or I implemented my own dynamic ip manager - I have all the knowledge of IT administrator to do it .... :( - or I was looking for another alternative .....
This is how I found the site duckdns.
These people have a service like the one I needed but .... after a little research they do not count (maybe I did not see ...) with an update client that can work as a windows service (it is an indispensable requirement for us since to the servers of my clients does not initiate sesion almost nobody almost never !!!
Well, the update specifications were simple with which .... hands down
What is it about?
A generic container for managing Windows services
The idea is to have a container or "shuttle or container application" that allows the loading of any service following basic guidelines given by a well-defined standard configuration.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="ConfigNucleo" type="MTC.Host.IComun.ConfiguracionNucleo, MTC.Host.IComun"/>
<section name="ConfigServicio" type="MTC.Host.IComun.ConfiguracionServicio, MTC.Host.IComun"/>
</configSections>
<ConfigNucleo tipoProveedor="MTC.Nucleo.DucDNS.NucleoDucDNS, MTC.Nucleo.DucDNS" />
<ConfigServicio nombre="MTCDucDNS" descripcion="Servidor de actualizaciones de DucDNS"
serviciosDependientes="Winmgmt" segundosEsperaInicio="5" cuentaServicio="LocalSystem" />
</configuration>
The configuration file above defines 2 sections:
ConfigService: sets the values for any type of service:
- nombre: Indicates the name of the service (as it will appear in the windows services).
- descripcion: the service description
- serviciosDependientes: the dependent services for the start of this service (very useful for example when the ignition sequence is of high priority eg the service must wait until the MSSQLServer service has been started before it can start)
- segundosEsperaInicio: Indicates a delay in the startup process (optional value)
- cuentaServicio: Indicates the windows service account. Here the values could be: LocalService, NetworkService, LocalSystem (the account that we should use by default), and finally User.
ConfigNucleo: Contains the values of the class that will currently implement the service functionality
- tipoProveedor: Indicates the name of the assembly (the class library) that contains the service itself
All right. The container or host application that will launch the service is a windows application without a main window.
static void Main(string[] args)
{
if (!path.EndsWith(Path.DirectorySeparatorChar.ToString()))
path += Path.DirectorySeparatorChar;
...
var queHace = QueHaceConLosArgumentos.Nada;
try
{
queHace = analizarArgumentos(args);
if (queHace != QueHaceConLosArgumentos.Nada)
procesarArgumentoSobreServicio(queHace, args);
else
ejecutarComoServicio();
}
catch (Exception ex)
{
...
}
}
Among other things, what is done when starting the application is to verify the arguments with which it is called.
These are of an enumerated type defined as:
private enum QueHaceConLosArgumentos { Nada = 0, Instala = 1, Desinstala = 2, EjecutaComoConsola = 3, EjecutaComoVentana = 4 }
- Instala: The application has been called to install the service
- Desinstala: The application has been called to uninstall a service
- ExecuteConsola: The application has been called to run as a windos console (shell)
- EjecutaComoVentana: The application has been called to execute as a windows application (with window)
Thus, analyzing with what argument has been called proceed to act:
private static void procesarArgumentoSobreServicio(QueHaceConLosArgumentos queHace, string[] args)
{
switch (queHace)
{
case QueHaceConLosArgumentos.Instala:
try
{
args[0] = Assembly.GetExecutingAssembly().Location;
ManagedInstallerClass.InstallHelper(args);
}
catch (Exception ex)
{
...
}
break;
case QueHaceConLosArgumentos.Desinstala:
string binpath = Assembly.GetExecutingAssembly().Location;
var toBeRemoved = ServiceController.GetServices().Where(s => GetImagePath(s.ServiceName) == binpath).Select(x => x.ServiceName);
var installer = new ProjectInstaller();
installer.Context = new InstallContext();
foreach (var sname in toBeRemoved)
try
{
installer.Uninstall(sname);
}
catch { }
break;
case QueHaceConLosArgumentos.EjecutaComoConsola:
ejecutarComoConsola();
break;
case QueHaceConLosArgumentos.EjecutaComoVentana:
ejecutarComoVentana();
break;
}
}
Installation
In case of installing the service (case QueHaceConLos Argumentos.Instala :) we have the call to InstallHelper method of the ManagedInstallerClass class.
Now, it could be the case that the application is called to be installed by passing additional parameters such as the account and the key with which the service should be installed.
Ej
MTC.Host.exe / i / account = guillermo / key = 11111
In this case we have to make a small modification so that the invocation to the method works by replacing the value of the zero position with
Assembly.GetExecutingAssembly ().
Uninstall
In the case of uninstalling the service (case QueHaceConLos Argumentos.Desinstala :) we have to do several things:
- Get the current location of the assembly that is running
- Obtain from the service controller all those whose image (binary) corresponds to that of the current assembly (the container host)
- With the help of the ProjectInstaller class (described later) proceed to uninstall dependent services
string binpath = Assembly.GetExecutingAssembly().Location;
var toBeRemoved = ServiceController.GetServices().Where(s => GetImagePath(s.ServiceName) == binpath).Select(x => x.ServiceName);
var installer = new ProjectInstaller();
installer.Context = new InstallContext();
foreach (var sname in toBeRemoved)
try
{
installer.Uninstall(sname);
}
catch { }
break;
ProjectInstaller allows (among other things) to be able to perform operations for example before installing the service. Eg to perform an audit of accounts, configuration files, remove the service in case it already exists, etc.
Also, the purpose of it is to be able to perform an uninstallation of a service by name
[RunInstaller(true)]
public class ProjectInstaller : Installer
{
private ServiceProcessInstaller process;
private ServiceInstaller service;
public ProjectInstaller()
{
process = new ServiceProcessInstaller();
service = new ServiceInstaller();
Installers.Add(process);
Installers.Add(service);
}
....
public void Uninstall(string serviceName)
{
service.ServiceName = serviceName;
base.Uninstall(null);
}
}
Run as a service
The next alternative is to run the application as a service
private static void ejecutarComoServicio()
{
var ServicesToRun = new System.ServiceProcess.ServiceBase[] { new MTCHost() };
System.ServiceProcess.ServiceBase.Run(ServicesToRun);
}
In this case what we do is to execute the Run method of the ServiceBase class indicating that the service to run is the same container (a new instance of the MTCHost class)
Run as window (interactive mode)
As we mentioned at the beginning of the article, it is necessary to have a means by which we can configure and access the parameters of the service in an interactive way. For this, we will use an instance of the ApplicationContext class
private static void ejecutarComoVentana()
{
string nombreApp = configServicio.nombre + "-App";
if (SingleInstanceClass.CheckForOtherApp(nombreApp))
{
MessageBox.Show(Properties.Resources.aplicacionEnEjecucion + nombreApp, "Error", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
return;
}
INucleo _instance = crearInstancia(false);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Dictionary<string, object> parametros = new Dictionary<string, object>(){
{"nucleo", _instance},
{"nombreApp", nombreApp},
{"nombreServicio", configServicio.nombre},
{"appHost", appHost}};
Application.Run(_instance.contextoAplicacion(new object[1] { parametros }));
}
Well as we want to ensure that there is no more than one instance of the application that interacts with the running service we perform this check by calling the CheckForOtherApp method of the SingleInstanceClass helper static class.
In case there is no other application in execution we proceed to create the instance of the service by calling the method createInstance.
To consider is that this method returns an interface (well defined) to an object reference of a base class called NucleoBase (see attached code).
If all goes well, we proceed to execute the instance of the ApplicationObject class (defined within the service itself) by passing it as parameters a dictionary with the values necessary for its correct operation ..
All this will be explained in more detail below.
Run as shell
It may also be interesting to be able to run the solution in console mode windows (shell) for eg debug purposes, etc.
In this case we have the last call option of the host application:
private static void ejecutarComoConsola()
{
bool madeConsole = false;
if (!AttachToConsole())
{
AllocConsole();
madeConsole = true;
}
try
{
INucleo _instance = crearInstancia(true);
string error = "";
if (_instance.iniciar(out error))
{
Console.WriteLine(Properties.Resources.presioneTeclaParaDetenerServicio);
Console.ReadLine();
_instance.detener();
}
else
{
Console.WriteLine(Properties.Resources.erroresInicioServicio + error);
Console.ReadLine();
}
if (madeConsole)
FreeConsole();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.ReadLine();
}
}
This way we can execute the solution as a console with the advantages of debug and so on.
INucleo. The interface of all the services of the framework
This interface defines the basic behavior of all the operations that we need to perform on services, as well as the means by which we can access the application context (ApplicationContext) that will allow to administrate the service through windows controls (eg Windows Forms, Context menu in the status bar, etc.
public interface INucleo : IDisposable
{
bool iniciar(out string error);
void detener();
ApplicationContext contextoAplicacion(object[] args);
string path { get; }
void buscarInfoEnsamblado(System.Reflection.Assembly ensamblado);
string productVersion { get; }
string productVersionHostContenedor { get; }
string nombre { get; }
void configurar();
void cambioDeSesion(SessionChangeDescription changeDescription);
Dictionary<string, string> infoEnsamblado { get; }
ConfiguracionServicio configServicioHost { get; }
bool iniciado { get; }
string cultura { get; }
}
The explanation of the methods and properties
Methods
- iniciar/detener: Start / stop service
- contextoAplicacion: Returns the instance of the current application context which will be used to manage the service interactively
- buscarInfoEnsamblado: forces each instance of the class to implement this method which allows obtaining information from the assembly in general
- configurar: forces each instance of the class to implement this method to ensure the configuration of the service
- cambioDeSesion: It may be necessary to know if there is a user logged in windows and also to know when there is a change of session by other users
Properties
- path: The execution path of the current service
- productVersion: The version of the current library of the core
- productVersionHostContenedor : The host version (MTC.Host.exe) containing the service
- nombre: The name of the service
- infoEnsamblado: All the assembly information (which is forced to be obtained by the call to theInstallInsection method
- configServicioHost: Returns all the configuration of the class library that implements the service
- iniciado: Indicates whether the service has been started or not
- cultura: returns information about culture (regional configuration)
Well let's look at the pieces of the puzzle to get a more detailed picture of the underlying idea.
One of the first things to consider is that each service can (and should) have its own configuration.
For this we see how to solve this first topic through the configuration file of the instance of a class that implements the interface INucleo
NucleoDucDNS.
A class that allows the dynamic DNS service to be updated from the interface INucleo DucDNS.
Viewing the specifications of DucDNS we have that the following information is necessary and required to update our IP address.
We are going to implement a class that allows the update of the dynamic DNS service.
The specifications of the update mechanism come in the direction:
tttps://www.duckdns.org/spec.jsp
Basically the service allows updating an ip address to a subdomain of the site using a simple HTTP GET
https:
There is also a simplified form of invocation defined as follows:
https:
There are, according to the documentation of the site, basic routers that do not allow the invocation through the use of parameters. In this case an invocation as defined above is useful.
Thus, with all the above we could define the following parameters:
- Domains: One or more separated by commas
- Token: The access key that the service gives us when we create an account
- IP (optional): The IP address that we want the service to place in our dns (if we leave blank the service uses the address from where the method is invoked)
- Verbose: Indicates if we want to have additional information at the time of the service update
- Simple: Indicates whether the invocation will be made with or without parameters
- An interval (in minutes) every time the update is performed against the site. Ex: 5/10/15/30/60 minutes
NOTES:
- The ipv6 parameter can also be completed but in this example we will leave it out
- The service allows you to use both https and http. While https is the preferred and recommended way there are situations where an https invocation is not allowed
As a first approximation we would have the configuration of the service could be as follows
public class ConfiguracionServidorDucDNS : ConfigurationSection
{
[ConfigurationProperty("token")]
public string token
{
get { return (string)base["token"]; }
set { base["token"] = value; }
}
[ConfigurationProperty("simple")]
public bool simple
{
get { return (bool)base["simple"]; }
set { base["simple"] = value; }
}
[ConfigurationProperty("verbose")]
public bool verbose
{
get { return (bool)base["verbose"]; }
set { base["verbose"] = value; }
}
[ConfigurationProperty("https")]
public bool https
{
get { return (bool)base["https"]; }
set { base["https"] = value; }
}
[ConfigurationProperty("minutosActualizacion")]
public int minutosActualizacion
{
get { return (int)base["minutosActualizacion"]; }
set { base["minutosActualizacion"] = value; }
}
[ConfigurationProperty("dominios")]
public string dominios
{
get { return (string)base["dominios"]; }
set { base["dominios"] = value; }
}
Well, the corresponding class can be seen in the configuration file of the core (the service that implements the interface) as follows
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="ConfiguracionServidorDucDNS" type="MTC.Nucleo.DucDNS.ConfiguracionServidorDucDNS, MTC.Nucleo.DucDNS"/>
</configSections>
<ConfiguracionServidorDucDNS token="" simple="false" verbose="false" https="true" dominios="" modoDebug="0" minutosActualizacion="5"/>
</configuration>
As you can see is defined a section called ServerDucDNSServer whose assembly is inside the same library in "MTC.Nucleo.DucDNS.ServerDucDNS Configuration, MTC.Nucleo.DucDNS"
Well, let's see how we converted this configuration into a useful class instance for the service itself
public NucleoDucDNS(Dictionary<string, object> parametros)
: base(parametros)
{
buscarInfoEnsamblado(System.Reflection.Assembly.GetExecutingAssembly());
_nombre = this.GetType().Name;
var archivoConfig = Path.GetFileName(System.Reflection.Assembly.GetExecutingAssembly().Location)+".config";
inicializar(archivoConfig);
configuracionServidorDucDNS = (ConfiguracionServidorDucDNS)config.GetSection(seccionConfiguracionServicio);
if (configuracionServidorDucDNS == null)
throw new Exception(string.Format(Properties.Resources.seccionNoEncontrada, seccionConfiguracionServicio));
...
...
}
First we have to be able to locate the configuration file of the instance of the class.
For this the line:
var archivoConfig = Path.GetFileName(System.Reflection.Assembly.GetExecutingAssembly().Location)+".config";
permite realizar dicha funcion.
Once the file is obtained as such we have to convert it into an instance of the class Configuration.
For this the call to the method:
inicializar(archivoConfig);
It transforms in:
protected void inicializar(string archivoConfig)
{
config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
ExeConfigurationFileMap fileMap = new ExeConfigurationFileMap();
fileMap.ExeConfigFilename = Path.Combine(_path, archivoConfig);
if (!File.Exists(fileMap.ExeConfigFilename))
throw new Exception(string.Format(Properties.Resources.archivoConfiguracionNoExiste, fileMap.ExeConfigFilename));
config = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);
}
If all goes well, in leaving this method we have a Configuration instance in the config class variable.
Returning to the constructor now we can easily instantiate the section of the configuration file in an instance of the class ServerDucDNS
configuracionServidorDucDNS = (ConfiguracionServidorDucDNS)config.GetSection(seccionConfiguracionServicio);
if (configuracionServidorDucDNS == null)
throw new Exception(string.Format(Properties.Resources.seccionNoEncontrada, seccionConfiguracionServicio));
Iniciar (The method that starts whatever the service does)
Having the class instance of the service then subtract then implement whatever it does.
For this we must implement the iniciar method defined in the interface
public bool iniciar(out string error)
{
bool result = false;
error = "";
log.Info(string.Format(Properties.Resources.iniciandoNucleo, _nombre, productVersion, productVersionHostContenedor));
iniciarTimerActualizacion();
_iniciado = result = true;
....
log.Info(Properties.Resources.nucleoIniciado);
return result;
}
Here, the iniciarTimerActualization method starts an internal clock that performs the update of the IP address of the computer where the service is running against the site https://www.duckdns.org
private void iniciarTimerActualizacion()
{
if (timerMinuto == null)
{
timerMinuto = new System.Timers.Timer();
timerMinuto.Elapsed += timerMinuto_Elapsed;
timerMinuto.Interval = 2000;
}
timerMinuto.Enabled = true;
cantMinutos = 0;
}
private void timerMinuto_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
timerMinuto.Enabled = false;
if (timerMinuto.Interval == 2000)
timerMinuto.Interval = 60000;
procesar();
timerMinuto.Enabled = true;
}
The internal clock is executed every one minute at the end of which the method is invoked which, among other things, performs the IP address update
private void procesar()
{
try
{
...
if ((cantMinutos % configuracionServidorDucDNS.minutosActualizacion) == 0)
actualizarDominios();
cantMinutos++;
...
}
catch(Exception ex)
{
log.Error(Utils.armarMensajeErrorExcepcion("procesar: ", ex));
}
Simple no ?? If you have spent the amount of minutes each amount you want to update is called the method that invokes the services of these people. This with the method call actualizarDominios.
private void actualizarDominios()
{
string token = configuracionServidorDucDNS.token;
string html = string.Empty, protocolo = configuracionServidorDucDNS.https ? "https" : "http",
verbose = configuracionServidorDucDNS.verbose ? "true" : "false";
var dominios = new Dictionary<string, bool>();
var sdominios = configuracionServidorDucDNS.dominios;
if (!string.IsNullOrEmpty(sdominios))
{
foreach (var d in sdominios.Split(';').Where(x => !string.IsNullOrEmpty(x)))
{
string[] entradaDominio = d.Split('|');
string dominio = entradaDominio[0];
bool actualiza = Int32.Parse(entradaDominio[1]) > 0;
if (actualiza)
{
if (!dominios.ContainsKey(dominio))
{
dominios.Add(dominio, actualiza);
actualizarDominio(dominio, token, protocolo, verbose);
}
}
else { }
}
}
else
log.Error(Properties.Resources.sinDominiosQueActualizar);
}
catch (Exception ex)
{
..
}
}
The above simplified code basically disarms the domain chain (which comes from the form
dominioA|1,dominioB|1,dominioC|0.
This is the names of the domains then a pipe followed by a value 1/0 that indicates whether or not to update, then the next domain separated by commas.
Thus, for each of the domains that must be updated, the method actualizarDominio is invoked with the necessary parameters to perform the process.
private void actualizarDominio(string dominio, string token, string protocolo, string verbose)
{
string html = string.Empty;
string url =
configuracionServidorDucDNS.simple ?
string.Format(@"{0}://duckdns.org/update/{1}/{2}/{3}",
protocolo, dominio, token, dominio) :
string.Format(@"{0}://www.duckdns.org/update?domains={1}&token={2}&verbose={3}",
protocolo, dominio, token, verbose);
var request = (HttpWebRequest)WebRequest.Create(url);
request.AutomaticDecompression = DecompressionMethods.GZip;
using (var response = (HttpWebResponse)request.GetResponse())
using (var stream = response.GetResponseStream())
using (var reader = new StreamReader(stream))
html = reader.ReadToEnd();
...
}
Well, until then we would basically have the functionality of what the service should do (whatever it is).
How can we interact with the same as we said at the beginning to be able to for example: configure it, see its log of errors, etc?
Well, let's see how this is done through the instantiation of the class ApplicationContext
TaskTrayApplicationContextBase y TaskTrayApplicationContext.
Subclasses of a not-so-known class: ApplicationContext
Well they are not so well-known classes but basically it allows the access to contextual information on an application process.
However, this class has basic constructors and some other that we can define to allow - for example the passage of arguments of the classic form object [] args.
As it was seen at the beginning, within the container application, the contextoAplicacion method is invoked on an instance of the core (INucleo) - the class library that contains the service.
Within the core this can be seen as follows
public ApplicationContext contextoAplicacion(object[] args)
{
if (_contextoAplicacion == null)
_contextoAplicacion = new TaskTrayApplicationContext(args);
return _contextoAplicacion;
}
Well, the great thing is this class is that we can generate an instance of several classes, such as one that allows to contain an icon in the status bar (trayIcon).
Thus we have for example:
public class TaskTrayApplicationContextBase : ApplicationContext
{
protected MenuItem itemModoServicio, itemModoServicio_iniciarDetener, itemModoServicio_reiniciar, itemModoServicio_instalarDesinstalar,
itemModoServicio_iniciarDetenerYSalir, itemModoAplicacion, itemModoAplicacion_iniciarDetener, itemModoAplicacion_iniciarDetenerYSalir,
itemLogDeEventos, itemConfigurador, itemAbrirCarpetaContenedora,
exitMenuItem;
protected NotifyIcon notifyIcon = new NotifyIcon();
..
public TaskTrayApplicationContextBase(object[] args)
{
try
{
parametros = args[0] as Dictionary<string, object>;
m_ponerEstadoServicio = new delegate_ponerEstadoServicio(ponerEstadoServicio);
inicializar();
ponerEstadoServicio();
iniciarMonitoreoServicio();
}
....
}
protected void inicializar()
{
try
{
itemModoServicio_iniciarDetener = new MenuItem(Properties.Resources.servicioIniciar, new EventHandler(itemIniciarDetenerServicio_Click));
itemModoServicio_reiniciar = new MenuItem(Properties.Resources.servicioReiniciar, new EventHandler(itemReiniciar_Click));
...
...
exitMenuItem = new MenuItem(Properties.Resources.itemSalir , new EventHandler(Exit));
notifyIcon.ContextMenu = new ContextMenu(new MenuItem[] {
itemModoServicio,
new MenuItem("-"),
itemModoAplicacion,
new MenuItem("-"), itemConfigurador, itemAbrirCarpetaContenedora, new MenuItem("-"), exitMenuItem
});
notifyIcon.ContextMenu.Popup += new EventHandler(ContextMenu_Popup);
notifyIcon.Visible = true;
}
Basically we initialize some variables, create some items from a context menu, define the events to be invoked when clicked on them.
Also, the call to the method ponerEstadoServicio allows us to put information on the bar (rather on the tray icon)
public void ponerEstadoServicio()
{
string texto = "";
if (MTCServiceInstaller.ServiceIsInstalled(nombreServicio))
switch (estadoServicio)
{
case System.ServiceProcess.ServiceControllerStatus.Running:
notifyIcon.Icon = Properties.Resources.Ejecutando;
texto = string.Format("{0} {1} {2}", nombreServicio, nucleo.productVersion, descripcionEstadoServicio);
itemModoAplicacion.Enabled = false;
break;
case System.ServiceProcess.ServiceControllerStatus.Stopped:
notifyIcon.Icon = Properties.Resources.Detenido;
texto = string.Format("{0} {1} {2}", nombreServicio, nucleo.productVersion, descripcionEstadoServicio);
itemModoAplicacion.Enabled = true;
itemModoAplicacion_iniciarDetenerYSalir.Enabled = false;
break;
...
...
More suff....
Basically the main concepts are already defined.
There are still many things to be done.
For example, how do you find the status bar icon of the service status change ?? .
Eg the service is in execution and by the control panel I stop it.
It is useful to report this event so that it can be reflected in the status bar.
MonitorEstadoServicio. A class that allows you to monitor the status of a service
Well, the class is not completely mine.
Researching on the internet finds everything.
There are always people who have done or solved almost everything and the best that shares it.
The idea is to create a class that through an instance of ManagementEventWatcher (another great class ...) can trigger an event when there has been a change in the state of a service.
When this happens "someone" you are interested in can be notified
public class MonitorEstadoServicio : IDisposable
{
bool _disposed = false;
ManagementEventWatcher watcher = null;
string nombreServicio;
public event EventHandler<EventoCambioEstadoServicioParamArgs> eventoCambioEstadoServicio;
public MonitorEstadoServicio(string nombreServicio)
{
this.nombreServicio = nombreServicio;
var eventQuery = new EventQuery();
eventQuery.QueryString = "SELECT * FROM __InstanceModificationEvent within 2 WHERE targetinstance isa 'Win32_Service'";
watcher = new ManagementEventWatcher(eventQuery);
watcher.EventArrived += watcher_EventArrived;
}
public void iniciar()
{
watcher.Start();
}
void watcher_EventArrived(object sender, EventArrivedEventArgs e)
{
ManagementBaseObject evento = e.NewEvent;
ManagementBaseObject targetInstance = ((ManagementBaseObject)evento["targetinstance"]);
PropertyDataCollection props = targetInstance.Properties;
foreach (PropertyData prop in props)
if (string.Compare(prop.Name, "Name", true) == 0 && string.Compare((string)prop.Value, nombreServicio, true) == 0)
{
OnCambioEstadoServicio(new EventoCambioEstadoServicioParamArgs(nombreServicio));
break;
}
}
protected void OnCambioEstadoServicio(EventoCambioEstadoServicioParamArgs e)
{
EventHandler<EventoCambioEstadoServicioParamArgs> handler = eventoCambioEstadoServicio;
if (handler != null)
handler(this, e);
}
Well with this idea in mind, within the TaskTrayApplicationContextBase class we have
MonitorEstadoServicio monitorEstadoServicio = null;
protected void iniciarMonitoreoServicio()
{
monitorEstadoServicio = new MonitorEstadoServicio(nombreServicio);
monitorEstadoServicio.eventoCambioEstadoServicio += monitorEstadoServicio_eventoCambioEstadoServicio;
monitorEstadoServicio.iniciar();
}
void monitorEstadoServicio_eventoCambioEstadoServicio(object sender, EventoCambioEstadoServicioParamArgs e)
{
ponerEstadoServicio();
}
public void ponerEstadoServicio()
{
string texto = "";
if (MTCServiceInstaller.ServiceIsInstalled(nombreServicio))
switch (estadoServicio)
{
case System.ServiceProcess.ServiceControllerStatus.Running:
notifyIcon.Icon = Properties.Resources.Ejecutando;
texto = string.Format("{0} {1} {2}", nombreServicio, nucleo.productVersion, descripcionEstadoServicio);
itemModoAplicacion.Enabled = false;
break;
case System.ServiceProcess.ServiceControllerStatus.Stopped:
notifyIcon.Icon = Properties.Resources.Detenido;
texto = string.Format("{0} {1} {2}", nombreServicio, nucleo.productVersion, descripcionEstadoServicio);
itemModoAplicacion.Enabled = true;
itemModoAplicacion_iniciarDetenerYSalir.Enabled = false;
break;
default:
notifyIcon.Icon = Properties.Resources.SinConfigurar;
texto = string.Format("{0} {1} {2}", nombreServicio, nucleo.productVersion, descripcionEstadoServicio);
itemModoAplicacion.Enabled = true;
break;
}
else
{
notifyIcon.Icon = Properties.Resources.SinConfigurar;
texto = string.Format("{0} {1}", nombreServicio, Properties.Resources.servicioNoInstalado);
itemModoAplicacion.Enabled = true;
}
ponerHint(texto);
}
private void ponerHint(string texto)
{
if (texto.Length > 63)
NotifyIconFix.SetNotifyIconText(notifyIcon, texto);
else
notifyIcon.Text = texto;
}
Simple no ??
All right. The rest of the code has more things (for example starting / stopping the service by calling auxiliary classes, calling a window that allows the configuration of the service etc.)
A bit of security maybe ??
Well, we already have the well defined blocks that allow access to the configuration window and put the values we need.
Now, as I mentioned in the beginning, the idea of this solution is to place it in the clients who have our software solution. Although the application (thinking now in the service in application mode) most of the time will be running on servers where users will not access (nor sniff) is not missing the case where also run with simple computers where there is some user And you want to double click the icon and who does not say to move the values.
Neither is going to be missing the one that wants to sniff in the places "where the configuration data reside" (the .config) and want to move it.
Well as "I know the cloth" I put a simple mechanism of security and encryption of the values which allow "STOP A LITTLE" these attempts. It is more than obvious that the mechanism is not designed for people of our field but for the common users with which the solution is not infallible but mitigates these attempts.
Let's see a little
private bool guardarConfiguracion()
{
bool result = false;
try
{
string sdominios = "";
foreach (ListViewItem item in lDominios.Items)
sdominios += item.Text + "|" + (item.Checked ? "1" : "0") + ";";
nucleo.configuracionServidorDucDNS.dominios = sdominios.Length > 0 ? encriptar(sdominios.Substring(0, sdominios.Length - 1)) : "";
nucleo.configuracionServidorDucDNS.token = encriptar(editToken.Text.Trim());
.....
nucleo.configuracionServidorDucDNS.passwordAdmin = editClave.Text.Length > 0 ? Encriptacion.getMd5Hash(editClave.Text) : "";
.....
Well the idea is that there are things that we want to keep them encrypted and then retrieve them decrypt them. Example of these things would be: the list of domains, the token, etc.
On the other hand, as we mentioned before, we need to be able to parameterize if the access to the configuration window will be done with or without the need to enter a password. This password is stored encrypted but not like these other values that are encrypted and decrypted but only "one trip". For this case we use MD5 and for the other case some variant of Rijandel.
I will not go into details of these encryption issues as they outnumber me (by far) but if I can give the code I have been using (obtained from anywhere on the web) for years and it works perfectly.
To not extend the article, the encryption code will not be listed but can be seen in the attachments
So things the configuration file saved with encryption looks more or less so
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="ConfiguracionServidorDucDNS" type="MTC.Nucleo.DucDNS.ConfiguracionServidorDucDNS, MTC.Nucleo.DucDNS"/>
</configSections>
<ConfiguracionServidorDucDNS token="sE5K6EpEgUKf+r3GydB7yU8YHXr6TGCswd7P+6Dj9H8="
simple="true" verbose="true" https="true" minutosActualizacion="5"
dominios="cPajnkIRFZe7YY2ap+urQA==" modoDebug="1" adminRequierePassword="true"
passwordAdmin="b0baee9d279d34fa1dfd71aadb908c3f" servidorSMTP="+7RpbvWCB9G/VfV8YjJPPA=="
puertoSMTP="587" usuarioSMTP="ZVlO5GOmoGrLUZLxitbNUOKxSP23/bd56DsDh+Ot2+A="
claveUsuarioSMTP="sgdabeKATYdXLs8KQINkMg==" emailEnvio="ZVlO5GOmoGrLUZLxitbNUOKxSP23/bd56DsDh+Ot2+A="
SMTPSSL="false" emailsNotificaciones="ZVlO5GOmoGrLUZLxitbNUKIVSP9KIWgkOicoONVKxY3zxhPQD9A2f5K4GvbWwMpM"
mascaraNotificaciones="21" descripcionEquipo="GUILLERMO-HP" />
</configuration>
Some about notifications ??
To close already with a bit more functionality.
It is useful for me to know if something goes wrong with updates and other things.
Although the solution audits practically everything is useful to me to know if something goes wrong.
The simplest would be to send me mail (and indeed it is and we use it) but let's say, the process will update the IP address and fail because there is no internet, it is more than obvious that it will not be able to send me mail. ...
Well, to solve this, what I did is create a queue where we are gluing all the notifications and somehow later we process the items by sending mail.
public enum TipoNotificacion : byte
{
Varios=0,
OtrosErrores = 1,
CambioIP = 2,
ErrorIntentoCambioIP = 3,
PerdidaConexionInternet = 4,
ReestablecimientoConexionInternet = 5,
ActualizacionCorrectaDireccionIP = 6
}
[Serializable]
public class Notificacion
{
public TipoNotificacion tipoNotificacion { get; set; }
public DateTime fecha { get; set; }
public string asunto { get; set; }
public string detalles { get; set; }
}
public partial class NucleoDucDNS : NucleoBase, INucleo
{
ColaProcesamiento<Notificacion> colaNotificaciones = null;
public NucleoDucDNS(Dictionary<string, object> parametros): base(parametros)
{
...
colaNotificaciones = new ColaProcesamiento<Notificacion>();
colaNotificaciones.nuevaTarea += Cola_nuevaTarea;
}
...
private void Cola_nuevaTarea(object sender, EventoNuevaTareaParamArgs e)
{
var notificacion = e.param as Notificacion;
...
}
private void actualizarDominio(string dominio, string token, string protocolo, string verbose)
{
....
using (var response = (HttpWebResponse)request.GetResponse())
using (var stream = response.GetResponseStream())
using (var reader = new StreamReader(stream))
html = reader.ReadToEnd();
...
if (html.StartsWith("KO", StringComparison.OrdinalIgnoreCase))
{
colaNotificaciones.encolar(new Notificacion()
{
tipoNotificacion = TipoNotificacion.ErrorIntentoCambioIP,
fecha = DateTime.Now,
asunto = Properties.Resources.errorIntentoActualizarDominio,
detalles = html
});
}
...
}
In the above code you can see a queue of notifications generates their creation and parameterization to know what to do when a new element is put together.
Then you see a portion of the code that performs the IP update attempt. If all goes well the DucDNS returns OK and something went wrong returns KO.
In the latter case (KO) we create an object of type Notification and we glue it.
Then the method Cola_nuevaTarea - which receives as an argument an argument of the type Notification - takes this parameter and tries to send it by mail (in the code above it is marked as
// send email with notification
If the attempt to send mail is correct the item in the queue is marked as processed so that it can be removed from the queue. But (for example there is no internet) is not removed so that when it is reset it can be sent.
We also have a mechanism to filter that we notify (can be seen in the images)
To finish
Sure there is much more to do but basically it is everything you need to be able to do almost anything with any service in the world of managed code.
This framweork (with its variants) I have been using it for years and has always behaved in a stable way.
There are other alternatives simpler, more complex but well, this is the one that gave me and my clients a more than satisfactory result
History
10 de diciembre del 2016.
Version 1.0.0.0
Initial post