Colabora
 

Instancia única de Aplicación

Mecanismo para hacer que una aplicación sólo pueda ejecutarse una vez

 

Fecha: 16/May/2008 (13-05-08)
Autor: Federico Daniel Colombo Gennarelli - thepirat000@hotmail.com

Nota del Guille (08/Feb/09):
El código mostrado es válido para Visual Basic 9.0 o superior con LINQ 3.5 o superior, ya que se utiliza LINQ.

 


Introducción

En este artículo se describirá una forma sencilla, configurable y útil de aplicar un patrón de instancia única para impedir que una aplicación se ejecute más de una vez en una misma sesión o en un mismo PC.

 

El requerimiento

1.- Se requiere un mecanismo para evitar que se pueda correr una aplicación dos veces en la misma PC.

Este bloqueo deberá ser configurable para permitir que sea POR SESIÓN o GLOBAL.

POR SESIÓN significa que podrá haber varios usuarios logueados al mismo PC (sesiones) cada quien corriendo una única instancia de la aplicación, pero no podrá correrse más de una instancia de la aplicación en cada sesión de usuario.

GLOBAL significa que sólo podrá existir una única instancia de la aplicación en todo el PC, independientemente de los usuarios logueados. Sólo un usuario podrá tener abierta la aplicación en determinado momento. 

2.- Además es necesario que, en caso de que exista una instancia de la aplicación corriendo previamente, la ventana principal de esta instancia sea mostrada.

Nota:
Habrá casos en los que la ventana principal no podrá ser mostrada: si no existe, si está oculta, etc.

3.- Deberá funcionar correctamente en los siguientes casos:

- Cuando se renombre el archivo ejecutable
- En modo debug desde el IDE de Visual Studio
- Cuando la aplicación se cierre mediante un Kill del sistema operativo

Mecanismo de exclusión mutua (Mutex):

Para solucionar este problema se utilizará una clase que implemente un mutex de nombre único:

El mutex provee un mecanismo de señalización entre procesos que permite, entre otras cosas, determinar si otro proceso creó un mutex con anterioridad de forma atómica. Además, un mutex será cerrado (o abandonado) siempre que se cierre la aplicación.

Una forma de crear (abrir) un mutex de señalización en VB.NET es la siguiente:

New System.Threading.Mutex(initiallyOwned As Boolean, mutexName As String, createdNew As Boolean)

El primer parámetro del contructor (initiallyOwned As Boolean) indica si se requiere que el hilo llamante sea el dueño (owner) del mutex. En este caso será falso, ya que cualquier instancia del programa intentará crear el mutex, y este continuará en su estado inicial durante toda su vida.

El segundo parámetro (Name as String) es el nombre que tendrá el mutex, este nombre puede incluir el prefijo "Global\" o "Local\" según la visibilidad requerida.
La visibilidad es útil en servidores corriendo Terminal Services, en los que varios usuarios pueden estar conectados al mismo servidor en diferentes instancias o sesiones de usuario.
En este caso, si el nombre del mutex comienza con "Local\", significará que el mutex será único a nivel sesión de usuario (aunque podrán existir otros mutex con ese mismo nombre abiertos en diferentes sesiones de usuario). En otras palabras: sólo UN mutex por sesión de usuario.
En cambio, cuando se utiliza el prefijo "Global\" el mutex será único para todo el sistema.

El tercer parámetro (createdNew as Boolean) es un parámetro de salida que contendrá un valor Verdadero en caso de que el mutex haya sido creado por el constructor y devolverá False en caso de que el mutex ya existiera con anterioridad.

Por lo tanto con un mutex podría solucionarse la primera parte del problema haciendo que la aplicación abra un mutex  cuyo nombre sea el nombre del ensamblado y lo mantenga durante toda la ejecución de la aplicación.
En el caso que el mutex existiera al momento de ser creado, se sabría que se tiene otra instancia de la aplicación abierta.

Mostrar la ventana principal

Para la segunda parte es necesario un mecanismo para enfocar y mostrar la ventana principal de un proceso.
Para poder hacer esto, debemos obtener primero el "ID de la ventana" ó "Window Handle" ó "hWnd" de la ventana principal del proceso en cuestión y luego hacer unas llamadas a la API de Windows con este hWnd para que la ventana sea mostrada.

Una forma de obtener el ID de la ventana principal de un proceso a partir de su nombre es la siguiente:

Dim mainhWnd As System.IntPtr = _
   (From p In Process.GetProcessesByName("NombreProceso") _
   Where Not p.MainWindowHandle.Equals(IntPtr.Zero) _
   Select p.MainWindowHandle).FirstOrDefault

Esta instrucción devolverá el identificador de la ventana principal del primer proceso llamado exactamente "NombreProceso". Además se evitan aquellos procesos que no tengan una ventana principal (cuyo MainWindowHandle sea IntPtr.Zero).

La función Process.GetProcessesByName(NombreAmistoso As String) devuelve un arreglo de Process (System.Diagnostics.Process) con todos los procesos cuyo nombre amistoso sea el pasado como parámetro.
El "nombre amistoso" de una aplicación es el nombre del ejecutable sin la extensión .exe. Este nombre puede obtenerse fácilmente con la siguiente instrucción: Process.GetCurrentProcess().ProcessName

Una vez que se tiene el Windows Handle de la ventana principal, es necesario decirle al sistema operativo que la muestre y la enfoque. Esto se hará mediante dos llamadas a la API de Windows (user32.dll), una a la función SetForegroundWindow y otra a la función ShowWindow.
La importación de las funciones de la API necesarias se muestran a continuación:

<System.Runtime.InteropServices.DllImport("user32.dll")> _
Private Shared Function ShowWindow(ByVal hWnd As System.IntPtr, ByVal nCmdShow As Integer) As Integer
End Function

<System.Runtime.InteropServices.DllImport("user32.dll")> _
Private Shared Function SetForegroundWindow(ByVal hWnd As System.IntPtr) As Integer
End Function

Private Const SW_SHOWMAXIMIZED As Integer = 3
Private Const SW_SHOWNORMAL As Integer = 1

Ambas funciones toman como primer parámetro el Windows Handle de la ventana en cuestión. El segundo parámetro de la función ShowWindow es una constante que indica en qué estado será mostrará la ventana. Como ejemplo se incluyen dos constantes que puede tomar este parámetro: SW_SHOWMAXIMIZED para indicar que la ventana se maximizará antes de mostrarse y SW_SHOWNORMAL para indicar que la ventana se mostrará en estado normal.

El código completo de una función que muestra la ventana principal de un proceso (con el mismo nombre que el proceso actual) se transcribe a continuacion:

Private Shared Function MostrarVentanaPPalProceso() As Boolean
    Dim sProcessName As String = Process.GetCurrentProcess.ProcessName

    'Apuntador a la ventana ppal del proceso con nombre sProcessName
    Dim mainhWnd As System.IntPtr = _
            (From p In Process.GetProcessesByName(sProcessName) _
            Where Not p.MainWindowHandle.Equals(IntPtr.Zero) _
            Select p.MainWindowHandle).FirstOrDefault

    If Not mainhWnd.Equals(IntPtr.Zero) Then
        'Muestro la ventana
        SetForegroundWindow(mainhWnd)
        ShowWindow(mainhWnd, SW_SHOWNORMAL)
        Return True
    Else
        Return False
    End If
End Function

Esta función obtiene el nombre del proceso actual en la variable sProcessName (que será el nombre del ejecutable pero sin la extensión exe) y a continuación obtiene el ID de su ventana principal para mostrarla con las dos llamadas a la API de Windows SetForegroundWindow y ShowWindow.

Nota:
En algunas versiones de Windows, la función de la API ShowWindow es suficiente para mostrar y enfocar una ventana. Pero en versiones más nuevas, esta función no es suficiente para enfocarla, haciendo necesaria una llamada anterior a SetForegroundWindow con este fin.

La solución. Clase clsInstanciaPrevia.

A continuación se transcribe el código completo de la clase que implementa este patrón de "instancia única de aplicación":

Public Class clsInstanciaPrevia
    ' Mutex local para sólo permitir una instancia de la aplicación por usuario
    Private Shared _mutex As System.Threading.Mutex

    'API de Windows
    <System.Runtime.InteropServices.DllImport("user32.dll")> _
    Private Shared Function ShowWindow(ByVal hWnd As System.IntPtr, ByVal nCmdShow As Integer) As Integer
    End Function

    <System.Runtime.InteropServices.DllImport("user32.dll")> _
    Private Shared Function SetForegroundWindow(ByVal hWnd As System.IntPtr) As Integer
    End Function

    Private Const SW_SHOWMAXIMIZED As Integer = 3
    Private Const SW_SHOWNORMAL As Integer = 1

    'Enumerador para inicar el tipo de bloqueo
    Public Enum eTipo As Integer
        POR_SESION = 0
        [GLOBAL] = 1
    End Enum

    'Función que devuelve TRUE si ya existe una instancia previa del programa corriendo
    'En caso de que la aplicación estuviera corriendo, intenta darle foco a la ventana principal
    Public Shared Function InstanciaPrevia(ByVal Tipo As eTipo) As Boolean
        'Obtengo el nombre del ensamblado donde se encuentra ésta función
        Dim NombreAssembly As String = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name

        'Nombre del mutex según Tipo (visibilidad)
        Dim mutexName As String = If(Tipo = eTipo.GLOBAL, "Global\", "Local\") & NombreAssembly

        Dim newMutexCreated As Boolean
        Try
            'Abro/Creo mutex con nombre único
            _mutex = New System.Threading.Mutex(False, mutexName, newMutexCreated)

            If newMutexCreated Then
                'Se creó el mutex, NO existe instancia previa
                Return False
            Else
                'El mutex ya existía, Libero el mutex 
                _mutex.Close()
                'Intento otorgar el foco al programa ya abierto anteriormente
                If Not MostrarVentanaPPalProceso() Then
                    'No se encontró la ventana principal
                    MsgBox("Ya existe una instancia previa del programa corriendo.")
                End If
                Return True
            End If
        Catch ex As Exception
            MsgBox(ex.Message)
            Return False
        End Try

    End Function

    'Intenta mostrar la ventana principal de los procesos con mi mismo nombre
    Private Shared Function MostrarVentanaPPalProceso() As Boolean
        Dim sProcessName As String = Process.GetCurrentProcess.ProcessName

        'Apuntador a la ventana ppal del proceso con nombre sProcessName
        Dim mainhWnd As System.IntPtr = _
                (From p In Process.GetProcessesByName(sProcessName) _
                Where Not p.MainWindowHandle.Equals(IntPtr.Zero) _
                Select p.MainWindowHandle).FirstOrDefault

        If Not mainhWnd.Equals(IntPtr.Zero) Then
            'Muestro la ventana
            SetForegroundWindow(mainhWnd)
            ShowWindow(mainhWnd, SW_SHOWNORMAL)
            Return True
        Else
            Return False
        End If
    End Function
End Class

La variable privada _mutex será el mutex que deberá mantenerse abierto hasta el cierre de la aplicación.
La función principal se llama InstanciaPrevia, ésta intenta abrir el mutex determinando si ya estaba abierto previamente y si es así, llama a la función que muestra la ventana principal de la instancia previa.ciaPrevia, ésta intenta abrir el mutex determinando si ya estaba abierto previamente y si es así, llama a la función que muestra la ventana principal de la instancia previa.

Tanto la variable _mutex como la función InstanciaPrevia son Shared dentro de la clase para que sean únicas dentro de la clase, independientemente de las instancias de ésta. A este tipo de funciones/propiedades se las accede únicamente mediante NombreDeLaClase.NombreMetodo y no está permitido accederlas a través de instancias de la clase. 

El punto de acceso a esta clase es la función InstanciaPrevia.
Esta función obtiene el nombre del ensablado actual y a continuación determina el nombre del Mutex (mutexName), que será "Global\NombreEnsamblado" o bien "Local\NombreEnsamblado" dependiendo del parámetro Tipo que indica el tipo de bloqueo a utilizar (global o por usuario)

Luego (dentro de un bloque try) se intenta abrir un nuevo mutex mediante el constructor de la clase System.Threading.Mutex que permite en la misma llamada del constructor determinar si el mutex ya existía previamente o si se está creando en este momento (parámetro de salida newMutexCreated).
Si se determina que el mutex ya había sido creado con anterioridad, se asume que existe una instancia previa de la aplicación corriéndose, y en ese caso (else), se cierra el mutex y se intenta mostrar la ventana principal de la instancia previa llamando a la función MostrarVentanaPPalProceso.
El cierre del mutex quizás parezca innecesario, pero existe el caso que la aplicación esté procesando el MessageBox antes de salir y en este caso no se requiere el mutex ocupado, pues la aplicación ya no será "válida" y se cerrará inmediatamente.

La función InstanciaPrevia devolverá Verdadero en caso de que exista una instancia previa de la aplicación corriendo, en cuyo caso, será responsabilidad del llamante cerrar la aplicación.

Utilizando la clase clsInstanciaPrevia

A continuación se transcribe el código necesario para utilizar la clase clsInstanciaPrevia dentro de una aplicación Windows Form con un Formulario como objeto de inicio :

Private Sub Form1_Load() Handles MyBase.Load
   If clsInstanciaPrevia.InstanciaPrevia(clsInstanciaPrevia.eTipo.POR_SESION) Then
        Me.Close()
   End If
End Sub

En este caso se cierra el formulario si existe una instancia previa de la aplicación.

 

Para utilizar la clase clsInstanciaPrevia dentro de una aplicación Windows Form con un Sub Main como objeto de inicio:

Otra caso podría ser cuando la aplicación comienza en un Sub Main y la clase clsInstanciaPrevia se encuentra en otro ensamblado (por ejemplo ClassLibrary1). En ese caso, el código es el siguiente:

Imports System.Windows.Forms

Module Principal
    Public Sub main()
        If Not clsInstanciaPrevia.InstanciaPrevia(clsInstanciaPrevia.eTipo.POR_SESION) Then
            Application.Run(Form1)
        End If
    End Sub
End Module

En este caso sólamente se corre la aplicación cuando la función InstanciaPrevia devuelve Falso.

En el código de ejemplo se incluye la clase clsInstanciaPrevia y un ejemplo de uso.

 

 

Espero que les sea útil.
Federico Daniel Colombo Gennarelli


Espacios de nombres usados en el código de este artículo:

System.Windows.Forms
System.Collections.Generic
System.Diagnostics

 



Compromiso del autor del artículo con el sitio del Guille:

Lo comentado en este artículo está probado (y funciona) con la siguiente configuración:

El autor se compromete personalmente de que lo expuesto en este artículo es cierto y lo ha comprobado usando la configuración indicada anteriormente.

En cualquier caso, el Guille no se responsabiliza del contenido de este artículo.

Si encuentras alguna errata o fallo en algún link (enlace), por favor comunícalo usando este link:

Gracias.


Código de ejemplo (comprimido):

 

Fichero con el código de ejemplo: thepirat_Instancia_Unica_Aplicacion.zip - 13 KB

(MD5 checksum: 8d1c308f2984c4ae18b1688460853097)

 


Ir al índice principal de el Guille