Ejecutar tareas elevadas durante el ciclo de vida del Rol de Azure

Cuando desarrollamos para Windows Azure podemos encontrarnos con distintos escenarios que van desde aplicaciones completamente .NET y aplicaciones que son migraciones de aplicaciones existentes. En ese sentido uno de los dolores de cabeza a la hora de trabajar con Azure son los registros de componentes COM durante el arranque del rol de Azure. Este tipo de problema se soluciona normalmente creando una tarea en el startup del rol que desea consumir ese tipo de componentes COM.

Si por ejemplo nosotros durante el ciclo de ejecución de nuestro rol queremos ejecutar un proceso con elevación, es decir con permisos completos de administrador no podemos hacerlo porque el proceso que hostea la web y el worker role no está elevado, y aunque nosotros lo indiquemos a la hora de ejecutar el proceso eso no va a funcionar.

Es por eso que podemos hacer un pequeño truco para que podamos ejecutar proceso elevados durante nuestro ciclo de ejecución del rol, es decir en cualquier momento, así podemos registrar componentes COM, o llamar a ejecutables del SO de manera mucho más cómoda. Para poder llegar a esa aproximación tenemos que buscar un entorno donde podamos ejecutar nuestras aplicaciones de manera elevada, y ese entorno es el entorno de startup del rol, así que de alguna manera lo que tenemos que tener es un proceso sentinel que se arranque en el startup del rol y que acepte peticiones para ejecutar procesos de manera elevada.

Pues justamente eso es lo que vamos a hacer, utilizando WCF para abrir un pipe de comunicación entre los procesos vamos a crear un servicio que escuche peticiones de otro proceso a través de un pipe para enviar un mensaje que representa una invocación de un proceso.

Vamos por pasos:

Definición del servicio

Como lo que queremos hacer es exponer un servicio de WCF a través de pipes de Windows, tenemos que definir la interfaz del contrato de operaciones:

[ServiceContract(Namespace = "http://azure.plainconcepts.com/schemas/04/2011/azure/executionhost")]
public interface IExecutionHost
{
    [OperationContract]
    void ExecuteTask(ProcessTask host);
}

Una vez que tenemos definido el contrato servicio tenemos que hacer dos cosas, primero hacer la implementación del servicio, es decir el proceso sentinel que escuchará las peticiones recibidas y hará el trabajo de ejecutar esos procesos.

[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
public class ExecutionHostService : IExecutionHost
{
    public void ExecuteTask(ProcessTask host)
    {
        Process process = new Process();
        process.StartInfo = host.StartInfo;
        process.Start();
    }
}

Otra cosa que tenemos que hacer en el proceso sentinel es hostear el servicio y ponerlo a escuchar peticiones a través del binding que nosotros seleccionemos, en este caso NetNamedPipeBinding:

public class ExecutionHostServiceManager
{
    public ExecutionHostServiceManager()
    {
        service = new ExecutionHostService();
        ServiceHost host = new ServiceHost(service);

        string address = "net.pipe://PlainConcepts/Azure/ExecutionHost";
        NetNamedPipeBinding binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None);
        host.AddServiceEndpoint(typeof(IExecutionHost), binding, address);

        // Add a mex endpoint
        long maxBufferPoolSize = binding.MaxBufferPoolSize;

        int maxBufferSize = binding.MaxBufferSize;

        int maxConnections = binding.MaxConnections;

        long maxReceivedMessageSize =
            binding.MaxReceivedMessageSize;

        NetNamedPipeSecurity security = binding.Security;

        string scheme = binding.Scheme;

        XmlDictionaryReaderQuotas readerQuotas =
            binding.ReaderQuotas;

        BindingElementCollection bCollection = binding.CreateBindingElements();

        HostNameComparisonMode hostNameComparisonMode =
            binding.HostNameComparisonMode;

        bool TransactionFlow = binding.TransactionFlow;

        TransactionProtocol transactionProtocol =
            binding.TransactionProtocol;

        EnvelopeVersion envelopeVersion =
            binding.EnvelopeVersion;

        TransferMode transferMode =
            binding.TransferMode;
        host.Open();
    }


    private ExecutionHostService service;
}

Todo ello lo tenemos que poner en un pequeño programa de consola que será el proceso en sí que hosteará el pipe de Windows que aceptará peticiones a través de WCF:

class Program
{
    static void Main(string[] args)
    {
        new ExecutionHostServiceManager();
        Thread.Sleep(Timeout.Infinite);
    }
}

