En la empresa en la que trabajo actualmente han surgido una serie de nuevos proyecto en la que parte de la solución pasa por la creación de Servicios Windows que ejecuten tareas de forma automática y sistemática (sin la intervención de un usuario). Debido a esto me han pedido que cree una proyecto plantilla que sirva como punto de inicio de cada proyecto especifico.

Las premisas de la plantilla eran las siguientes:

  1. Que se pueda depurar haciendo F5 en Visual Studio
  2. Que sea fácil de instalar
  3. Que tenga incorporado un logger para “trazar” la aplicación.
Aunque todas estas premisas son tareas comunes cuando desarrollas un servicio el tema de implementarlas en un servicio base se debe a la necesidad de que todos los servicios que se creen partiendo de esta "Plantilla" tengan esta funcionalidad implementada de la misma forma.

Que se pueda depurar haciendo F5 en Visual Studio

Aunque hay varias formas de depurar un servicio de Windows (tal y como se describe en MSDN) seguro que todo desarrollador agradecería que con un simple F5 se pueda entrar en modo depuración del servicio.

Para resolver este punto, y entendiendo que el código que se quiere depurar es el que se encuentra dentro del método OnStart() del servicio. Podríamos modificar el método Main que te crea la plantilla por defecto que viene con VisualStudio ( al hacer File –> New Project –> Windows Service ) para que acepte un arreglo de string como parámetro y en función del valor del parámetro crear una instancia de la clase del servicio y llamar al metodo OnStart().

using System.ServiceProcess;
using System.Threading;

namespace WindowsService1
{
    static class Program
    {
        /// 
        /// The main entry point for the application.
        /// 
        static void Main(string[] args)
        {
            if (args[0] == "D" || args[0] == "-D")
            {
                var service = new Service1();
                service.OnStart(null);

                do
                {
                    Thread.Sleep(1000);
                } while (true);
            }
            else
            {
                ServiceBase[] ServicesToRun;
                ServicesToRun = new ServiceBase[]
                                    {
                                        new Service1()
                                    };
                ServiceBase.Run(ServicesToRun);
            }
        }
    }
}

En este punto nos encontramos con un problema y es que el método OnStart() es un método protegido por lo que solo se puede llamar desde la clase en la que esta definido o en las que deriven de esta. Así que una solución podría ser pasar el punto de entrada de la aplicación (Método MAIN) a nuestra case Service1 y eliminar el archivo Program.cs. A continuación se muestra como va quedando la clase.

using System.ServiceProcess;
using System.Threading;

namespace WindowsService1
{
    public partial class Service1 : ServiceBase
    {
        public Service1()
        {
            InitializeComponent();
        }

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

        protected override void OnStop()
        {
        }

        static void Main(string[] args)
        {
            if (args[0] == "D" || args[0] == "-D")
            {
                var service = new Service1();
                service.OnStart(null);

                do
                {
                    Thread.Sleep(1000);
                } while (true);
            }
            else
            {
                ServiceBase[] ServicesToRun;
                ServicesToRun = new ServiceBase[]
                                    {
                                        new Service1()
                                    };
                ServiceBase.Run(ServicesToRun);
            }
        }
    }
}

En este punto solo nos queda pasarle este parámetro a método Main a presionar la tecla F5 estando en VisualStudio. Esto es realmente sencillo, para ello haremos click derecho –> Propiedades sobre el proyecto de Servicio. y dentro del Tab “Debug” nos vamos a la opción “Start Options” y dentro de esta buscamos la caja de texto “Command line arguments” y escribimos –D

Properties_Debug

y con esto ya podemos depurar pulsando la tecla F5 desde Visual Studio.

Que sea facil de instalar

Para instalar un servicio podemos hacerlo utilizando las herramientas habituales: Proyecto de Instalación de VisualeStudio, herramienta de instalación InstallUtil.exe, InstallShield etc. Pero en esta ocasión y siendo consecuentes con la solución anterior queremos que el servicio se instale y desinstale pasándole parámetros (-I, -U)

Para lograr esto hemos utilizado la plataforma P/Invoke y las APIs para win32.

Una herramienta de mucha utilidad siempre se trabaje con win32 y pinvoke es la wiki desarrollada por redgate (pinvoke.net).

A continuación el código de la clase que utilizamos para instalar y desinstalar el servicio.

using System;
using System.Runtime.InteropServices;

namespace WindowsService
{
    class ServiceInstaller
    {
        [DllImport("advapi32.dll")]
        public static extern IntPtr OpenSCManager(string lpMachineName,string lpScdb, int scParameter);

        [DllImport("Advapi32.dll")]
        public static extern IntPtr CreateService(IntPtr scHandle,string lpSvcName,string lpDisplayName, int dwDesiredAccess,int dwServiceType,int dwStartType,int dwErrorControl,string lpPathName, string lpLoadOrderGroup,int lpdwTagId,string lpDependencies,string lpServiceStartName,string lpPassword);

        [DllImport("advapi32.dll")]
        public static extern IntPtr CloseServiceHandle(IntPtr schandle);

        [DllImport("advapi32.dll")]
        public static extern IntPtr StartService(IntPtr svhandle, int dwNumServiceArgs, string lpServiceArgVectors);
  
        [DllImport("advapi32.dll",SetLastError=true)]
        public static extern IntPtr OpenService(IntPtr schandle,string lpSvcName,int dwNumServiceArgs);

        [DllImport("advapi32.dll")]
        public static extern IntPtr DeleteService(IntPtr svhandle);
 
        [DllImport("kernel32.dll")]
        public static extern int GetLastError();
  
        public void Install(string servicePath, string serviceName, string serviceDisplayName)
        {
            int SC_MANAGER_CREATE_SERVICE = 0x0002;
            int SERVICE_WIN32_OWN_PROCESS = 0x00000010;
            //int SERVICE_AUTO_START                = 0x00000002;
            int SERVICE_DEMAND_START = 0x00000003;
            int SERVICE_ERROR_NORMAL = 0x00000001;

            int STANDARD_RIGHTS_REQUIRED = 0xF0000;
            int SERVICE_QUERY_CONFIG = 0x0001;
            int SERVICE_CHANGE_CONFIG = 0x0002;
            int SERVICE_QUERY_STATUS = 0x0004;
            int SERVICE_ENUMERATE_DEPENDENTS = 0x0008;
            int SERVICE_START = 0x0010;
            int SERVICE_STOP = 0x0020;
            int SERVICE_PAUSE_CONTINUE = 0x0040;
            int SERVICE_INTERROGATE = 0x0080;
            int SERVICE_USER_DEFINED_CONTROL = 0x0100;

            int SERVICE_ALL_ACCESS = (STANDARD_RIGHTS_REQUIRED |
                SERVICE_QUERY_CONFIG |
                SERVICE_CHANGE_CONFIG |
                SERVICE_QUERY_STATUS |
                SERVICE_ENUMERATE_DEPENDENTS |
                SERVICE_START |
                SERVICE_STOP |
                SERVICE_PAUSE_CONTINUE |
                SERVICE_INTERROGATE |
                SERVICE_USER_DEFINED_CONTROL);

            IntPtr iptSCMHandle = OpenSCManager(null, null, SC_MANAGER_CREATE_SERVICE);

            if (iptSCMHandle == IntPtr.Zero)
            {
                throw new CustomServiceException(GetLastError());
            }

            IntPtr iptSVCHandle = CreateService(iptSCMHandle,
                                                serviceName,
                                                serviceDisplayName,
                                                SERVICE_ALL_ACCESS,
                                                SERVICE_WIN32_OWN_PROCESS,
                                                SERVICE_DEMAND_START,
                                                SERVICE_ERROR_NORMAL,
                                                servicePath,
                                                null, 0, null,
                                                null, null);

            if (iptSVCHandle == IntPtr.Zero)
            {
                throw new CustomServiceException(GetLastError());
            }

            if (CloseServiceHandle(iptSVCHandle) == IntPtr.Zero)
            {
                throw new CustomServiceException(GetLastError());
            }
        }

        public void Uninstall(string serviceName)
        {
            int GENERIC_WRITE = 0x40000000;
            int DELETE = 0x10000;
            IntPtr iptSCMHandle;
            IntPtr iptSVCHandle;

            iptSCMHandle = OpenSCManager(null, null, GENERIC_WRITE);

            if (iptSCMHandle == IntPtr.Zero)
            {
                throw new CustomServiceException(GetLastError());
            }

            iptSVCHandle = OpenService(iptSCMHandle, serviceName, DELETE);
            if (iptSVCHandle == IntPtr.Zero)
            {
                throw new CustomServiceException(GetLastError());
            }

            if (DeleteService(iptSVCHandle) == IntPtr.Zero)
            {
                throw new CustomServiceException(GetLastError());
            }

            if (CloseServiceHandle(iptSCMHandle) == IntPtr.Zero)
            {
                throw new CustomServiceException(GetLastError());
            }
        }
        
    }
}

Con esto logramos instalar y desinstalar el servicio por líneas de comando.

Command Prompt

Que tenga incorporado un logger para “trazar” la aplicación.

Debido a la amplia variedad de productos que existen para implementar trazas en una aplicación. A continuación enumero algunas de las que he utilizado y creo son las mas populares.

  1. Log4net
  2. NLog
  3. Enterprise Libraries

y debido al hecho de que este es un servicio plantilla. La implementación del sistema de trazas no debería estar acoplado al servicio. Para esto hemos creado un contrato (interface ILogger) la cual provee al servicio de una capa de abstracción de la implementación real, con el objetivo de poder cambiarla cada vez que se desee. En este caso en particular y debido al amplio uso de las Enterprise Libraries  dentro de mi empresa, la implementación concreta del logger esta basada en este producto.

Aquí dejo el código fuente completo del Servicio .


Desde que salieron las versiones “express” de Microsoft SQL Server, (a partir de la versión 2005),estas han tenido una gran aceptación debido a que una de sus principales ventajas es la utilización de forma gratuita tanto para el desarrollo como para el despliegue de aplicaciones a mediana escala.

Junto con estas versiones de SQL, Microsoft nos ofrece una herramienta (SQL Server Management Studio Express) para la administración de la base de datos. Y aunque con esta herramienta tenemos muchas de las opciones disponibles en las versiones de pago, una de las que mas se echa en falta es la capacidad de poder programar tareas (Jobs) de SQL Server, ya que el Agente de SQL Server no forma parte de la versión “Express”. Funcionalidad extremadamente útil en la programación de copias de seguridad.

Solución

Para lograr automatizar las copias de seguridad, lo primero que hay que hacer es crear un script de sql que las realice. Este puede ser tan complicado como se necesite. En mi caso un requerimiento era poder contar con una copia de seguridad de al menos una semana. A continuación les muestro el script que utilizo. Este script lo único que hace es incluir el nombre del día de la semana en la ruta del fichero de copia de seguridad, así cuando este se ejecute solo sobrescribirá el “.bak” una vez a la semana.

use master

declare @localServerPath varchar(100) -–ruta local en el servidor

set @localServerPath = 'C:\SqlServer\_BACKUP\' 

declare databasesToBackup cursor for 
select name from master..sysdatabases where name not in ('tempdb') 

open databasesToBackup 
declare 
@dbName varchar(100), 
@backupPath varchar(100) -–nombre del archivo de copia de seguridad incluida la ruta

fetch next  from databasesToBackup into @dbName 

While (@@fetch_status = 0) 
BEGIN 
   set @backupPath = @localServerPath +@dbName + '\' + CONVERT(VARCHAR,LOWER(Datename(DW, GETDATE()))) + '\' + @dbName + '.bak'
   backup database @dbName to disk = @backupPath with init 
   fetch next  from databasesToBackup into @dbName 
END 

close databasesToBackup 
deallocate databasesToBackup 

Una vez que tenemos el script que nos realiza la copia la copia de seguridad, podemos crear una tarea programada en Windows que ejecute un .bat como el que sigue.

Sqlcmd -E -S .\SQLEXPRESS -iC:\SqlServer\_BACKUP\DailyBackUp.sql

Donde DailyBackUp.sql es el fichero que contiene el script de sql antes mencionado.

Para la creación de la tarea programada abrimos el "Programador de Tareas de Windows"

Programador_de_Tareas

Le damos un nombre descriptivo

Tarea_Basica

Escogemos la periodicidad:

repeticion

y le damos el archivo .bat que queremos ejecutar.

iniciar_programa

y con esto tendríamos automatizadas las copias de seguridad a pesar de no contar con el agente de SQL Server.


Una de las tareas que me han asignado en mi nuevo trabajo consiste en la generación de las diferentes versiones del proyecto en el que trabaja el equipo de desarrollo al que pertenezco. Además de llevar todo el tema de branching y builds del servidor de TFS, al final de cada Build exitosa, hay que realizar un proceso que hasta ahora solo se hacía de forma manual, y que consistía en desplegar el resultado de la compilación en un Servidor FTP para que otro equipo (QA) lo pruebe y ponga en “Producción”.

Después de generar 3 versiones de la aplicación a mano, ya tenia noción suficiente para determinar que parte de todo el proceso podría ser automatizada. Una vez comprendido todo el proceso en lugar de crearme una aplicación con C# o VB.NET decidí utilizar “PowerShell” para crear un script que realice todas las operaciones mecánicas de copiar, renombrar y subir ficheros al FTP, al final también estoy usando .NET

Dentro de todos estos procesos, el de subir una carpeta y todos sus ficheros de forma recursiva es algo que seguramente reutilizaré en algún otro script, por lo que es esta es una buena oportunidad para crear otra entrada en este blog.

Descripción del proceso

Una de las mejoras que le han añadido a la versión 2 de Powershell, es la integración de un IDE para la creación y/o modificación de scripts. En este caso le han llamado ISE que significa (Integrated Scripting Environment) y entre otras cosas permite la depuración de scripts con “puntos de parada” (Break Points) y todo.

PowerSellISE

Entrando en materia. Lo parte fundamental de proceso consiste en ejecutar la siguiente línea de comandos.

ftp "-s:$FtpCommandFilePath" $HOSTNAME

Donde $FtpCommandFilePath no es más que la ruta a un fichero que contiene todos los comandos que queremos pasarle a la conexión con el FTP, desde el usuario y la contraseña que vamos a utilizar para conectarnos hasta el “Quit” final para desconectarnos.

Es script para la creación de este fichero y la ejecución final del comando es el siguiente.

Param (
    $LOCALFOLDER = "C:\Test"
    )
# FTP Configuration where the final structure is uploaded
$HOSTNAME = "192.168.1.10"; # FTP server's IP
$UPLOAD_DIR = "ChildFolder";    # Folder where the final structure is uploaded
$USER = "user";     # Enter a user name for FTP Server
$PASS = "password"; # Enter a password for FTP Password

if( $USER.Length -eq 0 ) { throw "Please provide a FTP server user name at Line 7"; }
if( $PASS.Length -eq 0 ) { throw "Please provide a FTP server password at Line 8"; }

function CopyFolderToFTP($sourceFolder, $uploadFiles)
{
    $name = [System.IO.Path]::GetFileName($sourceFolder);
    $result =  $result + "MKD `"" + $name + "`"`r`n"
    $result =  $result + "CD " + $name + "`r`n";
    
    if($uploadFiles)
    {
        foreach ($file in [System.IO.Directory]::GetFiles($sourceFolder))
        {
            $result = $result + "PUT `"" + $file + "`"`r`n";
        }
    }
    foreach ($folder in [System.IO.Directory]::GetDirectories($sourceFolder))
    {                    
        $result = CopyFolderToFTP $folder $uploadFiles;
    }
    $result =  $result + "CD ..`r`n";
    return $result;
}


"Uploading file to FTP server..."
$FtpCommandFilePath = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine( $pwd.ToString(), "FTPCommand.txt" ) );    
if( $UPLOAD_DIR.Length -gt 0 ) { $FtpChdirCommand = "CD " + $UPLOAD_DIR; }
    
$FtpUploadCommand = CopyFolderToFTP $LOCALFOLDER $TRUE;
    
$FtpCommands = @( $USER, $PASS, $FtpChdirCommand, "BINARY", $FtpUploadCommand, "QUIT" );
$FtpCommand = [String]::Join( "`r`n", $FtpCommands );
set-content $FtpCommandFilePath $FtpCommand
    
ftp "-s:$FtpCommandFilePath" $HOSTNAME
    
del $FtpCommandFilePath
"FTP Complete."

Para ejecutarlo solo tienes que abrir Powershell navegar hasta la carpeta donde tienes el script y ejecutarlo pasándole como parámetro la carpeta que quieres subir al FTP. En mi caso lo tengo creado en el escritorio y quiero subir la carpeta “C:\Test”

RunScript

Y con esto tendríamos. Espero que este script le sea útil a alguien más.


Una de las primeras tareas, relacionadas con la organización de un proyecto dentro del control de versiones que debe de realizar un equipo de desarrollo, es la de definir una estructura de directorios en la que los integrantes del equipo puedan encontrar fácilmente los ficheros que necesitan y que además, incentive la correcta ubicación los dichos ficheros.

Aunque la estructura de directorios puede ser distinta en dependencia del tipo de proyecto y sobre todo de las dimensiones del mismo, según mi experiencia una estructura inicial que podría aplicarse a la mayoría de los proyectos podría ser la siguiente.

directorios

Donde:

  • DB –> Contiene los scripts necesarios para generar la base de datos (si es que el proyecto necesita una)
  • Documents –> Contiene los documentos del equipo relacionados con el proyecto (Ej. Release Notes, Requirements, etc.).
  • Help –> Contiene todos los ficheros necesarios para generar la ayuda del producto (Ej. *.chm)
  • Install –> Contienes los ficheros necesarios para crear el programa de instalación de la aplicación.
  • Libs –> Contiene dlls de terceros usadas por la aplicación.
  • Src –> Directiorio raiz para los distintos proyectos que pertenecen a la solucion y que por lo tanto se deben compilar juntos.
    • ProjectName –> El proyecto inicial que provee la interface grafica (si es que la tiene) o al menos es el que tendrá el hilo principal de la aplicación.
    • ProjectNameLib –> Biblioteca de clases que contiene la mayoría del código.
    • ProjectName –> Proyecto de pruebas que contiene las pruebas unitarias (Unit Test) para la biblioteca de clases ProjectNameLib.
  • Utilis –> Varios scripts, ficheros .bat, e información necesaria para el desarrollo.

Aunque el nombre de los directorios anteriormente enumerados nos da una idea del contenido que podrían tener, una practica recomendable es la de incluir un fichero de texto (Ej. index.txt) que contenga la descripción del uso que se le pretende dar a dichas carpetas.

Si bien es verdad que crear una estructura de directorios similar a la anteriormente descrita, e incluir un fichero de texto por cada directorio explicando el contenido de los mismos no es una tarea que consuma mucho tiempo, si que es una tarea es repetitiva. Por lo que he dedicado un rato a escribir un programita que permita crear esta estructura de una manera mas fácil y casi automática.

ProjectStructure.exe

El programa es una aplicación Windows Form que abre con la estructura de directorios anteriormente descrita. En la parte superior encontraremos una caja de texto donde escribiremos el nombre que le queremos dar al proyecto.

projectStructure

En la parte izquierda tenemos un “Property Grid” donde podremos modificar el nombre o la descripción de la carpeta que tengamos seleccionada en el árbol de la derecha.

En cualquier proyecto es bastante frecuente que el nombre de este, aparezca contenido en una o mas carpetas de la solución, por lo que para poder utilizar una misma estructura de directorios en varios proyectos, los nombres de las carpetas deben cambiar cada vez que cambia el nombre del proyecto. Para hacer este cambio dinámico la aplicación incluye la variable $(ProjectName) la cual será sustituida por el nombre del proyecto. La descripción, sin embargo, y en el caso de que no esté vacía, se utilizará para la creación del fichero de texto que explica el contenido de cada carpeta.

PropertyGrid

En la parte derecha tenemos un árbol que nos brinda una vista previa de como quedará la estructura final del proyecto. Además es en este donde podremos añadir o eliminar carpetas según nos convenga.

AddDelete

Pero lo mejor es que una vez creada la estructura que pensamos nos conviene mejor, podemos establecerla como la predeterminada para utilizarla en el futuro, si es que la que yo les propongo no les vale.

SetAsDefault

Una vez configurada la estructura de directorios necesaria, solo deben hacer clic en el botón y les aparecerá un diálogo para escoger la carpeta del disco duro donde crear la estructura del proyecto.

Esto ha sido todo, espero que este pequeño programa les sea de utilidad.

Aquí les dejo el código fuente (Sources.zip) por si quieren verlo, o el ejecutable (bin.zip) por si no quieren compilar. Por cierto la aplicación esta hecha con VisualStudio 2010 RC, pero para ejecutarla con el Framework 3.5 tienen.


Existen ocasiones donde es necesitamos en un proyecto relacionar archivos de forma visual, tal cual lo hace VisualStudio con un formulario y el código autogenerado (Designer.cs) o los resources (.resx).

nested6

pero cuando añadimos nuevos archivos a la solución (en este caso Principal.cs y Principal.partial.cs), por defecto no existe una forma de especificar si un archivo depende de otro. Así que los archivos quedan separados.

nested1

La forma de lograr esta dependencia visual es editando manualmente el proyecto y modificándolo en consecuencia. Para ello procederemos a descargar el proyecto de la solución.

nested2

luego editamos el proyecto

nested3

y veremos algo como el código que sigue.

<ItemGroup>
    <Compile Include="Program.cs" />
    <Compile Include="Principal.cs" />
    <Compile Include="Principal.partial.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>

a continuación lo modificamos para especificar que queremos que el fichero Principal.partial.cs dependa de Principal.cs.

<ItemGroup>
    <Compile Include="Program.cs" />
    <Compile Include="Principal.cs" />
    <Compile Include="Principal.partial.cs">
          <DependentUpon>Principal.cs</DependentUpon>
    </Compile>
    <Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>

Ya solo tendremos que recargar el proyecto.

nested4

y habremos conseguido nuestro objetivo.

nested5


«Older Posts |