Fijaros que al final de la ejecución de la clase hay un Thread.Sleep(Timeout.Infinite) que nos permite esperar eternamente en el proceso para que así el proceso esté disponible durante todo el ciclo de vida del rol, permitiéndonos ejecutar un proceso elevado en cualquier momento.

Haciendo llamadas al servicio

Como bien es sabido para poder hacer llamadas a un servicio de WCF lo primero que tenemos que hacer es generar un proxy en el cliente para hacer esas llamadas. Como queremos hacerlo todo por código para simplificar, lo que vamos a hacer es una clase que herede de ClientBase<T> siendo T la interfaz del contrato de operaciones de nuestro servicio.

public class ExecutionHostClient : ClientBase<IExecutionHost>
{
    static ExecutionHostClient()
    {
        string address = "net.pipe://PlainConcepts/Azure/ExecutionHost";
        NetNamedPipeBinding binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None);
        binding.CloseTimeout = TimeSpan.MaxValue;
        binding.ReceiveTimeout = TimeSpan.MaxValue;
        binding.SendTimeout = TimeSpan.MaxValue;
        EndpointAddress endpoint = new EndpointAddress(address);
        client = new ExecutionHostClient(binding, endpoint);
    }

    public ExecutionHostClient(Binding binding, EndpointAddress remoteAddress) :
        base(binding, remoteAddress)
    {
    }

    public void ExecuteTask(ProcessTask task)
    {
        Channel.ExecuteTask(task);
    }

    public static void ExecuteRemoteTask(ProcessTask task)
    {
        client.ExecuteTask(task);
    }

    private static ExecutionHostClient client;
}

Es importante que el proxy se inicialice con el mismo binding que el de servidor para que las invocaciones funcionen. En este ejemplo para simplificar tenemos una referencia estatica del proxy y solamente lo exponemos a través de un método estático.

Invocando servicios

Para el ejemplo actual podemos registrar los componentes COM de una carpeta que tengamos en nuestro worker role:

public class RegisterComHelper
    {
        public RegisterComHelper()
        {
 
        }
 
        public void Register()
        {
            // hay que buscar la localizacion en el servidor de azure de donde estan los ensamblados
            // como no sabemos donde estan los ficheros tenemos que buscar el modulo 
            // Habitania.RegisterCom.dll que es especifico para este ejemplo
            // así nos aseguramos que estamos buscando la dll correcta
            Process current = Process.GetCurrentProcess();
            var found = (from p in current.Modules.Cast<ProcessModule>().ToList()
                         where p.ModuleName == "PlainConcepts.Azure.WorkerRoleDemo.dll"
                         select p).FirstOrDefault();
 
            if (found != null)
            {
                // a partir de la locacion del modulo cargada por el proceso 
                // somos capaces de encontrar la informacion del directorio y buscar
                // la carpeta dlls que contiene la lista de dlls que queremos registar
                string directoryLocation = Path.GetDirectoryName(found.FileName);
 
                string dllPath = Path.Combine(directoryLocation, "V3COM30");
 
                string[] files = Directory.GetFiles(dllPath);
 
                foreach (var item in files)
                {
                    if (item.EndsWith(".dll"))
                        RegisterComObject(item);
                }
 
                dllPath = Path.Combine(directoryLocation, "V3COM");
 
                files = Directory.GetFiles(dllPath);
 
                foreach (var item in files)
                {
                    if (item.EndsWith(".dll"))
                        RegisterComObject(item);
                }
            }
        }
 
        private void RegisterComObject(string filePath)
        {
            ProcessStartInfo info = new ProcessStartInfo();
            info.FileName = Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.System),
                "regsvr32.exe");
            info.Arguments = string.Format("/i {0}", filePath);
            info.UseShellExecute = false;
 
 
            ExecutionHostClient.ExecuteRemoteTask(new ProcessTask()
            {
                StartInfo = info
            });
        }
    }

Este método para trabajar con Windows Azure puede ser un poco complicado de montar, pero una vez hecho tenemos un mecanismo muy sencillo para hacer cosas más complicadas como por ejemplo ejecutar otro tipo de tareas de mantenimiento directamente desde ahí.

El codigo completo del ejemplo aquí.

Luis Guerrero.

2 comentarios en “Ejecutar tareas elevadas durante el ciclo de vida del Rol de Azure”

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *